@lunora/server 0.0.0 → 1.0.0-alpha.2
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/LICENSE.md +105 -0
- package/README.md +130 -9
- package/__assets__/package-og.svg +14 -0
- package/dist/data-model.d.mts +328 -0
- package/dist/data-model.d.ts +328 -0
- package/dist/data-model.mjs +1 -0
- package/dist/drizzle.d.mts +1 -0
- package/dist/drizzle.d.ts +1 -0
- package/dist/drizzle.mjs +1 -0
- package/dist/index.d.mts +1741 -0
- package/dist/index.d.ts +1741 -0
- package/dist/index.mjs +24 -0
- package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +187 -0
- package/dist/packem_shared/LunoraError-DhggBJZF.mjs +51 -0
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-BZYd5-uo.mjs +114 -0
- package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +11 -0
- package/dist/packem_shared/bindOrm-DCuyr46L.mjs +71 -0
- package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +100 -0
- package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +29 -0
- package/dist/packem_shared/defineAggregateIndex-DxSso0rH.mjs +236 -0
- package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +8 -0
- package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +20 -0
- package/dist/packem_shared/httpAction-B7FYUEgr.mjs +340 -0
- package/dist/packem_shared/initLunora-CATvPsVt.mjs +86 -0
- package/dist/packem_shared/mask-Jc84C_hK.mjs +211 -0
- package/dist/packem_shared/onConnect-CIPXKPyw.mjs +13 -0
- package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +15 -0
- package/dist/packem_shared/rls-BDKRbMCA.mjs +551 -0
- package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +18 -0
- package/dist/packem_shared/storageRules-4a30FSpI.mjs +88 -0
- package/dist/packem_shared/types.d-BDY0FYHK.d.ts +135 -0
- package/dist/packem_shared/types.d-DmvyEMD6.d.mts +135 -0
- package/dist/rls/testing.d.mts +63 -0
- package/dist/rls/testing.d.ts +63 -0
- package/dist/rls/testing.mjs +49 -0
- package/dist/types.d.mts +1051 -0
- package/dist/types.d.ts +1051 -0
- package/dist/types.mjs +31 -0
- package/package.json +59 -17
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,1741 @@
|
|
|
1
|
+
import { Validator, Infer, v } from '@lunora/values';
|
|
2
|
+
export { type ColumnValidator, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
|
|
3
|
+
import { ArgsValidator, InferArgs, RegisteredAction, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, RelationDefinition, GlobalBackend, OnDeleteAction, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.mjs";
|
|
4
|
+
export { type AnyApi, type AuthState, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LunoraLogger, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerCtx, type TriggerDatabase, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type VectorMatch, type VectorMatches, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, anyApi } from "./types.mjs";
|
|
5
|
+
import { Context, Hono } from 'hono';
|
|
6
|
+
import { b as Permission, R as Role, T as TypedDefinePolicyInput, a as Policy, D as DefinePolicyInput, W as WhereInput, c as RlsOptions } from "./packem_shared/types.d-DmvyEMD6.mjs";
|
|
7
|
+
export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-DmvyEMD6.mjs";
|
|
8
|
+
export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
|
|
9
|
+
import "./data-model.mjs";
|
|
10
|
+
/**
|
|
11
|
+
* Make any `config.storage` result bucket-aware so `ctx.storage.bucket(name)`
|
|
12
|
+
* always resolves. A `createBucketStorage(...)` result already carries
|
|
13
|
+
* `.bucket` / `.bucketName` and is returned as-is; a single `createStorage(...)`
|
|
14
|
+
* (or the no-storage stub) is tagged as the `"default"` bucket, where
|
|
15
|
+
* `.bucket(name)` is the identity — single-bucket apps address one binding under
|
|
16
|
+
* every name.
|
|
17
|
+
*
|
|
18
|
+
* This is the runtime counterpart the generated `_generated/shard.ts` imports to
|
|
19
|
+
* wrap `ctx.storage`; it lives here (the single source) rather than being stamped
|
|
20
|
+
* inline into every generated file, so the bucket-tagging behaviour has one home
|
|
21
|
+
* alongside the storage ctx types. The input is genuinely heterogeneous (a thunk
|
|
22
|
+
* result cast through `unknown`), so the signature is `unknown → unknown`; the
|
|
23
|
+
* generated caller casts the result to its storage type.
|
|
24
|
+
*/
|
|
25
|
+
declare const asBucketStorage: (raw: unknown) => unknown;
|
|
26
|
+
/** Builder discriminator. Codegen reads this kind. */
|
|
27
|
+
type TerminalKind = FunctionKind;
|
|
28
|
+
/** Initial (empty) accumulated args for a fresh builder. */
|
|
29
|
+
type EmptyArgs = Record<never, never>;
|
|
30
|
+
/**
|
|
31
|
+
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
32
|
+
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
33
|
+
* extension, and the result type reflects the widened context.
|
|
34
|
+
*/
|
|
35
|
+
interface MiddlewareNext<ContextIn> {
|
|
36
|
+
(): Promise<ContextIn>;
|
|
37
|
+
<Extension extends Record<string, unknown>>(options: {
|
|
38
|
+
ctx: Extension;
|
|
39
|
+
}): Promise<ContextIn & Extension>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* A middleware receives the current context and a `next` continuation. Its
|
|
43
|
+
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
44
|
+
* propagates the extension into every downstream `.use()` and the handler.
|
|
45
|
+
*/
|
|
46
|
+
type Middleware<ContextIn, ContextOut> = (options: {
|
|
47
|
+
ctx: ContextIn;
|
|
48
|
+
next: MiddlewareNext<ContextIn>;
|
|
49
|
+
}) => ContextOut | Promise<ContextOut>;
|
|
50
|
+
/** Options accepted by `initLunora.dataModel<DM>().create(...)`. Reserved for transformer/error-formatter wiring. */
|
|
51
|
+
type CreateOptions = Record<never, never>;
|
|
52
|
+
/**
|
|
53
|
+
* `Output` carries the type declared by `.output(validator)`. It defaults to
|
|
54
|
+
* the `undefined` sentinel meaning "not declared": in that state the terminal
|
|
55
|
+
* stays generic over the handler's own return type. Once `.output()` sets it to
|
|
56
|
+
* a concrete type, the terminal requires the handler to return that type and
|
|
57
|
+
* the registration is typed to it (the runtime parses the result through the
|
|
58
|
+
* validator). `[Output] extends [undefined]` is wrapped in a tuple so a union
|
|
59
|
+
* `Output` doesn't distribute and so the test is for the exact sentinel.
|
|
60
|
+
*/
|
|
61
|
+
interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
62
|
+
readonly __lunoraProcedure: "query";
|
|
63
|
+
input: <A extends ArgsValidator>(validators: A) => QueryBuilder<Context, A & Args, Output>;
|
|
64
|
+
output: <V extends Validator>(validator: V) => QueryBuilder<Context, Args, Infer<V>>;
|
|
65
|
+
query: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
66
|
+
args: InferArgs<Args>;
|
|
67
|
+
ctx: Context;
|
|
68
|
+
}) => Promise<R> | R) => RegisteredQuery<Args, Awaited<R>> : (handler: (options: {
|
|
69
|
+
args: InferArgs<Args>;
|
|
70
|
+
ctx: Context;
|
|
71
|
+
}) => Output | Promise<Output>) => RegisteredQuery<Args, Output>;
|
|
72
|
+
/**
|
|
73
|
+
* Terminal: declare this procedure as a streaming query. The handler is an
|
|
74
|
+
* async generator (or any function returning an `AsyncIterable<R>`) that
|
|
75
|
+
* yields one chunk per server-pushed frame. The third `signal` argument is
|
|
76
|
+
* tripped when the client cancels — break out of the loop or check
|
|
77
|
+
* `signal.aborted` between yields. `.output()` does not apply: per-chunk
|
|
78
|
+
* validation is opt-in via the handler itself.
|
|
79
|
+
*/
|
|
80
|
+
stream: <R>(handler: (options: {
|
|
81
|
+
args: InferArgs<Args>;
|
|
82
|
+
ctx: Context;
|
|
83
|
+
signal: AbortSignal;
|
|
84
|
+
}) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => RegisteredStream<Args, R>;
|
|
85
|
+
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => QueryBuilder<ContextOut, Args, Output>;
|
|
86
|
+
}
|
|
87
|
+
interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
88
|
+
readonly __lunoraProcedure: "mutation";
|
|
89
|
+
input: <A extends ArgsValidator>(validators: A) => MutationBuilder<Context, A & Args, Output>;
|
|
90
|
+
mutation: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
91
|
+
args: InferArgs<Args>;
|
|
92
|
+
ctx: Context;
|
|
93
|
+
}) => Promise<R> | R) => RegisteredMutation<Args, Awaited<R>> : (handler: (options: {
|
|
94
|
+
args: InferArgs<Args>;
|
|
95
|
+
ctx: Context;
|
|
96
|
+
}) => Output | Promise<Output>) => RegisteredMutation<Args, Output>;
|
|
97
|
+
output: <V extends Validator>(validator: V) => MutationBuilder<Context, Args, Infer<V>>;
|
|
98
|
+
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => MutationBuilder<ContextOut, Args, Output>;
|
|
99
|
+
}
|
|
100
|
+
interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
101
|
+
readonly __lunoraProcedure: "action";
|
|
102
|
+
action: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
103
|
+
args: InferArgs<Args>;
|
|
104
|
+
ctx: Context;
|
|
105
|
+
}) => Promise<R> | R) => RegisteredAction<Args, Awaited<R>> : (handler: (options: {
|
|
106
|
+
args: InferArgs<Args>;
|
|
107
|
+
ctx: Context;
|
|
108
|
+
}) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
|
|
109
|
+
input: <A extends ArgsValidator>(validators: A) => ActionBuilder<Context, A & Args, Output>;
|
|
110
|
+
output: <V extends Validator>(validator: V) => ActionBuilder<Context, Args, Infer<V>>;
|
|
111
|
+
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => ActionBuilder<ContextOut, Args, Output>;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Internal builder variants. Identical to their public counterparts but carry
|
|
115
|
+
* the `__lunoraVisibility: "internal"` brand codegen keys off to route the
|
|
116
|
+
* registration into the `internal` object (and keep it off `api`). `input`/`use`
|
|
117
|
+
* return the internal builder type so the brand survives the whole chain.
|
|
118
|
+
*/
|
|
119
|
+
interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
120
|
+
readonly __lunoraProcedure: "query";
|
|
121
|
+
readonly __lunoraVisibility: "internal";
|
|
122
|
+
input: <A extends ArgsValidator>(validators: A) => InternalQueryBuilder<Context, A & Args, Output>;
|
|
123
|
+
output: <V extends Validator>(validator: V) => InternalQueryBuilder<Context, Args, Infer<V>>;
|
|
124
|
+
query: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
125
|
+
args: InferArgs<Args>;
|
|
126
|
+
ctx: Context;
|
|
127
|
+
}) => Promise<R> | R) => RegisteredQuery<Args, Awaited<R>> : (handler: (options: {
|
|
128
|
+
args: InferArgs<Args>;
|
|
129
|
+
ctx: Context;
|
|
130
|
+
}) => Output | Promise<Output>) => RegisteredQuery<Args, Output>;
|
|
131
|
+
/** See {@link QueryBuilder.stream}; the internal variant routes the registration into `internal` instead of `api`. */
|
|
132
|
+
stream: <R>(handler: (options: {
|
|
133
|
+
args: InferArgs<Args>;
|
|
134
|
+
ctx: Context;
|
|
135
|
+
signal: AbortSignal;
|
|
136
|
+
}) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => RegisteredStream<Args, R>;
|
|
137
|
+
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalQueryBuilder<ContextOut, Args, Output>;
|
|
138
|
+
}
|
|
139
|
+
interface InternalMutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
140
|
+
readonly __lunoraProcedure: "mutation";
|
|
141
|
+
readonly __lunoraVisibility: "internal";
|
|
142
|
+
input: <A extends ArgsValidator>(validators: A) => InternalMutationBuilder<Context, A & Args, Output>;
|
|
143
|
+
mutation: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
144
|
+
args: InferArgs<Args>;
|
|
145
|
+
ctx: Context;
|
|
146
|
+
}) => Promise<R> | R) => RegisteredMutation<Args, Awaited<R>> : (handler: (options: {
|
|
147
|
+
args: InferArgs<Args>;
|
|
148
|
+
ctx: Context;
|
|
149
|
+
}) => Output | Promise<Output>) => RegisteredMutation<Args, Output>;
|
|
150
|
+
output: <V extends Validator>(validator: V) => InternalMutationBuilder<Context, Args, Infer<V>>;
|
|
151
|
+
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalMutationBuilder<ContextOut, Args, Output>;
|
|
152
|
+
}
|
|
153
|
+
interface InternalActionBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
154
|
+
readonly __lunoraProcedure: "action";
|
|
155
|
+
readonly __lunoraVisibility: "internal";
|
|
156
|
+
action: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
157
|
+
args: InferArgs<Args>;
|
|
158
|
+
ctx: Context;
|
|
159
|
+
}) => Promise<R> | R) => RegisteredAction<Args, Awaited<R>> : (handler: (options: {
|
|
160
|
+
args: InferArgs<Args>;
|
|
161
|
+
ctx: Context;
|
|
162
|
+
}) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
|
|
163
|
+
input: <A extends ArgsValidator>(validators: A) => InternalActionBuilder<Context, A & Args, Output>;
|
|
164
|
+
output: <V extends Validator>(validator: V) => InternalActionBuilder<Context, Args, Infer<V>>;
|
|
165
|
+
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalActionBuilder<ContextOut, Args, Output>;
|
|
166
|
+
}
|
|
167
|
+
/** The public root builders plus their `internal*` counterparts, returned by `.create()`. */
|
|
168
|
+
interface LunoraBuilders {
|
|
169
|
+
action: ActionBuilder<ActionCtx, EmptyArgs>;
|
|
170
|
+
internalAction: InternalActionBuilder<ActionCtx, EmptyArgs>;
|
|
171
|
+
internalMutation: InternalMutationBuilder<MutationCtx, EmptyArgs>;
|
|
172
|
+
internalQuery: InternalQueryBuilder<QueryCtx, EmptyArgs>;
|
|
173
|
+
mutation: MutationBuilder<MutationCtx, EmptyArgs>;
|
|
174
|
+
query: QueryBuilder<QueryCtx, EmptyArgs>;
|
|
175
|
+
}
|
|
176
|
+
interface DataModelInit<DataModel> {
|
|
177
|
+
/** Phantom carrier for the generated `DataModel`; reserved for typed `ctx.db` (Plan2 1.2.7). */
|
|
178
|
+
readonly __dataModel?: DataModel;
|
|
179
|
+
create: (options?: CreateOptions) => LunoraBuilders;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Entry point for the procedure builder. `dataModel<DM>()` binds the generated
|
|
183
|
+
* `DataModel` (phantom for now), and `.create()` yields the public root builders
|
|
184
|
+
* plus their `internal*` counterparts.
|
|
185
|
+
*/
|
|
186
|
+
declare const initLunora: {
|
|
187
|
+
dataModel: <DataModel>() => DataModelInit<DataModel>;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Redact secrets from a free-form message. Masks, in order: any quoted value
|
|
191
|
+
* whose contents look like a credential (so a value surfaced as `received string
|
|
192
|
+
* "sk_live_…"` is masked even though the surrounding text is not a token); a
|
|
193
|
+
* `scheme://user:password@host` URL credential (the password segment); any
|
|
194
|
+
* known-prefix credential token wherever it appears, at any length; any value
|
|
195
|
+
* following a secret-named key in `KEY=value` / `KEY: value` form; and any
|
|
196
|
+
* remaining bare high-entropy ≥24-char token run anywhere in the message.
|
|
197
|
+
*
|
|
198
|
+
* This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
|
|
199
|
+
* secret under a non-secret-named key (and embedded credentials in shapes not
|
|
200
|
+
* enumerated here) can still slip through. Treat it as a backstop — prefer
|
|
201
|
+
* structured logging that never serializes raw env/secret fields in the first
|
|
202
|
+
* place over relying on post-hoc scrubbing of untrusted data.
|
|
203
|
+
*
|
|
204
|
+
* Exported because it is independently useful — call it before logging anything
|
|
205
|
+
* derived from `env`, request bodies, or thrown errors.
|
|
206
|
+
*/
|
|
207
|
+
declare const redactSecrets: (message: string) => string;
|
|
208
|
+
/** One key's validation failure, secrets already redacted out of `message`. */
|
|
209
|
+
interface EnvKeyFailure {
|
|
210
|
+
/** The env key that failed. */
|
|
211
|
+
key: string;
|
|
212
|
+
/** Redacted human-readable reason. */
|
|
213
|
+
message: string;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Thrown when one or more env keys are missing or fail validation. Carries the
|
|
217
|
+
* structured list of `failures` (each with the offending `key`) so callers can
|
|
218
|
+
* react programmatically; `message` is the joined, secret-redacted summary.
|
|
219
|
+
*
|
|
220
|
+
* Named export only (no default) per the repo export convention.
|
|
221
|
+
*/
|
|
222
|
+
declare class LunoraEnvError extends Error {
|
|
223
|
+
override readonly name = "LunoraEnvError";
|
|
224
|
+
readonly failures: ReadonlyArray<EnvKeyFailure>;
|
|
225
|
+
constructor(failures: ReadonlyArray<EnvKeyFailure>);
|
|
226
|
+
}
|
|
227
|
+
/** A record of `v.*` validators describing the expected env shape. */
|
|
228
|
+
type EnvShape = Record<string, Validator>;
|
|
229
|
+
/**
|
|
230
|
+
* The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
|
|
231
|
+
* become optional keys; everything else is required. Mirrors how `InferArgs`
|
|
232
|
+
* derives an args object from a validator map.
|
|
233
|
+
*/
|
|
234
|
+
type InferEnv<S extends EnvShape> = { [K in keyof S as undefined extends Infer<S[K]> ? K : never]?: Infer<S[K]> } & { [K in keyof S as undefined extends Infer<S[K]> ? never : K]: Infer<S[K]> };
|
|
235
|
+
/**
|
|
236
|
+
* The accessor returned by {@link defineEnv}. A typed view over an `env` object
|
|
237
|
+
* plus a `.parse(env)` escape hatch that validates every key eagerly.
|
|
238
|
+
*
|
|
239
|
+
* Call the accessor with the worker's `env` to get the typed, lazily-validated
|
|
240
|
+
* proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
|
|
241
|
+
*/
|
|
242
|
+
interface EnvAccessor<S extends EnvShape> {
|
|
243
|
+
/** Validate every key eagerly and return the typed, plain (non-proxy) object. Use for fail-fast-at-boot. */
|
|
244
|
+
parse: (env: unknown) => InferEnv<S>;
|
|
245
|
+
/** Lazily-validated, per-key-cached typed view over `env`. Keys are validated on first access. */
|
|
246
|
+
(env: unknown): InferEnv<S>;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Define a typed, validated accessor over a Worker's `env`. Pass a record of
|
|
250
|
+
* `v.*` validators; receive an accessor that validates lazily per key (cached
|
|
251
|
+
* per `env` identity) and infers its output type from the validators.
|
|
252
|
+
*
|
|
253
|
+
* ```ts
|
|
254
|
+
* import { defineEnv, v } from "@lunora/server";
|
|
255
|
+
*
|
|
256
|
+
* const config = defineEnv({
|
|
257
|
+
* STRIPE_KEY: v.string(),
|
|
258
|
+
* PORT: v.optional(v.number()),
|
|
259
|
+
* });
|
|
260
|
+
*
|
|
261
|
+
* export default {
|
|
262
|
+
* fetch(request, env) {
|
|
263
|
+
* const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
|
|
264
|
+
* // …
|
|
265
|
+
* },
|
|
266
|
+
* };
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
|
|
270
|
+
* invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
|
|
271
|
+
*/
|
|
272
|
+
declare const defineEnv: <S extends EnvShape>(shape: S) => EnvAccessor<S>;
|
|
273
|
+
/**
|
|
274
|
+
* Canonical error type for Lunora procedures and middleware.
|
|
275
|
+
*
|
|
276
|
+
* The runtime's structural error mapper keys off `name === "LunoraError"` plus
|
|
277
|
+
* the numeric `status`, so throwing one of these from a handler or middleware
|
|
278
|
+
* yields the right RPC/HTTP status without any further wiring. `code` carries
|
|
279
|
+
* the machine-readable reason for clients.
|
|
280
|
+
*/
|
|
281
|
+
declare const CODE_STATUS: {
|
|
282
|
+
readonly BAD_REQUEST: 400;
|
|
283
|
+
readonly CONFLICT: 409;
|
|
284
|
+
/**
|
|
285
|
+
* `count()` invoked against a table whose context carries an active RLS
|
|
286
|
+
* policy. The operation itself is unsupported in an RLS-restricted reader
|
|
287
|
+
* (kitcn's documented constraint) — the request is well-formed and the
|
|
288
|
+
* caller is authorized, so this is a 422 (semantic conflict) rather than a
|
|
289
|
+
* 403 (policy denial).
|
|
290
|
+
*/
|
|
291
|
+
readonly COUNT_RLS_UNSUPPORTED: 422;
|
|
292
|
+
readonly FORBIDDEN: 403;
|
|
293
|
+
readonly INTERNAL_SERVER_ERROR: 500;
|
|
294
|
+
/**
|
|
295
|
+
* An analytical reduction (`aggregate` / `groupBy`) was invoked over a
|
|
296
|
+
* column that the procedure's `mask()` middleware redacts. A masked column
|
|
297
|
+
* can't be summed, averaged, or grouped without leaking the very values the
|
|
298
|
+
* mask hides (a group key *is* the raw value; an aggregate is computed from
|
|
299
|
+
* it), so the operation fails closed. The request is well-formed and the
|
|
300
|
+
* caller is authorized — this is a 422 (semantic conflict), mirroring
|
|
301
|
+
* `COUNT_RLS_UNSUPPORTED`.
|
|
302
|
+
*/
|
|
303
|
+
readonly MASK_UNSUPPORTED: 422;
|
|
304
|
+
readonly NOT_FOUND: 404;
|
|
305
|
+
readonly NOT_IMPLEMENTED: 501;
|
|
306
|
+
/**
|
|
307
|
+
* A write policy's `when` returned a relation-crossing predicate
|
|
308
|
+
* (`some`/`none`/`every`/`is`/`isNot`). The in-memory write-policy evaluator
|
|
309
|
+
* has no child fetcher and cannot resolve a relation node, so the policy is
|
|
310
|
+
* unsupported as written. Relation predicates are valid in *read* policies
|
|
311
|
+
* and query `where` clauses (the pre-resolver handles them there). The
|
|
312
|
+
* request is well-formed; this is a 422 (semantic conflict), mirroring the
|
|
313
|
+
* sibling `*_UNSUPPORTED` codes.
|
|
314
|
+
*/
|
|
315
|
+
readonly RELATION_PREDICATE_UNSUPPORTED: 422;
|
|
316
|
+
readonly TOO_MANY_REQUESTS: 429;
|
|
317
|
+
readonly UNAUTHORIZED: 401;
|
|
318
|
+
readonly UNPROCESSABLE: 422;
|
|
319
|
+
};
|
|
320
|
+
type LunoraErrorCode = keyof typeof CODE_STATUS;
|
|
321
|
+
declare class LunoraError extends Error {
|
|
322
|
+
override readonly name = "LunoraError";
|
|
323
|
+
readonly code: LunoraErrorCode;
|
|
324
|
+
readonly status: number;
|
|
325
|
+
constructor(code: LunoraErrorCode, message?: string);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* The per-table `ctx.db` accessor (the `ctx.db.messages.findMany(...)` form) and
|
|
329
|
+
* the kitcn-style `ctx.orm` namespace, as plain runtime helpers. This is the ONE
|
|
330
|
+
* source of truth for the facade shape, shared by two callers so they can never
|
|
331
|
+
* drift (a drift here is security-relevant — a facade accessor the RLS
|
|
332
|
+
* middleware forgot to re-bind would read around policy). `@lunora/codegen`
|
|
333
|
+
* emits `ctx.db`/`ctx.orm` by calling these over the raw shard writer (and the
|
|
334
|
+
* D1 `globalDb` writer for `.global()` tables); the RLS middleware re-binds the
|
|
335
|
+
* policy tables by calling them over the policy-enforcing wrapped writer.
|
|
336
|
+
*
|
|
337
|
+
* `bindTableFacade(writer, table)` pins `tableName` on the structural writer so
|
|
338
|
+
* callers address rows by id (`get`/`delete`/`patch`/`replace`) or by the bound
|
|
339
|
+
* table (everything else). The binding is identical regardless of which writer
|
|
340
|
+
* is passed — that's the whole point.
|
|
341
|
+
*/
|
|
342
|
+
/**
|
|
343
|
+
* Minimal structural writer the facade binds over. Declared with **method**
|
|
344
|
+
* syntax (not arrow properties) so a more-specifically-typed writer — both
|
|
345
|
+
* `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
|
|
346
|
+
* stays assignable under bivariant parameter checking. That is the whole reason
|
|
347
|
+
* the shared helper can serve both callers, hence the rule exemption.
|
|
348
|
+
*/
|
|
349
|
+
interface FacadeWriterLike {
|
|
350
|
+
aggregate(tableName: string, options: unknown): Promise<unknown>;
|
|
351
|
+
count(tableName: string, where?: unknown): Promise<number>;
|
|
352
|
+
delete(id: string, expectedTable?: string): Promise<void>;
|
|
353
|
+
deleteMany?(ids: ReadonlyArray<string>, options?: {
|
|
354
|
+
limit?: number;
|
|
355
|
+
}, expectedTable?: string): Promise<{
|
|
356
|
+
deleted: number;
|
|
357
|
+
}>;
|
|
358
|
+
findFirst(tableName: string, args?: unknown): Promise<unknown>;
|
|
359
|
+
findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
|
|
360
|
+
findMany(tableName: string, args?: unknown): Promise<unknown>;
|
|
361
|
+
get(id: string, expectedTable?: string): Promise<unknown>;
|
|
362
|
+
groupBy(tableName: string, options: unknown): Promise<unknown>;
|
|
363
|
+
insert(tableName: string, document: Record<string, unknown>): Promise<string>;
|
|
364
|
+
insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
365
|
+
limit?: number;
|
|
366
|
+
}): Promise<string[]>;
|
|
367
|
+
patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
|
|
368
|
+
patchMany?(patches: ReadonlyArray<{
|
|
369
|
+
id: string;
|
|
370
|
+
patch: Record<string, unknown>;
|
|
371
|
+
}>, options?: {
|
|
372
|
+
limit?: number;
|
|
373
|
+
}, expectedTable?: string): Promise<void>;
|
|
374
|
+
query(tableName: string): {
|
|
375
|
+
withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
|
|
376
|
+
};
|
|
377
|
+
rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
|
|
378
|
+
rankPage(tableName: string, indexName: string, options?: unknown): Promise<unknown>;
|
|
379
|
+
replace(id: string, document: Record<string, unknown>, expectedTable?: string): Promise<void>;
|
|
380
|
+
}
|
|
381
|
+
/** The per-table accessor object returned for the `ctx.db` table form. */
|
|
382
|
+
interface FacadeEntry {
|
|
383
|
+
aggregate: (options: unknown) => Promise<unknown>;
|
|
384
|
+
count: (where?: unknown) => Promise<number>;
|
|
385
|
+
delete: (id: string) => Promise<void>;
|
|
386
|
+
deleteMany: (ids: ReadonlyArray<string>, options?: {
|
|
387
|
+
limit?: number;
|
|
388
|
+
}) => Promise<{
|
|
389
|
+
deleted: number;
|
|
390
|
+
}>;
|
|
391
|
+
findFirst: (args?: unknown) => Promise<unknown>;
|
|
392
|
+
findFirstOrThrow: (args?: unknown) => Promise<unknown>;
|
|
393
|
+
findMany: (args?: unknown) => Promise<unknown>;
|
|
394
|
+
get: (id: string) => Promise<unknown>;
|
|
395
|
+
groupBy: (options: unknown) => Promise<unknown>;
|
|
396
|
+
insert: (document: Record<string, unknown>) => Promise<string>;
|
|
397
|
+
insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
398
|
+
limit?: number;
|
|
399
|
+
}) => Promise<string[]>;
|
|
400
|
+
patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
|
|
401
|
+
patchMany: (patches: ReadonlyArray<{
|
|
402
|
+
id: string;
|
|
403
|
+
values: Record<string, unknown>;
|
|
404
|
+
}>, options?: {
|
|
405
|
+
limit?: number;
|
|
406
|
+
}) => Promise<void>;
|
|
407
|
+
rank: (indexName: string, options: unknown) => Promise<unknown>;
|
|
408
|
+
rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
|
|
409
|
+
replace: (id: string, document: Record<string, unknown>) => Promise<void>;
|
|
410
|
+
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Bind a structural writer to one table, producing its `ctx.db` table accessor.
|
|
414
|
+
*
|
|
415
|
+
* The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
|
|
416
|
+
* `tableName` as `expectedTable` so the underlying writer scopes its id lookup
|
|
417
|
+
* to this table. Without it, a branded `Id<"posts">` carrying another table's
|
|
418
|
+
* id would resolve cross-table (the writer probes every table by id), letting
|
|
419
|
+
* `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
|
|
420
|
+
* mutate — a row in an unrelated table (IDOR). Writers that ignore the second
|
|
421
|
+
* argument keep their previous global behaviour; the scoping is opt-in via this
|
|
422
|
+
* forwarded name.
|
|
423
|
+
*/
|
|
424
|
+
declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) => FacadeEntry;
|
|
425
|
+
/** The kitcn-style `ctx.orm` namespace over a per-table facade map. */
|
|
426
|
+
interface OrmLike {
|
|
427
|
+
delete: (table: string, id: string) => Promise<void>;
|
|
428
|
+
insert: (table: string) => {
|
|
429
|
+
values: (document: Record<string, unknown>) => Promise<string>;
|
|
430
|
+
};
|
|
431
|
+
query: Record<string, FacadeEntry>;
|
|
432
|
+
replace: (table: string, id: string) => {
|
|
433
|
+
with: (document: Record<string, unknown>) => Promise<void>;
|
|
434
|
+
};
|
|
435
|
+
update: (table: string, id: string) => {
|
|
436
|
+
set: (values: Record<string, unknown>) => Promise<void>;
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
/** Build `ctx.orm` over a per-table facade map (table name → FacadeEntry). */
|
|
440
|
+
declare const bindOrm: (facade: Record<string, FacadeEntry>) => OrmLike;
|
|
441
|
+
/** HTTP verbs the typed {@link httpRoute} builder can bind to. */
|
|
442
|
+
type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
|
|
443
|
+
/**
|
|
444
|
+
* Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
|
|
445
|
+
* HTTP actions run in the worker (the "action runtime"), separate from the
|
|
446
|
+
* transactional store, so there is no direct `db` / `vectors` / `scheduler` /
|
|
447
|
+
* `storage` surface — reach the data layer through `runQuery` / `runMutation` /
|
|
448
|
+
* `runAction`, which forward to the owning shard.
|
|
449
|
+
*/
|
|
450
|
+
type HttpActionCtx = Pick<ActionCtx, "auth" | "fetch" | "runAction" | "runMutation" | "runQuery">;
|
|
451
|
+
/** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
|
|
452
|
+
type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
|
|
453
|
+
/**
|
|
454
|
+
* The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
|
|
455
|
+
* The runtime injects the per-request {@link HttpActionCtx} on the private
|
|
456
|
+
* `__lunoraCtx` binding; the router's lifting middleware promotes it to
|
|
457
|
+
* `c.var.lunora` so handlers can read it as a typed variable.
|
|
458
|
+
*/
|
|
459
|
+
interface LunoraHttpEnv {
|
|
460
|
+
Bindings: Record<string, unknown> & {
|
|
461
|
+
__lunoraCtx?: HttpActionCtx;
|
|
462
|
+
};
|
|
463
|
+
Variables: {
|
|
464
|
+
lunora: HttpActionCtx;
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/** The hono app type {@link httpRouter} returns. */
|
|
468
|
+
type LunoraHttpApp = Hono<LunoraHttpEnv>;
|
|
469
|
+
/** A compiled route handler: a hono handler that resolves to a raw {@link Response}. */
|
|
470
|
+
type LunoraRouteHandler = (c: Context<LunoraHttpEnv>) => Promise<Response>;
|
|
471
|
+
/**
|
|
472
|
+
* Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
|
|
473
|
+
* hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
|
|
474
|
+
* runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
|
|
475
|
+
* {@link httpRouter}; `request` is the underlying `c.req.raw`.
|
|
476
|
+
*/
|
|
477
|
+
declare const httpAction: (handler: HttpActionHandler) => LunoraRouteHandler;
|
|
478
|
+
/**
|
|
479
|
+
* Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
|
|
480
|
+
* the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
|
|
481
|
+
* {@link httpAction} and the typed {@link httpRoute} builder can read the action
|
|
482
|
+
* context. The full hono surface is available — plugins, path params, `.route`:
|
|
483
|
+
*
|
|
484
|
+
* ```ts
|
|
485
|
+
* const app = httpRouter();
|
|
486
|
+
* app.use("*", cors());
|
|
487
|
+
* app.post("/webhook", httpAction(onWebhook));
|
|
488
|
+
* app.get("/users/:id", getUser);
|
|
489
|
+
* export default createWorker({ httpRouter: app, ... });
|
|
490
|
+
* ```
|
|
491
|
+
*
|
|
492
|
+
* The lifting middleware throws if the context is absent. `createWorker` injects
|
|
493
|
+
* it on every request the router sees, so this only trips when the app is run
|
|
494
|
+
* outside the runtime — a misconfiguration we surface loudly rather than let
|
|
495
|
+
* `c.var.lunora` be silently `undefined` despite its non-optional type.
|
|
496
|
+
*/
|
|
497
|
+
declare const httpRouter: () => LunoraHttpApp;
|
|
498
|
+
/** The `{ ctx, searchParams, body, params }` a typed route handler receives. */
|
|
499
|
+
interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator> {
|
|
500
|
+
body: InferArgs<Body>;
|
|
501
|
+
ctx: HttpActionCtx;
|
|
502
|
+
params: InferArgs<Params>;
|
|
503
|
+
searchParams: InferArgs<SearchParams>;
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
|
|
507
|
+
* handler receives. There is no parsed `body` — streams are typically GET, and
|
|
508
|
+
* the raw `request` is exposed if a handler needs to read the body itself.
|
|
509
|
+
* `signal` is tripped when the client disconnects.
|
|
510
|
+
*/
|
|
511
|
+
interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
|
|
512
|
+
ctx: HttpActionCtx;
|
|
513
|
+
params: InferArgs<Params>;
|
|
514
|
+
request: Request;
|
|
515
|
+
searchParams: InferArgs<SearchParams>;
|
|
516
|
+
signal: AbortSignal;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* A typed REST route under construction. `.searchParams()` / `.body()` /
|
|
520
|
+
* `.params()` accumulate validator maps (later calls merge, a colliding key
|
|
521
|
+
* wins) that decode the URL query, JSON body, and hono path params into the
|
|
522
|
+
* handler's typed `searchParams` / `body` / `params`. Like the procedure
|
|
523
|
+
* builder, `.output(validator)` defaults to the `undefined` sentinel — while
|
|
524
|
+
* unset the handler is generic over its own return; once set the handler must
|
|
525
|
+
* return that type and the result is parsed through the validator before
|
|
526
|
+
* serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
|
|
527
|
+
* `Output` doesn't distribute and the test is for the exact sentinel.
|
|
528
|
+
*
|
|
529
|
+
* The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
|
|
530
|
+
* directly with `app.get(path, route)`.
|
|
531
|
+
*/
|
|
532
|
+
interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
|
|
533
|
+
body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
|
|
534
|
+
handler: [Output] extends [undefined] ? <R>(handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Promise<R> | R) => LunoraRouteHandler : (handler: (options: HttpRouteHandlerOptions<SearchParams, Body, Params>) => Output | Promise<Output>) => LunoraRouteHandler;
|
|
535
|
+
output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
|
|
536
|
+
params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
|
|
537
|
+
searchParams: <S extends ArgsValidator>(validators: S) => HttpRouteBuilder<S & SearchParams, Body, Params, Output>;
|
|
538
|
+
/**
|
|
539
|
+
* Terminal: declare this route as a streaming Server-Sent Events endpoint.
|
|
540
|
+
* The handler is an async generator (or any function returning an
|
|
541
|
+
* `AsyncIterable<R>`) that yields one chunk per SSE `data:` frame; on
|
|
542
|
+
* iterator completion the route writes a final `event: complete` frame; on
|
|
543
|
+
* throw, an `event: error` frame is written with `{code, message}` before
|
|
544
|
+
* the stream closes. The chunks are JSON-encoded; `R` is inferred from the
|
|
545
|
+
* handler's yielded type.
|
|
546
|
+
*/
|
|
547
|
+
stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
|
|
548
|
+
}
|
|
549
|
+
/** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
|
|
550
|
+
type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
|
|
551
|
+
/** The verb-keyed entry point: `httpRoute.get("/api/todos")…`. */
|
|
552
|
+
interface HttpRoute {
|
|
553
|
+
delete: HttpRouteFactory;
|
|
554
|
+
get: HttpRouteFactory;
|
|
555
|
+
head: HttpRouteFactory;
|
|
556
|
+
options: HttpRouteFactory;
|
|
557
|
+
patch: HttpRouteFactory;
|
|
558
|
+
post: HttpRouteFactory;
|
|
559
|
+
put: HttpRouteFactory;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
|
|
563
|
+
* typed route and a hand-written {@link httpAction} are interchangeable when
|
|
564
|
+
* mounted on {@link httpRouter}:
|
|
565
|
+
*
|
|
566
|
+
* ```ts
|
|
567
|
+
* export const listTodos = httpRoute
|
|
568
|
+
* .get("/api/todos")
|
|
569
|
+
* .searchParams({ limit: v.number(), q: v.optional(v.string()) })
|
|
570
|
+
* .output(v.array(v.object({ id: v.string(), text: v.string() })))
|
|
571
|
+
* .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
|
|
572
|
+
*
|
|
573
|
+
* export const getTodo = httpRoute
|
|
574
|
+
* .get("/api/todos/:id")
|
|
575
|
+
* .params({ id: v.string() })
|
|
576
|
+
* .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
|
|
577
|
+
*
|
|
578
|
+
* const app = httpRouter();
|
|
579
|
+
* app.get("/api/todos", listTodos);
|
|
580
|
+
* app.get("/api/todos/:id", getTodo);
|
|
581
|
+
* ```
|
|
582
|
+
*/
|
|
583
|
+
declare const httpRoute: HttpRoute;
|
|
584
|
+
/**
|
|
585
|
+
* Structural view of an R2 object body, as returned by `@lunora/storage`'s
|
|
586
|
+
* `download()`. Re-declared here (not imported) so `@lunora/server` takes no
|
|
587
|
+
* runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
|
|
588
|
+
*/
|
|
589
|
+
interface StorageObjectBody {
|
|
590
|
+
/** The object body stream (`null` for a zero-byte object). */
|
|
591
|
+
body: ReadableStream | null;
|
|
592
|
+
etag: string;
|
|
593
|
+
httpMetadata?: {
|
|
594
|
+
contentType?: string;
|
|
595
|
+
};
|
|
596
|
+
key: string;
|
|
597
|
+
/** Hex SHA-256, when R2 carries a checksum (surfaced by `@lunora/storage`). */
|
|
598
|
+
sha256?: string;
|
|
599
|
+
/** Base64 SHA-256 (RFC 9530 digest encoding), when R2 carries a checksum. */
|
|
600
|
+
sha256Base64?: string;
|
|
601
|
+
size: number;
|
|
602
|
+
}
|
|
603
|
+
/** Byte window forwarded to `download()` so R2 streams just the requested slice. */
|
|
604
|
+
interface StorageRange {
|
|
605
|
+
length: number;
|
|
606
|
+
offset: number;
|
|
607
|
+
}
|
|
608
|
+
/** The minimal storage surface {@link serveStorageObject} needs: a metadata-rich `download`. */
|
|
609
|
+
interface StorageDownloader {
|
|
610
|
+
download: (key: string, options?: {
|
|
611
|
+
range?: StorageRange;
|
|
612
|
+
}) => Promise<StorageObjectBody | null>;
|
|
613
|
+
}
|
|
614
|
+
/** Any ctx that carries a {@link StorageDownloader} on `.storage` (Query/Mutation/Action ctx all do). */
|
|
615
|
+
interface ContextWithStorage {
|
|
616
|
+
storage: StorageDownloader;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Stream a stored object as an HTTP {@link Response} from an `httpAction`
|
|
620
|
+
* handler, with correct `Content-Type`, `ETag`, and `Accept-Ranges: bytes`.
|
|
621
|
+
* Honors a single-range `Range` request → **206 Partial Content** with
|
|
622
|
+
* `Content-Range` + `Content-Length`; otherwise **200**. A missing object is a
|
|
623
|
+
* **404**; an out-of-bounds range is a **416** with a `Content-Range` of
|
|
624
|
+
* `bytes` star-slash-size.
|
|
625
|
+
*
|
|
626
|
+
* A range request re-issues the `download()` with the resolved `{ offset, length }`
|
|
627
|
+
* window so R2 streams only those bytes back to the Worker — the slice is never
|
|
628
|
+
* buffered in the isolate. The first `download()` is used only for the object's
|
|
629
|
+
* size + metadata (its body is left unread and cancelled). For very large
|
|
630
|
+
* objects a signed URL (`ctx.storage.getSignedUrl`) is still cheaper since the
|
|
631
|
+
* client then ranges against R2/CDN directly with no Worker hop.
|
|
632
|
+
*/
|
|
633
|
+
declare const serveStorageObject: (context: ContextWithStorage, key: string, request: Request) => Promise<Response>;
|
|
634
|
+
/** Handler for a connection-lifecycle hook. */
|
|
635
|
+
type LifecycleHandler = (context: MutationCtx, event: LifecycleEvent) => Promise<void> | void;
|
|
636
|
+
/** Register a hook that fires once when a client's WebSocket connects. */
|
|
637
|
+
declare const onConnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
|
|
638
|
+
/** Register a hook that fires once when a client's WebSocket disconnects. */
|
|
639
|
+
declare const onDisconnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
|
|
640
|
+
/**
|
|
641
|
+
* Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
|
|
642
|
+
* `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
|
|
643
|
+
* resolver, same `can(...)` permission check — so an author can branch a mask
|
|
644
|
+
* on the caller's role/permission. `row` is the full pre-mask row the column
|
|
645
|
+
* belongs to; `column` is the column currently being masked. Both are absent
|
|
646
|
+
* when the context is used for the procedure-wide `bypass` check (no specific
|
|
647
|
+
* cell is in play yet).
|
|
648
|
+
*/
|
|
649
|
+
interface MaskContext<Context = unknown> {
|
|
650
|
+
readonly auth: {
|
|
651
|
+
/** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
|
|
652
|
+
readonly can: (permission: Permission | string) => boolean;
|
|
653
|
+
readonly identity?: Record<string, unknown> | null;
|
|
654
|
+
readonly roles: ReadonlyArray<string>;
|
|
655
|
+
readonly userId: null | string;
|
|
656
|
+
};
|
|
657
|
+
/** The column currently being masked. Present only inside a per-cell {@link MaskFn}. */
|
|
658
|
+
readonly column?: string;
|
|
659
|
+
readonly ctx: Context;
|
|
660
|
+
/** The full pre-mask row the masked cell belongs to. Present only inside a per-cell {@link MaskFn}. */
|
|
661
|
+
readonly row?: Record<string, unknown>;
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* A custom masking function. Receives the raw cell value and the
|
|
665
|
+
* {@link MaskContext}, returns the value to surface. Use it for partial masks
|
|
666
|
+
* (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
|
|
667
|
+
* or format-preserving tokens. A function that **throws** fails closed — the
|
|
668
|
+
* cell is redacted to `null`, never leaked raw.
|
|
669
|
+
*/
|
|
670
|
+
type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
|
|
671
|
+
/**
|
|
672
|
+
* How a column is masked:
|
|
673
|
+
*
|
|
674
|
+
* - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
|
|
675
|
+
* the right choice for any value that must actually be kept secret.
|
|
676
|
+
* - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
|
|
677
|
+
* same input always yields the same token (joinable/groupable client-side).
|
|
678
|
+
* **This is NOT a confidentiality control.** It is a non-cryptographic,
|
|
679
|
+
* unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
|
|
680
|
+
* phone numbers, SSNs) are brute-force-recoverable by the very caller you are
|
|
681
|
+
* masking from, and identical values always produce identical tokens across
|
|
682
|
+
* rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
|
|
683
|
+
* stable pseudonym for grouping/joining and leaking the value is acceptable —
|
|
684
|
+
* never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
|
|
685
|
+
* - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
|
|
686
|
+
*/
|
|
687
|
+
type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
|
|
688
|
+
/** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
|
|
689
|
+
type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
|
|
690
|
+
/**
|
|
691
|
+
* The mask declaration passed to `mask(...)`: a table → column → strategy map.
|
|
692
|
+
* Deliberately a plain object literal so the codegen feeder can statically read
|
|
693
|
+
* which columns a procedure masks (powering the `mask_uncovered_pii_column`
|
|
694
|
+
* advisor lint), exactly as the RLS feeder reads policy tables.
|
|
695
|
+
*/
|
|
696
|
+
type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
|
|
697
|
+
/**
|
|
698
|
+
* Options for `mask(policies, options)`.
|
|
699
|
+
*
|
|
700
|
+
* - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
|
|
701
|
+
* inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
|
|
702
|
+
* not listed grants no permissions (fails closed for unknown roles).
|
|
703
|
+
* - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
|
|
704
|
+
* mask is skipped (the caller sees raw values). Use it for a privileged
|
|
705
|
+
* viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
|
|
706
|
+
* branching every column when an entire class of caller should see clear data.
|
|
707
|
+
*/
|
|
708
|
+
interface MaskOptions<Context = unknown> {
|
|
709
|
+
readonly bypass?: (context: MaskContext<Context>) => boolean;
|
|
710
|
+
readonly roles?: ReadonlyArray<Role>;
|
|
711
|
+
}
|
|
712
|
+
interface QueryPage$1 {
|
|
713
|
+
continueCursor: null | string;
|
|
714
|
+
isDone: boolean;
|
|
715
|
+
page: Record<string, unknown>[];
|
|
716
|
+
}
|
|
717
|
+
interface QueryArgs$1 {
|
|
718
|
+
baseWhere?: unknown;
|
|
719
|
+
cursor?: null | string;
|
|
720
|
+
limit?: number;
|
|
721
|
+
where?: unknown;
|
|
722
|
+
with?: Record<string, unknown>;
|
|
723
|
+
}
|
|
724
|
+
interface AggregateArgs$1 {
|
|
725
|
+
field?: string;
|
|
726
|
+
op: string;
|
|
727
|
+
}
|
|
728
|
+
interface GroupByArgs$1 {
|
|
729
|
+
agg?: {
|
|
730
|
+
field?: string;
|
|
731
|
+
op: string;
|
|
732
|
+
};
|
|
733
|
+
by: ReadonlyArray<string>;
|
|
734
|
+
}
|
|
735
|
+
interface TableReaderLike$1 {
|
|
736
|
+
collect: () => Promise<Record<string, unknown>[]>;
|
|
737
|
+
filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike$1;
|
|
738
|
+
first: () => Promise<Record<string, unknown> | null>;
|
|
739
|
+
order: (direction: "asc" | "desc") => TableReaderLike$1;
|
|
740
|
+
paginate: (options: {
|
|
741
|
+
cursor?: null | string;
|
|
742
|
+
numItems: number;
|
|
743
|
+
}) => Promise<QueryPage$1>;
|
|
744
|
+
take: (limit: number) => Promise<Record<string, unknown>[]>;
|
|
745
|
+
unique: () => Promise<Record<string, unknown> | null>;
|
|
746
|
+
withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
|
|
747
|
+
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Structural projection of the runtime ORM writer — the same subset
|
|
751
|
+
* `../rls/middleware` mirrors, so the wrapper is interchangeable between
|
|
752
|
+
* `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
|
|
753
|
+
* inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
|
|
754
|
+
*/
|
|
755
|
+
interface MaskDatabase {
|
|
756
|
+
aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
|
|
757
|
+
count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
|
|
758
|
+
delete: (id: string, expectedTable?: string) => Promise<void>;
|
|
759
|
+
deleteMany: (ids: ReadonlyArray<string>, options?: {
|
|
760
|
+
limit?: number;
|
|
761
|
+
}) => Promise<{
|
|
762
|
+
deleted: number;
|
|
763
|
+
}>;
|
|
764
|
+
findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
|
|
765
|
+
findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
|
|
766
|
+
findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
|
|
767
|
+
get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
|
|
768
|
+
groupBy: (tableName: string, options: GroupByArgs$1) => Promise<ReadonlyArray<{
|
|
769
|
+
key: Record<string, unknown>;
|
|
770
|
+
value: null | number;
|
|
771
|
+
}>>;
|
|
772
|
+
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
773
|
+
insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
774
|
+
limit?: number;
|
|
775
|
+
}) => Promise<string[]>;
|
|
776
|
+
lookupById?: (id: string, expectedTable?: string) => Promise<null | {
|
|
777
|
+
row: Record<string, unknown>;
|
|
778
|
+
tableName: string;
|
|
779
|
+
}>;
|
|
780
|
+
patch: (id: string, patch: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
781
|
+
patchMany: (patches: ReadonlyArray<{
|
|
782
|
+
id: string;
|
|
783
|
+
patch: Record<string, unknown>;
|
|
784
|
+
}>, options?: {
|
|
785
|
+
limit?: number;
|
|
786
|
+
}) => Promise<void>;
|
|
787
|
+
query: (tableName: string) => TableReaderLike$1;
|
|
788
|
+
rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
|
|
789
|
+
position: number;
|
|
790
|
+
total: number;
|
|
791
|
+
}>;
|
|
792
|
+
rankBefore?: (tableName: string, indexName: string, options: unknown) => Promise<{
|
|
793
|
+
before: number;
|
|
794
|
+
total: number;
|
|
795
|
+
}>;
|
|
796
|
+
rankPage: (tableName: string, indexName: string, options?: unknown) => Promise<QueryPage$1>;
|
|
797
|
+
replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
798
|
+
}
|
|
799
|
+
/** Roles list source on the context. Tolerant of older auth states (mirrors RLS's `AuthLike`). */
|
|
800
|
+
type AuthLike$1 = {
|
|
801
|
+
getIdentity?: () => Promise<Record<string, unknown> | null>;
|
|
802
|
+
roles?: ReadonlyArray<string>;
|
|
803
|
+
userId?: null | string;
|
|
804
|
+
};
|
|
805
|
+
interface MaskContextIn {
|
|
806
|
+
auth?: AuthLike$1;
|
|
807
|
+
db: MaskDatabase;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
|
|
811
|
+
* Closes over the policy map at builder-construction time; resolves identity +
|
|
812
|
+
* the `bypass` decision per call against the live ctx.
|
|
813
|
+
*
|
|
814
|
+
* IMPORTANT: a mask is in scope only for procedures whose builder chain
|
|
815
|
+
* includes this middleware — opt-in, never global (the same invariant as RLS).
|
|
816
|
+
*/
|
|
817
|
+
declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
|
|
818
|
+
/**
|
|
819
|
+
* Online data-migration authoring API.
|
|
820
|
+
*
|
|
821
|
+
* `defineMigration` declares a per-document backfill over one table: `up`
|
|
822
|
+
* transforms every existing row, `down` (optional) reverses it. Unlike the D1
|
|
823
|
+
* SQL schema migrations in `@lunora/d1`, these run *inside each shard's*
|
|
824
|
+
* Durable Object against live documents, in keyset batches, and are resumable —
|
|
825
|
+
* the per-shard runner in `@lunora/do` tracks progress in a reserved
|
|
826
|
+
* `__lunora_migrations` table so an interrupted run picks up where it stopped.
|
|
827
|
+
*
|
|
828
|
+
* The returned object carries a `__lunoraMigration` brand so codegen can
|
|
829
|
+
* discover declarations through the type checker (mirroring the procedure
|
|
830
|
+
* builder's `__lunoraProcedure` brand) and emit them into a `LUNORA_MIGRATIONS`
|
|
831
|
+
* registry the DO and CLI look migrations up by id.
|
|
832
|
+
*/
|
|
833
|
+
/** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
|
|
834
|
+
type MigrationDocument = Record<string, unknown>;
|
|
835
|
+
/**
|
|
836
|
+
* Transform applied to one document. Return a new document to rewrite the row,
|
|
837
|
+
* or `undefined` to leave it untouched (skipped, not counted as changed). The
|
|
838
|
+
* runner always preserves the original `_id` and `_creationTime`, so the
|
|
839
|
+
* returned document neither needs to nor should change row identity.
|
|
840
|
+
*/
|
|
841
|
+
type MigrationTransform = (document: MigrationDocument) => MigrationDocument | undefined | void;
|
|
842
|
+
interface MigrationDefinition {
|
|
843
|
+
/** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
|
|
844
|
+
readonly batchSize?: number;
|
|
845
|
+
/** Optional reverse transform, applied by `migrate down`. */
|
|
846
|
+
readonly down?: MigrationTransform;
|
|
847
|
+
/** Stable, unique identifier — the key per-shard run-state is tracked under. */
|
|
848
|
+
readonly id: string;
|
|
849
|
+
/** Table whose documents this migration iterates. */
|
|
850
|
+
readonly table: string;
|
|
851
|
+
/** Forward transform, applied to every row by `migrate up`. */
|
|
852
|
+
readonly up: MigrationTransform;
|
|
853
|
+
}
|
|
854
|
+
/** A {@link MigrationDefinition} plus the codegen discovery marker. */
|
|
855
|
+
interface RegisteredMigration extends MigrationDefinition {
|
|
856
|
+
readonly __lunoraMigration: true;
|
|
857
|
+
}
|
|
858
|
+
/** Declare an online data migration. See the module docs for runtime semantics. */
|
|
859
|
+
declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
|
|
860
|
+
/**
|
|
861
|
+
* The prefixed tables a single plugin `P` contributes, or an empty map when it
|
|
862
|
+
* ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
|
|
863
|
+
* so {@link InstalledTables} can fold a tuple of plugins.
|
|
864
|
+
*/
|
|
865
|
+
type ExtensionTablesOf<P> = P extends {
|
|
866
|
+
readonly extension: SchemaExtension<infer X> & {
|
|
867
|
+
readonly key: infer K;
|
|
868
|
+
};
|
|
869
|
+
} ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
|
|
870
|
+
/**
|
|
871
|
+
* Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
|
|
872
|
+
* auto-prefixed extension tables left-to-right — the type-level mirror of
|
|
873
|
+
* {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
|
|
874
|
+
*/
|
|
875
|
+
type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
|
|
876
|
+
/**
|
|
877
|
+
* Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
|
|
878
|
+
* `ctx.api.<key>` additions {@link composePluginMiddleware} accumulates as each
|
|
879
|
+
* plugin middleware runs. Independent of the incoming context, which the builder
|
|
880
|
+
* infers at the `.use(...)` site.
|
|
881
|
+
*/
|
|
882
|
+
type ComposedOut<Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? ComposedOut<Rest> & (Head extends Plugin<any, any, infer Out> ? Out : unknown) : unknown;
|
|
883
|
+
/**
|
|
884
|
+
* Schema fragment a plugin contributes. Same shape as the `tables` map
|
|
885
|
+
* passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
|
|
886
|
+
* `defineSchema` argument so a plugin can ship vector decls alongside its
|
|
887
|
+
* tables.
|
|
888
|
+
*/
|
|
889
|
+
interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
|
|
890
|
+
/** Stable key identifying the plugin that owns this extension. */
|
|
891
|
+
readonly key: string;
|
|
892
|
+
/**
|
|
893
|
+
* Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
|
|
894
|
+
* each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
|
|
895
|
+
* collide with an app table; do **not** namespace manually.
|
|
896
|
+
*/
|
|
897
|
+
readonly tables: T;
|
|
898
|
+
/**
|
|
899
|
+
* Optional standalone vector indexes the plugin ships, keyed by index
|
|
900
|
+
* name. Merged into the host schema's `vectorIndexes`; a key collision
|
|
901
|
+
* with the base schema is a hard error (same policy as tables).
|
|
902
|
+
*/
|
|
903
|
+
readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
|
|
907
|
+
* error messages on collision) and a type-level brand.
|
|
908
|
+
*/
|
|
909
|
+
declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
|
|
910
|
+
tables: T;
|
|
911
|
+
vectorIndexes?: Record<string, VectorIndexDefinition>;
|
|
912
|
+
}) => SchemaExtension<T>;
|
|
913
|
+
/**
|
|
914
|
+
* A plugin packages an optional schema extension and optional middleware.
|
|
915
|
+
* Both are independently usable: an app can install only the schema (e.g.
|
|
916
|
+
* for plugins that ship background workers but no per-request behavior)
|
|
917
|
+
* or only the middleware (plugins that augment ctx without persistent
|
|
918
|
+
* state).
|
|
919
|
+
*/
|
|
920
|
+
interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
|
|
921
|
+
/**
|
|
922
|
+
* Optional schema extension. Apps install via
|
|
923
|
+
* `defineSchema(...).extend(plugin.extension)`.
|
|
924
|
+
*/
|
|
925
|
+
readonly extension?: SchemaExtension<TExtension>;
|
|
926
|
+
/** Stable key identifying the plugin. Matches `extension.key` when set. */
|
|
927
|
+
readonly key: string;
|
|
928
|
+
/**
|
|
929
|
+
* Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
|
|
930
|
+
* The middleware can extend `ctx`; convention is to attach helpers under
|
|
931
|
+
* `ctx.api.<key>`, e.g.
|
|
932
|
+
*
|
|
933
|
+
* ```ts
|
|
934
|
+
* middleware: ({ ctx, next }) =>
|
|
935
|
+
* next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
|
|
936
|
+
* ```
|
|
937
|
+
*/
|
|
938
|
+
readonly middleware?: Middleware<TContextIn, TContextOut>;
|
|
939
|
+
}
|
|
940
|
+
/** Options to {@link definePlugin}. */
|
|
941
|
+
interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>, TContextIn, TContextOut> {
|
|
942
|
+
extension?: SchemaExtension<TExtension>;
|
|
943
|
+
middleware?: Middleware<TContextIn, TContextOut>;
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* Package a schema extension + middleware as a reusable plugin. Either
|
|
947
|
+
* field is optional — `definePlugin("foo", {})` is valid but degenerate.
|
|
948
|
+
*/
|
|
949
|
+
declare const definePlugin: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>) => Plugin<TExtension, TContextIn, TContextOut>;
|
|
950
|
+
/**
|
|
951
|
+
* Bundle of registered functions a {@link Component} ships. Keys are the
|
|
952
|
+
* function's local name (e.g. `check`, `reset`); the registered function
|
|
953
|
+
* value carries its own kind / args / handler.
|
|
954
|
+
*
|
|
955
|
+
* Users re-export from their own lunora module so codegen picks them up:
|
|
956
|
+
*
|
|
957
|
+
* ```ts
|
|
958
|
+
* // lunora/ratelimit.ts
|
|
959
|
+
* import { ratelimit } from "@vendor/ratelimit-component";
|
|
960
|
+
* export const { check, reset } = ratelimit.functions;
|
|
961
|
+
* // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
|
|
962
|
+
* ```
|
|
963
|
+
*
|
|
964
|
+
* Codegen follows the re-export back to the bundled `query/mutation/action`
|
|
965
|
+
* call (property access or destructuring both work), so the functions land in
|
|
966
|
+
* the generated `api` under the re-exporting file's namespace.
|
|
967
|
+
*/
|
|
968
|
+
type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
|
|
969
|
+
/**
|
|
970
|
+
* Component = {@link Plugin} with a bundle of registered functions. The
|
|
971
|
+
* extension + middleware + functions are independent: a component can ship
|
|
972
|
+
* functions without a schema (e.g. a stateless utility), or a schema
|
|
973
|
+
* without functions (e.g. shared table definitions), and any combination.
|
|
974
|
+
*/
|
|
975
|
+
interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
|
|
976
|
+
readonly functions: F;
|
|
977
|
+
}
|
|
978
|
+
interface DefineComponentOptions<TExtension extends Record<string, TableDefinition>, TContextIn, TContextOut, F extends ComponentFunctions> extends DefinePluginOptions<TExtension, TContextIn, TContextOut> {
|
|
979
|
+
/** Registered functions the component ships. Keys are the function's local name. */
|
|
980
|
+
functions?: F;
|
|
981
|
+
}
|
|
982
|
+
/**
|
|
983
|
+
* Convenience wrapper around {@link definePlugin} that also bundles a set
|
|
984
|
+
* of registered functions. The resulting `component.functions` object is a
|
|
985
|
+
* record of `name → registered query/mutation/action`; consumers
|
|
986
|
+
* re-export entries so codegen discovers them as user functions:
|
|
987
|
+
*
|
|
988
|
+
* ```ts
|
|
989
|
+
* export const ratelimit = defineComponent("ratelimit", {
|
|
990
|
+
* // Bare `buckets` merges in as `ratelimit_buckets`.
|
|
991
|
+
* extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
|
|
992
|
+
* middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
|
|
993
|
+
* functions: {
|
|
994
|
+
* check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
|
|
995
|
+
* reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
|
|
996
|
+
* },
|
|
997
|
+
* });
|
|
998
|
+
* ```
|
|
999
|
+
*
|
|
1000
|
+
* Re-exporting an entry (by property access or destructuring) is enough for
|
|
1001
|
+
* codegen to discover it in the host app's namespace — the discovery resolver
|
|
1002
|
+
* chases the re-export back to the bundled registration call.
|
|
1003
|
+
*/
|
|
1004
|
+
declare const defineComponent: <TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions>(key: string, options: DefineComponentOptions<TExtension, TContextIn, TContextOut, F>) => Component<TExtension, TContextIn, TContextOut, F>;
|
|
1005
|
+
/**
|
|
1006
|
+
* Map every key `K` of an extension's table map `X` to its auto-prefixed name
|
|
1007
|
+
* `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
|
|
1008
|
+
* so the typed `.extend(...)` chain reflects the real merged table names.
|
|
1009
|
+
*/
|
|
1010
|
+
type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K] };
|
|
1011
|
+
/**
|
|
1012
|
+
* Merge a {@link SchemaExtension} into an existing schema. Returns a new
|
|
1013
|
+
* schema object — never mutates the input.
|
|
1014
|
+
*
|
|
1015
|
+
* Extension tables are auto-namespaced: each bare table name is prefixed with
|
|
1016
|
+
* the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
|
|
1017
|
+
* style, and every intra-extension reference (relation targets, aggregate /
|
|
1018
|
+
* rank index `on`, standalone vector index `table`) is rewritten to match.
|
|
1019
|
+
* References to base/app tables are left untouched.
|
|
1020
|
+
*
|
|
1021
|
+
* Because each extension lives in its own `key` namespace, app↔component
|
|
1022
|
+
* collisions are impossible. The only remaining hard error is two extensions
|
|
1023
|
+
* sharing the same `key` and producing the same prefixed table (or vector
|
|
1024
|
+
* index) name — silent shadow would let one plugin hijack another's data.
|
|
1025
|
+
*/
|
|
1026
|
+
declare const mergeSchemaExtension: <T extends Record<string, TableDefinition>, X extends Record<string, TableDefinition>, Key extends string = string>(base: Schema<T>, extension: SchemaExtension<X> & {
|
|
1027
|
+
readonly key: Key;
|
|
1028
|
+
}) => Schema<PrefixedTables<X, Key> & T>;
|
|
1029
|
+
/**
|
|
1030
|
+
* Install several plugins' schema extensions in one call — the one-shot
|
|
1031
|
+
* counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
|
|
1032
|
+
* without an `extension` (middleware-only) are skipped; tables from those that
|
|
1033
|
+
* do are auto-prefixed and reference-rewritten exactly as
|
|
1034
|
+
* {@link mergeSchemaExtension} does for a single `.extend(...)`.
|
|
1035
|
+
*
|
|
1036
|
+
* ```ts
|
|
1037
|
+
* const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
|
|
1038
|
+
* // → todos + ratelimit_* + audit_*
|
|
1039
|
+
* ```
|
|
1040
|
+
*
|
|
1041
|
+
* Pair it with {@link composePluginMiddleware} to attach every plugin's
|
|
1042
|
+
* middleware in a single `.use(...)`, so installing N plugins is two calls
|
|
1043
|
+
* rather than N `.extend(...)` + N `.use(...)`.
|
|
1044
|
+
*/
|
|
1045
|
+
declare const installPlugins: <T extends Record<string, TableDefinition>, const Plugins extends ReadonlyArray<Plugin<any, any, any>>>(base: Schema<T>, plugins: Plugins) => Schema<InstalledTables<T, Plugins>>;
|
|
1046
|
+
/**
|
|
1047
|
+
* Compose every plugin's middleware into a single middleware you attach with one
|
|
1048
|
+
* `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
|
|
1049
|
+
* in array order, each seeing the context the previous one widened, so the final
|
|
1050
|
+
* `next({ ctx })` the builder receives carries every plugin's `ctx.api.<key>`
|
|
1051
|
+
* additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
|
|
1052
|
+
* value, the middleware sibling of {@link installPlugins}.
|
|
1053
|
+
*
|
|
1054
|
+
* `ContextIn` is left free so the builder infers it from the context at the
|
|
1055
|
+
* `.use(...)` site; the result type widens it by the union of the plugins'
|
|
1056
|
+
* outputs.
|
|
1057
|
+
*/
|
|
1058
|
+
declare const composePluginMiddleware: <ContextIn = unknown, const Plugins extends ReadonlyArray<Plugin<any, any, any>> = ReadonlyArray<Plugin<any, any, any>>>(plugins: Plugins) => Middleware<ContextIn, ComposedOut<Plugins> & ContextIn>;
|
|
1059
|
+
/** Options for `.vectorize(field, opts)` (DSL Shape A). */
|
|
1060
|
+
interface VectorizeOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1061
|
+
dimensions: number;
|
|
1062
|
+
embed: VectorEmbedder;
|
|
1063
|
+
/** Logical index name; must match a `[[vectorize]]` binding in wrangler. */
|
|
1064
|
+
index: string;
|
|
1065
|
+
/** Fields mirrored into Vectorize metadata for filtering. */
|
|
1066
|
+
metadata?: ReadonlyArray<keyof Shape & string>;
|
|
1067
|
+
metric: VectorMetric;
|
|
1068
|
+
}
|
|
1069
|
+
/** A `one` (many-to-one) relation descriptor; phantom `Target` carries the target table name. */
|
|
1070
|
+
interface OneRelation<Target extends string = string> extends RelationDefinition {
|
|
1071
|
+
readonly __target?: Target;
|
|
1072
|
+
readonly kind: "one";
|
|
1073
|
+
}
|
|
1074
|
+
/** A `many` (one-to-many) relation descriptor; phantom `Target` carries the target table name. */
|
|
1075
|
+
interface ManyRelation<Target extends string = string> extends RelationDefinition {
|
|
1076
|
+
readonly __target?: Target;
|
|
1077
|
+
readonly kind: "many";
|
|
1078
|
+
}
|
|
1079
|
+
/** The `r` argument passed to `.relations((r) => …)`. */
|
|
1080
|
+
interface RelationBuilder {
|
|
1081
|
+
/** One-to-many: the FK `field` lives on the target table, matching this table's `references` (default `_id`). */
|
|
1082
|
+
many: <Target extends string>(table: Target, options: {
|
|
1083
|
+
field: string;
|
|
1084
|
+
references?: string;
|
|
1085
|
+
}) => ManyRelation<Target>;
|
|
1086
|
+
/** Many-to-one: the FK `field` lives on this table, pointing at `table`.`references` (default `_id`). */
|
|
1087
|
+
one: <Target extends string>(table: Target, options: {
|
|
1088
|
+
field: string;
|
|
1089
|
+
onDelete?: OnDeleteAction;
|
|
1090
|
+
references?: string;
|
|
1091
|
+
}) => OneRelation<Target>;
|
|
1092
|
+
}
|
|
1093
|
+
/**
|
|
1094
|
+
* Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
|
|
1095
|
+
* `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
|
|
1096
|
+
* `COUNT(*) GROUP BY userId` accelerator.
|
|
1097
|
+
*/
|
|
1098
|
+
interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1099
|
+
/** Group keys; counter rows are one per distinct tuple. Omitted = single-row aggregate over the whole table. */
|
|
1100
|
+
by?: ReadonlyArray<keyof Shape & string>;
|
|
1101
|
+
/** The column the reducer applies to. Required for `sum`/`min`/`max`/`avg`; ignored for `count`. */
|
|
1102
|
+
field?: keyof Shape & string;
|
|
1103
|
+
/** Reducer (default `count`). */
|
|
1104
|
+
op?: AggregateOp;
|
|
1105
|
+
/** Static predicate baked into the counter — only matching rows are aggregated. */
|
|
1106
|
+
where?: Record<string, unknown>;
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
|
|
1110
|
+
* accepts either an array of `{ field, direction }` keys, or the shorthand
|
|
1111
|
+
* `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
|
|
1112
|
+
* rank — omitted ⇒ one global rank over the whole table.
|
|
1113
|
+
*/
|
|
1114
|
+
interface InlineRankIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1115
|
+
/** Columns that scope each ranking; omitted ⇒ one global rank. */
|
|
1116
|
+
partitionBy?: ReadonlyArray<keyof Shape & string>;
|
|
1117
|
+
/** Ordered sort keys driving the rank. Required. */
|
|
1118
|
+
sortBy: ReadonlyArray<{
|
|
1119
|
+
direction?: "asc" | "desc";
|
|
1120
|
+
field: keyof Shape & string;
|
|
1121
|
+
}>;
|
|
1122
|
+
/** Static predicate baked into the index; only matching rows enter. */
|
|
1123
|
+
where?: Record<string, unknown>;
|
|
1124
|
+
}
|
|
1125
|
+
interface TableBuilder<Shape extends Record<string, Validator> = Record<string, Validator>> extends TableDefinition<Shape> {
|
|
1126
|
+
/** Declare an aggregate (counter/sum/…) maintained by triggers for O(1) reads. */
|
|
1127
|
+
aggregateIndex: (name: string, options?: InlineAggregateIndexOptions<Shape>) => TableBuilder<Shape>;
|
|
1128
|
+
/**
|
|
1129
|
+
* Mark this table as written outside Lunora's discoverable insert path —
|
|
1130
|
+
* by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
|
|
1131
|
+
* better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
|
|
1132
|
+
* (`table_without_insert`) then skip it instead of flagging the absent
|
|
1133
|
+
* `ctx.db.insert(...)`.
|
|
1134
|
+
*/
|
|
1135
|
+
externallyManaged: () => TableBuilder<Shape>;
|
|
1136
|
+
/**
|
|
1137
|
+
* Mark this table as global (cross-shard). Backed by **D1** by default;
|
|
1138
|
+
* pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
|
|
1139
|
+
* via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
|
|
1140
|
+
* table stays reactive — live queries re-run on write.
|
|
1141
|
+
*/
|
|
1142
|
+
global: (options?: {
|
|
1143
|
+
backend?: GlobalBackend;
|
|
1144
|
+
}) => TableBuilder<Shape>;
|
|
1145
|
+
/** Add a secondary index. */
|
|
1146
|
+
index: (name: string, fields: ReadonlyArray<string>, options?: {
|
|
1147
|
+
unique?: boolean;
|
|
1148
|
+
}) => TableBuilder<Shape>;
|
|
1149
|
+
/**
|
|
1150
|
+
* Opt this table OUT of secure-by-default RLS. Under a schema marked
|
|
1151
|
+
* `.rls("required")`, every table is protected (the write path denies raw,
|
|
1152
|
+
* non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
|
|
1153
|
+
* plain `query`/`mutation` may read/write it without an RLS policy. No effect
|
|
1154
|
+
* when the schema does not require RLS.
|
|
1155
|
+
*/
|
|
1156
|
+
public: () => TableBuilder<Shape>;
|
|
1157
|
+
/**
|
|
1158
|
+
* Declare a rank index (sorted companion table, btree-backed) for
|
|
1159
|
+
* `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
|
|
1160
|
+
*/
|
|
1161
|
+
rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
|
|
1162
|
+
/** Declare relations to other tables, loaded via `findMany({ with })`. */
|
|
1163
|
+
relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
|
|
1164
|
+
/** Add a search index over a field with optional filter fields. */
|
|
1165
|
+
searchIndex: (name: string, options: {
|
|
1166
|
+
field: string;
|
|
1167
|
+
filterFields?: ReadonlyArray<string>;
|
|
1168
|
+
}) => TableBuilder<Shape>;
|
|
1169
|
+
/** Route storage by the named field — one DO per distinct value. */
|
|
1170
|
+
shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
|
|
1171
|
+
/** Declare named lifecycle triggers fired inline within the write path. */
|
|
1172
|
+
triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
|
|
1173
|
+
/** Declare a vector index over a single text field on this table. */
|
|
1174
|
+
vectorize: (field: keyof Shape & string, options: VectorizeOptions<Shape>) => TableBuilder<Shape>;
|
|
1175
|
+
}
|
|
1176
|
+
/** Options for `defineVectorIndex(...)` (DSL Shape B). */
|
|
1177
|
+
interface VectorIndexOptions {
|
|
1178
|
+
dimensions: number;
|
|
1179
|
+
embed: VectorEmbedder;
|
|
1180
|
+
/** Optional projection of the source row into Vectorize metadata. */
|
|
1181
|
+
metadata?: (row: Record<string, unknown>) => Record<string, unknown>;
|
|
1182
|
+
metric: VectorMetric;
|
|
1183
|
+
/** The vector source: which table, and how to derive the embedded text. */
|
|
1184
|
+
source: {
|
|
1185
|
+
select: (row: Record<string, unknown>) => string;
|
|
1186
|
+
table: string;
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
/**
|
|
1190
|
+
* Build a table definition. Returned object is both the table definition (for
|
|
1191
|
+
* `defineSchema`) and a fluent builder for indexes + sharding metadata.
|
|
1192
|
+
*/
|
|
1193
|
+
declare const defineTable: <Shape extends Record<string, Validator>>(shape: Shape) => TableBuilder<Shape>;
|
|
1194
|
+
/**
|
|
1195
|
+
* Declare a standalone vector index (DSL Shape B). Pass the returned value in
|
|
1196
|
+
* the `vectorIndexes` map of {@link defineSchema} when the source is derived
|
|
1197
|
+
* from multiple fields or a computation rather than a single column.
|
|
1198
|
+
*/
|
|
1199
|
+
declare const defineVectorIndex: (options: VectorIndexOptions) => VectorIndexDefinition;
|
|
1200
|
+
/**
|
|
1201
|
+
* Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
|
|
1202
|
+
* Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
|
|
1203
|
+
* the owning table explicitly via `on` — handy when a single counter wants to
|
|
1204
|
+
* live next to the schema map rather than inside a table chain.
|
|
1205
|
+
*/
|
|
1206
|
+
interface AggregateIndexOptions {
|
|
1207
|
+
by?: ReadonlyArray<string>;
|
|
1208
|
+
field?: string;
|
|
1209
|
+
on: string;
|
|
1210
|
+
op?: AggregateOp;
|
|
1211
|
+
where?: Record<string, unknown>;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Declare a standalone aggregate index. Pass the returned value to
|
|
1215
|
+
* `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
|
|
1216
|
+
* the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
|
|
1217
|
+
* (DO + D1) read every index uniformly off the table definition.
|
|
1218
|
+
*/
|
|
1219
|
+
declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
|
|
1220
|
+
/**
|
|
1221
|
+
* Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
|
|
1222
|
+
* Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
|
|
1223
|
+
* `table` so it can sit next to the schema map.
|
|
1224
|
+
*/
|
|
1225
|
+
interface RankIndexOptions {
|
|
1226
|
+
partitionBy?: ReadonlyArray<string>;
|
|
1227
|
+
sortBy: ReadonlyArray<{
|
|
1228
|
+
direction?: "asc" | "desc";
|
|
1229
|
+
field: string;
|
|
1230
|
+
}>;
|
|
1231
|
+
table: string;
|
|
1232
|
+
where?: Record<string, unknown>;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Declare a standalone rank index. Pass the returned value to
|
|
1236
|
+
* `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
|
|
1237
|
+
* by index name — the schema attaches it to `tables[on].rankIndexes`.
|
|
1238
|
+
*/
|
|
1239
|
+
declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
|
|
1240
|
+
/**
|
|
1241
|
+
* Build the application schema. The first argument is the table map; the
|
|
1242
|
+
* optional second argument registers standalone `defineVectorIndex(...)`
|
|
1243
|
+
* declarations (DSL Shape B) keyed by index name. The optional third argument
|
|
1244
|
+
* registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
|
|
1245
|
+
* the optional fourth argument registers standalone `defineRankIndex(...)`
|
|
1246
|
+
* declarations. Both are folded into the matching `tables[on].*Indexes` array
|
|
1247
|
+
* so runtime backends read every index uniformly off the table definition.
|
|
1248
|
+
*/
|
|
1249
|
+
/**
|
|
1250
|
+
* Schema with an in-place `.extend(plugin.extension)` method. Used so apps
|
|
1251
|
+
* can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
|
|
1252
|
+
*
|
|
1253
|
+
* `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
|
|
1254
|
+
* the merged tables. Extension tables are auto-namespaced by the extension
|
|
1255
|
+
* `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
|
|
1256
|
+
* prefixed names via {@link PrefixedTables}. Chains:
|
|
1257
|
+
* `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
|
|
1258
|
+
* `a`'s prefixed tables then `b`'s.
|
|
1259
|
+
*/
|
|
1260
|
+
type ExtendableSchema<T extends Record<string, TableDefinition>> = {
|
|
1261
|
+
extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
|
|
1262
|
+
readonly key: Key;
|
|
1263
|
+
}) => ExtendableSchema<PrefixedTables<X, Key> & T>;
|
|
1264
|
+
/**
|
|
1265
|
+
* Turn on secure-by-default RLS for the whole schema. Every table is then
|
|
1266
|
+
* protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
|
|
1267
|
+
* procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
|
|
1268
|
+
* `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
|
|
1269
|
+
* mode, so `.rls("required")` composes with `.extend(...)` either order.
|
|
1270
|
+
*/
|
|
1271
|
+
rls: (mode: "required") => ExtendableSchema<T>;
|
|
1272
|
+
} & Schema<T>;
|
|
1273
|
+
declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
|
|
1274
|
+
/** Default time-to-live for a presence row: a heartbeat keeps a member "present" for this long. */
|
|
1275
|
+
declare const DEFAULT_TTL_MS = 3e4;
|
|
1276
|
+
declare const PRESENCE_BARE_TABLE = "present";
|
|
1277
|
+
/**
|
|
1278
|
+
* The prefixed table name the extension produces at merge time. The handlers
|
|
1279
|
+
* read/write this name directly so they always agree with the merged schema.
|
|
1280
|
+
*/
|
|
1281
|
+
declare const PRESENCE_TABLE: "presence_present";
|
|
1282
|
+
/**
|
|
1283
|
+
* A single present member as returned by `listPresent`.
|
|
1284
|
+
*
|
|
1285
|
+
* Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
|
|
1286
|
+
* connection secret — disclosing every member's `sessionId` would let any
|
|
1287
|
+
* subscriber enumerate them and target the heartbeat / disconnect write paths.
|
|
1288
|
+
* A "who's here" UI needs only `userId` + awareness `data`; the caller already
|
|
1289
|
+
* knows its own session id locally (the `usePresence` hook returns it).
|
|
1290
|
+
*/
|
|
1291
|
+
interface PresenceMember {
|
|
1292
|
+
/** Opaque awareness blob (selection, cursor, name, color…). */
|
|
1293
|
+
data?: Record<string, unknown>;
|
|
1294
|
+
/** Last heartbeat time (epoch ms). */
|
|
1295
|
+
lastSeen: number;
|
|
1296
|
+
/** The room / channel / document this presence is scoped to. */
|
|
1297
|
+
roomId: string;
|
|
1298
|
+
/** Authenticated user id, when known. */
|
|
1299
|
+
userId?: string;
|
|
1300
|
+
}
|
|
1301
|
+
/** Options for {@link definePresence}. */
|
|
1302
|
+
interface DefinePresenceOptions {
|
|
1303
|
+
/**
|
|
1304
|
+
* Grace window (ms) before a gracefully-closed session is dropped from the
|
|
1305
|
+
* present list. When `0` (the default), `onDisconnect` hard-deletes the
|
|
1306
|
+
* session's row the instant its socket closes. When `> 0`, the row is
|
|
1307
|
+
* instead aged so the read-time TTL filter hides it `disconnectGraceMs`
|
|
1308
|
+
* from now — a reconnect with the same `sessionId` within the window
|
|
1309
|
+
* re-heartbeats and restores full presence with no visible flicker (the
|
|
1310
|
+
* AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
|
|
1311
|
+
*/
|
|
1312
|
+
disconnectGraceMs?: number;
|
|
1313
|
+
/**
|
|
1314
|
+
* How long (ms) a heartbeat keeps a member present. `listPresent` excludes
|
|
1315
|
+
* rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
|
|
1316
|
+
*/
|
|
1317
|
+
ttlMs?: number;
|
|
1318
|
+
}
|
|
1319
|
+
/** The registered functions a presence component ships. */
|
|
1320
|
+
interface PresenceFunctions {
|
|
1321
|
+
/**
|
|
1322
|
+
* Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
|
|
1323
|
+
* delete its presence row so it disappears from `listPresent` with no TTL
|
|
1324
|
+
* lag. Targets the row by the `{ roomId, sessionId }` the client passed as
|
|
1325
|
+
* the connection `context`, and only deletes it when the disconnecting
|
|
1326
|
+
* VERIFIED identity owns the row (so a forged context can't evict another
|
|
1327
|
+
* member). The TTL filter + `sweep` remain the fallback for ungraceful drops
|
|
1328
|
+
* where no `context` was recorded.
|
|
1329
|
+
*/
|
|
1330
|
+
disconnect: RegisteredLifecycleHook;
|
|
1331
|
+
/**
|
|
1332
|
+
* Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
|
|
1333
|
+
* Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
|
|
1334
|
+
* subscribers receive a single-row delta, not a churn of insert/delete. A
|
|
1335
|
+
* heartbeat may only patch a row owned by the same identity (an existing row
|
|
1336
|
+
* held by a different `userId` is refused with `FORBIDDEN`), so a client
|
|
1337
|
+
* can't overwrite another member's awareness data via a guessed `sessionId`.
|
|
1338
|
+
*/
|
|
1339
|
+
heartbeat: RegisteredMutation<{
|
|
1340
|
+
data: ReturnType<typeof v.optional>;
|
|
1341
|
+
roomId: ReturnType<typeof v.string>;
|
|
1342
|
+
sessionId: ReturnType<typeof v.string>;
|
|
1343
|
+
}, {
|
|
1344
|
+
lastSeen: number;
|
|
1345
|
+
}>;
|
|
1346
|
+
/**
|
|
1347
|
+
* Live query returning the non-expired members of `roomId`, newest heartbeat
|
|
1348
|
+
* first. Subscribe to it for a reactive "who's here" list.
|
|
1349
|
+
*/
|
|
1350
|
+
listPresent: RegisteredQuery<{
|
|
1351
|
+
roomId: ReturnType<typeof v.string>;
|
|
1352
|
+
}, PresenceMember[]>;
|
|
1353
|
+
/**
|
|
1354
|
+
* Internal mutation that hard-deletes every expired row for `roomId`. Stale
|
|
1355
|
+
* rows already vanish from `listPresent` via the read-time TTL filter; this
|
|
1356
|
+
* only reclaims storage. Schedule it (cron / `runAfter`) if you care.
|
|
1357
|
+
*/
|
|
1358
|
+
sweep: RegisteredMutation<{
|
|
1359
|
+
roomId: ReturnType<typeof v.string>;
|
|
1360
|
+
}, {
|
|
1361
|
+
deleted: number;
|
|
1362
|
+
}>;
|
|
1363
|
+
}
|
|
1364
|
+
/** The component shape `definePresence` returns: the presence extension + typed functions. */
|
|
1365
|
+
type PresenceComponent = Component<{
|
|
1366
|
+
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
1367
|
+
}> & {
|
|
1368
|
+
functions: PresenceFunctions;
|
|
1369
|
+
};
|
|
1370
|
+
/**
|
|
1371
|
+
* The presence schema extension: a single `present` table, auto-namespaced to
|
|
1372
|
+
* `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
|
|
1373
|
+
* heartbeat upsert and by `roomId` for `listPresent`.
|
|
1374
|
+
*/
|
|
1375
|
+
declare const presenceExtension: SchemaExtension<{
|
|
1376
|
+
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
1377
|
+
}>;
|
|
1378
|
+
declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
|
|
1379
|
+
/**
|
|
1380
|
+
* The middlewares `protectPublic` chains, in the order they run. Every field is
|
|
1381
|
+
* optional, so a bundle can be just a rate limit, just a captcha, or any mix —
|
|
1382
|
+
* pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
|
|
1383
|
+
* from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
|
|
1384
|
+
* `@lunora/auth`). They are accepted as values rather than imported here so
|
|
1385
|
+
* `@lunora/server` keeps no dependency on those packages (which depend on it).
|
|
1386
|
+
*/
|
|
1387
|
+
interface ProtectPublicOptions<Context> {
|
|
1388
|
+
/**
|
|
1389
|
+
* A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
|
|
1390
|
+
* an obvious flood is cheaper to reject with the in-memory limiter than with
|
|
1391
|
+
* a Turnstile siteverify round-trip.
|
|
1392
|
+
*/
|
|
1393
|
+
captcha?: Middleware<Context, Context>;
|
|
1394
|
+
/**
|
|
1395
|
+
* A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
|
|
1396
|
+
* any network-bound check below it runs.
|
|
1397
|
+
*/
|
|
1398
|
+
rateLimit?: Middleware<Context, Context>;
|
|
1399
|
+
/** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
|
|
1400
|
+
use?: ReadonlyArray<Middleware<Context, Context>>;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Compose the recommended public-procedure protections into a single
|
|
1404
|
+
* `.use()`-able middleware. It is thin sugar over middleware composition — no
|
|
1405
|
+
* new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
|
|
1406
|
+
* and any extra middlewares so a public mutation that creates users, sends
|
|
1407
|
+
* mail, or consumes credits is guarded in one attachment:
|
|
1408
|
+
*
|
|
1409
|
+
* ```ts
|
|
1410
|
+
* export const signUp = mutation
|
|
1411
|
+
* .use(protectPublic({
|
|
1412
|
+
* rateLimit: rateLimit(limiter, "signup"),
|
|
1413
|
+
* captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
|
|
1414
|
+
* }))
|
|
1415
|
+
* .handler(async (ctx, args) => { ... });
|
|
1416
|
+
* ```
|
|
1417
|
+
*
|
|
1418
|
+
* The bundle is context-preserving — each inner middleware leaves the context
|
|
1419
|
+
* unchanged — so it slots into any `.use()` chain without reshaping the
|
|
1420
|
+
* procedure context. Omitted fields are skipped; an empty bundle is a
|
|
1421
|
+
* transparent pass-through.
|
|
1422
|
+
*/
|
|
1423
|
+
declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
|
|
1424
|
+
declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
|
|
1425
|
+
/**
|
|
1426
|
+
* Build a project-bound, relation-aware `definePolicy` typed against the
|
|
1427
|
+
* generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
|
|
1428
|
+
* `createPolicyDsl<DataModel, Relations>()` binding into `_generated/server.ts`,
|
|
1429
|
+
* so importing `definePolicy` from the generated module constrains `table` to a
|
|
1430
|
+
* real table name and type-checks the `when` predicate — including Prisma-style
|
|
1431
|
+
* relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
|
|
1432
|
+
* on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
|
|
1433
|
+
* the compile-time surface narrows, so a policy authored either way is
|
|
1434
|
+
* discovered identically by the `rls()` chain.
|
|
1435
|
+
*/
|
|
1436
|
+
declare const createPolicyDsl: <DM, REL extends Record<keyof DM, object>>() => <T extends keyof DM, Context = unknown>(input: TypedDefinePolicyInput<DM, REL, T, Context>) => Policy<Context>;
|
|
1437
|
+
/**
|
|
1438
|
+
* Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
|
|
1439
|
+
* it to a role through `defineRole`'s `permissions`, register those roles with
|
|
1440
|
+
* the middleware via `rls(policies, { roles })`, then check it in a policy with
|
|
1441
|
+
* `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
|
|
1442
|
+
* worked example.
|
|
1443
|
+
*/
|
|
1444
|
+
declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
|
|
1445
|
+
/**
|
|
1446
|
+
* Collect a list of policies into the structure the `rls()` middleware
|
|
1447
|
+
* consumes. Multiple read policies on the same table OR together (any one
|
|
1448
|
+
* matching reveals the row); multiple write policies for the same `(table,
|
|
1449
|
+
* op)` AND together (every one must allow). The middleware keeps them in order
|
|
1450
|
+
* and decides — see `./middleware`.
|
|
1451
|
+
*
|
|
1452
|
+
* Validates against an **accidentally duplicated policy** — the same
|
|
1453
|
+
* `(table, on)` registered with the *same* decision function (a copy-paste, or
|
|
1454
|
+
* the same policy object spread in twice). Because multiple DISTINCT policies
|
|
1455
|
+
* per `(table, on)` are intentional, the check keys on the `when` reference too:
|
|
1456
|
+
* only a reference-identical `when` for the same `(table, on)` is a real
|
|
1457
|
+
* duplicate. Throws at module load so the misconfiguration surfaces immediately
|
|
1458
|
+
* rather than as a silently double-evaluated predicate at request time.
|
|
1459
|
+
*/
|
|
1460
|
+
declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
|
|
1461
|
+
declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
|
|
1462
|
+
/**
|
|
1463
|
+
* Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
|
|
1464
|
+
* runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
|
|
1465
|
+
* `restrictsCounts` straight off these option objects, so as long as the
|
|
1466
|
+
* fields here stay name-compatible the wrapper is portable across the two
|
|
1467
|
+
* dialects without an inter-package dependency.
|
|
1468
|
+
*/
|
|
1469
|
+
interface QueryArgs {
|
|
1470
|
+
baseWhere?: WhereInput;
|
|
1471
|
+
cursor?: null | string;
|
|
1472
|
+
limit?: number;
|
|
1473
|
+
orderBy?: ReadonlyArray<unknown>;
|
|
1474
|
+
/**
|
|
1475
|
+
* Per-target-table read filter the RLS wrapper attaches so a `with` relation
|
|
1476
|
+
* is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
|
|
1477
|
+
* the top-level read: `(table) => readBase(table).baseWhere`.
|
|
1478
|
+
*/
|
|
1479
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1480
|
+
restrictsCounts?: boolean;
|
|
1481
|
+
where?: WhereInput;
|
|
1482
|
+
with?: Record<string, unknown>;
|
|
1483
|
+
}
|
|
1484
|
+
interface CountArgs {
|
|
1485
|
+
baseWhere?: WhereInput;
|
|
1486
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1487
|
+
restrictsCounts?: boolean;
|
|
1488
|
+
where?: WhereInput;
|
|
1489
|
+
}
|
|
1490
|
+
/** Structural mirror of `@lunora/do`'s `AggregateOptions` — only the fields the wrapper touches. */
|
|
1491
|
+
interface AggregateArgs {
|
|
1492
|
+
baseWhere?: WhereInput;
|
|
1493
|
+
field?: string;
|
|
1494
|
+
op: string;
|
|
1495
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1496
|
+
restrictsCounts?: boolean;
|
|
1497
|
+
where?: WhereInput;
|
|
1498
|
+
}
|
|
1499
|
+
/** Structural mirror of `@lunora/do`'s `GroupByOptions`. */
|
|
1500
|
+
interface GroupByArgs {
|
|
1501
|
+
agg?: {
|
|
1502
|
+
field?: string;
|
|
1503
|
+
op: string;
|
|
1504
|
+
};
|
|
1505
|
+
baseWhere?: WhereInput;
|
|
1506
|
+
by: ReadonlyArray<string>;
|
|
1507
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1508
|
+
restrictsCounts?: boolean;
|
|
1509
|
+
where?: WhereInput;
|
|
1510
|
+
}
|
|
1511
|
+
/** Structural mirror of `@lunora/do`'s `RankOptions`. */
|
|
1512
|
+
interface RankArgs {
|
|
1513
|
+
baseWhere?: WhereInput;
|
|
1514
|
+
restrictsCounts?: boolean;
|
|
1515
|
+
row: Record<string, unknown> | string;
|
|
1516
|
+
where?: WhereInput;
|
|
1517
|
+
}
|
|
1518
|
+
/** Structural mirror of `@lunora/do`'s `RankBeforeOptions`. */
|
|
1519
|
+
interface RankBeforeArgs {
|
|
1520
|
+
partitionKey: string;
|
|
1521
|
+
restrictsCounts?: boolean;
|
|
1522
|
+
rowId: string;
|
|
1523
|
+
sortValues: ReadonlyArray<unknown>;
|
|
1524
|
+
}
|
|
1525
|
+
/** Structural mirror of `@lunora/do`'s `RankPageOptions`. */
|
|
1526
|
+
interface RankPageArgs {
|
|
1527
|
+
baseWhere?: WhereInput;
|
|
1528
|
+
cursor?: null | string;
|
|
1529
|
+
restrictsCounts?: boolean;
|
|
1530
|
+
take?: number;
|
|
1531
|
+
where?: WhereInput;
|
|
1532
|
+
}
|
|
1533
|
+
interface QueryPage {
|
|
1534
|
+
continueCursor: null | string;
|
|
1535
|
+
isDone: boolean;
|
|
1536
|
+
page: Record<string, unknown>[];
|
|
1537
|
+
}
|
|
1538
|
+
interface TableReaderLike {
|
|
1539
|
+
collect: () => Promise<Record<string, unknown>[]>;
|
|
1540
|
+
filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike;
|
|
1541
|
+
first: () => Promise<Record<string, unknown> | null>;
|
|
1542
|
+
paginate: (options: {
|
|
1543
|
+
cursor?: null | string;
|
|
1544
|
+
numItems: number;
|
|
1545
|
+
}) => Promise<QueryPage>;
|
|
1546
|
+
take: (limit: number) => Promise<Record<string, unknown>[]>;
|
|
1547
|
+
withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike;
|
|
1548
|
+
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike;
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Structural projection of the runtime ORM writer. The wrapper relies only
|
|
1552
|
+
* on these fields, so it's interchangeable between `@lunora/do`'s
|
|
1553
|
+
* `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
|
|
1554
|
+
*/
|
|
1555
|
+
interface DatabaseWriterLike {
|
|
1556
|
+
/**
|
|
1557
|
+
* Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
|
|
1558
|
+
* `baseWhere` into `options` so the reduction only sees policy-visible rows
|
|
1559
|
+
* (safe: an aggregate scoped to `where` never reveals a hidden row — see
|
|
1560
|
+
* `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
|
|
1561
|
+
* wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
|
|
1562
|
+
*/
|
|
1563
|
+
aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
|
|
1564
|
+
count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
|
|
1565
|
+
delete: (id: string, expectedTable?: string) => Promise<void>;
|
|
1566
|
+
deleteMany: (ids: ReadonlyArray<string>, options?: {
|
|
1567
|
+
limit?: number;
|
|
1568
|
+
}, expectedTable?: string) => Promise<{
|
|
1569
|
+
deleted: number;
|
|
1570
|
+
}>;
|
|
1571
|
+
findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
|
|
1572
|
+
findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
|
|
1573
|
+
findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
|
|
1574
|
+
get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
|
|
1575
|
+
/**
|
|
1576
|
+
* Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
|
|
1577
|
+
* reduction is scoped to policy-visible rows, so a group count tallies only
|
|
1578
|
+
* rows the caller may read. Required for the same reason as `aggregate`.
|
|
1579
|
+
*/
|
|
1580
|
+
groupBy: (tableName: string, options: GroupByArgs) => Promise<ReadonlyArray<{
|
|
1581
|
+
key: Record<string, unknown>;
|
|
1582
|
+
value: null | number;
|
|
1583
|
+
}>>;
|
|
1584
|
+
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
1585
|
+
insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
1586
|
+
limit?: number;
|
|
1587
|
+
}) => Promise<string[]>;
|
|
1588
|
+
insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
1589
|
+
allowExplicitId?: boolean;
|
|
1590
|
+
limit?: number;
|
|
1591
|
+
}) => Promise<string[]>;
|
|
1592
|
+
/**
|
|
1593
|
+
* Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
|
|
1594
|
+
* already knows the owning table of an id internally, so it can return
|
|
1595
|
+
* `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
|
|
1596
|
+
* uses it to collapse the per-call membership-probe fan-out (1 `get` + N
|
|
1597
|
+
* `findFirst` across every policy table) down to one lookup. Writers that
|
|
1598
|
+
* don't implement it fall back to the probe path.
|
|
1599
|
+
*/
|
|
1600
|
+
lookupById?: (id: string, expectedTable?: string) => Promise<null | {
|
|
1601
|
+
row: Record<string, unknown>;
|
|
1602
|
+
tableName: string;
|
|
1603
|
+
}>;
|
|
1604
|
+
patch: (id: string, patch: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
1605
|
+
patchMany: (patches: ReadonlyArray<{
|
|
1606
|
+
id: string;
|
|
1607
|
+
patch: Record<string, unknown>;
|
|
1608
|
+
}>, options?: {
|
|
1609
|
+
limit?: number;
|
|
1610
|
+
}, expectedTable?: string) => Promise<void>;
|
|
1611
|
+
query: (tableName: string) => TableReaderLike;
|
|
1612
|
+
/**
|
|
1613
|
+
* Rank a row within its partition. A position is a count-of-rows-before, so
|
|
1614
|
+
* — exactly like `count()` — it can't be trusted in an RLS-restricted
|
|
1615
|
+
* reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
|
|
1616
|
+
* for the same reason as `aggregate`.
|
|
1617
|
+
*/
|
|
1618
|
+
rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
|
|
1619
|
+
position: number;
|
|
1620
|
+
total: number;
|
|
1621
|
+
}>;
|
|
1622
|
+
/** Cross-shard rank primitive — same count-of-before RLS hazard as `rank`; failed closed under a read policy. */
|
|
1623
|
+
rankBefore?: (tableName: string, indexName: string, options: RankBeforeArgs) => Promise<{
|
|
1624
|
+
before: number;
|
|
1625
|
+
total: number;
|
|
1626
|
+
}>;
|
|
1627
|
+
/**
|
|
1628
|
+
* Sorted pagination over a rank companion. The companion stores only the
|
|
1629
|
+
* partition + sort keys + id, so an arbitrary read `baseWhere` can't be
|
|
1630
|
+
* enforced against it (and re-filtering the fetched rows would break page
|
|
1631
|
+
* sizing). RLS therefore fails it closed rather than leak hidden rows.
|
|
1632
|
+
* Required for the same reason as `aggregate`.
|
|
1633
|
+
*/
|
|
1634
|
+
rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
|
|
1635
|
+
replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* What a procedure's `ctx.db` must structurally satisfy for the middleware
|
|
1639
|
+
* to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
|
|
1640
|
+
* rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
|
|
1641
|
+
* the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
|
|
1642
|
+
* and structural matching keeps this module free of an `@lunora/do`-typed
|
|
1643
|
+
* `ctx`.
|
|
1644
|
+
*/
|
|
1645
|
+
type RlsDatabase = DatabaseWriterLike;
|
|
1646
|
+
/** Roles list source on the context. Tolerant of older auth states. */
|
|
1647
|
+
type AuthLike = {
|
|
1648
|
+
getIdentity?: () => Promise<Record<string, unknown> | null>;
|
|
1649
|
+
roles?: ReadonlyArray<string>;
|
|
1650
|
+
userId?: null | string;
|
|
1651
|
+
};
|
|
1652
|
+
/** Minimal shape the middleware needs on the incoming ctx. */
|
|
1653
|
+
interface RlsContextIn {
|
|
1654
|
+
auth?: AuthLike;
|
|
1655
|
+
db: RlsDatabase;
|
|
1656
|
+
}
|
|
1657
|
+
declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
|
|
1658
|
+
/**
|
|
1659
|
+
* Operations a storage rule can gate. `read` covers `download` / `getMetadata`
|
|
1660
|
+
* / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
|
|
1661
|
+
* `delete` is `delete`; `list` is a prefix listing (governed via the file
|
|
1662
|
+
* browser / admin path, not `ctx.storage` which has no `list`).
|
|
1663
|
+
*/
|
|
1664
|
+
type StorageOperation = "delete" | "list" | "read" | "write";
|
|
1665
|
+
/** A rule's decision. `true` allows, `false` denies, `undefined` opts this rule out. */
|
|
1666
|
+
type StorageRuleDecision = boolean | undefined;
|
|
1667
|
+
/**
|
|
1668
|
+
* Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
|
|
1669
|
+
* (the per-request userId / roles / identity and the `can(permission)` helper),
|
|
1670
|
+
* so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
|
|
1671
|
+
* `key` is the object key the operation targets (for `list`, the listing
|
|
1672
|
+
* prefix). `ctx` is the full procedure context the middleware closed over.
|
|
1673
|
+
*/
|
|
1674
|
+
interface StorageRuleContext<Context = unknown> {
|
|
1675
|
+
readonly auth: {
|
|
1676
|
+
readonly can: (permission: Permission | string) => boolean;
|
|
1677
|
+
readonly identity?: Record<string, unknown> | null;
|
|
1678
|
+
readonly roles: ReadonlyArray<string>;
|
|
1679
|
+
readonly userId: null | string;
|
|
1680
|
+
};
|
|
1681
|
+
readonly ctx: Context;
|
|
1682
|
+
/** The object key the operation targets (the listing prefix for `list`). */
|
|
1683
|
+
readonly key: string;
|
|
1684
|
+
}
|
|
1685
|
+
/** A registered storage rule as stored in the rule table. */
|
|
1686
|
+
interface StorageRule<Context = unknown> {
|
|
1687
|
+
/**
|
|
1688
|
+
* Logical bucket the rule governs — matched against the accessor's bucket
|
|
1689
|
+
* (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
|
|
1690
|
+
* A rule only applies to operations on its own bucket. The unnamed bucket is
|
|
1691
|
+
* `"default"`. Also surfaced in the studio's access-rules view.
|
|
1692
|
+
*/
|
|
1693
|
+
readonly bucket: string;
|
|
1694
|
+
readonly on: StorageOperation;
|
|
1695
|
+
/** Optional key-prefix scope; the rule only governs keys under it. Absent ⇒ the whole bucket. */
|
|
1696
|
+
readonly prefix?: string;
|
|
1697
|
+
readonly when: (context: StorageRuleContext<Context>) => StorageRuleDecision;
|
|
1698
|
+
}
|
|
1699
|
+
/** Input accepted by `defineStorageRule`. The result is the same shape. */
|
|
1700
|
+
interface DefineStorageRuleInput<Context = unknown> {
|
|
1701
|
+
bucket: string;
|
|
1702
|
+
on: StorageOperation;
|
|
1703
|
+
prefix?: string;
|
|
1704
|
+
when: (context: StorageRuleContext<Context>) => StorageRuleDecision;
|
|
1705
|
+
}
|
|
1706
|
+
/**
|
|
1707
|
+
* Options for the `storageRules(rules, options)` middleware. `roles` registers
|
|
1708
|
+
* the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
|
|
1709
|
+
* `RlsOptions.roles` does — fail-closed for unlisted roles.
|
|
1710
|
+
*/
|
|
1711
|
+
interface StorageRulesOptions {
|
|
1712
|
+
readonly roles?: ReadonlyArray<Role>;
|
|
1713
|
+
}
|
|
1714
|
+
declare const defineStorageRule: <Context = unknown>(input: DefineStorageRuleInput<Context>) => StorageRule<Context>;
|
|
1715
|
+
/**
|
|
1716
|
+
* Collect a list of storage rules into the structure the `storageRules()`
|
|
1717
|
+
* middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
|
|
1718
|
+
* any one allowing grants the operation (each rule grants a slice of the
|
|
1719
|
+
* keyspace).
|
|
1720
|
+
*
|
|
1721
|
+
* Validates against an **accidentally duplicated rule** — the same
|
|
1722
|
+
* `(bucket, on, prefix)` registered with the *same* decision function (a
|
|
1723
|
+
* copy-paste, or the same rule object spread in twice). Because multiple
|
|
1724
|
+
* DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
|
|
1725
|
+
* `when` reference too. Throws at module load so the misconfiguration surfaces
|
|
1726
|
+
* immediately rather than as a silently double-evaluated predicate.
|
|
1727
|
+
*/
|
|
1728
|
+
declare const defineStorageRules: <Context = unknown>(rules: ReadonlyArray<StorageRule<Context>>) => ReadonlyArray<StorageRule<Context>>;
|
|
1729
|
+
/** The minimal `ctx.auth` shape the middleware reads — a structural subset that the full AuthState satisfies. Tolerant of older auth states (mirrors RLS's `AuthLike`). */
|
|
1730
|
+
type StorageAuthLike = {
|
|
1731
|
+
getIdentity?: () => Promise<Record<string, unknown> | null>;
|
|
1732
|
+
roles?: ReadonlyArray<string>;
|
|
1733
|
+
userId?: null | string;
|
|
1734
|
+
};
|
|
1735
|
+
interface StorageContextIn {
|
|
1736
|
+
auth?: StorageAuthLike;
|
|
1737
|
+
storage?: unknown;
|
|
1738
|
+
}
|
|
1739
|
+
declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
|
|
1740
|
+
declare const VERSION = "0.0.0";
|
|
1741
|
+
export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, type DataModelInit, type DefineComponentOptions, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExtendableSchema, type FacadeEntry, type FacadeWriterLike, type FunctionKind, type HttpActionCtx, type HttpActionHandler, type HttpMethod, type HttpRoute, type HttpRouteBuilder, type HttpRouteFactory, type HttpRouteHandlerOptions, type HttpStreamHandlerOptions, type InferArgs, type InferEnv, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraErrorCode, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationDefinition, type MigrationDocument, type MigrationTransform, type MutationBuilder, type MutationCtx, type OnDeleteAction, type OneRelation, type OrmLike, DEFAULT_TTL_MS as PRESENCE_DEFAULT_TTL_MS, PRESENCE_TABLE, type Permission, type Plugin, type Policy, type PrefixedTables, type PresenceComponent, type PresenceFunctions, type PresenceMember, type ProtectPublicOptions, type QueryBuilder, type QueryCtx, type RankIndexDefinition, type RankIndexOptions, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type Role, type Schema, type SchemaExtension, type StorageOperation, type StorageRule, type StorageRuleContext, type StorageRuleDecision, type StorageRulesOptions, type TableBuilder, type TableDefinition, type TerminalKind, type TriggerBuilder, type TriggerDefinition, type TypedDefinePolicyInput, VERSION, type VectorEmbedder, type VectorIndexDefinition, type VectorIndexOptions, type VectorMetric, type VectorizeOptions, type WhereInput, asBucketStorage, bindOrm, bindTableFacade, composePluginMiddleware, createPolicyDsl, defineAggregateIndex, defineComponent, defineEnv, defineMigration, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, httpAction, httpRoute, httpRouter, initLunora, installPlugins, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules };
|