@palbase/backend 25.1.0 → 27.0.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.
Files changed (104) hide show
  1. package/dist/bin/palbase-backend.cjs +2401 -1039
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +87 -51
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/chunk-CGNN2PUH.js +213 -0
  6. package/dist/chunk-CGNN2PUH.js.map +1 -0
  7. package/dist/chunk-DRZFQRJI.js +370 -0
  8. package/dist/chunk-DRZFQRJI.js.map +1 -0
  9. package/dist/chunk-GYK6QYS4.js +708 -0
  10. package/dist/chunk-GYK6QYS4.js.map +1 -0
  11. package/dist/chunk-I3C4PFIW.js +930 -0
  12. package/dist/chunk-I3C4PFIW.js.map +1 -0
  13. package/dist/{chunk-VDF2T4AS.js → chunk-OO7R25AI.js} +1213 -591
  14. package/dist/chunk-OO7R25AI.js.map +1 -0
  15. package/dist/chunk-TS4U7NBD.js +885 -0
  16. package/dist/chunk-TS4U7NBD.js.map +1 -0
  17. package/dist/{chunk-YOY5DFQS.js → chunk-TWX6JTGJ.js} +76 -34
  18. package/dist/{chunk-YOY5DFQS.js.map → chunk-TWX6JTGJ.js.map} +1 -1
  19. package/dist/{chunk-35PNTIRN.js → chunk-VVMJEVQP.js} +63 -162
  20. package/dist/chunk-VVMJEVQP.js.map +1 -0
  21. package/dist/{chunk-7D4SUZUM.js → chunk-VXPNPVAG.js} +3 -1
  22. package/dist/db/env.cjs.map +1 -1
  23. package/dist/db/env.d.cts +2 -2
  24. package/dist/db/env.d.ts +2 -2
  25. package/dist/db/index.cjs +754 -344
  26. package/dist/db/index.cjs.map +1 -1
  27. package/dist/db/index.d.cts +2 -2
  28. package/dist/db/index.d.ts +2 -2
  29. package/dist/db/index.js +7 -4
  30. package/dist/engine/index.cjs +2339 -1006
  31. package/dist/engine/index.cjs.map +1 -1
  32. package/dist/engine/index.d.cts +6 -6
  33. package/dist/engine/index.d.ts +6 -6
  34. package/dist/engine/index.js +7 -6
  35. package/dist/index-BrvvxSpn.d.ts +4844 -0
  36. package/dist/index-Bve7BBTL.d.cts +4844 -0
  37. package/dist/{index-CUomTA3e.d.ts → index-NuzRCuxe.d.ts} +171 -296
  38. package/dist/{index-ClpDeSos.d.cts → index-VtToZmUm.d.cts} +171 -296
  39. package/dist/index.cjs +2694 -1169
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +165 -19
  42. package/dist/index.d.ts +165 -19
  43. package/dist/index.js +738 -477
  44. package/dist/index.js.map +1 -1
  45. package/dist/module-Dl1KFVtc.d.cts +54 -0
  46. package/dist/module-Dl1KFVtc.d.ts +54 -0
  47. package/dist/openapi/index.cjs +1330 -484
  48. package/dist/openapi/index.cjs.map +1 -1
  49. package/dist/openapi/index.d.cts +4 -2
  50. package/dist/openapi/index.d.ts +4 -2
  51. package/dist/openapi/index.js +1264 -474
  52. package/dist/openapi/index.js.map +1 -1
  53. package/dist/{registry-dZZ5JKYg.d.ts → registry-B0eyOF9x.d.ts} +1 -1
  54. package/dist/{registry-CC0WBQq6.d.cts → registry-Bk9_rbNd.d.cts} +1 -1
  55. package/dist/stack.cjs.map +1 -1
  56. package/dist/test/index.cjs +705 -141
  57. package/dist/test/index.cjs.map +1 -1
  58. package/dist/test/index.d.cts +30 -4
  59. package/dist/test/index.d.ts +30 -4
  60. package/dist/test/index.js +472 -124
  61. package/dist/test/index.js.map +1 -1
  62. package/docs/README.md +33 -18
  63. package/docs/auth.md +1 -1
  64. package/docs/background.md +2 -2
  65. package/docs/database.md +221 -50
  66. package/docs/endpoints.md +3 -4
  67. package/docs/events.md +3 -3
  68. package/docs/getting-started.md +1 -1
  69. package/docs/llms-full.txt +401 -117
  70. package/docs/migrations.md +2 -2
  71. package/docs/schema.md +19 -10
  72. package/docs/services.md +116 -26
  73. package/package.json +8 -4
  74. package/stager/generics.js +205 -0
  75. package/stager/stage.js +39 -3
  76. package/template/AGENTS.md +110 -72
  77. package/template/db/public.ts +1 -1
  78. package/template/{controllers → modules/health}/health.controller.ts +1 -1
  79. package/template/modules/health/health.module.ts +24 -0
  80. package/template/modules/notes/note.service.test.ts +49 -0
  81. package/template/modules/notes/note.service.ts +108 -0
  82. package/template/{controllers → modules/notes}/notes.controller.ts +17 -11
  83. package/template/modules/notes/notes.module.ts +37 -0
  84. package/template/package.json +5 -3
  85. package/template/scripts/test.sh +33 -0
  86. package/template/tsconfig.json +29 -30
  87. package/dist/chunk-35PNTIRN.js.map +0 -1
  88. package/dist/chunk-CJSKYY76.js +0 -627
  89. package/dist/chunk-CJSKYY76.js.map +0 -1
  90. package/dist/chunk-CRQKCRGF.js +0 -276
  91. package/dist/chunk-CRQKCRGF.js.map +0 -1
  92. package/dist/chunk-G4R6BTLV.js +0 -662
  93. package/dist/chunk-G4R6BTLV.js.map +0 -1
  94. package/dist/chunk-VDF2T4AS.js.map +0 -1
  95. package/dist/chunk-XABBC7JP.js +0 -55
  96. package/dist/chunk-XABBC7JP.js.map +0 -1
  97. package/dist/endpoint-CTEHhb7A.d.ts +0 -2386
  98. package/dist/endpoint-DYHMo6cC.d.cts +0 -2386
  99. package/dist/index-CW21M9Z3.d.ts +0 -1222
  100. package/dist/index-CmBK76nx.d.cts +0 -1222
  101. package/template/services/note.service.test.ts +0 -45
  102. package/template/services/note.service.ts +0 -76
  103. /package/dist/{chunk-7D4SUZUM.js.map → chunk-VXPNPVAG.js.map} +0 -0
  104. /package/template/{models/notes → modules/notes/dto}/create.ts +0 -0
@@ -0,0 +1,4844 @@
1
+ import { Tables, TableTypes, Schemas } from './db/env.cjs';
2
+ import { ZodSchema, z } from 'zod';
3
+ import { PalbaseFlagKey, Buckets, BucketTypes, PalbaseSecretName } from './stack.cjs';
4
+ import { AsyncLocalStorage } from 'node:async_hooks';
5
+
6
+ /** Supported HTTP methods for endpoints. */
7
+ type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
8
+ /**
9
+ * Verified device claim, minted at the platform attestation boundary (App
10
+ * Attest / Play Integrity). Server-owned and immutable: it is populated ONLY
11
+ * from a successful, request-bound verification — never from a raw client
12
+ * header or from user/JWT metadata.
13
+ */
14
+ interface VerifiedDevice {
15
+ /** Server-minted `auth.installations` anchor (`ins_…`); may be "" until the installation binding is wired at the verify boundary. */
16
+ installationId: string;
17
+ /** Palauth device record (`dev_…`) proven by the App Attest assertion. Absent on the Android integrity path. */
18
+ authDeviceId?: string;
19
+ platform: "ios" | "android" | "web";
20
+ /** Proven verification tier — never a string-matched or header-asserted value. */
21
+ assurance: "attested" | "app_integrity" | "registered";
22
+ }
23
+ /** Authenticated user attached to the request context. */
24
+ interface User {
25
+ id: string;
26
+ /** User's email, if they signed up with one (absent for phone-only users). */
27
+ email?: string;
28
+ /**
29
+ * Whether that email address has been confirmed.
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.
34
+ *
35
+ * `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.
39
+ */
40
+ emailVerified: boolean;
41
+ role: string;
42
+ metadata: Record<string, unknown>;
43
+ /**
44
+ * Reserved, server-owned verified device claim. `null` on an authenticated
45
+ * request with no verified device. The context assembler is the sole writer;
46
+ * user/JWT metadata can never populate it.
47
+ */
48
+ device: VerifiedDevice | null;
49
+ }
50
+ /** Authentication configuration for an endpoint. */
51
+ interface AuthConfig {
52
+ /** Whether authentication is required. Defaults to true. */
53
+ required: boolean;
54
+ /** Required role for access. If undefined, any authenticated user is allowed.
55
+ *
56
+ * Matched against the caller's `metadata.role` — NOT `user.role`, which is the
57
+ * database role RLS reads and is always "authenticated" for a signed-in user.
58
+ * Not signed in → 401; signed in with a different or missing role → 403. */
59
+ role?: string;
60
+ /** Require a confirmed email address. An unverified caller gets 403
61
+ * `email_not_verified`. Fences a whole controller; for a partial rule read
62
+ * `user.emailVerified` in the handler instead. */
63
+ verifiedEmail?: boolean;
64
+ }
65
+
66
+ /** Middleware context — subset of EndpointContext without input (not yet validated). */
67
+ interface MiddlewareContext extends PalbaseModuleClients {
68
+ params: Record<string, string>;
69
+ query: Record<string, string>;
70
+ headers: Record<string, string>;
71
+ user: User | null;
72
+ db: DBClient;
73
+ env: Record<string, string>;
74
+ log: Logger;
75
+ cache: CacheClient;
76
+ requestId: string;
77
+ environmentId: string;
78
+ }
79
+ /** Middleware function signature — receives context and next function. */
80
+ type MiddlewareHandler = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void>;
81
+ /**
82
+ * REMOVED IN BEHAVIOUR, KEPT IN NAME.
83
+ *
84
+ * There is no middleware pipeline in this runtime. No bundler reads a
85
+ * `middleware/` directory, the engine never calls a handler defined here, and
86
+ * measured on 2026-08-31 this function had no caller anywhere in the runtime or
87
+ * the CLI. It returned its argument unchanged, so code written against it
88
+ * compiled, deployed, and then never ran — with nothing reporting that.
89
+ *
90
+ * A silent shell is the worst version of a retired feature: it lets a user (or a
91
+ * coding assistant, which is how this surfaced) ship a request logger, an auth
92
+ * check or a rate limiter that simply does not exist in production. So the call
93
+ * refuses, and says where the work belongs.
94
+ *
95
+ * The SYMBOL survives because removing a published export costs a major and
96
+ * 25.0.1 had just shipped. Deleting it is a proposal for the next one; the types
97
+ * below stay either way, so a file that only annotates with them still compiles.
98
+ *
99
+ * This is the shape the SDK already uses for a retired surface: `@Query(schema)`
100
+ * on a parameter throws at decoration time with a message naming its
101
+ * replacement.
102
+ */
103
+ declare function defineMiddleware(_fn: MiddlewareHandler): never;
104
+
105
+ /** On delete action for foreign key references. */
106
+ type OnDeleteAction = 'cascade' | 'set null' | 'restrict' | 'no action';
107
+ /**
108
+ * The ON DELETE actions permitted on a foreign key to the built-in auth users
109
+ * (`auth.users`). Both let a user's rows be removed (`cascade`) or detached
110
+ * (`set null`) when the account is erased; `restrict` / `no action` would BLOCK
111
+ * erasure and are therefore excluded. This is the CLIENT-SIDE mirror of the
112
+ * server's auth-FK deletion policy — the server (validateAuthUserFK) is the real
113
+ * boundary, this narrows the type so the common mistake is caught at compile time.
114
+ */
115
+ type AuthUserOnDelete = Extract<OnDeleteAction, 'cascade' | 'set null'>;
116
+ /** Column type identifiers. */
117
+ type ColumnType = 'uuid' | 'text' | 'integer' | 'bigint' | 'numeric' | 'boolean' | 'timestamp' | 'jsonb' | 'enum' | 'vector';
118
+ /** Base column definition shared by all column types. */
119
+ interface ColumnDef {
120
+ type: ColumnType;
121
+ nullable: boolean;
122
+ primaryKey: boolean;
123
+ defaultValue?: unknown;
124
+ defaultRandom?: boolean;
125
+ defaultNow?: boolean;
126
+ references?: {
127
+ table: string;
128
+ column: string;
129
+ };
130
+ /** Pending FK target, resolved by `defineSchema` once every binding exists. */
131
+ referencesThunk?: () => ColumnBuilder;
132
+ /** FK onto THIS table — no thunk needed, the target is the declaring table. */
133
+ selfRefColumn?: string;
134
+ /** This column OWNS the row: erasure, RLS and the `owner` relation read it. */
135
+ owns?: true;
136
+ /** Explicit name for the FORWARD relation (child → parent), when the one
137
+ * derived from the column would be ambiguous or unclear. */
138
+ refAs?: string;
139
+ /**
140
+ * Explicit name for the REVERSE relation (parent → children).
141
+ *
142
+ * Separate from {@link refAs} because the two directions are different facts:
143
+ * the forward name describes the parent this row points at (`author`), the
144
+ * reverse one describes the rows hanging off the parent (`posts`). One option
145
+ * naming both made an ordinary schema undeclarable — `posts.author_id` and
146
+ * `comments.author_id` both named `{ as: "author" }` collided on `users`, and
147
+ * the refusal asked for the `{ as }` they had both already written.
148
+ */
149
+ reverseAs?: string;
150
+ /** The table this column belongs to; set by `defineTable`. */
151
+ ownerTable?: {
152
+ name: string;
153
+ columns: Record<string, ColumnBuilder>;
154
+ };
155
+ /**
156
+ * The name this column used to have. A diff cannot tell a rename from a drop and
157
+ * an add — both leave one name gone and another present — so the intent has to be
158
+ * declared. Without it, renaming a column loses its data.
159
+ */
160
+ renamedFrom?: string;
161
+ /**
162
+ * This release's PROMISE that it does not reference this column — neither
163
+ * reads it nor writes it, and never names it in a filter, a sort or a SET.
164
+ *
165
+ * The contraction gate reads it and nothing else does: dropping a column is
166
+ * refused while the RUNNING release's declaration still lacks this mark, so
167
+ * removing a column is two deploys — mark it, ship, then drop it.
168
+ *
169
+ * The word is `ignored` and not `deprecated` deliberately: RFC 9745 defines
170
+ * deprecation as changing NO behaviour, and this changes what a deploy will
171
+ * accept. Rails calls the same thing `ignored_columns`.
172
+ */
173
+ ignored?: boolean;
174
+ onDeleteAction?: OnDeleteAction;
175
+ /** FR-044: yalnız AÇIKÇA `false` bildirilince var. Türev FK index'ini kapatır. */
176
+ index?: boolean;
177
+ /** FR-049: kolon `increment()` ile güncelleniyor. Plan HOT çakışmasını uyarır. */
178
+ counter?: boolean;
179
+ enumName?: string;
180
+ enumValues?: string[];
181
+ unique?: boolean;
182
+ /**
183
+ * The value is written by the DATABASE — a trigger, a rule, an identity — not by
184
+ * the author and not by a DEFAULT this schema declares. It makes the column
185
+ * optional on INSERT without putting a DEFAULT in the DDL.
186
+ *
187
+ * Before this existed the only way to keep a trigger-filled column off the
188
+ * INSERT type was to give it a fake `default()`: a value the schema claimed to
189
+ * write and the trigger immediately overwrote. That made the schema lie about
190
+ * its own data.
191
+ */
192
+ dbAssigned?: boolean;
193
+ /** vector(n): the declared dimension count — part of the TYPE (typmod), read
194
+ * by the wire serializer and the deploy's auto-index (FR-001). */
195
+ dimensions?: number;
196
+ /**
197
+ * How the stored value is projected in and out of this process (FR-009).
198
+ *
199
+ * NOT part of the DDL: the column's Postgres type is unchanged and this pair
200
+ * is never serialized into a migration. It exists so the row surface can hand
201
+ * back the type the application actually works with.
202
+ */
203
+ transform?: ColumnTransform;
204
+ }
205
+ /**
206
+ * The read/write pair a column may declare (FR-009).
207
+ *
208
+ * `fromDb` takes whatever the driver produced for this column and returns the
209
+ * value the application sees; `toDb` is its inverse on the way out. Kept
210
+ * deliberately unexported — a column declares one inline, nobody needs to name
211
+ * the shape.
212
+ */
213
+ interface ColumnTransform<T = unknown> {
214
+ fromDb: (value: unknown) => T;
215
+ toDb: (value: T) => unknown;
216
+ }
217
+ declare const __colKind: unique symbol;
218
+ declare const __colNullable: unique symbol;
219
+ declare const __colHasDefault: unique symbol;
220
+ declare const __colEnumValues: unique symbol;
221
+ declare const __colPayload: unique symbol;
222
+ declare const __colTransform: unique symbol;
223
+ /**
224
+ * Fluent column builder with phantom type params:
225
+ * K — ColumnType literal (e.g. "text", "integer")
226
+ * N — boolean: true when nullable() has been called last (false = NOT NULL)
227
+ * D — boolean: true when a default has been set
228
+ * E — enum value union (never for non-enum columns)
229
+ * P — jsonb payload shape (unknown unless jsonb<T>() supplied one)
230
+ * T — transform target type (`never` when the column declares no transform;
231
+ * `never` is the sentinel because it is the only type that survives
232
+ * `[T] extends [never]` and never collides with a real target type)
233
+ *
234
+ * All six params have defaults so bare `ColumnBuilder` (no args) still
235
+ * satisfies `Record<string, ColumnBuilder>` in schema.ts without modification.
236
+ *
237
+ * The six `declare readonly` brand fields carry the phantom types into the
238
+ * structural shape so that conditional types like ColValue<C> can discriminate
239
+ * on K without requiring runtime values on those fields.
240
+ */
241
+ declare class ColumnBuilder<K extends ColumnType = ColumnType, N extends boolean = boolean, D extends boolean = boolean, E = unknown, P = unknown, T = unknown> {
242
+ readonly [__colKind]: K;
243
+ readonly [__colNullable]: N;
244
+ readonly [__colHasDefault]: D;
245
+ readonly [__colEnumValues]: E;
246
+ readonly [__colPayload]: P;
247
+ readonly [__colTransform]: T;
248
+ readonly _def: ColumnDef;
249
+ constructor(type: K, existingDef?: ColumnDef);
250
+ /** Mark this column as the primary key. */
251
+ primaryKey(): ColumnBuilder<K, N, D, E, P, T>;
252
+ /** Mark this column as NOT NULL (default). */
253
+ notNull(): ColumnBuilder<K, false, D, E, P, T>;
254
+ /** Allow NULL values. */
255
+ nullable(): ColumnBuilder<K, true, D, E, P, T>;
256
+ /** Set a default value. */
257
+ default(value: unknown): ColumnBuilder<K, N, true, E, P, T>;
258
+ /** UUID: generate a random default (gen_random_uuid()). */
259
+ defaultRandom(): ColumnBuilder<K, N, true, E, P, T>;
260
+ /** Timestamp: default to now(). */
261
+ defaultNow(): ColumnBuilder<K, N, true, E, P, T>;
262
+ /**
263
+ * The DATABASE assigns this column's value — a trigger, a rule, an identity.
264
+ *
265
+ * The column becomes optional on INSERT (the author has nothing to send) while
266
+ * the DDL stays free of a DEFAULT this schema would not honour. It is NOT
267
+ * `default()`: that declares a value the schema promises to write.
268
+ *
269
+ * Naming: deliberately not `generated()`. Postgres has GENERATED columns and
270
+ * they are a different thing; borrowing the word would send a reader — or a
271
+ * model writing a schema — to the wrong feature.
272
+ */
273
+ dbAssigned(): ColumnBuilder<K, N, true, E, P, T>;
274
+ /** Add a foreign key reference. */
275
+ /**
276
+ * Declares that this column used to be called `previous`.
277
+ *
278
+ * A schema diff sees one name gone and another present; it cannot know whether
279
+ * you renamed a column or dropped one and added another, and the two are very
280
+ * different — the second loses every value. Saying so here turns the plan into
281
+ * `ALTER TABLE … RENAME COLUMN` instead.
282
+ *
283
+ * Once the rename has been applied the annotation is inert (the old name is no
284
+ * longer there to rename), so it can be deleted at your leisure.
285
+ */
286
+ renamedFrom(previous: string): ColumnBuilder<K, N, D, E, P, T>;
287
+ /**
288
+ * See {@link ColumnDef.ignored}.
289
+ *
290
+ * COPIES the def rather than mutating it. The constructor takes an existing
291
+ * def BY REFERENCE, so every builder derived from another shares one object —
292
+ * `const a = slug.unique()` leaves `a._def === slug._def`. An in-place
293
+ * `ignored = true` therefore marks every column sharing that def, including
294
+ * one another table actively reads, and the gate would let THAT column be
295
+ * dropped. Measured before this copy existed.
296
+ *
297
+ * The aliasing is older than this method and other fields leak through it too.
298
+ * The reason this one cannot wait: every other leak produces a VISIBLE schema
299
+ * difference — the plan shows it, the DDL shows it. This one is invisible by
300
+ * design (no DDL, no diff, no plan line), so its only effect is to disarm a
301
+ * safety gate in silence.
302
+ */
303
+ ignored(): ColumnBuilder<K, N, D, E, P, T>;
304
+ /**
305
+ * Foreign key onto another table's column.
306
+ *
307
+ * The target is a THUNK, not a direct reference. In a cycle (`x → y`, `y → x`)
308
+ * the second table does not exist yet when the first is built; a direct
309
+ * reference makes TypeScript chase its own tail (TS7022 — measured, and making
310
+ * the return type independent of the target does NOT help). The thunk is
311
+ * invoked in `defineSchema`, where every binding exists and every table
312
+ * already knows its name.
313
+ *
314
+ * In a cycle, ONE side needs an explicit return type:
315
+ * `references((): AnyColumn => y.id)`. One side is enough — measured.
316
+ * For a self-reference use `selfReferences(column)`: no thunk, no annotation.
317
+ *
318
+ * `as` names the FORWARD relation (`author_id` → `author` by default);
319
+ * `reverseAs` names the REVERSE one on the parent, whose default is this
320
+ * table's own name (`users.posts`). Two foreign keys from one table onto one
321
+ * parent therefore need a `reverseAs` on at least one of them — the reverse
322
+ * names would otherwise both be this table's name.
323
+ */
324
+ references(target: () => AnyColumn, opts?: {
325
+ as?: string;
326
+ reverseAs?: string;
327
+ onDelete?: OnDeleteAction;
328
+ /**
329
+ * Bu FK kolonu için TÜREVİ index üretilsin mi (FR-044). Varsayılan açık.
330
+ *
331
+ * Postgres bir foreign key'i otomatik indekslemez — yalnız hedef
332
+ * taraftaki unique kısıt vardır. Bedeli FK üzerinden her JOIN'de ve her
333
+ * `ON DELETE CASCADE`'de ödenir: bir parent silinirken child tablo tam
334
+ * taranır.
335
+ *
336
+ * `false` demek gerçek bir ihtiyaç, nezaket değil (D-028): FK kolonu aynı
337
+ * zamanda SIK GÜNCELLENEN bir kolonsa index HOT güncellemeyi kırar — ve
338
+ * HOT, DEĞİŞEN kolon indeksliyse kırılır, tablo indeksli diye değil.
339
+ */
340
+ index?: boolean;
341
+ }): ColumnBuilder<K, N, D, E, P, T>;
342
+ /**
343
+ * Bu kolon `increment()` / `decrement()` ile güncelleniyor (FR-049).
344
+ *
345
+ * Deploy kontrolcü kodunu OKUMAZ, o yüzden bildirimin söylemesi gerekiyor —
346
+ * ve söylediği anda plan bir şeyi görebiliyor: aynı kolon hem sayaç hem
347
+ * indeksliyse her güncelleme HOT'u kaybeder.
348
+ *
349
+ * ZİNCİR (D-028+D-030): HOT kaybı → ölü tuple → autovacuum yükü → ve
350
+ * autovacuum worker'ları KÜME GENELİNDE bir kaynak, yani bedeli başka
351
+ * kiracıların tabloları da öder.
352
+ *
353
+ * Sektörde çare "dokümana uyarı yaz"dır, çünkü index'i ekleyen kişi
354
+ * `increment()`'i yazan kişi değildir. Bu bildirimde ikisi de YAN YANA
355
+ * duruyor.
356
+ *
357
+ * Uyarı, HATA DEĞİL: sayaç kolonunu indekslemek bazen doğru karardır.
358
+ */
359
+ counter(): ColumnBuilder<K, N, D, E, P, T>;
360
+ /**
361
+ * Foreign key onto THIS table (`parent_id → id`) — category trees, comment
362
+ * replies, org charts.
363
+ *
364
+ * No thunk and no type annotation: the target table is the one being declared,
365
+ * so there is nothing to defer and nothing for TypeScript to chase in a circle.
366
+ * Drizzle forces an explicit `(): AnyPgColumn =>` here because its reference
367
+ * always goes through a callback; measured, we do not need one.
368
+ */
369
+ selfReferences(column: string, opts?: {
370
+ as?: string;
371
+ onDelete?: OnDeleteAction;
372
+ }): ColumnBuilder<K, N, D, E, P, T>;
373
+ /** Set the ON DELETE action for a foreign key reference. */
374
+ onDelete(action: OnDeleteAction): ColumnBuilder<K, N, D, E, P, T>;
375
+ /** Add a single-column UNIQUE constraint. */
376
+ unique(): ColumnBuilder<K, N, D, E, P, T>;
377
+ /**
378
+ * Declare how this column's value is projected in and out of the process.
379
+ *
380
+ * The DDL does not move: `numeric` stays `numeric`, and the driver still hands
381
+ * back what Postgres sent. What changes is the type the row surface exposes —
382
+ * it becomes `Target`:
383
+ *
384
+ * amount: numeric().transform<number>({ fromDb: Number, toDb: String })
385
+ *
386
+ * `numeric` surfacing as `string` is CORRECT (a JS number cannot hold
387
+ * arbitrary precision), and that is exactly why this exists: application code
388
+ * that does arithmetic on the column otherwise rewrites the same
389
+ * `Number(row.amount)` / `String(x)` pair in every controller that touches it,
390
+ * and each rewrite is a place the two directions can drift apart.
391
+ *
392
+ * A transform is a PROJECTION, never a constraint: it lives only in this
393
+ * process, so it can neither validate nor migrate what is stored.
394
+ */
395
+ transform<Target>(fns: ColumnTransform<Target>): ColumnBuilder<K, N, D, E, P, Target>;
396
+ }
397
+ /**
398
+ * Extracts the TypeScript value type for a column, respecting nullability.
399
+ * - "uuid" | "text" | "timestamp" | "bigint" | "numeric" → string (or string | null when N = true)
400
+ * Note: bigint/numeric surface as string — JS number loses precision past 2^53,
401
+ * and pgx/PostgREST serialize int8/numeric as strings. App code uses
402
+ * BigInt(row.amount) for bigint, or a decimal lib for numeric.
403
+ * - "integer" → number
404
+ * - "boolean" → boolean
405
+ * - "jsonb" → P (the dev-supplied payload shape from jsonb<T>(), else unknown)
406
+ * - "enum" → E (the union of literal values)
407
+ *
408
+ * A declared `.transform<T>()` OVERRIDES the table above: the column then
409
+ * surfaces as T (or T | null when nullable), because that is the value the
410
+ * application is handed. Nullability is still the column's, not the
411
+ * transform's — `fromDb` is not called for a NULL.
412
+ */
413
+ type ColValue<C> = C extends ColumnBuilder<ColumnType, infer N, boolean, unknown, unknown, infer T> ? [unknown] extends [T] ? ColStoredValue<C> : N extends true ? T | null : T : never;
414
+ /** The value as the DATABASE hands it over — the branch table above, before any
415
+ * transform. This is what a column's `fromDb` receives. */
416
+ type ColStoredValue<C> = C extends ColumnBuilder<'uuid' | 'text' | 'timestamp' | 'bigint' | 'numeric', infer N, infer _D, infer _E, infer _P> ? N extends true ? string | null : string : C extends ColumnBuilder<'integer', infer N, infer _D, infer _E, infer _P> ? N extends true ? number | null : number : C extends ColumnBuilder<'boolean', infer N, infer _D, infer _E, infer _P> ? N extends true ? boolean | null : boolean : C extends ColumnBuilder<'jsonb', infer N, infer _D, infer _E, infer P> ? N extends true ? P | null : P : C extends ColumnBuilder<'vector', infer N, infer _D, infer _E, infer _P> ? N extends true ? number[] | null : number[] : C extends ColumnBuilder<'enum', infer N, infer _D, infer E, infer _P> ? N extends true ? E | null : E : never;
417
+ /**
418
+ * True when a column is optional on INSERT:
419
+ * - nullable columns (N = true) — the DB allows NULL so the field may be omitted
420
+ * - columns with a default (D = true) — the DB fills in the value when absent
421
+ */
422
+ type ColIsOptionalOnInsert<C> = C extends ColumnBuilder<ColumnType, true, boolean, unknown, unknown, unknown> ? true : C extends ColumnBuilder<ColumnType, boolean, true, unknown, unknown, unknown> ? true : false;
423
+ /** Create a UUID column. */
424
+ declare function uuid(): ColumnBuilder<'uuid', false, false, never>;
425
+ /** Create a TEXT column. */
426
+ declare function text(): ColumnBuilder<'text', false, false, never>;
427
+ /** Create an INTEGER column. Emits int4 (max ~2.1B). */
428
+ declare function integer(): ColumnBuilder<'integer', false, false, never>;
429
+ /**
430
+ * Create a BIGINT column (Postgres int8, max ~9.2×10^18).
431
+ * Surfaces as `string` in row/insert types — JS number loses precision past 2^53
432
+ * and pgx/PostgREST serialize int8 as a JSON string. Use BigInt(row.column) in app code.
433
+ */
434
+ declare function bigint(): ColumnBuilder<'bigint', false, false, never>;
435
+ /**
436
+ * Create a NUMERIC column (Postgres `numeric`/`decimal`, arbitrary precision).
437
+ * For exact fractional values (money with cents as a decimal, rates, weights)
438
+ * where int4/int8 don't fit. Surfaces as `string` in row/insert types — JS
439
+ * number can't hold arbitrary-precision decimals without rounding, and
440
+ * pgx/PostgREST serialize numeric as a JSON string. Parse with a decimal lib
441
+ * (or BigInt for scaled integers) in app code.
442
+ */
443
+ declare function numeric(): ColumnBuilder<'numeric', false, false, never>;
444
+ /** Create a BOOLEAN column. */
445
+ declare function boolean(): ColumnBuilder<'boolean', false, false, never>;
446
+ /** Create a TIMESTAMP column. */
447
+ declare function timestamp(): ColumnBuilder<'timestamp', false, false, never>;
448
+ /**
449
+ * Create a JSONB column. Pass a payload type to make the generated row/insert
450
+ * type concrete instead of `unknown`:
451
+ *
452
+ * tags: jsonb<string[]>() // row.tags: string[]
453
+ * meta: jsonb<{ tier: string }>() // row.meta: { tier: string }
454
+ * raw: jsonb() // row.raw: unknown (back-compat)
455
+ *
456
+ * The runtime accepts a plain JS object/array directly (no JSON.stringify); the
457
+ * generic only refines the TYPE the env codegen emits.
458
+ */
459
+ declare function jsonb<T = unknown>(): ColumnBuilder<'jsonb', false, false, never, T>;
460
+ /**
461
+ * Create an ENUM column.
462
+ * @param name The PostgreSQL enum type name (used in DDL).
463
+ * @param values A readonly tuple of valid string values — kept `const` so the
464
+ * union `V[number]` is as narrow as possible.
465
+ */
466
+ declare function enumType<const V extends readonly string[]>(name: string, values: V): ColumnBuilder<'enum', false, false, V[number]>;
467
+ /** vector(n) — pgvector kolonu. n TİPİN parçasıdır (typmod) ve [1, 2000] —
468
+ * 2000 = pgvector'ün HNSW-indekslenebilir tavanı; auto-index bu beyanla bağlı
469
+ * (spec FR-001, D-3). */
470
+ declare function vector(dimensions: number): ColumnBuilder<'vector', false, false, unknown, number[]>;
471
+ /**
472
+ * Any column, whatever its type parameters.
473
+ *
474
+ * Exported so a cycle can be broken from ONE side:
475
+ * `references((): AnyColumn => y.id)`.
476
+ */
477
+ type AnyColumn = ColumnBuilder;
478
+ /**
479
+ * The column that OWNS this row: a `text` FK onto `auth.users(id)`, NOT NULL,
480
+ * ON DELETE CASCADE.
481
+ *
482
+ * Ownership drives account erasure, so cascade is the only correct action and
483
+ * takes no argument. The referencing column must be `text` (palauth ids are
484
+ * `usr_<uuid>`) and NOT NULL — both are implied here rather than left to the
485
+ * caller, so three rules the type could not express before become UNWRITABLE.
486
+ *
487
+ * At most ONE per table. The old shape let several columns reference
488
+ * `auth.users` and picked the FIRST IN DECLARATION ORDER as the owner — moving a
489
+ * `created_by` above a `user_id` silently changed which rows an account deletion
490
+ * took with it. A second one is now rejected at push.
491
+ *
492
+ * For a column that merely POINTS at a user without owning the row
493
+ * (`created_by`, `edited_by`), use `userRef({ onDelete })`.
494
+ */
495
+ declare function ownedByUser(): ColumnBuilder<'text', false, false, never>;
496
+ /**
497
+ * A plain FK onto `auth.users(id)` that does NOT own the row.
498
+ *
499
+ * `created_by` / `edited_by`: deleting that user must not delete the row. ON
500
+ * DELETE is required and limited to `cascade | set null` so an erasure request
501
+ * is never blocked by a lingering FK; `set null` needs a nullable column.
502
+ */
503
+ declare function userRef(opts: {
504
+ onDelete: AuthUserOnDelete;
505
+ as?: string;
506
+ }): ColumnBuilder<'text', boolean, false, never>;
507
+ /**
508
+ * A plain FK onto `auth.installations(id)` — the app-scoped verified-device
509
+ * anchor.
510
+ *
511
+ * An installation is an APP INSTALL, not a user: this is NOT ownership. A
512
+ * user-owned row still needs its own `ownedByUser()` so account erasure removes
513
+ * it; an installation reference alone does not tie a row to a user's deletion.
514
+ */
515
+ declare function installationRef(opts: {
516
+ onDelete: AuthUserOnDelete;
517
+ as?: string;
518
+ }): ColumnBuilder<'text', boolean, false, never>;
519
+
520
+ /**
521
+ * policy.ts — the RLS policy authoring DSL.
522
+ *
523
+ * `policy(name)` returns a fluent builder that mirrors the `ColumnBuilder`
524
+ * style in columns.ts: each chainable method mutates the underlying
525
+ * definition and returns the builder so calls compose. The terminal value is
526
+ * a plain {@link PolicyDef} — the exact JSON shape the runtime's
527
+ * `schema_extract.js` reads off the bundled module and the Go side parses into
528
+ * `PolicyJSON` (CONTRACT-POLICY).
529
+ *
530
+ * @example
531
+ * import { policy } from "@palbase/backend";
532
+ *
533
+ * policy("owner_select")
534
+ * .for("select")
535
+ * .to("authenticated")
536
+ * .using("owner = (select auth.uid())");
537
+ */
538
+ /** Politika ifadesinin düğümleri. DÜZ VERİ: tele bu şekilde gider. */
539
+ type PolicyExpr = {
540
+ kind: "col";
541
+ name: string;
542
+ /**
543
+ * Hangi tablonun kolonu (FR-023). Yoksa POLİTİKANIN KENDİ tablosu —
544
+ * Postgres politika ifadesinde onu niteliksiz çözer. Doluysa bir
545
+ * `existsIn` alt-sorgusunun takma adı.
546
+ *
547
+ * Bu ayrım korelasyonun tamamı: ayırt edilmezse `team_id = team_id`
548
+ * üretilir, her satır için doğrudur, ve politika HİÇBİR ŞEYİ süzmez —
549
+ * yani tüm belgeler herkese görünür.
550
+ */
551
+ alias?: string;
552
+ } | {
553
+ kind: "authUid";
554
+ } | {
555
+ kind: "lit";
556
+ value: string | number | boolean | null;
557
+ } | {
558
+ kind: "binop";
559
+ op: PolicyBinOp;
560
+ left: PolicyExpr;
561
+ right: PolicyExpr;
562
+ } | {
563
+ kind: "and";
564
+ parts: PolicyExpr[];
565
+ } | {
566
+ kind: "or";
567
+ parts: PolicyExpr[];
568
+ } | {
569
+ kind: "not";
570
+ expr: PolicyExpr;
571
+ } | {
572
+ kind: "isNull";
573
+ expr: PolicyExpr;
574
+ negated: boolean;
575
+ } | {
576
+ /**
577
+ * Korelasyonlu `EXISTS (SELECT 1 FROM <table> <alias> WHERE <predicate>)`.
578
+ *
579
+ * FR-024 BAĞLAYICI: alt-sorgu DOĞRUDAN üretilir, bir `SECURITY DEFINER`
580
+ * fonksiyona SARILMAZ. Ölçüldü (spec Research Appendix, pg16): doğrudan
581
+ * alt-sorguda hedef tablonun KENDİ RLS'i uygulanıyor — yani sızıntı
582
+ * kanalı yok. Sarmalama ise onu bypass ederdi.
583
+ */
584
+ kind: "exists";
585
+ table: string;
586
+ alias: string;
587
+ /**
588
+ * POLİTİKANIN KENDİ tablosunun adı — korelasyonun çalışması için ŞART.
589
+ *
590
+ * ÖLÇÜLDÜ (canlı pg16): alt-sorgu içinde niteliksiz bir kolon adı
591
+ * Postgres tarafından İÇ tabloya çözülüyor. Yani dış kolon niteliksiz
592
+ * bırakılınca `pb_e1.team_id = pb_e1.team_id` üretiliyor — her satır
593
+ * için doğru, ve politika HİÇBİR ŞEYİ süzmüyor: iki farklı kullanıcı
594
+ * da tüm belgeleri gördü. Dış tablo adı burada taşınıyor ki emitter
595
+ * `"documents"."team_id"` yazabilsin.
596
+ */
597
+ outer: string;
598
+ predicate: PolicyExpr;
599
+ };
600
+ /** Karşılaştırma operatörleri. `in` sağ tarafta liste bekler. */
601
+ type PolicyBinOp = "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in";
602
+ /**
603
+ * Bir ifadenin ETRAFINDAKİ akıcı sarmalayıcı.
604
+ *
605
+ * Düğüm (`expr`) düz veri kalıyor, metotlar sarmalayıcıda yaşıyor: `_def.using`
606
+ * serileştiğinde metotlu bir nesne değil, tam olarak tele gidecek şekil olsun
607
+ * diye. `.using()` sarmalayıcıyı açar.
608
+ */
609
+ declare class PolicyExprRef {
610
+ readonly expr: PolicyExpr;
611
+ constructor(expr: PolicyExpr);
612
+ private bin;
613
+ eq(other: PolicyOperand): PolicyExprRef;
614
+ neq(other: PolicyOperand): PolicyExprRef;
615
+ gt(other: PolicyOperand): PolicyExprRef;
616
+ gte(other: PolicyOperand): PolicyExprRef;
617
+ lt(other: PolicyOperand): PolicyExprRef;
618
+ lte(other: PolicyOperand): PolicyExprRef;
619
+ /**
620
+ * `IS NULL` / `IS NOT NULL` — `= NULL` SQL'de her zaman UNKNOWN'dır, o yüzden
621
+ * niyet ayrı bir düğümle yazılır. Partial index'in en sık koşulu
622
+ * (`WHERE deleted_at IS NULL`) tam olarak bu.
623
+ */
624
+ isNull(negated?: boolean): PolicyExprRef;
625
+ and(...others: PolicyExprRef[]): PolicyExprRef;
626
+ or(...others: PolicyExprRef[]): PolicyExprRef;
627
+ not(): PolicyExprRef;
628
+ }
629
+ /** `.eq()` gibi metotların sağ tarafı: başka bir ifade ya da düz bir değer. */
630
+ type PolicyOperand = PolicyExprRef | string | number | boolean | null;
631
+ /**
632
+ * Politika ifadesi bağlamı — `defineTable`'ın `policies` callback'ine verilir.
633
+ *
634
+ * Kolon adları TABLONUN KENDİ kolonlarıyla sınırlıdır (FR-022). Bu, `policies`
635
+ * bir dizi değil de callback olduğu için mümkün: TypeScript `columns`'tan
636
+ * `C`'yi çıkarıp callback'i onunla bağlamlandırıyor. `policy("x")` tek başına
637
+ * çağrıldığında hangi tabloda olduğunu bilemezdi.
638
+ */
639
+ interface PolicyExprCtx<Cols extends string = string> {
640
+ /** Bu tablonun bir kolonu. Bilinmeyen ad DERLENMEZ. */
641
+ col(name: Cols): PolicyExprRef;
642
+ /**
643
+ * "Bu satırın sahibi olduğum bir takıma ait mi?" — üyelik tabanlı kiracılık
644
+ * (FR-023), korelasyonlu bir `EXISTS` alt-sorgusu olarak.
645
+ *
646
+ * ```ts
647
+ * policies: (p) => [
648
+ * policy("team_read").for("select").using(
649
+ * p.existsIn(teamMembers, (m) =>
650
+ * m.col("team_id").eq(p.col("team_id"))
651
+ * .and(m.col("user_id").eq(p.auth.uid()))),
652
+ * ),
653
+ * ]
654
+ * ```
655
+ *
656
+ * `m` ÜYELİK tablosunun kolonlarını, `p` bu tablonunkileri verir; ikisi de
657
+ * tipli ve SQL'de farklı nitelenirler. Karışsalardı `team_id = team_id`
658
+ * üretilir ve politika hiçbir şeyi süzmezdi.
659
+ *
660
+ * ÖZYİNELEME TUZAĞI: üyelik tablosunun KENDİ politikası `existsIn`
661
+ * KULLANMAZ — `user_id = auth.uid()` yazar. İki tablo birbirini
662
+ * sorgularsa Postgres sorgu anında
663
+ * `infinite recursion detected in policy for relation …` der. Asimetri
664
+ * kasıtlıdır: üyelik tablosu basit olanı alır, diğer her tablo ona
665
+ * alt-sorgu ATAR.
666
+ */
667
+ existsIn<C extends ColumnMap, N extends string>(table: TableHandle<C, N>, predicate: (row: PolicyExprCtx<Extract<keyof C, string>>) => PolicyExprRef): PolicyExprRef;
668
+ /** Çağıranın kimliği. SQL'e `(select auth.uid())` olarak iner (InitPlan). */
669
+ auth: {
670
+ uid(): PolicyExprRef;
671
+ };
672
+ /** Düz bir değer — karşılaştırmanın sağ tarafında açıkça literal demek için. */
673
+ lit(v: string | number | boolean | null): PolicyExprRef;
674
+ and(...parts: PolicyExprRef[]): PolicyExprRef;
675
+ or(...parts: PolicyExprRef[]): PolicyExprRef;
676
+ not(part: PolicyExprRef): PolicyExprRef;
677
+ }
678
+ /**
679
+ * İfade bağlamını kurar. `defineTable` bunu kendi kolon adlarıyla çağırır;
680
+ * testler ve elle kullanım için de dışa açık.
681
+ */
682
+ declare function exprCtx<Cols extends string = string>(_columns?: readonly Cols[], depth?: number,
683
+ /**
684
+ * POLİTİKANIN KENDİ tablosu — her derinlikte AYNI kalır.
685
+ *
686
+ * Alt-sorgunun içinde takma adsız duran bir kolon, dış sorgudaki tabloya
687
+ * bağlanmak zorundadır; Go tarafı onu bu adla nitelendiriyor. Değer iç içe
688
+ * geçmede ORTA tablonun adına kayıyordu (`meta.name` aşağı veriliyordu) ve
689
+ * sonuç canlıda `ERROR: missing FROM-clause entry for table "…"` oluyordu —
690
+ * yani iç içe `existsIn` içinden politikanın kendi kolonuna erişmek imkânsızdı
691
+ * (nihai inceleme I-1, ölçüldü). Kök tablo aşağı DEĞİŞMEDEN taşınır.
692
+ */
693
+ ownTable?: string): PolicyExprCtx<Cols>;
694
+ /** The SQL command a policy applies to. `"all"` covers SELECT/INSERT/UPDATE/DELETE. */
695
+ type PolicyCommand = "all" | "select" | "insert" | "update" | "delete";
696
+ /** Whether a policy is permissive (OR-combined, the default) or restrictive
697
+ * (AND-combined). Mirrors Postgres `CREATE POLICY ... AS PERMISSIVE|RESTRICTIVE`. */
698
+ type PolicyMode = "permissive" | "restrictive";
699
+ /**
700
+ * The compiled, serializable policy definition — the EXACT shape consumed by
701
+ * `schema_extract.js` → Go `PolicyJSON` (CONTRACT-POLICY).
702
+ *
703
+ * - `roles`: the DB roles this policy applies to (`TO` clause). An empty array
704
+ * means the policy applies to PUBLIC (all roles) — the Postgres default.
705
+ * - `using`: the `USING (...)` row-visibility expression, or `null` when none.
706
+ * - `withCheck`: the `WITH CHECK (...)` write-validation expression, or `null`.
707
+ * - `permissive`: `true` for `AS PERMISSIVE` (default), `false` for restrictive.
708
+ */
709
+ interface PolicyDef {
710
+ name: string;
711
+ command: PolicyCommand;
712
+ roles: string[];
713
+ /** Yapı (FR-022) ya da ham string (FR-028 kaçış kapağı). */
714
+ using: string | PolicyExpr | null;
715
+ withCheck: string | PolicyExpr | null;
716
+ permissive: boolean;
717
+ }
718
+ declare class PolicyBuilder {
719
+ readonly _def: PolicyDef;
720
+ constructor(name: string);
721
+ /** Restrict the policy to a single SQL command (default `"all"`). */
722
+ for(command: PolicyCommand): this;
723
+ /**
724
+ * Set the DB roles the policy applies to (the `TO` clause), replacing any
725
+ * previously-set roles. Call with no arguments to target PUBLIC (all roles).
726
+ *
727
+ * @example
728
+ * policy("p").to("authenticated")
729
+ * policy("p").to("authenticated", "service_role")
730
+ * policy("p").to() // PUBLIC
731
+ */
732
+ to(...roles: string[]): this;
733
+ /**
734
+ * `USING (...)` satır görünürlüğü ifadesi.
735
+ *
736
+ * Tercih edilen biçim YAPI'dır (FR-022) — `policies` callback'inin verdiği
737
+ * bağlamla kurulur ve kolon adları derlemede doğrulanır:
738
+ *
739
+ * ```ts
740
+ * policies: (p) => [
741
+ * policy("owner_read").for("select").using(p.col("owner").eq(p.auth.uid())),
742
+ * ]
743
+ * ```
744
+ *
745
+ * Ham string KAÇIŞ KAPAĞI olarak kalır (FR-028): yapının ifade edemediği bir
746
+ * şey gerektiğinde yol kapanmaz. Ama o yolda kolon adı doğrulaması yoktur ve
747
+ * hata deploy anında Postgres'ten gelir.
748
+ */
749
+ using(expr: string | PolicyExprRef): this;
750
+ /**
751
+ * "Rows of THIS table whose owner the caller is a member of" — the membership
752
+ * pattern, written so it cannot recurse.
753
+ *
754
+ * THE TRAP IT EXISTS FOR. Written by hand, membership policies point at each
755
+ * other: `channels` is visible to members, so its policy reads
756
+ * `channel_members`; `channel_members` is visible to members, so its policy
757
+ * reads `channels`. Postgres refuses the pair at query time with `infinite
758
+ * recursion detected in policy for relation ...`, and the error names the
759
+ * relation but not the cycle. The way out is asymmetry — the MEMBERSHIP table
760
+ * is protected by `user_id = auth.uid()` and nothing else, and every other
761
+ * table subqueries INTO it. That shape was in the platform's own schema and
762
+ * written down nowhere; a customer recovered it by reading that schema.
763
+ *
764
+ * `(select auth.uid())` rather than a bare call: the scalar subquery is
765
+ * evaluated ONCE per statement instead of per row.
766
+ *
767
+ * @example
768
+ * // channels: visible to members. The membership table gets the simple one.
769
+ * policy("member_read").for("select").to("authenticated")
770
+ * .memberOf("channel_members", "channel_id")
771
+ * // → id IN (SELECT "channel_id" FROM "channel_members"
772
+ * // WHERE "user_id" = (select auth.uid()))
773
+ */
774
+ memberOf(membershipTable: string, foreignKey: string, options?: {
775
+ column?: string;
776
+ userColumn?: string;
777
+ }): this;
778
+ /** `WITH CHECK (...)` yazma doğrulaması — `using` ile aynı iki biçim. */
779
+ withCheck(expr: string | PolicyExprRef): this;
780
+ /** Set the policy mode: `"permissive"` (default, OR-combined) or
781
+ * `"restrictive"` (AND-combined). */
782
+ as(mode: PolicyMode): this;
783
+ }
784
+ /**
785
+ * Start authoring an RLS policy. Returns a {@link PolicyBuilder}; the resulting
786
+ * `PolicyBuilder` is accepted directly in a table's `policies: [...]` array
787
+ * (its `_def` is read at schema-extract time).
788
+ *
789
+ * @param name The policy name. Palbase reconciliation keys policies by
790
+ * `(table, name)`, so names must be unique per table.
791
+ */
792
+ declare function policy(name: string): PolicyBuilder;
793
+
794
+ /**
795
+ * Postgres extensions a Palbase project can enable from its schema.
796
+ *
797
+ * Extensions are config-as-code: declare them in `defineSchema({ extensions })`
798
+ * and the deploy installs them (CREATE EXTENSION … SCHEMA extensions) using the
799
+ * deploy path's privileged connection. They are NOT toggled live from Studio —
800
+ * CREATE EXTENSION requires a superuser role that only the deploy path holds.
801
+ *
802
+ * The list is an allowlist (a string-literal union) so editors autocomplete the
803
+ * supported names and a typo fails typecheck. It is intentionally extensible:
804
+ * add a name here (+ confirm the base image ships it) to support more.
805
+ */
806
+ declare const PALBASE_EXTENSIONS: readonly ["vector", "pg_trgm", "unaccent", "citext", "cube", "earthdistance", "hstore", "ltree", "btree_gist", "pgcrypto", "uuid-ossp"];
807
+ /** A Postgres extension supported by Palbase (allowlist union). */
808
+ type PalbaseExtension = (typeof PALBASE_EXTENSIONS)[number];
809
+ /**
810
+ * Extensions that depend on another extension. The deploy installs
811
+ * dependencies first; declaring `earthdistance` without `cube` still works
812
+ * because the deploy resolves the order, but listing both is clearer.
813
+ */
814
+ declare const EXTENSION_DEPENDENCIES: Partial<Record<PalbaseExtension, PalbaseExtension[]>>;
815
+ /** Runtime guard: is `name` a supported Palbase extension? */
816
+ declare function isPalbaseExtension(name: string): name is PalbaseExtension;
817
+
818
+ /**
819
+ * A named raw-SQL DDL object declared in a schema file (`db/public.ts`,
820
+ * `db/billing.ts`, …) for anything the typed DSL
821
+ * cannot express (EXCLUDE, CHECK, partial/expression indexes, triggers, views).
822
+ * The deploy emits `up` verbatim on the privileged DDL connection — same trust
823
+ * posture as policy().using(). Tracked by NAME (not by diffing the body), so a
824
+ * changed body needs a new name or an explicit drop+add.
825
+ */
826
+ interface RawConstraintDef {
827
+ name: string;
828
+ up: string;
829
+ down?: string;
830
+ }
831
+ declare function raw(name: string, up: string, opts?: {
832
+ down?: string;
833
+ }): RawConstraintDef;
834
+
835
+ /** Embedding sağlayıcı DESKRIPTORU — canlı istemci değil, düz veri: şemayla
836
+ * birlikte serileşir, çağrıyı Go worker (yazma) ve engine (sorgu) yapar.
837
+ * Adlandırma Vercel AI SDK'nın aynasıdır (openai.embedding("...")) ama paket
838
+ * bağımlılığı bilinçli olarak YOKTUR (spec C-3, UD-016). v1 sağlayıcı: openai (D-10). */
839
+ interface EmbeddingModelRef {
840
+ provider: "openai";
841
+ model: string;
842
+ dimensions?: number;
843
+ apiKeyName?: string;
844
+ baseURL?: string;
845
+ }
846
+ /** Chat/damıtma modeli DESKRIPTORU (C-11, D-019) — memory beyanının extract'i.
847
+ * Embedding gibi düz veridir; çağrıyı worker yapar, anahtar vault'taki
848
+ * OPENAI_API_KEY'dir (D-017: aynı sağlayıcı, yeni dış sistem yok). */
849
+ interface ChatModelRef {
850
+ provider: "openai";
851
+ model: string;
852
+ }
853
+ declare const openai: {
854
+ embedding(model: string, opts?: {
855
+ dimensions?: number;
856
+ apiKeyName?: string;
857
+ baseURL?: string;
858
+ }): EmbeddingModelRef;
859
+ chat(model: string): ChatModelRef;
860
+ };
861
+
862
+ /**
863
+ * A map of column builders keyed by column name — the value you write under
864
+ * the `columns` key of `defineTable("<name>", { columns })`.
865
+ *
866
+ * The default `Record<string, ColumnBuilder>` keeps bare references compiling
867
+ * without a type argument.
868
+ */
869
+ type ColumnMap = Record<string, ColumnBuilder>;
870
+ /**
871
+ * The author-facing value written under each table key:
872
+ * `{ columns, rls?, policies? }`.
873
+ *
874
+ * - `columns`: the column map (required).
875
+ * - `rls`: enable + FORCE row-level security on this table. **Defaults to
876
+ * `true`**, and is forced on when `policies` is non-empty. A table with RLS
877
+ * and no policies is deny-all, which is the starting state: nothing reads it
878
+ * until a policy says who may. Set `rls: false` only for a genuinely public
879
+ * table — it is an explicit opt-out that a reviewer can grep for, not
880
+ * something you get by forgetting.
881
+ * - `policies`: the RLS policies for this table, authored with `policy(name)`.
882
+ * Each entry may be a {@link PolicyBuilder} (the normal `policy(...)` chain)
883
+ * or a raw {@link PolicyDef} object.
884
+ *
885
+ * The `C` type parameter preserves the precise per-column phantom types so the
886
+ * typed `Database.public.*` surface keeps inferring insert/row shapes.
887
+ */
888
+ /** Arama metriği — tek kelime; opclass ve operatör bundan türetilir, asla yüzeye çıkmaz (D-5/D-6). */
889
+ type SearchMetric = "cosine" | "euclidean" | "inner_product";
890
+ /** Bir vektör arama kolu. `model` varsa auto-embed: platform yazma+sorgu embedding'ini üstlenir (D-9). */
891
+ interface VectorSearchDecl {
892
+ /** Hedef vector kolonu; tabloda TEK vector kolonu varsa atlanabilir (Go apply çözer, FR-010). */
893
+ column?: string;
894
+ /** Varsa auto-embed. Deskriptor düz veridir — şemayla serileşir (C-3). */
895
+ model?: EmbeddingModelRef;
896
+ /** model varsa ZORUNLU: embed kaynak kolonları = trigger'ın UPDATE OF listesi (C-2). */
897
+ from?: string[];
898
+ metric?: SearchMetric;
899
+ /** Kaynak metin değişince ara-dönem davranışı (yalnız auto-embed'de):
900
+ * "null" (vars.) — embedding anında NULL'lanır; satır yeni vektör yazılana
901
+ * dek anlamsal aramada aday değildir. Bayat eşleşme asla servis edilmez.
902
+ * "keep" — eski vektör aramada kalır, platform yenisini yazınca sessizce
903
+ * değişir. Görünürlük penceresi sıfır; bedeli saniyeler süren bayat
904
+ * eşleşme riski (Confluence-tipi sync yükleri için). */
905
+ staleness?: "null" | "keep";
906
+ }
907
+ /** Tablonun arama beyanı — İKİ biçim (D-007, tek yüzey):
908
+ *
909
+ * YENİ (önerilen): `{ from, model, ... }` — `from` kolonları hem FTS'e hem
910
+ * embed'e girer. Tabloda vector kolonu declare edilmişse SATIR-modu; yoksa
911
+ * CHUNK-modu otomatiktir (D-010): vektörler türev `__palbase_chunks`
912
+ * tablosunda yaşar, içerik otomatik bölünür. `text: false` FTS'i kapatır,
913
+ * `text: [..]` FTS kolonlarını from'dan ayırır. `chunks` yalnız ince ayar.
914
+ *
915
+ * ESKİ: `text: string[]` + `vector: {...}` — aynen çalışır, wire çıktısı
916
+ * bayt-aynı kalır (NFR-B1). İki biçim KARIŞTIRILAMAZ. */
917
+ interface SearchDecl {
918
+ text?: string[] | boolean;
919
+ vector?: VectorSearchDecl | VectorSearchDecl[];
920
+ /** Yeni biçim: arama kaynağı kolonlar (FTS + embed). Varlığı yeni biçimi seçer. */
921
+ from?: string[];
922
+ /** Yeni biçim: auto-embed modeli (zorunlu — BYO için eski biçimi kullanın). */
923
+ model?: EmbeddingModelRef;
924
+ metric?: SearchMetric;
925
+ staleness?: "null" | "keep";
926
+ /** Chunk-modu ince ayarı (yalnız vector kolonsuz tabloda anlamlı). */
927
+ chunks?: {
928
+ size?: number;
929
+ overlap?: number;
930
+ };
931
+ /** Sorgu-yeniden-yazımı: tek yönlü eş anlamlı haritası (FR-026). */
932
+ synonyms?: Record<string, string[]>;
933
+ /** Geçerlilik kolonları türetilir; arama varsayılan yalnız günceli tarar (FR-029). */
934
+ validity?: boolean;
935
+ }
936
+ /** Hafıza beyanı (FR-032, D-019): kaynak tablonun yazımlarından platform
937
+ * fact damıtır ve `into` tablosuna yazar. Hedef NORMAL declared tablodur —
938
+ * kendi search/unique/validity beyanlarıyla. Okuma = Database.$search(into).
939
+ * subject default "owner": fact'in kime ait olduğu kolonu (iki tabloda da). */
940
+ interface MemoryDecl {
941
+ from: string[];
942
+ into: string;
943
+ extract: ChatModelRef;
944
+ subject?: string;
945
+ }
946
+ /**
947
+ * Bir index bildiriminin girdi şekli (FR-041…043).
948
+ *
949
+ * `columns` ile `expression` birbirinin alternatifidir: biri kolon listesi,
950
+ * diğeri `lower(email)` gibi bir ifade. `where` partial index koşuludur ve
951
+ * filtre şekli sorgu tarafındaki `WhereFilter` ile aynıdır — iki ayrı filtre
952
+ * dili olmaz.
953
+ */
954
+ /**
955
+ * Bir index'in TEL şekli — `IndexJSON` ile aynı alanlar (T002).
956
+ *
957
+ * Bildirilmeyen alan HİÇ yazılmaz: sıfır değer yazmak, bildirmeyen her index'i
958
+ * diff'te değişmiş gösterir ve her deploy'a boş bir migration ekler.
959
+ */
960
+ interface IndexDef {
961
+ name: string;
962
+ columns?: string[];
963
+ expression?: string;
964
+ where?: PolicyExpr;
965
+ sort?: "asc" | "desc";
966
+ nulls?: "first" | "last";
967
+ include?: string[];
968
+ }
969
+ /**
970
+ * `index("<ad>")` — C-5 (FR-041…043).
971
+ *
972
+ * ```ts
973
+ * indexes: (c) => [
974
+ * index("orders_open")
975
+ * .on(c.col("status")) // kolon TİPLİ
976
+ * .where(c.col("deleted_at").isNull()) // partial
977
+ * .include(["total"]), // covering
978
+ * ]
979
+ * ```
980
+ *
981
+ * `policy()` ile AYNI dili konuşur: `where` politika ifade yapısının ta kendisi.
982
+ * Üçüncü bir filtre lehçesi açmak, aynı sorunun iki yazımı demekti.
983
+ *
984
+ * Kolon bağlamı callback'ten geliyor çünkü index tablo tanımının İÇİNDE
985
+ * bildiriliyor — `orders.status` yazmak, `orders` bağlaması o noktada henüz
986
+ * kurulmadığı için mümkün değil (T015'te `policies` için ölçülen aynı kısıt).
987
+ */
988
+ declare class IndexBuilder {
989
+ readonly _def: IndexDef;
990
+ constructor(name: string);
991
+ /** Index'in kolonları. SIRA KORUNUR — çok kolonlu bir index'in anlamı sırasıdır. */
992
+ on(...cols: PolicyExprRef[]): this;
993
+ /**
994
+ * İfade index'i (`lower(email)`), kolon yerine.
995
+ *
996
+ * Metin VERBATIM emit ediliyor — `policy().using("…")` ile aynı güven duruşu.
997
+ * Tipli yapı bunu ifade edemiyor ve kaçış kapağını kapatmak, yazarı elle
998
+ * migration yazmaya iterdi.
999
+ */
1000
+ onExpression(expr: string): this;
1001
+ /** Partial index koşulu — politika ifade dilinin ta kendisi. */
1002
+ where(expr: PolicyExprRef): this;
1003
+ /** Sıra. Tek yönlü index'te sıra genelde önemsizdir; ORDER BY ile eşleşen
1004
+ * bir index arandığında değildir. */
1005
+ desc(): this;
1006
+ asc(): this;
1007
+ /** NULL'ların yeri — `ORDER BY … NULLS LAST` ile eşleşmesi için. */
1008
+ nulls(where: "first" | "last"): this;
1009
+ /** Covering index (`INCLUDE`): index-only scan için taşınan ama
1010
+ * ARANMAYAN kolonlar. */
1011
+ include(cols: string[]): this;
1012
+ }
1013
+ declare function index(name: string): IndexBuilder;
1014
+ interface TableInput<C extends ColumnMap = ColumnMap> {
1015
+ columns: C;
1016
+ rls?: boolean;
1017
+ /**
1018
+ * Bu tablonun RLS politikaları — TABLONUN KENDİ KOLONLARININ bir fonksiyonu
1019
+ * olarak (FR-022).
1020
+ *
1021
+ * Callback olmasının sebebi tip: `columns`'tan çıkarılan `C`, TypeScript
1022
+ * tarafından buraya bağlamlandırılıyor, yani `p.col("ownerr")` yazım hatası
1023
+ * DERLEME hatası oluyor. Düz bir dizide `policy("x")` tek başına çağrıldığı
1024
+ * için hangi tabloda olduğunu bilemez, ve yanlış kolon adı ancak deploy
1025
+ * anında `CREATE POLICY` çalışırken Postgres'ten dönerdi. Bir RLS
1026
+ * politikasında yanlış kolon adı yetki hatasıdır; derlemede yakalanır.
1027
+ *
1028
+ * ```ts
1029
+ * policies: (p) => [
1030
+ * policy("owner_read").for("select").using(p.col("owner").eq(p.auth.uid())),
1031
+ * ]
1032
+ * ```
1033
+ */
1034
+ policies?: (p: PolicyExprCtx<Extract<keyof C, string>>) => (PolicyBuilder | PolicyDef)[];
1035
+ /** Composite/named primary key (ordered column names). Omit for single-column inline .primaryKey(). */
1036
+ primaryKey?: string[];
1037
+ /** Named multi-column UNIQUE constraints. */
1038
+ unique?: {
1039
+ name: string;
1040
+ columns: string[];
1041
+ }[];
1042
+ /** Named raw-SQL DDL objects (EXCLUDE, triggers, views) that the typed DSL cannot express. */
1043
+ raw?: RawConstraintDef[];
1044
+ /**
1045
+ * Named first-class CHECK constraints. Diffed by NAME with a BODY compare:
1046
+ * a changed `expr` (after pg normalization) recreates the constraint
1047
+ * (DROP + ADD). `expr` is trusted SQL emitted verbatim (like policy USING),
1048
+ * `name` is identifier-validated.
1049
+ */
1050
+ checks?: {
1051
+ name: string;
1052
+ expr: string;
1053
+ }[];
1054
+ /**
1055
+ * Plain (non-unique) btree indexes over an ordered column list, emitted as
1056
+ * standalone `CREATE INDEX [IF NOT EXISTS] name ON table (col1, col2)`
1057
+ * statements (NOT a table clause — a separate migration statement category).
1058
+ * Structural compare by NAME (no expression normalization). `name` and each
1059
+ * column are identifier-validated by the Go differ.
1060
+ *
1061
+ * Scope: columns-only plain btree. Partial (`where`) and expression indexes
1062
+ * are a deliberate follow-up — modelling them needs the same raw-SQL
1063
+ * normalization round-trip CHECK uses (Task 10), so they are NOT in this
1064
+ * type yet to avoid a half-working partial-index path.
1065
+ */
1066
+ indexes?: (p: PolicyExprCtx<Extract<keyof C, string>>) => (IndexBuilder | IndexDef)[];
1067
+ /**
1068
+ * appendOnly (FR-030): tablo yalnız INSERT kabul eder — UPDATE/DELETE hem
1069
+ * veritabanı yetkisiyle hem üretilen tipte reddedilir (T017/T018).
1070
+ * Ledger deseninin veri katmanından istediği TEK şey budur: bakiye türetilmiş
1071
+ * bir değerdir ve entry log'u değiştirilebilirse doğruluğunu kaybeder.
1072
+ */
1073
+ appendOnly?: boolean;
1074
+ /** Arama beyanı — bkz. SearchDecl. */
1075
+ search?: SearchDecl;
1076
+ /** Hafıza beyanı — bkz. MemoryDecl (FR-032). */
1077
+ memory?: MemoryDecl;
1078
+ }
1079
+ /**
1080
+ * A table definition — the runtime value the Go runtime's `schema_extract.js`
1081
+ * reads. It keys tables by `tableDef.name`, reads `tableDef.columns` for the
1082
+ * column DDL, and `tableDef.rls` + `tableDef.policies` for RLS.
1083
+ *
1084
+ * `defineSchema` derives `name` from the object key, so authors never repeat
1085
+ * the table name. `rls`/`policies` are always present after normalization
1086
+ * (defaulted to `true`/`[]`).
1087
+ *
1088
+ * The `C` type parameter preserves the precise per-column phantom types so that
1089
+ * downstream mapped types (InsertShape, RowShape) can discriminate on them.
1090
+ */
1091
+ interface TableDef<C extends ColumnMap = ColumnMap> {
1092
+ name: string;
1093
+ columns: C;
1094
+ rls: boolean;
1095
+ policies: PolicyDef[];
1096
+ primaryKey?: string[];
1097
+ unique?: {
1098
+ name: string;
1099
+ columns: string[];
1100
+ }[];
1101
+ /** Named raw-SQL DDL objects emitted verbatim on deploy. Tracked by name. */
1102
+ raw?: RawConstraintDef[];
1103
+ /** Named first-class CHECK constraints. Diffed by name + (normalized) body. */
1104
+ checks?: {
1105
+ name: string;
1106
+ expr: string;
1107
+ }[];
1108
+ /** Index bildirimleri; partial/expression/sıra/covering alanlarıyla (FR-041…043). */
1109
+ indexes?: IndexDef[];
1110
+ /** appendOnly (FR-030) — bkz. TableInput. */
1111
+ appendOnly?: boolean;
1112
+ /** Arama beyanı, doğrulanmış ve taşınmış hali. */
1113
+ search?: SearchDecl;
1114
+ /** Hafıza beyanı, doğrulanmış ve taşınmış hali (FR-032). */
1115
+ memory?: MemoryDecl;
1116
+ }
1117
+ /**
1118
+ * A schema definition containing multiple tables, keyed by table name.
1119
+ *
1120
+ * The `T` type parameter preserves the exact `TableDef<...>` type for each
1121
+ * table so that `SchemaDef["tables"]["rooms"]` resolves to the precise
1122
+ * `TableDef<{ id: ColumnBuilder<'uuid', false, true, never>; ... }>`.
1123
+ */
1124
+ interface SchemaDef<T extends Record<string, TableDef> = Record<string, TableDef>> {
1125
+ /** The schema's own name — `public`, `billing`, … Declared, never derived. */
1126
+ name: string;
1127
+ /** Reachable over HTTP? False unless the author opts in. */
1128
+ exposed: boolean;
1129
+ tables: T;
1130
+ /** Postgres extensions to install on deploy. Normalized to `[]` when absent. */
1131
+ extensions: PalbaseExtension[];
1132
+ }
1133
+ /** Map the author's `{ tables: { <name>: { columns } } }` input to the
1134
+ * `{ tables: { <name>: TableDef<columns> } }` runtime/type shape, threading the
1135
+ * per-table column map `T[K]["columns"]` so column-level inference survives. */
1136
+ type TablesByName<T extends readonly TableHandle[]> = {
1137
+ [K in T[number] as K[typeof TABLE_META]["name"]]: K[typeof TABLE_META];
1138
+ };
1139
+ /**
1140
+ * How a schema file reads. One file per schema — `db/public.ts`, `db/billing.ts`
1141
+ * — and the schema says its own name:
1142
+ *
1143
+ * const todos = defineTable("todos", {
1144
+ * columns: {
1145
+ * id: uuid().primaryKey().defaultRandom(),
1146
+ * owner: text().notNull(),
1147
+ * title: text().notNull(),
1148
+ * },
1149
+ * rls: true,
1150
+ * policies: [
1151
+ * policy("owner_all").for("all").to("authenticated")
1152
+ * .using("owner = (select auth.uid())")
1153
+ * .withCheck("owner = (select auth.uid())"),
1154
+ * ],
1155
+ * });
1156
+ *
1157
+ * export default defineSchema("public", { tables: [todos] });
1158
+ *
1159
+ * The dictionary form `defineSchema({ tables: { todos: {…} } })` is RETIRED and
1160
+ * refused by name: a table whose name comes from a key does not know what it is
1161
+ * called while it is being built, so it cannot resolve `references(() =>
1162
+ * other.id)`, and a schema that does not say its own name cannot tell one
1163
+ * `invoices` from another schema's `invoices`.
1164
+ *
1165
+ * The returned value is `{ name, tables: { todos: { name, columns, rls,
1166
+ * policies } }, extensions, exposed }` — the exact shape the runtime schema
1167
+ * extractor parses. Per-column phantom types are preserved so
1168
+ * `Database.public.todos.insert({...})` stays typed.
1169
+ *
1170
+ * RLS normalization: `rls` defaults to **`true`**, `policies` to `[]`. A table
1171
+ * that declares neither is therefore deny-all — nothing reads it until a policy
1172
+ * says who may, which is the safe starting point rather than a bug. Declare
1173
+ * `rls: false` for a genuinely public table; that is an explicit, greppable
1174
+ * statement of intent instead of an omission. When `policies` is non-empty,
1175
+ * `rls` is forced on (ENABLE + FORCE) regardless of the declared flag — a table
1176
+ * with policies must have RLS enabled or the policies would be inert.
1177
+ */
1178
+ /**
1179
+ * Where a table's metadata lives.
1180
+ *
1181
+ * The table VALUE is its columns, so `lists.id` is the column — which means a
1182
+ * column called `name` would overwrite the table's own name if metadata sat in
1183
+ * a plain field. Measured, not imagined: the existing `users` / `rooms` tests
1184
+ * (both declare a `name` column) produced an `[object Object]` table key.
1185
+ * `columns`, `rls`, `search`, `memory`, `unique` and `indexes` are all plausible
1186
+ * column names too. A column cannot collide with a symbol.
1187
+ */
1188
+ declare const TABLE_META: unique symbol;
1189
+ /** A value produced by `defineTable`: the columns, plus metadata behind the symbol. */
1190
+ type TableHandle<C extends ColumnMap = ColumnMap, N extends string = string> = C & {
1191
+ readonly [TABLE_META]: TableDef<C> & {
1192
+ name: N;
1193
+ };
1194
+ };
1195
+ /**
1196
+ * Declare one table as a standalone value that KNOWS ITS OWN NAME.
1197
+ *
1198
+ * The name lives here, not in a dictionary key. A table whose name comes from a
1199
+ * key cannot resolve `references(() => other.id)` when it is built — it does not
1200
+ * know what to call itself — which forces a two-phase resolution with an
1201
+ * empty-name intermediate state. One name source, no empty identity.
1202
+ *
1203
+ * The returned object spreads its own columns, so a sibling can point at it:
1204
+ * `references(() => lists.id)`.
1205
+ */
1206
+ declare function defineTable<const N extends string, C extends ColumnMap>(name: N, input: TableInput<C>): TableHandle<C, N>;
1207
+ /**
1208
+ * Collect declared tables into one schema.
1209
+ *
1210
+ * Takes an ARRAY, not a dictionary: the tables already carry their names, and a
1211
+ * second name source is a second thing that can disagree.
1212
+ */
1213
+ declare function defineSchema<const T extends readonly TableHandle[]>(name: string, input: {
1214
+ tables: T;
1215
+ exposed?: boolean;
1216
+ extensions?: PalbaseExtension[];
1217
+ }): SchemaDef<TablesByName<T>>;
1218
+
1219
+ /**
1220
+ * Local typed interfaces for the 9 Palbase module clients injected into
1221
+ * every endpoint context via `ctx.auth`, `ctx.storage`, etc.
1222
+ *
1223
+ * Design intent
1224
+ * ─────────────
1225
+ * • @palbase/backend owns this contract — no runtime or type dependency on
1226
+ * @palbase/server or any module package.
1227
+ * • These interfaces are STRUCTURALLY compatible with the runtime's
1228
+ * ServerClient module objects so assignment is valid without a cast.
1229
+ * • All parameter and return types are defined LOCALLY here (not imported
1230
+ * from modules/) to keep the package self-contained.
1231
+ *
1232
+ * Privilege note
1233
+ * ──────────────
1234
+ * These clients run with the project's managed-runtime (privileged) key —
1235
+ * they bypass end-user RLS; intentional for server handlers. Treat every
1236
+ * call as if it has admin access to the project's data.
1237
+ */
1238
+
1239
+ /** A user as returned by auth verifyUserToken. */
1240
+ interface PalbaseUser {
1241
+ id: string;
1242
+ email: string;
1243
+ emailVerified?: boolean;
1244
+ createdAt?: string;
1245
+ updatedAt?: string;
1246
+ metadata?: Record<string, unknown>;
1247
+ }
1248
+ /** A session as returned by auth.getSession(). */
1249
+ interface PalbaseSession {
1250
+ accessToken: string;
1251
+ refreshToken: string;
1252
+ expiresAt: number;
1253
+ }
1254
+ /** MFA enroll result. */
1255
+ interface PalbaseMFAEnrollResult {
1256
+ enrollment_id?: string;
1257
+ secret?: string;
1258
+ otp_url?: string;
1259
+ qr_code?: string;
1260
+ recovery_codes?: string[];
1261
+ status?: string;
1262
+ }
1263
+ /** MFA token response. */
1264
+ interface PalbaseTokenResponse {
1265
+ access_token: string;
1266
+ refresh_token: string;
1267
+ token_type: string;
1268
+ expires_in: number;
1269
+ }
1270
+ /** MFA factor. */
1271
+ interface PalbaseMFAFactor {
1272
+ id: string;
1273
+ type: string;
1274
+ verified: boolean;
1275
+ created_at: string;
1276
+ }
1277
+ /** Device token view returned by registerDevice. */
1278
+ interface PalbaseDeviceTokenView {
1279
+ id: string;
1280
+ device_id: string;
1281
+ platform: "android" | "ios" | "web";
1282
+ status: "active" | "inactive";
1283
+ created_at: string;
1284
+ updated_at: string;
1285
+ }
1286
+ /** Device info item returned by device.list(). */
1287
+ interface PalbaseDeviceInfo {
1288
+ id: string;
1289
+ platform: string;
1290
+ attestation_status: string;
1291
+ bound: boolean;
1292
+ created_at: string;
1293
+ }
1294
+ /** Params for device.attestAndroid(). */
1295
+ interface PalbaseAttestAndroidParams {
1296
+ verdict_token: string;
1297
+ }
1298
+ /** Result of device.attestAndroid(). */
1299
+ interface PalbaseAttestAndroidResult {
1300
+ device_id: string;
1301
+ attestation_status: string;
1302
+ device_integrity?: string;
1303
+ }
1304
+ /** Params for device.attestiOS(). */
1305
+ interface PalbaseAttestiOSParams {
1306
+ attestation_object: string;
1307
+ key_id: string;
1308
+ challenge: string;
1309
+ }
1310
+ /** Result of device.attestiOS(). */
1311
+ interface PalbaseAttestiOSResult {
1312
+ device_id: string;
1313
+ attestation_status: string;
1314
+ }
1315
+ /** Params for device.bind(). */
1316
+ interface PalbaseBindDeviceParams {
1317
+ device_id: string;
1318
+ public_key: string;
1319
+ platform_attestation?: string;
1320
+ }
1321
+ /** Params for device.verifyRequestSignature() (server-only). */
1322
+ interface PalbaseVerifyRequestSignatureParams {
1323
+ payload: string;
1324
+ signature: string;
1325
+ }
1326
+ /** Flag context (user targeting). */
1327
+ interface PalbaseFlagContext {
1328
+ userId?: string;
1329
+ properties?: Record<string, unknown>;
1330
+ }
1331
+ /** Feature flag variant. */
1332
+ interface PalbaseFlagVariant {
1333
+ name: string;
1334
+ payload?: unknown;
1335
+ }
1336
+ /** Feature flag (getAll result item). */
1337
+ interface PalbaseFlag {
1338
+ name: string;
1339
+ enabled: boolean;
1340
+ variant?: PalbaseFlagVariant;
1341
+ }
1342
+ /**
1343
+ * Any JSON value a feature flag (or per-user override) can hold once resolved.
1344
+ * Mirrors the wire shape the user-flags module stores and returns.
1345
+ */
1346
+ type PalbaseFlagValue = boolean | number | string | null | PalbaseFlagValue[] | {
1347
+ [key: string]: PalbaseFlagValue;
1348
+ };
1349
+ /** Where a resolved flag value came from. */
1350
+ type PalbaseFlagSource = "system" | "user";
1351
+ /** Result of {@link PalbaseFlagsClient.setOverride} /
1352
+ * {@link PalbaseFlagsServiceClient.setOverrideForUser}. */
1353
+ interface PalbaseSetOverrideResult {
1354
+ key: string;
1355
+ value: PalbaseFlagValue;
1356
+ source: PalbaseFlagSource;
1357
+ }
1358
+ /** Result of {@link PalbaseFlagsServiceClient.setOverridesForUser}. */
1359
+ interface PalbaseSetOverridesResult {
1360
+ values: Record<string, PalbaseFlagValue>;
1361
+ }
1362
+ /** Result of {@link PalbaseFlagsServiceClient.clearOverrideForUser} — `value`
1363
+ * is the system default the user falls back to. */
1364
+ interface PalbaseClearOverrideResult {
1365
+ key: string;
1366
+ value: PalbaseFlagValue;
1367
+ source: PalbaseFlagSource;
1368
+ }
1369
+ /** Result of {@link PalbaseFlagsServiceClient.clearAllOverridesForUser}. */
1370
+ interface PalbaseClearAllOverridesResult {
1371
+ deleted: number;
1372
+ }
1373
+ /** One cross-user operation for {@link PalbaseFlagsServiceClient.batchSetOverrides}. */
1374
+ interface PalbaseBatchOverrideOperation {
1375
+ userId: string;
1376
+ values: Record<string, PalbaseFlagValue>;
1377
+ }
1378
+ /** Result of {@link PalbaseFlagsServiceClient.batchSetOverrides}. */
1379
+ interface PalbaseBatchSetOverridesResult {
1380
+ applied: number;
1381
+ }
1382
+ /** File object returned by storage operations. */
1383
+ interface PalbaseFileObject {
1384
+ /** The object's path inside its bucket. `name` is the same value. */
1385
+ path: string;
1386
+ name: string;
1387
+ bucket: string;
1388
+ size: number;
1389
+ contentType: string;
1390
+ /** SHA-256 of the stored bytes, computed as they were written. */
1391
+ checksum: string;
1392
+ /** Present for images: the decoded dimensions of the ORIGINAL. */
1393
+ width?: number;
1394
+ height?: number;
1395
+ /**
1396
+ * A ThumbHash — a few dozen bytes that render as a blurred preview while the
1397
+ * real image loads, with no second request to fetch it.
1398
+ */
1399
+ thumbhash?: string;
1400
+ /**
1401
+ * Declared rendition name → the URL that serves it. Produced at upload from
1402
+ * what `config/storage.ts` declared, so this is a lookup rather than a
1403
+ * transform anyone pays for per request.
1404
+ */
1405
+ variants: Record<string, string>;
1406
+ }
1407
+ /** Storage signed URL response. */
1408
+ interface PalbaseSignedUrlResponse {
1409
+ signedUrl: string;
1410
+ }
1411
+ /** Storage upload options. */
1412
+ interface PalbaseUploadOptions {
1413
+ contentType?: string;
1414
+ upsert?: boolean;
1415
+ }
1416
+ /** Storage transform options for public URLs. */
1417
+ interface PalbaseTransformOptions {
1418
+ width?: number;
1419
+ height?: number;
1420
+ format?: "webp" | "avif" | "jpeg" | "png";
1421
+ }
1422
+ /** Storage list options. */
1423
+ interface PalbaseListOptions {
1424
+ limit?: number;
1425
+ offset?: number;
1426
+ sortBy?: {
1427
+ column: string;
1428
+ order: "asc" | "desc";
1429
+ };
1430
+ }
1431
+ /** Edge-function invoke options. */
1432
+ interface PalbaseInvokeOptions {
1433
+ body?: unknown;
1434
+ headers?: Record<string, string>;
1435
+ method?: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
1436
+ }
1437
+ /** Push recipient — user ID, array of user IDs, or a topic. */
1438
+ type PalbasePushRecipient = string | string[] | {
1439
+ topic: string;
1440
+ };
1441
+ /** Localized text (plain or locale map). */
1442
+ type PalbaseLocalizedText = string | Record<string, string>;
1443
+ /** Push send params. */
1444
+ interface PalbasePushSendParams {
1445
+ to: PalbasePushRecipient;
1446
+ title?: PalbaseLocalizedText;
1447
+ body?: PalbaseLocalizedText;
1448
+ variables?: Record<string, string>;
1449
+ default_locale?: string;
1450
+ data?: Record<string, string>;
1451
+ image?: string;
1452
+ badge?: number;
1453
+ sound?: string;
1454
+ deep_link?: string;
1455
+ collapse_key?: string;
1456
+ priority?: "high" | "normal";
1457
+ ttl?: number;
1458
+ silent?: boolean;
1459
+ content_available?: boolean;
1460
+ category?: string;
1461
+ metadata?: unknown;
1462
+ channels?: Array<"push" | "inbox">;
1463
+ inbox_action_url?: string;
1464
+ }
1465
+ /** Push send response. */
1466
+ interface PalbasePushSendResponse {
1467
+ message_id?: string;
1468
+ message_ids?: string[];
1469
+ recipients: number;
1470
+ }
1471
+ /** Email send params. */
1472
+ interface PalbaseEmailSendParams {
1473
+ to: string | string[];
1474
+ subject?: string;
1475
+ /**
1476
+ * @deprecated The server reads `template_slug`, not `template` — this
1477
+ * field is forwarded as-is and ignored by palnotify. Use `templateSlug`
1478
+ * instead. Retained for source-compat with older callers.
1479
+ */
1480
+ template?: string;
1481
+ /**
1482
+ * Server email-template slug. The notifications client maps this to the
1483
+ * wire field `template_slug` (mutually exclusive with `subject` + body
1484
+ * fields when not overriding template output).
1485
+ */
1486
+ templateSlug?: string;
1487
+ /**
1488
+ * Which locale of `templateSlug` to render — a BCP47 short tag ("tr").
1489
+ * Falls back to "en" when the slug has no row for it. Declaring a locale in
1490
+ * config/notifications.ts is only half the feature: without this the extra
1491
+ * rows exist and are unreachable.
1492
+ */
1493
+ locale?: string;
1494
+ variables?: Record<string, unknown>;
1495
+ html?: string;
1496
+ text?: string;
1497
+ from?: {
1498
+ email: string;
1499
+ name?: string;
1500
+ };
1501
+ reply_to?: string;
1502
+ category?: string;
1503
+ }
1504
+ /** Email send response. */
1505
+ interface PalbaseEmailSendResponse {
1506
+ message_id?: string;
1507
+ message_ids?: string[];
1508
+ }
1509
+ /** SMS send params. */
1510
+ interface PalbaseSmsSendParams {
1511
+ to: string | string[];
1512
+ /**
1513
+ * SMS body text. Either `body` OR `templateSlug` must be supplied —
1514
+ * the two are mutually exclusive at the server.
1515
+ */
1516
+ body?: string;
1517
+ /**
1518
+ * Server SMS-template slug. The notifications client maps this to the
1519
+ * wire field `template_slug`. When provided, palnotify renders the
1520
+ * template with `variables` and uses the result as the SMS body.
1521
+ */
1522
+ templateSlug?: string;
1523
+ /** Which locale of `templateSlug` to render — see PalbaseEmailSendParams.locale. */
1524
+ locale?: string;
1525
+ variables?: Record<string, unknown>;
1526
+ category?: string;
1527
+ }
1528
+ /**
1529
+ * Phone-verification start parameters.
1530
+ *
1531
+ * There is no body field, by construction: the provider generates the code AND
1532
+ * the message text from its own template. A caller-supplied body would be
1533
+ * silently dropped — a send that reports success while delivering something the
1534
+ * caller never wrote.
1535
+ */
1536
+ interface PalbaseVerificationStartParams {
1537
+ /** E.164 recipient, e.g. "+14155551212". */
1538
+ to: string;
1539
+ /** Template language (e.g. "tr", "en"). Empty uses the service default. */
1540
+ locale?: string;
1541
+ /** Optional user attribution for the message log. */
1542
+ user_id?: string;
1543
+ }
1544
+ /** Phone-verification start response. The code is never included. */
1545
+ interface PalbaseVerificationStartResponse {
1546
+ /** Provider-side handle for this attempt. Not a secret, and not the code. */
1547
+ verification_sid: string;
1548
+ /** Provider lifecycle value, typically "pending". */
1549
+ status: string;
1550
+ to: string;
1551
+ /** palnotify message-log row id, so the send appears in message history. */
1552
+ message_id?: string;
1553
+ }
1554
+ /** Phone-verification check parameters. */
1555
+ interface PalbaseVerificationCheckParams {
1556
+ to: string;
1557
+ code: string;
1558
+ }
1559
+ /**
1560
+ * Phone-verification verdict. A wrong or expired code is `approved: false` with
1561
+ * a successful HTTP status — a mismatch is a normal outcome, not an error.
1562
+ */
1563
+ interface PalbaseVerificationCheckResponse {
1564
+ approved: boolean;
1565
+ }
1566
+ /** SMS send response. */
1567
+ interface PalbaseSmsSendResponse {
1568
+ message_id?: string;
1569
+ message_ids?: string[];
1570
+ }
1571
+ /** Inbox send params (managed-runtime: create inbox row for a user). */
1572
+ interface PalbaseInboxSendParams {
1573
+ to: string;
1574
+ title?: string;
1575
+ body: string;
1576
+ data?: unknown;
1577
+ action_url?: string;
1578
+ category?: string;
1579
+ channels?: Array<"push" | "inbox">;
1580
+ push_deep_link?: string;
1581
+ }
1582
+ /** Inbox send response. */
1583
+ interface PalbaseInboxSendResponse {
1584
+ message_id?: string;
1585
+ skipped?: boolean;
1586
+ }
1587
+ /** A per-channel status entry in a multi-channel response. */
1588
+ interface PalbaseChannelOutcome {
1589
+ status: "queued" | "sent" | "skipped" | "failed";
1590
+ message_id?: string;
1591
+ message_ids?: string[];
1592
+ recipients?: number;
1593
+ error?: string;
1594
+ }
1595
+ /** Multi-channel fan-out response. */
1596
+ interface PalbaseMultiChannelResponse {
1597
+ channels: Record<string, PalbaseChannelOutcome>;
1598
+ }
1599
+ /** An inbox message as returned by inbox.list. */
1600
+ interface PalbaseInboxMessage {
1601
+ id: string;
1602
+ user_id?: string;
1603
+ title?: string;
1604
+ body: string;
1605
+ data?: unknown;
1606
+ action_url?: string;
1607
+ category?: string;
1608
+ is_read: boolean;
1609
+ read_at?: string;
1610
+ created_at: string;
1611
+ }
1612
+ /** Inbox list options. */
1613
+ interface PalbaseInboxListOptions {
1614
+ cursor?: string;
1615
+ limit?: number;
1616
+ is_read?: boolean;
1617
+ category?: string;
1618
+ include_archived?: boolean;
1619
+ }
1620
+ /** Inbox list result. */
1621
+ interface PalbaseInboxListResult {
1622
+ messages: PalbaseInboxMessage[];
1623
+ next_cursor?: string;
1624
+ }
1625
+ /** Notification preferences (channel × category opt-in/out). */
1626
+ type PalbasePreferences = Partial<Record<"push" | "email" | "sms" | "inbox", Record<string, boolean>>>;
1627
+ /** Register device params. */
1628
+ interface PalbaseRegisterDeviceParams {
1629
+ device_id: string;
1630
+ token: string;
1631
+ platform: "android" | "ios" | "web";
1632
+ app_version?: string;
1633
+ locale?: string;
1634
+ }
1635
+ /** Email template view (camelCase). */
1636
+ interface PalbaseEmailTemplate {
1637
+ id: string;
1638
+ slug: string;
1639
+ locale: string;
1640
+ subject: string;
1641
+ htmlBody: string;
1642
+ textBody?: string;
1643
+ variables: string[];
1644
+ isDefault: boolean;
1645
+ createdAt: string;
1646
+ updatedAt: string;
1647
+ }
1648
+ /** Payload for templates.email.create. */
1649
+ interface PalbaseCreateEmailTemplateInput {
1650
+ slug: string;
1651
+ locale?: string;
1652
+ subject: string;
1653
+ htmlBody: string;
1654
+ textBody?: string;
1655
+ variables?: string[];
1656
+ }
1657
+ /** Payload for templates.email.update — patch semantics. */
1658
+ interface PalbaseUpdateEmailTemplateInput {
1659
+ subject?: string;
1660
+ htmlBody?: string;
1661
+ textBody?: string;
1662
+ variables?: string[];
1663
+ }
1664
+ /** SMS template view (camelCase). No subject / html / text distinction. */
1665
+ interface PalbaseSMSTemplate {
1666
+ id: string;
1667
+ slug: string;
1668
+ locale: string;
1669
+ body: string;
1670
+ variables: string[];
1671
+ isDefault: boolean;
1672
+ createdAt: string;
1673
+ updatedAt: string;
1674
+ }
1675
+ /** Payload for templates.sms.create. */
1676
+ interface PalbaseCreateSMSTemplateInput {
1677
+ slug: string;
1678
+ locale?: string;
1679
+ body: string;
1680
+ variables?: string[];
1681
+ }
1682
+ /** Payload for templates.sms.update — patch semantics. */
1683
+ interface PalbaseUpdateSMSTemplateInput {
1684
+ body?: string;
1685
+ variables?: string[];
1686
+ }
1687
+ /** Analytics event properties. */
1688
+ type PalbaseAnalyticsProperties = Record<string, unknown>;
1689
+ /** Analytics identify traits. */
1690
+ type PalbaseIdentifyTraits = Record<string, unknown>;
1691
+ /** Analytics count query input. */
1692
+ interface PalbaseCountQueryInput {
1693
+ eventName?: string;
1694
+ eventNames?: string[];
1695
+ from: number;
1696
+ to: number;
1697
+ interval?: "hour" | "day";
1698
+ filters?: Record<string, string>;
1699
+ breakdown?: string;
1700
+ }
1701
+ /** A single time-bucket in a count result. */
1702
+ interface PalbaseCountBucket {
1703
+ t: number;
1704
+ count: number;
1705
+ breakdown?: string;
1706
+ }
1707
+ /** Count query result. */
1708
+ interface PalbaseCountResult {
1709
+ series: PalbaseCountBucket[];
1710
+ from_mv: boolean;
1711
+ }
1712
+ /** Events query input. */
1713
+ interface PalbaseEventsQueryInput {
1714
+ from: number;
1715
+ to: number;
1716
+ eventName?: string;
1717
+ distinctId?: string;
1718
+ limit?: number;
1719
+ cursor?: string;
1720
+ }
1721
+ /** A single event row. */
1722
+ interface PalbaseEventRow {
1723
+ event_id: string;
1724
+ timestamp: number;
1725
+ event_name: string;
1726
+ distinct_id: string;
1727
+ properties: Record<string, unknown>;
1728
+ }
1729
+ /** Events query result. */
1730
+ interface PalbaseEventsResult {
1731
+ events: PalbaseEventRow[];
1732
+ next_cursor?: string;
1733
+ }
1734
+ /** Properties query input. */
1735
+ interface PalbasePropertiesQueryInput {
1736
+ eventName?: string;
1737
+ from?: number;
1738
+ to?: number;
1739
+ }
1740
+ /** Property descriptor (from /query/properties). */
1741
+ interface PalbasePropertyDescriptor {
1742
+ name: string;
1743
+ value_count: number;
1744
+ }
1745
+ /** Users query input. */
1746
+ interface PalbaseUsersQueryInput {
1747
+ from: number;
1748
+ to: number;
1749
+ filters?: Record<string, string>;
1750
+ limit?: number;
1751
+ cursor?: string;
1752
+ }
1753
+ /** A single user row. */
1754
+ interface PalbaseUserRow {
1755
+ distinct_id: string;
1756
+ first_seen: number;
1757
+ last_seen: number;
1758
+ event_count: number;
1759
+ properties?: Record<string, unknown>;
1760
+ }
1761
+ /** Users query result. */
1762
+ interface PalbaseUsersResult {
1763
+ users: PalbaseUserRow[];
1764
+ next_cursor?: string;
1765
+ }
1766
+ /** Funnel query input. */
1767
+ interface PalbaseFunnelQueryInput {
1768
+ steps: Array<{
1769
+ event_name: string;
1770
+ filters?: Record<string, string>;
1771
+ }>;
1772
+ from: number;
1773
+ to: number;
1774
+ conversionWindowSeconds?: number;
1775
+ breakdown?: string;
1776
+ }
1777
+ /** Funnel step result. */
1778
+ interface PalbaseFunnelStepResult {
1779
+ event_name: string;
1780
+ count: number;
1781
+ conversion_rate: number;
1782
+ }
1783
+ /** Funnel query result. */
1784
+ interface PalbaseFunnelResult {
1785
+ steps: PalbaseFunnelStepResult[];
1786
+ overall_conversion_rate: number;
1787
+ }
1788
+ /** Retention query input. */
1789
+ interface PalbaseRetentionQueryInput {
1790
+ firstEvent: string;
1791
+ returnEvent: string;
1792
+ from: number;
1793
+ to: number;
1794
+ periodDays?: number;
1795
+ periods?: number;
1796
+ }
1797
+ /** Retention cohort result item. */
1798
+ interface PalbaseRetentionCohort {
1799
+ cohort_start: number;
1800
+ size: number;
1801
+ periods: number[];
1802
+ }
1803
+ /** Retention query result. */
1804
+ interface PalbaseRetentionResult {
1805
+ cohorts: PalbaseRetentionCohort[];
1806
+ }
1807
+ /** Cohort query input. */
1808
+ interface PalbaseCohortQueryInput {
1809
+ name: string;
1810
+ rules: Array<{
1811
+ event_name: string;
1812
+ filters?: Record<string, string>;
1813
+ }>;
1814
+ from: number;
1815
+ to: number;
1816
+ }
1817
+ /** Cohort query result. */
1818
+ interface PalbaseCohortResult {
1819
+ size: number;
1820
+ distinct_ids_sample: string[];
1821
+ }
1822
+ /** Analytics overview result. */
1823
+ interface PalbaseOverviewResult {
1824
+ dau: number;
1825
+ wau: number;
1826
+ mau: number;
1827
+ total_events: number;
1828
+ top_events: Array<{
1829
+ event_name: string;
1830
+ count: number;
1831
+ }>;
1832
+ }
1833
+ /** Event names result. */
1834
+ interface PalbaseEventNamesResult {
1835
+ names: string[];
1836
+ }
1837
+ /** User detail result. */
1838
+ interface PalbaseUserDetailResult {
1839
+ distinct_id: string;
1840
+ first_seen: number;
1841
+ last_seen: number;
1842
+ event_count: number;
1843
+ properties: Record<string, unknown>;
1844
+ recent_events: PalbaseEventRow[];
1845
+ }
1846
+ /** Create link params. */
1847
+ interface PalbaseCreateLinkParams {
1848
+ url: string;
1849
+ title?: string;
1850
+ description?: string;
1851
+ imageUrl?: string;
1852
+ ios?: {
1853
+ bundleId: string;
1854
+ appStoreId?: string;
1855
+ fallbackUrl?: string;
1856
+ minimumVersion?: string;
1857
+ };
1858
+ android?: {
1859
+ packageName: string;
1860
+ fallbackUrl?: string;
1861
+ minimumVersion?: number;
1862
+ };
1863
+ web?: {
1864
+ fallbackUrl?: string;
1865
+ };
1866
+ social?: {
1867
+ title?: string;
1868
+ description?: string;
1869
+ imageUrl?: string;
1870
+ };
1871
+ expiresAt?: string;
1872
+ customShortCode?: string;
1873
+ }
1874
+ /** Update link params. */
1875
+ interface PalbaseUpdateLinkParams {
1876
+ url?: string;
1877
+ title?: string;
1878
+ description?: string;
1879
+ imageUrl?: string;
1880
+ ios?: {
1881
+ bundleId?: string;
1882
+ appStoreId?: string;
1883
+ fallbackUrl?: string;
1884
+ minimumVersion?: string;
1885
+ };
1886
+ android?: {
1887
+ packageName?: string;
1888
+ fallbackUrl?: string;
1889
+ minimumVersion?: number;
1890
+ };
1891
+ web?: {
1892
+ fallbackUrl?: string;
1893
+ };
1894
+ social?: {
1895
+ title?: string;
1896
+ description?: string;
1897
+ imageUrl?: string;
1898
+ };
1899
+ expiresAt?: string;
1900
+ }
1901
+ /** A link object. */
1902
+ interface PalbaseLink {
1903
+ id: string;
1904
+ shortCode: string;
1905
+ shortUrl: string;
1906
+ url: string;
1907
+ title?: string;
1908
+ clickCount: number;
1909
+ createdAt: string;
1910
+ }
1911
+ /** Link details (extended link). */
1912
+ interface PalbaseLinkDetails extends PalbaseLink {
1913
+ description?: string;
1914
+ imageUrl?: string;
1915
+ ios?: Record<string, unknown>;
1916
+ android?: Record<string, unknown>;
1917
+ web?: Record<string, unknown>;
1918
+ social?: Record<string, unknown>;
1919
+ expiresAt?: string;
1920
+ }
1921
+ /** Link analytics. */
1922
+ interface PalbaseLinkAnalytics {
1923
+ totalClicks: number;
1924
+ clicksByPlatform: Record<string, number>;
1925
+ clicksByCountry: Record<string, number>;
1926
+ clicksByDay: Array<{
1927
+ date: string;
1928
+ clicks: number;
1929
+ }>;
1930
+ }
1931
+ /** QR code options. */
1932
+ interface PalbaseQrCodeOptions {
1933
+ size?: number;
1934
+ format?: "png" | "svg";
1935
+ }
1936
+ /** Match params for deferred deep link resolution. */
1937
+ interface PalbaseMatchParams {
1938
+ fingerprintHash: string;
1939
+ }
1940
+ /** Initial deep link. */
1941
+ interface PalbaseInitialLink {
1942
+ url: string;
1943
+ params?: Record<string, string>;
1944
+ }
1945
+ /** List links options. */
1946
+ interface PalbaseListLinksOptions {
1947
+ limit?: number;
1948
+ offset?: number;
1949
+ }
1950
+ /** List links result. */
1951
+ interface PalbaseListLinksResult {
1952
+ links: PalbaseLink[];
1953
+ total: number;
1954
+ }
1955
+ /**
1956
+ * Auth client surface available on `ctx.auth`.
1957
+ * Exposes server-relevant methods only. Browser-only patterns are omitted:
1958
+ * — signUp / signIn / signOut / refresh / requestPasswordReset / confirmPasswordReset
1959
+ * / changePassword / resendVerification / verifyEmail (client flow helpers)
1960
+ * — onAuthStateChange / onTokenChange (subscription callbacks)
1961
+ * — getOAuthURL / signInWithCredential (OAuth browser redirects)
1962
+ * — requestMagicLink / verifyMagicLink (client flow helpers)
1963
+ * — setTokens / getAccessToken (internal token management)
1964
+ * — listSessions / revokeSession / revokeAllSessions (user-self management)
1965
+ * — listIdentities / linkIdentity / unlinkIdentity (user-self management)
1966
+ * — listTrustedDevices / registerTrustedDevice / revokeTrustedDevice (user-self management)
1967
+ */
1968
+ interface PalbaseAuthClient {
1969
+ /**
1970
+ * Verify a user's JWT by calling GET /auth/user. Returns the user if the
1971
+ * token is valid; error otherwise. Service-role privileged.
1972
+ */
1973
+ verifyUserToken(jwt: string): Promise<PalbaseResult<PalbaseUser>>;
1974
+ /**
1975
+ * Get the current session held by the client (synchronous — no network
1976
+ * call). On the server the managed-runtime client does not hold a user
1977
+ * session; this always returns `{ data: null, error: null }`.
1978
+ */
1979
+ getSession(): {
1980
+ data: PalbaseSession | null;
1981
+ error: null;
1982
+ };
1983
+ /** MFA admin surface — enroll/verify/manage factors on behalf of users. */
1984
+ mfa: {
1985
+ /** Enroll a new MFA factor (TOTP or email). */
1986
+ enroll(params: {
1987
+ type: "totp" | "email";
1988
+ }): Promise<PalbaseResult<PalbaseMFAEnrollResult>>;
1989
+ /** Verify an enrollment code. */
1990
+ verifyEnrollment(code: string): Promise<PalbaseResult<{
1991
+ status: string;
1992
+ }>>;
1993
+ /** Challenge an MFA factor (verify code, obtain token). */
1994
+ challenge(params: {
1995
+ mfa_token: string;
1996
+ type: "totp" | "email";
1997
+ code: string;
1998
+ }): Promise<PalbaseResult<PalbaseTokenResponse>>;
1999
+ /** Recover via backup code. */
2000
+ recovery(params: {
2001
+ mfa_token: string;
2002
+ code: string;
2003
+ }): Promise<PalbaseResult<PalbaseTokenResponse>>;
2004
+ /** List enrolled factors. */
2005
+ listFactors(): Promise<PalbaseResult<{
2006
+ factors: PalbaseMFAFactor[];
2007
+ }>>;
2008
+ /** Remove a factor. Requires current password. */
2009
+ removeFactor(factorId: string, currentPassword: string): Promise<PalbaseResult<{
2010
+ status: string;
2011
+ }>>;
2012
+ /** Regenerate recovery codes. */
2013
+ regenerateRecoveryCodes(): Promise<PalbaseResult<{
2014
+ recovery_codes: string[];
2015
+ }>>;
2016
+ /** Email MFA: start enrollment. */
2017
+ emailEnroll(): Promise<PalbaseResult<{
2018
+ status: string;
2019
+ }>>;
2020
+ /** Email MFA: send challenge. */
2021
+ emailChallenge(params: {
2022
+ mfa_token: string;
2023
+ }): Promise<PalbaseResult<{
2024
+ status: string;
2025
+ }>>;
2026
+ /** Email MFA: verify code. */
2027
+ emailVerify(params: {
2028
+ mfa_token: string;
2029
+ code: string;
2030
+ }): Promise<PalbaseResult<PalbaseTokenResponse>>;
2031
+ };
2032
+ /** Device attestation surface (App Attest / Play Integrity). */
2033
+ device: {
2034
+ /** Generate a device attestation challenge. */
2035
+ generateChallenge(): Promise<PalbaseResult<{
2036
+ challenge: string;
2037
+ }>>;
2038
+ /** Attest an Android device with a Play Integrity verdict token. */
2039
+ attestAndroid(params: PalbaseAttestAndroidParams): Promise<PalbaseResult<PalbaseAttestAndroidResult>>;
2040
+ /** Attest an iOS device with App Attest attestation data. */
2041
+ attestiOS(params: PalbaseAttestiOSParams): Promise<PalbaseResult<PalbaseAttestiOSResult>>;
2042
+ /** Bind a verified device with a public key for request signing. */
2043
+ bind(params: PalbaseBindDeviceParams): Promise<PalbaseResult<{
2044
+ success: boolean;
2045
+ }>>;
2046
+ /** List all devices for the current user. */
2047
+ list(): Promise<PalbaseResult<{
2048
+ devices: PalbaseDeviceInfo[];
2049
+ }>>;
2050
+ /** Delete a device by ID. */
2051
+ delete(deviceId: string): Promise<PalbaseResult<{
2052
+ success: boolean;
2053
+ }>>;
2054
+ /**
2055
+ * Verify a request signature from a device (server-only).
2056
+ * Not exposed in the client SDK.
2057
+ */
2058
+ verifyRequestSignature(deviceId: string, params: PalbaseVerifyRequestSignatureParams): Promise<PalbaseResult<{
2059
+ verified: boolean;
2060
+ }>>;
2061
+ /** Get the cached App Check token, or null if not available / expired. */
2062
+ getToken(): string | null;
2063
+ /** Whether App Check is active (token cached and not expired). */
2064
+ readonly isActive: boolean;
2065
+ /** Set a cached App Check token manually (e.g. after attest flow). */
2066
+ setCachedToken(token: string, expiresInMs: number): void;
2067
+ /** Clean up timers and cached state. */
2068
+ dispose(): void;
2069
+ };
2070
+ }
2071
+ /**
2072
+ * Bucket-level file operations available via `ctx.storage.bucket(name)`.
2073
+ * `getPublicUrl` is synchronous (no network call — constructs URL locally).
2074
+ */
2075
+ interface PalbaseBucketClient {
2076
+ /** Upload a file. */
2077
+ upload(path: string, file: Blob | ArrayBuffer | ReadableStream, options?: PalbaseUploadOptions): Promise<PalbaseResult<PalbaseFileObject>>;
2078
+ /** Download a file as a Blob. */
2079
+ download(path: string): Promise<PalbaseResult<Blob>>;
2080
+ /**
2081
+ * The public URL for an object, or for one of its declared renditions.
2082
+ *
2083
+ * `variant` names a rendition from `config/storage.ts` — NOT arbitrary
2084
+ * dimensions. Renditions are produced at upload and served as immutable
2085
+ * objects, so a URL here costs nothing to serve and caches forever; an
2086
+ * arbitrary size would be a transform on the request path, one public URL
2087
+ * away from a CPU amplification attack.
2088
+ *
2089
+ * Asking for a variant the bucket does not declare is an error rather than a
2090
+ * silent fall back to the original: an app that requested a 200px thumbnail
2091
+ * and quietly received a 5 MB photograph works, looks right, and is found
2092
+ * months later on a bandwidth bill.
2093
+ */
2094
+ getPublicUrl(path: string, options?: {
2095
+ variant?: string;
2096
+ }): string;
2097
+ /**
2098
+ * A time-limited URL for a private object.
2099
+ *
2100
+ * The duration is NAMED (`{ expiresIn: "1h" }`) rather than a bare number,
2101
+ * because "3600" reads as seconds to whoever wrote it and as minutes to
2102
+ * whoever maintains it. Same human spelling `fileSizeLimit` takes.
2103
+ */
2104
+ createSignedUrl(path: string, options: {
2105
+ expiresIn: string;
2106
+ }): Promise<PalbaseResult<PalbaseSignedUrlResponse>>;
2107
+ /** List objects in the bucket (optionally filtered by prefix). */
2108
+ list(prefix?: string, options?: PalbaseListOptions): Promise<PalbaseResult<PalbaseFileObject[]>>;
2109
+ /** Delete one or more objects. */
2110
+ remove(paths: string[]): Promise<PalbaseResult<PalbaseFileObject[]>>;
2111
+ /** Move / rename an object. */
2112
+ move(from: string, to: string): Promise<PalbaseResult<void>>;
2113
+ /** Copy an object. */
2114
+ copy(from: string, to: string): Promise<PalbaseResult<void>>;
2115
+ }
2116
+ /**
2117
+ * Storage client available on `ctx.storage`.
2118
+ * Only `bucket()` is exposed — managed-runtime callers select a bucket first.
2119
+ */
2120
+ interface PalbaseStorageClient {
2121
+ /** Get a bucket-scoped client for file operations. */
2122
+ bucket(name: string): PalbaseBucketClient;
2123
+ }
2124
+ /**
2125
+ * Functions client available on `ctx.functions`.
2126
+ * Invoke edge functions from a backend endpoint.
2127
+ */
2128
+ interface PalbaseFunctionsClient {
2129
+ /** Invoke a named edge function. */
2130
+ invoke<T = unknown>(fnName: string, options?: PalbaseInvokeOptions): Promise<PalbaseResult<T>>;
2131
+ }
2132
+ /**
2133
+ * Cross-user admin write surface, reached via `Flags.asService()`.
2134
+ *
2135
+ * These five methods set/clear overrides for an ARBITRARY user (named
2136
+ * explicitly), so they bypass the current-request-user scope that `Flags.*`
2137
+ * reads and `Flags.setOverride(...)` are bound to. Mirrors the
2138
+ * `Database` / `Database.$asService()` split: the privileged, cross-user path
2139
+ * is moved OFF the default surface so it is greppable and intentional — a
2140
+ * handler that calls `Flags.setOverrideForUser(...)` on the default surface is
2141
+ * a compile error and must reach for `Flags.asService()` first.
2142
+ *
2143
+ * @example
2144
+ * await Flags.asService().setOverrideForUser("user_123", "new_checkout", true);
2145
+ */
2146
+ interface PalbaseFlagsServiceClient {
2147
+ /**
2148
+ * Set (or replace) a single feature-flag override for one user. The override
2149
+ * shadows the project (system) default for that user until cleared.
2150
+ */
2151
+ setOverrideForUser(userId: string, key: PalbaseFlagKey, value: PalbaseFlagValue): Promise<PalbaseResult<PalbaseSetOverrideResult>>;
2152
+ /** Set multiple overrides for one user in a single call. */
2153
+ setOverridesForUser(userId: string, values: Record<string, PalbaseFlagValue>): Promise<PalbaseResult<PalbaseSetOverridesResult>>;
2154
+ /** Clear one override for a user, restoring the project (system) default. */
2155
+ clearOverrideForUser(userId: string, key: PalbaseFlagKey): Promise<PalbaseResult<PalbaseClearOverrideResult>>;
2156
+ /** Clear all overrides for a user, restoring project (system) defaults. */
2157
+ clearAllOverridesForUser(userId: string): Promise<PalbaseResult<PalbaseClearAllOverridesResult>>;
2158
+ /** Apply override writes across many users in one request (max 1000 ops). */
2159
+ batchSetOverrides(operations: ReadonlyArray<PalbaseBatchOverrideOperation>): Promise<PalbaseResult<PalbaseBatchSetOverridesResult>>;
2160
+ }
2161
+ /**
2162
+ * Flags client available as the `Flags` singleton (and on `ctx.flags`).
2163
+ * Evaluate feature flags server-side (managed-runtime key — user targeting
2164
+ * is optional via context).
2165
+ *
2166
+ * Writes follow the `Database` model:
2167
+ * — `setOverride(key, value)` (default) overrides the flag for the CURRENT
2168
+ * request user; no userId argument, no admin power required. It errors when
2169
+ * there is no signed-in user (an anonymous request).
2170
+ * — `asService()` returns the {@link PalbaseFlagsServiceClient} carrying the
2171
+ * cross-user admin writes (`setOverrideForUser`, …) for an arbitrary target
2172
+ * user. Greppable + explicit, exactly like `Database.$asService()`.
2173
+ *
2174
+ * Reads (`isEnabled`/`getVariant`/`get`/`getAll`) stay on this default surface
2175
+ * and are already current-user-scoped via the request identity.
2176
+ */
2177
+ interface PalbaseFlagsClient {
2178
+ /** Is a flag enabled for the given context? */
2179
+ isEnabled(flagName: PalbaseFlagKey, context?: PalbaseFlagContext): Promise<PalbaseResult<boolean>>;
2180
+ /** Get the active variant of a multivariate flag. */
2181
+ getVariant(flagName: PalbaseFlagKey, context?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlagVariant>>;
2182
+ /** Get all flags for the project. */
2183
+ getAll(context?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlag[]>>;
2184
+ /**
2185
+ * Resolve a flag's value, with an optional fallback.
2186
+ *
2187
+ * `get(key)` · `get(key, default)` · `get(key, ctx)` · `get(key, default, ctx)`.
2188
+ * The default is substituted when the flag is absent AND when the flags
2189
+ * service is unreachable — a product that hides a feature because a lookup
2190
+ * timed out is behaving correctly.
2191
+ *
2192
+ * Declared here as of 2026-08-15. The client has always had it and its tests
2193
+ * have always covered it, but the interface did not: `Flags.get("x")` was a
2194
+ * type error in a handler and worked at runtime. That is the same gap as
2195
+ * `Documents.doc(...)` — declared and never implemented — seen from the other
2196
+ * side, and moving the implementation into this package is what surfaced it.
2197
+ */
2198
+ get(flagName: PalbaseFlagKey, defaultOrContext?: PalbaseFlagValue | PalbaseFlagContext, maybeContext?: PalbaseFlagContext): Promise<PalbaseResult<PalbaseFlagValue>>;
2199
+ /**
2200
+ * Set (or replace) a single feature-flag override for the CURRENT REQUEST
2201
+ * USER. No userId argument — the override is bound to the signed-in user the
2202
+ * handler is serving, so a handler can flip a flag for that user without
2203
+ * admin power. The override shadows the project (system) default for that
2204
+ * user until cleared.
2205
+ *
2206
+ * Errors when there is no signed-in user (an anonymous request); reach for
2207
+ * `Flags.asService().setOverrideForUser(userId, key, value)` to write a flag
2208
+ * for an arbitrary (cross-user) target.
2209
+ *
2210
+ * @example
2211
+ * await Flags.setOverride("new_checkout", true);
2212
+ */
2213
+ setOverride(key: PalbaseFlagKey, value: PalbaseFlagValue): Promise<PalbaseResult<PalbaseSetOverrideResult>>;
2214
+ /**
2215
+ * Return the cross-user admin write surface ({@link PalbaseFlagsServiceClient}).
2216
+ * Use sparingly and explicitly — the default `Flags.setOverride(...)` path is
2217
+ * bound to the current request user; `asService()` is how you write a flag
2218
+ * override for an ARBITRARY user. Mirrors `Database.$asService()`.
2219
+ */
2220
+ asService(): PalbaseFlagsServiceClient;
2221
+ }
2222
+ /**
2223
+ * Realtime broadcast surface (backend handler → subscribed clients).
2224
+ *
2225
+ * **Every channel must be DECLARED, or the join is refused.** Channel names are
2226
+ * not free text: `channels.ts` at the project root publishes which names exist
2227
+ * and who may subscribe, publish, or write state, and a client joining a name no
2228
+ * declaration matches is rejected without ever reaching your code. Declare with
2229
+ * `ownerOnly()`, `publicChannel()`, or a custom `authorize(ctx)` — see
2230
+ * {@link defineChannels}. Broadcasting to an undeclared channel reaches nobody.
2231
+ *
2232
+ * Backend-side this is BROADCAST-ONLY: a handler pushes an event to a channel
2233
+ * and every client subscribed to that channel (via the client SDK's
2234
+ * `pb.realtime.channel(...).on(...)`) receives it. There is no `subscribe()` on
2235
+ * the backend — a stateless request handler can't hold a socket; it fires an
2236
+ * HTTP broadcast and returns. Delivery is fire-and-forget: `broadcast` resolves
2237
+ * once the broadcast is accepted (or with an `error` if it could not be sent),
2238
+ * but it never blocks the handler waiting on subscribers.
2239
+ *
2240
+ * Channel names are yours to choose (e.g. `"room:42"`, `"orders"`) but not yours
2241
+ * to leave undeclared: each must match a pattern in `channels.ts`, which is where
2242
+ * `"room:{roomId}"` is authorized. The Palbase-managed `flags:<ref>` channels are
2243
+ * internal — author channels for your own features.
2244
+ *
2245
+ * @example
2246
+ * // Notify everyone in a chat room that a message landed:
2247
+ * await Realtime.broadcast("room:42", "message", { text, from: user.id });
2248
+ */
2249
+ interface PalbaseRealtimeClient {
2250
+ /**
2251
+ * A channel's SHARED STATE — a keyed map every subscriber sees, including
2252
+ * the ones that join later.
2253
+ *
2254
+ * That is the whole difference from `broadcast`: a broadcast reaches whoever
2255
+ * is listening at that instant, and a client connecting a second afterwards
2256
+ * learns nothing until the next one. State is handed to every joiner in the
2257
+ * snapshot they get, so "the score is 3-1" or "this flag is now on" arrives
2258
+ * even for a client that was not there when it changed.
2259
+ */
2260
+ state: {
2261
+ /** Write one entry. Fire-and-forget, like `broadcast`. */
2262
+ set(topic: string, key: string, value: Record<string, unknown>): Promise<PalbaseResult<void>>;
2263
+ /** Remove one entry. */
2264
+ clear(topic: string, key: string): Promise<PalbaseResult<void>>;
2265
+ };
2266
+ /**
2267
+ * Broadcast `event` with `payload` to everyone subscribed to `channel`.
2268
+ *
2269
+ * Fire-and-forget: resolves `{ data: undefined, error: null }` when the
2270
+ * broadcast was accepted, or `{ data: null, error }` when it could not be
2271
+ * sent (e.g. realtime not provisioned). A failed broadcast never throws and
2272
+ * never fails the handler.
2273
+ *
2274
+ * @param channel App-defined channel name (e.g. `"room:42"`). Do NOT prefix
2275
+ * with `"realtime:"` — that prefix is internal to the transport.
2276
+ * @param event Event name subscribers filter on (e.g. `"message"`).
2277
+ * @param payload JSON-serializable event body.
2278
+ */
2279
+ broadcast(channel: string, event: string, payload?: Record<string, unknown>): Promise<PalbaseResult<void>>;
2280
+ }
2281
+ /**
2282
+ * Push sub-client surface (server-only: fan-out to users / topics).
2283
+ */
2284
+ interface PalbasePushClient {
2285
+ /** Send a push notification to one or more users, or a topic. */
2286
+ send(params: PalbasePushSendParams): Promise<PalbaseResult<PalbasePushSendResponse | PalbaseMultiChannelResponse>>;
2287
+ }
2288
+ /**
2289
+ * Email sub-client surface (managed-runtime).
2290
+ */
2291
+ interface PalbaseEmailClient {
2292
+ /** Send a transactional email. */
2293
+ send(params: PalbaseEmailSendParams): Promise<PalbaseResult<PalbaseEmailSendResponse>>;
2294
+ }
2295
+ /**
2296
+ * SMS sub-client surface (managed-runtime).
2297
+ */
2298
+ interface PalbaseSmsClient {
2299
+ /** Send an SMS message. */
2300
+ send(params: PalbaseSmsSendParams): Promise<PalbaseResult<PalbaseSmsSendResponse>>;
2301
+ }
2302
+ /**
2303
+ * Phone-verification (OTP) sub-client surface (managed-runtime).
2304
+ *
2305
+ * Separate from `sms` because a verification carries no caller-authored text.
2306
+ * It is also the rail that works without a registered sender: a verification
2307
+ * service owns its own sender identity, which plain SMS requires you to
2308
+ * provision (and, in many countries, register) yourself.
2309
+ */
2310
+ interface PalbaseVerificationsClient {
2311
+ /** Send a verification code over SMS. */
2312
+ start(params: PalbaseVerificationStartParams): Promise<PalbaseResult<PalbaseVerificationStartResponse>>;
2313
+ /** Check a submitted code. A wrong code resolves to `approved: false`. */
2314
+ check(params: PalbaseVerificationCheckParams): Promise<PalbaseResult<PalbaseVerificationCheckResponse>>;
2315
+ }
2316
+ /**
2317
+ * Inbox sub-client surface (managed-runtime send + user read operations).
2318
+ */
2319
+ interface PalbaseInboxClient {
2320
+ /** Service-role: create an inbox notification row for a user. */
2321
+ send(params: PalbaseInboxSendParams): Promise<PalbaseResult<PalbaseInboxSendResponse | PalbaseMultiChannelResponse>>;
2322
+ /** List inbox messages (user-scoped or admin). */
2323
+ list(options?: PalbaseInboxListOptions): Promise<PalbaseResult<PalbaseInboxListResult>>;
2324
+ /** Count unread messages. */
2325
+ unreadCount(): Promise<PalbaseResult<{
2326
+ count: number;
2327
+ }>>;
2328
+ /** Mark a message as read. */
2329
+ markRead(id: string): Promise<PalbaseResult<void>>;
2330
+ /** Mark all messages read. */
2331
+ markAllRead(): Promise<PalbaseResult<void>>;
2332
+ /** Archive (soft-delete) a message. */
2333
+ archive(id: string): Promise<PalbaseResult<void>>;
2334
+ }
2335
+ /**
2336
+ * Preferences sub-client surface.
2337
+ */
2338
+ interface PalbasePreferencesClient {
2339
+ /** Get notification preferences. */
2340
+ get(): Promise<PalbaseResult<PalbasePreferences>>;
2341
+ /** Update notification preferences. */
2342
+ update(params: PalbasePreferences): Promise<PalbaseResult<PalbasePreferences>>;
2343
+ }
2344
+ /**
2345
+ * Email-template CRUD sub-client (managed-runtime gated server-side).
2346
+ */
2347
+ interface PalbaseEmailTemplatesClient {
2348
+ /** List all email templates. */
2349
+ list(): Promise<PalbaseResult<PalbaseEmailTemplate[]>>;
2350
+ /** Get one email template by ID. */
2351
+ get(id: string): Promise<PalbaseResult<PalbaseEmailTemplate>>;
2352
+ /** Create an email template. */
2353
+ create(input: PalbaseCreateEmailTemplateInput): Promise<PalbaseResult<PalbaseEmailTemplate>>;
2354
+ /** Update an email template (patch semantics). */
2355
+ update(id: string, input: PalbaseUpdateEmailTemplateInput): Promise<PalbaseResult<PalbaseEmailTemplate>>;
2356
+ /** Delete an email template. */
2357
+ delete(id: string): Promise<PalbaseResult<void>>;
2358
+ }
2359
+ /**
2360
+ * SMS-template CRUD sub-client (managed-runtime gated server-side).
2361
+ */
2362
+ interface PalbaseSMSTemplatesClient {
2363
+ /** List all SMS templates. */
2364
+ list(): Promise<PalbaseResult<PalbaseSMSTemplate[]>>;
2365
+ /** Get one SMS template by ID. */
2366
+ get(id: string): Promise<PalbaseResult<PalbaseSMSTemplate>>;
2367
+ /** Create an SMS template. */
2368
+ create(input: PalbaseCreateSMSTemplateInput): Promise<PalbaseResult<PalbaseSMSTemplate>>;
2369
+ /** Update an SMS template (patch semantics). */
2370
+ update(id: string, input: PalbaseUpdateSMSTemplateInput): Promise<PalbaseResult<PalbaseSMSTemplate>>;
2371
+ /** Delete an SMS template. */
2372
+ delete(id: string): Promise<PalbaseResult<void>>;
2373
+ }
2374
+ /**
2375
+ * Templates sub-client — parallel CRUD for email and SMS templates.
2376
+ * Service-role gated server-side; both surfaces map TS camelCase to
2377
+ * wire snake_case in the runtime client.
2378
+ */
2379
+ interface PalbaseTemplatesClient {
2380
+ /** Email template CRUD. */
2381
+ email: PalbaseEmailTemplatesClient;
2382
+ /** SMS template CRUD. */
2383
+ sms: PalbaseSMSTemplatesClient;
2384
+ }
2385
+ /**
2386
+ * Notifications client available on `ctx.notifications`.
2387
+ * Service-role: all send operations require privileged access.
2388
+ * Omits ClientNotificationsClient (browser-only narrowed surface).
2389
+ */
2390
+ interface PalbaseNotificationsClient {
2391
+ /** Push notification sender. */
2392
+ push: PalbasePushClient;
2393
+ /** Email sender. */
2394
+ email: PalbaseEmailClient;
2395
+ /** SMS sender. */
2396
+ sms: PalbaseSmsClient;
2397
+ /** Phone verification (OTP) sender and checker. */
2398
+ verifications: PalbaseVerificationsClient;
2399
+ /** Inbox (in-app) message sender and reader. */
2400
+ inbox: PalbaseInboxClient;
2401
+ /** Notification preferences manager. */
2402
+ preferences: PalbasePreferencesClient;
2403
+ /** Email + SMS template CRUD (managed-runtime). */
2404
+ templates: PalbaseTemplatesClient;
2405
+ /** Register a device for push notifications. */
2406
+ registerDevice(params: PalbaseRegisterDeviceParams): Promise<PalbaseResult<PalbaseDeviceTokenView>>;
2407
+ /** Remove a device registration. */
2408
+ unregisterDevice(deviceId: string): Promise<PalbaseResult<void>>;
2409
+ }
2410
+ /**
2411
+ * Analytics query namespace (read-side endpoints).
2412
+ */
2413
+ interface PalbaseAnalyticsQueryNamespace {
2414
+ /** Count events over time. */
2415
+ count(input: PalbaseCountQueryInput): Promise<PalbaseResult<PalbaseCountResult>>;
2416
+ /** List raw events. */
2417
+ events(input: PalbaseEventsQueryInput): Promise<PalbaseResult<PalbaseEventsResult>>;
2418
+ /** List event property descriptors. */
2419
+ properties(input?: PalbasePropertiesQueryInput): Promise<PalbaseResult<PalbasePropertyDescriptor[]>>;
2420
+ /** Query users by filters. */
2421
+ users(input: PalbaseUsersQueryInput): Promise<PalbaseResult<PalbaseUsersResult>>;
2422
+ /** Run a funnel query. */
2423
+ funnel(input: PalbaseFunnelQueryInput): Promise<PalbaseResult<PalbaseFunnelResult>>;
2424
+ /** Run a retention query. */
2425
+ retention(input: PalbaseRetentionQueryInput): Promise<PalbaseResult<PalbaseRetentionResult>>;
2426
+ /** Run a cohort query. */
2427
+ cohort(input: PalbaseCohortQueryInput): Promise<PalbaseResult<PalbaseCohortResult>>;
2428
+ }
2429
+ /**
2430
+ * Analytics management namespace (meta-level endpoints).
2431
+ */
2432
+ interface PalbaseAnalyticsManagementNamespace {
2433
+ /** Get project-level overview stats. */
2434
+ overview(): Promise<PalbaseResult<PalbaseOverviewResult>>;
2435
+ /** Get all event names seen by the project. */
2436
+ eventNames(): Promise<PalbaseResult<PalbaseEventNamesResult>>;
2437
+ /** Get details for a specific user. */
2438
+ userDetail(distinctId: string): Promise<PalbaseResult<PalbaseUserDetailResult>>;
2439
+ /** Delete all data for a user (GDPR erasure). */
2440
+ deleteUser(distinctId: string): Promise<PalbaseResult<void>>;
2441
+ }
2442
+ /**
2443
+ * Analytics client available on `ctx.analytics`.
2444
+ * Capture events server-side and run analytical queries.
2445
+ */
2446
+ interface PalbaseAnalyticsClient {
2447
+ /** Capture a custom event. */
2448
+ capture(event: string, properties?: PalbaseAnalyticsProperties, distinctId?: string): Promise<PalbaseResult<void>>;
2449
+ /** Identify a user with traits. */
2450
+ identify(distinctId: string, traits?: PalbaseIdentifyTraits): Promise<PalbaseResult<void>>;
2451
+ /** Track a screen view. */
2452
+ screen(screenName: string, properties?: PalbaseAnalyticsProperties, distinctId?: string): Promise<PalbaseResult<void>>;
2453
+ /** Query namespace for analytics read operations. */
2454
+ query: PalbaseAnalyticsQueryNamespace;
2455
+ /** Management namespace for meta-level operations. */
2456
+ management: PalbaseAnalyticsManagementNamespace;
2457
+ }
2458
+ /**
2459
+ * Deep-links client available on `ctx.links`.
2460
+ * Create, manage, and resolve deep links from server endpoints.
2461
+ * Omits `getInitialLink()` (client SDK convenience — browser/app only).
2462
+ */
2463
+ interface PalbaseLinksClient {
2464
+ /** Create a short link. */
2465
+ create(params: PalbaseCreateLinkParams): Promise<PalbaseResult<PalbaseLink>>;
2466
+ /** List links (paginated). */
2467
+ list(options?: PalbaseListLinksOptions): Promise<PalbaseResult<PalbaseListLinksResult>>;
2468
+ /** Get link details + analytics summary. */
2469
+ get(linkId: string): Promise<PalbaseResult<PalbaseLinkDetails>>;
2470
+ /** Update a link. */
2471
+ update(linkId: string, params: PalbaseUpdateLinkParams): Promise<PalbaseResult<PalbaseLink>>;
2472
+ /** Delete a link. */
2473
+ delete(linkId: string): Promise<PalbaseResult<{
2474
+ success: boolean;
2475
+ }>>;
2476
+ /** Get click analytics for a link. */
2477
+ analytics(linkId: string): Promise<PalbaseResult<PalbaseLinkAnalytics>>;
2478
+ /** Generate a QR code for a link (returns PNG or SVG Blob). */
2479
+ qrCode(linkId: string, options?: PalbaseQrCodeOptions): Promise<PalbaseResult<Blob>>;
2480
+ /** Match a deferred deep link by device fingerprint. */
2481
+ match(params: PalbaseMatchParams): Promise<PalbaseResult<PalbaseInitialLink | null>>;
2482
+ }
2483
+
2484
+ /**
2485
+ * runtime.ts — request-scoped service singletons.
2486
+ *
2487
+ * The backend SDK no longer threads a `ctx` god-object through every handler.
2488
+ * Instead, controller methods import PascalCase service singletons directly:
2489
+ *
2490
+ * import { Controller, Post, Body, Database } from "@palbase/backend";
2491
+ *
2492
+ * \@Controller("/todos")
2493
+ * export default class TodosController {
2494
+ * \@Post("") create(\@Body(CreateTodoBody) body: CreateTodoBody): unknown {
2495
+ * return Database.insert("todos", { title: body.title });
2496
+ * }
2497
+ * }
2498
+ *
2499
+ * The singletons are thin Proxies. Every property access forwards to the live
2500
+ * client for the CURRENT request scope, resolved through {@link __getRuntime}.
2501
+ *
2502
+ * # Request-scope resolution (persistent app-server)
2503
+ *
2504
+ * The runtime is a long-running Node process that serves many concurrent
2505
+ * requests on one event loop (NOT a fresh subprocess per request). A single
2506
+ * module-global slot would let one in-flight request's services bleed into
2507
+ * another's. So the services are carried in an {@link AsyncLocalStorage} store
2508
+ * ({@link __requestALS}) that the runtime sets per request with
2509
+ * {@link __runWithRuntime}; every async continuation of that request reads its
2510
+ * own store. `__getRuntime` reads the ALS store first; the module-global slot
2511
+ * (set by {@link __setRuntime}) is only a fallback for callers that run OUTSIDE
2512
+ * an ALS scope (dev-server, unit tests, the legacy single-shot path). Because
2513
+ * each `br-<ref>` pod is single-tenant, there is no cross-tenant leakage; the
2514
+ * ALS store is what prevents cross-REQUEST leakage within the shared process.
2515
+ *
2516
+ * The seam that makes `import { Database } from "@palbase/backend"` resolve to
2517
+ * the runtime-injected client: `@palbase/backend` is marked esbuild-EXTERNAL
2518
+ * when the tenant bundle is built, and the package is installed globally in the
2519
+ * pod (NODE_PATH=/usr/local/lib/node_modules). So worker.js's
2520
+ * `require('@palbase/backend')` and the bundle's `import` resolve to ONE shared
2521
+ * module instance — the ALS store and `__setRuntime` slot on that instance are
2522
+ * visible to the singletons the bundle imported.
2523
+ */
2524
+
2525
+ /** The set of live clients the runtime injects per request scope.
2526
+ *
2527
+ * Realtime is BROADCAST-ONLY here (a stateless handler can push an event but
2528
+ * cannot hold a subscription socket — `subscribe()` lives on the client SDK).
2529
+ *
2530
+ * EXCLUDED on purpose: Functions, Links, Analytics, Auth. They are not
2531
+ * exposed as backend handler singletons (auth lives on the client SDK; the rest
2532
+ * are out of scope for backend endpoints). */
2533
+ interface RuntimeServices {
2534
+ Database: DBClient;
2535
+ Secrets: SecretsService;
2536
+ Documents: PalbaseDocsClient;
2537
+ Storage: PalbaseStorageClient;
2538
+ Cache: CacheClient;
2539
+ Log: Logger;
2540
+ Notifications: PalbaseNotificationsClient;
2541
+ Flags: PalbaseFlagsClient;
2542
+ Realtime: PalbaseRealtimeClient;
2543
+ }
2544
+ /**
2545
+ * The per-request ALS box.
2546
+ *
2547
+ * `runtime` is the service bundle. `userId` is the request's authenticated user
2548
+ * id, written by the runtime immediately after it assembles the request object
2549
+ * (worker.js: `requestALS.getStore().userId = pbReq.user?.id || null`) and
2550
+ * `null` on an anonymous request. It was already being written there for the
2551
+ * Flags client's auto-bind; declaring it here makes the existing contract typed
2552
+ * instead of implicit, which is what lets `currentSubjectId()` read a
2553
+ * server-owned identity rather than trusting anything the caller sent.
2554
+ */
2555
+ interface RequestStore {
2556
+ runtime: RuntimeServices;
2557
+ userId?: string | null;
2558
+ /** This request's id (`req_…`), written by the runtime. Used as the spend
2559
+ * idempotency scope when the caller supplied no `Idempotency-Key`. */
2560
+ requestId?: string;
2561
+ /** The caller's `Idempotency-Key` header, or `null` when absent. What makes a
2562
+ * client's retry replay its first result instead of charging twice. */
2563
+ idempotencyKey?: string | null;
2564
+ }
2565
+ /**
2566
+ * Per-request store. The persistent runtime runs each request inside
2567
+ * {@link __runWithRuntime}, so every async continuation of that request reads
2568
+ * its OWN `runtime` (and any other request-scoped fields the runtime adds).
2569
+ *
2570
+ * Exported with a `__` prefix so the runtime (worker.js) shares the SAME ALS
2571
+ * instance across the one module instance — two ALS instances would silently
2572
+ * not see each other's stores. NOT part of the public author-facing API.
2573
+ */
2574
+ declare const __requestALS: AsyncLocalStorage<RequestStore>;
2575
+ /** Install the live clients in the process-global fallback slot.
2576
+ *
2577
+ * Persistent-server requests should use {@link __runWithRuntime} instead; this
2578
+ * remains for dev-server / tests / the legacy single-shot path that run without
2579
+ * an ALS scope. NOT part of the public author-facing API. */
2580
+ declare function __setRuntime(services: RuntimeServices): void;
2581
+ /** Run `fn` with `services` bound as the request-scoped runtime.
2582
+ *
2583
+ * The persistent worker calls this once per request so concurrent requests
2584
+ * never share a services slot. NOT part of the public author-facing API. */
2585
+ declare function __runWithRuntime<T>(services: RuntimeServices, fn: () => T): T;
2586
+ /** Read the live clients, throwing if accessed outside a request scope.
2587
+ *
2588
+ * Resolves the ALS store first (persistent server, per-request), then the
2589
+ * process-global fallback (dev-server / tests). NOT part of the public
2590
+ * author-facing API — used by the runtime and the singleton Proxies. */
2591
+ declare function __getRuntime(): RuntimeServices;
2592
+ /** A lifecycle hook. Sync or async; the runtime awaits what it returns. */
2593
+ type LifecycleHook = () => void | Promise<void>;
2594
+ /** Runs one release's shutdown hooks. Handed back by {@link __runStartHooks}
2595
+ * and called by the engine's `app.shutdown()`. Idempotent. */
2596
+ type ShutdownRunner = () => Promise<void>;
2597
+ /**
2598
+ * Run `hook` ONCE while the application comes up, before it serves anything.
2599
+ *
2600
+ * Call it at MODULE SCOPE in a file the application imports — the same rule
2601
+ * `defineDefaultAuth` and `@Controller` follow, and for the same reason: the
2602
+ * declaration is claimed when the app boots, which is after module loading and
2603
+ * before the first request. `name` is not decoration: a hook that throws is
2604
+ * reported by that name and the boot is REFUSED, so it is what tells an
2605
+ * operator which resource did not come up.
2606
+ *
2607
+ * There is no request scope yet, so the `Database`/`Secrets`/… singletons are
2608
+ * NOT available inside a start hook. A secret is read from `process.env` here
2609
+ * (the runtime mirrors the vault into it at boot).
2610
+ *
2611
+ * @example
2612
+ * // resources/graph.ts
2613
+ * import neo4j from "neo4j-driver";
2614
+ * import { onStart, onShutdown } from "@palbase/backend";
2615
+ *
2616
+ * export let graph: Driver;
2617
+ * onStart("graph", () => {
2618
+ * graph = neo4j.driver(process.env.NEO4J_URL!, neo4j.auth.basic("neo4j", process.env.NEO4J_PASSWORD!));
2619
+ * });
2620
+ * onShutdown("graph", () => graph.close());
2621
+ */
2622
+ declare function onStart(name: string, hook: LifecycleHook): void;
2623
+ /**
2624
+ * Run `hook` while the application shuts down — the place a pool opened in
2625
+ * {@link onStart} is closed.
2626
+ *
2627
+ * Shutdown is BEST-EFFORT by design: a hook that throws is reported by name and
2628
+ * the rest still run. A drain that abandoned the remaining hooks on the first
2629
+ * failure would leak exactly what this exists to release, and the process is
2630
+ * leaving anyway.
2631
+ *
2632
+ * Hooks run in REVERSE declaration order, so a resource is released before what
2633
+ * it was built on.
2634
+ */
2635
+ declare function onShutdown(name: string, hook: LifecycleHook): void;
2636
+ /**
2637
+ * CLAIM what has been declared, run the start hooks, and hand back the runner
2638
+ * for this release's shutdown hooks. Called by the engine's `createApp`; the
2639
+ * `App.shutdown()` it builds calls what comes back. NOT part of the public
2640
+ * author-facing API.
2641
+ *
2642
+ * IT CLAIMS RATHER THAN READS, which is what makes it correct in this runtime:
2643
+ * a candidate release is loaded BESIDE the live one in one process
2644
+ * (`v2/runtime/src/registry-scope.ts`), and both bundles append to the one
2645
+ * shared slot above. If each app read the whole list, the live app's shutdown
2646
+ * would close the candidate's pool and the candidate's would close the live
2647
+ * app's. Taking the declarations leaves each app holding exactly its own.
2648
+ *
2649
+ * A start hook that throws REFUSES THE BOOT — with the hook's name in the
2650
+ * message — after releasing whatever the earlier hooks already opened. Serving
2651
+ * from a half-initialised app is the silence this whole surface replaces, and a
2652
+ * boot that dies holding an open pool is the leak it replaces.
2653
+ */
2654
+ declare function __runStartHooks(): Promise<ShutdownRunner>;
2655
+ /** Drop every declaration. For tests, which declare repeatedly in one process.
2656
+ * NOT part of the public author-facing API. */
2657
+ declare function __resetLifecycleHooks(): void;
2658
+ /**
2659
+ * Build the `.tables` accessor for the top-level `Database`. Each
2660
+ * `tables.<name>` access returns a small object that forwards the five CRUD ops
2661
+ * to the underlying client using `name` as the string table identifier. The
2662
+ * shapes are typed against the generated `palbase-env.d.ts` (`EnvTables`); at
2663
+ * runtime they are plain string-keyed calls, so no schema value is needed here.
2664
+ *
2665
+ * Returns `EnvTables` — TS cannot infer the mapped type through the Proxy, so
2666
+ * a single structural narrowing names the surface (the proxy returns a
2667
+ * correctly-shaped accessor for whatever string member is read).
2668
+ */
2669
+ /** T018 (C-8): similar/recommend'in string-keyed yüzü. DBOps'a (endpoint.ts)
2670
+ * BİLEREK eklenmedi — search-param imza üçlüsü (engine/db + typed-db +
2671
+ * endpoint) büyümesin: proxy dispatch runtime'da engine ops'una zaten ulaşır,
2672
+ * derleme güvenliğini typed yüzey (EnvTypedTable) verir. */
2673
+ interface RecoOps {
2674
+ similar(table: string, id: string, params?: Record<string, unknown>): Promise<Record<string, unknown>[]>;
2675
+ recommend(table: string, params: Record<string, unknown>): Promise<Record<string, unknown>[]>;
2676
+ }
2677
+ /**
2678
+ * The project's own Postgres (pgx, schema `env_<envId>`).
2679
+ *
2680
+ * Typed by default: `Database.tables.<name>.insert({...})` is typed against
2681
+ * the project's generated `palbase-env.d.ts` with NO import and NO generic.
2682
+ * The raw string ops (`query`/`insert`/`update`/`delete`/`findById`/`findMany`)
2683
+ * are also available for dynamic table names and read-only SQL.
2684
+ *
2685
+ * RLS is enforced by default (the runtime runs each op as `authenticated` with
2686
+ * the verified user's claims). To bypass RLS, call `Database.asService()` —
2687
+ * explicit and greppable — which runs as the `service_role` (BYPASSRLS).
2688
+ *
2689
+ * @example
2690
+ * import { Database } from "@palbase/backend";
2691
+ *
2692
+ * const todo = await Database.public.todos.insert({ title: req.input.title });
2693
+ * todo.id; // string ✓
2694
+ * const rows = await Database.$query("SELECT id FROM todos WHERE done = $1", [false]);
2695
+ * const all = await Database.$asService().public.todos.findMany({}); // RLS bypass
2696
+ */
2697
+ declare const Database: EnvTypedDatabase;
2698
+ /** Firestore-like document client (PalDocs). */
2699
+ declare const Documents: PalbaseDocsClient;
2700
+ /**
2701
+ * `buckets.<name>` — the storage twin of `Database.tables.<name>`, and the same
2702
+ * mechanism: `config/storage.ts` generates a `Buckets` augmentation into
2703
+ * `palbase-env.d.ts`, so a bucket name is a property with no import and no
2704
+ * generic, and a typo is a compile error.
2705
+ *
2706
+ * The intermediate `.buckets` is there for the reason `.tables` is: bucket
2707
+ * names must not share a namespace with the client's own methods, or the day
2708
+ * somebody declares a bucket called `bucket` the surface breaks.
2709
+ */
2710
+ type EnvBuckets = {
2711
+ [K in keyof Buckets]: TypedBucketClient<Buckets[K] extends BucketTypes ? Buckets[K] : BucketTypes>;
2712
+ };
2713
+ /** One bucket, with its declared variant names in the type. */
2714
+ interface TypedBucketClient<B extends BucketTypes> extends Omit<PalbaseBucketClient, "getPublicUrl"> {
2715
+ /** The public URL for the object, or for one of THIS bucket's declared
2716
+ * renditions. A name the bucket does not declare is a compile error. */
2717
+ getPublicUrl(path: string, options?: {
2718
+ variant?: B["variants"];
2719
+ }): string;
2720
+ }
2721
+ /**
2722
+ * Object storage: buckets, objects, renditions and signed URLs.
2723
+ *
2724
+ * `Storage.buckets.posts.upload(...)` is the typed path. `Storage.bucket(name)`
2725
+ * remains for a name computed at runtime — rare, and it gives up the typing,
2726
+ * which is the honest trade rather than a second blessed way to do it.
2727
+ */
2728
+ declare const Storage: PalbaseStorageClient & {
2729
+ buckets: EnvBuckets;
2730
+ };
2731
+ /** JSON-typed cache (get/set/incr/getOrSet). */
2732
+ declare const Cache: CacheClient;
2733
+ /**
2734
+ * This tenant's secrets — API keys, provider credentials, signing material the
2735
+ * BACKEND owns.
2736
+ *
2737
+ * `await Secrets.get("STRIPE_KEY")` in a handler. There is no `.env` to read
2738
+ * and no file to mount: a secret is written through the vault's authenticated
2739
+ * API (or the CLI/MCP that calls it), and this is how the deployed code reads
2740
+ * it back. A name this tenant has not set answers null — as does every name
2741
+ * the STACK holds, because no route returns a platform secret's value at all.
2742
+ */
2743
+ declare const Secrets: SecretsService;
2744
+ /** Structured logger. */
2745
+ declare const Log: Logger;
2746
+ /** Push / email / SMS / in-app notifications. */
2747
+ declare const Notifications: PalbaseNotificationsClient;
2748
+ /**
2749
+ * Feature flags.
2750
+ *
2751
+ * Mirrors the `Database` / `Database.asService()` model. The default surface is
2752
+ * RLS-equivalent for flags: reads resolve against the CURRENT request user and
2753
+ * `Flags.setOverride(key, value)` writes an override for that same signed-in
2754
+ * user (no userId argument, no admin power). Cross-user admin writes
2755
+ * (`setOverrideForUser`, …) live behind `Flags.asService()` — explicit and
2756
+ * greppable, just like `Database.asService()`.
2757
+ *
2758
+ * @example
2759
+ * import { Flags } from "@palbase/backend";
2760
+ *
2761
+ * if (await Flags.isEnabled("new_checkout")) { ... } // current user
2762
+ * await Flags.setOverride("new_checkout", true); // current user
2763
+ * await Flags.asService().setOverrideForUser("u_9", "x", true); // cross-user
2764
+ */
2765
+ declare const Flags: PalbaseFlagsClient;
2766
+ /**
2767
+ * The Realtime broadcast singleton for the current request scope. Backend-side
2768
+ * Realtime is BROADCAST-ONLY (a stateless handler can push but not subscribe —
2769
+ * `subscribe()` lives on the client SDK's `pb.realtime`). Fire-and-forget:
2770
+ * `broadcast` resolves once accepted (or with an `error`), never blocking the
2771
+ * handler on subscribers.
2772
+ *
2773
+ * @example
2774
+ * import { Realtime } from "@palbase/backend";
2775
+ *
2776
+ * await Realtime.broadcast("room:42", "message", { text, from: user.id });
2777
+ */
2778
+ declare const Realtime: PalbaseRealtimeClient;
2779
+
2780
+ /**
2781
+ * typed-db.ts — Task 2: TypedDB schema-derived insert/row shapes.
2782
+ *
2783
+ * Derives INSERT and full-row TypeScript types from a `defineSchema()` result
2784
+ * and wraps the untyped runtime `DBClient` with a typed facade.
2785
+ *
2786
+ * No value-any. No `as unknown as X`. The two narrow `as` casts in
2787
+ * `makeTypedTable` are safe because:
2788
+ * - `data as Record<string, unknown>`: InsertShape<T> maps string keys to
2789
+ * typed values; all value types are subsets of `unknown`, so the cast is
2790
+ * structurally sound.
2791
+ * - `result as RowShape<T>`: The runtime DBClient returns `Record<string,
2792
+ * unknown>` which is the erased form of the typed row; we're narrowing back
2793
+ * to the precise shape that the schema declared.
2794
+ * Both casts are narrowing only (not widening) and correctness is guaranteed
2795
+ * by the schema the caller provides.
2796
+ */
2797
+
2798
+ /** Keys of C whose columns are required on INSERT (not nullable, no default). */
2799
+ type RequiredKeys<C> = {
2800
+ [K in keyof C]: ColIsOptionalOnInsert<C[K]> extends true ? never : K;
2801
+ }[keyof C];
2802
+ /** Keys of C whose columns are optional on INSERT (nullable or has a default). */
2803
+ type OptionalKeys<C> = {
2804
+ [K in keyof C]: ColIsOptionalOnInsert<C[K]> extends true ? K : never;
2805
+ }[keyof C];
2806
+ /**
2807
+ * The TypeScript type for an INSERT payload for table `T`.
2808
+ * - Required: columns that are NOT NULL and have no DB-level default.
2809
+ * - Optional: columns that are nullable or carry a default.
2810
+ *
2811
+ * When all columns are optional, `RequiredKeys<C>` resolves to `never` and
2812
+ * the first part becomes `{}`, which is a neutral element for `&`.
2813
+ */
2814
+ type InsertShape<T extends TableDef> = {
2815
+ [K in RequiredKeys<T["columns"]>]: ColValue<T["columns"][K]>;
2816
+ } & {
2817
+ [K in OptionalKeys<T["columns"]>]?: ColValue<T["columns"][K]>;
2818
+ };
2819
+ /**
2820
+ * The TypeScript type for a full row returned by the DB for table `T`.
2821
+ * Every column is present; nullable columns resolve to `T | null`.
2822
+ */
2823
+ type RowShape<T extends TableDef> = {
2824
+ [K in keyof T["columns"]]: ColValue<T["columns"][K]>;
2825
+ };
2826
+ /** A typed table accessor that mirrors the runtime DBClient surface. */
2827
+ interface TypedTable<T extends TableDef> {
2828
+ insert(data: InsertShape<T>): Promise<RowShape<T>>;
2829
+ put(q: {
2830
+ data: InsertShape<T>;
2831
+ onConflict: readonly string[];
2832
+ }): Promise<RowShape<T>>;
2833
+ /** Update the row by id; resolves to the updated row, or `null` if no row
2834
+ * matched (absent or RLS-hidden) — an idempotent outcome, mirroring
2835
+ * `findById`. The runtime returns a null row rather than throwing. */
2836
+ update(q: {
2837
+ where: {
2838
+ id: string;
2839
+ };
2840
+ set: Partial<InsertShape<T>>;
2841
+ }): Promise<RowShape<T> | null>;
2842
+ delete(id: string): Promise<void>;
2843
+ findById(id: string): Promise<RowShape<T> | null>;
2844
+ /** Rows matching the filter. Operators, ordering and paging are the ENGINE's
2845
+ * surface — this declaration is what makes them callable. */
2846
+ findMany<K extends keyof RowShape<T> = keyof RowShape<T>>(q?: QueryInput<RowShape<T>, K>): Promise<Pick<RowShape<T>, K>[]>;
2847
+ /**
2848
+ * Update every matching row in one statement; an empty filter is refused.
2849
+ *
2850
+ * **0 satır dönmesi hata DEĞİL, başarı da değil** (FR-014): koşulu `where`'e
2851
+ * koyup dönen diziyi kontrol etmek, "önce oku sonra yaz"ın yarış koşulu
2852
+ * olmayan hâlidir.
2853
+ *
2854
+ * ```ts
2855
+ * const [row] = await Database.public.accounts.updateMany({
2856
+ * where: { id, balance: { gte: amount } },
2857
+ * set: { balance: decrement(amount) },
2858
+ * });
2859
+ * if (row === undefined) throw new Conflict("yetersiz bakiye");
2860
+ * ```
2861
+ */
2862
+ updateMany(q: MutateInput<RowShape<T>, InsertShape<T>>): Promise<RowShape<T>[]>;
2863
+ /** Delete every matching row; resolves to how many. Empty filter refused. */
2864
+ deleteMany(q: {
2865
+ where: WhereFilter<RowShape<T>> | SqlFragment;
2866
+ }): Promise<number>;
2867
+ /** How many rows match. An empty filter is legitimate: counting is a read. */
2868
+ count(q?: {
2869
+ where?: WhereFilter<RowShape<T>> | SqlFragment;
2870
+ }): Promise<number>;
2871
+ }
2872
+ /** A typed DB facade covering all tables declared in schema `S`. */
2873
+ interface TypedDB<S extends SchemaDef> {
2874
+ tables: {
2875
+ [K in keyof S["tables"]]: TypedTable<S["tables"][K]>;
2876
+ };
2877
+ /** Run a transaction plan. See {@link EnvTypedDatabase.transaction}. */
2878
+ transaction<T>(fn: (tx: TypedTx<S>) => T extends Promise<unknown> ? never : T): Promise<Materialized<T>>;
2879
+ }
2880
+ /** The plan-building handle a `TypedDB<S>` transaction callback receives: the
2881
+ * schema's tables, expressed as plan operations rather than awaited calls.
2882
+ *
2883
+ * YALNIZ `tables`, ve ÇALIŞMA ZAMANI DA yalnız onu veriyor.
2884
+ *
2885
+ * Bir süre runtime `{ tables, public }` döndürüyordu ama tip yalnız `tables`
2886
+ * söylüyordu: `tx.public.x` derlenmiyor, koşsaydı çalışacaktı — tipin çalışma
2887
+ * zamanından AZ söylemesi (gözcü M-8). Tipe `public` EKLEMEK yanlış düzeltmeydi
2888
+ * ve `check:api` onu adıyla reddetti: `TypedTx` dışa açık, ve zorunlu bir üye
2889
+ * eklemek onu İNŞA EDEN müşterinin kodunu kırar — "the new shape demands more
2890
+ * than the recorded one". Yalan runtime'dan kaldırılarak kapatıldı; ikisi artık
2891
+ * aynı şeyi söylüyor. `Database.$transaction`'ın `tx.public`'i ayrı bir yüzey
2892
+ * (`TxPlan`) ve orada tip de runtime da onu taşıyor. */
2893
+ type TypedTx<S extends SchemaDef> = TxPlanHandle<{
2894
+ [K in keyof S["tables"]]: TxTable<RowShape<S["tables"][K]>, InsertShape<S["tables"][K]>>;
2895
+ }>;
2896
+ /**
2897
+ * Wraps a raw `DBClient` with the type-safe `TypedDB<S>` facade derived from
2898
+ * the provided schema. No behavior change for the direct ops — all calls
2899
+ * delegate to `raw` with the table name as a plain string.
2900
+ *
2901
+ * `transaction` does NOT delegate to a per-op client: the callback describes a
2902
+ * plan against a fresh {@link TxPlanBuilder}, and the whole plan travels in one
2903
+ * `raw.txPlan` call. The schema is used only for its table NAMES; the values
2904
+ * are typed by `S` at compile time and are plain strings at run time.
2905
+ *
2906
+ * The `as` casts are single structural narrowings from a dynamically-built
2907
+ * object to the precise mapped type (TS cannot infer the mapped-type result
2908
+ * through `Object.keys` iteration) — see the module-level doc comment.
2909
+ */
2910
+ declare function makeTypedDB<S extends SchemaDef>(schema: S, raw: DBClient): TypedDB<S>;
2911
+ /** Bir where değeri: düz eşitlik YA DA operatör nesnesi (FR-016). */
2912
+ /**
2913
+ * Metin operatörleri YALNIZ metin kolonlarında görünür (FR-005).
2914
+ *
2915
+ * `contains` bir sayı kolonunda anlamsızdır ve onu tipte sunmak "ifade edilemez
2916
+ * kıl" ilkesinin tersidir: derleme anında yakalanabilecek bir hata çalışma
2917
+ * zamanına ertelenir. Koşullu tip ÖZYİNELEMESİZDİR (N-2).
2918
+ */
2919
+ /**
2920
+ * Bir KOLONA yapılan referans (FR-011) — karşılaştırmanın sağ tarafında değer
2921
+ * yerine durabilen tek şey.
2922
+ *
2923
+ * Şekli okunabilir (`{ $col: "…" }`) ama KİMLİĞİ şekli değil: nesne
2924
+ * `Symbol.for("palbase.db.ref")` ile markalanır. Şekil tek başına yetseydi
2925
+ * güvenilmeyen bir istek gövdesi `{"$col":"tenant_id"}` gönderip kiracılık
2926
+ * predikatını `t."tenant_id" = t."tenant_id"` totolojisine çevirebilirdi —
2927
+ * ölçüldü (W2-A/C1). Marka enumerable olmadığı için JSON round-trip'i onu
2928
+ * düşürür; süreç içinde (transaction planı dahil) korunur.
2929
+ */
2930
+ type ColRef<N extends string = string> = {
2931
+ readonly $col: N;
2932
+ };
2933
+
2934
+ /**
2935
+ * `total > amount_paid` — bugün bunu yazmanın tek yolu ham SQL'e düşmekti,
2936
+ * çünkü sağ taraf bir DEĞER değil.
2937
+ *
2938
+ * Ad literal olarak yakalanır (`const N`), ve doğruluğu ÇAĞRIDA değil
2939
+ * KULLANIM YERİNDE kanıtlanır: filtre `WhereFilter<Row>` beklediği için
2940
+ * `ColRef<"yok_boyle">` oraya atanamaz. Yani `col()`'a satır tipini elle
2941
+ * vermek gerekmiyor, yanlış ad yine de derleme hatası (P5).
2942
+ *
2943
+ * Dönen nesne MARKALIDIR (`brandRef`): telden gelen `{"$col":"x"}` bir kolon
2944
+ * referansı SAYILMAZ, "bilinmeyen operatör" diye reddedilir. Gerekçe
2945
+ * `db/input-guards.ts`'te ölçümüyle birlikte yazılı.
2946
+ */
2947
+ declare function col<const N extends string>(name: N): ColRef<N>;
2948
+ /** Bu satırın kolonlarından birine referans. `Row` bilinmiyorsa hiçbiri. */
2949
+ /**
2950
+ * Bir kolonun karşılaştırma AİLESİ — ve neden PG tipi değil de bu.
2951
+ *
2952
+ * ÖLÇÜLDÜ (gözcü, canlı pg16): TS tarafı PG tipini TAŞIMIYOR. `numeric`,
2953
+ * `bigint`, `text`, `uuid` ve `timestamp` hepsi `string`'e, `integer` `number`'a
2954
+ * düşüyor (`env-gen.ts:30-60`). Bu yüzden TS tipine kurulan HİÇBİR kural doğru
2955
+ * olamaz — kural ne olursa olsun bir taraf yanlışa düşer:
2956
+ *
2957
+ * { qty: { gt: col("amount") } } integer ↔ numeric PG: GEÇERLİ
2958
+ * { total: { gt: col("note") } } integer ↔ text PG: GEÇERSİZ
2959
+ *
2960
+ * İkisi de TS'te "number ↔ string". Ayırt edecek bilgi tipte YOK.
2961
+ *
2962
+ * YANLIŞ RET, YANLIŞ KABULDEN KÖTÜDÜR — ve tercih buna göre yapıldı. Yanlış bir
2963
+ * ret meşru bir sorguyu İFADE EDİLEMEZ kılar, yani yazarı `$query`'ye düşürür:
2964
+ * bu yüzeyin kapatmak için var olduğu şey. Yanlış bir kabul ise Postgres'in
2965
+ * GÜRÜLTÜLÜ hatasına çıkar (`operator does not exist: integer > text`) — kötü
2966
+ * ama sessiz değil.
2967
+ *
2968
+ * O yüzden aile ayrımı yalnız TS'in GERÇEKTEN ayırdığı yerlerde yapılıyor:
2969
+ * `boolean`, vektör dizisi ve `Date` kendi başlarına; metin ve sayı ise TEK
2970
+ * ailede, çünkü aralarındaki PG ayrımı tipte yok.
2971
+ *
2972
+ * KALICI ÇÖZÜM tipte değil ÜRETİMDE: `env-gen` kolon başına bir PG-tip markası
2973
+ * bassın (`string & { readonly __pg?: "numeric" }`). O zaman `numeric ↔ integer`
2974
+ * kabul, `numeric ↔ text` ret olur ve ikisi de DOĞRU olur. Defterde teklif.
2975
+ */
2976
+ type PgFamily<V> = [NonNullable<V>] extends [boolean] ? "bool" : [NonNullable<V>] extends [readonly (number | string)[]] ? "array" : [NonNullable<V>] extends [Date] ? "date" : [NonNullable<V>] extends [string | number] ? "scalar" : "opaque";
2977
+ /** `Row`'un `V` ile karşılaştırılabilir kolonları — aynı aileden olanlar. */
2978
+ type ColumnsComparableTo<Row, V> = {
2979
+ [K in keyof Row]-?: PgFamily<Row[K]> extends PgFamily<V> ? K : never;
2980
+ }[keyof Row];
2981
+ /**
2982
+ * Bu satırın kolonlarından birine referans. `Row` bilinmiyorsa hiçbiri.
2983
+ *
2984
+ * İKİNCİ PARAMETRE kolonun tipi. Verilmezse (`unknown`) KISIT YOKTUR — bu
2985
+ * `findMany`'nin `Row`'u bilmediği yolların ve jsonb kolonlarının hâli, ve
2986
+ * ikisinde de tipin söyleyebileceği bir şey yok.
2987
+ */
2988
+ type ColRefOf<Row, V = unknown> = [keyof Row] extends [never] ? never : [unknown] extends [V] ? ColRef<Extract<keyof Row, string>> : ColRef<Extract<ColumnsComparableTo<Row, V>, string>>;
2989
+ /**
2990
+ * Doğrulanmış, GÖMÜLEBİLİR bir SQL parçası (FR-018, FR-019).
2991
+ *
2992
+ * Wire şekli `{ $sql: { text, values } }`: `text` parçalar hâlinde tutulur ve
2993
+ * değerler ARADA durur, çünkü birleştirilmiş tek bir string'in içinden hangi
2994
+ * kısmın kullanıcı verisi olduğunu bir daha kimse çıkaramaz — enjeksiyonun
2995
+ * doğduğu yer tam olarak orasıdır.
2996
+ */
2997
+ type SqlFragment = {
2998
+ readonly $sql: {
2999
+ readonly text: readonly string[];
3000
+ readonly values: readonly unknown[];
3001
+ };
3002
+ };
3003
+ /**
3004
+ * Retryable bir hatada işlemi yeniden dener (FR-037).
3005
+ *
3006
+ * D-016: retry'SIZ bir izolasyon yükseltmesi sunmak defect'tir — kullanıcıya
3007
+ * çalışmayan bir düğme vermektir. `SerializationFailure`'ın metni
3008
+ * *"{ retry: n } verebilirsiniz"* diyor; bu fonksiyon o düğmenin gerçekten
3009
+ * çalışan yarısı.
3010
+ *
3011
+ * VARSAYILAN SIFIR. Sessiz bir varsayılan retry, idempotent OLMAYAN bir işlemi
3012
+ * çağıranın haberi olmadan iki kez çalıştırırdı — bir para transferini iki kez.
3013
+ * Tekrar denemek çağıranın kararı.
3014
+ *
3015
+ * Yalnız `isRetryable(e)` olan hatalar tekrarlanır. Küme dar tutuluyor: `23505`
3016
+ * buraya girseydi tekrar denemek aynı cevabı verir ve döngü boşuna dönerdi.
3017
+ */
3018
+ declare function withRetry<T>(fn: () => Promise<T>, opts?: {
3019
+ retry?: number;
3020
+ }): Promise<T>;
3021
+ /**
3022
+ * K3: sorgudan KAÇMAK için değil, sorgunun İÇİNE girmek için kaçış kapağı.
3023
+ *
3024
+ * ```ts
3025
+ * Database.public.notes.findMany({
3026
+ * where: sqlFragment`similarity(title, ${q}) > 0.3`,
3027
+ * orderBy: { column: "created_at", direction: "desc" },
3028
+ * limit: 20,
3029
+ * })
3030
+ * ```
3031
+ *
3032
+ * `select` / `orderBy` / `limit` / RLS aynen çalışmaya devam eder — "yetmedi,
3033
+ * ham SQL'e geçeyim" anının bugünkü maliyeti tam olarak bunları kaybetmekti.
3034
+ *
3035
+ * TANIMLAYICI GÖMME YÜZEYİ YOK (`.raw` / `.unsafe` / `.identifier`). Drizzle'ın
3036
+ * CVE-2026-39356'sı ve Knex'in CVE-2019-10757'si tam o kapakta doğdu.
3037
+ *
3038
+ * Ve kapak fonksiyonun ÜSTÜNDE olmakla bitmiyordu: parça MARKALI değilken
3039
+ * `{"$sql":{"text":["1=1 -- pwned"],"values":[]}}` düz JSON'dan uydurulup
3040
+ * guard'ın tamamını atlıyordu (W2-B/C3, ölçüldü). Marka o kapağı wire ŞEKLİNE
3041
+ * de koyuyor — parça yalnız bu template tag'inden çıkabilir.
3042
+ */
3043
+ declare function sqlFragment(strings: TemplateStringsArray, ...values: unknown[]): SqlFragment;
3044
+ type TextOps<V> = V extends string ? {
3045
+ contains?: string;
3046
+ icontains?: string;
3047
+ startsWith?: string;
3048
+ endsWith?: string;
3049
+ } : Record<never, never>;
3050
+ /** Operatör gövdesi, kolon-referans tipi DIŞARIDAN verilmiş hâliyle.
3051
+ *
3052
+ * `C` bir parametre, çünkü `ColRefOf<Row, V>` aksi hâlde altı operatör
3053
+ * konumunda AYRI AYRI instantiate edilirdi — kolon başına altı mapped type.
3054
+ * Böyle kolon başına BİR tane (N-2). */
3055
+ type WhereOpBody<V, C> = {
3056
+ gt?: V | C;
3057
+ gte?: V | C;
3058
+ lt?: V | C;
3059
+ lte?: V | C;
3060
+ neq?: V | C;
3061
+ in?: V[];
3062
+ /** `IS NULL` / `IS NOT NULL` (FR-006) — `= NULL` SQL'de her zaman UNKNOWN'dır. */
3063
+ isNull?: boolean;
3064
+ } & TextOps<V>;
3065
+ type WhereOp<V, Row = unknown> = WhereOpWith<V, ColRefOf<Row, V>>;
3066
+ /**
3067
+ * Operatör sözlüğü, kolon-referans/plan-tutamağı konumu DIŞARIDAN verilmiş.
3068
+ *
3069
+ * `export`, çünkü plan yüzeyi (`tx-plan.ts`) aynı sözlüğü `C = ColRefOf<…> |
3070
+ * Ref<…>` ile kuruyor: bir plan filtresi ÖNCEKİ bir işlemin sonucuna
3071
+ * bakabiliyor, `findMany` bakamıyor. Ref'i `V`'ye eklemek YANLIŞ olurdu —
3072
+ * `TextOps<V>` `V extends string` diye soruyor ve `string | Ref<string>` o
3073
+ * soruya HAYIR der, yani `contains`/`startsWith` sessizce KAYBOLURDU.
3074
+ */
3075
+ type WhereOpWith<V, C> = V | C | WhereOpBody<V, C>;
3076
+ /**
3077
+ * A filter over a row: every field optional, each one a plain value (equality)
3078
+ * or an operator object. THE filter language — `findMany`, `updateMany`,
3079
+ * `deleteMany` and `count` all take this one, because two spellings of a filter
3080
+ * is how the two come to disagree.
3081
+ */
3082
+ type WhereFilter<Row> = {
3083
+ [K in keyof Row]?: WhereOp<Row[K], Row>;
3084
+ } & {
3085
+ /**
3086
+ * Boolean bileşimi (FR-007). Dallar aynı filtre dilidir — iki yazım olmaz.
3087
+ *
3088
+ * ÖZYİNELEME BURADA BAŞLIYOR ve N-2'nin izlediği şey tam olarak budur:
3089
+ * Kysely'nin TS7'de 9,8M instantiation üreten vakası derin generic
3090
+ * özyinelemeydi. Burada derinlik yazarın filtresi kadardır ve pratikte
3091
+ * bir-iki seviyedir; tip maliyeti T001'in tavanına karşı ölçülür.
3092
+ */
3093
+ OR?: WhereFilter<Row>[];
3094
+ AND?: WhereFilter<Row>[];
3095
+ NOT?: WhereFilter<Row>;
3096
+ };
3097
+ /**
3098
+ * Ordering and paging for a read.
3099
+ *
3100
+ * `column` is `keyof Row`, not `string`: a mistyped column name is a compile
3101
+ * error here rather than a runtime rejection three layers down. `offset`
3102
+ * without `limit` is refused by the engine — a page with no size is not a page.
3103
+ */
3104
+ type OrderBySpec<Row> = {
3105
+ column: Extract<keyof Row, string>;
3106
+ direction?: "asc" | "desc";
3107
+ /** NULL'ların yeri (FR-008). Verilmezse Postgres varsayılanı geçerlidir ve o
3108
+ * varsayılan YÖNE GÖRE DEĞİŞİR: ASC'de NULLS LAST, DESC'te NULLS FIRST. */
3109
+ nulls?: "first" | "last";
3110
+ };
3111
+ type FindManyOpts<Row> = {
3112
+ /** Tek sıralama ya da SIRALI liste — sıra korunur (FR-008). */
3113
+ orderBy?: OrderBySpec<Row> | OrderBySpec<Row>[];
3114
+ limit?: number;
3115
+ offset?: number;
3116
+ };
3117
+ /**
3118
+ * Bir okumanın TEK parametresi (FR-004b).
3119
+ *
3120
+ * `where` bir ALAN, konumsal bir argüman değil. Kullanıcının kararı: "deterministik
3121
+ * olması lazım, tek düzlem" — aynı bilginin iki yere dağılması, alanlar çoğaldıkça
3122
+ * (select/include/orderBy) "hangisi nereye" sorusunu ezberlenecek bir şeye çevirir.
3123
+ * Prisma ve Drizzle de tek obje kullanıyor. Bedeli açık ve kabul edildi: en sık
3124
+ * kullanım uzuyor — `findMany({ where: { owner: uid } })`.
3125
+ *
3126
+ * ÖZYİNELEMESİZ kalmak zorunda (N-2): tip maliyeti şekilden değil özyineleme
3127
+ * derinliğinden geliyor (Kysely'nin TS7'de 9,8M instantiation vakası).
3128
+ */
3129
+ /**
3130
+ * `has` — İLİŞKİ ÜZERİNDEN SÜZME, JOIN'in ihtiyaç duyulmayan hâli.
3131
+ *
3132
+ * ```ts
3133
+ * Database.public.interests.findMany({
3134
+ * where: { has: { user_interests: { user_id: uid } } },
3135
+ * orderBy: [{ column: "sort_order" }, { column: "name" }],
3136
+ * })
3137
+ * ```
3138
+ * → `… WHERE t."id" IN (SELECT r1."interest_id" FROM "user_interests" r1
3139
+ * WHERE r1."user_id" = $1)`
3140
+ *
3141
+ * İlişki ADLARI ve gittikleri kolonlar `palbase-env.d.ts`'in ZATEN bastığı
3142
+ * `relations` bloğundan geliyor — yabancı anahtarlardan türetiliyorlar, yazar
3143
+ * hiçbir kolon adı yazmıyor. O blok üretiliyordu ve HİÇBİR okuma onu
3144
+ * kullanmıyordu; `has` onun karşılığı.
3145
+ *
3146
+ * İç filtre AYNI dil: bir tablo ötesinde de `gt`, `icontains`, `OR`, ve iç içe
3147
+ * `has` yazılabiliyor.
3148
+ */
3149
+ /**
3150
+ * İlişkinin HEDEF TABLOSU — anahtar ŞEMA NİTELİKLİ gelir.
3151
+ *
3152
+ * `buildRelations` `to`'yu `qualifiedTableKey` ile yazıyor: public için düz ad
3153
+ * (`"interests"`), başka şema için noktalı (`"billing.invoices"`). `Tables`
3154
+ * yalnız public'i taşır, diğerleri `Schemas`'tadır — noktalı anahtarı doğrudan
3155
+ * `Tables`'ta aramak HER ZAMAN ıskalar ve iç filtre sessizce `Record<string,
3156
+ * unknown>`'a düşerdi: bilinmeyen kolon da, yanlış tip de derlenirdi (ölçüldü).
3157
+ */
3158
+ type RelatedTable<To> = To extends `${infer S}.${infer T}` ? S extends keyof Schemas ? T extends keyof Schemas[S] ? Schemas[S][T] : never : never : To extends keyof Tables ? Tables[To] : never;
3159
+ type RelatedRow<To> = RelatedTable<To> extends {
3160
+ row: infer R;
3161
+ } ? R : Record<string, unknown>;
3162
+ type RelatedRels<To> = RelatedTable<To> extends {
3163
+ relations: infer R;
3164
+ } ? R : unknown;
3165
+ /**
3166
+ * İç içe `has` ÜÇ seviyeyle sınırlı.
3167
+ *
3168
+ * Üretilen `relations` bloğu ÇİFT YÖNLÜ: `customers.invoices` ile
3169
+ * `invoices.customer` birbirini gösteriyor, yani grafiğin kendisi döngülü.
3170
+ * Sınırsız açılım bu döngüyü sonsuz bir tipe çevirirdi — TypeScript'in
3171
+ * "excessively deep" hatası, yazılan sorgunun karmaşıklığından değil ŞEMANIN
3172
+ * şeklinden gelirdi. Üç seviye, ölçtüğümüz tüm gerçek sorguların üstünde.
3173
+ */
3174
+ type HasDepth = 0 | 1 | 2 | 3;
3175
+ type HasDec = [0, 0, 1, 2];
3176
+ type HasNest<Rels, D extends HasDepth> = [keyof Rels] extends [never] ? unknown : D extends 0 ? unknown : {
3177
+ has?: HasFilter<Rels, HasDec[D]>;
3178
+ };
3179
+ type HasFilter<Rels, D extends HasDepth = 3> = {
3180
+ [R in keyof Rels]?: Rels[R] extends {
3181
+ to: infer To;
3182
+ } ? WhereFilter<RelatedRow<To>> & HasNest<RelatedRels<To>, D> : never;
3183
+ };
3184
+ /** İlişki bilgisi taşıyan bir tablo tipinin filtre yüzeyi. */
3185
+ type WhereWithRelations<Row, Rels> = WhereFilter<Row> & {
3186
+ has?: HasFilter<Rels>;
3187
+ };
3188
+ /**
3189
+ * `has` dalı — SADECE `has`, satır filtresi olmadan.
3190
+ *
3191
+ * Plan yolunun filtre dili (`TxWhere`) düz op'unkiyle aynı değil: orada bir
3192
+ * alan ÖNCEKİ bir işlemin satırına referans (`Ref`) taşıyabiliyor. `has`'ı
3193
+ * plana taşımak için o dili kopyalamak gerekmiyor — kesişimle EKLENİYOR.
3194
+ * İlişki yoksa `unknown` dönüyor, çünkü `X & unknown = X`: dal yok olur ve
3195
+ * `has` fazla-alan denetimine takılır.
3196
+ */
3197
+ type HasOnly<Rels> = [unknown] extends [Rels] ? unknown : [keyof Rels] extends [never] ? unknown : {
3198
+ has?: HasFilter<Rels>;
3199
+ };
3200
+ /**
3201
+ * `has` dalı NE ZAMAN var olur.
3202
+ *
3203
+ * İlişkisi OLMAYAN bir tabloda `has` hiç yazılamamalı. `HasFilter<{}>` boş bir
3204
+ * nesne tipidir ve boş nesne tipi HER nesneyi kabul eder — yani dalı koşulsuz
3205
+ * eklemek, ilişkisiz bir tabloda `has: { neyse_ne: {} }`'yi SESSİZCE geçirirdi
3206
+ * (ölçüldü: kapı yeşil, çalışma anında "böyle bir ilişki yok" hatası).
3207
+ */
3208
+ type HasBranch<Row, Rels> = [unknown] extends [Rels] ? never : [keyof Rels] extends [never] ? never : WhereWithRelations<Row, Rels>;
3209
+ type QueryInput<Row, K extends keyof Row = keyof Row, Rels = unknown> = {
3210
+ /**
3211
+ * Filtre — ya tipli filtre dili ya da bir `sqlFragment` (FR-018). İkisi de
3212
+ * AYNI alandır: kademe atlamak bir parametre değişikliği, ayrı bir çağrı
3213
+ * yolu değil (P1).
3214
+ */
3215
+ where?: WhereFilter<Row> | SqlFragment | HasBranch<Row, Rels>;
3216
+ /**
3217
+ * Projeksiyon (FR-009): yalnız bu kolonlar çekilir ve DÖNÜŞ TİPİ buna daralır.
3218
+ *
3219
+ * Tip `Pick<Row, K>` ile TÜRETİLİR, üretilmez: `select` kombinasyonları için
3220
+ * tip basmak, üretilen `.d.ts`'i kombinatoryal olarak şişirirdi (N-1: tablo
3221
+ * başına ≤ 25 satır).
3222
+ */
3223
+ select?: readonly K[];
3224
+ } & FindManyOpts<Row>;
3225
+ /** Bir yazmanın TEK parametresi: neyi (`where`) neye çevirdiğin (`set`). */
3226
+ type MutateInput<Row, Insert, Rels = unknown> = {
3227
+ where: WhereFilter<Row> | SqlFragment | HasBranch<Row, Rels>;
3228
+ set: SetShape<Insert>;
3229
+ };
3230
+ /**
3231
+ * `set`'e yazılabilen değer (FR-012): kolonun kendi tipi ya da — sayısal
3232
+ * kolonlarda — kolonun ŞU ANKİ değerini okuyan bir ifade.
3233
+ *
3234
+ * `increment()` metin ya da boolean kolonda YOK: "kolona ekle"nin orada bir
3235
+ * anlamı olmadığı için ifade edilemez kılınıyor (P5 — anlatmak değil, ifade
3236
+ * edilemez kılmak). `numeric` kolonlar TypeScript'te `string` taşır, o yüzden
3237
+ * string de sayısal sayılır; ayrımı burada yapamayız, ama D-007 zaten miktarın
3238
+ * JS `number`'a hiç uğramamasını istiyor.
3239
+ */
3240
+ type SetValue<V> = V | (NonNullable<V> extends number | string ? TxColumnExpr : never) | (NonNullable<V> extends Date ? TxNow : never);
3241
+ /** Bir update'in `set`'i: insert şeklinin herhangi bir alt kümesi, ifadelerle. */
3242
+ type SetShape<Insert> = {
3243
+ [K in keyof Insert]?: SetValue<Insert[K]>;
3244
+ };
3245
+ /** search() parametreleri, satır tipiyle koşullanmış (FR-013). `offset` BİLEREK yok (UD-013). */
3246
+ interface SearchParamsTyped<T extends TableTypes> {
3247
+ /** Metin sorgusu: FTS kolunu besler; embed beyanlıysa sorgu vektörü de bundan üretilir. */
3248
+ query?: string;
3249
+ /** Hazır sorgu vektörü — verilirse embed çağrısı olmaz (FR-025). */
3250
+ vector?: number[];
3251
+ /** `findMany`'nin filtre dili — İKİNCİ tip argümanıyla, yoksa `Row` sessizce
3252
+ * `unknown`'a düşer ve `col()` burada ifade edilemez olurdu. Motor `search`
3253
+ * için aynı `compileWhereBare`'i kullanıyor; yetenek orada (gözcü I7). */
3254
+ where?: {
3255
+ [K in keyof T["row"]]?: WhereOp<T["row"][K], T["row"]>;
3256
+ };
3257
+ /** default 20, tavan 100 (engine uygular). */
3258
+ limit?: number;
3259
+ /** Birden çok vektör kolonunda hedef seçimi (model geçişi, FR-013/using). */
3260
+ using?: string;
3261
+ mode?: "hybrid" | "text" | "vector";
3262
+ /** Nihai (RRF-sonrası) skor alt eşiği — süzme LIMIT'ten önce uygulanır (FR-001). */
3263
+ minScore?: number;
3264
+ /** Chunk-modunda satır başına en iyi blok sayısı (1..10, vars. 3; FR-015). */
3265
+ blocksPerRow?: number;
3266
+ /** Tazelik çürümesi: nihai skor RRF-sonrası exp(-ln(2)*yaş/halfLife) ile çarpılır;
3267
+ * field bir timestamp kolonu, halfLife "90s" | "15m" | "12h" | "30d" biçiminde (FR-004). */
3268
+ recency?: {
3269
+ field: Extract<keyof T["row"], string>;
3270
+ halfLife: string;
3271
+ };
3272
+ /** Satır-modunda FTS eşleşme vurgusu: sonuç satırına `_highlight` ekler;
3273
+ * chunk-modda no-op — bloklar zaten eşleşen kesittir (FR-025). */
3274
+ highlight?: boolean;
3275
+ /** Validity'li tabloda zaman penceresi: varsayılan yalnız güncel versiyon;
3276
+ * "all" tüm versiyonlar; {asOf} o anda geçerli olan (FR-029). */
3277
+ validity?: "all" | {
3278
+ asOf: string;
3279
+ };
3280
+ /** Alan-boost (FR-030): skor * (1 + w·x/(1+x)) — sayısal kolonla sınırlı
3281
+ * çarpan, dış servissiz; bileşim RRF → boost → recency → minScore. */
3282
+ boost?: {
3283
+ field: Extract<keyof T["row"], string>;
3284
+ weight: number;
3285
+ };
3286
+ }
3287
+ /**
3288
+ * `facets()` dönüşü (FR-027, FR-058): kolon adı → o kolonun top-20 değeri ve
3289
+ * sayacı.
3290
+ *
3291
+ * Bu tip bir zamanlar `search()` dönüş DİZİSİNİN üstünde taşınıyordu; dizi-üstü
3292
+ * özellik `JSON.stringify`'da kayboluyor, yani bir kontrolcü onu döndürmeye
3293
+ * çalıştığında yanıt gövdesinde hiç görünmüyordu. Sayaçlar artık BAĞIMSIZ
3294
+ * dönüşle geliyor ve tip de o dönüşü adlandırıyor.
3295
+ */
3296
+ type SearchFacets = Record<string, {
3297
+ value: string | null;
3298
+ count: number;
3299
+ }[]>;
3300
+ /** similar()/recommend() taşıyıcı opsiyonları (T018, FR-022): search'ün
3301
+ * paramlarından query/vector/mode düşer — hedef vektörü metodun kendisi
3302
+ * DB'den kurar; facets/highlight de düşer (T020) — engine bu ikisini
3303
+ * similar/recommend'e geçirmez, tip vaadi gerçekle aynı kalır. */
3304
+ type SimilarParamsTyped<T extends TableTypes> = Omit<SearchParamsTyped<T>, "query" | "vector" | "mode" | "facets" | "highlight">;
3305
+ /** recommend() parametreleri (T018, FR-023). */
3306
+ type RecommendParamsTyped<T extends TableTypes> = SimilarParamsTyped<T> & {
3307
+ /** Kaynak beğeniler — hedef vektör bunların DB-içi avg'ı; boş olamaz. */
3308
+ positive: string[];
3309
+ /** İtilen örnekler — hedef pos.v + (pos.v - neg.v) ile yönlenir. */
3310
+ negative?: string[];
3311
+ };
3312
+ /**
3313
+ * Tablonun ilişki bloğu — env `Tables` girdisinden.
3314
+ *
3315
+ * TEK yerde türetiliyor ve HER op'a aynısı veriliyor: `has` yalnız `findMany`'de
3316
+ * olsaydı, "aynı filtre dili her op'ta" sözü ilk `deleteMany`'de kırılırdı.
3317
+ */
3318
+ type RelsOf<T> = T extends {
3319
+ relations: infer R;
3320
+ } ? R : unknown;
3321
+ /** Temel tablo erişimcisi — search'süz beş op. */
3322
+ interface EnvTypedTableBase<T extends TableTypes> {
3323
+ insert(data: T["insert"]): Promise<T["row"]>;
3324
+ /**
3325
+ * Bir idempotency anahtarını sahiplen (FR-033).
3326
+ *
3327
+ * ```ts
3328
+ * const { inserted, row } = await Database.public.payments.claim(
3329
+ * { idem_key: req.headers["idempotency-key"] },
3330
+ * { amount, user_id },
3331
+ * );
3332
+ * if (!inserted) return row; // aynı istek ikinci kez geldi
3333
+ * ```
3334
+ *
3335
+ * `inserted: false` bir HATA DEĞİL: dönen satır ilk çağrının satırıdır.
3336
+ * İlk argüman satırı BULAN alanlar, ikincisi yalnız yazılanlar — ikinci çağrı
3337
+ * farklı bir yük gönderse bile satır anahtarla bulunur.
3338
+ */
3339
+ claim(unique: Partial<T["insert"]>, extra?: Partial<T["insert"]>): Promise<{
3340
+ inserted: boolean;
3341
+ row: T["row"];
3342
+ }>;
3343
+ /**
3344
+ * Satırı yaz; `onConflict` kolonlarında çakışırsa ÜZERİNE yaz (FR-034).
3345
+ *
3346
+ * ```ts
3347
+ * await Database.public.settings.put({
3348
+ * data: { user_id, theme: "dark" },
3349
+ * onConflict: ["user_id"],
3350
+ * });
3351
+ * ```
3352
+ *
3353
+ * `onConflict` kolonları benzersiz bir kısıt ya da index taşımalı — Postgres
3354
+ * onlarla eşleştirir — ve güncellemeden dışlanırlar, çünkü eşleşen şey onlar.
3355
+ *
3356
+ * **`upsert`'ün yerine geldi ve adı bilerek değişti.** `upsert` tek isim
3357
+ * altında iki niyet taşıyordu: "yoksa ekle varsa güncelle" (bu) ve "idempotent
3358
+ * yaz" (artık {@link EnvTypedTableBase.claim}). İkincisi için ÖLÇÜLMÜŞ biçimde
3359
+ * yanlıştı: `DO UPDATE` ikinci çağrının verisiyle birincininkini EZİYOR
3360
+ * (10.00 → 999.00 ölçüldü). İki niyeti tek isimde tutmak, yanlış olanı
3361
+ * seçmeyi kolay yapıyordu.
3362
+ */
3363
+ put(q: {
3364
+ data: T["insert"];
3365
+ onConflict: readonly Extract<keyof T["row"], string>[];
3366
+ }): Promise<T["row"]>;
3367
+ /** Update the row by id; resolves to the updated row, or `null` if no row
3368
+ * matched (absent or RLS-hidden) — an idempotent outcome, mirroring
3369
+ * `findById`. The runtime returns a null row rather than throwing. */
3370
+ update(q: {
3371
+ where: {
3372
+ id: string;
3373
+ };
3374
+ set: Partial<T["insert"]>;
3375
+ }): Promise<T["row"] | null>;
3376
+ delete(id: string): Promise<void>;
3377
+ findById(id: string): Promise<T["row"] | null>;
3378
+ /** Rows matching the filter. See {@link WhereFilter} / {@link FindManyOpts} —
3379
+ * this declaration is what makes the engine's operators callable. */
3380
+ findMany<K extends keyof T["row"] = keyof T["row"]>(q?: QueryInput<T["row"], K, RelsOf<T>>): Promise<Pick<T["row"], K>[]>;
3381
+ /**
3382
+ * Update every matching row in one statement; an empty filter is refused.
3383
+ *
3384
+ * **0 satır dönmesi hata DEĞİL, başarı da değil** (FR-014): koşulu `where`'e
3385
+ * koyup dönen diziyi kontrol etmek, "önce oku sonra yaz"ın yarış koşulu
3386
+ * olmayan hâlidir.
3387
+ *
3388
+ * ```ts
3389
+ * const [row] = await Database.public.accounts.updateMany({
3390
+ * where: { id, balance: { gte: amount } },
3391
+ * set: { balance: decrement(amount) },
3392
+ * });
3393
+ * if (row === undefined) throw new Conflict("yetersiz bakiye");
3394
+ * ```
3395
+ */
3396
+ updateMany(q: MutateInput<T["row"], T["insert"], RelsOf<T>>): Promise<T["row"][]>;
3397
+ /** Delete every matching row; resolves to how many. Empty filter refused. */
3398
+ deleteMany(q: {
3399
+ where: WhereFilter<T["row"]> | SqlFragment | HasBranch<T["row"], RelsOf<T>>;
3400
+ }): Promise<number>;
3401
+ /** How many rows match. An empty filter is legitimate: counting is a read. */
3402
+ count(q?: {
3403
+ where?: WhereFilter<T["row"]> | SqlFragment | HasBranch<T["row"], RelsOf<T>>;
3404
+ }): Promise<number>;
3405
+ /** Validity'li tabloda satırın yeni versiyonu (FR-029, C-9): eski satır
3406
+ * kapanır (valid_to/superseded_by), yenisi TEK savepoint'te eklenir; dönüş
3407
+ * yeni satır. Validity beyanı olmayan tabloda adlandırılmış çalışma-zamanı
3408
+ * hatası — tip düzeyinde ayrım env `Tables` bayrağı taşımadığından yapılamaz. */
3409
+ supersede(id: string, row: T["insert"]): Promise<T["row"]>;
3410
+ }
3411
+ /** Tablo erişimcisi: env girdisi `searchable: true` taşıyorsa (vector kolonu ya da
3412
+ * search beyanı — env-gen üretir) `search()` üyesi VARDIR; yoksa üye hiç yoktur ve
3413
+ * çağrı derleme hatasıdır (FR-013). Yapısal koşul TableTypes'ı genişletmeden çalışır. */
3414
+ /**
3415
+ * `appendOnly` tabloda YAYIMLANMAYAN üyeler (FR-031).
3416
+ *
3417
+ * Altısı da ayrı ayrı: biri unutulursa append-only sözü o üye üzerinden sessizce
3418
+ * delinir. `insert` ve okuma üyeleri kalır — düzeltme SİLMEKLE değil, telafi
3419
+ * kaydı EKLEMEKLE yapılır (FR-032).
3420
+ */
3421
+ type AppendOnlyForbidden = "update" | "updateMany" | "delete" | "deleteMany" | "put" | "supersede";
3422
+ type EnvTypedTable<T extends TableTypes> = (T extends {
3423
+ appendOnly: true;
3424
+ } ? Omit<EnvTypedTableBase<T>, AppendOnlyForbidden> : EnvTypedTableBase<T>) & (T extends {
3425
+ searchable: true;
3426
+ } ? {
3427
+ search(params: SearchParamsTyped<T>): Promise<Array<T["row"] & {
3428
+ _score: number;
3429
+ _highlight?: string;
3430
+ }>>;
3431
+ /** "Bu satıra benzeyenler" (FR-022): hedef vektör DB'den okunur,
3432
+ * kaynak satır sonuçta yoktur; id yoksa adlandırılmış hata. */
3433
+ similar(id: string, params?: SimilarParamsTyped<T>): Promise<Array<T["row"] & {
3434
+ _score: number;
3435
+ }>>;
3436
+ /** D-021: sayaçlar bağımsız dönüşle — search'ün dizi-üstü _facets'i
3437
+ * JSON.stringify'da kaybolur; ciddi sözleşme budur. */
3438
+ facets(params: {
3439
+ facets: Array<keyof T["row"] & string>;
3440
+ where?: Partial<T["row"]>;
3441
+ validity?: "all" | {
3442
+ asOf: string;
3443
+ };
3444
+ }): Promise<SearchFacets>;
3445
+ /** positive/negative beğenilerden öneri (FR-023): hedef vektör DB-içi
3446
+ * avg CTE'leriyle; kaynak id'ler sonuçta yoktur. */
3447
+ recommend(params: RecommendParamsTyped<T>): Promise<Array<T["row"] & {
3448
+ _score: number;
3449
+ }>>;
3450
+ } : Record<never, never>);
3451
+ /** The `tables` map exposed on `Database`/`tx`, keyed by the env `Tables`
3452
+ * interface. When no schema is declared `Tables` is empty, so `tables` is an
3453
+ * empty object — accessing `.tables.foo` is then a compile error (no member). */
3454
+ type EnvTables = {
3455
+ [K in keyof Tables]: EnvTypedTable<Tables[K]>;
3456
+ };
3457
+ /**
3458
+ * The project's NON-public schemas, keyed by schema name, each exposing its own
3459
+ * `tables` map — the shape `Database.schema("billing")` returns.
3460
+ *
3461
+ * The intermediate `tables` is there for the reason {@link EnvTables} sits under
3462
+ * `.tables`: a schema's table names must not share a namespace with anything the
3463
+ * accessor itself might grow.
3464
+ *
3465
+ * Empty by default. The generated `palbase-env.d.ts` augments `Schemas` with one
3466
+ * member per declared schema other than `public`, so a project that declares
3467
+ * none has `keyof Schemas = never` and every `schema(...)` call is a compile
3468
+ * error rather than a runtime surprise.
3469
+ */
3470
+ /**
3471
+ * @deprecated ARTIK VAR OLMAYAN BİR ŞEKLİ tarif ediyor. Bu tip her şemayı
3472
+ * `{ tables: … }` altında gösteriyor; yüzey FR-001 ile `Database.<şema>.<tablo>`
3473
+ * oldu ve `.tables` ara katmanı KALKTI. Güncel şekil: {@link EnvSchemaSurface}.
3474
+ *
3475
+ * Kaldırılmadı çünkü dışa açık bir tip ve kaldırmak SEKİZİNCİ bir kırıcı olurdu;
3476
+ * ama bir tip, çalışma zamanının ÜRETMEDİĞİ bir şekli tarif ediyorsa yalan
3477
+ * söylüyor demektir — `const x: EnvSchemas["billing"] = Database.billing`
3478
+ * açıklanamayan bir derleme hatası verir. Bir sonraki kırıcı sürümde silinmeli
3479
+ * (defter D-18).
3480
+ *
3481
+ * Bu tipi ÜRETEN iki fonksiyon (`makeTablesAccessor`, `makeSchemaAccessor`)
3482
+ * ölçüldü: hiçbir yerden çağrılmıyorlardı ve silindiler. Canlı yol
3483
+ * `makeTypedSurface`'ın proxy'si.
3484
+ */
3485
+ type EnvSchemas = {
3486
+ [S in keyof Schemas]: {
3487
+ tables: {
3488
+ [T in keyof Schemas[S]]: EnvTypedTable<Extract<Schemas[S][T], TableTypes>>;
3489
+ };
3490
+ };
3491
+ };
3492
+ /** The project's tables as PLAN operations, keyed by the env `Tables`
3493
+ * interface. The transaction twin of {@link EnvTables}. */
3494
+ /** `appendOnly` bir tabloda transaction planının da kaybettiği üyeler. Doğrudan
3495
+ * yüzeyin `AppendOnlyForbidden`'ının plan-yolu ikizi; adlar farklı çünkü plan
3496
+ * yüzeyinin üye adları farklı. */
3497
+ type TxAppendOnlyForbidden = "put" | "updateWhere" | "deleteWhere";
3498
+ /**
3499
+ * Plan yüzeyi de `appendOnly`'yi UYGULUYOR — ve uygulamıyor olması bir kusurdu.
3500
+ *
3501
+ * `EnvTypedTable` altı üyeyi Omit ediyordu, ama `TxTables` hiçbirini: yani
3502
+ * `tx.tables.entries.updateWhere(…)` ve `.put(…)` DERLENİYORDU. Tip bir şeyi
3503
+ * "ifade edilemez" ilan edip ikinci bir kapıda ifade edilebilir bırakırsa,
3504
+ * ilan yalandır (doğrulayıcı gözcü, FR-031).
3505
+ *
3506
+ * Motor da aynı üçünü adıyla reddediyor — tip atlanınca susmaması için.
3507
+ */
3508
+ type TxTables = {
3509
+ [K in keyof Tables]: Tables[K] extends {
3510
+ appendOnly: true;
3511
+ } ? Omit<TxTable<Tables[K]["row"], Tables[K]["insert"], RelsOf<Tables[K]>>, TxAppendOnlyForbidden> : TxTable<Tables[K]["row"], Tables[K]["insert"], RelsOf<Tables[K]>>;
3512
+ };
3513
+ /** Bir şemanın tabloları PLAN operasyonları olarak — `TxTables`'ın public
3514
+ * DIŞI şemalar için ikizi. */
3515
+ type TxTablesOf<S> = {
3516
+ [T in keyof S]: Extract<S[T], TableTypes> extends {
3517
+ appendOnly: true;
3518
+ } ? Omit<TxTable<Extract<S[T], TableTypes>["row"], Extract<S[T], TableTypes>["insert"], RelsOf<Extract<S[T], TableTypes>>>, TxAppendOnlyForbidden> : TxTable<Extract<S[T], TableTypes>["row"], Extract<S[T], TableTypes>["insert"], RelsOf<Extract<S[T], TableTypes>>>;
3519
+ };
3520
+ /**
3521
+ * Plan tutamağının şema yüzeyi — `EnvSchemaSurface`'ın BİREBİR ikizi.
3522
+ *
3523
+ * NEDEN AYNI ŞEKİL: `Database.billing.invoices` yazılabiliyorken
3524
+ * `tx.billing.invoices` yazılamıyordu, yani `billing` şemasındaki iki tabloyu
3525
+ * TEK ATOMİK PLANDA yazmak imkânsızdı — yazar `$query`'ye düşüyor, tipi ve RLS
3526
+ * yardımını kaybediyordu. Bu yüzeyin kapatmak için var olduğu düşüşün ta
3527
+ * kendisi, ve GOAL'ün "ciddi ölçekli fintech" yarısının tam ortasında
3528
+ * (nihai inceleme I-6).
3529
+ */
3530
+ type TxSchemaSurface = {
3531
+ public: TxTables;
3532
+ } & {
3533
+ [S in keyof Schemas]: TxTablesOf<Schemas[S]>;
3534
+ };
3535
+ /**
3536
+ * The handle a `Database.$transaction(…)` callback receives.
3537
+ *
3538
+ * Tables only — no `query`, no `findById`, no `asService`. A read whose value
3539
+ * the plan does not write belongs outside the transaction, where it costs one
3540
+ * round trip and is an ordinary value you can branch on.
3541
+ *
3542
+ * `tx.public.x` ve `tx.<şema>.x`, `Database` ile aynı şekil. `tx.tables.x`
3543
+ * public'in TAKMA ADI olarak duruyor: bu run'ın göç notu onu öğretiyor ve her
3544
+ * mevcut çağrı onu kullanıyor — kaldırmak sekizinci bir kırıcı olurdu ve
3545
+ * hiçbir şey kazandırmazdı.
3546
+ */
3547
+ type TxPlan = TxPlanHandle<TxTables> & TxSchemaSurface;
3548
+ /**
3549
+ * The RLS-bypass sibling returned by `Database.$asService()`. Same typed surface
3550
+ * as {@link EnvTypedDatabase} — `tables`, the raw string ops, and a typed
3551
+ * `transaction` — but it does NOT re-expose `asService` (no double-bypass).
3552
+ * Every op it performs runs as the `service_role` (BYPASSRLS).
3553
+ */
3554
+ type EnvServiceDatabase = EnvSchemaSurface & {
3555
+ /** Ham SQL. Generic verilmezse `unknown[]` döner (FR-021). */
3556
+ $query: DBClient["query"];
3557
+ $insert: DBClient["insert"];
3558
+ $update: DBClient["update"];
3559
+ $delete: DBClient["delete"];
3560
+ $findById: DBClient["findById"];
3561
+ $findMany: DBClient["findMany"];
3562
+ $put: DBClient["put"];
3563
+ $updateMany: DBClient["updateMany"];
3564
+ $deleteMany: DBClient["deleteMany"];
3565
+ $count: DBClient["count"];
3566
+ $search: DBClient["search"];
3567
+ $similar: RecoOps["similar"];
3568
+ $recommend: RecoOps["recommend"];
3569
+ $facets: DBClient["facets"];
3570
+ $supersede: DBClient["supersede"];
3571
+ $claim: DBClient["claim"];
3572
+ $lockRows: DBClient["lockRows"];
3573
+ $advisoryXactLock: DBClient["advisoryXactLock"];
3574
+ $attempt: DBClient["attempt"];
3575
+ /** Bkz. {@link EnvTypedDatabase.$transaction}. */
3576
+ /**
3577
+ * `opts.retry` verilirse, `SerializationFailure` / `DeadlockDetected`
3578
+ * alındığında plan BAŞTAN kurulup yeniden çalıştırılır (FR-037).
3579
+ * Varsayılan 0 — sessiz bir retry, idempotent olmayan bir işlemi çağıranın
3580
+ * haberi olmadan iki kez çalıştırırdı.
3581
+ */
3582
+ $transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T, opts?: {
3583
+ retry?: number;
3584
+ }): Promise<Materialized<T>>;
3585
+ };
3586
+ /**
3587
+ * The typed-by-default Database surface: the raw string-keyed `DBClient` ops
3588
+ * PLUS a `tables` map typed against the project's generated `palbase-env.d.ts`,
3589
+ * a `transaction` that runs a whole plan in one request, and `asService()` for
3590
+ * the explicit RLS-bypass sibling.
3591
+ *
3592
+ * The low-level `txPlan` op is deliberately NOT re-exposed here: `transaction`
3593
+ * is the surface, and a hand-built plan would bypass the ref/guard machinery
3594
+ * that makes one safe to write.
3595
+ */
3596
+ type EnvSchemaSurface = {
3597
+ public: EnvTables;
3598
+ } & {
3599
+ [S in keyof Schemas]: {
3600
+ [T in keyof Schemas[S]]: EnvTypedTable<Extract<Schemas[S][T], TableTypes>>;
3601
+ };
3602
+ };
3603
+ /**
3604
+ * Projenin veri yüzeyi: `Database.<şema>.<tablo>` + `$` önekli sistem üyeleri.
3605
+ *
3606
+ * Bugünkü `tables` ve `schema("x")` ikilisinin yerine geçer (FR-001). İki yol
3607
+ * tek yola iner: `public` de diğer şemalar gibi adıyla anılır, ara katman yoktur.
3608
+ *
3609
+ * @example
3610
+ * await Database.public.notes.findMany({ where: { owner: uid } });
3611
+ * await Database.billing.invoices.findMany({ where: { paid: false } });
3612
+ * await Database.$transaction((tx) => { … });
3613
+ */
3614
+ type EnvTypedDatabase = EnvSchemaSurface & {
3615
+ /** Ham SQL. Generic verilmezse `unknown[]` döner (FR-021). */
3616
+ $query: DBClient["query"];
3617
+ $insert: DBClient["insert"];
3618
+ $update: DBClient["update"];
3619
+ $delete: DBClient["delete"];
3620
+ $findById: DBClient["findById"];
3621
+ $findMany: DBClient["findMany"];
3622
+ $put: DBClient["put"];
3623
+ $updateMany: DBClient["updateMany"];
3624
+ $deleteMany: DBClient["deleteMany"];
3625
+ $count: DBClient["count"];
3626
+ $search: DBClient["search"];
3627
+ $similar: RecoOps["similar"];
3628
+ $recommend: RecoOps["recommend"];
3629
+ $facets: DBClient["facets"];
3630
+ $supersede: DBClient["supersede"];
3631
+ $claim: DBClient["claim"];
3632
+ $lockRows: DBClient["lockRows"];
3633
+ $advisoryXactLock: DBClient["advisoryXactLock"];
3634
+ $attempt: DBClient["attempt"];
3635
+ /** RLS'i bypass eden kardeş yüzey. Kendisi `$asService` TAŞIMAZ — çift bypass yok. */
3636
+ $asService(): EnvServiceDatabase;
3637
+ /**
3638
+ * Bir transaction çalıştırır. Callback işlemleri TARİF eder; tarifin tamamı
3639
+ * tek istekte gider ve broker onu tek bir transaction içinde koşturur.
3640
+ * Callback SENKRONDUR: döndüğünde hiçbir şey çalışmamıştır.
3641
+ */
3642
+ /**
3643
+ * `opts.retry` verilirse, `SerializationFailure` / `DeadlockDetected`
3644
+ * alındığında plan BAŞTAN kurulup yeniden çalıştırılır (FR-037).
3645
+ * Varsayılan 0 — sessiz bir retry, idempotent olmayan bir işlemi çağıranın
3646
+ * haberi olmadan iki kez çalıştırırdı.
3647
+ */
3648
+ $transaction<T>(fn: (tx: TxPlan) => T extends Promise<unknown> ? never : T, opts?: {
3649
+ retry?: number;
3650
+ }): Promise<Materialized<T>>;
3651
+ };
3652
+
3653
+ /**
3654
+ * tx-plan.ts — `Database.$transaction()` as a PLAN, not a pinned session.
3655
+ *
3656
+ * A transaction used to be a conversation: BEGIN, then one network round trip
3657
+ * per operation, then COMMIT. Each of those round trips cost ~4 ms and, because
3658
+ * the pooler runs in transaction mode, an open transaction pinned a Postgres
3659
+ * backend for the whole conversation. A 121-operation statement upload pinned
3660
+ * one backend for ~490 ms.
3661
+ *
3662
+ * So the callback no longer TALKS to the database. It DESCRIBES what should
3663
+ * happen; the description is serialised and sent once; the broker runs the whole
3664
+ * thing inside one transaction and answers once. Committing on return and
3665
+ * rolling back on throw is unchanged — that is the only property tenant code
3666
+ * actually asked for.
3667
+ *
3668
+ * The consequences, stated plainly, because they are the whole design:
3669
+ *
3670
+ * - The callback is SYNCHRONOUS. There is nothing to await: no statement has
3671
+ * run yet when it returns. `async` on the callback and `await` inside it are
3672
+ * both compile errors (see {@link TxPlan} and {@link NotAwaitable}).
3673
+ * - `insert()` does not hand back a row, it hands back {@link TxRows}. Reading
3674
+ * a field requires `.expectOne(err)` first, which makes "what if the row
3675
+ * isn't there" a question you cannot route around: it is the argument.
3676
+ * - A field read from a row is a {@link Ref} — a PROMISE OF A VALUE THE SERVER
3677
+ * WILL PRODUCE, not the value. It can be written into a later operation and
3678
+ * it can be returned from the callback (it is substituted for the real value
3679
+ * before `transaction()` resolves). It cannot be branched on. See the
3680
+ * "Truthiness" note below — this is the sharp edge of the whole design.
3681
+ * - Control flow that needs a real value must move OUT of the callback: read
3682
+ * before the transaction, or express the condition as a guard
3683
+ * (`updateWhere({ id, accepted_at: null }, …).expectOne(new Conflict(…))`)
3684
+ * which the server evaluates and which rolls the whole plan back.
3685
+ *
3686
+ * # Truthiness — the hole this file CANNOT close
3687
+ *
3688
+ * JavaScript does not let a Proxy trap truthiness. `if (ref)` takes the true
3689
+ * branch, always, for every Ref, and no `get` handler ever runs. `tsc` is silent
3690
+ * because a Ref is a perfectly good object. So:
3691
+ *
3692
+ * const pot = tx.tables.pots.select({ id }, { limit: 1 }).expectOne(e);
3693
+ * if (!pot.balance) { … } // ← ALWAYS false. Silently wrong data.
3694
+ *
3695
+ * What this file does close: coercion (`Symbol.toPrimitive`/`valueOf`/
3696
+ * `toString`), awaiting (`then` is a callable member with a non-thenable
3697
+ * signature, which is a *compile* error), serialisation (`toJSON`), and nesting
3698
+ * a Ref inside a literal value where the server would store it as data. What it
3699
+ * cannot close is a bare truthiness test. The real defence is the build-time
3700
+ * static analysis (`tx_analysis.js`, phase P4); until that ships, this hole is
3701
+ * open and this comment is the only warning.
3702
+ *
3703
+ * # Wire contract
3704
+ *
3705
+ * The JSON this file emits is consumed by
3706
+ * the plan executor in `engine/db.ts`. That executor rejects
3707
+ * unknown fields at every level, so an op carries EXACTLY the fields its kind
3708
+ * takes. Everything here that looks like a needless restriction is one of the
3709
+ * server's rules made visible early:
3710
+ *
3711
+ * - `$ref` only points BACKWARDS, and only at an op statically known to yield
3712
+ * at most one row (insert, or a `one`/`atMost 1` guard, or `select limit 1`).
3713
+ * `.expectOne()` is what this file uses to satisfy that, always.
3714
+ * - `$expr` is a closed set: `inc`/`dec` (update only — they read the column's
3715
+ * current value) and `now()`.
3716
+ * - `update`/`delete` require a `where`; `insert` refuses one.
3717
+ * - `insertMany` rows must all set the same columns.
3718
+ * - ≤1000 ops, ≤5000 rows per insertMany, ≤8 MiB of JSON.
3719
+ *
3720
+ * Column keys are emitted SORTED, so the same callback always produces byte-
3721
+ * identical JSON. That is what lets the Go decoder be locked to golden files
3722
+ * this SDK emits (`testdata/tx_plan_golden/`).
3723
+ */
3724
+ /**
3725
+ * A plan handle was used as if it were a value: awaited, coerced to a string or
3726
+ * number, serialised, or nested inside another value.
3727
+ *
3728
+ * Thrown while the callback is still BUILDING the plan, so nothing has been sent
3729
+ * and nothing has been written.
3730
+ */
3731
+
3732
+ declare class TxRefError extends Error {
3733
+ constructor(message: string);
3734
+ }
3735
+ /**
3736
+ * The plan the callback described cannot be sent: it breaks a rule the server
3737
+ * would reject, and rejecting it here names the line that wrote it instead of
3738
+ * returning a 400 about an op index.
3739
+ */
3740
+ declare class TxPlanError extends Error {
3741
+ constructor(message: string);
3742
+ }
3743
+ /** A backwards reference to an earlier op's single-row result. */
3744
+ interface TxWireRef {
3745
+ $ref: {
3746
+ op: number;
3747
+ field: string;
3748
+ };
3749
+ }
3750
+ /** A call from the server's closed function set. */
3751
+ interface TxWireExpr {
3752
+ $expr: {
3753
+ fn: "inc" | "dec";
3754
+ by: number | string;
3755
+ } | {
3756
+ fn: "now";
3757
+ };
3758
+ }
3759
+ /** One value in a `values`/`set`/`where` map: a literal, a `$ref` or a `$expr`. */
3760
+ type TxWireValue = TxWireRef | TxWireExpr | unknown;
3761
+ /** The tenant's declared expectation about an op's row count. `slot` indexes the
3762
+ * client-side error table — the error OBJECT never travels. */
3763
+ interface TxWireGuard {
3764
+ kind: "one" | "none" | "atLeast" | "atMost";
3765
+ n: number;
3766
+ slot: number;
3767
+ }
3768
+ /** One operation in the wire plan. Fields are omitted, never null: the decoder
3769
+ * rejects a field that does not belong to the op's kind. */
3770
+ interface TxWireOp {
3771
+ /** upsert and insertMany: the columns Postgres matches on. */
3772
+ onConflict?: readonly string[];
3773
+ /** insertMany only: what a collision does. Absent means no ON CONFLICT clause
3774
+ * at all, which is what every insertMany did before this option existed. */
3775
+ action?: "ignore" | "update";
3776
+ op: "insert" | "insertMany" | "upsert" | "update" | "delete" | "select";
3777
+ table: string;
3778
+ values?: Record<string, TxWireValue>;
3779
+ rows?: Record<string, TxWireValue>[];
3780
+ set?: Record<string, TxWireValue>;
3781
+ where?: Record<string, TxWireValue>;
3782
+ limit?: number;
3783
+ lock?: "update";
3784
+ guard?: TxWireGuard;
3785
+ }
3786
+ /** The plan the engine executes on the request's own transaction. */
3787
+ interface TxPlanBody {
3788
+ ops: TxWireOp[];
3789
+ }
3790
+ /** One op's outcome, positionally matched to the plan's ops. */
3791
+ interface TxPlanOpResult {
3792
+ rows: Record<string, unknown>[];
3793
+ rows_affected: number;
3794
+ }
3795
+ /** One result per op, in plan order. */
3796
+ interface TxPlanResponse {
3797
+ results: TxPlanOpResult[];
3798
+ }
3799
+ /**
3800
+ * The fields the runtime must copy from the broker's error envelope onto the
3801
+ * rejection it throws out of {@link DBClient.txPlan}.
3802
+ *
3803
+ * `slot` is the whole point: on a guard failure the server answers with the
3804
+ * INDEX of the expectation that did not hold, never with an error message of its
3805
+ * own, and this SDK maps that index back to the `Error` the callback handed to
3806
+ * `.expectOne(…)`. Without `slot` a guard failure degrades to a generic 409.
3807
+ */
3808
+ interface TxPlanRejection {
3809
+ status?: number;
3810
+ /** `tx_plan_invalid` | `tx_guard_failed` | `tx_ref_unresolved` | a pg class. */
3811
+ error_code?: string;
3812
+ /** Present only for `tx_guard_failed`: the client-side error table index. */
3813
+ slot?: number;
3814
+ /** Present on a database error: which op failed. */
3815
+ op?: number;
3816
+ }
3817
+ declare const refBrand: unique symbol;
3818
+ declare const rowBrand: unique symbol;
3819
+ declare const rowsBrand: unique symbol;
3820
+ /**
3821
+ * Makes a handle a compile error to `await`.
3822
+ *
3823
+ * `then` is declared as a CALLABLE member whose signature is not `PromiseLike`,
3824
+ * which is precisely the shape TypeScript rejects: `await handle` is TS1320 and
3825
+ * `async () => handle` is TS1058. A non-callable `then` would not do it — the
3826
+ * compiler simply ignores those.
3827
+ */
3828
+ interface NotAwaitable {
3829
+ /** Not a promise. Nothing here has run yet; there is nothing to await. */
3830
+ then(doNotAwaitAPlanHandle: "a transaction plan is built synchronously"): never;
3831
+ }
3832
+ /**
3833
+ * A value the SERVER will produce, standing in for a column of a row this plan
3834
+ * writes or reads.
3835
+ *
3836
+ * Legal uses: write it into a later operation's `values`/`set`/`where`, or
3837
+ * return it from the callback (it is replaced by the real value before
3838
+ * `transaction()` resolves).
3839
+ *
3840
+ * Illegal, and caught: `await`, `String(ref)`, `` `${ref}` ``, `ref + 1`,
3841
+ * `JSON.stringify(ref)`, burying it inside a jsonb object.
3842
+ *
3843
+ * Illegal, and NOT caught: `if (ref)`. See the truthiness note at the top.
3844
+ */
3845
+ interface Ref<T> extends NotAwaitable {
3846
+ readonly [refBrand]: T;
3847
+ }
3848
+ /** The brand carried by a single-row handle, and the seam `Materialized` reads
3849
+ * to turn `return st` into the whole row. */
3850
+ interface TxRowHandle<Row> extends NotAwaitable {
3851
+ readonly [rowBrand]: Row;
3852
+ }
3853
+ /**
3854
+ * A row this plan is known to produce exactly one of. Every property is a
3855
+ * {@link Ref}; returning the handle itself yields the whole row.
3856
+ *
3857
+ * Only `.expectOne(err)` produces one — which is the design: a row you can read
3858
+ * fields from is a row whose absence you have already answered for.
3859
+ */
3860
+ type TxRow<Row> = {
3861
+ readonly [K in keyof Row]: Ref<Row[K]>;
3862
+ } & TxRowHandle<Row>;
3863
+ /**
3864
+ * The result of one operation, before any expectation is declared about it.
3865
+ *
3866
+ * Deliberately not a row and not a list: an operation's row count is not known
3867
+ * until the server runs it, so the only thing that can be said about it here is
3868
+ * an EXPECTATION. Declaring one is also the only way to get a readable row.
3869
+ *
3870
+ * At most one expectation per operation — the wire carries one guard per op, and
3871
+ * a second call throws rather than silently dropping the first.
3872
+ */
3873
+ interface TxRows<Row> extends NotAwaitable {
3874
+ readonly [rowsBrand]: Row;
3875
+ /**
3876
+ * Require exactly one row, and read it. On any other count the server rolls
3877
+ * the whole transaction back and this `error` is thrown to the caller.
3878
+ *
3879
+ * This is the only way to reach a row's fields, and the only shape a `$ref`
3880
+ * may point at.
3881
+ */
3882
+ expectOne(error: Error): TxRow<Row>;
3883
+ /** Require zero rows (e.g. "this membership must not already exist"). */
3884
+ expectNone(error: Error): void;
3885
+ /** Require at least `n` rows. */
3886
+ expectAtLeast(n: number, error: Error): void;
3887
+ /** Require at most `n` rows. */
3888
+ expectAtMost(n: number, error: Error): void;
3889
+ }
3890
+ /** `now()` — the server's clock, usable wherever a value is. */
3891
+ interface TxNow extends NotAwaitable {
3892
+ readonly $expr: {
3893
+ fn: "now";
3894
+ };
3895
+ }
3896
+ /** `inc(n)` / `dec(n)` — read the column's CURRENT value and write it back
3897
+ * changed. Only meaningful in an update's `set`, which is where the types allow
3898
+ * it and where the server allows it. */
3899
+ interface TxColumnExpr extends NotAwaitable {
3900
+ readonly $expr: {
3901
+ fn: "inc" | "dec";
3902
+ by: number | string;
3903
+ };
3904
+ }
3905
+ /**
3906
+ * Resolve a callback's return type against what actually comes back: every
3907
+ * {@link Ref} becomes its value, every {@link TxRow} becomes its row, and
3908
+ * anything else keeps its shape.
3909
+ *
3910
+ * A {@link TxRows} resolves to an explanatory string type rather than a row
3911
+ * list: it has no single answer to give, and saying so in the type is louder
3912
+ * than a runtime throw.
3913
+ */
3914
+ type Materialized<T> = T extends Ref<infer U> ? U : T extends TxRowHandle<infer R> ? R : T extends TxRows<unknown> ? "a TxRows cannot leave the transaction callback — read a row with .expectOne(err) first" : T extends Date ? T : T extends object ? {
3915
+ [K in keyof T]: Materialized<T[K]>;
3916
+ } : T;
3917
+ /** A value written by an INSERT: a literal, an earlier row's field, or `now()`.
3918
+ * `inc`/`dec` are absent on purpose — they read a current value, and an inserted
3919
+ * row has none. */
3920
+ type TxInsertValue<V> = V | Ref<V> | TxNow;
3921
+ /** A value written by an UPDATE's `set`: everything an insert takes, plus the
3922
+ * read-modify-write expressions. */
3923
+ /**
3924
+ * TEK KURAL: ifade tutamağı yalnız sayısal-benzeri kolonlarda.
3925
+ *
3926
+ * Bu tip KOŞULSUZDU ve doğrudan yolun `SetValue<V>`'si koşulluydu, yani aynı
3927
+ * nesne için İKİ tip kuralı vardı: `tx.tables.todos.updateWhere({id}, { done:
3928
+ * increment(1) })` (boolean kolon!) DERLENİYOR, `updateMany`'nin aynısı derleme
3929
+ * hatası veriyordu. Bu run'ın kapatmak için var olduğu şey "aynı iş için iki
3930
+ * uyumsuz yazım"dı; tip kuralı ikinci yazımın kendisi olmuştu (gözcü I6/I-1).
3931
+ */
3932
+ type TxSetValue<V> = V | Ref<V> | TxNow | (NonNullable<V> extends number | string ? TxColumnExpr : never);
3933
+ /** An insert payload: the table's insert shape, with refs and `now()` allowed. */
3934
+ type TxInsertShape<Insert> = {
3935
+ [K in keyof Insert]: TxInsertValue<Insert[K]>;
3936
+ };
3937
+ /** An update's `set`: any subset of the insert shape, with expressions allowed. */
3938
+ type TxSetShape<Insert> = {
3939
+ [K in keyof Insert]?: TxSetValue<Insert[K]>;
3940
+ };
3941
+ /**
3942
+ * A filter. Every entry is an equality test and they are AND-ed; a `null`
3943
+ * becomes `IS NULL`, which is what makes `{ accepted_at: null }` a usable
3944
+ * "not yet accepted" guard rather than a clause that matches nothing.
3945
+ */
3946
+ /**
3947
+ * Plan filtresinin tipi — `WhereFilter<Row>` ile AYNI sözlük, artı `Ref`.
3948
+ *
3949
+ * Eskiden yalnız eşitlikti (`Row[K] | Ref<Row[K]>`), ve iki şeye mal oluyordu:
3950
+ * FR-014'ün amiral deseni (`{ balance: { gte: amount } }`) `$transaction`
3951
+ * İÇİNDE yazılamıyordu — koşullu bir yazmayı plana koyamayan yazar `$query`'ye
3952
+ * düşüyordu — ve motor tarafında tip atlandığında aynı nesne SESSİZCE parametre
3953
+ * olarak bağlanıyordu.
3954
+ *
3955
+ * `Ref` fazladan üye ve öyle kalmalı: bir plan filtresi ÖNCEKİ bir işlemin
3956
+ * döndürdüğü değere bakabilir, `findMany` bakamaz — plan dışında böyle bir
3957
+ * "önceki işlem" yok.
3958
+ */
3959
+ type TxWhereField<Row, K extends keyof Row> = WhereOpWith<Row[K], ColRefOf<Row, Row[K]> | Ref<Row[K]>>;
3960
+ type TxWhere<Row, Rels = unknown> = {
3961
+ [K in keyof Row]?: TxWhereField<Row, K>;
3962
+ } & {
3963
+ OR?: TxWhere<Row, Rels>[];
3964
+ AND?: TxWhere<Row, Rels>[];
3965
+ NOT?: TxWhere<Row, Rels>;
3966
+ } & HasOnly<Rels>;
3967
+ /** Options for a plan `select`. */
3968
+ interface TxSelectOptions {
3969
+ /** Cap the rows read. */
3970
+ limit?: number;
3971
+ /** Take a real `FOR UPDATE` row lock for the rest of the transaction. */
3972
+ lock?: "update";
3973
+ }
3974
+ /** One table, as the plan sees it. */
3975
+ interface TxTable<Row, Insert, Rels = unknown> {
3976
+ /** Insert one row. Returns a handle — call `.expectOne(err)` to read fields. */
3977
+ insert(values: TxInsertShape<Insert>): TxRows<Row>;
3978
+ /**
3979
+ * Insert many rows in ONE statement. Every row must set the same columns
3980
+ * (a row that omits one would silently take the column's default).
3981
+ *
3982
+ * An empty list writes nothing and sends nothing.
3983
+ */
3984
+ /**
3985
+ * Insert many rows in ONE statement, optionally choosing what a collision does.
3986
+ *
3987
+ * Without `opts` this is a plain multi-row INSERT and a collision aborts the
3988
+ * transaction — the behaviour every call had before the option existed.
3989
+ *
3990
+ * `action: "ignore"` emits `ON CONFLICT DO NOTHING`, which is how "insert the
3991
+ * ones that are new" becomes one round-trip instead of one per row with a
3992
+ * 23505 caught around each. **The returned rows are the ones actually
3993
+ * INSERTED**: a row that collided is skipped, so it is absent from the result
3994
+ * — Postgres does not return what it did not write.
3995
+ *
3996
+ * `action: "update"` emits `ON CONFLICT DO UPDATE`, setting every non-conflict
3997
+ * column from the incoming row, and every row comes back.
3998
+ */
3999
+ insertMany(rows: readonly TxInsertShape<Insert>[], opts?: {
4000
+ onConflict: readonly Extract<keyof Row, string>[];
4001
+ action?: "ignore" | "update";
4002
+ }): TxRows<Row>;
4003
+ /**
4004
+ * Satırı yaz, `onConflict` kolonlarında çakışırsa üzerine yaz — planın
4005
+ * savepoint'i içinde, `Database.<şema>.<tablo>.put()` ile AYNI anlamda.
4006
+ *
4007
+ * Adı bilerek aynı: aynı iş için transaction içinde ve dışında iki farklı
4008
+ * yazım, bu run'ın kapatmak için var olduğu şeydir (P1). TEL şekli
4009
+ * (`op: "upsert"`) değişmedi — o iç sözleşme, yazarın gördüğü ad değil.
4010
+ *
4011
+ * Bir operasyon olmasının sebebi: alternatifi burada yazılamaz — başarısız
4012
+ * bir insert tüm transaction'ı abort eder, yani "dene, sonra geri düş" iki
4013
+ * plan adımı olamaz.
4014
+ */
4015
+ put(values: TxInsertShape<Insert>, options: {
4016
+ onConflict: readonly Extract<keyof Row, string>[];
4017
+ }): TxRows<Row>;
4018
+ /**
4019
+ * Update every row matching `where`. The filter comes first because it is the
4020
+ * dangerous half: an update whose `where` you got wrong rewrites rows you
4021
+ * never looked at. The server refuses an update with no `where` at all.
4022
+ */
4023
+ updateWhere(where: TxWhere<Row, Rels>, set: TxSetShape<Insert>): TxRows<Row>;
4024
+ /** Delete every row matching `where`. The server refuses an unfiltered delete. */
4025
+ deleteWhere(where: TxWhere<Row, Rels>): TxRows<Row>;
4026
+ /** Read rows, optionally locking them for the rest of the transaction. */
4027
+ select(where?: TxWhere<Row, Rels>, options?: TxSelectOptions): TxRows<Row>;
4028
+ }
4029
+ /**
4030
+ * The handle a transaction callback receives.
4031
+ *
4032
+ * It carries tables and nothing else: no `query`, no `findById`, no `asService`.
4033
+ * A read whose value the plan does not write belongs OUTSIDE the transaction,
4034
+ * where it costs one round trip and can be branched on like an ordinary value.
4035
+ */
4036
+ interface TxPlanHandle<TTables> {
4037
+ /**
4038
+ * @deprecated `tx.public` kullanın. Bu ad public'in takma adı olarak DURUYOR
4039
+ * (göç notu onu öğretiyor ve her mevcut çağrı onu kullanıyor), ama ARTIK
4040
+ * ÖĞRETİLMİYOR: doğrudan yüzeyde `Database.tables` FR-001 ile kaldırıldı, ve
4041
+ * plan yüzeyinin onu öğretmeye devam etmesi yazarı bir yüzeyde çalışıp
4042
+ * diğerinde derlenmeyen bir yazıma alıştırıyordu (gözcü M-6).
4043
+ */
4044
+ tables: TTables;
4045
+ }
4046
+ /** The server's `now()`. */
4047
+ declare function now(): TxNow;
4048
+ /** Add `by` to the column's current value. Only valid in an update's `set`. */
4049
+ declare function increment(by: number | string): TxColumnExpr;
4050
+ /**
4051
+ * `increment`'in eski adı. AYNI fabrikadır — iki uygulama değil, iki ad.
4052
+ *
4053
+ * @deprecated `increment()` kullanın; bu ad geriye dönük uyumluluk için duruyor.
4054
+ */
4055
+ declare const inc: typeof increment;
4056
+ /** Subtract `by` from the column's current value. Only valid in an update's `set`. */
4057
+ declare function decrement(by: number | string): TxColumnExpr;
4058
+ /**
4059
+ * `decrement`'in eski adı. AYNI fabrikadır.
4060
+ *
4061
+ * @deprecated `decrement()` kullanın.
4062
+ */
4063
+ declare const dec: typeof decrement;
4064
+
4065
+ /**
4066
+ * The brand that identifies an HttpError ACROSS SDK instances.
4067
+ *
4068
+ * A process legitimately holds more than one copy of this SDK — the runtime
4069
+ * loads the engine from its own node_modules while the tenant's bundle carries
4070
+ * an inlined copy, which is why the controller registry and the error registry
4071
+ * are both anchored on `Symbol.for`. The one place that did not follow the
4072
+ * pattern was the engine's catch: `err instanceof HttpError` compares CLASS
4073
+ * IDENTITY, so a `throw new NotFound()` from the bundle's copy did not match
4074
+ * the engine's copy and every typed error in every deployed backend degraded to
4075
+ * `500 internal_error`. Measured through the edge on a real deploy: a route
4076
+ * throwing `NotFound` answered 500 while the runtime's own log printed the
4077
+ * error object with `status: 404` right beside it.
4078
+ *
4079
+ * `Symbol.for` puts this in the cross-realm registry, so every copy of the SDK
4080
+ * agrees on it by VALUE rather than by identity.
4081
+ */
4082
+ declare const HTTP_ERROR_BRAND: unique symbol;
4083
+ /** HTTP error with structured error response format.
4084
+ *
4085
+ * The base class for the throwable error classes (`PalError`, `Conflict`,
4086
+ * `NotFound`, …). Construct one directly with `throw new HttpError(404,
4087
+ * "todo_not_found", "No such todo")`, or throw a named subclass
4088
+ * (`throw new NotFound("todo not found")`). The runtime catches any `HttpError`
4089
+ * and emits the standard envelope; on the wire (and to iOS) it surfaces as
4090
+ * `BackendError.server(code, status, message, requestId)`.
4091
+ *
4092
+ * The optional `data` field carries a structured payload alongside the
4093
+ * standard envelope — for errors that need to ship extra context
4094
+ * (e.g. `new Conflict("locked", "title_locked", { retryAfter: 30 })`). It rides
4095
+ * through to the iOS typed enum's associated value.
4096
+ */
4097
+ declare class HttpError extends Error {
4098
+ readonly status: number;
4099
+ readonly error: string;
4100
+ readonly errorDescription: string;
4101
+ readonly data?: unknown;
4102
+ /** See {@link HTTP_ERROR_BRAND} — how the engine recognises this across SDK copies. */
4103
+ readonly [HTTP_ERROR_BRAND] = true;
4104
+ constructor(status: number, error: string, errorDescription: string, data?: unknown);
4105
+ /**
4106
+ * Serialize to the standard Palbase error response format.
4107
+ * The `requestId` is injected by the runtime layer from the request context.
4108
+ * When called without arguments (e.g. JSON.stringify), request_id is omitted.
4109
+ * When `data` is set, it is appended as a strict-superset field.
4110
+ */
4111
+ toJSON(requestId?: string): {
4112
+ error: string;
4113
+ error_description: string;
4114
+ status: number;
4115
+ request_id?: string;
4116
+ data?: unknown;
4117
+ };
4118
+ }
4119
+ /**
4120
+ * Throw with a custom HTTP status + wire code. The general-purpose escape hatch
4121
+ * when none of the named classes (`Conflict`/`NotFound`/…) fits.
4122
+ *
4123
+ * @example
4124
+ * throw new PalError(418, "teapot", "I'm a teapot");
4125
+ */
4126
+ declare class PalError extends HttpError {
4127
+ constructor(status: number, code: string, description: string, data?: unknown);
4128
+ }
4129
+ /** Base for the named status classes. Each subclass fixes its HTTP status; the
4130
+ * `code` defaults to the class's canonical wire code (overridable), and the
4131
+ * `message` defaults to a human-readable label (overridable). */
4132
+ declare abstract class NamedHttpError extends HttpError {
4133
+ protected constructor(status: number, defaultCode: string, name: string, message?: string, code?: string, data?: unknown);
4134
+ }
4135
+ /**
4136
+ * 400 — the request was malformed or failed validation. Carries a fixed typed
4137
+ * payload: `new BadRequest({ fields: [{ field: "email", message: "invalid" }] })`.
4138
+ * The shape is declared once in the SDK so codegen surfaces `error.data.fields`
4139
+ * typed on the client.
4140
+ */
4141
+ declare class BadRequest extends NamedHttpError {
4142
+ readonly data: BadRequestData;
4143
+ constructor(data: BadRequestData, message?: string);
4144
+ }
4145
+ /** 401 — the caller is not authenticated. */
4146
+ declare class Unauthorized extends NamedHttpError {
4147
+ constructor(message?: string, code?: string, data?: unknown);
4148
+ }
4149
+ /** 403 — the caller is authenticated but not allowed. */
4150
+ declare class Forbidden extends NamedHttpError {
4151
+ constructor(message?: string, code?: string, data?: unknown);
4152
+ }
4153
+ /** 404 — the requested resource does not exist. */
4154
+ declare class NotFound extends NamedHttpError {
4155
+ constructor(message?: string, code?: string, data?: unknown);
4156
+ }
4157
+ /** 409 — the request conflicts with the current state. */
4158
+ declare class Conflict extends NamedHttpError {
4159
+ constructor(message?: string, code?: string, data?: unknown);
4160
+ }
4161
+ /**
4162
+ * 409 — a write was refused because it would duplicate an existing row.
4163
+ * Carries the NAME of the unique constraint Postgres named (`users_email_key`).
4164
+ *
4165
+ * The engine produces it: a statement rejected with SQLSTATE `23505` is
4166
+ * converted here rather than surfacing as an opaque driver error (see
4167
+ * `engine/db.ts`, `diagnosingDriver`). What that removes is the string match —
4168
+ * before this, the only way to act on a duplicate was to test the driver
4169
+ * message for "duplicate key value violates unique constraint", a contract
4170
+ * nobody signed that breaks on a Postgres upgrade, a locale, or a constraint
4171
+ * rename, silently and in production.
4172
+ *
4173
+ * THE NAME IS A FIELD AND STAYS OUT OF THE DEFAULT MESSAGE. The two are not
4174
+ * the same audience. `constraint` is read by the code that catches this — the
4175
+ * developer, who already knows the schema. `errorDescription` is the HTTP
4176
+ * response body, and an UNCAUGHT duplicate puts it in front of the
4177
+ * application's end user: `users_email_key` there discloses how the schema is
4178
+ * built to whoever sent the request. The platform's own data API took the same
4179
+ * decision one surface over and wrote down why —
4180
+ * `v2/internal/modules/database/internal/handler/pgerror.go:83-87` collapses
4181
+ * every 23xxx to a generic conflict, "never disclose the constraint/column
4182
+ * name". A thrower who WANTS the name on the wire passes it deliberately
4183
+ * (`new UniqueViolation(c, \`\${c} already exists\`)`, or through `data`).
4184
+ *
4185
+ * @example
4186
+ * try {
4187
+ * await Database.public.users.insert({ email });
4188
+ * } catch (e) {
4189
+ * if (UniqueViolation.is(e) && e.constraint === "users_email_key") {
4190
+ * throw new Conflict("That email is taken", "email_taken");
4191
+ * }
4192
+ * throw e;
4193
+ * }
4194
+ */
4195
+ declare class UniqueViolation extends Conflict {
4196
+ /**
4197
+ * Whether `e` is a unique violation — REGARDLESS of which copy of this SDK
4198
+ * constructed it.
4199
+ *
4200
+ * Use this instead of `instanceof`. Measured on a live stack: a controller
4201
+ * bundle INLINES its own copy of `@palbase/backend`, and the engine that
4202
+ * raises this error is the runtime's copy. Two copies, two class identities,
4203
+ * and `e instanceof UniqueViolation` is false in the one place a caller
4204
+ * writes it — a check that reads as correct and silently never matches.
4205
+ */
4206
+ static is(e: unknown): e is UniqueViolation;
4207
+ /** The unique constraint the statement violated, as Postgres named it.
4208
+ * `""` when the driver did not say which — see `engine/db.ts`. */
4209
+ readonly constraint: string;
4210
+ constructor(constraint: string, message?: string, code?: string, data?: unknown);
4211
+ }
4212
+ /**
4213
+ * Eşzamanlılık yüzünden BAŞARISIZ OLAN, ama TEKRAR DENENİRSE başarılı olabilecek
4214
+ * hataların ortak yüzeyi (FR-036).
4215
+ *
4216
+ * `retryable: true` bir tahmin değil, Postgres'in söylediği şey: `40001` ve
4217
+ * `40P01` "bu işlemi baştan çalıştır" anlamına gelir. Kümenin DAR tutulması
4218
+ * kasıtlı — `23505` buraya girseydi, tekrar denemek aynı cevabı verir ve
4219
+ * döngü sonsuza kadar dönerdi.
4220
+ */
4221
+ interface RetryableError extends Error {
4222
+ readonly retryable: true;
4223
+ /** Beş karakterlik SQLSTATE — hangi eşzamanlılık hatası olduğunu SÖYLER. */
4224
+ readonly sqlstate: string;
4225
+ }
4226
+ /**
4227
+ * `e` tekrar denenebilir mi?
4228
+ *
4229
+ * `instanceof` DEĞİL şekil kontrolü — ve sebebi ÖLÇÜLMÜŞ (bkz. {@link
4230
+ * UniqueViolation.is}): bir controller bundle'ı kendi `@palbase/backend`
4231
+ * kopyasını inline'lıyor, hatayı fırlatan motor ise runtime'ın kopyası. İki
4232
+ * sınıf kimliği, ve `e instanceof SerializationFailure` yazan tek yerde
4233
+ * sessizce hiç eşleşmiyor.
4234
+ */
4235
+ declare function isRetryable(e: unknown): e is RetryableError;
4236
+ /**
4237
+ * `40001 serialization_failure` — SERIALIZABLE ya da REPEATABLE READ altında
4238
+ * eşzamanlı bir işlem bu işlemi geçersiz kıldı.
4239
+ *
4240
+ * D-016: bu hata BLOKLANMIYOR, FIRLATILIYOR. Retry'sız bir izolasyon
4241
+ * yükseltmesi sunmak defect'tir — kullanıcıya çalışmayan bir düğme vermektir:
4242
+ * SERIALIZABLE'a çıkıp `40001`'i yakalamayan kod, önceden çalışan işlemleri
4243
+ * rastgele kaybetmeye başlar.
4244
+ */
4245
+ declare class SerializationFailure extends Conflict implements RetryableError {
4246
+ readonly retryable: true;
4247
+ readonly sqlstate: string;
4248
+ static is(e: unknown): e is SerializationFailure;
4249
+ constructor(sqlstate?: string, message?: string, data?: unknown);
4250
+ }
4251
+ /**
4252
+ * `40P01 deadlock_detected` — iki işlem birbirinin kilidini bekliyordu;
4253
+ * Postgres birini seçip iptal etti.
4254
+ *
4255
+ * Bu hatayı GÖRMEK bir tasarım sinyali: kilit sırası deterministik değilse
4256
+ * tekrar denemek yalnız maliyeti öteler. `Database.$lockRows(...)` (FR-035)
4257
+ * ya da çok satırlı yazmanın CTE'si (FR-015) sırayı sabitler.
4258
+ */
4259
+ declare class DeadlockDetected extends Conflict implements RetryableError {
4260
+ readonly retryable: true;
4261
+ readonly sqlstate: string;
4262
+ static is(e: unknown): e is DeadlockDetected;
4263
+ constructor(sqlstate?: string, message?: string, data?: unknown);
4264
+ }
4265
+ /** A single field-level validation failure carried by {@link BadRequest}. */
4266
+ interface FieldError {
4267
+ /** The offending field's name (dotted path for nested fields). */
4268
+ field: string;
4269
+ /** Human-readable reason the field failed. */
4270
+ message: string;
4271
+ }
4272
+ /** The fixed, typed payload {@link BadRequest} ships. */
4273
+ interface BadRequestData {
4274
+ /** The fields that failed validation. */
4275
+ fields: FieldError[];
4276
+ }
4277
+ /** The fixed, typed payload {@link TooManyRequests} ships. */
4278
+ interface TooManyRequestsData {
4279
+ /** Seconds the caller should wait before retrying. */
4280
+ retryAfter: number;
4281
+ }
4282
+ /**
4283
+ * 429 — the caller has exceeded the rate limit. Carries a fixed typed payload:
4284
+ * `new TooManyRequests({ retryAfter: 30 })`. The shape is declared once in the
4285
+ * SDK (error-registry pre-seed) so codegen surfaces `error.data.retryAfter`
4286
+ * typed on the client — no per-project definition needed.
4287
+ */
4288
+ declare class TooManyRequests extends NamedHttpError {
4289
+ readonly data: TooManyRequestsData;
4290
+ constructor(data: TooManyRequestsData, message?: string);
4291
+ }
4292
+
4293
+ /** Uploaded file metadata injected into endpoint context when a file is present.
4294
+ * `data` is typed as `Uint8Array` for SDK portability (Buffer extends Uint8Array in Node).
4295
+ */
4296
+ interface FileContext {
4297
+ filename: string;
4298
+ contentType: string;
4299
+ size: number;
4300
+ data: Uint8Array;
4301
+ }
4302
+ /** Rate limit configuration for an endpoint. */
4303
+ interface RateLimitConfig {
4304
+ /** Maximum number of requests in the window. */
4305
+ max: number;
4306
+ /** Window duration in seconds. */
4307
+ window: number;
4308
+ }
4309
+ /** The six raw string-keyed DB operations shared by `DBClient` and the
4310
+ * transaction-scoped client. */
4311
+ interface DBOps {
4312
+ /**
4313
+ * Run SQL on the REQUEST'S OWN transaction.
4314
+ *
4315
+ * This is **not** read-only. Nothing marks that transaction `READ ONLY`, so a
4316
+ * write passed here executes and commits with the request — a plain `UPDATE`, a
4317
+ * write hidden in a CTE, or a function that writes. What stands between a
4318
+ * statement and your data is RLS and the role the request runs as, exactly as
4319
+ * for every other operation on this client.
4320
+ *
4321
+ * `INSERT … ON CONFLICT DO UPDATE` therefore works here; `tables.<t>.put()`
4322
+ * is the same thing with types. Array parameters are encoded as Postgres array
4323
+ * literals, so `= ANY($1::uuid[])` takes a plain JS array.
4324
+ *
4325
+ * This comment used to claim a `READ ONLY` transaction. It was wrong, and the
4326
+ * cost was measured: a tenant read it, concluded a put was impossible from
4327
+ * here, abandoned `ON CONFLICT` and wrote a database trigger instead.
4328
+ */
4329
+ /**
4330
+ * Ham SQL. Generic VERİLMEZSE `unknown[]` döner (FR-021).
4331
+ *
4332
+ * Eskiden `Record<string, unknown>[]` dönüyordu ve bu bir YALANDI: ham SQL'in
4333
+ * sonucunu tipleyen tek şey geliştiricinin iddiasıdır, ve hiçbir iddia
4334
+ * yapılmamışsa doğru cevap "bilmiyorum"dur. Drizzle aynı durumda `unknown`
4335
+ * döndürüp yalanı görünür kılıyor; bir satırın alanına erişmek istiyorsanız
4336
+ * tipi açıkça yazın — o zaman iddia sizin, ve okunabilir.
4337
+ *
4338
+ * BU OP READ-ONLY DEĞİLDİR. Adı "query" olduğu için okuma yaptığı varsayılır;
4339
+ * motor bu transaction'ı READ ONLY işaretlemez (`engine/db.ts`), yani buradan
4340
+ * yazma da yapılabilir. Nothing marks that transaction READ ONLY — okuyan
4341
+ * tersini varsaymasın diye açıkça yazılıdır.
4342
+ */
4343
+ query<T = unknown>(sql: string, params?: unknown[]): Promise<T[]>;
4344
+ insert(table: string, data: Record<string, unknown>): Promise<Record<string, unknown>>;
4345
+ /**
4346
+ * Insert the row, or update it when it collides on `onConflict` — one
4347
+ * statement, so two concurrent callers cannot both lose.
4348
+ *
4349
+ * Read-then-write cannot be made safe here by catching the unique violation: a
4350
+ * request runs in one transaction, so the failed insert aborts it and every
4351
+ * later statement answers `current transaction is aborted`.
4352
+ */
4353
+ put(table: string, data: Record<string, unknown>, opts: {
4354
+ onConflict: readonly string[];
4355
+ }): Promise<Record<string, unknown>>;
4356
+ /**
4357
+ * Update the row with the given id and resolve to the updated row, or `null`
4358
+ * if no row matched (the id is absent, or the row is hidden by RLS). This is
4359
+ * an idempotent outcome, not an error — it mirrors `findById`. Map `null` to
4360
+ * a 404 in your service if a missing row should be a client error.
4361
+ */
4362
+ update(table: string, id: string, data: Record<string, unknown>): Promise<Record<string, unknown> | null>;
4363
+ delete(table: string, id: string): Promise<void>;
4364
+ findById(table: string, id: string): Promise<Record<string, unknown> | null>;
4365
+ /** Hibrit arama (FR-013/014) — tek SQL, RRF; engine implement eder (T017). */
4366
+ search(table: string, params?: {
4367
+ query?: string;
4368
+ vector?: number[];
4369
+ where?: Record<string, unknown>;
4370
+ limit?: number;
4371
+ using?: string;
4372
+ mode?: "hybrid" | "text" | "vector";
4373
+ /** Nihai (RRF-sonrası) skor alt eşiği (FR-001). */
4374
+ minScore?: number;
4375
+ /** RRF-sonrası üstel tazelik çürümesi (FR-004; feed/haber düğmesi). */
4376
+ recency?: {
4377
+ field: string;
4378
+ halfLife: string;
4379
+ };
4380
+ /** Chunk-modunda satır başına en iyi blok sayısı (FR-015). */
4381
+ blocksPerRow?: number;
4382
+ /** @deprecated FR-058: search'ün parametresi DEĞİL — facets() kullanın. */
4383
+ facets?: string[];
4384
+ /** Satır-modunda ts_headline vurgusu: satırlara `_highlight` (FR-025; chunk-modda no-op). */
4385
+ highlight?: boolean;
4386
+ /** Validity'li tabloda zaman penceresi: vars. yalnız güncel; "all" tümü; {asOf} o an (FR-029). */
4387
+ validity?: "all" | {
4388
+ asOf: string;
4389
+ };
4390
+ /** Alan-boost (FR-030): skor * (1 + w·x/(1+x)) — sınırlı çarpan, dış servissiz. */
4391
+ boost?: {
4392
+ field: string;
4393
+ weight: number;
4394
+ };
4395
+ }): Promise<Record<string, unknown>[]>;
4396
+ /** Validity'li tabloda satırın yeni versiyonu — eski kapanır (valid_to/superseded_by),
4397
+ * yenisi eklenir; TEK savepoint, dönüş yeni satır (FR-029, C-9). */
4398
+ supersede(table: string, id: string, row: Record<string, unknown>): Promise<Record<string, unknown>>;
4399
+ /**
4400
+ * Update every row the filter matches, in one statement; resolves to them.
4401
+ * An EMPTY filter is refused — a whole-table write is not something to reach
4402
+ * by accident.
4403
+ *
4404
+ * **0 satır dönmesi bir hata değildir, ve BAŞARI da değildir.** Koşullu bir
4405
+ * update'in tamamı burada saklı:
4406
+ *
4407
+ * ```ts
4408
+ * const [row] = await db.updateMany("accounts",
4409
+ * { id, balance: { gte: amount } }, // koşul WHERE'de
4410
+ * { balance: decrement(amount) }); // tek statement
4411
+ * if (row === undefined) throw new Conflict("yetersiz bakiye");
4412
+ * ```
4413
+ *
4414
+ * 1 satır = uygulandı. **0 satır = koşul tutmadı** (satır yok ya da bakiye
4415
+ * yetmedi) — ikisini ayıran tek şey dönen dizidir. Çağrı hata fırlatmadı diye
4416
+ * işlem oldu SAYILAMAZ; kontrol çağıranın işi.
4417
+ */
4418
+ updateMany(table: string, where: Record<string, unknown>, set: Record<string, unknown>): Promise<Record<string, unknown>[]>;
4419
+ /**
4420
+ * Bir idempotency anahtarını sahiplen (FR-033).
4421
+ *
4422
+ * `{ inserted: true, row }` bu çağrı yazdı; `{ inserted: false, row }` başkası
4423
+ * önce yazdı ve dönen satır ONUN satırı. İkisi de BAŞARILI sonuçtur —
4424
+ * "zaten vardı" bir hata değil, idempotency'nin tanımıdır.
4425
+ *
4426
+ * `unique` satırı BULAN alanlar, `extra` yalnız yazılanlar. Ayrım şart:
4427
+ * ikinci çağrı farklı bir yük gönderirse satır yine anahtarla bulunur.
4428
+ *
4429
+ * Altında `ON CONFLICT` YOKTUR — ölçüldü (pg16): `DO UPDATE` ikincinin
4430
+ * verisiyle birincininkini EZER (10.00 → 999.00), `DO NOTHING` hiçbir satır
4431
+ * DÖNDÜRMEZ. Plain INSERT + 23505 yakalama, birincinin satırını koruyan
4432
+ * tek dal.
4433
+ */
4434
+ /**
4435
+ * Bu satırları kilitle — sırayı KATMAN koyar (FR-035).
4436
+ *
4437
+ * Uygulama dokunacağı kümeyi önden bildirir; hangi sırayla kilitleneceğine
4438
+ * katman karar verir. Çağıranın sırasına güvenmek, iki çağıranın aynı iki
4439
+ * satıra ters sırayla girmesi ve deadlock demektir — `deadlock_timeout`
4440
+ * varsayılanı 1 saniye, yani kaybeden bir saniye bekleyip hata alır.
4441
+ *
4442
+ * `FOR NO KEY UPDATE` — `updateMany`'nin CTE'siyle aynı mod.
4443
+ */
4444
+ lockRows(table: string, ids: readonly string[]): Promise<void>;
4445
+ /**
4446
+ * Bir ADI kilitle — satırı değil (FR-054). Kilitlenecek satırı olmayan
4447
+ * işler için ("aynı anda tek bir fatura kapanışı koşsun").
4448
+ *
4449
+ * YALNIZ transaction kapsamlı: commit/rollback'te Postgres bırakır. Oturum
4450
+ * kapsamlı sürüm bağlantı havuza dönerken kilit üstünde kalacağı için
4451
+ * SUNULMUYOR — bir sonraki isteği hiç bitmeyecek şekilde bekletirdi.
4452
+ */
4453
+ advisoryXactLock(key: string): Promise<void>;
4454
+ claim(table: string, unique: Record<string, unknown>, extra?: Record<string, unknown>): Promise<{
4455
+ inserted: boolean;
4456
+ row: Record<string, unknown>;
4457
+ }>;
4458
+ /** Delete every row the filter matches; resolves to how many. Empty filter
4459
+ * refused, for the same reason and more so. */
4460
+ deleteMany(table: string, where: Record<string, unknown>): Promise<number>;
4461
+ /** How many rows match — the half of pagination limit/offset cannot supply.
4462
+ * An empty filter is legitimate here: counting is a read. */
4463
+ count(table: string, where?: Record<string, unknown>): Promise<number>;
4464
+ /** Filtre + sıralama + sayfa. `offset` yalnız `limit` ile birlikte geçerlidir
4465
+ * (limitsiz offset sayfa değildir; motor adıyla reddeder — FR-004). */
4466
+ findMany(table: string, query?: Record<string, unknown>, opts?: {
4467
+ orderBy?: {
4468
+ column: string;
4469
+ direction?: "asc" | "desc";
4470
+ };
4471
+ limit?: number;
4472
+ offset?: number;
4473
+ }): Promise<Record<string, unknown>[]>;
4474
+ /** "Bu satıra benzer satırlar" (FR-022) — satır-modunda kolon vektörüyle,
4475
+ * chunk-modunda chunk-ortalamasıyla; kendisi hariç. */
4476
+ similar(table: string, id: string, opts?: {
4477
+ limit?: number;
4478
+ where?: Record<string, unknown>;
4479
+ minScore?: number;
4480
+ blocksPerRow?: number;
4481
+ validity?: "all" | {
4482
+ asOf: string;
4483
+ };
4484
+ boost?: {
4485
+ field: string;
4486
+ weight: number;
4487
+ };
4488
+ }): Promise<Record<string, unknown>[]>;
4489
+ /** D-021 (FR-027): kolon sayaçları bağımsız dönüşle — search'ün dizi-üstü
4490
+ * `_facets` özelliği serileşmede kaybolur; ciddi sözleşme budur. */
4491
+ facets(table: string, params: {
4492
+ facets: string[];
4493
+ where?: Record<string, unknown>;
4494
+ validity?: "all" | {
4495
+ asOf: string;
4496
+ };
4497
+ }): Promise<Record<string, {
4498
+ value: string | null;
4499
+ count: number;
4500
+ }[]>>;
4501
+ /** Çok-örnekli öneri (FR-023): avg(pos) + (avg(pos) − avg(neg)); positive'ler sonuç dışı. */
4502
+ recommend(table: string, opts: {
4503
+ positive: string[];
4504
+ negative?: string[];
4505
+ limit?: number;
4506
+ where?: Record<string, unknown>;
4507
+ minScore?: number;
4508
+ blocksPerRow?: number;
4509
+ validity?: "all" | {
4510
+ asOf: string;
4511
+ };
4512
+ boost?: {
4513
+ field: string;
4514
+ weight: number;
4515
+ };
4516
+ }): Promise<Record<string, unknown>[]>;
4517
+ }
4518
+ /** Database client interface injected into endpoint context. */
4519
+ interface DBClient extends DBOps {
4520
+ /**
4521
+ * Run a whole transaction as ONE plan, executed on the request's own
4522
+ * transaction inside a savepoint.
4523
+ *
4524
+ * The low-level seam behind `Database.$transaction(plan)`: the SDK builds the
4525
+ * plan, this sends it, and the broker executes every operation inside a single
4526
+ * transaction that commits or rolls back before the response is written.
4527
+ * Nothing pins a Postgres backend across round trips, because there is only
4528
+ * one round trip.
4529
+ *
4530
+ * On failure the runtime must reject with an error carrying the broker's
4531
+ * envelope fields — see {@link TxPlanRejection}. `slot` in particular is what
4532
+ * turns a `tx_guard_failed` back into the `Error` the tenant handed to
4533
+ * `.expectOne(…)`; without it a declared expectation degrades to a generic 409.
4534
+ */
4535
+ txPlan(plan: TxPlanBody): Promise<TxPlanResponse>;
4536
+ /**
4537
+ * Run `fn` inside a SAVEPOINT on this request's transaction, so a failure in
4538
+ * it rolls back only what `fn`'s handle wrote.
4539
+ *
4540
+ * The handle is a PARAMETER on purpose: an ambient swap would capture writes
4541
+ * a concurrent `Promise.all` branch made outside the boundary.
4542
+ */
4543
+ attempt<T>(fn: (tx: DBOps) => Promise<T>): Promise<T>;
4544
+ /**
4545
+ * Return a sibling DB client that bypasses Row-Level Security by running as
4546
+ * the `service_role` (BYPASSRLS). Use sparingly and explicitly — the default
4547
+ * `Database.*` path is RLS-enforced. The returned client exposes the same op
4548
+ * surface (`query`/`insert`/.../`txPlan`) but never re-exposes `asService`
4549
+ * (no double-bypass).
4550
+ */
4551
+ asService(): Omit<DBClient, "asService">;
4552
+ }
4553
+ /** Logger interface injected into endpoint context. */
4554
+ interface Logger {
4555
+ info(message: string, ...args: unknown[]): void;
4556
+ warn(message: string, ...args: unknown[]): void;
4557
+ error(message: string, ...args: unknown[]): void;
4558
+ debug(message: string, ...args: unknown[]): void;
4559
+ }
4560
+ /**
4561
+ * Cache client interface injected into endpoint context.
4562
+ *
4563
+ * The cache is JSON-typed: values are serialized to/from JSON, so any JSON
4564
+ * value (objects, arrays, numbers, booleans, strings) round-trips. `get<T>`
4565
+ * therefore returns `T | null` rather than `string | null`.
4566
+ */
4567
+ /**
4568
+ * The tenant's secrets, as a controller sees them.
4569
+ *
4570
+ * `get` answers null for a name this tenant has not set — including every name
4571
+ * the STACK holds, which is not an accident of storage but the shape of the
4572
+ * surface: the runtime is handed a tenant client, and there is no route that
4573
+ * returns a platform secret's value at all. Tenant code cannot read the keys
4574
+ * the stack signs with, by mistake or on purpose.
4575
+ *
4576
+ * Reads are cheap. The value is held in the process and dropped when the deploy
4577
+ * pointer reports a new secrets generation, so a rotated secret arrives without
4578
+ * a restart and a hot path pays a map lookup.
4579
+ */
4580
+ interface SecretsService {
4581
+ /**
4582
+ * A secret's value, or `null` when this backend's vault holds no value under
4583
+ * that name.
4584
+ *
4585
+ * The name is a `PalbaseSecretName`, not a `string`: it comes from the
4586
+ * generated `palbase-stack.d.ts`, so a secret the stack does not hold cannot
4587
+ * be spelled here at all. THE GATE IS THE COMPILER.
4588
+ *
4589
+ * That gate replaced a declaration. `config/secrets.ts` used to restate, in
4590
+ * the repo, names the vault already held, and the push compared the two lists
4591
+ * — a check that only ran at deploy, and only if the author remembered to
4592
+ * declare. The type runs on every keystroke and cannot be forgotten.
4593
+ */
4594
+ get(name: PalbaseSecretName): Promise<string | null>;
4595
+ }
4596
+ interface CacheClient {
4597
+ /** Read a value. Returns `null` on a cache miss. */
4598
+ get<T = unknown>(key: string): Promise<T | null>;
4599
+ /** Write a JSON-serializable value with an optional TTL (seconds). */
4600
+ set(key: string, value: unknown, ttl?: number): Promise<void>;
4601
+ /** Delete a key. */
4602
+ del(key: string): Promise<void>;
4603
+ /** Atomically increment an integer counter, returning the new value. */
4604
+ incr(key: string): Promise<number>;
4605
+ /**
4606
+ * Stampede-safe read-through cache fill. On a hit, returns the cached value.
4607
+ * On a miss, a single caller (across all pod replicas, coordinated by a
4608
+ * distributed lock) runs `fn`, caches the result for `ttl` seconds, and
4609
+ * returns it; concurrent callers wait for that result instead of also
4610
+ * running `fn`. If no value lands within the lock's TTL, the call rejects —
4611
+ * it does NOT run `fn` on timeout (that would reintroduce the stampede).
4612
+ *
4613
+ * @param ttl value TTL in seconds.
4614
+ */
4615
+ getOrSet<T>(key: string, ttl: number, fn: () => Promise<T> | T): Promise<T>;
4616
+ }
4617
+ /** Result envelope used across the Palbase Server SDK clients. */
4618
+ interface PalbaseResult<T> {
4619
+ data: T | null;
4620
+ error: {
4621
+ message?: string;
4622
+ code?: string;
4623
+ } | null;
4624
+ status?: number;
4625
+ }
4626
+ /** Document snapshot returned by docs.get(). */
4627
+ interface PalbaseDocumentSnapshot<T = Record<string, unknown>> {
4628
+ id: string;
4629
+ exists: boolean;
4630
+ data(): T | undefined;
4631
+ ref: {
4632
+ path: string;
4633
+ };
4634
+ }
4635
+ /** Collection query snapshot returned by collection.get(). */
4636
+ interface PalbaseQuerySnapshot<T = Record<string, unknown>> {
4637
+ docs: PalbaseDocumentSnapshot<T>[];
4638
+ empty: boolean;
4639
+ size: number;
4640
+ /** Firestore-shaped change list. Every document of a one-shot read is "added". */
4641
+ docChanges(): Array<{
4642
+ type: "added";
4643
+ doc: PalbaseDocumentSnapshot<T>;
4644
+ }>;
4645
+ }
4646
+ /** Comparison operators supported by docs.where(). */
4647
+ type PalbaseWhereOperator = "==" | "!=" | "<" | "<=" | ">" | ">=" | "in" | "array-contains";
4648
+ /** Document reference exposed by Documents.collection(...).doc(...). */
4649
+ interface PalbaseDocumentRef<T = Record<string, unknown>> {
4650
+ readonly path: string;
4651
+ set(data: T): Promise<PalbaseResult<void>>;
4652
+ get(): Promise<PalbaseResult<PalbaseDocumentSnapshot<T>>>;
4653
+ update(data: Partial<T>): Promise<PalbaseResult<void>>;
4654
+ delete(): Promise<PalbaseResult<void>>;
4655
+ /** A subcollection of this document — `rooms/1` → `rooms/1/messages`. */
4656
+ collection<C extends Record<string, unknown> = Record<string, unknown>>(name: string): PalbaseCollectionRef<C>;
4657
+ }
4658
+ /** Collection reference exposed by Documents.collection(...). */
4659
+ interface PalbaseCollectionRef<T = Record<string, unknown>> {
4660
+ readonly path: string;
4661
+ doc(id: string): PalbaseDocumentRef<T>;
4662
+ add(data: T): Promise<PalbaseResult<PalbaseDocumentRef<T>>>;
4663
+ where(field: string, op: PalbaseWhereOperator, value: unknown): PalbaseCollectionRef<T>;
4664
+ orderBy(field: string, direction?: "asc" | "desc"): PalbaseCollectionRef<T>;
4665
+ limit(n: number): PalbaseCollectionRef<T>;
4666
+ get(): Promise<PalbaseResult<PalbaseQuerySnapshot<T>>>;
4667
+ }
4668
+ /** Docs client surface available on the Documents singleton. */
4669
+ interface PalbaseDocsClient {
4670
+ collection<T extends Record<string, unknown> = Record<string, unknown>>(path: string): PalbaseCollectionRef<T>;
4671
+ doc<T extends Record<string, unknown> = Record<string, unknown>>(path: string): PalbaseDocumentRef<T>;
4672
+ /**
4673
+ * Apply up to 500 writes in one call.
4674
+ *
4675
+ * Declared here because the client has always offered it: an interface that
4676
+ * described less than the implementation is how `Documents.doc(...)` came to
4677
+ * be declared and never implemented — the gap this whole move exists to close.
4678
+ */
4679
+ batch(operations: Array<{
4680
+ op: "set" | "update" | "delete";
4681
+ ref: {
4682
+ path: string;
4683
+ };
4684
+ data?: unknown;
4685
+ }>): Promise<PalbaseResult<void>>;
4686
+ }
4687
+ /** Calling-client metadata, derived from request headers.
4688
+ *
4689
+ * Every header is named here, because the previous version of this comment named
4690
+ * only two of the four and the next reader invented `X-Palbase-Platform` for the
4691
+ * third. The four are canonical across the platform — the same names the Go
4692
+ * side reads in user-flags/internal/middleware/clientcontext.go — and the deploy
4693
+ * gate REFUSES an `@Headers` schema that declares an `x-palbase-*` key, which
4694
+ * makes `@Client()` the only sanctioned way to read them.
4695
+ *
4696
+ * All fields are nullable: a request may come from a non-SDK caller (curl,
4697
+ * server-to-server) that sends none of them. Note that the web SDK does not
4698
+ * send `X-Palbase-Sdk-Version` at all, so `sdkVersion` is null for every browser
4699
+ * caller. The semver comparison helpers (`appVersionAtLeast`, …) arrive in
4700
+ * Phase 2 — Phase 1 surfaces only the raw data fields. */
4701
+ interface ClientInfo {
4702
+ /** Palbase SDK version (`X-Palbase-Sdk-Version`), or null. */
4703
+ sdkVersion: string | null;
4704
+ /** Calling app's own version (`X-Palbase-Client-Version`), or null. */
4705
+ appVersion: string | null;
4706
+ /** Platform identifier (`X-Platform`, e.g. "ios", "android", "web"), or null. */
4707
+ platform: string | null;
4708
+ /** OS version string (`X-OS-Version`), or null. */
4709
+ osVersion: string | null;
4710
+ }
4711
+ /** Palbase module clients, as a structural bundle.
4712
+ *
4713
+ * NOT part of the endpoint surface anymore — endpoint handlers reach services
4714
+ * via the PascalCase singletons (`Database`, `Documents`, …). This type is
4715
+ * retained as an INTERNAL shape for the sibling contexts that still carry a
4716
+ * `ctx` (middleware, jobs, workers, hooks, webhooks — out of Phase 1 scope).
4717
+ * It is intentionally not re-exported from `index.ts`.
4718
+ *
4719
+ * Structurally typed against the runtime's `ServerClient`; the runtime injects
4720
+ * the real client, so mismatched names would surface as `undefined is not a
4721
+ * function` at call time — keep these in sync with `ServerClient`. */
4722
+ interface PalbaseModuleClients {
4723
+ auth: PalbaseAuthClient;
4724
+ storage: PalbaseStorageClient;
4725
+ docs: PalbaseDocsClient;
4726
+ realtime: PalbaseRealtimeClient;
4727
+ functions: PalbaseFunctionsClient;
4728
+ flags: PalbaseFlagsClient;
4729
+ notifications: PalbaseNotificationsClient;
4730
+ analytics: PalbaseAnalyticsClient;
4731
+ links: PalbaseLinksClient;
4732
+ }
4733
+ /** Declared-error definition.
4734
+ *
4735
+ * `code` is the stable snake_case identifier that lands on the wire envelope's
4736
+ * `error` field. `status` is the HTTP status code returned. `data`, if set,
4737
+ * is a Zod schema whose value rides on the envelope's `data` field — the CLI
4738
+ * codegen lowers it to the typed enum's associated value on iOS.
4739
+ *
4740
+ * `description` is optional human-readable text; the OpenAPI generator uses it
4741
+ * for the response description and the iOS codegen surfaces it in the
4742
+ * generated method's doc-comment.
4743
+ */
4744
+ interface ErrorDef<TData extends ZodSchema = ZodSchema> {
4745
+ status: number;
4746
+ code: string;
4747
+ description?: string;
4748
+ data?: TData;
4749
+ }
4750
+ /** Map of declared errors keyed by their TypeScript-side names.
4751
+ *
4752
+ * Keys are the friendly names the handler uses (`req.errors.todoLocked`);
4753
+ * `code` on each value is the wire identifier. The TS name is what the iOS
4754
+ * codegen lowers to (camelCase enum cases), and the wire `code` is what the
4755
+ * envelope's `error` field carries.
4756
+ */
4757
+ type ErrorMap = Record<string, ErrorDef>;
4758
+ /** Throwable proxy projected onto `req.errors` when `errors` is declared.
4759
+ *
4760
+ * Each entry is a constructor: declared errors with a `data` schema demand
4761
+ * the payload as a required argument; declared errors without `data` take
4762
+ * none. Throwing the result emits the standard envelope (with `data` when
4763
+ * present).
4764
+ *
4765
+ * throw req.errors.todoNotFound();
4766
+ * throw req.errors.todoLocked({ retryAfter: 30 });
4767
+ */
4768
+ type ErrorThrowers<TErrors extends ErrorMap | undefined> = TErrors extends ErrorMap ? {
4769
+ [K in keyof TErrors]: TErrors[K]["data"] extends ZodSchema ? (data: z.infer<NonNullable<TErrors[K]["data"]>>) => HttpError : () => HttpError;
4770
+ } : Record<string, never>;
4771
+ /** The request-scoped object passed to every endpoint handler.
4772
+ *
4773
+ * Replaces the old `ctx` god-object. `PBRequest` carries ONLY request-scoped
4774
+ * data — the typed `input`, route/query params, headers, the authenticated
4775
+ * `user`, calling-client metadata, trace ids, and the endpoint's declared
4776
+ * error throwers. Services (`Database`, `Documents`, `Cache`, …) are NOT on
4777
+ * the request: import them directly from `@palbase/backend` as singletons.
4778
+ *
4779
+ * import { Controller, Get, Req, Database } from "@palbase/backend";
4780
+ *
4781
+ * \@Controller("/todos")
4782
+ * export class TodosController {
4783
+ * \@Get("") list(\@Req() req: PBRequest): unknown {
4784
+ * return Database.$findMany("todos");
4785
+ * }
4786
+ * }
4787
+ *
4788
+ * Most controller methods reach individual request slices via their own
4789
+ * parameter decorator (`@Body`/`@QueryParams`/`@Param`/`@User`/…); `@Req()` is the
4790
+ * escape hatch that injects this whole object.
4791
+ *
4792
+ * Generic parameters:
4793
+ * - `TInput` — the validated `input` type (the `@Body` schema's `z.infer`). The
4794
+ * user-facing form is single-generic: `PBRequest<TodoInput>`.
4795
+ * - `TAuthed` — whether `user` is non-null. DEFAULTS to `true` (the common
4796
+ * case; the auth pipeline returns 401 before the handler when auth is
4797
+ * required, so a non-null `user` is runtime-honest). A route whose effective
4798
+ * auth is `false` yields `User | null`.
4799
+ * - `TErrors` — RETAINED for back-compat of the `errors` thrower shape; the
4800
+ * class-controller model throws global error classes
4801
+ * (`Conflict`/`NotFound`/…) instead, so `req.errors` is empty in practice.
4802
+ */
4803
+ interface PBRequest<TInput = unknown, TAuthed extends boolean = true, TErrors extends ErrorMap | undefined = undefined> {
4804
+ /** Validated request input (body for POST/PUT/PATCH; `{}` otherwise). */
4805
+ input: TInput;
4806
+ /** Matched route params (e.g. `{ id }` for `/todos/[id]`). */
4807
+ params: Record<string, string>;
4808
+ /** Parsed query-string params. */
4809
+ query: Record<string, string>;
4810
+ /** Request headers (lowercase keys). */
4811
+ headers: Record<string, string>;
4812
+ /** Authenticated user. Non-null (`User`) by default; `User | null` only when
4813
+ * the route's effective auth disables enforcement (driven by `TAuthed`, which
4814
+ * the runtime resolves from the route/controller `auth` cascade via
4815
+ * {@link IsAuthed}). */
4816
+ user: TAuthed extends true ? User : User | null;
4817
+ /** Calling-client metadata derived from request headers (all nullable). */
4818
+ client: ClientInfo;
4819
+ /** Uploaded file, or null when the request has no file part. */
4820
+ file: FileContext | null;
4821
+ /** HTTP method of the incoming request (e.g. "GET", "POST"). */
4822
+ method: string;
4823
+ /** Per-request id (`req_<…>`), preserved for back-compat correlation. */
4824
+ requestId: string;
4825
+ /** W3C trace id (primary correlation key across modules). */
4826
+ traceId: string;
4827
+ /** W3C span id for this handler invocation. */
4828
+ spanId: string;
4829
+ /** Typed throwers for the endpoint's declared errors. RETAINED for the
4830
+ * `@Req()` escape-hatch shape; the class-controller model throws global error
4831
+ * classes (`Conflict`/`NotFound`/…) instead, so this is empty in practice. */
4832
+ errors: ErrorThrowers<TErrors>;
4833
+ }
4834
+ /** Middleware function signature — uses MiddlewareContext (no input, not yet validated). */
4835
+ type Middleware = (ctx: MiddlewareContext, next: () => Promise<void>) => Promise<void>;
4836
+ /** The shape an endpoint's `auth` config may take.
4837
+ *
4838
+ * Either a bare boolean (`true`/`false`) or an object form (`{ required?,
4839
+ * role? }`). The object form is `Partial<AuthConfig>` so `required` may be
4840
+ * omitted — which the runtime treats as `required: true` (see {@link IsAuthed}).
4841
+ */
4842
+ type AuthSpec = boolean | Partial<AuthConfig>;
4843
+
4844
+ export { Notifications as $, type AuthSpec as A, BadRequest as B, type CacheClient as C, type DBClient as D, EXTENSION_DEPENDENCIES as E, type ErrorThrowers as F, type FileContext as G, HttpError as H, Flags as I, Forbidden as J, type HttpMethod as K, IndexBuilder as L, type IndexDef as M, type InsertShape as N, type LifecycleHook as O, type PalbaseResult as P, Log as Q, type RateLimitConfig as R, type SchemaDef as S, type TxPlanBody as T, type Logger as U, type Materialized as V, type Middleware as W, type MiddlewareContext as X, type MiddlewareHandler as Y, NotFound as Z, __runWithRuntime as _, type DBOps as a, type PalbaseListOptions as a$, type OnDeleteAction as a0, PALBASE_EXTENSIONS as a1, type PBRequest as a2, PalError as a3, type PalbaseAnalyticsClient as a4, type PalbaseAnalyticsManagementNamespace as a5, type PalbaseAnalyticsProperties as a6, type PalbaseAnalyticsQueryNamespace as a7, type PalbaseAttestAndroidParams as a8, type PalbaseAttestAndroidResult as a9, type PalbaseExtension as aA, type PalbaseFileObject as aB, type PalbaseFlag as aC, type PalbaseFlagContext as aD, type PalbaseFlagSource as aE, type PalbaseFlagValue as aF, type PalbaseFlagVariant as aG, type PalbaseFlagsClient as aH, type PalbaseFlagsServiceClient as aI, type PalbaseFunctionsClient as aJ, type PalbaseFunnelQueryInput as aK, type PalbaseFunnelResult as aL, type PalbaseIdentifyTraits as aM, type PalbaseInboxClient as aN, type PalbaseInboxListOptions as aO, type PalbaseInboxListResult as aP, type PalbaseInboxMessage as aQ, type PalbaseInboxSendParams as aR, type PalbaseInboxSendResponse as aS, type PalbaseInitialLink as aT, type PalbaseInvokeOptions as aU, type PalbaseLink as aV, type PalbaseLinkAnalytics as aW, type PalbaseLinkDetails as aX, type PalbaseLinksClient as aY, type PalbaseListLinksOptions as aZ, type PalbaseListLinksResult as a_, type PalbaseAttestiOSParams as aa, type PalbaseAttestiOSResult as ab, type PalbaseAuthClient as ac, type PalbaseBatchOverrideOperation as ad, type PalbaseBatchSetOverridesResult as ae, type PalbaseBindDeviceParams as af, type PalbaseBucketClient as ag, type PalbaseClearAllOverridesResult as ah, type PalbaseClearOverrideResult as ai, type PalbaseCohortQueryInput as aj, type PalbaseCohortResult as ak, type PalbaseCollectionRef as al, type PalbaseCountQueryInput as am, type PalbaseCountResult as an, type PalbaseCreateLinkParams as ao, type PalbaseDeviceInfo as ap, type PalbaseDeviceTokenView as aq, type PalbaseDocsClient as ar, type PalbaseDocumentRef as as, type PalbaseDocumentSnapshot as at, type PalbaseEmailClient as au, type PalbaseEmailSendParams as av, type PalbaseEmailSendResponse as aw, type PalbaseEventNamesResult as ax, type PalbaseEventsQueryInput as ay, type PalbaseEventsResult as az, type TxPlanResponse as b, TxPlanError as b$, type PalbaseMatchParams as b0, type PalbaseMultiChannelResponse as b1, type PalbaseNotificationsClient as b2, type PalbaseOverviewResult as b3, type PalbasePreferences as b4, type PalbasePreferencesClient as b5, type PalbasePushClient as b6, type PalbasePushSendParams as b7, type PalbasePushSendResponse as b8, type PalbaseQrCodeOptions as b9, type PolicyExprCtx as bA, PolicyExprRef as bB, type PolicyMode as bC, type PolicyOperand as bD, type RawConstraintDef as bE, Realtime as bF, type Ref as bG, type RequestStore as bH, type RowShape as bI, Secrets as bJ, type SecretsService as bK, SerializationFailure as bL, type SetShape as bM, type SetValue as bN, type ShutdownRunner as bO, type SqlFragment as bP, Storage as bQ, TABLE_META as bR, type TableDef as bS, type TableHandle as bT, type TableInput as bU, TooManyRequests as bV, type TxColumnExpr as bW, type TxInsertShape as bX, type TxInsertValue as bY, type TxNow as bZ, type TxPlan as b_, type PalbaseQuerySnapshot as ba, type PalbaseRealtimeClient as bb, type PalbaseRegisterDeviceParams as bc, type PalbaseRetentionQueryInput as bd, type PalbaseRetentionResult as be, type PalbaseSession as bf, type PalbaseSetOverrideResult as bg, type PalbaseSetOverridesResult as bh, type PalbaseSignedUrlResponse as bi, type PalbaseSmsClient as bj, type PalbaseSmsSendParams as bk, type PalbaseSmsSendResponse as bl, type PalbaseStorageClient as bm, type PalbaseTransformOptions as bn, type PalbaseUpdateLinkParams as bo, type PalbaseUploadOptions as bp, type PalbaseUser as bq, type PalbaseUserDetailResult as br, type PalbaseUsersQueryInput as bs, type PalbaseUsersResult as bt, type PalbaseVerifyRequestSignatureParams as bu, type PalbaseWhereOperator as bv, type PolicyBinOp as bw, PolicyBuilder as bx, type PolicyCommand as by, type PolicyDef as bz, type RuntimeServices as c, type TxPlanHandle as c0, type TxPlanOpResult as c1, type TxPlanRejection as c2, TxRefError as c3, type TxRow as c4, type TxRows as c5, type TxSelectOptions as c6, type TxSetShape as c7, type TxSetValue as c8, type TxTable as c9, enumType as cA, exprCtx as cB, inc as cC, increment as cD, index as cE, installationRef as cF, integer as cG, isPalbaseExtension as cH, isRetryable as cI, jsonb as cJ, makeTypedDB as cK, now as cL, numeric as cM, onShutdown as cN, onStart as cO, openai as cP, ownedByUser as cQ, policy as cR, raw as cS, sqlFragment as cT, text as cU, timestamp as cV, userRef as cW, uuid as cX, vector as cY, withRetry as cZ, type TxTables as ca, type TxWhere as cb, type TxWireExpr as cc, type TxWireGuard as cd, type TxWireOp as ce, type TxWireRef as cf, type TxWireValue as cg, type TypedDB as ch, type TypedTable as ci, type TypedTx as cj, Unauthorized as ck, UniqueViolation as cl, type User as cm, type VerifiedDevice as cn, __getRuntime as co, __resetLifecycleHooks as cp, __runStartHooks as cq, __setRuntime as cr, bigint as cs, boolean as ct, col as cu, dec as cv, decrement as cw, defineMiddleware as cx, defineSchema as cy, defineTable as cz, __requestALS as d, type PolicyExpr as e, type AnyColumn as f, type AuthConfig as g, Cache as h, type ClientInfo as i, type ColRef as j, ColumnBuilder as k, type ColumnDef as l, type ColumnMap as m, type ColumnType as n, Conflict as o, Database as p, DeadlockDetected as q, Documents as r, type EmbeddingModelRef as s, type EnvSchemas as t, type EnvServiceDatabase as u, type EnvTables as v, type EnvTypedDatabase as w, type EnvTypedTable as x, type ErrorDef as y, type ErrorMap as z };