@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.
- package/dist/index.d.ts +949 -0
- package/dist/studio.d.ts +360 -0
- package/package.json +11 -6
- package/src/index.ts +83 -0
- package/tsconfig.json +0 -7
package/dist/index.d.ts
ADDED
|
@@ -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[];
|