@palbase/backend 33.0.2 → 34.1.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 (84) hide show
  1. package/dist/bin/palbase-backend.cjs +2273 -358
  2. package/dist/bin/palbase-backend.cjs.map +1 -1
  3. package/dist/bin/palbase-backend.js +9 -7
  4. package/dist/bin/palbase-backend.js.map +1 -1
  5. package/dist/{chunk-YIQ4RS4F.js → chunk-5UGPXKMJ.js} +331 -5
  6. package/dist/chunk-5UGPXKMJ.js.map +1 -0
  7. package/dist/chunk-GHNC2UHS.js +20 -0
  8. package/dist/chunk-GHNC2UHS.js.map +1 -0
  9. package/dist/{chunk-AAT5G7KY.js → chunk-JJW3L62T.js} +2 -2
  10. package/dist/chunk-JJW3L62T.js.map +1 -0
  11. package/dist/{chunk-SI4KGEM3.js → chunk-P7MGSAFT.js} +102 -96
  12. package/dist/chunk-P7MGSAFT.js.map +1 -0
  13. package/dist/{chunk-XOX6RFPZ.js → chunk-PRH45NZU.js} +46 -3
  14. package/dist/chunk-PRH45NZU.js.map +1 -0
  15. package/dist/{chunk-26GH54D2.js → chunk-RZLY4BOY.js} +5 -3
  16. package/dist/{chunk-26GH54D2.js.map → chunk-RZLY4BOY.js.map} +1 -1
  17. package/dist/{chunk-KATPXCJ5.js → chunk-UMESJJRS.js} +19 -40
  18. package/dist/chunk-UMESJJRS.js.map +1 -0
  19. package/dist/chunk-VXPNPVAG.js +40 -0
  20. package/dist/chunk-VXPNPVAG.js.map +1 -0
  21. package/dist/{chunk-WWUG2QXF.js → chunk-X2UYIXDS.js} +59 -4
  22. package/dist/chunk-X2UYIXDS.js.map +1 -0
  23. package/dist/{chunk-C6COAB3E.js → chunk-XZPDPYVY.js} +1905 -195
  24. package/dist/chunk-XZPDPYVY.js.map +1 -0
  25. package/dist/{chunk-TVCCR6SO.js → chunk-YO5GYM73.js} +58 -9
  26. package/dist/chunk-YO5GYM73.js.map +1 -0
  27. package/dist/db/env.cjs.map +1 -1
  28. package/dist/db/env.d.cts +2 -0
  29. package/dist/db/env.d.ts +2 -0
  30. package/dist/db/index.cjs +285 -2
  31. package/dist/db/index.cjs.map +1 -1
  32. package/dist/db/index.d.cts +46 -3
  33. package/dist/db/index.d.ts +46 -3
  34. package/dist/db/index.js +14 -5
  35. package/dist/{index-DMZG3kpo.d.ts → endpoint-BXPdsTA5.d.ts} +370 -58
  36. package/dist/{index-DB_nW-AV.d.cts → endpoint-DqhUP7RT.d.cts} +370 -58
  37. package/dist/engine/index.cjs +2279 -356
  38. package/dist/engine/index.cjs.map +1 -1
  39. package/dist/engine/index.d.cts +5 -5
  40. package/dist/engine/index.d.ts +5 -5
  41. package/dist/engine/index.js +17 -7
  42. package/dist/{index-DLveQoOf.d.cts → index-Co0Hj_aO.d.cts} +108 -50
  43. package/dist/{index-D-3duy8Y.d.ts → index-MhCL7ll4.d.ts} +108 -50
  44. package/dist/index.cjs +806 -386
  45. package/dist/index.cjs.map +1 -1
  46. package/dist/index.d.cts +7 -6
  47. package/dist/index.d.ts +7 -6
  48. package/dist/index.js +27 -15
  49. package/dist/index.js.map +1 -1
  50. package/dist/openapi/index.cjs +7 -0
  51. package/dist/openapi/index.cjs.map +1 -1
  52. package/dist/openapi/index.d.cts +3 -3
  53. package/dist/openapi/index.d.ts +3 -3
  54. package/dist/openapi/index.js +4 -3
  55. package/dist/openapi/index.js.map +1 -1
  56. package/dist/{registry-DSTThhKf.d.cts → registry-BkIVHPzB.d.cts} +3 -1
  57. package/dist/{registry-JjF5lcj4.d.ts → registry-CvGLR5j1.d.ts} +3 -1
  58. package/dist/test/index.cjs +103 -66
  59. package/dist/test/index.cjs.map +1 -1
  60. package/dist/test/index.d.cts +3 -3
  61. package/dist/test/index.d.ts +3 -3
  62. package/dist/test/index.js +33 -7
  63. package/dist/test/index.js.map +1 -1
  64. package/docs/README.md +1 -1
  65. package/docs/auth.md +58 -0
  66. package/docs/database.md +308 -0
  67. package/docs/examples/check-query-contracts.ts +5 -0
  68. package/docs/examples/database-dx.contract.json +65 -0
  69. package/docs/examples/database-dx.ts +63 -0
  70. package/docs/examples/money-transfer.ts +73 -0
  71. package/docs/llms-full.txt +382 -1
  72. package/docs/schema.md +15 -0
  73. package/package.json +1 -2
  74. package/template/db/public.ts +12 -0
  75. package/template/package.json +1 -1
  76. package/dist/chunk-AAT5G7KY.js.map +0 -1
  77. package/dist/chunk-C6COAB3E.js.map +0 -1
  78. package/dist/chunk-KATPXCJ5.js.map +0 -1
  79. package/dist/chunk-SI4KGEM3.js.map +0 -1
  80. package/dist/chunk-TVCCR6SO.js.map +0 -1
  81. package/dist/chunk-WWUG2QXF.js.map +0 -1
  82. package/dist/chunk-XOX6RFPZ.js.map +0 -1
  83. package/dist/chunk-YIQ4RS4F.js.map +0 -1
  84. package/stack-images.json +0 -24
@@ -90,7 +90,7 @@ service the controllers call.
90
90
 
91
91
  > **Never** emit `defineController`, `defineHandler`, `defineEndpoint`, `route.get(...)`,
92
92
  > `req.input`, `req.params`, or `req.errors` — those are the removed legacy model
93
- > and will not compile against `@palbase/backend` 33.
93
+ > and will not compile against `@palbase/backend` 34.
94
94
 
95
95
  ### Complete CRUD example (copy-pasteable, compiles)
96
96
 
@@ -931,6 +931,64 @@ Two helpers are available in every policy body:
931
931
  Both take the caller from `auth.uid()` and accept no user id, so a policy cannot
932
932
  ask about somebody else.
933
933
 
934
+ ### `can` — what a row says you may do
935
+
936
+ Every row a table hands back carries `can: string[]`: the names of the table's
937
+ named **update** policies whose `using` expression is true for THIS row and THIS
938
+ caller. It is computed in the same `SELECT` that fetched the row — the policy is
939
+ the only copy of the rule, and `can` is its echo.
940
+
941
+ ```ts
942
+ const transfer = await Database.public.transfers.findById(id);
943
+ transfer?.can; // e.g. ["transfers_approve"] — typed from db/public.ts
944
+ ```
945
+
946
+ To put it on the wire, name it in the response schema — the enum comes from the
947
+ policy declaration, never from a hand-typed list:
948
+
949
+ ```ts
950
+ import { z, can } from "@palbase/backend";
951
+ import publicSchema from "../../db/public";
952
+
953
+ export const TransferSchema = z.object({
954
+ id: z.string(),
955
+ status: z.string(),
956
+ can: can(publicSchema.tables.transfers),
957
+ });
958
+ ```
959
+
960
+ The contract, word for word: **`can` is for the interface; it cannot be used for
961
+ an authorization decision; it may be incomplete; the server decides again on
962
+ every request.** A policy whose `using` the engine cannot select as a column is
963
+ dropped from `can` with a `warn` line — it never becomes a silent `true`. In a
964
+ raw-string `using`, write column names unqualified (`status`, not
965
+ `transfers.status` or `public.transfers.status`) and wrap every
966
+ `auth.has_permission(...)` in `(select …)`; a comment, an unbalanced parenthesis
967
+ or an unterminated quote drops the policy the same way.
968
+
969
+ Rows read under `Database.$asService()` carry `can: []`: no policy applies to
970
+ the service role and every `TO` gate is false for it, so a row fetched there and
971
+ handed to a user says nothing about what that user may do — read it as the user.
972
+
973
+ Only `.for("update")` policies are echoed. A `.for("all")` policy such as
974
+ `notes_owner` above governs UPDATE too, but it is not a named update policy and
975
+ never appears in `can` — a table whose only policy is `for("all")` gets
976
+ `can: []` on every row, and `can(table)` refuses it by name. To get an
977
+ affordance, declare a separate `.for("update")` policy beside it (the template's
978
+ `notes_edit_own` next to `notes_owner`).
979
+
980
+ The name `can` is reserved for this echo. A table that declares a column named
981
+ `can` keeps its column and gets no echo (with a `warn`); a **relation** named
982
+ `can` — a table named `can`, a `can_id` foreign key, `as: "can"` or
983
+ `reverseAs: "can"` — is refused by `defineTable` itself, because `row.can` and
984
+ `relations.can` cannot both be true of one row.
985
+
986
+ When the server refuses, the 403 body names the cure: `required: "transfers.approve"`.
987
+ `pb.auth.onPermissionDenied { required in … }` (iOS) / `pb.auth.onPermissionDenied(required => …)`
988
+ (web) fires on it, so a screen drawn from a stale `can` refetches and repairs itself.
989
+ `pb.auth.permissions.holds("transfers.approve")` is the live, observable set of the
990
+ caller's permissions — pushed over realtime the moment a role changes.
991
+
934
992
  ## Email verification
935
993
 
936
994
  The platform handles verification end to end. **You do not configure a sender**,
@@ -1075,6 +1133,69 @@ await Database.public.todos.delete(todo.id);
1075
1133
 
1076
1134
  See [schema.md](./schema.md) for the full typed-table surface.
1077
1135
 
1136
+ ## Transaction boundaries and retries
1137
+
1138
+ Ordinary Database writes commit when the request completes. `$transaction(fn)`
1139
+ builds a synchronous plan executed inside a savepoint of that same transaction.
1140
+ A savepoint cannot refresh its parent snapshot or retry the parent's COMMIT;
1141
+ passing a nonzero `retry` to `$transaction` is refused with a migration message.
1142
+
1143
+ Use `$atomic` for an explicit, independently committed transaction with ordinary
1144
+ async reads and writes. Its promise resolves after COMMIT succeeds. Every retry
1145
+ opens a fresh transaction and reruns the complete callback, including decision
1146
+ reads. Choose `read committed`, `repeatable read` or `serializable` isolation.
1147
+
1148
+ ```ts
1149
+ import { Conflict, NotFound, decrement } from "@palbase/backend";
1150
+
1151
+ const result = await Database.$atomic(async (tx) => {
1152
+ const account = await tx.public.accounts.findById(accountId);
1153
+ if (!account) throw new NotFound();
1154
+ // All dependent writes, the ledger and an outbox event belong in this scope.
1155
+ return tx.$transaction((plan) => {
1156
+ return plan.public.accounts.updateWhere(
1157
+ { id: accountId, balance: { gte: amount } },
1158
+ { balance: decrement(amount) },
1159
+ ).expectOne(new Conflict("Insufficient balance"));
1160
+ });
1161
+ }, { isolation: "serializable", retry: 3, lockTimeoutMs: 1000, statementTimeoutMs: 5000 });
1162
+ ```
1163
+
1164
+ `retry` defaults to 0 and is limited to 10. Only serialization and deadlock
1165
+ failures are retried. A lost connection during COMMIT has an unknown outcome
1166
+ and is not automatically replayed; use an idempotency key to reconcile it.
1167
+ Keep external effects out of a retried callback: write an outbox record there
1168
+ and deliver it after commit.
1169
+
1170
+ The complete [money-transfer.ts](./examples/money-transfer.ts) recipe uses the
1171
+ public `DBClient.atomic` seam, typed guards, exact integer minor units, a payload
1172
+ checked idempotency key, ordered account locks, balanced ledger entries and an
1173
+ outbox. Its `transferSchema` declares the database constraints and tenant RLS;
1174
+ export it from `db/public.ts`, review `palbase db plan` and run `palbase db apply`.
1175
+ It is the same transfer body used by the real PostgreSQL verification suite;
1176
+ importing it alone does not apply its constraints. Derive `input.tenant` from verified application scope. Use globally
1177
+ unique request keys, or include the tenant in the key. An outbox worker delivers
1178
+ after commit and consumers deduplicate by event id.
1179
+
1180
+ `$atomic` commits independently: a later handler error does not undo it. Avoid
1181
+ writing the same data in the surrounding request transaction. The callback's
1182
+ typed `tx` and ambient `Database` calls share its transaction. Handles saved from
1183
+ another transaction, nested roots and `$asService()` inside it are refused.
1184
+ For service-role work choose `Database.$asService().$atomic(...)` before entering
1185
+ the callback; it keeps that one identity throughout every retry.
1186
+
1187
+ The request's default and service-role surfaces remain separate transactions;
1188
+ their commits are not atomic together. Put a business operation's atomic writes
1189
+ in one `$atomic` scope. `fakeDatabase` does not emulate PostgreSQL isolation or
1190
+ commit/retry behavior; use a real PostgreSQL test for that boundary.
1191
+
1192
+ Schema-aware timestamp results use UTC ISO strings with six fractional digits,
1193
+ preserving PostgreSQL microseconds through reads, returned writes, relations,
1194
+ aggregates and transaction plan references. Infinities, BC dates and years outside
1195
+ 1–9999 retain PostgreSQL's text representation. These queries return the declared
1196
+ column shape. Raw `$query` results follow the driver's types; request `column::text`
1197
+ when a raw query must preserve timestamp precision rather than a JavaScript Date.
1198
+
1078
1199
  ## Raw string-keyed operations
1079
1200
 
1080
1201
  For a table name computed at runtime, or for SQL the typed helpers do not
@@ -1752,6 +1873,251 @@ Guidelines:
1752
1873
  second surface waits for a lock the first cannot release until the handler
1753
1874
  returns. The service side gives up after five seconds and tells you this;
1754
1875
  the default side would wait until the edge cuts the request.
1876
+ ## Cursor pages and bulk counts
1877
+
1878
+ `page` returns `{ rows, pageInfo }`. A projection narrows `rows` in TypeScript;
1879
+ the cursor keeps the ordering values independently, including fields omitted by
1880
+ `select`.
1881
+
1882
+ ```ts
1883
+ const page = await Database.public.todos.page({
1884
+ select: ["id", "title"],
1885
+ orderBy: { column: "created_at", direction: "desc" },
1886
+ first: 50,
1887
+ after: cursor, // omit for the first page
1888
+ });
1889
+ // Next: after: page.pageInfo.endCursor
1890
+ // Previous: last: 50, before: page.pageInfo.startCursor
1891
+ ```
1892
+
1893
+ Sizes are 1–1000 (default 50). Use `first`/`after` or `last`/`before` together.
1894
+ The engine appends every missing primary-key column to the ordering, including
1895
+ composite keys. `direction` and `nulls` are honored independently. Both directions
1896
+ return rows in the requested order. Empty pages have null cursors and report
1897
+ whether matching rows remain on the opposite side of the supplied boundary.
1898
+ The page and its flags use one PostgreSQL statement snapshot. Across requests,
1899
+ inserts, deletes and changes to ordering fields can change the list; a cursor is
1900
+ not a frozen snapshot. Prefer immutable ordering columns for exports.
1901
+
1902
+ Install the table declaration and an index matching the tenant/filter columns
1903
+ followed by the ordering columns and primary key. Keyset pagination avoids
1904
+ scanning a skipped prefix; it does not provide arbitrary page-number jumps.
1905
+ JSON/vector ordering is refused. Text ordering follows the database collation.
1906
+
1907
+ In the engine, cursor encryption uses `PALBASE_CURSOR_SECRET`, falling back to
1908
+ `PALBASE_SERVICE_ROLE_KEY`. Keep this secret shared across replicas and restarts;
1909
+ rotation invalidates existing cursors. Cursors are tied to the table declaration,
1910
+ filter, projection, relations, ordering and authorization claims (JWT issuance/
1911
+ expiry identifiers are excluded). Reusing one with another query or identity
1912
+ returns `BadRequest`. A malformed token is rejected before database access.
1913
+ Standalone engine consumers configure `setCursorKey(secret)` from the engine
1914
+ entrypoint. `fakeDatabase` requires an explicit page test double; PostgreSQL
1915
+ integration tests cover collation, RLS and cursor semantics.
1916
+
1917
+ ```ts
1918
+ const inserted = await Database.public.todos.insertMany(rows, {
1919
+ returning: false,
1920
+ });
1921
+ ```
1922
+
1923
+ `inserted` is a number. The engine omits `RETURNING` and reads PostgreSQL's affected
1924
+ row count, including zero when `onConflict` with `action: "ignore"` skips every
1925
+ row. `action: "update"` counts both inserted and updated rows. The default returns
1926
+ rows as before; a dynamic boolean produces a `number | Row[]` type. This option
1927
+ belongs to the asynchronous bulk surface; declarative transaction plan row handles
1928
+ continue to return their rows. Custom SQL drivers must expose an integer `count`
1929
+ on a non-returning result. Empty input opens no connection. The batch remains one
1930
+ statement and participates in its enclosing transaction's rollback.
1931
+
1932
+ ## Reusable queries and response contracts
1933
+
1934
+ `defineQuery` combines an explicit selection with a reusable input/filter. The
1935
+ result is also a Zod array schema: a controller can return
1936
+ `Promise<z.infer<typeof ListTodos>>`, and the existing build binder uses that
1937
+ same value for response validation, OpenAPI and generated clients.
1938
+
1939
+ ```ts
1940
+ export const ListTodos = defineQuery("ListTodos", {
1941
+ table: todos, // a defineTable value already registered by defineSchema
1942
+ input: z.object({ prefix: z.string().default("") }),
1943
+ select: ["id", "title"],
1944
+ where: ({ prefix }) => ({ title: { startsWith: prefix } }),
1945
+ orderBy: { column: "id" },
1946
+ limit: 50,
1947
+ });
1948
+
1949
+ const rows = await ListTodos.run(Database, { prefix: "Buy" });
1950
+ const compiled = ListTodos.sql(Database, { prefix: "Buy" }); // no connection
1951
+ // compiled.sql and compiled.params describe the same statement run() executes.
1952
+ ```
1953
+
1954
+ `run` validates its input before SQL and its response before returning. SQL uses
1955
+ the same SELECT compiler as `findMany`, under the caller's transaction and RLS
1956
+ identity. Table names retain their declared schema. Selection is mandatory;
1957
+ unselected database columns cannot silently expand the API. Numeric/bigint and
1958
+ timestamp outputs retain their exact string representations, and named codecs
1959
+ determine the surfaced type. A selected `jsonb<T>()` needs a matching Zod schema
1960
+ in `fields: { payload: PayloadSchema }`, because its TypeScript payload is erased
1961
+ at runtime. Free-form column transforms require migration to a named codec.
1962
+
1963
+ Keep a contract manifest in the application repository:
1964
+
1965
+ ```ts
1966
+ const generated = queryManifest([ListTodos]);
1967
+ // Write generated JSON for the initial, reviewed baseline.
1968
+ assertQueryManifest([ListTodos], committedManifest); // run in the CI/build check
1969
+ ```
1970
+
1971
+ The manifest contains input/response OpenAPI components, selected storage types,
1972
+ nullability and codecs. A changed contract fails the check until its diff is
1973
+ reviewed and the committed baseline is updated. The runtime also refuses a query
1974
+ whose selected columns disagree with the installed declaration, before SQL.
1975
+ This check compares declarations; migration validation still owns drift in the
1976
+ physical PostgreSQL schema. Queries currently project one table; use `findMany`
1977
+ or `page` for nested relation reads.
1978
+
1979
+ `await ListTodos.explain(Database, input)` executes plain `EXPLAIN (FORMAT JSON)`
1980
+ with the same role and parameters. It never enables `ANALYZE`. Plans can contain
1981
+ filter literals, so inspect them in development/operator tooling. No EXPLAIN is
1982
+ run automatically on production requests. The complete controller/schema example
1983
+ is [examples/database-dx.ts](./examples/database-dx.ts).
1984
+
1985
+ ## Nested relations and unique lookups
1986
+
1987
+ `with` accepts three relation edges, with independent selection, filtering,
1988
+ ordering and limits at each level. The whole read remains one SQL statement.
1989
+ Each related table retains RLS; `many` returns an array, `one` returns a row or
1990
+ null. Numeric/bigint strings, timestamp microseconds, vectors and named codecs
1991
+ are restored at each level. Joins use the foreign key's declared target column,
1992
+ including a non-primary unique column.
1993
+
1994
+ ```ts
1995
+ const customers = await Database.billing.customers.findMany({
1996
+ select: ["id"],
1997
+ with: { invoices: { select: ["id"], limit: 10,
1998
+ with: { customer: { select: ["name"] } },
1999
+ } },
2000
+ });
2001
+
2002
+ const account = await Database.public.accounts.findUnique({
2003
+ where: { tenant_id: tenantId, account_number: number },
2004
+ select: ["balance"],
2005
+ });
2006
+ ```
2007
+
2008
+ The lookup example requires a declared unique/primary key on
2009
+ `["tenant_id", "account_number"]`. Regenerate `palbase-env.d.ts` to get exact key
2010
+ types. Supply one entire key; partial composites, mixed keys, operators and null
2011
+ values are refused. A missing or RLS-hidden row returns null. More than one match
2012
+ raises a constraint-drift error. The direct `makeTypedDB` facade validates keys
2013
+ at runtime; the generated `Database.<schema>.<table>` surface also checks them at
2014
+ compile time. `findById` keeps its existing `id`-column contract.
2015
+
2016
+ ## Commands and workload admission
2017
+
2018
+ `$command` accepts the synchronous transaction-plan callback used by
2019
+ `$transaction`. It retains guards, dependent row references, sequential visibility
2020
+ and savepoint rollback. Its default `mode: "auto"` compiles supported plans when
2021
+ the helper is installed and uses the sequential path otherwise. Choose
2022
+ `mode: "compiled"` to require compilation or `mode: "sequential"` to opt out.
2023
+ It settles with its enclosing transaction; an independently durable command
2024
+ belongs inside `$atomic`.
2025
+
2026
+ Enable the helper with `PALBASE_DB_COMMANDS=true`; standalone engine consumers
2027
+ call `installCommandExecutor(driver, roles)`. Installation needs DDL rights.
2028
+ Execution is `SECURITY INVOKER`, so the caller's role and RLS remain active.
2029
+ Compilation currently excludes SQL fragments, column codecs/transforms, vector
2030
+ tables, nullable WHERE references and certain JavaScript number/JSON scalar
2031
+ bindings whose driver semantics would differ. Strict mode refuses these before
2032
+ SQL; automatic mode preserves the sequential behavior. Plans with no row refs
2033
+ send parameterized statements in order on one connection without waiting for
2034
+ each result before submitting the next. SQL guards abort before later queued
2035
+ writes run, and the engine drains every result before rollback. These are still
2036
+ separate SQL statements, with sequential visibility. Plans with row refs use
2037
+ one invoker helper call with sequential statements inside PostgreSQL. The helper
2038
+ adds JSON/dynamic-SQL processing; measure the actual plan and network before
2039
+ selecting it for throughput. Fewer round trips alone do not guarantee a speedup.
2040
+
2041
+ For contention on the same accounts/resources, declare a workload once:
2042
+
2043
+ ```ts
2044
+ const transfers = defineWorkload("transfers", {
2045
+ concurrency: 16, queueLimit: 128,
2046
+ tenantConcurrency: 8, tenantQueueLimit: 32, queueTimeoutMs: 1000,
2047
+ });
2048
+
2049
+ await Database.$atomic(async tx => {
2050
+ // Decision reads, idempotency claim, writes, ledger and outbox belong here.
2051
+ }, {
2052
+ workload: transfers,
2053
+ conflictKeys: [fromAccountId, toAccountId],
2054
+ timeoutMs: 3000, signal, retry: 2,
2055
+ });
2056
+ ```
2057
+
2058
+ Overlapping resource keys are admitted in order. Independent keys and tenants
2059
+ can proceed while those requests wait. Queue size and per-tenant budgets are
2060
+ bounded; full/expired queues return 429 before opening a transaction. Tenant
2061
+ identity comes from the configured verified claim (`tenant_id`, then `sub`, then
2062
+ one anonymous bucket). Keys are shared within a workload; include the tenant in
2063
+ a key if resource IDs are only tenant-unique. Rejected/cancelled waiters leave the
2064
+ queue. The lease spans every retry and transaction settlement.
2065
+
2066
+ These limits apply per engine/driver instance. Divide pool/admission budgets
2067
+ across replicas; PostgreSQL locks and constraints remain authoritative across
2068
+ instances. Admission is optional because independent work also pays its cost.
2069
+ Each driver supports at most 64 named workloads.
2070
+
2071
+ `timeoutMs` includes queueing, callback work and retries. The default driver uses
2072
+ a separate control pool of at most two connections to cancel active PostgreSQL
2073
+ statements. Reserve those connections in the deployment budget. Standalone Bun
2074
+ drivers can use `withQueryCancellation(applicationPool, controlPool)` with the
2075
+ same server/login. The engine waits for cancellation and rollback before releasing
2076
+ the application connection. JavaScript callbacks themselves are not preempted;
2077
+ late DB handles are invalidated. Pool acquisition and an in-flight COMMIT still
2078
+ have to settle, so the deadline is not a hard wall-clock response guarantee.
2079
+ An acknowledged COMMIT stays successful; an unknown commit outcome is never
2080
+ automatically replayed.
2081
+
2082
+ ## Query diagnostics and budgets
2083
+
2084
+ Set `PALBASE_DB_DIAGNOSTICS=true` for request summaries, or provide
2085
+ `EngineConfig.databaseDiagnostics` with `onQuery`/`onComplete` callbacks.
2086
+ Standalone consumers pass the same options as the third argument to
2087
+ `createRequestDatabase`. Diagnostics are disabled by default. For a route budget:
2088
+
2089
+ ```ts
2090
+ @Get("/", { databaseBudget: { maxQueries: 8, maxRows: 100, maxBytes: 65536 } })
2091
+ ```
2092
+
2093
+ Counts include application SQL, authorization reads, service queries and retries;
2094
+ transaction setup is timed separately. Query count is checked before SQL. Row
2095
+ and estimated JSON-byte budgets are checked after results arrive; use SQL limits
2096
+ and projections to avoid producing excessive data. A caught budget error still
2097
+ prevents COMMIT of the affected transaction. An earlier independent `$atomic`
2098
+ commit remains durable.
2099
+
2100
+ Summaries correlate the route/request ID with query names and SQL fingerprints.
2101
+ `captureSource: true` adds a bounded runtime stack; `includeSql: true` adds bounded
2102
+ SQL text. Bind values are never recorded. Raw SQL text can itself contain inline
2103
+ literals. `Database.$diagnostics()` returns the current snapshot, or null when
2104
+ disabled; the completion callback includes settlement timings. Only the first
2105
+ 200 completed statements are retained, while totals cover every statement.
2106
+
2107
+ | Metric | Meaning |
2108
+ | --- | --- |
2109
+ | `queueMs` | Workload admission wait, including refused waits |
2110
+ | `poolMs` | Driver acquisition through transaction callback entry, including BEGIN |
2111
+ | `setupMs` | Isolation, cancellation ticket and role/claims setup |
2112
+ | `sqlMs` | Awaited driver time, including execution, lock and network waits |
2113
+ | `commitMs` / `rollbackMs` | Observed settlement waits |
2114
+ | `rows` / `bytes` | Returned root rows / estimated JSON result bytes, not wire bytes |
2115
+ | `commits` / `commitFailures` | Acknowledged commits / failed commit acknowledgements |
2116
+ | `rollbackRequests` / `retries` | Requested rollbacks / whole-transaction replays |
2117
+
2118
+ Durations can overlap across concurrent operations; summing them is not request
2119
+ wall time. The driver cannot separate lock wait from server execution. Telemetry
2120
+ callback failures do not change an acknowledged transaction outcome.
1755
2121
 
1756
2122
 
1757
2123
 
@@ -2435,6 +2801,21 @@ calling user's rows — no `WHERE owner = …` needed in the handler. To read or
2435
2801
  write across all users (e.g. an admin job), use the explicit bypass:
2436
2802
  `Database.$asService()` (see [database.md](./database.md#bypassing-rls--databaseasservice)).
2437
2803
 
2804
+ Name your **update** policies deliberately: each name becomes a value in the row's
2805
+ `can` array (`row.can: ("transfers_approve" | "transfers_cancel")[]` in
2806
+ `palbase-env.d.ts`), and `can(table)` puts the same names into a response schema
2807
+ as an enum. Only `.for("update")` counts: a `.for("all")` policy like the
2808
+ owner policies above governs UPDATE but is never echoed, so a table with only
2809
+ `for("all")` policies carries `can: []`. In a raw-string `using`, write column
2810
+ names UNQUALIFIED (`status = 'pending_approval'`, not `transfers.status` or
2811
+ `public.transfers.status`) and wrap `auth.has_permission(...)` in `(select …)`:
2812
+ the engine selects the expression under a row alias, and a `using` it cannot
2813
+ carry there — a table qualifier, a comment, an unwrapped `has_permission`, a
2814
+ policy name longer than 59 bytes — is dropped from `can` with a `warn`, never
2815
+ spliced. A column or relation named `can` is reserved (see [auth.md](./auth.md)).
2816
+ `can` is for the interface; it cannot be used for an authorization decision; it
2817
+ may be incomplete; the server decides again on every request.
2818
+
2438
2819
  ### How policies are applied
2439
2820
 
2440
2821
  On deploy, Palbase diffs your declared schema against the live database and
package/docs/schema.md CHANGED
@@ -676,6 +676,21 @@ calling user's rows — no `WHERE owner = …` needed in the handler. To read or
676
676
  write across all users (e.g. an admin job), use the explicit bypass:
677
677
  `Database.$asService()` (see [database.md](./database.md#bypassing-rls--databaseasservice)).
678
678
 
679
+ Name your **update** policies deliberately: each name becomes a value in the row's
680
+ `can` array (`row.can: ("transfers_approve" | "transfers_cancel")[]` in
681
+ `palbase-env.d.ts`), and `can(table)` puts the same names into a response schema
682
+ as an enum. Only `.for("update")` counts: a `.for("all")` policy like the
683
+ owner policies above governs UPDATE but is never echoed, so a table with only
684
+ `for("all")` policies carries `can: []`. In a raw-string `using`, write column
685
+ names UNQUALIFIED (`status = 'pending_approval'`, not `transfers.status` or
686
+ `public.transfers.status`) and wrap `auth.has_permission(...)` in `(select …)`:
687
+ the engine selects the expression under a row alias, and a `using` it cannot
688
+ carry there — a table qualifier, a comment, an unwrapped `has_permission`, a
689
+ policy name longer than 59 bytes — is dropped from `can` with a `warn`, never
690
+ spliced. A column or relation named `can` is reserved (see [auth.md](./auth.md)).
691
+ `can` is for the interface; it cannot be used for an authorization decision; it
692
+ may be incomplete; the server decides again on every request.
693
+
679
694
  ### How policies are applied
680
695
 
681
696
  On deploy, Palbase diffs your declared schema against the live database and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palbase/backend",
3
- "version": "33.0.2",
3
+ "version": "34.1.0",
4
4
  "description": "Palbase Backend SDK — class controllers (@Controller/@Get/@Post + @Body/@QueryParams/@Param), error classes, schema DSL",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -91,7 +91,6 @@
91
91
  "files": [
92
92
  "dist",
93
93
  "docs",
94
- "stack-images.json",
95
94
  "stager",
96
95
  "template"
97
96
  ],
@@ -31,6 +31,18 @@ const notes = defineTable("notes", {
31
31
  .using("user_id = (select auth.uid())")
32
32
  .withCheck("user_id = (select auth.uid())"),
33
33
 
34
+ // A NAMED update policy is also what the row's `can` array echoes back:
35
+ // `note.can` contains "notes_edit_own" when THIS caller may edit THIS row
36
+ // (typed from this file). Put it on the wire with
37
+ // `can(publicSchema.tables.notes)` in the response schema — the schema's
38
+ // table definition, not this handle. `can` is for the interface; it cannot be used for an
39
+ // authorization decision; it may be incomplete; the server decides again
40
+ // on every request.
41
+ policy("notes_edit_own")
42
+ .for("update")
43
+ .to("authenticated")
44
+ .using("user_id = (select auth.uid())"),
45
+
34
46
  // MODERATION, when you want it — delete these two if you do not.
35
47
  //
36
48
  // Roles are yours and there is no built-in "admin": you declare them from
@@ -9,7 +9,7 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@palbase/backend": "^33.0.0",
12
+ "@palbase/backend": "^34.0.0",
13
13
  "reflect-metadata": "^0.2.2"
14
14
  },
15
15
  "engines": {
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/decorators/registry.ts","../src/decorators/controller.ts"],"sourcesContent":["// The decorator registry — the single plain-data store the method + parameter\n// decorators write into, and the deploy/dispatch pipeline reads back. No\n// `reflect-metadata`, no `emitDecoratorMetadata`: the registry is built from the\n// decorator arguments + the parameter INDEX that esbuild/tsc preserve for legacy\n// parameter decorators (verified — see the design spec §0/§4.1).\n//\n// A controller class carries its route metadata on a symbol-keyed static\n// property (`ROUTES`). `@Get`/`@Post`/… append a {@link RouteMeta} entry;\n// `@Body`/`@User`/… append a {@link ParamMeta} entry onto the route for the\n// method they decorate. Because parameter decorators run BEFORE the method\n// decorator for the same member (TS evaluates innermost-first, params before the\n// method), the route entry may not exist yet when a param decorator fires — so\n// param metadata is buffered per method name and merged when the method\n// decorator creates the route entry.\nimport type { AuthSpec, RateLimitConfig } from \"../endpoint.js\";\nimport type { UploadConfig } from \"./upload.js\";\nimport type { SseConfig } from \"./sse.js\";\nimport type { ZodTypeAny } from \"zod\";\n\n/** The HTTP verbs a route may declare, upper-cased (the runtime router +\n * OpenAPI lower-case on their own). */\nexport type HttpMethodUpper = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\" | \"QUERY\";\n\n/** Route-level options accepted by the method decorators (`@Get`/`@Post`/…). */\nexport interface RouteOptions {\n /** OVERRIDES the controller-level default auth for this one route. */\n auth?: AuthSpec;\n /** Per-route rate limit. */\n rateLimit?: RateLimitConfig;\n /** Direct-storage upload config — present ONLY on `@Upload` routes (the\n * `@Get`/`@Post`/… decorators never set it). Its presence is what MARKS a\n * route as an upload route through the whole pipeline (registry → flatten →\n * openapi → codegen). The bytes go client→storage directly; the method body\n * runs as the completion handler. See {@link UploadConfig} (decorators/upload.ts). */\n uploadConfig?: UploadConfig;\n /** Streaming config — present ONLY on `@Sse` routes (the `@Get`/`@Post`/…\n * decorators never set it). Its presence is what MARKS a route as a streaming\n * route through the whole pipeline (registry → flatten → openapi → codegen),\n * exactly as `uploadConfig` does for uploads — never a special HTTP verb. An\n * `@Sse` route registers POST like any input-bearing route, so the verb cannot\n * carry the distinction. See {@link SseConfig} (decorators/sse.ts). */\n sseConfig?: SseConfig;\n}\n\n/** The kind of value a parameter decorator injects. Drives both dispatch\n * (which request slice to inject) and codegen (which OpenAPI parameter source a\n * schema-bearing kind maps to). */\nexport type ParamKind =\n | \"body\"\n | \"query\"\n | \"param\"\n | \"headers\"\n | \"user\"\n | \"optionalUser\"\n | \"client\"\n | \"requestId\"\n | \"traceId\"\n | \"req\"\n // `@UploadedObject()` — injects the uploaded object (completion input) on an\n // `@Upload` route. No schema (the shape is the fixed UploadedObject type).\n | \"uploadedObject\"\n // `@SseOut()` — injects the frame writer on an `@Sse` route. No schema (the\n // shape is the fixed SseWriter type).\n | \"sseOut\"\n // `@Signal()` — injects the request's AbortSignal, which aborts when the\n // client disconnects. No schema. NOT derivable from `@Req()`: PBRequest\n // carries only request-scoped data and has no signal (endpoint.ts:358-363).\n | \"signal\";\n\n/** One parameter decorator's recorded metadata. `index` is the parameter\n * position esbuild/tsc preserve; `schema` is present for the schema-bearing\n * kinds (`body`/`query`/`headers`); `name` is the path-param name for `param`. */\nexport interface ParamMeta {\n index: number;\n kind: ParamKind;\n /** Zod schema for `body`/`query`/`headers` (validation + codegen source). */\n schema?: ZodTypeAny;\n /** Path-param name for `@Param(\"id\")`. */\n name?: string;\n}\n\n/** One inferred throw site: the error CLASS name (e.g. \"TodoLocked\") and its\n * wire code (e.g. \"todo_locked\"). `status`, `hasData`, and the data JSON schema\n * are NOT carried here — they resolve from the error registry by `code` at\n * extract/openapi time (single source of truth). */\nexport interface ThrowDescriptor {\n name: string;\n code: string;\n}\n\n/** One route's recorded metadata: the verb + subpath + method name + options,\n * the ordered parameter metas, and the resolved return schema (injected by the\n * codegen step — see `returnSchema`). */\nexport interface RouteMeta {\n method: HttpMethodUpper;\n subpath: string;\n fnName: string;\n options: RouteOptions;\n params: ParamMeta[];\n /** Response schema for the route, if any. Derived from the method's RETURN\n * TYPE by codegen and written here via `recordReturn` (a generated top-level\n * IIFE injected per controller), not by an author-written decorator. */\n returnSchema?: ZodTypeAny;\n /** Error classes this route can throw, if inferred. Derived from the method\n * body + service call graph by the deploy stager's throw analysis and written\n * here via `recordThrows` (a generated top-level IIFE injected per controller,\n * the `recordReturn` twin), not by an author-written decorator. */\n throws?: ThrowDescriptor[];\n}\n\n/** Symbol the route metadata list is stored under on a controller class. Using\n * a symbol (not a string key) keeps it off the public structural surface and\n * avoids any chance of an authored property collision. */\nexport const ROUTES: unique symbol = Symbol.for(\"palbase.backend.routes\");\n\n/** Symbol the per-method buffered parameter metas are stored under while a class\n * is being decorated. Parameter decorators fire before the method decorator, so\n * they buffer here keyed by method name; the method decorator drains the buffer\n * into the route entry it creates. */\nconst PARAM_BUFFER: unique symbol = Symbol.for(\"palbase.backend.paramBuffer\");\n\n/** A room's own slots. Rooms are NOT routes — no verb, no path, no params — so\n * they get their own carrier slots instead of being squeezed into RouteMeta. */\nconst ROOM: unique symbol = Symbol.for(\"palbase.backend.room\");\nconst ROOM_HOOKS: unique symbol = Symbol.for(\"palbase.backend.roomHooks\");\n\n/** Symbol the per-method buffered return-type schemas are stored under while a\n * class's registry is being populated. The codegen-injected `recordReturn` call\n * can fire before OR after the method decorator; it buffers here keyed by method\n * name and `recordRoute` drains it into the route entry (and `recordReturn`\n * writes through if the route already exists). Buffering on BOTH sides means a\n * fully-formed route entry always carries its return schema — a raw-symbol\n * reader (the runtime) never has to re-merge. */\nconst RETURN_BUFFER: unique symbol = Symbol.for(\"palbase.backend.returnBuffer\");\n\n/** Symbol the per-method buffered throw descriptors are stored under while a\n * class's registry is being populated. The stager-injected `recordThrows` call\n * can fire before OR after the method decorator; it buffers here keyed by method\n * name and `recordRoute` drains it into the route entry (and `recordThrows`\n * writes through if the route already exists). Buffering on BOTH sides means a\n * fully-formed route entry always carries its throw descriptors — a raw-symbol\n * reader (the runtime) never has to re-merge. */\nconst THROWS_BUFFER: unique symbol = Symbol.for(\"palbase.backend.throwsBuffer\");\n\n/** A class constructor carrying the symbol-keyed registry slots. We type the\n * registry-bearing class as this so the decorators can read/write the slots\n * without `any` — a plain `Function` does not carry index signatures. */\ninterface RegistryCarrier {\n [ROUTES]?: RouteMeta[];\n [PARAM_BUFFER]?: Record<string, ParamMeta[]>;\n [ROOM]?: RoomMeta;\n [ROOM_HOOKS]?: RoomBuffer;\n [RETURN_BUFFER]?: Record<string, ZodTypeAny>;\n [THROWS_BUFFER]?: Record<string, ThrowDescriptor[]>;\n}\n\n/** Coerce a decorated target (class constructor or its prototype) into the\n * registry carrier that owns the slots. Method/param decorators receive the\n * PROTOTYPE as their target; the class decorator receives the constructor. We\n * always anchor the registry on the CONSTRUCTOR so `getRoutes(ctor)` finds it. */\nfunction carrierOf(target: object): RegistryCarrier {\n // For instance-member decorators, `target` is the prototype; its `.constructor`\n // is the class. For a static member or the class decorator, `target` is the\n // constructor already. Resolve to the constructor either way.\n const ctor =\n typeof target === \"function\"\n ? (target as unknown as RegistryCarrier)\n : (((target as { constructor?: unknown }).constructor ??\n target) as unknown as RegistryCarrier);\n return ctor;\n}\n\n/** Get (creating if absent) the own route list for a class constructor. Own —\n * not inherited — so a subclass does not mutate its base's routes. */\nfunction ownRoutes(carrier: RegistryCarrier): RouteMeta[] {\n if (!Object.prototype.hasOwnProperty.call(carrier, ROUTES)) {\n carrier[ROUTES] = [];\n }\n return carrier[ROUTES] as RouteMeta[];\n}\n\n/** Get (creating if absent) the own per-method param buffer for a class. */\nfunction ownParamBuffer(carrier: RegistryCarrier): Record<string, ParamMeta[]> {\n if (!Object.prototype.hasOwnProperty.call(carrier, PARAM_BUFFER)) {\n carrier[PARAM_BUFFER] = {};\n }\n return carrier[PARAM_BUFFER] as Record<string, ParamMeta[]>;\n}\n\n/** Record a route (called by the method decorators). Drains any parameter\n * metas already buffered for `fnName` into the new route entry, then sorts them\n * by parameter index so dispatch can inject positionally. */\nexport function recordRoute(\n target: object,\n fnName: string,\n method: HttpMethodUpper,\n subpath: string,\n options: RouteOptions,\n): void {\n const carrier = carrierOf(target);\n const routes = ownRoutes(carrier);\n const buffer = ownParamBuffer(carrier);\n const params = (buffer[fnName] ?? []).slice().sort((a, b) => a.index - b.index);\n const route: RouteMeta = { method, subpath, fnName, options, params };\n // Drain a buffered return schema (the recordReturn-ran-first ordering) so the\n // route entry is complete the moment it's created — a raw-symbol consumer\n // (the runtime extractor/worker) sees the return schema without re-merging.\n const returnBuffer = carrier[RETURN_BUFFER];\n if (returnBuffer && returnBuffer[fnName] !== undefined) {\n route.returnSchema = returnBuffer[fnName];\n }\n // Same drain for buffered throw descriptors (the recordThrows-ran-first\n // ordering) — the route entry is complete the moment it's created.\n const throwsBuffer = carrier[THROWS_BUFFER];\n if (throwsBuffer && throwsBuffer[fnName] !== undefined) {\n route.throws = throwsBuffer[fnName];\n }\n routes.push(route);\n}\n\n/** Record one parameter decorator (called by `@Body`/`@User`/…). Buffers per\n * method name; the method decorator merges the buffer into the route entry. If\n * the route already exists (method decorator ran first — TS does evaluate the\n * method decorator AFTER its parameter decorators, but we stay order-robust),\n * the meta is also appended directly so neither ordering loses it. */\nexport function recordParam(target: object, fnName: string, meta: ParamMeta): void {\n const carrier = carrierOf(target);\n const buffer = ownParamBuffer(carrier);\n (buffer[fnName] ??= []).push(meta);\n\n // Order-robust: if the route already exists, merge in place + keep sorted.\n const routes = carrier[ROUTES];\n if (routes) {\n const route = routes.find((r) => r.fnName === fnName);\n if (route) {\n route.params.push(meta);\n route.params.sort((a, b) => a.index - b.index);\n }\n }\n}\n\n/** Attach a return schema to the route for `fnName` (called by the codegen\n * injection that reads the method's return type). If the route does not exist\n * yet, the schema is buffered (RETURN_BUFFER) and drained into the route by\n * `recordRoute` when the method decorator runs. */\nexport function recordReturn(target: object, fnName: string, schema: ZodTypeAny): void {\n const carrier = carrierOf(target);\n const routes = carrier[ROUTES];\n const route = routes?.find((r) => r.fnName === fnName);\n if (route) {\n route.returnSchema = schema;\n return;\n }\n if (!Object.prototype.hasOwnProperty.call(carrier, RETURN_BUFFER)) {\n carrier[RETURN_BUFFER] = {};\n }\n const returnBuffer = carrier[RETURN_BUFFER];\n if (returnBuffer) returnBuffer[fnName] = schema;\n}\n\n/** Attach the inferred throw descriptors to the route for `fnName` (called by\n * the stager-injected IIFE that carries the throw analysis result — the\n * `recordReturn` twin). If the route does not exist yet, the descriptors are\n * buffered (THROWS_BUFFER) and drained into the route by `recordRoute` when the\n * method decorator runs. */\nexport function recordThrows(target: object, fnName: string, throws: ThrowDescriptor[]): void {\n const carrier = carrierOf(target);\n const routes = carrier[ROUTES];\n const route = routes?.find((r) => r.fnName === fnName);\n if (route) {\n route.throws = throws;\n return;\n }\n if (!Object.prototype.hasOwnProperty.call(carrier, THROWS_BUFFER)) {\n carrier[THROWS_BUFFER] = {};\n }\n const throwsBuffer = carrier[THROWS_BUFFER];\n if (throwsBuffer) throwsBuffer[fnName] = throws;\n}\n\n/** Read the route metadata for a controller class (the deploy/dispatch entry\n * point). Applies any buffered return schemas + throw descriptors (for the\n * recordReturn/recordThrows-runs-before orderings) and returns a defensive copy\n * so callers cannot mutate the registry.\n */\nexport function getRoutes(ctor: object): RouteMeta[] {\n const carrier = carrierOf(ctor);\n const routes = carrier[ROUTES] ?? [];\n const returnBuffer = carrier[RETURN_BUFFER];\n if (returnBuffer) {\n for (const route of routes) {\n const buffered = returnBuffer[route.fnName];\n if (buffered && route.returnSchema === undefined) {\n route.returnSchema = buffered;\n }\n }\n }\n const throwsBuffer = carrier[THROWS_BUFFER];\n if (throwsBuffer) {\n for (const route of routes) {\n const buffered = throwsBuffer[route.fnName];\n if (buffered && route.throws === undefined) {\n route.throws = buffered;\n }\n }\n }\n return routes.map((r) => ({\n ...r,\n params: r.params.slice(),\n ...(r.throws !== undefined ? { throws: r.throws.slice() } : {}),\n }));\n}\n\n// ── rooms ───────────────────────────────────────────────────────────────────\n\n/** Which lifecycle hook a method is bound to. */\nexport type RoomHook = \"authorize\" | \"first\" | \"join\" | \"leave\" | \"empty\";\n\nexport interface RoomMessageMeta {\n fnName: string;\n schema: ZodTypeAny;\n}\n\nexport interface RoomMeta {\n pattern: string;\n events: Record<string, ZodTypeAny>;\n graceMs: number;\n /** hook → the method name that implements it. */\n hooks: Partial<Record<RoomHook, string>>;\n /** inbound message name → its method and payload schema. */\n messages: Record<string, RoomMessageMeta>;\n}\n\ninterface RoomBuffer {\n hooks: Partial<Record<RoomHook, string>>;\n messages: Record<string, RoomMessageMeta>;\n}\n\n/** Get (creating if absent) the own hook buffer. Own — not inherited — so a\n * subclass never mutates its base's hooks. Member decorators fill this BEFORE\n * the class decorator runs; controller.ts:203 depends on the same ordering. */\nfunction ownRoomBuffer(carrier: RegistryCarrier): RoomBuffer {\n if (!Object.prototype.hasOwnProperty.call(carrier, ROOM_HOOKS)) {\n carrier[ROOM_HOOKS] = { hooks: {}, messages: {} };\n }\n return carrier[ROOM_HOOKS] as RoomBuffer;\n}\n\n/** Record a lifecycle hook. A second method for the same hook is refused: two\n * answers to \"who handles this\" cannot be resolved at dispatch, and silently\n * keeping one is how a hook stops running without anyone being told. */\nexport function recordRoomHook(target: object, hook: RoomHook, fnName: string): void {\n const buffer = ownRoomBuffer(carrierOf(target));\n const existing = buffer.hooks[hook];\n if (existing !== undefined && existing !== fnName) {\n const label = `On${hook.charAt(0).toUpperCase()}${hook.slice(1)}`;\n throw new Error(\n `@${label} is declared twice in one room (${existing} and ${fnName}). ` +\n `A room has one of each hook.`,\n );\n }\n buffer.hooks[hook] = fnName;\n}\n\n/** Record an inbound message handler. */\nexport function recordRoomMessage(\n target: object,\n name: string,\n fnName: string,\n schema: ZodTypeAny,\n): void {\n const buffer = ownRoomBuffer(carrierOf(target));\n const existing = buffer.messages[name];\n if (existing !== undefined && existing.fnName !== fnName) {\n throw new Error(\n `@OnMessage(\"${name}\") is declared twice in one room (${existing.fnName} and ${fnName}).`,\n );\n }\n buffer.messages[name] = { fnName, schema };\n}\n\n/** Record the room itself (called by the class decorator), draining the buffer\n * the member decorators already filled. */\nexport function recordRoom(\n ctor: object,\n meta: Omit<RoomMeta, \"hooks\" | \"messages\">,\n): void {\n const carrier = carrierOf(ctor);\n const buffer = ownRoomBuffer(carrier);\n carrier[ROOM] = { ...meta, hooks: buffer.hooks, messages: buffer.messages };\n // AND into the shared class registry, the same slot @Controller pushes to.\n //\n // Without this line a room compiles, bundles and deploys, and is never called:\n // the bundler's entry exports `SDK.getRegisteredControllers()` and the runtime\n // reads its rooms out of THAT list (server.ts's `collectRooms`). A room that\n // only marks its own constructor is a room nobody can find — measured, on the\n // fixture, at the last gate before it would have worked.\n //\n // Rooms and controllers share one list because they are one thing to the\n // loader: classes a bundle declared. What each IS is decided by the marker it\n // carries, never by which list it arrived in.\n const g = globalThis as unknown as Record<symbol, unknown[] | undefined>;\n const REGISTRY = Symbol.for(\"palbase.backend.allControllers\");\n const all = (g[REGISTRY] ??= []);\n if (!all.includes(ctor)) all.push(ctor);\n}\n\n/** The room a class declares, or undefined. A class with no `@Room` is not a\n * room — the marker is the CONFIG's presence, never a name or a base class\n * (the rule upload.ts:13-16 states for uploads). */\nexport function getRoom(ctor: object): RoomMeta | undefined {\n const carrier = carrierOf(ctor);\n if (!Object.prototype.hasOwnProperty.call(carrier, ROOM)) return undefined;\n return carrier[ROOM] as RoomMeta;\n}\n","// `@Controller(basePath, options?)` — the class decorator that marks a class as\n// a Palbase backend controller. It stamps a non-enumerable `__palbase`\n// discriminant + the resolved controller metadata onto the class so the\n// deploy/dispatch pipeline (and `isController`/`resolveController`) can detect\n// and read it without `reflect-metadata`.\nimport type { AuthSpec } from \"../endpoint.js\";\nimport { getRoutes } from \"./registry.js\";\n\n/** The controller metadata stamped onto a `@Controller`-decorated class. The\n * default export of a `controllers/*.controller.ts` file resolves to this via\n * {@link resolveController}. */\nexport interface ControllerMeta {\n /** Discriminant the runtime + tooling read. */\n readonly __palbase: \"controller\";\n /** The base path every route in this controller mounts under (e.g. \"/todos\"). */\n basePath: string;\n /** Controller-level default auth, applied to routes that don't set their own\n * (`@Get(\"/x\", { auth })` overrides this). `undefined` ⇒ the application\n * default ({@link defineDefaultAuth}), and secure-by-default below that —\n * see {@link resolveEffectiveAuth} for the whole cascade. */\n defaultAuth?: AuthSpec;\n}\n\n/** Options accepted by `@Controller`. */\nexport interface ControllerOptions {\n /** Default auth for ALL routes in this controller (route-level overrides;\n * omitting it falls through to the application default declared with\n * {@link defineDefaultAuth}). */\n auth?: AuthSpec;\n}\n\n/** Symbol the controller metadata is stamped under. Symbol-keyed (not a string\n * property) so it never collides with an authored member and stays off the\n * structural surface. */\nexport const CONTROLLER_META: unique symbol = Symbol.for(\"palbase.backend.controllerMeta\");\n\n/**\n * Every class `@Controller` has decorated, in decoration order.\n *\n * This is what lets a controller file need no export at all: importing the file\n * runs the decorator, the decorator records the class here, and the runtime\n * reads the list. Without it the only handle on a class is its export name, so\n * every controller had to be exported AND named in a generated entry — the\n * ceremony NestJS still charges (`export class` PLUS\n * `@Module({controllers:[…]})`).\n *\n * Keyed on a well-known Symbol against globalThis rather than held in a module\n * variable, because a deployed bundle inlines its own copy of this package: two\n * copies would keep two lists, and the runtime would read the empty one. The\n * same hazard `runtimeHooks` exists for, closed the same way — one shared slot.\n */\nconst REGISTRY: unique symbol = Symbol.for(\"palbase.backend.allControllers\") as never;\n\nfunction registry(): unknown[] {\n const g = globalThis as unknown as Record<symbol, unknown[] | undefined>;\n const existing = g[REGISTRY];\n if (existing) return existing;\n const fresh: unknown[] = [];\n g[REGISTRY] = fresh;\n return fresh;\n}\n\n/**\n * The controller classes this process has loaded, in decoration order.\n *\n * Decoration order is import order, which the bundler fixes by sorting the\n * files it emits imports for — so two builds of one tree produce the same\n * route table, and route precedence is not a function of module-resolution\n * accidents.\n */\nexport function getRegisteredControllers(): readonly unknown[] {\n return registry().slice();\n}\n\n/** Empty the registry. For tests, which load controllers repeatedly. */\nexport function __resetRegisteredControllers(): void {\n registry().length = 0;\n}\n\n/**\n * The APPLICATION-level default auth.\n *\n * Held on globalThis under a well-known Symbol for exactly the reason\n * {@link REGISTRY} is: a deployed bundle inlines its own copy of this package,\n * and two copies keeping two defaults is how a security setting silently\n * becomes two different settings.\n */\nconst APP_DEFAULT_AUTH: unique symbol = Symbol.for(\"palbase.backend.appDefaultAuth\") as never;\n\nfunction appAuthSlot(): Record<symbol, AuthSpec | undefined> {\n return globalThis as unknown as Record<symbol, AuthSpec | undefined>;\n}\n\n/**\n * Declare the default auth for EVERY route in the application — the ring the\n * cascade consults when neither the route nor its controller says anything.\n *\n * The measured problem it removes: `auth: { verifiedEmail: true }` repeated by\n * hand on ten `@Controller`s. A security setting that must be repeated is a\n * security setting that will be forgotten — the eleventh controller opens the\n * door and nothing says so.\n *\n * Call it at MODULE SCOPE in a file the application imports (the controllers'\n * own barrel, or a module a controller imports). The cascade reads this slot\n * when the route table is built and when the spec is emitted — both of which\n * run after module loading — so declaration order does not matter, but being\n * imported at all does.\n *\n * @example\n * defineDefaultAuth({ verifiedEmail: true }); // every route, unless it says otherwise\n */\nexport function defineDefaultAuth(auth: AuthSpec): void {\n appAuthSlot()[APP_DEFAULT_AUTH] = auth;\n}\n\n/** The declared application default, or `undefined` when none was declared. */\nexport function getDefaultAuth(): AuthSpec | undefined {\n return appAuthSlot()[APP_DEFAULT_AUTH];\n}\n\n/** Clear the application default. For tests, which declare it repeatedly. */\nexport function __resetDefaultAuth(): void {\n delete appAuthSlot()[APP_DEFAULT_AUTH];\n}\n\n/**\n * THE auth cascade: route → controller → application → `true`.\n *\n * One function, every caller — the route table (`engine/router.ts`) and the\n * spec emitter (`openapi/controllers.ts`) ASK for the answer instead of\n * spelling the chain themselves. Two hand-written copies of a cascade is how\n * the build-time answer and the runtime answer come to disagree about who may\n * call an endpoint, and the disagreement shows up as an open door.\n *\n * The terminal `true` is secure-by-default and is load-bearing: a route that\n * declared nothing, under a controller that declared nothing, in an\n * application that declared nothing, is CLOSED.\n */\nexport function resolveEffectiveAuth(\n routeAuth: AuthSpec | undefined,\n controllerAuth: AuthSpec | undefined,\n): AuthSpec {\n return routeAuth ?? controllerAuth ?? getDefaultAuth() ?? true;\n}\n\n/** A class carrying the stamped controller metadata + discriminant. */\ninterface ControllerCarrier {\n __palbase?: \"controller\";\n [CONTROLLER_META]?: ControllerMeta;\n}\n\n/** The one path segment the platform owns. The isolate matches\n * `^/webhooks/([^/]+)$` on the raw request path BEFORE controller dispatch, so\n * anything a controller resolves to under it answers `404 webhook_not_found`\n * and never runs. */\nconst RESERVED_FIRST_SEGMENT = \"webhooks\";\n\n/**\n * Throw if `path` resolves under the reserved segment. Segments are compared the\n * way the isolate compares them — `split(\"/\").filter(Boolean)` — NOT by string\n * prefix, because empty segments collapse there: `@Controller(\"/\")` +\n * `@Post(\"/webhooks/x\")` composes to `//webhooks/x`, which the isolate serves as\n * `/webhooks/x`. A prefix check reads that as safe; the segment check does not.\n * `/webhooksy` stays allowed for the same reason — it is a different segment.\n *\n * Every verb is refused, not just the POST the isolate currently intercepts: the\n * reservation is of the URL namespace, so a `@Get(\"/webhooks/x\")` that happens\n * to work today would be silently shadowed the moment the isolate's method gate\n * widens. Refusing at build is recoverable; discovering it as a 404 is not.\n */\nfunction assertNotReserved(path: string, subject: string): void {\n const [first] = path.split(\"/\").filter(Boolean);\n if (first === RESERVED_FIRST_SEGMENT) {\n throw new Error(\n `${subject} resolves under the reserved /${RESERVED_FIRST_SEGMENT} path — ` +\n \"inbound webhooks are served there and would shadow this route\",\n );\n }\n}\n\n/**\n * Mark a class as a Palbase backend controller. `basePath` is the mount path\n * for every route the class declares; `options.auth` sets the controller-level\n * default auth (a route's own `auth` overrides it; absent ⇒ secure-by-default).\n *\n * @example\n * \\@Controller(\"/todos\", { auth: false })\n * export class TodosController {\n * \\@Get(\"\") list(\\@QueryParams(ListTodosQuery) q: ListTodosQuery): TodoSchema[] { … }\n * }\n */\nexport function Controller(basePath: string, options: ControllerOptions = {}) {\n return function <T extends abstract new (...args: never[]) => object>(ctor: T): T {\n // /webhooks/* belongs to the platform: the isolate matches the inbound\n // webhook route before controller dispatch, so a controller mounted here\n // would never receive a request. Silent shadowing is the failure mode this\n // whole change exists to remove, so refuse it at build.\n //\n // The COMPOSED path is what gets shadowed, not the base path. `@Controller(\"\")`\n // and `@Controller(\"/\")` both pass a base-path-only check while a\n // `@Post(\"/webhooks/stripe\")` inside them resolves to exactly the path the\n // isolate intercepts. Method decorators run BEFORE the class decorator (TS\n // evaluates members first), so every route this class declares is already in\n // the registry here — which is why the composed check can live at this one\n // seam instead of on the dispatch read path. The `@Controller(\"\") +\n // @Post(\"/webhooks/stripe\")` test is the lock on that ordering: if it ever\n // stopped holding, that test goes red.\n assertNotReserved(basePath, `@Controller(\"${basePath}\")`);\n for (const route of getRoutes(ctor)) {\n assertNotReserved(\n `${basePath}${route.subpath}`,\n `@${route.method}(\"${route.subpath}\") in @Controller(\"${basePath}\")`,\n );\n }\n\n const carrier = ctor as unknown as ControllerCarrier;\n const meta: ControllerMeta = {\n __palbase: \"controller\",\n basePath,\n ...(options.auth !== undefined ? { defaultAuth: options.auth } : {}),\n };\n // Non-enumerable so it doesn't leak onto instances / structural checks.\n Object.defineProperty(carrier, CONTROLLER_META, {\n value: meta,\n enumerable: false,\n configurable: true,\n writable: false,\n });\n // The bare `__palbase` discriminant is the cheap detection marker the\n // runtime/extractor checks; keep it readable but non-enumerable.\n Object.defineProperty(carrier, \"__palbase\", {\n value: \"controller\",\n enumerable: false,\n configurable: true,\n writable: false,\n });\n // Record it, so importing the file is enough and exporting is optional.\n // Guarded against a double-decoration re-entering the same class twice.\n const all = registry();\n if (!all.includes(ctor)) all.push(ctor);\n return ctor;\n };\n}\n\n/** True when `value` is a `@Controller`-decorated class (cheap discriminant\n * check). Accepts the class constructor (the default export of a controller\n * file). */\nexport function isController(value: unknown): boolean {\n if (typeof value !== \"function\" && (typeof value !== \"object\" || value === null)) {\n return false;\n }\n const carrier = value as ControllerCarrier;\n return carrier.__palbase === \"controller\" && carrier[CONTROLLER_META] !== undefined;\n}\n\n/** Read the resolved controller metadata off a `@Controller`-decorated class.\n * Throws if the class was not decorated — callers should gate with\n * {@link isController} first (the loader does). */\nexport function resolveController(ctor: unknown): ControllerMeta {\n if (typeof ctor !== \"function\" && (typeof ctor !== \"object\" || ctor === null)) {\n throw new TypeError(\"resolveController: value is not a class\");\n }\n const meta = (ctor as ControllerCarrier)[CONTROLLER_META];\n if (!meta) {\n throw new TypeError(\n \"resolveController: class is not a @Controller — decorate it with `@Controller(path)` and list it in a module's `controllers`\",\n );\n }\n return meta;\n}\n\n/**\n * A class the runtime constructs takes NO constructor parameters.\n *\n * ONE writer, four callers (controller, hook, job, webhook) and the build's own\n * check. Four hand-written copies of this message is how the four come to\n * disagree about what is refused — and the disagreement is silent, because a\n * class that slips past one of them still ends up with `undefined` fields.\n *\n * Why it is refused rather than injected: there is no container. The parameter\n * would arrive `undefined`, the code would compile, deploy, and fail at the\n * first request that touches the field — the most expensive place to learn it.\n */\nexport function assertZeroArgConstructor(Ctrl: unknown, kind: string): void {\n const arity = (Ctrl as { length?: number }).length ?? 0;\n if (arity === 0) return;\n\n // This function's NAME is a promise: whatever reaches it will be constructed\n // with a zero-argument constructor. Letting a class with parameters through\n // would make the name a lie — worse than the rename FR-029 guards against.\n //\n // An earlier design made it \"declaration-aware\": pass when metadata matches\n // arity. Measured, that re-opens the exact silence this exists to close.\n // ANY decorator triggers metadata emission, not just `@Injectable`:\n //\n // @Job(...) class J { constructor(repo: unknown) {} }\n // → design:paramtypes = [Object] ← matches arity\n //\n // so J would pass, then be built with `new J()`, and `repo` would be\n // `undefined` at the first scheduled run. Four tests in this repo asserted\n // that refusal and all four went red; the tests were right.\n //\n // What changed instead is the MESSAGE, and who calls this. Callers holding a\n // container resolve from it and never come here (router.ts, job/hook/webhook);\n // this stays the guard for the container-less path, and keeps its promise.\n const name = (Ctrl as { name?: string }).name ?? \"<anonymous>\";\n throw new Error(\n `${kind} ${name} declares a constructor with ${arity} parameter(s), and this path ` +\n `builds it with a zero-argument constructor — every parameter would arrive as ` +\n `undefined. If you meant to inject them, add @Injectable() to ${name} and list it ` +\n `in a module's providers, so the container builds it instead.`,\n );\n}\n"],"mappings":"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","names":["ROUTES","Symbol","for","PARAM_BUFFER","ROOM","ROOM_HOOKS","RETURN_BUFFER","THROWS_BUFFER","carrierOf","target","ctor","ownRoutes","carrier","Object","prototype","hasOwnProperty","call","ownParamBuffer","recordRoute","fnName","method","subpath","options","routes","buffer","params","slice","sort","a","b","index","route","returnBuffer","undefined","returnSchema","throwsBuffer","throws","push","recordParam","meta","find","r","recordThrows","target","fnName","throws","carrier","carrierOf","routes","ROUTES","route","find","r","Object","prototype","hasOwnProperty","call","THROWS_BUFFER","throwsBuffer","getRoutes","ctor","returnBuffer","RETURN_BUFFER","buffered","returnSchema","undefined","map","params","slice","ownRoomBuffer","ROOM_HOOKS","hooks","messages","recordRoomHook","hook","buffer","existing","label","charAt","toUpperCase","Error","recordRoomMessage","name","schema","recordRoom","meta","ROOM","g","globalThis","REGISTRY","Symbol","for","all","includes","push","getRoom","CONTROLLER_META","Symbol","for","REGISTRY","registry","g","globalThis","existing","fresh","getRegisteredControllers","slice","__resetRegisteredControllers","length","APP_DEFAULT_AUTH","appAuthSlot","defineDefaultAuth","auth","getDefaultAuth","__resetDefaultAuth","resolveEffectiveAuth","routeAuth","controllerAuth","RESERVED_FIRST_SEGMENT","assertNotReserved","path","subject","first","split","filter","Boolean","Error","Controller","basePath","options","ctor","route","getRoutes","subpath","method","carrier","meta","__palbase","undefined","defaultAuth","Object","defineProperty","value","enumerable","configurable","writable","all","includes","push","isController","resolveController","TypeError","assertZeroArgConstructor","Ctrl","kind","arity","name"]}