@lunora/server 1.0.0-alpha.3 → 1.0.0-alpha.31
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 +6 -0
- package/README.md +50 -0
- package/dist/data-model.d.mts +231 -156
- package/dist/data-model.d.ts +231 -156
- package/dist/index.d.mts +1191 -811
- package/dist/index.d.ts +1191 -811
- package/dist/index.mjs +20 -15
- package/dist/packem_shared/{LunoraEnvError-DjFkpkSP.mjs → LunoraEnvError-BGmd1Qs0.mjs} +4 -4
- package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +9 -0
- package/dist/packem_shared/{PRESENCE_DEFAULT_TTL_MS-UQuUI5sV.mjs → PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs} +5 -5
- package/dist/packem_shared/{bindOrm-Ce57S3N9.mjs → bindOrm-CaY7Wq9Z.mjs} +42 -23
- package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +107 -0
- package/dist/packem_shared/{composePluginMiddleware-Ck5_TUO8.mjs → composePluginMiddleware-z62dttBo.mjs} +11 -8
- package/dist/packem_shared/{createPolicyDsl-De67zPDS.mjs → createPolicyDsl-By3QB4he.mjs} +4 -1
- package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +58 -0
- package/dist/packem_shared/{defineAggregateIndex-B20MIOmj.mjs → defineAggregateIndex-cdo0g-un.mjs} +108 -8
- package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +35 -0
- package/dist/packem_shared/{defineMigration-CAJLr6fx.mjs → defineMigration-Hx01yIht.mjs} +3 -1
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +11 -0
- package/dist/packem_shared/defineShape-C5scNOrf.mjs +18 -0
- package/dist/packem_shared/{defineStorageRule-qu0mpilX.mjs → defineStorageRule-B5nL4Z1P.mjs} +4 -1
- package/dist/packem_shared/functions-Di9FUNkf.mjs +5 -0
- package/dist/packem_shared/{httpAction-B7FYUEgr.mjs → httpAction-DCXoYPIk.mjs} +42 -27
- package/dist/packem_shared/{initLunora-CATvPsVt.mjs → initLunora-D0Wuki7S.mjs} +36 -7
- package/dist/packem_shared/{mask-eCUYOwhd.mjs → mask-BepaW7YN.mjs} +132 -9
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +13 -0
- package/dist/packem_shared/{protectPublic-BjFkQ_Or.mjs → protectPublic-BlcGpiRc.mjs} +1 -1
- package/dist/packem_shared/{rls-Bi9HiyDC.mjs → rls-BaDQf7MG.mjs} +34 -6
- package/dist/packem_shared/{run-middleware-CYQOuoV6.mjs → run-middleware-I6EiQfxL.mjs} +3 -1
- package/dist/packem_shared/{storageRules-4a30FSpI.mjs → storageRules-6QxzDOcx.mjs} +1 -1
- package/dist/packem_shared/types.d-C4CMJK8x.d.mts +141 -0
- package/dist/packem_shared/types.d-DdYF8E18.d.ts +141 -0
- package/dist/rls/testing.d.mts +31 -31
- package/dist/rls/testing.d.ts +31 -31
- package/dist/rls/testing.mjs +1 -1
- package/dist/types.d.mts +957 -433
- package/dist/types.d.ts +957 -433
- package/package.json +6 -5
- package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
- package/dist/packem_shared/types.d-BDY0FYHK.d.ts +0 -135
- package/dist/packem_shared/types.d-DmvyEMD6.d.mts +0 -135
package/dist/index.d.mts
CHANGED
|
@@ -1,37 +1,39 @@
|
|
|
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";
|
|
1
|
+
import { Validator, Infer, ValidatorMap, InferValidatorMap, v } from '@lunora/values';
|
|
2
|
+
export { type ColumnValidator, type GeoPoint, type Id, type Infer, ValidationError, type Validator, type ValidatorKind, v } from '@lunora/values';
|
|
3
|
+
import { ArgsValidator, InferArgs, RegisteredAction, ExposeConfig, X402ProcedureConfig, ActionCtx, MutationCtx, RegisteredMutation, QueryCtx, RegisteredQuery, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, ExternalSourceDefinition, TriggerBuilder, TriggerDefinition, VectorEmbedder, VectorMetric, AggregateIndexDefinition, RankIndexDefinition } from "./types.mjs";
|
|
4
|
+
export { type AnyApi, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type FunctionVisibility, type GeoBoundingBox, type GeoFilterBuilder, type GeoIndexDefinition, type GeoPointInput, type IndexDefinition, type IndexRangeBuilder, type LifecycleEventKind, type LogFields, type LunoraLogMethod, type LunoraLogger, type LunoraMetrics, type LunoraTracer, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type SpanHandle, 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 TtlDefinition, 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 { LunoraError as LunoraError$1, LunoraErrorCode } from '@lunora/errors';
|
|
6
|
+
export type { LunoraErrorCode } from '@lunora/errors';
|
|
5
7
|
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-
|
|
7
|
-
export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-
|
|
8
|
+
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-C4CMJK8x.mjs";
|
|
9
|
+
export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-C4CMJK8x.mjs";
|
|
8
10
|
export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
|
|
9
11
|
import "./data-model.mjs";
|
|
10
12
|
/**
|
|
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
|
-
*/
|
|
13
|
+
* Make any `config.storage` result bucket-aware so `ctx.storage.bucket(name)`
|
|
14
|
+
* always resolves. A `createBucketStorage(...)` result already carries
|
|
15
|
+
* `.bucket` / `.bucketName` and is returned as-is; a single `createStorage(...)`
|
|
16
|
+
* (or the no-storage stub) is tagged as the `"default"` bucket, where
|
|
17
|
+
* `.bucket(name)` is the identity — single-bucket apps address one binding under
|
|
18
|
+
* every name.
|
|
19
|
+
*
|
|
20
|
+
* This is the runtime counterpart the generated `_generated/shard.ts` imports to
|
|
21
|
+
* wrap `ctx.storage`; it lives here (the single source) rather than being stamped
|
|
22
|
+
* inline into every generated file, so the bucket-tagging behaviour has one home
|
|
23
|
+
* alongside the storage ctx types. The input is genuinely heterogeneous (a thunk
|
|
24
|
+
* result cast through `unknown`), so the signature is `unknown → unknown`; the
|
|
25
|
+
* generated caller casts the result to its storage type.
|
|
26
|
+
*/
|
|
25
27
|
declare const asBucketStorage: (raw: unknown) => unknown;
|
|
26
28
|
/** Builder discriminator. Codegen reads this kind. */
|
|
27
29
|
type TerminalKind = FunctionKind;
|
|
28
30
|
/** Initial (empty) accumulated args for a fresh builder. */
|
|
29
31
|
type EmptyArgs = Record<never, never>;
|
|
30
32
|
/**
|
|
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
|
-
*/
|
|
33
|
+
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
34
|
+
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
35
|
+
* extension, and the result type reflects the widened context.
|
|
36
|
+
*/
|
|
35
37
|
interface MiddlewareNext<ContextIn> {
|
|
36
38
|
(): Promise<ContextIn>;
|
|
37
39
|
<Extension extends Record<string, unknown>>(options: {
|
|
@@ -39,10 +41,10 @@ interface MiddlewareNext<ContextIn> {
|
|
|
39
41
|
}): Promise<ContextIn & Extension>;
|
|
40
42
|
}
|
|
41
43
|
/**
|
|
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
|
-
*/
|
|
44
|
+
* A middleware receives the current context and a `next` continuation. Its
|
|
45
|
+
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
46
|
+
* propagates the extension into every downstream `.use()` and the handler.
|
|
47
|
+
*/
|
|
46
48
|
type Middleware<ContextIn, ContextOut> = (options: {
|
|
47
49
|
ctx: ContextIn;
|
|
48
50
|
next: MiddlewareNext<ContextIn>;
|
|
@@ -50,16 +52,23 @@ type Middleware<ContextIn, ContextOut> = (options: {
|
|
|
50
52
|
/** Options accepted by `initLunora.dataModel<DM>().create(...)`. Reserved for transformer/error-formatter wiring. */
|
|
51
53
|
type CreateOptions = Record<never, never>;
|
|
52
54
|
/**
|
|
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
|
-
*/
|
|
55
|
+
* `Output` carries the type declared by `.output(validator)`. It defaults to
|
|
56
|
+
* the `undefined` sentinel meaning "not declared": in that state the terminal
|
|
57
|
+
* stays generic over the handler's own return type. Once `.output()` sets it to
|
|
58
|
+
* a concrete type, the terminal requires the handler to return that type and
|
|
59
|
+
* the registration is typed to it (the runtime parses the result through the
|
|
60
|
+
* validator). `[Output] extends [undefined]` is wrapped in a tuple so a union
|
|
61
|
+
* `Output` doesn't distribute and so the test is for the exact sentinel.
|
|
62
|
+
*/
|
|
61
63
|
interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
62
64
|
readonly __lunoraProcedure: "query";
|
|
65
|
+
/**
|
|
66
|
+
* Publish this query on the opt-in public REST surface (plan 167) — the
|
|
67
|
+
* runtime mints `GET /_lunora/rest/<namespace>/<fn>` (and `POST`), dispatching
|
|
68
|
+
* THROUGH the procedure so `ctx.auth` / RLS / validators are enforced, and the
|
|
69
|
+
* generated OpenAPI describes it. Default-closed: omit to keep it RPC-only.
|
|
70
|
+
*/
|
|
71
|
+
expose: (config: ExposeConfig) => QueryBuilder<Context, Args, Output>;
|
|
63
72
|
input: <A extends ArgsValidator>(validators: A) => QueryBuilder<Context, A & Args, Output>;
|
|
64
73
|
output: <V extends Validator>(validator: V) => QueryBuilder<Context, Args, Infer<V>>;
|
|
65
74
|
query: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
@@ -70,22 +79,36 @@ interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined>
|
|
|
70
79
|
ctx: Context;
|
|
71
80
|
}) => Output | Promise<Output>) => RegisteredQuery<Args, Output>;
|
|
72
81
|
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
82
|
+
* Terminal: declare this procedure as a streaming query. The handler is an
|
|
83
|
+
* async generator (or any function returning an `AsyncIterable<R>`) that
|
|
84
|
+
* yields one chunk per server-pushed frame. The third `signal` argument is
|
|
85
|
+
* tripped when the client cancels — break out of the loop or check
|
|
86
|
+
* `signal.aborted` between yields. `.output()` does not apply: per-chunk
|
|
87
|
+
* validation is opt-in via the handler itself.
|
|
88
|
+
*/
|
|
80
89
|
stream: <R>(handler: (options: {
|
|
81
90
|
args: InferArgs<Args>;
|
|
82
91
|
ctx: Context;
|
|
83
92
|
signal: AbortSignal;
|
|
84
93
|
}) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => RegisteredStream<Args, R>;
|
|
85
94
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => QueryBuilder<ContextOut, Args, Output>;
|
|
95
|
+
/**
|
|
96
|
+
* Mark this query as paid. The origin worker answers an unpaid client RPC
|
|
97
|
+
* with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
|
|
98
|
+
* is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
|
|
99
|
+
* recipient, and facilitator come from the worker-level x402 charge config.
|
|
100
|
+
*/
|
|
101
|
+
x402: (config: X402ProcedureConfig) => QueryBuilder<Context, Args, Output>;
|
|
86
102
|
}
|
|
87
103
|
interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
88
104
|
readonly __lunoraProcedure: "mutation";
|
|
105
|
+
/**
|
|
106
|
+
* Publish this mutation on the opt-in public REST surface (plan 167) — the
|
|
107
|
+
* runtime mints `POST /_lunora/rest/<namespace>/<fn>`, dispatching THROUGH the
|
|
108
|
+
* procedure so `ctx.auth` / RLS / validators are enforced, and the generated
|
|
109
|
+
* OpenAPI describes it. Default-closed: omit to keep it RPC-only.
|
|
110
|
+
*/
|
|
111
|
+
expose: (config: ExposeConfig) => MutationBuilder<Context, Args, Output>;
|
|
89
112
|
input: <A extends ArgsValidator>(validators: A) => MutationBuilder<Context, A & Args, Output>;
|
|
90
113
|
mutation: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
91
114
|
args: InferArgs<Args>;
|
|
@@ -96,6 +119,13 @@ interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefine
|
|
|
96
119
|
}) => Output | Promise<Output>) => RegisteredMutation<Args, Output>;
|
|
97
120
|
output: <V extends Validator>(validator: V) => MutationBuilder<Context, Args, Infer<V>>;
|
|
98
121
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => MutationBuilder<ContextOut, Args, Output>;
|
|
122
|
+
/**
|
|
123
|
+
* Mark this mutation as paid. The origin worker answers an unpaid client RPC
|
|
124
|
+
* with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
|
|
125
|
+
* is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
|
|
126
|
+
* recipient, and facilitator come from the worker-level x402 charge config.
|
|
127
|
+
*/
|
|
128
|
+
x402: (config: X402ProcedureConfig) => MutationBuilder<Context, Args, Output>;
|
|
99
129
|
}
|
|
100
130
|
interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
101
131
|
readonly __lunoraProcedure: "action";
|
|
@@ -106,16 +136,30 @@ interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined>
|
|
|
106
136
|
args: InferArgs<Args>;
|
|
107
137
|
ctx: Context;
|
|
108
138
|
}) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
|
|
139
|
+
/**
|
|
140
|
+
* Publish this action on the opt-in public REST surface (plan 167) — the
|
|
141
|
+
* runtime mints `POST /_lunora/rest/<namespace>/<fn>`, dispatching THROUGH the
|
|
142
|
+
* procedure so `ctx.auth` / RLS / validators are enforced, and the generated
|
|
143
|
+
* OpenAPI describes it. Default-closed: omit to keep it RPC-only.
|
|
144
|
+
*/
|
|
145
|
+
expose: (config: ExposeConfig) => ActionBuilder<Context, Args, Output>;
|
|
109
146
|
input: <A extends ArgsValidator>(validators: A) => ActionBuilder<Context, A & Args, Output>;
|
|
110
147
|
output: <V extends Validator>(validator: V) => ActionBuilder<Context, Args, Infer<V>>;
|
|
111
148
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => ActionBuilder<ContextOut, Args, Output>;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Mark this action as paid. The origin worker answers an unpaid client RPC
|
|
151
|
+
* with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
|
|
152
|
+
* is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
|
|
153
|
+
* recipient, and facilitator come from the worker-level x402 charge config.
|
|
154
|
+
*/
|
|
155
|
+
x402: (config: X402ProcedureConfig) => ActionBuilder<Context, Args, Output>;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Internal builder variants. Identical to their public counterparts but carry
|
|
159
|
+
* the `__lunoraVisibility: "internal"` brand codegen keys off to route the
|
|
160
|
+
* registration into the `internal` object (and keep it off `api`). `input`/`use`
|
|
161
|
+
* return the internal builder type so the brand survives the whole chain.
|
|
162
|
+
*/
|
|
119
163
|
interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
120
164
|
readonly __lunoraProcedure: "query";
|
|
121
165
|
readonly __lunoraVisibility: "internal";
|
|
@@ -179,31 +223,38 @@ interface DataModelInit<DataModel> {
|
|
|
179
223
|
create: (options?: CreateOptions) => LunoraBuilders;
|
|
180
224
|
}
|
|
181
225
|
/**
|
|
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
|
-
*/
|
|
226
|
+
* Entry point for the procedure builder. `dataModel<DM>()` binds the generated
|
|
227
|
+
* `DataModel` (phantom for now), and `.create()` yields the public root builders
|
|
228
|
+
* plus their `internal*` counterparts.
|
|
229
|
+
*/
|
|
186
230
|
declare const initLunora: {
|
|
187
231
|
dataModel: <DataModel>() => DataModelInit<DataModel>;
|
|
188
232
|
};
|
|
189
233
|
/**
|
|
190
|
-
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
|
|
234
|
+
* Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
|
|
235
|
+
* `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
|
|
236
|
+
* absent or non-Secrets-Store binding throws a directed error pointing at the
|
|
237
|
+
* wrangler config; the lookup is lazy, so an unused secret never resolves.
|
|
238
|
+
*/
|
|
239
|
+
declare const createSecrets: (env: Record<string, unknown>) => Secrets;
|
|
240
|
+
/**
|
|
241
|
+
* Redact secrets from a free-form message. Masks, in order: any quoted value
|
|
242
|
+
* whose contents look like a credential (so a value surfaced as `received string
|
|
243
|
+
* "sk_live_…"` is masked even though the surrounding text is not a token); a
|
|
244
|
+
* `scheme://user:password@host` URL credential (the password segment); any
|
|
245
|
+
* known-prefix credential token wherever it appears, at any length; any value
|
|
246
|
+
* following a secret-named key in `KEY=value` / `KEY: value` form; and any
|
|
247
|
+
* remaining bare high-entropy ≥24-char token run anywhere in the message.
|
|
248
|
+
*
|
|
249
|
+
* This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
|
|
250
|
+
* secret under a non-secret-named key (and embedded credentials in shapes not
|
|
251
|
+
* enumerated here) can still slip through. Treat it as a backstop — prefer
|
|
252
|
+
* structured logging that never serializes raw env/secret fields in the first
|
|
253
|
+
* place over relying on post-hoc scrubbing of untrusted data.
|
|
254
|
+
*
|
|
255
|
+
* Exported because it is independently useful — call it before logging anything
|
|
256
|
+
* derived from `env`, request bodies, or thrown errors.
|
|
257
|
+
*/
|
|
207
258
|
declare const redactSecrets: (message: string) => string;
|
|
208
259
|
/** One key's validation failure, secrets already redacted out of `message`. */
|
|
209
260
|
interface EnvKeyFailure {
|
|
@@ -213,32 +264,31 @@ interface EnvKeyFailure {
|
|
|
213
264
|
message: string;
|
|
214
265
|
}
|
|
215
266
|
/**
|
|
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
|
|
223
|
-
override readonly name = "LunoraEnvError";
|
|
267
|
+
* Thrown when one or more env keys are missing or fail validation. Carries the
|
|
268
|
+
* structured list of `failures` (each with the offending `key`) so callers can
|
|
269
|
+
* react programmatically; `message` is the joined, secret-redacted summary.
|
|
270
|
+
*
|
|
271
|
+
* Named export only (no default) per the repo export convention.
|
|
272
|
+
*/
|
|
273
|
+
declare class LunoraEnvError extends LunoraError$1 {
|
|
224
274
|
readonly failures: ReadonlyArray<EnvKeyFailure>;
|
|
225
275
|
constructor(failures: ReadonlyArray<EnvKeyFailure>);
|
|
226
276
|
}
|
|
227
277
|
/** A record of `v.*` validators describing the expected env shape. */
|
|
228
278
|
type EnvShape = Record<string, Validator>;
|
|
229
279
|
/**
|
|
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]
|
|
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
|
-
*/
|
|
280
|
+
* The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
|
|
281
|
+
* become optional keys; everything else is required. Mirrors how `InferArgs`
|
|
282
|
+
* derives an args object from a validator map.
|
|
283
|
+
*/
|
|
284
|
+
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]>; };
|
|
285
|
+
/**
|
|
286
|
+
* The accessor returned by {@link defineEnv}. A typed view over an `env` object
|
|
287
|
+
* plus a `.parse(env)` escape hatch that validates every key eagerly.
|
|
288
|
+
*
|
|
289
|
+
* Call the accessor with the worker's `env` to get the typed, lazily-validated
|
|
290
|
+
* proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
|
|
291
|
+
*/
|
|
242
292
|
interface EnvAccessor<S extends EnvShape> {
|
|
243
293
|
/** Validate every key eagerly and return the typed, plain (non-proxy) object. Use for fail-fast-at-boot. */
|
|
244
294
|
parse: (env: unknown) => InferEnv<S>;
|
|
@@ -246,91 +296,40 @@ interface EnvAccessor<S extends EnvShape> {
|
|
|
246
296
|
(env: unknown): InferEnv<S>;
|
|
247
297
|
}
|
|
248
298
|
/**
|
|
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
|
-
*/
|
|
299
|
+
* Define a typed, validated accessor over a Worker's `env`. Pass a record of
|
|
300
|
+
* `v.*` validators; receive an accessor that validates lazily per key (cached
|
|
301
|
+
* per `env` identity) and infers its output type from the validators.
|
|
302
|
+
*
|
|
303
|
+
* ```ts
|
|
304
|
+
* import { defineEnv, v } from "@lunora/server";
|
|
305
|
+
*
|
|
306
|
+
* const config = defineEnv({
|
|
307
|
+
* STRIPE_KEY: v.string(),
|
|
308
|
+
* PORT: v.optional(v.number()),
|
|
309
|
+
* });
|
|
310
|
+
*
|
|
311
|
+
* export default {
|
|
312
|
+
* fetch(request, env) {
|
|
313
|
+
* const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
|
|
314
|
+
* // …
|
|
315
|
+
* },
|
|
316
|
+
* };
|
|
317
|
+
* ```
|
|
318
|
+
*
|
|
319
|
+
* Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
|
|
320
|
+
* invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
|
|
321
|
+
*/
|
|
272
322
|
declare const defineEnv: <S extends EnvShape>(shape: S) => EnvAccessor<S>;
|
|
323
|
+
declare class LunoraError extends LunoraError$1 {
|
|
324
|
+
constructor(code: LunoraErrorCode, message?: string, data?: unknown);
|
|
325
|
+
}
|
|
273
326
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
|
|
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
|
-
* Minimal structural writer the facade binds over. Declared with **method**
|
|
329
|
-
* syntax (not arrow properties) so a more-specifically-typed writer — both
|
|
330
|
-
* `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
|
|
331
|
-
* stays assignable under bivariant parameter checking. That is the whole reason
|
|
332
|
-
* the shared helper can serve both callers, hence the rule exemption.
|
|
333
|
-
*/
|
|
327
|
+
* Minimal structural writer the facade binds over. Declared with **method**
|
|
328
|
+
* syntax (not arrow properties) so a more-specifically-typed writer — both
|
|
329
|
+
* `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
|
|
330
|
+
* stays assignable under bivariant parameter checking. That is the whole reason
|
|
331
|
+
* the shared helper can serve both callers, hence the rule exemption.
|
|
332
|
+
*/
|
|
334
333
|
interface FacadeWriterLike {
|
|
335
334
|
aggregate(tableName: string, options: unknown): Promise<unknown>;
|
|
336
335
|
count(tableName: string, where?: unknown): Promise<number>;
|
|
@@ -342,6 +341,11 @@ interface FacadeWriterLike {
|
|
|
342
341
|
}, expectedTable?: string): Promise<{
|
|
343
342
|
deleted: number;
|
|
344
343
|
}>;
|
|
344
|
+
deleteWhere?(tableName: string, where: Record<string, unknown>, options?: {
|
|
345
|
+
limit?: number;
|
|
346
|
+
}): Promise<{
|
|
347
|
+
deleted: number;
|
|
348
|
+
}>;
|
|
345
349
|
findFirst(tableName: string, args?: unknown): Promise<unknown>;
|
|
346
350
|
findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
|
|
347
351
|
findMany(tableName: string, args?: unknown): Promise<unknown>;
|
|
@@ -350,15 +354,27 @@ interface FacadeWriterLike {
|
|
|
350
354
|
insert(tableName: string, document: Record<string, unknown>): Promise<string>;
|
|
351
355
|
insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
352
356
|
limit?: number;
|
|
353
|
-
|
|
357
|
+
skipDuplicates?: boolean;
|
|
358
|
+
}): Promise<(string | null)[]>;
|
|
354
359
|
patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
|
|
355
360
|
patchMany?(patches: ReadonlyArray<{
|
|
356
361
|
id: string;
|
|
357
362
|
patch: Record<string, unknown>;
|
|
358
363
|
}>, options?: {
|
|
359
364
|
limit?: number;
|
|
360
|
-
}, expectedTable?: string): Promise<
|
|
365
|
+
}, expectedTable?: string): Promise<{
|
|
366
|
+
patched: number;
|
|
367
|
+
}>;
|
|
368
|
+
patchWhere?(tableName: string, args: {
|
|
369
|
+
patch: Record<string, unknown>;
|
|
370
|
+
where: Record<string, unknown>;
|
|
371
|
+
}, options?: {
|
|
372
|
+
limit?: number;
|
|
373
|
+
}): Promise<{
|
|
374
|
+
patched: number;
|
|
375
|
+
}>;
|
|
361
376
|
query(tableName: string): {
|
|
377
|
+
withGeoIndex(indexName: string, build: (q: unknown) => unknown): unknown;
|
|
362
378
|
withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
|
|
363
379
|
};
|
|
364
380
|
rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
|
|
@@ -371,11 +387,19 @@ interface FacadeEntry {
|
|
|
371
387
|
aggregate: (options: unknown) => Promise<unknown>;
|
|
372
388
|
count: (where?: unknown) => Promise<number>;
|
|
373
389
|
delete: (id: string) => Promise<void>;
|
|
374
|
-
deleteMany:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
390
|
+
deleteMany: {
|
|
391
|
+
(ids: ReadonlyArray<string>, options?: {
|
|
392
|
+
limit?: number;
|
|
393
|
+
}): Promise<{
|
|
394
|
+
deleted: number;
|
|
395
|
+
}>;
|
|
396
|
+
(args: {
|
|
397
|
+
limit?: number;
|
|
398
|
+
where: Record<string, unknown>;
|
|
399
|
+
}): Promise<{
|
|
400
|
+
deleted: number;
|
|
401
|
+
}>;
|
|
402
|
+
};
|
|
379
403
|
/** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
|
|
380
404
|
exists: (where?: unknown) => Promise<boolean>;
|
|
381
405
|
findFirst: (args?: unknown) => Promise<unknown>;
|
|
@@ -386,16 +410,34 @@ interface FacadeEntry {
|
|
|
386
410
|
/** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
|
|
387
411
|
hardDelete: (id: string) => Promise<void>;
|
|
388
412
|
insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
|
|
413
|
+
/**
|
|
414
|
+
* Insert many documents into this table in one call. With
|
|
415
|
+
* `{ skipDuplicates: true }`, UNIQUE breaches resolve to `null` for that row
|
|
416
|
+
* instead of failing the batch. The typed facade narrows the return to
|
|
417
|
+
* `Id<T>[]` when skipDuplicates is not requested.
|
|
418
|
+
*/
|
|
389
419
|
insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
390
420
|
limit?: number;
|
|
391
|
-
|
|
421
|
+
skipDuplicates?: boolean;
|
|
422
|
+
}) => Promise<(string | null)[]>;
|
|
392
423
|
patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
|
|
393
|
-
patchMany:
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
424
|
+
patchMany: {
|
|
425
|
+
(patches: ReadonlyArray<{
|
|
426
|
+
id: string;
|
|
427
|
+
values: Record<string, unknown>;
|
|
428
|
+
}>, options?: {
|
|
429
|
+
limit?: number;
|
|
430
|
+
}): Promise<{
|
|
431
|
+
patched: number;
|
|
432
|
+
}>;
|
|
433
|
+
(args: {
|
|
434
|
+
limit?: number;
|
|
435
|
+
values: Record<string, unknown>;
|
|
436
|
+
where: Record<string, unknown>;
|
|
437
|
+
}): Promise<{
|
|
438
|
+
patched: number;
|
|
439
|
+
}>;
|
|
440
|
+
};
|
|
399
441
|
rank: (indexName: string, options: unknown) => Promise<unknown>;
|
|
400
442
|
rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
|
|
401
443
|
replace: (id: string, document: Record<string, unknown>) => Promise<void>;
|
|
@@ -405,15 +447,16 @@ interface FacadeEntry {
|
|
|
405
447
|
upsert: (args: UpsertArgs) => Promise<UpsertResult>;
|
|
406
448
|
/** Sequential `upsert` over many rows sharing one `target`; returns one result per input row in order. */
|
|
407
449
|
upsertMany: (args: UpsertManyArgs) => Promise<UpsertResult[]>;
|
|
450
|
+
withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => unknown;
|
|
408
451
|
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
|
|
409
452
|
}
|
|
410
453
|
/** Options accepted by the per-table `insert` accessor. */
|
|
411
454
|
interface FacadeInsertOptions {
|
|
412
455
|
/**
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
456
|
+
* When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
|
|
457
|
+
* silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
|
|
458
|
+
* other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
|
|
459
|
+
*/
|
|
417
460
|
skipDuplicates?: boolean;
|
|
418
461
|
}
|
|
419
462
|
/** The conflict target for `upsert`/`upsertMany`: one field name or a tuple of them. */
|
|
@@ -441,17 +484,17 @@ interface UpsertManyArgs {
|
|
|
441
484
|
target: UpsertTarget;
|
|
442
485
|
}
|
|
443
486
|
/**
|
|
444
|
-
* Bind a structural writer to one table, producing its `ctx.db` table accessor.
|
|
445
|
-
*
|
|
446
|
-
* The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
|
|
447
|
-
* `tableName` as `expectedTable` so the underlying writer scopes its id lookup
|
|
448
|
-
* to this table. Without it, a branded `Id<"posts">` carrying another table's
|
|
449
|
-
* id would resolve cross-table (the writer probes every table by id), letting
|
|
450
|
-
* `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
|
|
451
|
-
* mutate — a row in an unrelated table (IDOR). Writers that ignore the second
|
|
452
|
-
* argument keep their previous global behaviour; the scoping is opt-in via this
|
|
453
|
-
* forwarded name.
|
|
454
|
-
*/
|
|
487
|
+
* Bind a structural writer to one table, producing its `ctx.db` table accessor.
|
|
488
|
+
*
|
|
489
|
+
* The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
|
|
490
|
+
* `tableName` as `expectedTable` so the underlying writer scopes its id lookup
|
|
491
|
+
* to this table. Without it, a branded `Id<"posts">` carrying another table's
|
|
492
|
+
* id would resolve cross-table (the writer probes every table by id), letting
|
|
493
|
+
* `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
|
|
494
|
+
* mutate — a row in an unrelated table (IDOR). Writers that ignore the second
|
|
495
|
+
* argument keep their previous global behaviour; the scoping is opt-in via this
|
|
496
|
+
* forwarded name.
|
|
497
|
+
*/
|
|
455
498
|
declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) => FacadeEntry;
|
|
456
499
|
/** The kitcn-style `ctx.orm` namespace over a per-table facade map. */
|
|
457
500
|
interface OrmLike {
|
|
@@ -472,21 +515,21 @@ declare const bindOrm: (facade: Record<string, FacadeEntry>) => OrmLike;
|
|
|
472
515
|
/** HTTP verbs the typed {@link httpRoute} builder can bind to. */
|
|
473
516
|
type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
|
|
474
517
|
/**
|
|
475
|
-
* Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
|
|
476
|
-
* HTTP actions run in the worker (the "action runtime"), separate from the
|
|
477
|
-
* transactional store, so there is no direct `db` / `vectors` / `scheduler` /
|
|
478
|
-
* `storage` surface — reach the data layer through `runQuery` / `runMutation` /
|
|
479
|
-
* `runAction`, which forward to the owning shard.
|
|
480
|
-
*/
|
|
481
|
-
type HttpActionCtx = Pick<ActionCtx, "auth" | "fetch" | "runAction" | "runMutation" | "runQuery">;
|
|
518
|
+
* Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
|
|
519
|
+
* HTTP actions run in the worker (the "action runtime"), separate from the
|
|
520
|
+
* transactional store, so there is no direct `db` / `vectors` / `scheduler` /
|
|
521
|
+
* `storage` surface — reach the data layer through `runQuery` / `runMutation` /
|
|
522
|
+
* `runAction`, which forward to the owning shard.
|
|
523
|
+
*/
|
|
524
|
+
type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery">;
|
|
482
525
|
/** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
|
|
483
526
|
type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
|
|
484
527
|
/**
|
|
485
|
-
* The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
|
|
486
|
-
* The runtime injects the per-request {@link HttpActionCtx} on the private
|
|
487
|
-
* `__lunoraCtx` binding; the router's lifting middleware promotes it to
|
|
488
|
-
* `c.var.lunora` so handlers can read it as a typed variable.
|
|
489
|
-
*/
|
|
528
|
+
* The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
|
|
529
|
+
* The runtime injects the per-request {@link HttpActionCtx} on the private
|
|
530
|
+
* `__lunoraCtx` binding; the router's lifting middleware promotes it to
|
|
531
|
+
* `c.var.lunora` so handlers can read it as a typed variable.
|
|
532
|
+
*/
|
|
490
533
|
interface LunoraHttpEnv {
|
|
491
534
|
Bindings: Record<string, unknown> & {
|
|
492
535
|
__lunoraCtx?: HttpActionCtx;
|
|
@@ -500,31 +543,31 @@ type LunoraHttpApp = Hono<LunoraHttpEnv>;
|
|
|
500
543
|
/** A compiled route handler: a hono handler that resolves to a raw {@link Response}. */
|
|
501
544
|
type LunoraRouteHandler = (c: Context<LunoraHttpEnv>) => Promise<Response>;
|
|
502
545
|
/**
|
|
503
|
-
* Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
|
|
504
|
-
* hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
|
|
505
|
-
* runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
|
|
506
|
-
* {@link httpRouter}; `request` is the underlying `c.req.raw`.
|
|
507
|
-
*/
|
|
546
|
+
* Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
|
|
547
|
+
* hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
|
|
548
|
+
* runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
|
|
549
|
+
* {@link httpRouter}; `request` is the underlying `c.req.raw`.
|
|
550
|
+
*/
|
|
508
551
|
declare const httpAction: (handler: HttpActionHandler) => LunoraRouteHandler;
|
|
509
552
|
/**
|
|
510
|
-
* Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
|
|
511
|
-
* the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
|
|
512
|
-
* {@link httpAction} and the typed {@link httpRoute} builder can read the action
|
|
513
|
-
* context. The full hono surface is available — plugins, path params, `.route`:
|
|
514
|
-
*
|
|
515
|
-
* ```ts
|
|
516
|
-
* const app = httpRouter();
|
|
517
|
-
* app.use("*", cors());
|
|
518
|
-
* app.post("/webhook", httpAction(onWebhook));
|
|
519
|
-
* app.get("/users/:id", getUser);
|
|
520
|
-
* export default createWorker({ httpRouter: app, ... });
|
|
521
|
-
* ```
|
|
522
|
-
*
|
|
523
|
-
* The lifting middleware throws if the context is absent. `createWorker` injects
|
|
524
|
-
* it on every request the router sees, so this only trips when the app is run
|
|
525
|
-
* outside the runtime — a misconfiguration we surface loudly rather than let
|
|
526
|
-
* `c.var.lunora` be silently `undefined` despite its non-optional type.
|
|
527
|
-
*/
|
|
553
|
+
* Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
|
|
554
|
+
* the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
|
|
555
|
+
* {@link httpAction} and the typed {@link httpRoute} builder can read the action
|
|
556
|
+
* context. The full hono surface is available — plugins, path params, `.route`:
|
|
557
|
+
*
|
|
558
|
+
* ```ts
|
|
559
|
+
* const app = httpRouter();
|
|
560
|
+
* app.use("*", cors());
|
|
561
|
+
* app.post("/webhook", httpAction(onWebhook));
|
|
562
|
+
* app.get("/users/:id", getUser);
|
|
563
|
+
* export default createWorker({ httpRouter: app, ... });
|
|
564
|
+
* ```
|
|
565
|
+
*
|
|
566
|
+
* The lifting middleware throws if the context is absent. `createWorker` injects
|
|
567
|
+
* it on every request the router sees, so this only trips when the app is run
|
|
568
|
+
* outside the runtime — a misconfiguration we surface loudly rather than let
|
|
569
|
+
* `c.var.lunora` be silently `undefined` despite its non-optional type.
|
|
570
|
+
*/
|
|
528
571
|
declare const httpRouter: () => LunoraHttpApp;
|
|
529
572
|
/** The `{ ctx, searchParams, body, params }` a typed route handler receives. */
|
|
530
573
|
interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator> {
|
|
@@ -534,11 +577,12 @@ interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body exten
|
|
|
534
577
|
searchParams: InferArgs<SearchParams>;
|
|
535
578
|
}
|
|
536
579
|
/**
|
|
537
|
-
* The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
|
|
538
|
-
* handler receives. There is no parsed `body` — streams are typically GET, and
|
|
539
|
-
* the raw `request` is exposed if a handler needs to read the body itself.
|
|
540
|
-
* `signal` is tripped when the client disconnects.
|
|
541
|
-
|
|
580
|
+
* The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
|
|
581
|
+
* handler receives. There is no parsed `body` — streams are typically GET, and
|
|
582
|
+
* the raw `request` is exposed if a handler needs to read the body itself.
|
|
583
|
+
* `signal` is tripped when the client disconnects.
|
|
584
|
+
* @experimental Part of the HTTP-SSE stream surface; reconnect/POST-body design questions are still open.
|
|
585
|
+
*/
|
|
542
586
|
interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
|
|
543
587
|
ctx: HttpActionCtx;
|
|
544
588
|
params: InferArgs<Params>;
|
|
@@ -547,35 +591,51 @@ interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params ex
|
|
|
547
591
|
signal: AbortSignal;
|
|
548
592
|
}
|
|
549
593
|
/**
|
|
550
|
-
* A typed REST route under construction. `.searchParams()` / `.body()` /
|
|
551
|
-
* `.params()` accumulate validator maps (later calls merge, a colliding key
|
|
552
|
-
* wins) that decode the URL query, JSON body, and hono path params into the
|
|
553
|
-
* handler's typed `searchParams` / `body` / `params`. Like the procedure
|
|
554
|
-
* builder, `.output(validator)` defaults to the `undefined` sentinel — while
|
|
555
|
-
* unset the handler is generic over its own return; once set the handler must
|
|
556
|
-
* return that type and the result is parsed through the validator before
|
|
557
|
-
* serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
|
|
558
|
-
* `Output` doesn't distribute and the test is for the exact sentinel.
|
|
559
|
-
*
|
|
560
|
-
* The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
|
|
561
|
-
* directly with `app.get(path, route)`.
|
|
562
|
-
*/
|
|
594
|
+
* A typed REST route under construction. `.searchParams()` / `.body()` /
|
|
595
|
+
* `.params()` accumulate validator maps (later calls merge, a colliding key
|
|
596
|
+
* wins) that decode the URL query, JSON body, and hono path params into the
|
|
597
|
+
* handler's typed `searchParams` / `body` / `params`. Like the procedure
|
|
598
|
+
* builder, `.output(validator)` defaults to the `undefined` sentinel — while
|
|
599
|
+
* unset the handler is generic over its own return; once set the handler must
|
|
600
|
+
* return that type and the result is parsed through the validator before
|
|
601
|
+
* serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
|
|
602
|
+
* `Output` doesn't distribute and the test is for the exact sentinel.
|
|
603
|
+
*
|
|
604
|
+
* The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
|
|
605
|
+
* directly with `app.get(path, route)`.
|
|
606
|
+
*/
|
|
563
607
|
interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
|
|
564
608
|
body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
|
|
609
|
+
/**
|
|
610
|
+
* Attach a `Cache-Control` header to the response. Only meaningful when
|
|
611
|
+
* Workers Cache is enabled in `wrangler.jsonc` (`"cache": { "enabled": true }`).
|
|
612
|
+
*/
|
|
613
|
+
cacheControl: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
|
|
614
|
+
/**
|
|
615
|
+
* Attach a `Cache-Tag` header to the response for tag-based purging via
|
|
616
|
+
* `ctx.cache.purge({ tags: [...] })`.
|
|
617
|
+
*/
|
|
618
|
+
cacheTag: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
|
|
565
619
|
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;
|
|
566
620
|
output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
|
|
567
621
|
params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
|
|
568
622
|
searchParams: <S extends ArgsValidator>(validators: S) => HttpRouteBuilder<S & SearchParams, Body, Params, Output>;
|
|
569
623
|
/**
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
624
|
+
* Terminal: declare this route as a streaming Server-Sent Events endpoint.
|
|
625
|
+
* The handler is an async generator (or any function returning an
|
|
626
|
+
* `AsyncIterable<R>`) that yields one chunk per SSE `data:` frame; on
|
|
627
|
+
* iterator completion the route writes a final `event: complete` frame; on
|
|
628
|
+
* throw, an `event: error` frame is written with `{code, message}` before
|
|
629
|
+
* the stream closes. The chunks are JSON-encoded; `R` is inferred from the
|
|
630
|
+
* handler's yielded type.
|
|
631
|
+
* @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
|
|
632
|
+
*/
|
|
578
633
|
stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
|
|
634
|
+
/**
|
|
635
|
+
* Attach a `Vary` header to the response so Cloudflare stores separate
|
|
636
|
+
* cached variants per distinct value of the listed request headers.
|
|
637
|
+
*/
|
|
638
|
+
vary: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
|
|
579
639
|
}
|
|
580
640
|
/** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
|
|
581
641
|
type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
|
|
@@ -590,33 +650,33 @@ interface HttpRoute {
|
|
|
590
650
|
put: HttpRouteFactory;
|
|
591
651
|
}
|
|
592
652
|
/**
|
|
593
|
-
* Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
|
|
594
|
-
* typed route and a hand-written {@link httpAction} are interchangeable when
|
|
595
|
-
* mounted on {@link httpRouter}:
|
|
596
|
-
*
|
|
597
|
-
* ```ts
|
|
598
|
-
* export const listTodos = httpRoute
|
|
599
|
-
* .get("/api/todos")
|
|
600
|
-
* .searchParams({ limit: v.number(), q: v.optional(v.string()) })
|
|
601
|
-
* .output(v.array(v.object({ id: v.string(), text: v.string() })))
|
|
602
|
-
* .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
|
|
603
|
-
*
|
|
604
|
-
* export const getTodo = httpRoute
|
|
605
|
-
* .get("/api/todos/:id")
|
|
606
|
-
* .params({ id: v.string() })
|
|
607
|
-
* .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
|
|
608
|
-
*
|
|
609
|
-
* const app = httpRouter();
|
|
610
|
-
* app.get("/api/todos", listTodos);
|
|
611
|
-
* app.get("/api/todos/:id", getTodo);
|
|
612
|
-
* ```
|
|
613
|
-
*/
|
|
653
|
+
* Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
|
|
654
|
+
* typed route and a hand-written {@link httpAction} are interchangeable when
|
|
655
|
+
* mounted on {@link httpRouter}:
|
|
656
|
+
*
|
|
657
|
+
* ```ts
|
|
658
|
+
* export const listTodos = httpRoute
|
|
659
|
+
* .get("/api/todos")
|
|
660
|
+
* .searchParams({ limit: v.number(), q: v.optional(v.string()) })
|
|
661
|
+
* .output(v.array(v.object({ id: v.string(), text: v.string() })))
|
|
662
|
+
* .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
|
|
663
|
+
*
|
|
664
|
+
* export const getTodo = httpRoute
|
|
665
|
+
* .get("/api/todos/:id")
|
|
666
|
+
* .params({ id: v.string() })
|
|
667
|
+
* .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
|
|
668
|
+
*
|
|
669
|
+
* const app = httpRouter();
|
|
670
|
+
* app.get("/api/todos", listTodos);
|
|
671
|
+
* app.get("/api/todos/:id", getTodo);
|
|
672
|
+
* ```
|
|
673
|
+
*/
|
|
614
674
|
declare const httpRoute: HttpRoute;
|
|
615
675
|
/**
|
|
616
|
-
* Structural view of an R2 object body, as returned by `@lunora/storage`'s
|
|
617
|
-
* `download()`. Re-declared here (not imported) so `@lunora/server` takes no
|
|
618
|
-
* runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
|
|
619
|
-
*/
|
|
676
|
+
* Structural view of an R2 object body, as returned by `@lunora/storage`'s
|
|
677
|
+
* `download()`. Re-declared here (not imported) so `@lunora/server` takes no
|
|
678
|
+
* runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
|
|
679
|
+
*/
|
|
620
680
|
interface StorageObjectBody {
|
|
621
681
|
/** The object body stream (`null` for a zero-byte object). */
|
|
622
682
|
body: ReadableStream | null;
|
|
@@ -647,21 +707,100 @@ interface ContextWithStorage {
|
|
|
647
707
|
storage: StorageDownloader;
|
|
648
708
|
}
|
|
649
709
|
/**
|
|
650
|
-
*
|
|
651
|
-
*
|
|
652
|
-
*
|
|
653
|
-
* `Content-
|
|
654
|
-
*
|
|
655
|
-
* `
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
*
|
|
660
|
-
*
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
|
|
710
|
+
* True when `value` is safe to use as an HTTP header field-value: no CR, LF, or
|
|
711
|
+
* NUL. Guards against response-header injection / `Headers`-construction throws
|
|
712
|
+
* when reflecting attacker-influenced object metadata (e.g. a stored
|
|
713
|
+
* `Content-Type`). Exported (see the `export {}` at the file end) so an `httpAction`
|
|
714
|
+
* handler can guard a request-derived header value before writing it — the fix the
|
|
715
|
+
* `http_action_response_header_injection` advisor lint points to.
|
|
716
|
+
*/
|
|
717
|
+
declare const isSafeHeaderValue: (value: string) => boolean;
|
|
718
|
+
/**
|
|
719
|
+
* Stream a stored object as an HTTP {@link Response} from an `httpAction`
|
|
720
|
+
* handler, with correct `Content-Type`, `ETag`, and `Accept-Ranges: bytes`.
|
|
721
|
+
* Honors a single-range `Range` request → **206 Partial Content** with
|
|
722
|
+
* `Content-Range` + `Content-Length`; otherwise **200**. A missing object is a
|
|
723
|
+
* **404**; an out-of-bounds range is a **416** with a `Content-Range` of
|
|
724
|
+
* `bytes` star-slash-size.
|
|
725
|
+
*
|
|
726
|
+
* A range request re-issues the `download()` with the resolved `{ offset, length }`
|
|
727
|
+
* window so R2 streams only those bytes back to the Worker — the slice is never
|
|
728
|
+
* buffered in the isolate. The first `download()` is used only for the object's
|
|
729
|
+
* size + metadata (its body is left unread and cancelled). For very large
|
|
730
|
+
* objects a signed URL (`ctx.storage.getSignedUrl`) is still cheaper since the
|
|
731
|
+
* client then ranges against R2/CDN directly with no Worker hop.
|
|
732
|
+
*/
|
|
664
733
|
declare const serveStorageObject: (context: ContextWithStorage, key: string, request: Request) => Promise<Response>;
|
|
734
|
+
/**
|
|
735
|
+
* What the worker does with a resolver's identity when it fails contract
|
|
736
|
+
* validation (a forged / malformed claim set arriving from an untrusted token).
|
|
737
|
+
* `"anonymous"` (default, safe) treats the request as anonymous, so the bad
|
|
738
|
+
* identity never reaches a policy as a valid identity (`ctx.auth.userId`
|
|
739
|
+
* becomes `undefined`). `"reject"` fails the request closed (a `401`) — use
|
|
740
|
+
* when a malformed credential should be a hard error, not a silent downgrade.
|
|
741
|
+
*/
|
|
742
|
+
type IdentityRejectMode = "anonymous" | "reject";
|
|
743
|
+
/** Options for {@link defineIdentity}. */
|
|
744
|
+
interface DefineIdentityOptions {
|
|
745
|
+
/**
|
|
746
|
+
* How to handle a resolver identity that violates the contract at the trust
|
|
747
|
+
* boundary. Defaults to `"anonymous"` (a forged claim set is downgraded to
|
|
748
|
+
* anonymous rather than flowing in as an unchecked cast).
|
|
749
|
+
*/
|
|
750
|
+
readonly onInvalid?: IdentityRejectMode;
|
|
751
|
+
}
|
|
752
|
+
/** Result of validating a candidate identity against the contract. */
|
|
753
|
+
type IdentityValidation = {
|
|
754
|
+
ok: true;
|
|
755
|
+
} | {
|
|
756
|
+
error: string;
|
|
757
|
+
ok: false;
|
|
758
|
+
};
|
|
759
|
+
/**
|
|
760
|
+
* A declared identity claim contract. Carries the codegen discovery brand, the
|
|
761
|
+
* declared claim validators, the reject policy, and a runtime `validate`. The
|
|
762
|
+
* `TClaims` type parameter is the inferred claim shape (always extending
|
|
763
|
+
* `{ userId: string }`); it is phantom (no runtime field) and exists so
|
|
764
|
+
* `@lunora/codegen` and {@link InferIdentity} can recover the type.
|
|
765
|
+
*/
|
|
766
|
+
interface IdentityContract<TClaims extends {
|
|
767
|
+
userId: string;
|
|
768
|
+
} = {
|
|
769
|
+
userId: string;
|
|
770
|
+
}> {
|
|
771
|
+
/**
|
|
772
|
+
* Phantom carrier for the inferred claim type. Never populated at runtime
|
|
773
|
+
* (`undefined`); present only so the type flows to codegen / {@link InferIdentity}.
|
|
774
|
+
*/
|
|
775
|
+
readonly __claimType?: TClaims;
|
|
776
|
+
readonly __lunoraIdentity: true;
|
|
777
|
+
/** The declared claim validators (a `@lunora/values` validator map). */
|
|
778
|
+
readonly claims: ValidatorMap;
|
|
779
|
+
/** Reject policy applied at the trust boundary. See {@link IdentityRejectMode}. */
|
|
780
|
+
readonly onInvalid: IdentityRejectMode;
|
|
781
|
+
/**
|
|
782
|
+
* Validate a resolver's returned identity against the declared claims. On
|
|
783
|
+
* success the caller keeps the original identity untouched (so undeclared
|
|
784
|
+
* claims are forwarded verbatim, preserving today's behaviour); on failure
|
|
785
|
+
* the worker applies the `onInvalid` policy.
|
|
786
|
+
*/
|
|
787
|
+
validate: (identity: Record<string, unknown>) => IdentityValidation;
|
|
788
|
+
}
|
|
789
|
+
/** Recover the declared claim type from a {@link defineIdentity} contract. */
|
|
790
|
+
type InferIdentity<T> = T extends IdentityContract<infer TClaims> ? TClaims : never;
|
|
791
|
+
/**
|
|
792
|
+
* Declare the identity claim contract. `claims` is a `@lunora/values` validator
|
|
793
|
+
* map whose inferred type must extend `{ userId: string }` — if it does not
|
|
794
|
+
* (e.g. `userId` is missing or not a required string), the argument type
|
|
795
|
+
* collapses to `never` and the call fails to typecheck.
|
|
796
|
+
* @example
|
|
797
|
+
* export const identity = defineIdentity({ userId: v.string(), tenantId: v.optional(v.string()), scopes: v.optional(v.array(v.string())) });
|
|
798
|
+
*/
|
|
799
|
+
declare const defineIdentity: <A extends ValidatorMap>(claims: InferValidatorMap<A> extends {
|
|
800
|
+
userId: string;
|
|
801
|
+
} ? A : never, options?: DefineIdentityOptions) => IdentityContract<InferValidatorMap<A> & {
|
|
802
|
+
userId: string;
|
|
803
|
+
}>;
|
|
665
804
|
/** Handler for a connection-lifecycle hook. */
|
|
666
805
|
type LifecycleHandler = (context: MutationCtx, event: LifecycleEvent) => Promise<void> | void;
|
|
667
806
|
/** Register a hook that fires once when a client's WebSocket connects. */
|
|
@@ -669,14 +808,14 @@ declare const onConnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
|
|
|
669
808
|
/** Register a hook that fires once when a client's WebSocket disconnects. */
|
|
670
809
|
declare const onDisconnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
|
|
671
810
|
/**
|
|
672
|
-
* Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
|
|
673
|
-
* `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
|
|
674
|
-
* resolver, same `can(...)` permission check — so an author can branch a mask
|
|
675
|
-
* on the caller's role/permission. `row` is the full pre-mask row the column
|
|
676
|
-
* belongs to; `column` is the column currently being masked. Both are absent
|
|
677
|
-
* when the context is used for the procedure-wide `bypass` check (no specific
|
|
678
|
-
* cell is in play yet).
|
|
679
|
-
*/
|
|
811
|
+
* Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
|
|
812
|
+
* `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
|
|
813
|
+
* resolver, same `can(...)` permission check — so an author can branch a mask
|
|
814
|
+
* on the caller's role/permission. `row` is the full pre-mask row the column
|
|
815
|
+
* belongs to; `column` is the column currently being masked. Both are absent
|
|
816
|
+
* when the context is used for the procedure-wide `bypass` check (no specific
|
|
817
|
+
* cell is in play yet).
|
|
818
|
+
*/
|
|
680
819
|
interface MaskContext<Context = unknown> {
|
|
681
820
|
readonly auth: {
|
|
682
821
|
/** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
|
|
@@ -692,50 +831,50 @@ interface MaskContext<Context = unknown> {
|
|
|
692
831
|
readonly row?: Record<string, unknown>;
|
|
693
832
|
}
|
|
694
833
|
/**
|
|
695
|
-
* A custom masking function. Receives the raw cell value and the
|
|
696
|
-
* {@link MaskContext}, returns the value to surface. Use it for partial masks
|
|
697
|
-
* (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
|
|
698
|
-
* or format-preserving tokens. A function that **throws** fails closed — the
|
|
699
|
-
* cell is redacted to `null`, never leaked raw.
|
|
700
|
-
*/
|
|
834
|
+
* A custom masking function. Receives the raw cell value and the
|
|
835
|
+
* {@link MaskContext}, returns the value to surface. Use it for partial masks
|
|
836
|
+
* (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
|
|
837
|
+
* or format-preserving tokens. A function that **throws** fails closed — the
|
|
838
|
+
* cell is redacted to `null`, never leaked raw.
|
|
839
|
+
*/
|
|
701
840
|
type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
|
|
702
841
|
/**
|
|
703
|
-
* How a column is masked:
|
|
704
|
-
*
|
|
705
|
-
* - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
|
|
706
|
-
* the right choice for any value that must actually be kept secret.
|
|
707
|
-
* - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
|
|
708
|
-
* same input always yields the same token (joinable/groupable client-side).
|
|
709
|
-
* **This is NOT a confidentiality control.** It is a non-cryptographic,
|
|
710
|
-
* unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
|
|
711
|
-
* phone numbers, SSNs) are brute-force-recoverable by the very caller you are
|
|
712
|
-
* masking from, and identical values always produce identical tokens across
|
|
713
|
-
* rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
|
|
714
|
-
* stable pseudonym for grouping/joining and leaking the value is acceptable —
|
|
715
|
-
* never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
|
|
716
|
-
* - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
|
|
717
|
-
*/
|
|
842
|
+
* How a column is masked:
|
|
843
|
+
*
|
|
844
|
+
* - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
|
|
845
|
+
* the right choice for any value that must actually be kept secret.
|
|
846
|
+
* - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
|
|
847
|
+
* same input always yields the same token (joinable/groupable client-side).
|
|
848
|
+
* **This is NOT a confidentiality control.** It is a non-cryptographic,
|
|
849
|
+
* unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
|
|
850
|
+
* phone numbers, SSNs) are brute-force-recoverable by the very caller you are
|
|
851
|
+
* masking from, and identical values always produce identical tokens across
|
|
852
|
+
* rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
|
|
853
|
+
* stable pseudonym for grouping/joining and leaking the value is acceptable —
|
|
854
|
+
* never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
|
|
855
|
+
* - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
|
|
856
|
+
*/
|
|
718
857
|
type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
|
|
719
858
|
/** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
|
|
720
859
|
type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
|
|
721
860
|
/**
|
|
722
|
-
* The mask declaration passed to `mask(...)`: a table → column → strategy map.
|
|
723
|
-
* Deliberately a plain object literal so the codegen feeder can statically read
|
|
724
|
-
* which columns a procedure masks (powering the `mask_uncovered_pii_column`
|
|
725
|
-
* advisor lint), exactly as the RLS feeder reads policy tables.
|
|
726
|
-
*/
|
|
861
|
+
* The mask declaration passed to `mask(...)`: a table → column → strategy map.
|
|
862
|
+
* Deliberately a plain object literal so the codegen feeder can statically read
|
|
863
|
+
* which columns a procedure masks (powering the `mask_uncovered_pii_column`
|
|
864
|
+
* advisor lint), exactly as the RLS feeder reads policy tables.
|
|
865
|
+
*/
|
|
727
866
|
type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
|
|
728
867
|
/**
|
|
729
|
-
* Options for `mask(policies, options)`.
|
|
730
|
-
*
|
|
731
|
-
* - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
|
|
732
|
-
* inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
|
|
733
|
-
* not listed grants no permissions (fails closed for unknown roles).
|
|
734
|
-
* - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
|
|
735
|
-
* mask is skipped (the caller sees raw values). Use it for a privileged
|
|
736
|
-
* viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
|
|
737
|
-
* branching every column when an entire class of caller should see clear data.
|
|
738
|
-
*/
|
|
868
|
+
* Options for `mask(policies, options)`.
|
|
869
|
+
*
|
|
870
|
+
* - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
|
|
871
|
+
* inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
|
|
872
|
+
* not listed grants no permissions (fails closed for unknown roles).
|
|
873
|
+
* - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
|
|
874
|
+
* mask is skipped (the caller sees raw values). Use it for a privileged
|
|
875
|
+
* viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
|
|
876
|
+
* branching every column when an entire class of caller should see clear data.
|
|
877
|
+
*/
|
|
739
878
|
interface MaskOptions<Context = unknown> {
|
|
740
879
|
readonly bypass?: (context: MaskContext<Context>) => boolean;
|
|
741
880
|
readonly roles?: ReadonlyArray<Role>;
|
|
@@ -749,12 +888,14 @@ interface QueryArgs$1 {
|
|
|
749
888
|
baseWhere?: unknown;
|
|
750
889
|
cursor?: null | string;
|
|
751
890
|
limit?: number;
|
|
891
|
+
orderBy?: ReadonlyArray<Record<string, unknown>>;
|
|
752
892
|
where?: unknown;
|
|
753
893
|
with?: Record<string, unknown>;
|
|
754
894
|
}
|
|
755
895
|
interface AggregateArgs$1 {
|
|
756
896
|
field?: string;
|
|
757
897
|
op: string;
|
|
898
|
+
where?: unknown;
|
|
758
899
|
}
|
|
759
900
|
interface GroupByArgs$1 {
|
|
760
901
|
agg?: {
|
|
@@ -762,6 +903,7 @@ interface GroupByArgs$1 {
|
|
|
762
903
|
op: string;
|
|
763
904
|
};
|
|
764
905
|
by: ReadonlyArray<string>;
|
|
906
|
+
where?: unknown;
|
|
765
907
|
}
|
|
766
908
|
interface TableReaderLike$1 {
|
|
767
909
|
collect: () => Promise<Record<string, unknown>[]>;
|
|
@@ -774,15 +916,16 @@ interface TableReaderLike$1 {
|
|
|
774
916
|
}) => Promise<QueryPage$1>;
|
|
775
917
|
take: (limit: number) => Promise<Record<string, unknown>[]>;
|
|
776
918
|
unique: () => Promise<Record<string, unknown> | null>;
|
|
919
|
+
withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike$1;
|
|
777
920
|
withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
|
|
778
921
|
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
|
|
779
922
|
}
|
|
780
923
|
/**
|
|
781
|
-
* Structural projection of the runtime ORM writer — the same subset
|
|
782
|
-
* `../rls/middleware` mirrors, so the wrapper is interchangeable between
|
|
783
|
-
* `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
|
|
784
|
-
* inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
|
|
785
|
-
*/
|
|
924
|
+
* Structural projection of the runtime ORM writer — the same subset
|
|
925
|
+
* `../rls/middleware` mirrors, so the wrapper is interchangeable between
|
|
926
|
+
* `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
|
|
927
|
+
* inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
|
|
928
|
+
*/
|
|
786
929
|
interface MaskDatabase {
|
|
787
930
|
aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
|
|
788
931
|
count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
|
|
@@ -792,6 +935,11 @@ interface MaskDatabase {
|
|
|
792
935
|
}) => Promise<{
|
|
793
936
|
deleted: number;
|
|
794
937
|
}>;
|
|
938
|
+
deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
|
|
939
|
+
limit?: number;
|
|
940
|
+
}) => Promise<{
|
|
941
|
+
deleted: number;
|
|
942
|
+
}>;
|
|
795
943
|
findFirst: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown> | null>;
|
|
796
944
|
findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
|
|
797
945
|
findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
|
|
@@ -803,7 +951,8 @@ interface MaskDatabase {
|
|
|
803
951
|
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
804
952
|
insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
805
953
|
limit?: number;
|
|
806
|
-
|
|
954
|
+
skipDuplicates?: boolean;
|
|
955
|
+
}) => Promise<(string | null)[]>;
|
|
807
956
|
lookupById?: (id: string, expectedTable?: string) => Promise<null | {
|
|
808
957
|
row: Record<string, unknown>;
|
|
809
958
|
tableName: string;
|
|
@@ -814,7 +963,17 @@ interface MaskDatabase {
|
|
|
814
963
|
patch: Record<string, unknown>;
|
|
815
964
|
}>, options?: {
|
|
816
965
|
limit?: number;
|
|
817
|
-
}) => Promise<
|
|
966
|
+
}) => Promise<{
|
|
967
|
+
patched: number;
|
|
968
|
+
}>;
|
|
969
|
+
patchWhere?: (tableName: string, args: {
|
|
970
|
+
patch: Record<string, unknown>;
|
|
971
|
+
where: Record<string, unknown>;
|
|
972
|
+
}, options?: {
|
|
973
|
+
limit?: number;
|
|
974
|
+
}) => Promise<{
|
|
975
|
+
patched: number;
|
|
976
|
+
}>;
|
|
818
977
|
query: (tableName: string) => TableReaderLike$1;
|
|
819
978
|
rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
|
|
820
979
|
position: number;
|
|
@@ -838,37 +997,22 @@ interface MaskContextIn {
|
|
|
838
997
|
db: MaskDatabase;
|
|
839
998
|
}
|
|
840
999
|
/**
|
|
841
|
-
* Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
|
|
842
|
-
* Closes over the policy map at builder-construction time; resolves identity +
|
|
843
|
-
* the `bypass` decision per call against the live ctx.
|
|
844
|
-
*
|
|
845
|
-
* IMPORTANT: a mask is in scope only for procedures whose builder chain
|
|
846
|
-
* includes this middleware — opt-in, never global (the same invariant as RLS).
|
|
847
|
-
*/
|
|
1000
|
+
* Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
|
|
1001
|
+
* Closes over the policy map at builder-construction time; resolves identity +
|
|
1002
|
+
* the `bypass` decision per call against the live ctx.
|
|
1003
|
+
*
|
|
1004
|
+
* IMPORTANT: a mask is in scope only for procedures whose builder chain
|
|
1005
|
+
* includes this middleware — opt-in, never global (the same invariant as RLS).
|
|
1006
|
+
*/
|
|
848
1007
|
declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
|
|
849
|
-
/**
|
|
850
|
-
* Online data-migration authoring API.
|
|
851
|
-
*
|
|
852
|
-
* `defineMigration` declares a per-document backfill over one table: `up`
|
|
853
|
-
* transforms every existing row, `down` (optional) reverses it. Unlike the D1
|
|
854
|
-
* SQL schema migrations in `@lunora/d1`, these run *inside each shard's*
|
|
855
|
-
* Durable Object against live documents, in keyset batches, and are resumable —
|
|
856
|
-
* the per-shard runner in `@lunora/do` tracks progress in a reserved
|
|
857
|
-
* `__lunora_migrations` table so an interrupted run picks up where it stopped.
|
|
858
|
-
*
|
|
859
|
-
* The returned object carries a `__lunoraMigration` brand so codegen can
|
|
860
|
-
* discover declarations through the type checker (mirroring the procedure
|
|
861
|
-
* builder's `__lunoraProcedure` brand) and emit them into a `LUNORA_MIGRATIONS`
|
|
862
|
-
* registry the DO and CLI look migrations up by id.
|
|
863
|
-
*/
|
|
864
1008
|
/** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
|
|
865
1009
|
type MigrationDocument = Record<string, unknown>;
|
|
866
1010
|
/**
|
|
867
|
-
* Transform applied to one document. Return a new document to rewrite the row,
|
|
868
|
-
* or `undefined` to leave it untouched (skipped, not counted as changed). The
|
|
869
|
-
* runner always preserves the original `_id` and `_creationTime`, so the
|
|
870
|
-
* returned document neither needs to nor should change row identity.
|
|
871
|
-
*/
|
|
1011
|
+
* Transform applied to one document. Return a new document to rewrite the row,
|
|
1012
|
+
* or `undefined` to leave it untouched (skipped, not counted as changed). The
|
|
1013
|
+
* runner always preserves the original `_id` and `_creationTime`, so the
|
|
1014
|
+
* returned document neither needs to nor should change row identity.
|
|
1015
|
+
*/
|
|
872
1016
|
type MigrationTransform = (document: MigrationDocument) => MigrationDocument | undefined | void;
|
|
873
1017
|
interface MigrationDefinition {
|
|
874
1018
|
/** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
|
|
@@ -889,83 +1033,132 @@ interface RegisteredMigration extends MigrationDefinition {
|
|
|
889
1033
|
/** Declare an online data migration. See the module docs for runtime semantics. */
|
|
890
1034
|
declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
|
|
891
1035
|
/**
|
|
892
|
-
*
|
|
893
|
-
*
|
|
894
|
-
*
|
|
895
|
-
|
|
1036
|
+
* A mutator declaration. `server` is authoritative; `client` is the optimistic
|
|
1037
|
+
* twin (optional — omit it to let the optimistic write fall through to the
|
|
1038
|
+
* server round-trip with no local preview). Both receive the same validated
|
|
1039
|
+
* `args`.
|
|
1040
|
+
*/
|
|
1041
|
+
interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
|
|
1042
|
+
/**
|
|
1043
|
+
* Validator for the mutator's arguments. Validated on the DO before `server`
|
|
1044
|
+
* runs and (when present) on the client before `client` runs, so both impls
|
|
1045
|
+
* see the same parsed shape. Omit for a parameterless mutator.
|
|
1046
|
+
*/
|
|
1047
|
+
readonly args?: Args;
|
|
1048
|
+
/**
|
|
1049
|
+
* Optimistic client implementation. Runs in a TanStack DB transaction
|
|
1050
|
+
* against the local collections; its writes are applied immediately and
|
|
1051
|
+
* automatically rolled back / rebased as the authoritative result syncs
|
|
1052
|
+
* back. Pure and side-effect-free beyond the local store. Omit to skip the
|
|
1053
|
+
* local preview.
|
|
1054
|
+
*/
|
|
1055
|
+
readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
|
|
1056
|
+
/**
|
|
1057
|
+
* Authoritative server implementation. Runs inside the shard DO with a full
|
|
1058
|
+
* {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
|
|
1059
|
+
* and poke back to subscribers. This is the source of truth — the client
|
|
1060
|
+
* impl is only a prediction of it.
|
|
1061
|
+
*/
|
|
1062
|
+
readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
|
|
1063
|
+
}
|
|
1064
|
+
/**
|
|
1065
|
+
* A {@link MutatorDefinition} plus the codegen discovery marker and a
|
|
1066
|
+
* dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
|
|
1067
|
+
* invokes a mutator exactly like a registered procedure.
|
|
1068
|
+
*/
|
|
1069
|
+
interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
|
|
1070
|
+
readonly __lunoraMutator: true;
|
|
1071
|
+
/** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
|
|
1072
|
+
readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
|
|
1073
|
+
/**
|
|
1074
|
+
* Marks the dispatch kind so codegen can register the mutator in the same
|
|
1075
|
+
* `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
|
|
1076
|
+
* reads `kind === "mutation"` to wrap the authoritative `server` impl in the
|
|
1077
|
+
* shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
|
|
1078
|
+
* ordinary `mutation`.
|
|
1079
|
+
*/
|
|
1080
|
+
readonly kind: "mutation";
|
|
1081
|
+
}
|
|
1082
|
+
/** Declare a custom mutator. See the module docs for runtime semantics. */
|
|
1083
|
+
declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
|
|
1084
|
+
/**
|
|
1085
|
+
* The prefixed tables a single plugin `P` contributes, or an empty map when it
|
|
1086
|
+
* ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
|
|
1087
|
+
* so {@link InstalledTables} can fold a tuple of plugins.
|
|
1088
|
+
*/
|
|
896
1089
|
type ExtensionTablesOf<P> = P extends {
|
|
897
1090
|
readonly extension: SchemaExtension<infer X> & {
|
|
898
1091
|
readonly key: infer K;
|
|
899
1092
|
};
|
|
900
1093
|
} ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
|
|
901
1094
|
/**
|
|
902
|
-
* Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
|
|
903
|
-
* auto-prefixed extension tables left-to-right — the type-level mirror of
|
|
904
|
-
* {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
|
|
905
|
-
*/
|
|
1095
|
+
* Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
|
|
1096
|
+
* auto-prefixed extension tables left-to-right — the type-level mirror of
|
|
1097
|
+
* {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
|
|
1098
|
+
*/
|
|
906
1099
|
type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
|
|
907
1100
|
/**
|
|
908
|
-
* Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
|
|
909
|
-
* `ctx.api.<key>` additions {@link composePluginMiddleware} accumulates as each
|
|
910
|
-
* plugin middleware runs. Independent of the incoming context, which the builder
|
|
911
|
-
* infers at the `.use(...)` site.
|
|
912
|
-
*/
|
|
1101
|
+
* Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
|
|
1102
|
+
* `ctx.api.<key>` additions {@link composePluginMiddleware} accumulates as each
|
|
1103
|
+
* plugin middleware runs. Independent of the incoming context, which the builder
|
|
1104
|
+
* infers at the `.use(...)` site.
|
|
1105
|
+
*/
|
|
913
1106
|
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;
|
|
914
1107
|
/**
|
|
915
|
-
* Schema fragment a plugin contributes. Same shape as the `tables` map
|
|
916
|
-
* passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
|
|
917
|
-
* `defineSchema` argument so a plugin can ship vector decls alongside its
|
|
918
|
-
* tables.
|
|
919
|
-
*/
|
|
1108
|
+
* Schema fragment a plugin contributes. Same shape as the `tables` map
|
|
1109
|
+
* passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
|
|
1110
|
+
* `defineSchema` argument so a plugin can ship vector decls alongside its
|
|
1111
|
+
* tables.
|
|
1112
|
+
*/
|
|
920
1113
|
interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
|
|
921
1114
|
/** Stable key identifying the plugin that owns this extension. */
|
|
922
1115
|
readonly key: string;
|
|
923
1116
|
/**
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1117
|
+
* Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
|
|
1118
|
+
* each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
|
|
1119
|
+
* collide with an app table; do **not** namespace manually.
|
|
1120
|
+
*/
|
|
928
1121
|
readonly tables: T;
|
|
929
1122
|
/**
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1123
|
+
* Optional standalone vector indexes the plugin ships, keyed by index
|
|
1124
|
+
* name. Merged into the host schema's `vectorIndexes`; a key collision
|
|
1125
|
+
* with the base schema is a hard error (same policy as tables).
|
|
1126
|
+
*/
|
|
934
1127
|
readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
|
|
935
1128
|
}
|
|
936
1129
|
/**
|
|
937
|
-
* Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
|
|
938
|
-
* error messages on collision) and a type-level brand.
|
|
939
|
-
*/
|
|
1130
|
+
* Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
|
|
1131
|
+
* error messages on collision) and a type-level brand.
|
|
1132
|
+
*/
|
|
940
1133
|
declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
|
|
941
1134
|
tables: T;
|
|
942
1135
|
vectorIndexes?: Record<string, VectorIndexDefinition>;
|
|
943
1136
|
}) => SchemaExtension<T>;
|
|
944
1137
|
/**
|
|
945
|
-
* A plugin packages an optional schema extension and optional middleware.
|
|
946
|
-
* Both are independently usable: an app can install only the schema (e.g.
|
|
947
|
-
* for plugins that ship background workers but no per-request behavior)
|
|
948
|
-
* or only the middleware (plugins that augment ctx without persistent
|
|
949
|
-
* state).
|
|
950
|
-
*/
|
|
1138
|
+
* A plugin packages an optional schema extension and optional middleware.
|
|
1139
|
+
* Both are independently usable: an app can install only the schema (e.g.
|
|
1140
|
+
* for plugins that ship background workers but no per-request behavior)
|
|
1141
|
+
* or only the middleware (plugins that augment ctx without persistent
|
|
1142
|
+
* state).
|
|
1143
|
+
*/
|
|
951
1144
|
interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
|
|
952
1145
|
/**
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
1146
|
+
* Optional schema extension. Apps install via
|
|
1147
|
+
* `defineSchema(...).extend(plugin.extension)`.
|
|
1148
|
+
*/
|
|
956
1149
|
readonly extension?: SchemaExtension<TExtension>;
|
|
957
1150
|
/** Stable key identifying the plugin. Matches `extension.key` when set. */
|
|
958
1151
|
readonly key: string;
|
|
959
1152
|
/**
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
1153
|
+
* Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
|
|
1154
|
+
* The middleware can extend `ctx`; convention is to attach helpers under
|
|
1155
|
+
* `ctx.api.<key>`, e.g.
|
|
1156
|
+
*
|
|
1157
|
+
* ```ts
|
|
1158
|
+
* middleware: ({ ctx, next }) =>
|
|
1159
|
+
* next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
|
|
1160
|
+
* ```
|
|
1161
|
+
*/
|
|
969
1162
|
readonly middleware?: Middleware<TContextIn, TContextOut>;
|
|
970
1163
|
}
|
|
971
1164
|
/** Options to {@link definePlugin}. */
|
|
@@ -974,35 +1167,51 @@ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>
|
|
|
974
1167
|
middleware?: Middleware<TContextIn, TContextOut>;
|
|
975
1168
|
}
|
|
976
1169
|
/**
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
*
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
1170
|
+
* Call signatures for {@link definePlugin}. When `extension` is supplied the
|
|
1171
|
+
* returned plugin's `extension` is typed as PRESENT (not `?`), so the
|
|
1172
|
+
* canonical install pattern `defineSchema(...).extend(plugin.extension)`
|
|
1173
|
+
* typechecks without a non-null assertion — the shape every scaffold template
|
|
1174
|
+
* ships. The bare-options signature keeps `extension` optional for plugins
|
|
1175
|
+
* that carry only middleware.
|
|
1176
|
+
*/
|
|
1177
|
+
interface DefinePluginFunction {
|
|
1178
|
+
<TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
|
|
1179
|
+
extension: SchemaExtension<TExtension>;
|
|
1180
|
+
}): Plugin<TExtension, TContextIn, TContextOut> & {
|
|
1181
|
+
readonly extension: SchemaExtension<TExtension>;
|
|
1182
|
+
};
|
|
1183
|
+
<TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
|
|
1184
|
+
}
|
|
1185
|
+
/**
|
|
1186
|
+
* Package a schema extension + middleware as a reusable plugin. Either
|
|
1187
|
+
* field is optional — `definePlugin("foo", {})` is valid but degenerate.
|
|
1188
|
+
*/
|
|
1189
|
+
declare const definePlugin: DefinePluginFunction;
|
|
1190
|
+
/**
|
|
1191
|
+
* Bundle of registered functions a {@link Component} ships. Keys are the
|
|
1192
|
+
* function's local name (e.g. `check`, `reset`); the registered function
|
|
1193
|
+
* value carries its own kind / args / handler.
|
|
1194
|
+
*
|
|
1195
|
+
* Users re-export from their own lunora module so codegen picks them up:
|
|
1196
|
+
*
|
|
1197
|
+
* ```ts
|
|
1198
|
+
* // lunora/ratelimit.ts
|
|
1199
|
+
* import { ratelimit } from "@vendor/ratelimit-component";
|
|
1200
|
+
* export const { check, reset } = ratelimit.functions;
|
|
1201
|
+
* // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
|
|
1202
|
+
* ```
|
|
1203
|
+
*
|
|
1204
|
+
* Codegen follows the re-export back to the bundled `query/mutation/action`
|
|
1205
|
+
* call (property access or destructuring both work), so the functions land in
|
|
1206
|
+
* the generated `api` under the re-exporting file's namespace.
|
|
1207
|
+
*/
|
|
999
1208
|
type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
|
|
1000
1209
|
/**
|
|
1001
|
-
* Component = {@link Plugin} with a bundle of registered functions. The
|
|
1002
|
-
* extension + middleware + functions are independent: a component can ship
|
|
1003
|
-
* functions without a schema (e.g. a stateless utility), or a schema
|
|
1004
|
-
* without functions (e.g. shared table definitions), and any combination.
|
|
1005
|
-
*/
|
|
1210
|
+
* Component = {@link Plugin} with a bundle of registered functions. The
|
|
1211
|
+
* extension + middleware + functions are independent: a component can ship
|
|
1212
|
+
* functions without a schema (e.g. a stateless utility), or a schema
|
|
1213
|
+
* without functions (e.g. shared table definitions), and any combination.
|
|
1214
|
+
*/
|
|
1006
1215
|
interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
|
|
1007
1216
|
readonly functions: F;
|
|
1008
1217
|
}
|
|
@@ -1011,81 +1220,81 @@ interface DefineComponentOptions<TExtension extends Record<string, TableDefiniti
|
|
|
1011
1220
|
functions?: F;
|
|
1012
1221
|
}
|
|
1013
1222
|
/**
|
|
1014
|
-
* Convenience wrapper around {@link definePlugin} that also bundles a set
|
|
1015
|
-
* of registered functions. The resulting `component.functions` object is a
|
|
1016
|
-
* record of `name → registered query/mutation/action`; consumers
|
|
1017
|
-
* re-export entries so codegen discovers them as user functions:
|
|
1018
|
-
*
|
|
1019
|
-
* ```ts
|
|
1020
|
-
* export const ratelimit = defineComponent("ratelimit", {
|
|
1021
|
-
* // Bare `buckets` merges in as `ratelimit_buckets`.
|
|
1022
|
-
* extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
|
|
1023
|
-
* middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
|
|
1024
|
-
* functions: {
|
|
1025
|
-
* check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
|
|
1026
|
-
* reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
|
|
1027
|
-
* },
|
|
1028
|
-
* });
|
|
1029
|
-
* ```
|
|
1030
|
-
*
|
|
1031
|
-
* Re-exporting an entry (by property access or destructuring) is enough for
|
|
1032
|
-
* codegen to discover it in the host app's namespace — the discovery resolver
|
|
1033
|
-
* chases the re-export back to the bundled registration call.
|
|
1034
|
-
*/
|
|
1223
|
+
* Convenience wrapper around {@link definePlugin} that also bundles a set
|
|
1224
|
+
* of registered functions. The resulting `component.functions` object is a
|
|
1225
|
+
* record of `name → registered query/mutation/action`; consumers
|
|
1226
|
+
* re-export entries so codegen discovers them as user functions:
|
|
1227
|
+
*
|
|
1228
|
+
* ```ts
|
|
1229
|
+
* export const ratelimit = defineComponent("ratelimit", {
|
|
1230
|
+
* // Bare `buckets` merges in as `ratelimit_buckets`.
|
|
1231
|
+
* extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
|
|
1232
|
+
* middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
|
|
1233
|
+
* functions: {
|
|
1234
|
+
* check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
|
|
1235
|
+
* reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
|
|
1236
|
+
* },
|
|
1237
|
+
* });
|
|
1238
|
+
* ```
|
|
1239
|
+
*
|
|
1240
|
+
* Re-exporting an entry (by property access or destructuring) is enough for
|
|
1241
|
+
* codegen to discover it in the host app's namespace — the discovery resolver
|
|
1242
|
+
* chases the re-export back to the bundled registration call.
|
|
1243
|
+
*/
|
|
1035
1244
|
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>;
|
|
1036
1245
|
/**
|
|
1037
|
-
* Map every key `K` of an extension's table map `X` to its auto-prefixed name
|
|
1038
|
-
* `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
|
|
1039
|
-
* so the typed `.extend(...)` chain reflects the real merged table names.
|
|
1040
|
-
*/
|
|
1041
|
-
type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K] };
|
|
1042
|
-
/**
|
|
1043
|
-
* Merge a {@link SchemaExtension} into an existing schema. Returns a new
|
|
1044
|
-
* schema object — never mutates the input.
|
|
1045
|
-
*
|
|
1046
|
-
* Extension tables are auto-namespaced: each bare table name is prefixed with
|
|
1047
|
-
* the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
|
|
1048
|
-
* style, and every intra-extension reference (relation targets, aggregate /
|
|
1049
|
-
* rank index `on`, standalone vector index `table`) is rewritten to match.
|
|
1050
|
-
* References to base/app tables are left untouched.
|
|
1051
|
-
*
|
|
1052
|
-
* Because each extension lives in its own `key` namespace, app↔component
|
|
1053
|
-
* collisions are impossible. The only remaining hard error is two extensions
|
|
1054
|
-
* sharing the same `key` and producing the same prefixed table (or vector
|
|
1055
|
-
* index) name — silent shadow would let one plugin hijack another's data.
|
|
1056
|
-
*/
|
|
1246
|
+
* Map every key `K` of an extension's table map `X` to its auto-prefixed name
|
|
1247
|
+
* `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
|
|
1248
|
+
* so the typed `.extend(...)` chain reflects the real merged table names.
|
|
1249
|
+
*/
|
|
1250
|
+
type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K]; };
|
|
1251
|
+
/**
|
|
1252
|
+
* Merge a {@link SchemaExtension} into an existing schema. Returns a new
|
|
1253
|
+
* schema object — never mutates the input.
|
|
1254
|
+
*
|
|
1255
|
+
* Extension tables are auto-namespaced: each bare table name is prefixed with
|
|
1256
|
+
* the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
|
|
1257
|
+
* style, and every intra-extension reference (relation targets, aggregate /
|
|
1258
|
+
* rank index `on`, standalone vector index `table`) is rewritten to match.
|
|
1259
|
+
* References to base/app tables are left untouched.
|
|
1260
|
+
*
|
|
1261
|
+
* Because each extension lives in its own `key` namespace, app↔component
|
|
1262
|
+
* collisions are impossible. The only remaining hard error is two extensions
|
|
1263
|
+
* sharing the same `key` and producing the same prefixed table (or vector
|
|
1264
|
+
* index) name — silent shadow would let one plugin hijack another's data.
|
|
1265
|
+
*/
|
|
1057
1266
|
declare const mergeSchemaExtension: <T extends Record<string, TableDefinition>, X extends Record<string, TableDefinition>, Key extends string = string>(base: Schema<T>, extension: SchemaExtension<X> & {
|
|
1058
1267
|
readonly key: Key;
|
|
1059
1268
|
}) => Schema<PrefixedTables<X, Key> & T>;
|
|
1060
1269
|
/**
|
|
1061
|
-
* Install several plugins' schema extensions in one call — the one-shot
|
|
1062
|
-
* counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
|
|
1063
|
-
* without an `extension` (middleware-only) are skipped; tables from those that
|
|
1064
|
-
* do are auto-prefixed and reference-rewritten exactly as
|
|
1065
|
-
* {@link mergeSchemaExtension} does for a single `.extend(...)`.
|
|
1066
|
-
*
|
|
1067
|
-
* ```ts
|
|
1068
|
-
* const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
|
|
1069
|
-
* // → todos + ratelimit_* + audit_*
|
|
1070
|
-
* ```
|
|
1071
|
-
*
|
|
1072
|
-
* Pair it with {@link composePluginMiddleware} to attach every plugin's
|
|
1073
|
-
* middleware in a single `.use(...)`, so installing N plugins is two calls
|
|
1074
|
-
* rather than N `.extend(...)` + N `.use(...)`.
|
|
1075
|
-
*/
|
|
1270
|
+
* Install several plugins' schema extensions in one call — the one-shot
|
|
1271
|
+
* counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
|
|
1272
|
+
* without an `extension` (middleware-only) are skipped; tables from those that
|
|
1273
|
+
* do are auto-prefixed and reference-rewritten exactly as
|
|
1274
|
+
* {@link mergeSchemaExtension} does for a single `.extend(...)`.
|
|
1275
|
+
*
|
|
1276
|
+
* ```ts
|
|
1277
|
+
* const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
|
|
1278
|
+
* // → todos + ratelimit_* + audit_*
|
|
1279
|
+
* ```
|
|
1280
|
+
*
|
|
1281
|
+
* Pair it with {@link composePluginMiddleware} to attach every plugin's
|
|
1282
|
+
* middleware in a single `.use(...)`, so installing N plugins is two calls
|
|
1283
|
+
* rather than N `.extend(...)` + N `.use(...)`.
|
|
1284
|
+
*/
|
|
1076
1285
|
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>>;
|
|
1077
1286
|
/**
|
|
1078
|
-
* Compose every plugin's middleware into a single middleware you attach with one
|
|
1079
|
-
* `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
|
|
1080
|
-
* in array order, each seeing the context the previous one widened, so the final
|
|
1081
|
-
* `next({ ctx })` the builder receives carries every plugin's `ctx.api.<key>`
|
|
1082
|
-
* additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
|
|
1083
|
-
* value, the middleware sibling of {@link installPlugins}.
|
|
1084
|
-
*
|
|
1085
|
-
* `ContextIn` is left free so the builder infers it from the context at the
|
|
1086
|
-
* `.use(...)` site; the result type widens it by the union of the plugins'
|
|
1087
|
-
* outputs.
|
|
1088
|
-
*/
|
|
1287
|
+
* Compose every plugin's middleware into a single middleware you attach with one
|
|
1288
|
+
* `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
|
|
1289
|
+
* in array order, each seeing the context the previous one widened, so the final
|
|
1290
|
+
* `next({ ctx })` the builder receives carries every plugin's `ctx.api.<key>`
|
|
1291
|
+
* additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
|
|
1292
|
+
* value, the middleware sibling of {@link installPlugins}.
|
|
1293
|
+
*
|
|
1294
|
+
* `ContextIn` is left free so the builder infers it from the context at the
|
|
1295
|
+
* `.use(...)` site; the result type widens it by the union of the plugins'
|
|
1296
|
+
* outputs.
|
|
1297
|
+
*/
|
|
1089
1298
|
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>;
|
|
1090
1299
|
/** Options for `.vectorize(field, opts)` (DSL Shape A). */
|
|
1091
1300
|
interface VectorizeOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
@@ -1122,10 +1331,10 @@ interface RelationBuilder {
|
|
|
1122
1331
|
}) => OneRelation<Target>;
|
|
1123
1332
|
}
|
|
1124
1333
|
/**
|
|
1125
|
-
* Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
|
|
1126
|
-
* `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
|
|
1127
|
-
* `COUNT(*) GROUP BY userId` accelerator.
|
|
1128
|
-
*/
|
|
1334
|
+
* Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
|
|
1335
|
+
* `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
|
|
1336
|
+
* `COUNT(*) GROUP BY userId` accelerator.
|
|
1337
|
+
*/
|
|
1129
1338
|
interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1130
1339
|
/** Group keys; counter rows are one per distinct tuple. Omitted = single-row aggregate over the whole table. */
|
|
1131
1340
|
by?: ReadonlyArray<keyof Shape & string>;
|
|
@@ -1137,11 +1346,11 @@ interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> =
|
|
|
1137
1346
|
where?: Record<string, unknown>;
|
|
1138
1347
|
}
|
|
1139
1348
|
/**
|
|
1140
|
-
* Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
|
|
1141
|
-
* accepts either an array of `{ field, direction }` keys, or the shorthand
|
|
1142
|
-
* `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
|
|
1143
|
-
* rank — omitted ⇒ one global rank over the whole table.
|
|
1144
|
-
*/
|
|
1349
|
+
* Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
|
|
1350
|
+
* accepts either an array of `{ field, direction }` keys, or the shorthand
|
|
1351
|
+
* `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
|
|
1352
|
+
* rank — omitted ⇒ one global rank over the whole table.
|
|
1353
|
+
*/
|
|
1145
1354
|
interface InlineRankIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1146
1355
|
/** Columns that scope each ranking; omitted ⇒ one global rank. */
|
|
1147
1356
|
partitionBy?: ReadonlyArray<keyof Shape & string>;
|
|
@@ -1157,19 +1366,29 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
|
|
|
1157
1366
|
/** Declare an aggregate (counter/sum/…) maintained by triggers for O(1) reads. */
|
|
1158
1367
|
aggregateIndex: (name: string, options?: InlineAggregateIndexOptions<Shape>) => TableBuilder<Shape>;
|
|
1159
1368
|
/**
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1369
|
+
* Mark this table as written outside Lunora's discoverable insert path —
|
|
1370
|
+
* by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
|
|
1371
|
+
* better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
|
|
1372
|
+
* (`table_without_insert`) then skip it instead of flagging the absent
|
|
1373
|
+
* `ctx.db.insert(...)`.
|
|
1374
|
+
*/
|
|
1166
1375
|
externallyManaged: () => TableBuilder<Shape>;
|
|
1167
1376
|
/**
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1377
|
+
* Declare a geospatial index over a `v.geoPoint()` column. The runtime keeps
|
|
1378
|
+
* a geohash companion so `withGeoIndex(name, q => q.near(point, radius))` and
|
|
1379
|
+
* `.within(bbox)` resolve as a geohash-prefix range scan + Haversine
|
|
1380
|
+
* refine/sort. `options.precision` tunes the geohash length (default 9).
|
|
1381
|
+
*/
|
|
1382
|
+
geoIndex: (name: string, options: {
|
|
1383
|
+
field: keyof Shape & string;
|
|
1384
|
+
precision?: number;
|
|
1385
|
+
}) => TableBuilder<Shape>;
|
|
1386
|
+
/**
|
|
1387
|
+
* Mark this table as global (cross-shard). Backed by **D1** by default;
|
|
1388
|
+
* pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
|
|
1389
|
+
* via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
|
|
1390
|
+
* table stays reactive — live queries re-run on write.
|
|
1391
|
+
*/
|
|
1173
1392
|
global: (options?: {
|
|
1174
1393
|
backend?: GlobalBackend;
|
|
1175
1394
|
}) => TableBuilder<Shape>;
|
|
@@ -1178,17 +1397,17 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
|
|
|
1178
1397
|
unique?: boolean;
|
|
1179
1398
|
}) => TableBuilder<Shape>;
|
|
1180
1399
|
/**
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1400
|
+
* Opt this table OUT of secure-by-default RLS. Under a schema marked
|
|
1401
|
+
* `.rls("required")`, every table is protected (the write path denies raw,
|
|
1402
|
+
* non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
|
|
1403
|
+
* plain `query`/`mutation` may read/write it without an RLS policy. No effect
|
|
1404
|
+
* when the schema does not require RLS.
|
|
1405
|
+
*/
|
|
1187
1406
|
public: () => TableBuilder<Shape>;
|
|
1188
1407
|
/**
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1408
|
+
* Declare a rank index (sorted companion table, btree-backed) for
|
|
1409
|
+
* `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
|
|
1410
|
+
*/
|
|
1192
1411
|
rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
|
|
1193
1412
|
/** Declare relations to other tables, loaded via `findMany({ with })`. */
|
|
1194
1413
|
relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
|
|
@@ -1200,23 +1419,47 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
|
|
|
1200
1419
|
/** Route storage by the named field — one DO per distinct value. */
|
|
1201
1420
|
shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
|
|
1202
1421
|
/**
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1422
|
+
* Turn on soft delete. Adds a nullable timestamp column (`options.field`,
|
|
1423
|
+
* default `deletedAt`) and changes `ctx.db.<table>.delete()` to **set** it
|
|
1424
|
+
* instead of removing the row; `onDelete: "cascade"` children are recursively
|
|
1425
|
+
* soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
|
|
1426
|
+
* `aggregate`/relation loads) then hide soft-deleted rows unless they pass
|
|
1427
|
+
* `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
|
|
1428
|
+
* `restore()` still address the row directly. `hardDelete()` physically
|
|
1429
|
+
* removes it (cascading as a real delete). Note: `includeDeleted` is a read
|
|
1430
|
+
* scope, not access control — anyone who can run the read can set it; a unique
|
|
1431
|
+
* index still rejects a new row that collides with a soft-deleted one (the row
|
|
1432
|
+
* physically persists).
|
|
1433
|
+
*/
|
|
1215
1434
|
softDelete: (options?: {
|
|
1216
1435
|
field?: string;
|
|
1217
1436
|
}) => TableBuilder<Shape>;
|
|
1437
|
+
/**
|
|
1438
|
+
* Materialize this table from an external Postgres/MySQL behind Cloudflare
|
|
1439
|
+
* Hyperdrive (plan 077). A system-driven poll loop reads the tenant slice
|
|
1440
|
+
* (`query`, with params bound from `tenantBy`) and lands it in the DO's SQLite,
|
|
1441
|
+
* after which `defineShape` carries it to clients unchanged. Implies
|
|
1442
|
+
* `.externallyManaged()` (rows come from the ingest loop, not user mutations).
|
|
1443
|
+
*
|
|
1444
|
+
* Orthogonal to `.shardBy()` — combine them for per-tenant DOs. **Under
|
|
1445
|
+
* `.shardBy()` `tenantBy` is mandatory** (the tenant-isolation boundary); the
|
|
1446
|
+
* `external_source_unscoped` advisor lint fails the build when it is absent, and
|
|
1447
|
+
* `external_source_on_global` rejects combining `.source()` with `.global()`.
|
|
1448
|
+
*/
|
|
1449
|
+
source: (definition: ExternalSourceDefinition) => TableBuilder<Shape>;
|
|
1218
1450
|
/** Declare named lifecycle triggers fired inline within the write path. */
|
|
1219
1451
|
triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
|
|
1452
|
+
/**
|
|
1453
|
+
* Declare a table-level TTL: a DO alarm-driven sweep auto-deletes rows whose
|
|
1454
|
+
* expiry has passed (or soft-deletes them when the table also
|
|
1455
|
+
* `.softDelete()`s). `field` is an epoch-millisecond column; without
|
|
1456
|
+
* `options.after` its value is the absolute expiry instant, with `after` the
|
|
1457
|
+
* row expires `after` ms past `field` (`field + after`). Coarse, cheap,
|
|
1458
|
+
* table-level — for per-row schedules use `@lunora/scheduler`.
|
|
1459
|
+
*/
|
|
1460
|
+
ttl: (field: keyof Shape & string, options?: {
|
|
1461
|
+
after?: number;
|
|
1462
|
+
}) => TableBuilder<Shape>;
|
|
1220
1463
|
/** Declare a vector index over a single text field on this table. */
|
|
1221
1464
|
vectorize: (field: keyof Shape & string, options: VectorizeOptions<Shape>) => TableBuilder<Shape>;
|
|
1222
1465
|
}
|
|
@@ -1234,22 +1477,22 @@ interface VectorIndexOptions {
|
|
|
1234
1477
|
};
|
|
1235
1478
|
}
|
|
1236
1479
|
/**
|
|
1237
|
-
* Build a table definition. Returned object is both the table definition (for
|
|
1238
|
-
* `defineSchema`) and a fluent builder for indexes + sharding metadata.
|
|
1239
|
-
*/
|
|
1480
|
+
* Build a table definition. Returned object is both the table definition (for
|
|
1481
|
+
* `defineSchema`) and a fluent builder for indexes + sharding metadata.
|
|
1482
|
+
*/
|
|
1240
1483
|
declare const defineTable: <Shape extends Record<string, Validator>>(inputShape: Shape) => TableBuilder<Shape>;
|
|
1241
1484
|
/**
|
|
1242
|
-
* Declare a standalone vector index (DSL Shape B). Pass the returned value in
|
|
1243
|
-
* the `vectorIndexes` map of {@link defineSchema} when the source is derived
|
|
1244
|
-
* from multiple fields or a computation rather than a single column.
|
|
1245
|
-
*/
|
|
1485
|
+
* Declare a standalone vector index (DSL Shape B). Pass the returned value in
|
|
1486
|
+
* the `vectorIndexes` map of {@link defineSchema} when the source is derived
|
|
1487
|
+
* from multiple fields or a computation rather than a single column.
|
|
1488
|
+
*/
|
|
1246
1489
|
declare const defineVectorIndex: (options: VectorIndexOptions) => VectorIndexDefinition;
|
|
1247
1490
|
/**
|
|
1248
|
-
* Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
|
|
1249
|
-
* Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
|
|
1250
|
-
* the owning table explicitly via `on` — handy when a single counter wants to
|
|
1251
|
-
* live next to the schema map rather than inside a table chain.
|
|
1252
|
-
*/
|
|
1491
|
+
* Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
|
|
1492
|
+
* Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
|
|
1493
|
+
* the owning table explicitly via `on` — handy when a single counter wants to
|
|
1494
|
+
* live next to the schema map rather than inside a table chain.
|
|
1495
|
+
*/
|
|
1253
1496
|
interface AggregateIndexOptions {
|
|
1254
1497
|
by?: ReadonlyArray<string>;
|
|
1255
1498
|
field?: string;
|
|
@@ -1258,17 +1501,17 @@ interface AggregateIndexOptions {
|
|
|
1258
1501
|
where?: Record<string, unknown>;
|
|
1259
1502
|
}
|
|
1260
1503
|
/**
|
|
1261
|
-
* Declare a standalone aggregate index. Pass the returned value to
|
|
1262
|
-
* `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
|
|
1263
|
-
* the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
|
|
1264
|
-
* (DO + D1) read every index uniformly off the table definition.
|
|
1265
|
-
*/
|
|
1504
|
+
* Declare a standalone aggregate index. Pass the returned value to
|
|
1505
|
+
* `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
|
|
1506
|
+
* the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
|
|
1507
|
+
* (DO + D1) read every index uniformly off the table definition.
|
|
1508
|
+
*/
|
|
1266
1509
|
declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
|
|
1267
1510
|
/**
|
|
1268
|
-
* Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
|
|
1269
|
-
* Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
|
|
1270
|
-
* `table` so it can sit next to the schema map.
|
|
1271
|
-
*/
|
|
1511
|
+
* Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
|
|
1512
|
+
* Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
|
|
1513
|
+
* `table` so it can sit next to the schema map.
|
|
1514
|
+
*/
|
|
1272
1515
|
interface RankIndexOptions {
|
|
1273
1516
|
partitionBy?: ReadonlyArray<string>;
|
|
1274
1517
|
sortBy: ReadonlyArray<{
|
|
@@ -1279,42 +1522,64 @@ interface RankIndexOptions {
|
|
|
1279
1522
|
where?: Record<string, unknown>;
|
|
1280
1523
|
}
|
|
1281
1524
|
/**
|
|
1282
|
-
* Declare a standalone rank index. Pass the returned value to
|
|
1283
|
-
* `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
|
|
1284
|
-
* by index name — the schema attaches it to `tables[on].rankIndexes`.
|
|
1285
|
-
*/
|
|
1525
|
+
* Declare a standalone rank index. Pass the returned value to
|
|
1526
|
+
* `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
|
|
1527
|
+
* by index name — the schema attaches it to `tables[on].rankIndexes`.
|
|
1528
|
+
*/
|
|
1286
1529
|
declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
|
|
1287
1530
|
/**
|
|
1288
|
-
* Build the application schema. The first argument is the table map; the
|
|
1289
|
-
* optional second argument registers standalone `defineVectorIndex(...)`
|
|
1290
|
-
* declarations (DSL Shape B) keyed by index name. The optional third argument
|
|
1291
|
-
* registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
|
|
1292
|
-
* the optional fourth argument registers standalone `defineRankIndex(...)`
|
|
1293
|
-
* declarations. Both are folded into the matching `tables[on].*Indexes` array
|
|
1294
|
-
* so runtime backends read every index uniformly off the table definition.
|
|
1295
|
-
*/
|
|
1296
|
-
/**
|
|
1297
|
-
* Schema with an in-place `.extend(plugin.extension)` method. Used so apps
|
|
1298
|
-
* can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
|
|
1299
|
-
*
|
|
1300
|
-
* `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
|
|
1301
|
-
* the merged tables. Extension tables are auto-namespaced by the extension
|
|
1302
|
-
* `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
|
|
1303
|
-
* prefixed names via {@link PrefixedTables}. Chains:
|
|
1304
|
-
* `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
|
|
1305
|
-
* `a`'s prefixed tables then `b`'s.
|
|
1306
|
-
*/
|
|
1531
|
+
* Build the application schema. The first argument is the table map; the
|
|
1532
|
+
* optional second argument registers standalone `defineVectorIndex(...)`
|
|
1533
|
+
* declarations (DSL Shape B) keyed by index name. The optional third argument
|
|
1534
|
+
* registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
|
|
1535
|
+
* the optional fourth argument registers standalone `defineRankIndex(...)`
|
|
1536
|
+
* declarations. Both are folded into the matching `tables[on].*Indexes` array
|
|
1537
|
+
* so runtime backends read every index uniformly off the table definition.
|
|
1538
|
+
*/
|
|
1539
|
+
/**
|
|
1540
|
+
* Schema with an in-place `.extend(plugin.extension)` method. Used so apps
|
|
1541
|
+
* can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
|
|
1542
|
+
*
|
|
1543
|
+
* `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
|
|
1544
|
+
* the merged tables. Extension tables are auto-namespaced by the extension
|
|
1545
|
+
* `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
|
|
1546
|
+
* prefixed names via {@link PrefixedTables}. Chains:
|
|
1547
|
+
* `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
|
|
1548
|
+
* `a`'s prefixed tables then `b`'s.
|
|
1549
|
+
*/
|
|
1307
1550
|
type ExtendableSchema<T extends Record<string, TableDefinition>> = {
|
|
1308
1551
|
extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
|
|
1309
1552
|
readonly key: Key;
|
|
1310
1553
|
}) => ExtendableSchema<PrefixedTables<X, Key> & T>;
|
|
1311
1554
|
/**
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1555
|
+
* Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
|
|
1556
|
+
* the scheduler, and `ctx.containers` — to a Cloudflare data-residency
|
|
1557
|
+
* jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
|
|
1558
|
+
* schema and emits it into the generated worker's `createWorker({ jurisdiction })`
|
|
1559
|
+
* (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
|
|
1560
|
+
* `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
|
|
1561
|
+
*
|
|
1562
|
+
* ⚠️ **Set this once, before your first deploy — changing or removing it
|
|
1563
|
+
* strands data.** A Durable Object name maps to a *different* ID in each
|
|
1564
|
+
* jurisdiction, so toggling this on an existing app makes every shard, scheduler
|
|
1565
|
+
* job, and session DO resolve to a NEW, empty DO; the previous data stays in the
|
|
1566
|
+
* old jurisdiction's DOs and is no longer reachable. There is no in-place
|
|
1567
|
+
* migration — you would have to export from the old jurisdiction and import
|
|
1568
|
+
* into the new one.
|
|
1569
|
+
*
|
|
1570
|
+
* Note: this pins **DO-backed** state only. D1-backed state — `.global()`
|
|
1571
|
+
* tables and `@lunora/auth` sessions alike — is governed by D1's own location
|
|
1572
|
+
* settings, not this option.
|
|
1573
|
+
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
1574
|
+
*/
|
|
1575
|
+
jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
|
|
1576
|
+
/**
|
|
1577
|
+
* Turn on secure-by-default RLS for the whole schema. Every table is then
|
|
1578
|
+
* protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
|
|
1579
|
+
* procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
|
|
1580
|
+
* `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
|
|
1581
|
+
* mode, so `.rls("required")` composes with `.extend(...)` either order.
|
|
1582
|
+
*/
|
|
1318
1583
|
rls: (mode: "required") => ExtendableSchema<T>;
|
|
1319
1584
|
} & Schema<T>;
|
|
1320
1585
|
declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
|
|
@@ -1322,19 +1587,19 @@ declare const defineSchema: <T extends Record<string, TableDefinition>>(tables:
|
|
|
1322
1587
|
declare const DEFAULT_TTL_MS = 3e4;
|
|
1323
1588
|
declare const PRESENCE_BARE_TABLE = "present";
|
|
1324
1589
|
/**
|
|
1325
|
-
* The prefixed table name the extension produces at merge time. The handlers
|
|
1326
|
-
* read/write this name directly so they always agree with the merged schema.
|
|
1327
|
-
*/
|
|
1590
|
+
* The prefixed table name the extension produces at merge time. The handlers
|
|
1591
|
+
* read/write this name directly so they always agree with the merged schema.
|
|
1592
|
+
*/
|
|
1328
1593
|
declare const PRESENCE_TABLE: "presence_present";
|
|
1329
1594
|
/**
|
|
1330
|
-
* A single present member as returned by `listPresent`.
|
|
1331
|
-
*
|
|
1332
|
-
* Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
|
|
1333
|
-
* connection secret — disclosing every member's `sessionId` would let any
|
|
1334
|
-
* subscriber enumerate them and target the heartbeat / disconnect write paths.
|
|
1335
|
-
* A "who's here" UI needs only `userId` + awareness `data`; the caller already
|
|
1336
|
-
* knows its own session id locally (the `usePresence` hook returns it).
|
|
1337
|
-
*/
|
|
1595
|
+
* A single present member as returned by `listPresent`.
|
|
1596
|
+
*
|
|
1597
|
+
* Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
|
|
1598
|
+
* connection secret — disclosing every member's `sessionId` would let any
|
|
1599
|
+
* subscriber enumerate them and target the heartbeat / disconnect write paths.
|
|
1600
|
+
* A "who's here" UI needs only `userId` + awareness `data`; the caller already
|
|
1601
|
+
* knows its own session id locally (the `usePresence` hook returns it).
|
|
1602
|
+
*/
|
|
1338
1603
|
interface PresenceMember {
|
|
1339
1604
|
/** Opaque awareness blob (selection, cursor, name, color…). */
|
|
1340
1605
|
data?: Record<string, unknown>;
|
|
@@ -1348,41 +1613,41 @@ interface PresenceMember {
|
|
|
1348
1613
|
/** Options for {@link definePresence}. */
|
|
1349
1614
|
interface DefinePresenceOptions {
|
|
1350
1615
|
/**
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1616
|
+
* Grace window (ms) before a gracefully-closed session is dropped from the
|
|
1617
|
+
* present list. When `0` (the default), `onDisconnect` hard-deletes the
|
|
1618
|
+
* session's row the instant its socket closes. When `> 0`, the row is
|
|
1619
|
+
* instead aged so the read-time TTL filter hides it `disconnectGraceMs`
|
|
1620
|
+
* from now — a reconnect with the same `sessionId` within the window
|
|
1621
|
+
* re-heartbeats and restores full presence with no visible flicker (the
|
|
1622
|
+
* AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
|
|
1623
|
+
*/
|
|
1359
1624
|
disconnectGraceMs?: number;
|
|
1360
1625
|
/**
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1626
|
+
* How long (ms) a heartbeat keeps a member present. `listPresent` excludes
|
|
1627
|
+
* rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
|
|
1628
|
+
*/
|
|
1364
1629
|
ttlMs?: number;
|
|
1365
1630
|
}
|
|
1366
1631
|
/** The registered functions a presence component ships. */
|
|
1367
1632
|
interface PresenceFunctions {
|
|
1368
1633
|
/**
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1634
|
+
* Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
|
|
1635
|
+
* delete its presence row so it disappears from `listPresent` with no TTL
|
|
1636
|
+
* lag. Targets the row by the `{ roomId, sessionId }` the client passed as
|
|
1637
|
+
* the connection `context`, and only deletes it when the disconnecting
|
|
1638
|
+
* VERIFIED identity owns the row (so a forged context can't evict another
|
|
1639
|
+
* member). The TTL filter + `sweep` remain the fallback for ungraceful drops
|
|
1640
|
+
* where no `context` was recorded.
|
|
1641
|
+
*/
|
|
1377
1642
|
disconnect: RegisteredLifecycleHook;
|
|
1378
1643
|
/**
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1644
|
+
* Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
|
|
1645
|
+
* Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
|
|
1646
|
+
* subscribers receive a single-row delta, not a churn of insert/delete. A
|
|
1647
|
+
* heartbeat may only patch a row owned by the same identity (an existing row
|
|
1648
|
+
* held by a different `userId` is refused with `FORBIDDEN`), so a client
|
|
1649
|
+
* can't overwrite another member's awareness data via a guessed `sessionId`.
|
|
1650
|
+
*/
|
|
1386
1651
|
heartbeat: RegisteredMutation<{
|
|
1387
1652
|
data: ReturnType<typeof v.optional>;
|
|
1388
1653
|
roomId: ReturnType<typeof v.string>;
|
|
@@ -1391,17 +1656,17 @@ interface PresenceFunctions {
|
|
|
1391
1656
|
lastSeen: number;
|
|
1392
1657
|
}>;
|
|
1393
1658
|
/**
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1659
|
+
* Live query returning the non-expired members of `roomId`, newest heartbeat
|
|
1660
|
+
* first. Subscribe to it for a reactive "who's here" list.
|
|
1661
|
+
*/
|
|
1397
1662
|
listPresent: RegisteredQuery<{
|
|
1398
1663
|
roomId: ReturnType<typeof v.string>;
|
|
1399
1664
|
}, PresenceMember[]>;
|
|
1400
1665
|
/**
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1666
|
+
* Internal mutation that hard-deletes every expired row for `roomId`. Stale
|
|
1667
|
+
* rows already vanish from `listPresent` via the read-time TTL filter; this
|
|
1668
|
+
* only reclaims storage. Schedule it (cron / `runAfter`) if you care.
|
|
1669
|
+
*/
|
|
1405
1670
|
sweep: RegisteredMutation<{
|
|
1406
1671
|
roomId: ReturnType<typeof v.string>;
|
|
1407
1672
|
}, {
|
|
@@ -1415,114 +1680,114 @@ type PresenceComponent = Component<{
|
|
|
1415
1680
|
functions: PresenceFunctions;
|
|
1416
1681
|
};
|
|
1417
1682
|
/**
|
|
1418
|
-
* The presence schema extension: a single `present` table, auto-namespaced to
|
|
1419
|
-
* `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
|
|
1420
|
-
* heartbeat upsert and by `roomId` for `listPresent`.
|
|
1421
|
-
*/
|
|
1683
|
+
* The presence schema extension: a single `present` table, auto-namespaced to
|
|
1684
|
+
* `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
|
|
1685
|
+
* heartbeat upsert and by `roomId` for `listPresent`.
|
|
1686
|
+
*/
|
|
1422
1687
|
declare const presenceExtension: SchemaExtension<{
|
|
1423
1688
|
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
1424
1689
|
}>;
|
|
1425
1690
|
declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
|
|
1426
1691
|
/**
|
|
1427
|
-
* The middlewares `protectPublic` chains, in the order they run. Every field is
|
|
1428
|
-
* optional, so a bundle can be just a rate limit, just a captcha, or any mix —
|
|
1429
|
-
* pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
|
|
1430
|
-
* from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
|
|
1431
|
-
* `@lunora/auth`). They are accepted as values rather than imported here so
|
|
1432
|
-
* `@lunora/server` keeps no dependency on those packages (which depend on it).
|
|
1433
|
-
*/
|
|
1692
|
+
* The middlewares `protectPublic` chains, in the order they run. Every field is
|
|
1693
|
+
* optional, so a bundle can be just a rate limit, just a captcha, or any mix —
|
|
1694
|
+
* pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
|
|
1695
|
+
* from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
|
|
1696
|
+
* `@lunora/auth`). They are accepted as values rather than imported here so
|
|
1697
|
+
* `@lunora/server` keeps no dependency on those packages (which depend on it).
|
|
1698
|
+
*/
|
|
1434
1699
|
interface ProtectPublicOptions<Context> {
|
|
1435
1700
|
/**
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1701
|
+
* A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
|
|
1702
|
+
* an obvious flood is cheaper to reject with the in-memory limiter than with
|
|
1703
|
+
* a Turnstile siteverify round-trip.
|
|
1704
|
+
*/
|
|
1440
1705
|
captcha?: Middleware<Context, Context>;
|
|
1441
1706
|
/**
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1707
|
+
* A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
|
|
1708
|
+
* any network-bound check below it runs.
|
|
1709
|
+
*/
|
|
1445
1710
|
rateLimit?: Middleware<Context, Context>;
|
|
1446
1711
|
/** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
|
|
1447
1712
|
use?: ReadonlyArray<Middleware<Context, Context>>;
|
|
1448
1713
|
}
|
|
1449
1714
|
/**
|
|
1450
|
-
* Compose the recommended public-procedure protections into a single
|
|
1451
|
-
* `.use()`-able middleware. It is thin sugar over middleware composition — no
|
|
1452
|
-
* new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
|
|
1453
|
-
* and any extra middlewares so a public mutation that creates users, sends
|
|
1454
|
-
* mail, or consumes credits is guarded in one attachment:
|
|
1455
|
-
*
|
|
1456
|
-
* ```ts
|
|
1457
|
-
* export const signUp = mutation
|
|
1458
|
-
* .use(protectPublic({
|
|
1459
|
-
* rateLimit: rateLimit(limiter, "signup"),
|
|
1460
|
-
* captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
|
|
1461
|
-
* }))
|
|
1462
|
-
* .handler(async (ctx, args) => { ... });
|
|
1463
|
-
* ```
|
|
1464
|
-
*
|
|
1465
|
-
* The bundle is context-preserving — each inner middleware leaves the context
|
|
1466
|
-
* unchanged — so it slots into any `.use()` chain without reshaping the
|
|
1467
|
-
* procedure context. Omitted fields are skipped; an empty bundle is a
|
|
1468
|
-
* transparent pass-through.
|
|
1469
|
-
*/
|
|
1715
|
+
* Compose the recommended public-procedure protections into a single
|
|
1716
|
+
* `.use()`-able middleware. It is thin sugar over middleware composition — no
|
|
1717
|
+
* new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
|
|
1718
|
+
* and any extra middlewares so a public mutation that creates users, sends
|
|
1719
|
+
* mail, or consumes credits is guarded in one attachment:
|
|
1720
|
+
*
|
|
1721
|
+
* ```ts
|
|
1722
|
+
* export const signUp = mutation
|
|
1723
|
+
* .use(protectPublic({
|
|
1724
|
+
* rateLimit: rateLimit(limiter, "signup"),
|
|
1725
|
+
* captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
|
|
1726
|
+
* }))
|
|
1727
|
+
* .handler(async (ctx, args) => { ... });
|
|
1728
|
+
* ```
|
|
1729
|
+
*
|
|
1730
|
+
* The bundle is context-preserving — each inner middleware leaves the context
|
|
1731
|
+
* unchanged — so it slots into any `.use()` chain without reshaping the
|
|
1732
|
+
* procedure context. Omitted fields are skipped; an empty bundle is a
|
|
1733
|
+
* transparent pass-through.
|
|
1734
|
+
*/
|
|
1470
1735
|
declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
|
|
1471
1736
|
declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
|
|
1472
1737
|
/**
|
|
1473
|
-
* Build a project-bound, relation-aware `definePolicy` typed against the
|
|
1474
|
-
* generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
|
|
1475
|
-
* `createPolicyDsl<DataModel, Relations>()` binding into `_generated/server.ts`,
|
|
1476
|
-
* so importing `definePolicy` from the generated module constrains `table` to a
|
|
1477
|
-
* real table name and type-checks the `when` predicate — including Prisma-style
|
|
1478
|
-
* relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
|
|
1479
|
-
* on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
|
|
1480
|
-
* the compile-time surface narrows, so a policy authored either way is
|
|
1481
|
-
* discovered identically by the `rls()` chain.
|
|
1482
|
-
*/
|
|
1483
|
-
declare const createPolicyDsl: <DM, REL extends Record<keyof DM, object>>() => <T extends keyof DM, Context = unknown>(input: TypedDefinePolicyInput<DM, REL, T, Context>) => Policy<Context>;
|
|
1484
|
-
/**
|
|
1485
|
-
* Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
|
|
1486
|
-
* it to a role through `defineRole`'s `permissions`, register those roles with
|
|
1487
|
-
* the middleware via `rls(policies, { roles })`, then check it in a policy with
|
|
1488
|
-
* `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
|
|
1489
|
-
* worked example.
|
|
1490
|
-
*/
|
|
1738
|
+
* Build a project-bound, relation-aware `definePolicy` typed against the
|
|
1739
|
+
* generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
|
|
1740
|
+
* `createPolicyDsl<DataModel, Relations>()` binding into `_generated/server.ts`,
|
|
1741
|
+
* so importing `definePolicy` from the generated module constrains `table` to a
|
|
1742
|
+
* real table name and type-checks the `when` predicate — including Prisma-style
|
|
1743
|
+
* relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
|
|
1744
|
+
* on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
|
|
1745
|
+
* the compile-time surface narrows, so a policy authored either way is
|
|
1746
|
+
* discovered identically by the `rls()` chain.
|
|
1747
|
+
*/
|
|
1748
|
+
declare const createPolicyDsl: <DM, REL extends Record<keyof DM, object>, Identity = Record<string, unknown>>() => <T extends keyof DM, Context = unknown>(input: TypedDefinePolicyInput<DM, REL, T, Context, Identity>) => Policy<Context>;
|
|
1749
|
+
/**
|
|
1750
|
+
* Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
|
|
1751
|
+
* it to a role through `defineRole`'s `permissions`, register those roles with
|
|
1752
|
+
* the middleware via `rls(policies, { roles })`, then check it in a policy with
|
|
1753
|
+
* `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
|
|
1754
|
+
* worked example.
|
|
1755
|
+
*/
|
|
1491
1756
|
declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
|
|
1492
1757
|
/**
|
|
1493
|
-
* Collect a list of policies into the structure the `rls()` middleware
|
|
1494
|
-
* consumes. Multiple read policies on the same table OR together (any one
|
|
1495
|
-
* matching reveals the row); multiple write policies for the same `(table,
|
|
1496
|
-
* op)` AND together (every one must allow). The middleware keeps them in order
|
|
1497
|
-
* and decides — see `./middleware`.
|
|
1498
|
-
*
|
|
1499
|
-
* Validates against an **accidentally duplicated policy** — the same
|
|
1500
|
-
* `(table, on)` registered with the *same* decision function (a copy-paste, or
|
|
1501
|
-
* the same policy object spread in twice). Because multiple DISTINCT policies
|
|
1502
|
-
* per `(table, on)` are intentional, the check keys on the `when` reference too:
|
|
1503
|
-
* only a reference-identical `when` for the same `(table, on)` is a real
|
|
1504
|
-
* duplicate. Throws at module load so the misconfiguration surfaces immediately
|
|
1505
|
-
* rather than as a silently double-evaluated predicate at request time.
|
|
1506
|
-
*/
|
|
1758
|
+
* Collect a list of policies into the structure the `rls()` middleware
|
|
1759
|
+
* consumes. Multiple read policies on the same table OR together (any one
|
|
1760
|
+
* matching reveals the row); multiple write policies for the same `(table,
|
|
1761
|
+
* op)` AND together (every one must allow). The middleware keeps them in order
|
|
1762
|
+
* and decides — see `./middleware`.
|
|
1763
|
+
*
|
|
1764
|
+
* Validates against an **accidentally duplicated policy** — the same
|
|
1765
|
+
* `(table, on)` registered with the *same* decision function (a copy-paste, or
|
|
1766
|
+
* the same policy object spread in twice). Because multiple DISTINCT policies
|
|
1767
|
+
* per `(table, on)` are intentional, the check keys on the `when` reference too:
|
|
1768
|
+
* only a reference-identical `when` for the same `(table, on)` is a real
|
|
1769
|
+
* duplicate. Throws at module load so the misconfiguration surfaces immediately
|
|
1770
|
+
* rather than as a silently double-evaluated predicate at request time.
|
|
1771
|
+
*/
|
|
1507
1772
|
declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
|
|
1508
1773
|
declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
|
|
1509
1774
|
/**
|
|
1510
|
-
* Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
|
|
1511
|
-
* runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
|
|
1512
|
-
* `restrictsCounts` straight off these option objects, so as long as the
|
|
1513
|
-
* fields here stay name-compatible the wrapper is portable across the two
|
|
1514
|
-
* dialects without an inter-package dependency.
|
|
1515
|
-
*/
|
|
1775
|
+
* Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
|
|
1776
|
+
* runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
|
|
1777
|
+
* `restrictsCounts` straight off these option objects, so as long as the
|
|
1778
|
+
* fields here stay name-compatible the wrapper is portable across the two
|
|
1779
|
+
* dialects without an inter-package dependency.
|
|
1780
|
+
*/
|
|
1516
1781
|
interface QueryArgs {
|
|
1517
1782
|
baseWhere?: WhereInput;
|
|
1518
1783
|
cursor?: null | string;
|
|
1519
1784
|
limit?: number;
|
|
1520
1785
|
orderBy?: ReadonlyArray<unknown>;
|
|
1521
1786
|
/**
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1787
|
+
* Per-target-table read filter the RLS wrapper attaches so a `with` relation
|
|
1788
|
+
* is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
|
|
1789
|
+
* the top-level read: `(table) => readBase(table).baseWhere`.
|
|
1790
|
+
*/
|
|
1526
1791
|
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1527
1792
|
restrictsCounts?: boolean;
|
|
1528
1793
|
where?: WhereInput;
|
|
@@ -1591,22 +1856,23 @@ interface TableReaderLike {
|
|
|
1591
1856
|
numItems: number;
|
|
1592
1857
|
}) => Promise<QueryPage>;
|
|
1593
1858
|
take: (limit: number) => Promise<Record<string, unknown>[]>;
|
|
1859
|
+
withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike;
|
|
1594
1860
|
withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike;
|
|
1595
1861
|
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike;
|
|
1596
1862
|
}
|
|
1597
1863
|
/**
|
|
1598
|
-
* Structural projection of the runtime ORM writer. The wrapper relies only
|
|
1599
|
-
* on these fields, so it's interchangeable between `@lunora/do`'s
|
|
1600
|
-
* `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
|
|
1601
|
-
*/
|
|
1864
|
+
* Structural projection of the runtime ORM writer. The wrapper relies only
|
|
1865
|
+
* on these fields, so it's interchangeable between `@lunora/do`'s
|
|
1866
|
+
* `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
|
|
1867
|
+
*/
|
|
1602
1868
|
interface DatabaseWriterLike {
|
|
1603
1869
|
/**
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1870
|
+
* Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
|
|
1871
|
+
* `baseWhere` into `options` so the reduction only sees policy-visible rows
|
|
1872
|
+
* (safe: an aggregate scoped to `where` never reveals a hidden row — see
|
|
1873
|
+
* `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
|
|
1874
|
+
* wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
|
|
1875
|
+
*/
|
|
1610
1876
|
aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
|
|
1611
1877
|
count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
|
|
1612
1878
|
delete: (id: string, expectedTable?: string, options?: {
|
|
@@ -1617,15 +1883,20 @@ interface DatabaseWriterLike {
|
|
|
1617
1883
|
}, expectedTable?: string) => Promise<{
|
|
1618
1884
|
deleted: number;
|
|
1619
1885
|
}>;
|
|
1886
|
+
deleteWhere?: (tableName: string, where: WhereInput, options?: {
|
|
1887
|
+
limit?: number;
|
|
1888
|
+
}) => Promise<{
|
|
1889
|
+
deleted: number;
|
|
1890
|
+
}>;
|
|
1620
1891
|
findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
|
|
1621
1892
|
findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
|
|
1622
1893
|
findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
|
|
1623
1894
|
get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
|
|
1624
1895
|
/**
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1896
|
+
* Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
|
|
1897
|
+
* reduction is scoped to policy-visible rows, so a group count tallies only
|
|
1898
|
+
* rows the caller may read. Required for the same reason as `aggregate`.
|
|
1899
|
+
*/
|
|
1629
1900
|
groupBy: (tableName: string, options: GroupByArgs) => Promise<ReadonlyArray<{
|
|
1630
1901
|
key: Record<string, unknown>;
|
|
1631
1902
|
value: null | number;
|
|
@@ -1633,19 +1904,20 @@ interface DatabaseWriterLike {
|
|
|
1633
1904
|
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
1634
1905
|
insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
1635
1906
|
limit?: number;
|
|
1636
|
-
|
|
1907
|
+
skipDuplicates?: boolean;
|
|
1908
|
+
}) => Promise<(string | null)[]>;
|
|
1637
1909
|
insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
1638
1910
|
allowExplicitId?: boolean;
|
|
1639
1911
|
limit?: number;
|
|
1640
1912
|
}) => Promise<string[]>;
|
|
1641
1913
|
/**
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1914
|
+
* Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
|
|
1915
|
+
* already knows the owning table of an id internally, so it can return
|
|
1916
|
+
* `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
|
|
1917
|
+
* uses it to collapse the per-call membership-probe fan-out (1 `get` + N
|
|
1918
|
+
* `findFirst` across every policy table) down to one lookup. Writers that
|
|
1919
|
+
* don't implement it fall back to the probe path.
|
|
1920
|
+
*/
|
|
1649
1921
|
lookupById?: (id: string, expectedTable?: string) => Promise<null | {
|
|
1650
1922
|
row: Record<string, unknown>;
|
|
1651
1923
|
tableName: string;
|
|
@@ -1656,14 +1928,24 @@ interface DatabaseWriterLike {
|
|
|
1656
1928
|
patch: Record<string, unknown>;
|
|
1657
1929
|
}>, options?: {
|
|
1658
1930
|
limit?: number;
|
|
1659
|
-
}, expectedTable?: string) => Promise<
|
|
1931
|
+
}, expectedTable?: string) => Promise<{
|
|
1932
|
+
patched: number;
|
|
1933
|
+
}>;
|
|
1934
|
+
patchWhere?: (tableName: string, args: {
|
|
1935
|
+
patch: Record<string, unknown>;
|
|
1936
|
+
where: WhereInput;
|
|
1937
|
+
}, options?: {
|
|
1938
|
+
limit?: number;
|
|
1939
|
+
}) => Promise<{
|
|
1940
|
+
patched: number;
|
|
1941
|
+
}>;
|
|
1660
1942
|
query: (tableName: string) => TableReaderLike;
|
|
1661
1943
|
/**
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1944
|
+
* Rank a row within its partition. A position is a count-of-rows-before, so
|
|
1945
|
+
* — exactly like `count()` — it can't be trusted in an RLS-restricted
|
|
1946
|
+
* reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
|
|
1947
|
+
* for the same reason as `aggregate`.
|
|
1948
|
+
*/
|
|
1667
1949
|
rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
|
|
1668
1950
|
position: number;
|
|
1669
1951
|
total: number;
|
|
@@ -1674,24 +1956,24 @@ interface DatabaseWriterLike {
|
|
|
1674
1956
|
total: number;
|
|
1675
1957
|
}>;
|
|
1676
1958
|
/**
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1959
|
+
* Sorted pagination over a rank companion. The companion stores only the
|
|
1960
|
+
* partition + sort keys + id, so an arbitrary read `baseWhere` can't be
|
|
1961
|
+
* enforced against it (and re-filtering the fetched rows would break page
|
|
1962
|
+
* sizing). RLS therefore fails it closed rather than leak hidden rows.
|
|
1963
|
+
* Required for the same reason as `aggregate`.
|
|
1964
|
+
*/
|
|
1683
1965
|
rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
|
|
1684
1966
|
replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
1685
1967
|
restore?: (id: string, expectedTable?: string) => Promise<void>;
|
|
1686
1968
|
}
|
|
1687
1969
|
/**
|
|
1688
|
-
* What a procedure's `ctx.db` must structurally satisfy for the middleware
|
|
1689
|
-
* to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
|
|
1690
|
-
* rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
|
|
1691
|
-
* the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
|
|
1692
|
-
* and structural matching keeps this module free of an `@lunora/do`-typed
|
|
1693
|
-
* `ctx`.
|
|
1694
|
-
*/
|
|
1970
|
+
* What a procedure's `ctx.db` must structurally satisfy for the middleware
|
|
1971
|
+
* to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
|
|
1972
|
+
* rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
|
|
1973
|
+
* the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
|
|
1974
|
+
* and structural matching keeps this module free of an `@lunora/do`-typed
|
|
1975
|
+
* `ctx`.
|
|
1976
|
+
*/
|
|
1695
1977
|
type RlsDatabase = DatabaseWriterLike;
|
|
1696
1978
|
/** Roles list source on the context. Tolerant of older auth states. */
|
|
1697
1979
|
type AuthLike = {
|
|
@@ -1706,21 +1988,119 @@ interface RlsContextIn {
|
|
|
1706
1988
|
}
|
|
1707
1989
|
declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
|
|
1708
1990
|
/**
|
|
1709
|
-
*
|
|
1710
|
-
*
|
|
1711
|
-
* `
|
|
1712
|
-
*
|
|
1713
|
-
*/
|
|
1991
|
+
* One `rls()` tag's read policies for a table, paired with the role→permission
|
|
1992
|
+
* grants of that SAME middleware. Keeping the role map per-group is what lets a
|
|
1993
|
+
* policy's `auth.can(...)` resolve against its own middleware's roles — never a
|
|
1994
|
+
* permission registered on a different `rls()` step.
|
|
1995
|
+
*/
|
|
1996
|
+
interface ScopedReadPolicies {
|
|
1997
|
+
readonly policies: ReadonlyArray<Policy>;
|
|
1998
|
+
readonly rolePermissions: ReadonlyMap<string, ReadonlySet<string>>;
|
|
1999
|
+
}
|
|
2000
|
+
/** Table-indexed read-policy groups, each scoped to the roles of the rls() middleware that declared it. */
|
|
2001
|
+
interface RlsReadRegistry {
|
|
2002
|
+
readonly byTable: ReadonlyMap<string, ReadonlyArray<ScopedReadPolicies>>;
|
|
2003
|
+
}
|
|
2004
|
+
/** The trusted, server-resolved facts a shape's RLS evaluation runs under. */
|
|
2005
|
+
interface ShapeReadWhereRequest {
|
|
2006
|
+
/** The shape ctx (the procedure context a policy `when` reads as `ctx`). */
|
|
2007
|
+
readonly ctx: unknown;
|
|
2008
|
+
/** Resolved identity claims (the socket's verified identity), or `null` when anonymous. */
|
|
2009
|
+
readonly identity: Record<string, unknown> | null;
|
|
2010
|
+
/** `true` when the schema is `.rls("required")` — gates the fail-closed branch. */
|
|
2011
|
+
readonly rlsRequired: boolean;
|
|
2012
|
+
/** Role labels the request carries (drives `auth.can(...)`). */
|
|
2013
|
+
readonly roles: ReadonlyArray<string>;
|
|
2014
|
+
/** The shape's own predicate (`where(ctx, args)`). */
|
|
2015
|
+
readonly shapeWhere: WhereInput;
|
|
2016
|
+
/** Logical table the shape replicates. */
|
|
2017
|
+
readonly table: string;
|
|
2018
|
+
/** `true` when the table is `.public()` (exempt from `.rls("required")` denial). */
|
|
2019
|
+
readonly tablePublic: boolean;
|
|
2020
|
+
/** Verified user id, or `null` when anonymous. */
|
|
2021
|
+
readonly userId: null | string;
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* Build the read-policy registry from the registered functions (pass
|
|
2025
|
+
* `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
|
|
2026
|
+
* grouped per `rls()` middleware so each group keeps its own role→permission map
|
|
2027
|
+
* (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
|
|
2028
|
+
* several procedures (a shared `const guard = rls(...)`) is folded once. This
|
|
2029
|
+
* mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
|
|
2030
|
+
* resolves against the roles of the middleware that declared it, never a union.
|
|
2031
|
+
*/
|
|
2032
|
+
declare const buildRlsReadRegistry: (functions: Iterable<unknown>) => RlsReadRegistry;
|
|
2033
|
+
/**
|
|
2034
|
+
* Compute the effective `where` a shape replicates: the table's RLS read
|
|
2035
|
+
* base-where AND the shape's own predicate. Returns the shape predicate
|
|
2036
|
+
* unchanged for a table with no read policy (a `.public()` or non-RLS table),
|
|
2037
|
+
* and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
|
|
2038
|
+
* exposes a protected, policy-less table.
|
|
2039
|
+
*/
|
|
2040
|
+
declare const composeShapeReadWhere: (registry: RlsReadRegistry, request: ShapeReadWhereRequest) => WhereInput;
|
|
2041
|
+
/**
|
|
2042
|
+
* A shape declaration. `where` receives the trusted procedure context and the
|
|
2043
|
+
* validated client args and returns the same {@link WhereInput} shape the RLS
|
|
2044
|
+
* DSL uses, so the DO can AND-merge it with the table's read base-where via the
|
|
2045
|
+
* existing where-compiler (zero second predicate implementation).
|
|
2046
|
+
*/
|
|
2047
|
+
interface ShapeDefinition<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> {
|
|
2048
|
+
/**
|
|
2049
|
+
* Validator for the client-supplied shape parameters. Validated on the DO
|
|
2050
|
+
* before `where` runs, so a malformed `args` envelope is rejected at the
|
|
2051
|
+
* subscription boundary rather than silently widening the partition. Omit
|
|
2052
|
+
* for a parameterless shape.
|
|
2053
|
+
*/
|
|
2054
|
+
readonly args?: Args;
|
|
2055
|
+
/**
|
|
2056
|
+
* Project the replicated rows to these columns (the system columns `_id` and
|
|
2057
|
+
* `_creationTime` are always included). Omit to replicate every column. An
|
|
2058
|
+
* empty array is rejected — it would replicate no data, which is never the
|
|
2059
|
+
* intent.
|
|
2060
|
+
*/
|
|
2061
|
+
readonly columns?: ReadonlyArray<string>;
|
|
2062
|
+
/** Logical table this shape replicates a partition of. */
|
|
2063
|
+
readonly table: string;
|
|
2064
|
+
/**
|
|
2065
|
+
* Predicate selecting the rows this shape replicates. AND-composed with the
|
|
2066
|
+
* table's RLS read base-where on the DO. Runs server-side with a trusted
|
|
2067
|
+
* `ctx` (identity/auth the client can't forge) and the validated client
|
|
2068
|
+
* `args`; returns a {@link WhereInput} using the same operator set as the
|
|
2069
|
+
* SQL compiler (`eq`/`in`/`lt`/… + `AND`/`OR`/`NOT`).
|
|
2070
|
+
*/
|
|
2071
|
+
readonly where: (context: Context, args: InferValidatorMap<Args>) => WhereInput;
|
|
2072
|
+
}
|
|
2073
|
+
/** A {@link ShapeDefinition} plus the codegen discovery marker and a dispatch-shaped `compileWhere`. */
|
|
2074
|
+
interface RegisteredShape<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> extends ShapeDefinition<Args, Context> {
|
|
2075
|
+
readonly __lunoraShape: true;
|
|
2076
|
+
/**
|
|
2077
|
+
* Validate `rawArgs`, then evaluate `where` under the trusted `ctx` and
|
|
2078
|
+
* return its {@link WhereInput}. Used by the generated DO's `resolveShape`
|
|
2079
|
+
* override: `ctx` is erased to `unknown` at this dispatch boundary (the DO
|
|
2080
|
+
* builds it from the socket's verified identity and hands it back as the
|
|
2081
|
+
* concrete {@link QueryContext} the predicate expects), exactly like
|
|
2082
|
+
* `RegisteredLunoraFunction.handler` erases its context.
|
|
2083
|
+
*/
|
|
2084
|
+
readonly compileWhere: (context: unknown, rawArgs: Record<string, unknown>) => WhereInput;
|
|
2085
|
+
}
|
|
2086
|
+
/** Declare a replication shape. See the module docs for runtime semantics. */
|
|
2087
|
+
declare const defineShape: <Args extends ValidatorMap = ValidatorMap, Context = QueryCtx>(definition: ShapeDefinition<Args, Context>) => RegisteredShape<Args, Context>;
|
|
2088
|
+
/**
|
|
2089
|
+
* Operations a storage rule can gate. `read` covers `download` / `getMetadata`
|
|
2090
|
+
* / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
|
|
2091
|
+
* `delete` is `delete`; `list` is a prefix listing (governed via the file
|
|
2092
|
+
* browser / admin path, not `ctx.storage` which has no `list`).
|
|
2093
|
+
*/
|
|
1714
2094
|
type StorageOperation = "delete" | "list" | "read" | "write";
|
|
1715
2095
|
/** A rule's decision. `true` allows, `false` denies, `undefined` opts this rule out. */
|
|
1716
2096
|
type StorageRuleDecision = boolean | undefined;
|
|
1717
2097
|
/**
|
|
1718
|
-
* Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
|
|
1719
|
-
* (the per-request userId / roles / identity and the `can(permission)` helper),
|
|
1720
|
-
* so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
|
|
1721
|
-
* `key` is the object key the operation targets (for `list`, the listing
|
|
1722
|
-
* prefix). `ctx` is the full procedure context the middleware closed over.
|
|
1723
|
-
*/
|
|
2098
|
+
* Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
|
|
2099
|
+
* (the per-request userId / roles / identity and the `can(permission)` helper),
|
|
2100
|
+
* so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
|
|
2101
|
+
* `key` is the object key the operation targets (for `list`, the listing
|
|
2102
|
+
* prefix). `ctx` is the full procedure context the middleware closed over.
|
|
2103
|
+
*/
|
|
1724
2104
|
interface StorageRuleContext<Context = unknown> {
|
|
1725
2105
|
readonly auth: {
|
|
1726
2106
|
readonly can: (permission: Permission | string) => boolean;
|
|
@@ -1735,11 +2115,11 @@ interface StorageRuleContext<Context = unknown> {
|
|
|
1735
2115
|
/** A registered storage rule as stored in the rule table. */
|
|
1736
2116
|
interface StorageRule<Context = unknown> {
|
|
1737
2117
|
/**
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
2118
|
+
* Logical bucket the rule governs — matched against the accessor's bucket
|
|
2119
|
+
* (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
|
|
2120
|
+
* A rule only applies to operations on its own bucket. The unnamed bucket is
|
|
2121
|
+
* `"default"`. Also surfaced in the studio's access-rules view.
|
|
2122
|
+
*/
|
|
1743
2123
|
readonly bucket: string;
|
|
1744
2124
|
readonly on: StorageOperation;
|
|
1745
2125
|
/** Optional key-prefix scope; the rule only governs keys under it. Absent ⇒ the whole bucket. */
|
|
@@ -1754,27 +2134,27 @@ interface DefineStorageRuleInput<Context = unknown> {
|
|
|
1754
2134
|
when: (context: StorageRuleContext<Context>) => StorageRuleDecision;
|
|
1755
2135
|
}
|
|
1756
2136
|
/**
|
|
1757
|
-
* Options for the `storageRules(rules, options)` middleware. `roles` registers
|
|
1758
|
-
* the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
|
|
1759
|
-
* `RlsOptions.roles` does — fail-closed for unlisted roles.
|
|
1760
|
-
*/
|
|
2137
|
+
* Options for the `storageRules(rules, options)` middleware. `roles` registers
|
|
2138
|
+
* the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
|
|
2139
|
+
* `RlsOptions.roles` does — fail-closed for unlisted roles.
|
|
2140
|
+
*/
|
|
1761
2141
|
interface StorageRulesOptions {
|
|
1762
2142
|
readonly roles?: ReadonlyArray<Role>;
|
|
1763
2143
|
}
|
|
1764
2144
|
declare const defineStorageRule: <Context = unknown>(input: DefineStorageRuleInput<Context>) => StorageRule<Context>;
|
|
1765
2145
|
/**
|
|
1766
|
-
* Collect a list of storage rules into the structure the `storageRules()`
|
|
1767
|
-
* middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
|
|
1768
|
-
* any one allowing grants the operation (each rule grants a slice of the
|
|
1769
|
-
* keyspace).
|
|
1770
|
-
*
|
|
1771
|
-
* Validates against an **accidentally duplicated rule** — the same
|
|
1772
|
-
* `(bucket, on, prefix)` registered with the *same* decision function (a
|
|
1773
|
-
* copy-paste, or the same rule object spread in twice). Because multiple
|
|
1774
|
-
* DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
|
|
1775
|
-
* `when` reference too. Throws at module load so the misconfiguration surfaces
|
|
1776
|
-
* immediately rather than as a silently double-evaluated predicate.
|
|
1777
|
-
*/
|
|
2146
|
+
* Collect a list of storage rules into the structure the `storageRules()`
|
|
2147
|
+
* middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
|
|
2148
|
+
* any one allowing grants the operation (each rule grants a slice of the
|
|
2149
|
+
* keyspace).
|
|
2150
|
+
*
|
|
2151
|
+
* Validates against an **accidentally duplicated rule** — the same
|
|
2152
|
+
* `(bucket, on, prefix)` registered with the *same* decision function (a
|
|
2153
|
+
* copy-paste, or the same rule object spread in twice). Because multiple
|
|
2154
|
+
* DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
|
|
2155
|
+
* `when` reference too. Throws at module load so the misconfiguration surfaces
|
|
2156
|
+
* immediately rather than as a silently double-evaluated predicate.
|
|
2157
|
+
*/
|
|
1778
2158
|
declare const defineStorageRules: <Context = unknown>(rules: ReadonlyArray<StorageRule<Context>>) => ReadonlyArray<StorageRule<Context>>;
|
|
1779
2159
|
/** 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`). */
|
|
1780
2160
|
type StorageAuthLike = {
|
|
@@ -1788,4 +2168,4 @@ interface StorageContextIn {
|
|
|
1788
2168
|
}
|
|
1789
2169
|
declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
|
|
1790
2170
|
declare const VERSION = "0.0.0";
|
|
1791
|
-
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
|
|
2171
|
+
export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, type DataModelInit, type DefineComponentOptions, type DefineIdentityOptions, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type EmptyArgs, type EnvAccessor, type EnvKeyFailure, type EnvShape, type ExposeConfig, 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 IdentityContract, type IdentityRejectMode, type IdentityValidation, type InferArgs, type InferEnv, type InferIdentity, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type LunoraBuilders, LunoraEnvError, LunoraError, 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 MutatorDefinition, 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 RegisteredMutator, type RegisteredQuery, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, 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, buildRlsReadRegistry, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineIdentity, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, httpAction, httpRoute, httpRouter, initLunora, installPlugins, isSafeHeaderValue, mask, mergeSchemaExtension, onConnect, onDisconnect, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules };
|