@pylonsync/sdk 0.3.291 → 0.3.293

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.
@@ -0,0 +1,949 @@
1
+ export type RouteMode = "static" | "server" | "live" | "ssr";
2
+ export type FieldType = "string" | "int" | "float" | "bool" | "datetime" | "richtext" | `id(${string})`;
3
+ /**
4
+ * CRDT container override for a field. Wire format is the kebab-case
5
+ * string each variant maps to (`"text"`, `"counter"`, `"movable-list"`,
6
+ * etc.). Mirror of `pylon_kernel::CrdtAnnotation` on the Rust side.
7
+ *
8
+ * - `"text"` upgrades a `string` to LoroText (collaborative
9
+ * character-level merge instead of LWW).
10
+ * - `"counter"` flips an `int` / `float` to LoroCounter so concurrent
11
+ * increments add instead of stomping each other.
12
+ * - `"list"`, `"movable-list"`, `"tree"` are reserved for ordered /
13
+ * reorderable / hierarchical collections — wire format locked in,
14
+ * server-side projection still pending implementation.
15
+ * - `"lww"` is explicit (matches the default for most scalar types).
16
+ */
17
+ export type CrdtAnnotation = "lww" | "text" | "counter" | "list" | "movable-list" | "tree";
18
+ /**
19
+ * Insert-time default marker stored on a field definition. Apps never
20
+ * construct these directly — they come from `.default(v)` / `.defaultNow()`
21
+ * / `.owner()` on a field builder. The runtime + codegen read them.
22
+ */
23
+ export type DefaultMarker = {
24
+ kind: "value";
25
+ value: unknown;
26
+ } | {
27
+ kind: "now";
28
+ } | {
29
+ kind: "owner";
30
+ };
31
+ export interface FieldDefinition {
32
+ type: FieldType;
33
+ optional: boolean;
34
+ unique: boolean;
35
+ /** CRDT container override. Omitted entirely for the default
36
+ * (LWW for scalars, LoroText for richtext). */
37
+ crdt?: CrdtAnnotation;
38
+ /**
39
+ * When true, the field is **never serialized in HTTP responses**.
40
+ * Use for secrets / billing-side identity / hashes that the server
41
+ * needs to read internally but should never leak to clients.
42
+ *
43
+ * Stripped at every public read boundary:
44
+ * - `GET /api/entities/<entity>` (list)
45
+ * - `GET /api/entities/<entity>/<id>` (single row)
46
+ * - `GET /api/auth/session` (User row projection)
47
+ * - Sync push deltas
48
+ *
49
+ * Still readable from inside server functions via `ctx.db.*` —
50
+ * the framework trusts your handler logic to decide what to
51
+ * return. If you pass the row through unmodified to the client,
52
+ * the field IS still stripped at the function-response boundary,
53
+ * provided the value is a plain row from `ctx.db.get` (which
54
+ * tags it with the entity name so the boundary knows what to
55
+ * filter).
56
+ *
57
+ * `passwordHash` on every User entity is implicitly serverOnly
58
+ * even without this annotation, by the framework's hardcoded
59
+ * convention. New apps should still mark it explicitly so the
60
+ * intent shows up in the schema.
61
+ */
62
+ serverOnly?: boolean;
63
+ /**
64
+ * When true, the field is **set on insert but cannot be changed
65
+ * by client updates**. The framework rejects any `PATCH`/`PUT`
66
+ * payload that mentions the field with a `READONLY_FIELD` error,
67
+ * before policy evaluation. Admin contexts bypass this check (as
68
+ * with all other framework gates), so migrations + ops scripts
69
+ * can still rewrite owner-shaped fields.
70
+ *
71
+ * Use for identity-shaped columns that need to be settable at
72
+ * creation but immutable after — `authorId`, `orgId`,
73
+ * `createdBy`, `stripeCustomerId`. Closes the canonical IDOR
74
+ * shape where a policy gates on `data.authorId == auth.userId`
75
+ * but the attacker passes a different `authorId` in the update
76
+ * payload to flip the row's ownership.
77
+ *
78
+ * Server-side writes (via `ctx.db.update` inside a function)
79
+ * still go through — readonly only blocks the HTTP entity
80
+ * routes (`PATCH /api/entities/<entity>/<id>`) and `/api/transact`.
81
+ * Server code is trusted to enforce its own invariants.
82
+ */
83
+ readonly?: boolean;
84
+ /**
85
+ * When true, the field is AEAD-encrypted at rest. The framework
86
+ * encrypts the value before writing to SQLite/Postgres and
87
+ * decrypts on read. Cipher: ChaCha20-Poly1305. Key:
88
+ * `PYLON_ENCRYPTION_KEY` env (32 bytes, hex or base64).
89
+ *
90
+ * Use for PII / secrets that must survive a DB-file leak: API
91
+ * keys stored on rows, social security numbers, OAuth tokens.
92
+ * Plaintext only exists inside the Pylon process.
93
+ *
94
+ * Restrictions:
95
+ * - Encrypted fields are NOT queryable. `ctx.db.lookup` /
96
+ * `WHERE encryptedField = 'x'` always returns nothing because
97
+ * each write produces fresh ciphertext.
98
+ * - Cannot combine with `unique: true`.
99
+ * - Valid only on `string`, `richtext`, and JSON-shaped fields.
100
+ *
101
+ * Decryption pre-pass means rows written BEFORE the field was
102
+ * annotated `encrypted: true` continue to read fine (passes
103
+ * through as plaintext). Next write through the mutation
104
+ * pipeline upgrades them to ciphertext.
105
+ */
106
+ encrypted?: boolean;
107
+ /**
108
+ * Insert-time default. Set via `.default(value)` / `.defaultNow()` /
109
+ * `.owner()` on the field builder; recorded for the runtime + codegen.
110
+ */
111
+ default?: DefaultMarker;
112
+ /**
113
+ * Allowed values for `field.enum([...])` — recorded so codegen emits a
114
+ * precise literal-union type instead of a wide `string`.
115
+ */
116
+ enumValues?: readonly string[];
117
+ }
118
+ interface FieldBuilder {
119
+ readonly _def: FieldDefinition;
120
+ optional(): FieldBuilder;
121
+ unique(): FieldBuilder;
122
+ /**
123
+ * Override the CRDT container for this field. See [`CrdtAnnotation`]
124
+ * for the full list. Most apps never call this — the default mapping
125
+ * (string→LWW, richtext→LoroText, …) is the right answer.
126
+ *
127
+ * Example: `field.string().crdt("text")` upgrades a string to a
128
+ * collaborative LoroText so two browser tabs editing the field
129
+ * concurrently merge cleanly instead of last-write-wins.
130
+ */
131
+ crdt(annotation: CrdtAnnotation): FieldBuilder;
132
+ /**
133
+ * Mark the field as never-returned in HTTP responses. See
134
+ * [`FieldDefinition.serverOnly`] for the full semantics.
135
+ *
136
+ * Example: `stripeCustomerId: field.string().serverOnly()` keeps
137
+ * the Stripe customer id out of `/api/entities/Org/<id>` responses
138
+ * while staying readable from `ctx.db.get` inside the
139
+ * stripeWebhook action.
140
+ */
141
+ serverOnly(): FieldBuilder;
142
+ /**
143
+ * Mark the field as set-on-insert-only. See [`FieldDefinition.readonly`]
144
+ * for the full semantics.
145
+ *
146
+ * Example: `authorId: field.id("User").readonly()` lets the framework
147
+ * reject any `PATCH` payload trying to rewrite the row's author —
148
+ * closes the IDOR-via-update-payload class.
149
+ */
150
+ readonly(): FieldBuilder;
151
+ /**
152
+ * Mark the field as AEAD-encrypted at rest. See
153
+ * [`FieldDefinition.encrypted`] for the full semantics +
154
+ * restrictions.
155
+ *
156
+ * Example: `apiKey: field.string().serverOnly().encrypted()`
157
+ * keeps the key out of HTTP responses AND encrypts the bytes
158
+ * sitting in SQLite.
159
+ */
160
+ encrypted(): FieldBuilder;
161
+ /**
162
+ * Mark the field as the row's **owner** — the framework stamps it
163
+ * with `auth.userId` on every insert and refuses any client attempt
164
+ * to set it to a different user. This is what makes *optimistic,
165
+ * local-first writes the default* for owned data: the client can
166
+ * `db.insert("Offer", { buyerId, … })` directly (paints the row in
167
+ * the local store instantly, no server round-trip) and still be
168
+ * secure — the server overwrites/validates the owner from the
169
+ * session, so a forged `buyerId` is rejected before it ever lands.
170
+ *
171
+ * Concretely `.owner()`:
172
+ * - on **insert**, fills the field from `auth.userId` when omitted,
173
+ * and rejects a non-admin caller who supplies a *different*
174
+ * non-empty value (`OWNER_MISMATCH`, 403) — closing the IDOR
175
+ * shape where a policy gates on `data.ownerId == auth.userId`
176
+ * but the attacker just sends someone else's id;
177
+ * - on **update**, behaves like [`readonly`] — the owner can't be
178
+ * reassigned through the HTTP entity routes.
179
+ *
180
+ * Reach for this instead of writing a server function whenever the
181
+ * only server-authoritative part of a create is "who made it":
182
+ * `authorId`, `buyerId`, `sellerId`, `createdBy`, `userId`. Guests
183
+ * count — their stable guest id is stamped. Admin contexts may set
184
+ * an explicit value (migrations, tooling).
185
+ *
186
+ * Example: `sellerId: field.string().owner()` lets a listing be
187
+ * created with a plain optimistic `db.insert` while the seller id
188
+ * stays unspoofable.
189
+ */
190
+ owner(): FieldBuilder;
191
+ /**
192
+ * Set a static insert-time default, recorded for the runtime +
193
+ * codegen. Example: `enabled: field.bool().default(true)`.
194
+ */
195
+ default(value: unknown): FieldBuilder;
196
+ /**
197
+ * Default a datetime field to insert-time `now()`. Example:
198
+ * `createdAt: field.datetime().defaultNow()`.
199
+ */
200
+ defaultNow(): FieldBuilder;
201
+ }
202
+ export declare const field: {
203
+ string: () => FieldBuilder;
204
+ int: () => FieldBuilder;
205
+ float: () => FieldBuilder;
206
+ /** Alias for `field.float()`. Lets either name work. */
207
+ number: () => FieldBuilder;
208
+ bool: () => FieldBuilder;
209
+ /** Alias for `field.bool()`. Lets either name work. */
210
+ boolean: () => FieldBuilder;
211
+ datetime: () => FieldBuilder;
212
+ richtext: () => FieldBuilder;
213
+ id: (target: string) => FieldBuilder;
214
+ /**
215
+ * `field.enum(["pending", "paid", "failed"])` — stored as a string with
216
+ * allowed-values metadata so codegen emits a precise literal union
217
+ * (`"pending" | "paid" | "failed"`) instead of a wide `string`.
218
+ */
219
+ enum: (values: readonly string[]) => FieldBuilder;
220
+ };
221
+ export interface IndexDefinition {
222
+ name: string;
223
+ fields: string[];
224
+ unique: boolean;
225
+ /**
226
+ * Optional SQL predicate. When set, the framework emits a *partial*
227
+ * index — `CREATE [UNIQUE] INDEX … WHERE <predicate>` — so the index
228
+ * (and any uniqueness constraint) only applies to rows matching the
229
+ * predicate.
230
+ *
231
+ * Use case: enforce "max 1 hobby-tier org per user" without breaking
232
+ * paid users who legitimately own many orgs:
233
+ *
234
+ * ```ts
235
+ * indexes: [{
236
+ * name: "uniq_hobby_owner",
237
+ * fields: ["createdBy"],
238
+ * unique: true,
239
+ * where: "plan = 'hobby'",
240
+ * }]
241
+ * ```
242
+ *
243
+ * The predicate is passed straight through to the database. Both
244
+ * SQLite and Postgres accept this syntax — write SQL the underlying
245
+ * engine understands. Pylon does NOT validate or escape this string,
246
+ * so DO NOT interpolate user input here.
247
+ */
248
+ where?: string;
249
+ }
250
+ export interface RelationDefinition {
251
+ name: string;
252
+ target: string;
253
+ field: string;
254
+ many?: boolean;
255
+ }
256
+ /**
257
+ * Per-entity search config. Presence of this object on an entity
258
+ * definition tells Pylon to create FTS5 + facet-bitmap shadow tables
259
+ * on the next schema push and maintain them on every write.
260
+ *
261
+ * - `text` – fields that participate in free-text MATCH (BM25).
262
+ * - `facets` – scalar fields (string / int / bool) that get live
263
+ * per-value counts via `db.useSearch`.
264
+ * - `sortable` – fields the client may order results by. Any `sort`
265
+ * on a field not in this list is silently ignored.
266
+ */
267
+ export interface SearchConfig {
268
+ text?: string[];
269
+ facets?: string[];
270
+ sortable?: string[];
271
+ }
272
+ export interface EntityDefinition {
273
+ name: string;
274
+ fields: Record<string, FieldBuilder>;
275
+ indexes?: IndexDefinition[];
276
+ relations?: RelationDefinition[];
277
+ search?: SearchConfig;
278
+ }
279
+ export declare function entity(name: string, fields: Record<string, FieldBuilder>, options?: {
280
+ indexes?: IndexDefinition[];
281
+ relations?: RelationDefinition[];
282
+ search?: SearchConfig;
283
+ }): EntityDefinition;
284
+ export declare function relation(def: RelationDefinition): RelationDefinition;
285
+ export type AuthMode = "public" | "user";
286
+ export interface RouteDefinition {
287
+ path: string;
288
+ mode: RouteMode;
289
+ query?: string;
290
+ auth?: AuthMode;
291
+ /**
292
+ * Project-relative module path (e.g. `app/hello/page`) for SSR
293
+ * routes. Required when `mode === "ssr"`. Discovered automatically
294
+ * by `discoverAppRoutes()`; only specify manually for one-off
295
+ * SSR routes outside the `app/` tree.
296
+ */
297
+ component?: string;
298
+ /**
299
+ * Layout module path chain (root→leaf). Each layout wraps the next
300
+ * as `children`. Only relevant for `mode === "ssr"`.
301
+ */
302
+ layouts?: string[];
303
+ /**
304
+ * Route kind. Omitted (or `"page"`) is a normal navigable page.
305
+ * `"not-found"` / `"error"` are SSR boundary modules discovered from
306
+ * `app/.../not-found.tsx` and `app/.../error.tsx`. Boundary routes are
307
+ * NOT matched as navigable URLs — the host renders `not-found` for
308
+ * unmatched URLs (HTTP 404) and `error` on render failure (HTTP 500).
309
+ * `path` records the segment prefix the boundary covers (`/` for root).
310
+ * `"route"` is a form/method handler (`app/.../route.ts` exporting
311
+ * POST/PUT/PATCH/DELETE) — matched on its `path` for non-GET requests only,
312
+ * never rendered as a page.
313
+ */
314
+ kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots";
315
+ }
316
+ export declare function defineRoute(route: RouteDefinition): RouteDefinition;
317
+ export interface InputFieldDefinition {
318
+ name: string;
319
+ type: FieldType;
320
+ optional?: boolean;
321
+ }
322
+ export interface QueryDefinition {
323
+ name: string;
324
+ input?: InputFieldDefinition[];
325
+ }
326
+ export declare function query(name: string, options?: {
327
+ input?: InputFieldDefinition[];
328
+ }): QueryDefinition;
329
+ export interface ActionDefinition {
330
+ name: string;
331
+ input?: InputFieldDefinition[];
332
+ }
333
+ export declare function action(name: string, options?: {
334
+ input?: InputFieldDefinition[];
335
+ }): ActionDefinition;
336
+ export interface PolicyDefinition {
337
+ /** Optional — `buildManifest` auto-generates a name from the entity
338
+ * + a counter when the fluent `.policies(policy({...}))` chain
339
+ * omits one. Explicit names are still recommended for the
340
+ * procedural API since they appear in policy-denied error
341
+ * messages. */
342
+ name?: string;
343
+ entity?: string;
344
+ action?: string;
345
+ /**
346
+ * Fallback allow expression — evaluated when a more-specific
347
+ * allowRead/allowWrite/allowUpdate/allowDelete isn't set. Kept for
348
+ * backwards compatibility with single-gate policies.
349
+ */
350
+ allow?: string;
351
+ /** Overrides `allow` for reads (pull, list, get). */
352
+ allowRead?: string;
353
+ /** Overrides `allow` for inserts. Falls back to `allowWrite`. */
354
+ allowInsert?: string;
355
+ /** Overrides `allow`/`allowWrite` for updates. */
356
+ allowUpdate?: string;
357
+ /** Overrides `allow`/`allowWrite` for deletes. */
358
+ allowDelete?: string;
359
+ /** Shared fallback for any write when the specific rule is missing. */
360
+ allowWrite?: string;
361
+ }
362
+ export declare function policy(def: PolicyDefinition): PolicyDefinition;
363
+ export interface PluginDefinition {
364
+ name: string;
365
+ entities?: EntityDefinition[];
366
+ hooks?: {
367
+ beforeInsert?: (entity: string, data: Record<string, unknown>) => Record<string, unknown> | null;
368
+ afterInsert?: (entity: string, id: string, data: Record<string, unknown>) => void;
369
+ beforeUpdate?: (entity: string, id: string, data: Record<string, unknown>) => Record<string, unknown> | null;
370
+ afterUpdate?: (entity: string, id: string, data: Record<string, unknown>) => void;
371
+ beforeDelete?: (entity: string, id: string) => boolean;
372
+ afterDelete?: (entity: string, id: string) => void;
373
+ };
374
+ }
375
+ export declare function definePlugin(def: PluginDefinition): PluginDefinition;
376
+ /**
377
+ * Serialized form of `field.X().owner()`. Carried in a field's
378
+ * `default` slot as a *dynamic* default — the value isn't known until
379
+ * a request arrives, so the framework's auth-aware mutation pipeline
380
+ * (OwnerStampPlugin, Rust side) fills it from `auth.userId` on insert
381
+ * and rejects any client attempt to set it to a different user. The
382
+ * storage-layer default-filler (`apply_field_defaults`) deliberately
383
+ * skips this shape — it has no auth context, so it can't (and must
384
+ * not) stamp an owner.
385
+ *
386
+ * `$auth: "userId"` is the only kind today; the object shape leaves
387
+ * room for future auth-derived defaults (e.g. `{$auth:"tenantId"}`)
388
+ * without another manifest migration.
389
+ */
390
+ export interface OwnerDefaultSentinel {
391
+ $auth: "userId";
392
+ }
393
+ export interface ManifestField {
394
+ name: string;
395
+ type: FieldType;
396
+ optional: boolean;
397
+ unique: boolean;
398
+ /** CRDT container override; matches `pylon_kernel::CrdtAnnotation` on
399
+ * the Rust side. Omitted entirely when the field uses the default. */
400
+ crdt?: CrdtAnnotation;
401
+ /** Set when the field is `field.X().serverOnly()` — see
402
+ * [`FieldDefinition.serverOnly`]. Omitted by default so JSON
403
+ * manifests stay compact for unannotated apps. */
404
+ serverOnly?: boolean;
405
+ /** Set when the field is `field.X().readonly()` — see
406
+ * [`FieldDefinition.readonly`]. Omitted by default. */
407
+ readonly?: boolean;
408
+ /** Default value to fill on insert when the row omits this field.
409
+ * - `"now"` → runtime stamps the current UTC time
410
+ * - `{$auth:…}` → a *dynamic* default the auth-aware mutation
411
+ * pipeline fills (see [`OwnerDefaultSentinel`]).
412
+ * Never stamped at the storage layer — the
413
+ * OwnerStampPlugin fills + enforces it.
414
+ * - any literal → runtime stamps that exact value
415
+ * Maps to `field.X().defaultNow()` / `.default(value)` /
416
+ * `field.X().owner()`. */
417
+ default?: "now" | string | number | boolean | null | OwnerDefaultSentinel;
418
+ /** Allowed values for `field.enum([...])` — recorded so codegen
419
+ * can emit a literal-union type and runtime validation can
420
+ * reject out-of-set inserts. Plain `field.string()` doesn't
421
+ * carry this; only `field.enum()`. */
422
+ enumValues?: readonly string[];
423
+ /** Set when the field is `field.X().encrypted()` — AEAD-encrypted
424
+ * at rest. See [`FieldDefinition.encrypted`]. */
425
+ encrypted?: boolean;
426
+ }
427
+ export interface ManifestIndex {
428
+ name: string;
429
+ fields: string[];
430
+ unique: boolean;
431
+ /** Optional partial-index predicate — see `IndexDefinition.where`. */
432
+ where?: string;
433
+ }
434
+ export interface ManifestRelation {
435
+ name: string;
436
+ target: string;
437
+ field: string;
438
+ many?: boolean;
439
+ }
440
+ export interface ManifestEntity {
441
+ name: string;
442
+ fields: ManifestField[];
443
+ indexes: ManifestIndex[];
444
+ relations?: ManifestRelation[];
445
+ /**
446
+ * Mirrors `pylon_kernel::ManifestSearchConfig`. When present, the
447
+ * runtime creates FTS5 + facet-bitmap shadow tables on schema push
448
+ * and maintains them on every write.
449
+ */
450
+ search?: {
451
+ text?: string[];
452
+ facets?: string[];
453
+ sortable?: string[];
454
+ };
455
+ }
456
+ export interface ManifestRoute {
457
+ path: string;
458
+ mode: string;
459
+ query?: string;
460
+ auth?: string;
461
+ component?: string;
462
+ layouts?: string[];
463
+ /** "not-found" / "error" boundaries, or "route" form handlers; omitted for normal pages. */
464
+ kind?: "page" | "not-found" | "error" | "route" | "sitemap" | "robots";
465
+ }
466
+ export interface ManifestInputField {
467
+ name: string;
468
+ type: FieldType;
469
+ optional: boolean;
470
+ unique: false;
471
+ }
472
+ export interface ManifestQuery {
473
+ name: string;
474
+ input?: ManifestInputField[];
475
+ }
476
+ export interface ManifestAction {
477
+ name: string;
478
+ input?: ManifestInputField[];
479
+ }
480
+ export interface ManifestPolicy {
481
+ name: string;
482
+ entity?: string;
483
+ action?: string;
484
+ allow?: string;
485
+ allowRead?: string;
486
+ allowInsert?: string;
487
+ allowUpdate?: string;
488
+ allowDelete?: string;
489
+ allowWrite?: string;
490
+ }
491
+ export declare const MANIFEST_VERSION = 1;
492
+ /** A recurring job: run a function on a cron schedule. Declared in the
493
+ * manifest via `cron(schedule, functionName)`; the runtime fires the named
494
+ * function with anonymous auth every time the schedule matches. Server-side
495
+ * `ctx.db.*` is trusted, so a maintenance handler reads/writes its entities
496
+ * directly without elevating. */
497
+ export interface ManifestCron {
498
+ /** Standard 5-field cron expression, e.g. `"0 * * * *"` (every hour). */
499
+ schedule: string;
500
+ /** Name of the function (query/mutation/action) to run. Should be an
501
+ * `internal: true` function so it isn't also reachable over HTTP. */
502
+ function: string;
503
+ /** Optional human description, surfaced by `pylon status` / tooling. */
504
+ description?: string;
505
+ }
506
+ /** A self-hosted web font. Declared via `font({...})`; at build the runtime
507
+ * fetches the family from Google Fonts, self-hosts the woff2 same-origin,
508
+ * generates `@font-face` + a size-adjusted fallback face (zero layout shift),
509
+ * and auto-injects the preload + faces into every SSR page's `<head>`. The app
510
+ * references the font through the CSS `variable`. */
511
+ export interface ManifestFont {
512
+ /** Google Fonts family name, e.g. "Geist" or "Inter". */
513
+ family: string;
514
+ /** CSS custom property the app uses to apply the font, e.g. "--font-geist".
515
+ * Set `font-family: var(--font-geist)` (it resolves to the family + the
516
+ * size-adjusted fallback + your `fallback` stack). */
517
+ variable: string;
518
+ /** Weights to load — specific (`["400","500","700"]`) or a CSS2 range
519
+ * (`["300..700"]`). Defaults to `["400"]`. */
520
+ weights?: string[];
521
+ /** Styles to load. Defaults to `["normal"]`. */
522
+ styles?: ("normal" | "italic")[];
523
+ /** Unicode subsets, e.g. `["latin"]`. Defaults to `["latin"]`. */
524
+ subsets?: string[];
525
+ /** CSS `font-display`. Defaults to `"swap"`. */
526
+ display?: "auto" | "block" | "swap" | "fallback" | "optional";
527
+ /** Emit `<link rel="preload">` for the font files so the browser fetches
528
+ * them early. Defaults to `true`. */
529
+ preload?: boolean;
530
+ /** Fallback stack appended after the family + the adjusted fallback face,
531
+ * e.g. `["system-ui", "sans-serif"]`. Defaults to `["sans-serif"]`. */
532
+ fallback?: string[];
533
+ /** Generate a size-adjusted fallback `@font-face` (matching x-height +
534
+ * metrics) so there's no layout shift when the web font swaps in. Defaults
535
+ * to `true`. Serialized snake_case to match the kernel manifest wire shape. */
536
+ adjust_font_fallback?: boolean;
537
+ }
538
+ export interface AppManifest {
539
+ manifest_version: number;
540
+ name: string;
541
+ version: string;
542
+ entities: ManifestEntity[];
543
+ routes: ManifestRoute[];
544
+ queries: ManifestQuery[];
545
+ actions: ManifestAction[];
546
+ policies: ManifestPolicy[];
547
+ auth?: ManifestAuthConfig;
548
+ /** App-level LLM provider config. Optional — env wins when set. */
549
+ llm?: ManifestLlmConfig;
550
+ /** Declared OAuth integrations. Auto-creates the `_Connection`
551
+ * entity at runtime boot. */
552
+ connections?: ManifestConnection[];
553
+ /** Recurring jobs — run a function on a cron schedule. */
554
+ crons?: ManifestCron[];
555
+ /** Self-hosted web fonts. Fetched + self-hosted at build; preload +
556
+ * `@font-face` auto-injected into the SSR `<head>`. */
557
+ fonts?: ManifestFont[];
558
+ }
559
+ /**
560
+ * Declare a recurring job. Runs the named function every time the cron
561
+ * `schedule` matches (the runtime checks once a minute).
562
+ *
563
+ * ```ts
564
+ * buildManifest({
565
+ * // ...
566
+ * crons: [
567
+ * cron("0 * * * *", "hourlyRollup"), // every hour, on the hour
568
+ * cron("15 3 * * *", "nightlyCleanup"), // daily at 03:15
569
+ * ],
570
+ * });
571
+ * ```
572
+ *
573
+ * `functionName` is a function in `functions/` — make it `internal: true`
574
+ * so it isn't also exposed over HTTP. It runs with anonymous auth, and a
575
+ * function's own `ctx.db.*` calls are server-side (not policy-gated), so a
576
+ * maintenance handler writes directly. Only call
577
+ * `ctx.auth.elevate({ admin: true, reason: "..." })` if it chains an
578
+ * `internal: true` function via `ctx.scheduler`, or you run with
579
+ * `PYLON_STRICT_FN_POLICIES=1`. The `reason` is mandatory (audited).
580
+ *
581
+ * Multiple replicas: each Pylon process runs its own scheduler. On a SHARED
582
+ * datastore (Postgres — the cloud default), the runtime takes a per-minute
583
+ * lease so each cron fires exactly ONCE per tick across all replicas. On
584
+ * per-replica SQLite there's no shared lease, so each replica fires — keep
585
+ * handlers idempotent there (most maintenance work naturally is). A
586
+ * single-machine app always fires exactly once.
587
+ */
588
+ export declare function cron(schedule: string, functionName: string, opts?: {
589
+ description?: string;
590
+ }): ManifestCron;
591
+ /**
592
+ * Declare a self-hosted web font (Google Fonts). At build, Pylon fetches the
593
+ * family, self-hosts the woff2 same-origin, generates `@font-face` + a
594
+ * size-adjusted fallback face, and auto-injects the preload + faces into every
595
+ * SSR page's `<head>` — no render-blocking third-party request, no layout shift.
596
+ *
597
+ * ```ts
598
+ * buildManifest({
599
+ * // ...
600
+ * fonts: [
601
+ * font({ family: "Geist", variable: "--font-sans", weights: ["300..700"], subsets: ["latin"] }),
602
+ * font({ family: "Geist Mono", variable: "--font-mono", weights: ["400..600"] }),
603
+ * ],
604
+ * });
605
+ * ```
606
+ *
607
+ * Then use it in CSS: `font-family: var(--font-sans);` (resolves to the family,
608
+ * the zero-CLS fallback, then your `fallback` stack).
609
+ */
610
+ export declare function font(opts: {
611
+ family: string;
612
+ variable: string;
613
+ weights?: string[];
614
+ styles?: ("normal" | "italic")[];
615
+ subsets?: string[];
616
+ display?: "auto" | "block" | "swap" | "fallback" | "optional";
617
+ preload?: boolean;
618
+ fallback?: string[];
619
+ adjustFontFallback?: boolean;
620
+ }): ManifestFont;
621
+ export declare function entitiesToManifest(entities: EntityDefinition[]): ManifestEntity[];
622
+ export declare function routesToManifest(routes: RouteDefinition[]): ManifestRoute[];
623
+ /**
624
+ * Walk the project's `app/` directory and discover file-based SSR
625
+ * routes. Returns `RouteDefinition[]` ready to slot into
626
+ * `buildManifest({ routes })`.
627
+ *
628
+ * Mapping (Next App Router-shaped):
629
+ * - `app/page.tsx` → `/`
630
+ * - `app/about/page.tsx` → `/about`
631
+ * - `app/blog/[slug]/page.tsx` → `/blog/:slug`
632
+ * - `app/layout.tsx` wraps every page below
633
+ * - `app/(marketing)/about/page.tsx` → `/about` (group strip)
634
+ *
635
+ * Sorts deterministically — literal segments before parameterized
636
+ * ones at each depth — so the Rust matcher's first-match-wins
637
+ * lookup picks the right route.
638
+ *
639
+ * `not-found.tsx` / `error.tsx` boundaries are emitted as `kind`-tagged
640
+ * routes here; `loading.tsx` is resolved at render time by the SSR runtime
641
+ * (filesystem walk), so it needs no discovery entry.
642
+ */
643
+ export declare function discoverAppRoutes(opts?: {
644
+ appDir?: string;
645
+ }): Promise<RouteDefinition[]>;
646
+ export declare function queriesToManifest(queries: QueryDefinition[]): ManifestQuery[];
647
+ export declare function actionsToManifest(actions: ActionDefinition[]): ManifestAction[];
648
+ export declare function policiesToManifest(policies: PolicyDefinition[]): ManifestPolicy[];
649
+ /**
650
+ * Auth configuration block for the manifest. Mirrors better-auth's
651
+ * `betterAuth({ user, session, trustedOrigins })` shape.
652
+ *
653
+ * All fields optional with sensible defaults — apps that don't pass
654
+ * an `auth({...})` block to `buildManifest` get the framework defaults
655
+ * (User entity named "User", strip `passwordHash`, 30-day sessions,
656
+ * no cookie cache, trusted origins from `PYLON_TRUSTED_ORIGINS` env).
657
+ *
658
+ * `trustedOrigins` is the unified source for **all three gates** —
659
+ * CORS, CSRF, and OAuth-redirect. Loopback origins
660
+ * (`http://localhost`, `127.0.0.1`, `[::1]`, any port) are always
661
+ * auto-trusted across all three gates so `pylon dev` works without
662
+ * any allowlist config.
663
+ *
664
+ * @example
665
+ * auth({
666
+ * user: {
667
+ * entity: "User",
668
+ * expose: ["id", "email", "displayName"],
669
+ * hide: ["passwordHash", "internalNotes"],
670
+ * },
671
+ * session: { expiresIn: 60 * 60 * 24 * 7 }, // 7 days
672
+ * trustedOrigins: ["https://app.example.com"],
673
+ * })
674
+ */
675
+ export type AuthConfig = {
676
+ user?: {
677
+ /** Manifest entity name pylon treats as the User table. Default `"User"`. */
678
+ entity?: string;
679
+ /** Allowlist of fields exposed via `/api/auth/session`. Empty = all (minus hide list). */
680
+ expose?: string[];
681
+ /** Additional fields stripped (combined with default `passwordHash` + `_*`). */
682
+ hide?: string[];
683
+ /**
684
+ * Field on the User row that, when truthy, lifts the session's
685
+ * `auth.is_admin = true`. Examples: `"isAdmin"` (bool column),
686
+ * `"role"` (string equal to "admin"), `"roles"` (array containing
687
+ * "admin"). Default unset — only `PYLON_ADMIN_TOKEN` grants admin.
688
+ *
689
+ * Set this when you want platform admins to sign in with their
690
+ * regular account (Studio gates on `is_admin`, dashboards can
691
+ * branch on it, etc.). The env-token path keeps working as the
692
+ * bootstrap / CI escape hatch.
693
+ */
694
+ adminField?: string;
695
+ };
696
+ session?: {
697
+ /** New session lifetime in seconds. Default 30 days. */
698
+ expiresIn?: number;
699
+ /** Cookie cache config — bake claims into the cookie so reads avoid the DB. */
700
+ cookieCache?: {
701
+ enabled?: boolean;
702
+ /** Max staleness in seconds. Default 5 minutes. */
703
+ maxAge?: number;
704
+ /** Auth-context fields baked into the cookie envelope (always includes `user_id`). */
705
+ claims?: string[];
706
+ };
707
+ };
708
+ /**
709
+ * Org / OrgMember / OrgInvite entity configuration. Apps that use
710
+ * the framework's `/api/auth/orgs/*` surface declare these entities
711
+ * in their schema with the framework's required fields. Add custom
712
+ * fields freely (logo, industry, billingEmail, etc.) — the framework
713
+ * reads / writes only the fields it manages.
714
+ *
715
+ * Defaults to entities named `Org`, `OrgMember`, `OrgInvite`. Rename
716
+ * via the three string fields if your codebase uses different names
717
+ * (e.g. `Organization` / `Membership`). Set `disabled: true` to opt
718
+ * out of the framework's routes entirely — useful when the app has
719
+ * its own org flow in TS and doesn't want the framework's parallel
720
+ * write paths.
721
+ */
722
+ org?: {
723
+ /** Entity name for the org table. Default `"Org"`. */
724
+ entity?: string;
725
+ /** Entity name for membership rows. Default `"OrgMember"`. */
726
+ memberEntity?: string;
727
+ /** Entity name for invite rows. Default `"OrgInvite"`. */
728
+ inviteEntity?: string;
729
+ /**
730
+ * Disable the framework's `/api/auth/orgs/*` routes. Endpoints
731
+ * return `501 ORG_NOT_CONFIGURED`. Use when you implement org
732
+ * management entirely in your own TypeScript functions.
733
+ */
734
+ disabled?: boolean;
735
+ };
736
+ /**
737
+ * Per-app trusted origins. Single declarative source for the three
738
+ * browser-facing gates: CORS, CSRF, OAuth `?callback=` validation.
739
+ * Merged with `PYLON_TRUSTED_ORIGINS` (OAuth) / `PYLON_CORS_ORIGIN`
740
+ * (CORS) / `PYLON_CSRF_ORIGINS` (CSRF) env vars when ops need to
741
+ * split per-gate. Loopback (`http://localhost`, `127.0.0.1`, `[::1]`,
742
+ * any port) is always auto-trusted at every gate.
743
+ */
744
+ trustedOrigins?: string[];
745
+ };
746
+ /**
747
+ * Developer-facing camelCase config consumed by the `llm({...})`
748
+ * factory. All fields optional; environment variables
749
+ * (`PYLON_LLM_PROVIDER`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`,
750
+ * `PYLON_LLM_MODEL`) take precedence so operators can override per
751
+ * deploy without redeploying the bundle.
752
+ */
753
+ export type LlmConfig = {
754
+ /** Provider name. Default: env detection. */
755
+ provider?: "anthropic" | "openai";
756
+ /** Default model when the caller doesn't pass `model`. */
757
+ defaultModel?: string;
758
+ /**
759
+ * Allowlist of models callers may request via the `model` field.
760
+ * Empty = no extra allowance beyond what `PYLON_AI_MODELS_ALLOWED`
761
+ * env provides. Non-admin callers can't request models outside
762
+ * this list.
763
+ */
764
+ allowedModels?: string[];
765
+ };
766
+ export type ManifestLlmConfig = {
767
+ provider?: "anthropic" | "openai";
768
+ default_model?: string;
769
+ allowed_models?: string[];
770
+ };
771
+ /**
772
+ * Developer-facing config for `defineConnection({...})`. Each entry
773
+ * adds a `ctx.connections.<name>` surface to mutation + action ctx
774
+ * (server-side OAuth tokens, never visible to the browser).
775
+ *
776
+ * `provider` selects the OAuth client wire shape from pylon-auth's
777
+ * built-in list (`google`, `github`, `slack`, `microsoft`, etc.).
778
+ * `name` is the app-facing key — different connections can target
779
+ * the same provider with different scopes
780
+ * (e.g. `google-calendar` vs `google-drive`).
781
+ *
782
+ * Configuration: per-provider client id + secret come from env
783
+ * (`PYLON_OAUTH_<PROVIDER>_CLIENT_ID`, `PYLON_OAUTH_<PROVIDER>_CLIENT_SECRET`).
784
+ * Callback URL is derived from `PYLON_PUBLIC_URL` +
785
+ * `/api/connections/<name>/callback`.
786
+ *
787
+ * Storage: the framework auto-creates a `_Connection` entity at
788
+ * boot when any connection is declared; token fields are AEAD-
789
+ * encrypted at rest (`PYLON_ENCRYPTION_KEY` is REQUIRED — boot
790
+ * fails without it when connections are declared).
791
+ */
792
+ export type ConnectionConfig = {
793
+ /** App-facing key. `ctx.connections.get(name)` matches on this. */
794
+ name: string;
795
+ /** Provider identifier matching pylon-auth's OAuth client. */
796
+ provider: string;
797
+ /** Whitespace-separated scopes. Empty = provider default. */
798
+ scopes?: string;
799
+ };
800
+ export type ManifestConnection = {
801
+ name: string;
802
+ provider: string;
803
+ scopes?: string;
804
+ };
805
+ /**
806
+ * Declare a server-side OAuth integration. Returns the manifest
807
+ * entry the runtime parses. Re-exported through `app.ts`:
808
+ *
809
+ * ```ts
810
+ * import { defineConnection } from "@pylonsync/sdk";
811
+ *
812
+ * export const googleConn = defineConnection({
813
+ * name: "google",
814
+ * provider: "google",
815
+ * scopes: "email profile https://www.googleapis.com/auth/calendar.readonly",
816
+ * });
817
+ * ```
818
+ *
819
+ * `buildManifest({ connections: [googleConn] })` carries this into
820
+ * the manifest; the runtime auto-creates the `_Connection` entity
821
+ * and exposes `ctx.connections.get("google")` to actions.
822
+ */
823
+ export declare function defineConnection(cfg: ConnectionConfig): ManifestConnection;
824
+ /**
825
+ * Build the manifest's `llm` block from the user-facing camelCase
826
+ * config. Returns the snake_case shape the Rust runtime parses.
827
+ *
828
+ * ```ts
829
+ * export default {
830
+ * llm: llm({
831
+ * provider: "anthropic",
832
+ * defaultModel: "claude-sonnet-4-5",
833
+ * allowedModels: ["claude-sonnet-4-5", "claude-haiku-4-5"],
834
+ * }),
835
+ * }
836
+ * ```
837
+ */
838
+ export declare function llm(cfg?: LlmConfig): ManifestLlmConfig;
839
+ export type ManifestAuthConfig = {
840
+ user: {
841
+ entity: string;
842
+ expose: string[];
843
+ hide: string[];
844
+ admin_field?: string;
845
+ };
846
+ session: {
847
+ expires_in: number;
848
+ cookie_cache: {
849
+ enabled: boolean;
850
+ max_age: number;
851
+ claims: string[];
852
+ };
853
+ };
854
+ org: {
855
+ entity: string;
856
+ member_entity: string;
857
+ invite_entity: string;
858
+ disabled: boolean;
859
+ };
860
+ trusted_origins: string[];
861
+ };
862
+ /**
863
+ * Build the manifest's `auth` block from the user-facing camelCase
864
+ * config. Translates to the snake_case shape the Rust runtime expects.
865
+ *
866
+ * Defaults match `pylon_kernel::ManifestAuthConfig::default()` so
867
+ * passing nothing is equivalent to omitting the `auth({...})` call.
868
+ */
869
+ export declare function auth(cfg?: AuthConfig): ManifestAuthConfig;
870
+ export declare function buildManifest(options: {
871
+ name: string;
872
+ version: string;
873
+ entities: EntityDefinition[];
874
+ routes: RouteDefinition[];
875
+ queries?: QueryDefinition[];
876
+ actions?: ActionDefinition[];
877
+ policies?: PolicyDefinition[];
878
+ auth?: ManifestAuthConfig;
879
+ llm?: ManifestLlmConfig;
880
+ connections?: ManifestConnection[];
881
+ crons?: ManifestCron[];
882
+ fonts?: ManifestFont[];
883
+ }): AppManifest;
884
+ export { defineStudioConfig, defineStudioExtensions, type BrandConfig, type ThemeConfig, type ThemeAccent, type ThemeAppearance, type IconName, type SidebarConfig, type SidebarSection, type SidebarItem, type SidebarPageItem, type SidebarResourceItem, type SidebarLinkItem, type SidebarHeadingItem, type SidebarFooter, type SidebarFooterCard, type SidebarFooterCustom, type ResourceConfig, type ResourceListConfig, type ColumnConfig, type ColumnRenderer, type RendererKind, type RendererText, type RendererAvatar, type RendererBadge, type RendererDate, type RendererLink, type RendererBoolean, type RendererNumber, type RendererJson, type RendererCustom, type BulkAction, type RowAction, type PageConfig, type StudioConfig, type StudioCellRendererProps, type StudioPageProps, type StudioExtensions, } from "./studio";
885
+ /**
886
+ * Behavior — a function that mutates the entity definition before it's
887
+ * registered. Implementations should be idempotent (the user can list
888
+ * the same behavior twice without breaking the schema).
889
+ */
890
+ export interface Behavior {
891
+ /** Stable identifier — surfaced in the manifest for tooling, lets a
892
+ * pass-through inspector see which behaviors are active. */
893
+ readonly id: string;
894
+ apply(def: EntityDefinition): EntityDefinition;
895
+ }
896
+ /**
897
+ * `timestamps` — auto-add `createdAt` + `updatedAt` datetime fields.
898
+ * The `defaultNow()` marker on each tells the runtime to fill `now()`
899
+ * on insert (and on update for `updatedAt`). Wiring lands with the
900
+ * runtime patch — until then, app code can still set the values
901
+ * manually and the fields exist on the row.
902
+ */
903
+ export declare const timestamps: Behavior;
904
+ /**
905
+ * `softDelete` — auto-add a nullable `deletedAt` datetime field.
906
+ * Rows with `deletedAt != null` are filtered from default reads
907
+ * (TS-side filtering today; runtime filter lands in the follow-up).
908
+ */
909
+ export declare const softDelete: Behavior;
910
+ /**
911
+ * `audit` — marker behavior. Tags the entity for the framework's
912
+ * audit pipeline (writes an `AuditEvent` row per mutation, recording
913
+ * the actor + diff). Runtime hook lands in a follow-up patch — for
914
+ * now the marker is preserved on the manifest so apps can opt in
915
+ * early without breaking later.
916
+ */
917
+ export declare const audit: Behavior;
918
+ /** Variadic index helper — `e.idx("customer", "createdAt")` reads
919
+ * better than the options-object form for the common case. */
920
+ declare function idx(...fields: string[]): IndexDefinition;
921
+ interface EntityBuilder {
922
+ readonly _def: EntityDefinition & {
923
+ behaviors?: string[];
924
+ };
925
+ indexes(...idxs: IndexDefinition[]): EntityBuilder;
926
+ policies(...policies: PolicyDefinition[]): EntityBuilder;
927
+ behaviors(list: readonly Behavior[]): EntityBuilder;
928
+ relations(...rels: RelationDefinition[]): EntityBuilder;
929
+ search(cfg: SearchConfig): EntityBuilder;
930
+ }
931
+ /**
932
+ * The fluent `e` namespace. Equivalent to the procedural `entity()`
933
+ * function — both produce manifest-compatible definitions, both can
934
+ * be mixed in the same app.
935
+ */
936
+ export declare const e: {
937
+ entity(name: string, fields: Record<string, FieldBuilder>): EntityBuilder;
938
+ idx: typeof idx;
939
+ };
940
+ /**
941
+ * Extract attached policies from a fluent entity. The manifest
942
+ * builder calls this when assembling the top-level `policies` list,
943
+ * so apps using the fluent `.policies(...)` chain don't have to
944
+ * register policies separately at the manifest root.
945
+ *
946
+ * Returns an empty array for entities produced by the procedural
947
+ * `entity()` API — those apps register policies the old way.
948
+ */
949
+ export declare function extractAttachedPolicies(e: EntityDefinition | EntityBuilder): PolicyDefinition[];