@lunora/server 1.0.0-alpha.8 → 1.0.0-alpha.80
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 +219 -0
- package/README.md +46 -0
- package/dist/data-model.d.mts +260 -164
- package/dist/data-model.d.ts +260 -164
- package/dist/data-model.mjs +0 -1
- package/dist/drizzle.mjs +1 -1
- package/dist/index.d.mts +1989 -1173
- package/dist/index.d.ts +1989 -1173
- package/dist/index.mjs +1 -28
- package/dist/otel.d.mts +543 -0
- package/dist/otel.d.ts +543 -0
- package/dist/otel.mjs +1 -0
- package/dist/packem_shared/DEFAULT_LIMIT-B947Pbij.mjs +1 -0
- package/dist/packem_shared/LunoraEnvError-CjFbbR9t.mjs +3 -0
- package/dist/packem_shared/LunoraError-DcKdk9Ti.mjs +1 -0
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-C-McIltl.mjs +1 -0
- package/dist/packem_shared/allowAll-DkRAgItV.mjs +1 -0
- package/dist/packem_shared/asBucketStorage-BthCnWop.mjs +1 -0
- package/dist/packem_shared/bindOrm-lWdeDi9P.mjs +1 -0
- package/dist/packem_shared/buildMaskRegistry-72SL8fJn.mjs +1 -0
- package/dist/packem_shared/buildRlsReadRegistry-Deq8inqS.mjs +1 -0
- package/dist/packem_shared/composePluginMiddleware-DQHF6mYr.mjs +1 -0
- package/dist/packem_shared/context-identity-B2XYGA0B.mjs +1 -0
- package/dist/packem_shared/createPolicyDsl-DNfByAUX.mjs +1 -0
- package/dist/packem_shared/createSecrets-CyrEddTF.mjs +1 -0
- package/dist/packem_shared/defineAggregateIndex-BtVAy69G.mjs +1 -0
- package/dist/packem_shared/defineIdentity-DwkNKwYa.mjs +1 -0
- package/dist/packem_shared/defineMigration-7GI9Qbtd.mjs +1 -0
- package/dist/packem_shared/defineMutator-DKy8UtbB.mjs +1 -0
- package/dist/packem_shared/defineShape-DYxbNl5W.mjs +1 -0
- package/dist/packem_shared/defineStorageRule-BvxHVq3b.mjs +1 -0
- package/dist/packem_shared/functions-B2-H4PQT.mjs +1 -0
- package/dist/packem_shared/httpAction-Ddi7f0t6.mjs +5 -0
- package/dist/packem_shared/initLunora-CRypyq1P.mjs +1 -0
- package/dist/packem_shared/mask-CfHNITgg.mjs +1 -0
- package/dist/packem_shared/middleware-DEW0yLvU.mjs +1 -0
- package/dist/packem_shared/onConnect-BLRoOpv2.mjs +1 -0
- package/dist/packem_shared/onQueryChange-gepYD6KC.mjs +1 -0
- package/dist/packem_shared/plugin-DQcLxTx1.mjs +1 -0
- package/dist/packem_shared/policy-tag-D4RzgQnw.mjs +1 -0
- package/dist/packem_shared/protectPublic-Bo4G9xaV.mjs +1 -0
- package/dist/packem_shared/rls-B3mNGBuc.mjs +1 -0
- package/dist/packem_shared/run-middleware-B-JeGGp9.mjs +1 -0
- package/dist/packem_shared/storageRules-CTnby6M1.mjs +1 -0
- package/dist/packem_shared/types.d-B-pIn8rE.d.mts +141 -0
- package/dist/packem_shared/types.d-CdPDEjhz.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 -49
- package/dist/types.d.mts +1552 -506
- package/dist/types.d.ts +1552 -506
- package/dist/types.mjs +1 -31
- package/package.json +17 -4
- package/dist/packem_shared/LunoraEnvError-DjFkpkSP.mjs +0 -187
- package/dist/packem_shared/LunoraError-DhggBJZF.mjs +0 -51
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-C7j9gY_d.mjs +0 -114
- package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
- package/dist/packem_shared/bindOrm-Ce57S3N9.mjs +0 -128
- package/dist/packem_shared/buildRlsReadRegistry-CkxuS69B.mjs +0 -107
- package/dist/packem_shared/composePluginMiddleware-Ck5_TUO8.mjs +0 -100
- package/dist/packem_shared/createPolicyDsl-De67zPDS.mjs +0 -29
- package/dist/packem_shared/createSecrets-TsIP9lOa.mjs +0 -55
- package/dist/packem_shared/defineAggregateIndex-ZdyU78gh.mjs +0 -291
- package/dist/packem_shared/defineMigration-CAJLr6fx.mjs +0 -8
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
- package/dist/packem_shared/defineShape-CJ27Wx7o.mjs +0 -17
- package/dist/packem_shared/defineStorageRule-qu0mpilX.mjs +0 -20
- package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
- package/dist/packem_shared/httpAction-B7FYUEgr.mjs +0 -340
- package/dist/packem_shared/initLunora-lxwHTEV3.mjs +0 -100
- package/dist/packem_shared/mask-eCUYOwhd.mjs +0 -211
- package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
- package/dist/packem_shared/protectPublic-BjFkQ_Or.mjs +0 -15
- package/dist/packem_shared/rls-ClmjkkxZ.mjs +0 -569
- package/dist/packem_shared/run-middleware-CYQOuoV6.mjs +0 -18
- package/dist/packem_shared/storageRules-4a30FSpI.mjs +0 -88
- 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,48 @@
|
|
|
1
|
-
import { Validator, Infer, ValidatorMap, InferValidatorMap, 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, 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 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, ColumnValidator, 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, DurableStreamOptions, RegisteredStream, FunctionKind, Secrets, LifecycleEvent, ShardInitEvent, RegisteredLifecycleHook, TableDefinition, RegisteredFunction, VectorIndexDefinition, Schema, AggregateOp, DurableObjectJurisdiction, RelationDefinition, GlobalBackend, OnDeleteAction, SearchLanguage, SearchStrategy, 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 LunoraWideEvent, type PaginationOptions, type PaginationResult, type RankSortKey, type ReadOnlyStorage, type RestCacheConfig, type RunQueryOptions, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type SearchFilterBuilder, type SearchIndexDefinition, type ShardMode, type SpanEvaluation, type SpanHandle, type SpanKind, type SpanLink, type SpanOptions, 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 WorkflowEventDefinition, 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 {
|
|
7
|
-
|
|
8
|
+
import { WhereOperators, QueryArgs as QueryArgs$2 } from "./data-model.mjs";
|
|
9
|
+
import { W as WhereInput, a as Policy, b as RlsOptions, c as Permission, R as Role, T as TypedDefinePolicyInput, D as DefinePolicyInput } from "./packem_shared/types.d-B-pIn8rE.mjs";
|
|
10
|
+
export type { d as PolicyContext, e as PolicyDecision, f as PolicyDecisionOf, P as PolicyOperation } from "./packem_shared/types.d-B-pIn8rE.mjs";
|
|
8
11
|
export { type CronJob, type CronJobsBuilder, type CronScheduleKind, type DailySchedule, type IntervalSchedule, type MonthlySchedule, type WeeklySchedule, cronJobs } from '@lunora/scheduler';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* (
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Make any resolved storage capability 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
|
+
* Lives here rather than in `@lunora/server` because two packages need it and
|
|
21
|
+
* neither may depend on the other: `@lunora/server` re-exports it as the runtime
|
|
22
|
+
* counterpart `_generated/shard.ts` imports, and `@lunora/runtime` uses it to
|
|
23
|
+
* build `ctx.storage` for an HTTP action from the worker's own R2 bindings.
|
|
24
|
+
* Inlined into each `dist` by the bundler, so no dependency edge is created.
|
|
25
|
+
*
|
|
26
|
+
* The input is genuinely heterogeneous (a thunk result cast through `unknown`),
|
|
27
|
+
* so the signature is `unknown → unknown`; callers cast the result.
|
|
28
|
+
*/
|
|
25
29
|
declare const asBucketStorage: (raw: unknown) => unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Options for the `.stream()` terminal. `durable: true` is shorthand for
|
|
32
|
+
* `durable: {}` — the runtime only ever sees the object form.
|
|
33
|
+
*/
|
|
34
|
+
interface StreamOptions {
|
|
35
|
+
durable?: boolean | DurableStreamOptions;
|
|
36
|
+
}
|
|
26
37
|
/** Builder discriminator. Codegen reads this kind. */
|
|
27
38
|
type TerminalKind = FunctionKind;
|
|
28
39
|
/** Initial (empty) accumulated args for a fresh builder. */
|
|
29
40
|
type EmptyArgs = Record<never, never>;
|
|
30
41
|
/**
|
|
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
|
-
*/
|
|
42
|
+
* `next()` advances the middleware chain. Called with no argument it forwards
|
|
43
|
+
* the current context unchanged; called with `{ ctx }` it shallow-merges the
|
|
44
|
+
* extension, and the result type reflects the widened context.
|
|
45
|
+
*/
|
|
35
46
|
interface MiddlewareNext<ContextIn> {
|
|
36
47
|
(): Promise<ContextIn>;
|
|
37
48
|
<Extension extends Record<string, unknown>>(options: {
|
|
@@ -39,28 +50,46 @@ interface MiddlewareNext<ContextIn> {
|
|
|
39
50
|
}): Promise<ContextIn & Extension>;
|
|
40
51
|
}
|
|
41
52
|
/**
|
|
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
|
-
*/
|
|
53
|
+
* A middleware receives the current context and a `next` continuation. Its
|
|
54
|
+
* return type becomes the builder's new context, so `return next({ ctx })`
|
|
55
|
+
* propagates the extension into every downstream `.use()` and the handler.
|
|
56
|
+
*/
|
|
46
57
|
type Middleware<ContextIn, ContextOut> = (options: {
|
|
47
58
|
ctx: ContextIn;
|
|
48
59
|
next: MiddlewareNext<ContextIn>;
|
|
49
60
|
}) => ContextOut | Promise<ContextOut>;
|
|
50
|
-
/** Options accepted by `initLunora.dataModel
|
|
61
|
+
/** Options accepted by `initLunora.dataModel<DM>().create(...)`. Reserved for transformer/error-formatter wiring. */
|
|
51
62
|
type CreateOptions = Record<never, never>;
|
|
52
63
|
/**
|
|
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
|
-
*/
|
|
64
|
+
* `Output` carries the type declared by `.output(validator)`. It defaults to
|
|
65
|
+
* the `undefined` sentinel meaning "not declared": in that state the terminal
|
|
66
|
+
* stays generic over the handler's own return type. Once `.output()` sets it to
|
|
67
|
+
* a concrete type, the terminal requires the handler to return that type and
|
|
68
|
+
* the registration is typed to it (the runtime parses the result through the
|
|
69
|
+
* validator). `[Output] extends [undefined]` is wrapped in a tuple so a union
|
|
70
|
+
* `Output` doesn't distribute and so the test is for the exact sentinel.
|
|
71
|
+
*/
|
|
61
72
|
interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
62
73
|
readonly __lunoraProcedure: "query";
|
|
74
|
+
/**
|
|
75
|
+
* Publish this query on the opt-in public REST surface (plan 167) — the
|
|
76
|
+
* runtime mints `GET /_lunora/rest/<namespace>/<fn>` (and `POST`), dispatching
|
|
77
|
+
* THROUGH the procedure so `ctx.auth` / RLS / validators are enforced, and the
|
|
78
|
+
* generated OpenAPI describes it. Default-closed: omit to keep it RPC-only.
|
|
79
|
+
*/
|
|
80
|
+
expose: (config: ExposeConfig) => QueryBuilder<Context, Args, Output>;
|
|
63
81
|
input: <A extends ArgsValidator>(validators: A) => QueryBuilder<Context, A & Args, Output>;
|
|
82
|
+
/**
|
|
83
|
+
* Attach static, per-procedure metadata. Merges across calls, is readable
|
|
84
|
+
* from middleware as `ctx.meta`, and is stamped onto the registration as
|
|
85
|
+
* `fn.meta` so codegen and other tooling can enumerate it.
|
|
86
|
+
*
|
|
87
|
+
* The point is policy that is DATA rather than a call: `.meta({ rateLimit:
|
|
88
|
+
* "pins/create" })` can be walked to generate a rate-limit registry or docs,
|
|
89
|
+
* where the same policy expressed only as `.use(rateLimit("pins/create"))`
|
|
90
|
+
* can only be executed. Mirrors tRPC's `.meta()`.
|
|
91
|
+
*/
|
|
92
|
+
meta: (value: Record<string, unknown>) => QueryBuilder<Context, Args, Output>;
|
|
64
93
|
output: <V extends Validator>(validator: V) => QueryBuilder<Context, Args, Infer<V>>;
|
|
65
94
|
query: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
66
95
|
args: InferArgs<Args>;
|
|
@@ -70,23 +99,54 @@ interface QueryBuilder<Context, Args extends ArgsValidator, Output = undefined>
|
|
|
70
99
|
ctx: Context;
|
|
71
100
|
}) => Output | Promise<Output>) => RegisteredQuery<Args, Output>;
|
|
72
101
|
/**
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
102
|
+
* Terminal: declare this procedure as a streaming query. The handler is an
|
|
103
|
+
* async generator (or any function returning an `AsyncIterable<R>`) that
|
|
104
|
+
* yields one chunk per server-pushed frame. The third `signal` argument is
|
|
105
|
+
* tripped when the client cancels — break out of the loop or check
|
|
106
|
+
* `signal.aborted` between yields. `.output()` does not apply: per-chunk
|
|
107
|
+
* validation is opt-in via the handler itself.
|
|
108
|
+
*
|
|
109
|
+
* Pass `{ durable: true }` to make the run outlive the socket that opened
|
|
110
|
+
* it: chunks are persisted as they are produced, so a reload resumes the
|
|
111
|
+
* same run from where it left off instead of dropping the work, and a
|
|
112
|
+
* second client with the same arguments attaches to the same transcript.
|
|
113
|
+
* That is what an LLM response wants; a progress ticker does not need it.
|
|
114
|
+
*/
|
|
80
115
|
stream: <R>(handler: (options: {
|
|
81
116
|
args: InferArgs<Args>;
|
|
82
117
|
ctx: Context;
|
|
83
118
|
signal: AbortSignal;
|
|
84
|
-
}) => AsyncGenerator<R, void, void> | AsyncIterable<R
|
|
119
|
+
}) => AsyncGenerator<R, void, void> | AsyncIterable<R>, options?: StreamOptions) => RegisteredStream<Args, R>;
|
|
85
120
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => QueryBuilder<ContextOut, Args, Output>;
|
|
121
|
+
/**
|
|
122
|
+
* Mark this query as paid. The origin worker answers an unpaid client RPC
|
|
123
|
+
* with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
|
|
124
|
+
* is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
|
|
125
|
+
* recipient, and facilitator come from the worker-level x402 charge config.
|
|
126
|
+
*/
|
|
127
|
+
x402: (config: X402ProcedureConfig) => QueryBuilder<Context, Args, Output>;
|
|
86
128
|
}
|
|
87
129
|
interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
88
130
|
readonly __lunoraProcedure: "mutation";
|
|
131
|
+
/**
|
|
132
|
+
* Publish this mutation on the opt-in public REST surface (plan 167) — the
|
|
133
|
+
* runtime mints `POST /_lunora/rest/<namespace>/<fn>`, dispatching THROUGH the
|
|
134
|
+
* procedure so `ctx.auth` / RLS / validators are enforced, and the generated
|
|
135
|
+
* OpenAPI describes it. Default-closed: omit to keep it RPC-only.
|
|
136
|
+
*/
|
|
137
|
+
expose: (config: ExposeConfig) => MutationBuilder<Context, Args, Output>;
|
|
89
138
|
input: <A extends ArgsValidator>(validators: A) => MutationBuilder<Context, A & Args, Output>;
|
|
139
|
+
/**
|
|
140
|
+
* Attach static, per-procedure metadata. Merges across calls, is readable
|
|
141
|
+
* from middleware as `ctx.meta`, and is stamped onto the registration as
|
|
142
|
+
* `fn.meta` so codegen and other tooling can enumerate it.
|
|
143
|
+
*
|
|
144
|
+
* The point is policy that is DATA rather than a call: `.meta({ rateLimit:
|
|
145
|
+
* "pins/create" })` can be walked to generate a rate-limit registry or docs,
|
|
146
|
+
* where the same policy expressed only as `.use(rateLimit("pins/create"))`
|
|
147
|
+
* can only be executed. Mirrors tRPC's `.meta()`.
|
|
148
|
+
*/
|
|
149
|
+
meta: (value: Record<string, unknown>) => MutationBuilder<Context, Args, Output>;
|
|
90
150
|
mutation: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
91
151
|
args: InferArgs<Args>;
|
|
92
152
|
ctx: Context;
|
|
@@ -96,6 +156,13 @@ interface MutationBuilder<Context, Args extends ArgsValidator, Output = undefine
|
|
|
96
156
|
}) => Output | Promise<Output>) => RegisteredMutation<Args, Output>;
|
|
97
157
|
output: <V extends Validator>(validator: V) => MutationBuilder<Context, Args, Infer<V>>;
|
|
98
158
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => MutationBuilder<ContextOut, Args, Output>;
|
|
159
|
+
/**
|
|
160
|
+
* Mark this mutation as paid. The origin worker answers an unpaid client RPC
|
|
161
|
+
* with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
|
|
162
|
+
* is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
|
|
163
|
+
* recipient, and facilitator come from the worker-level x402 charge config.
|
|
164
|
+
*/
|
|
165
|
+
x402: (config: X402ProcedureConfig) => MutationBuilder<Context, Args, Output>;
|
|
99
166
|
}
|
|
100
167
|
interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
101
168
|
readonly __lunoraProcedure: "action";
|
|
@@ -106,20 +173,56 @@ interface ActionBuilder<Context, Args extends ArgsValidator, Output = undefined>
|
|
|
106
173
|
args: InferArgs<Args>;
|
|
107
174
|
ctx: Context;
|
|
108
175
|
}) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
|
|
176
|
+
/**
|
|
177
|
+
* Publish this action on the opt-in public REST surface (plan 167) — the
|
|
178
|
+
* runtime mints `POST /_lunora/rest/<namespace>/<fn>`, dispatching THROUGH the
|
|
179
|
+
* procedure so `ctx.auth` / RLS / validators are enforced, and the generated
|
|
180
|
+
* OpenAPI describes it. Default-closed: omit to keep it RPC-only.
|
|
181
|
+
*/
|
|
182
|
+
expose: (config: ExposeConfig) => ActionBuilder<Context, Args, Output>;
|
|
109
183
|
input: <A extends ArgsValidator>(validators: A) => ActionBuilder<Context, A & Args, Output>;
|
|
184
|
+
/**
|
|
185
|
+
* Attach static, per-procedure metadata. Merges across calls, is readable
|
|
186
|
+
* from middleware as `ctx.meta`, and is stamped onto the registration as
|
|
187
|
+
* `fn.meta` so codegen and other tooling can enumerate it.
|
|
188
|
+
*
|
|
189
|
+
* The point is policy that is DATA rather than a call: `.meta({ rateLimit:
|
|
190
|
+
* "pins/create" })` can be walked to generate a rate-limit registry or docs,
|
|
191
|
+
* where the same policy expressed only as `.use(rateLimit("pins/create"))`
|
|
192
|
+
* can only be executed. Mirrors tRPC's `.meta()`.
|
|
193
|
+
*/
|
|
194
|
+
meta: (value: Record<string, unknown>) => ActionBuilder<Context, Args, Output>;
|
|
110
195
|
output: <V extends Validator>(validator: V) => ActionBuilder<Context, Args, Infer<V>>;
|
|
111
196
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => ActionBuilder<ContextOut, Args, Output>;
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
|
|
118
|
-
|
|
197
|
+
/**
|
|
198
|
+
* Mark this action as paid. The origin worker answers an unpaid client RPC
|
|
199
|
+
* with HTTP 402, verifies + settles the x402 payment, then dispatches. `price`
|
|
200
|
+
* is USD (a number of dollars or a `"0.01"`/`"$0.01"` string); the network,
|
|
201
|
+
* recipient, and facilitator come from the worker-level x402 charge config.
|
|
202
|
+
*/
|
|
203
|
+
x402: (config: X402ProcedureConfig) => ActionBuilder<Context, Args, Output>;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Internal builder variants. Identical to their public counterparts but carry
|
|
207
|
+
* the `__lunoraVisibility: "internal"` brand codegen keys off to route the
|
|
208
|
+
* registration into the `internal` object (and keep it off `api`). `input`/`use`
|
|
209
|
+
* return the internal builder type so the brand survives the whole chain.
|
|
210
|
+
*/
|
|
119
211
|
interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
120
212
|
readonly __lunoraProcedure: "query";
|
|
121
213
|
readonly __lunoraVisibility: "internal";
|
|
122
214
|
input: <A extends ArgsValidator>(validators: A) => InternalQueryBuilder<Context, A & Args, Output>;
|
|
215
|
+
/**
|
|
216
|
+
* Attach static, per-procedure metadata. Merges across calls, is readable
|
|
217
|
+
* from middleware as `ctx.meta`, and is stamped onto the registration as
|
|
218
|
+
* `fn.meta` so codegen and other tooling can enumerate it.
|
|
219
|
+
*
|
|
220
|
+
* The point is policy that is DATA rather than a call: `.meta({ rateLimit:
|
|
221
|
+
* "pins/create" })` can be walked to generate a rate-limit registry or docs,
|
|
222
|
+
* where the same policy expressed only as `.use(rateLimit("pins/create"))`
|
|
223
|
+
* can only be executed. Mirrors tRPC's `.meta()`.
|
|
224
|
+
*/
|
|
225
|
+
meta: (value: Record<string, unknown>) => InternalQueryBuilder<Context, Args, Output>;
|
|
123
226
|
output: <V extends Validator>(validator: V) => InternalQueryBuilder<Context, Args, Infer<V>>;
|
|
124
227
|
query: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
125
228
|
args: InferArgs<Args>;
|
|
@@ -133,13 +236,24 @@ interface InternalQueryBuilder<Context, Args extends ArgsValidator, Output = und
|
|
|
133
236
|
args: InferArgs<Args>;
|
|
134
237
|
ctx: Context;
|
|
135
238
|
signal: AbortSignal;
|
|
136
|
-
}) => AsyncGenerator<R, void, void> | AsyncIterable<R
|
|
239
|
+
}) => AsyncGenerator<R, void, void> | AsyncIterable<R>, options?: StreamOptions) => RegisteredStream<Args, R>;
|
|
137
240
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalQueryBuilder<ContextOut, Args, Output>;
|
|
138
241
|
}
|
|
139
242
|
interface InternalMutationBuilder<Context, Args extends ArgsValidator, Output = undefined> {
|
|
140
243
|
readonly __lunoraProcedure: "mutation";
|
|
141
244
|
readonly __lunoraVisibility: "internal";
|
|
142
245
|
input: <A extends ArgsValidator>(validators: A) => InternalMutationBuilder<Context, A & Args, Output>;
|
|
246
|
+
/**
|
|
247
|
+
* Attach static, per-procedure metadata. Merges across calls, is readable
|
|
248
|
+
* from middleware as `ctx.meta`, and is stamped onto the registration as
|
|
249
|
+
* `fn.meta` so codegen and other tooling can enumerate it.
|
|
250
|
+
*
|
|
251
|
+
* The point is policy that is DATA rather than a call: `.meta({ rateLimit:
|
|
252
|
+
* "pins/create" })` can be walked to generate a rate-limit registry or docs,
|
|
253
|
+
* where the same policy expressed only as `.use(rateLimit("pins/create"))`
|
|
254
|
+
* can only be executed. Mirrors tRPC's `.meta()`.
|
|
255
|
+
*/
|
|
256
|
+
meta: (value: Record<string, unknown>) => InternalMutationBuilder<Context, Args, Output>;
|
|
143
257
|
mutation: [Output] extends [undefined] ? <R>(handler: (options: {
|
|
144
258
|
args: InferArgs<Args>;
|
|
145
259
|
ctx: Context;
|
|
@@ -161,6 +275,17 @@ interface InternalActionBuilder<Context, Args extends ArgsValidator, Output = un
|
|
|
161
275
|
ctx: Context;
|
|
162
276
|
}) => Output | Promise<Output>) => RegisteredAction<Args, Output>;
|
|
163
277
|
input: <A extends ArgsValidator>(validators: A) => InternalActionBuilder<Context, A & Args, Output>;
|
|
278
|
+
/**
|
|
279
|
+
* Attach static, per-procedure metadata. Merges across calls, is readable
|
|
280
|
+
* from middleware as `ctx.meta`, and is stamped onto the registration as
|
|
281
|
+
* `fn.meta` so codegen and other tooling can enumerate it.
|
|
282
|
+
*
|
|
283
|
+
* The point is policy that is DATA rather than a call: `.meta({ rateLimit:
|
|
284
|
+
* "pins/create" })` can be walked to generate a rate-limit registry or docs,
|
|
285
|
+
* where the same policy expressed only as `.use(rateLimit("pins/create"))`
|
|
286
|
+
* can only be executed. Mirrors tRPC's `.meta()`.
|
|
287
|
+
*/
|
|
288
|
+
meta: (value: Record<string, unknown>) => InternalActionBuilder<Context, Args, Output>;
|
|
164
289
|
output: <V extends Validator>(validator: V) => InternalActionBuilder<Context, Args, Infer<V>>;
|
|
165
290
|
use: <ContextOut>(middleware: Middleware<Context, ContextOut>) => InternalActionBuilder<ContextOut, Args, Output>;
|
|
166
291
|
}
|
|
@@ -179,38 +304,38 @@ interface DataModelInit<DataModel> {
|
|
|
179
304
|
create: (options?: CreateOptions) => LunoraBuilders;
|
|
180
305
|
}
|
|
181
306
|
/**
|
|
182
|
-
* Entry point for the procedure builder. `dataModel
|
|
183
|
-
* `DataModel` (phantom for now), and `.create()` yields the public root builders
|
|
184
|
-
* plus their `internal*` counterparts.
|
|
185
|
-
*/
|
|
307
|
+
* Entry point for the procedure builder. `dataModel<DM>()` binds the generated
|
|
308
|
+
* `DataModel` (phantom for now), and `.create()` yields the public root builders
|
|
309
|
+
* plus their `internal*` counterparts.
|
|
310
|
+
*/
|
|
186
311
|
declare const initLunora: {
|
|
187
312
|
dataModel: <DataModel>() => DataModelInit<DataModel>;
|
|
188
313
|
};
|
|
189
314
|
/**
|
|
190
|
-
* Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
|
|
191
|
-
* `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
|
|
192
|
-
* absent or non-Secrets-Store binding throws a directed error pointing at the
|
|
193
|
-
* wrangler config; the lookup is lazy, so an unused secret never resolves.
|
|
194
|
-
*/
|
|
315
|
+
* Build the `ctx.secrets` reader from the worker `env`. `get(name)` resolves
|
|
316
|
+
* `env[name].get()` — the `secrets_store_secrets[]` binding of that name. An
|
|
317
|
+
* absent or non-Secrets-Store binding throws a directed error pointing at the
|
|
318
|
+
* wrangler config; the lookup is lazy, so an unused secret never resolves.
|
|
319
|
+
*/
|
|
195
320
|
declare const createSecrets: (env: Record<string, unknown>) => Secrets;
|
|
196
321
|
/**
|
|
197
|
-
* Redact secrets from a free-form message. Masks, in order: any quoted value
|
|
198
|
-
* whose contents look like a credential (so a value surfaced as `received string
|
|
199
|
-
* "sk_live_…"` is masked even though the surrounding text is not a token); a
|
|
200
|
-
* `scheme://user:password@host` URL credential (the password segment); any
|
|
201
|
-
* known-prefix credential token wherever it appears, at any length; any value
|
|
202
|
-
* following a secret-named key in `KEY=value` / `KEY: value` form; and any
|
|
203
|
-
* remaining bare high-entropy ≥24-char token run anywhere in the message.
|
|
204
|
-
*
|
|
205
|
-
* This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
|
|
206
|
-
* secret under a non-secret-named key (and embedded credentials in shapes not
|
|
207
|
-
* enumerated here) can still slip through. Treat it as a backstop — prefer
|
|
208
|
-
* structured logging that never serializes raw env/secret fields in the first
|
|
209
|
-
* place over relying on post-hoc scrubbing of untrusted data.
|
|
210
|
-
*
|
|
211
|
-
* Exported because it is independently useful — call it before logging anything
|
|
212
|
-
* derived from `env`, request bodies, or thrown errors.
|
|
213
|
-
*/
|
|
322
|
+
* Redact secrets from a free-form message. Masks, in order: any quoted value
|
|
323
|
+
* whose contents look like a credential (so a value surfaced as `received string
|
|
324
|
+
* "sk_live_…"` is masked even though the surrounding text is not a token); a
|
|
325
|
+
* `scheme://user:password@host` URL credential (the password segment); any
|
|
326
|
+
* known-prefix credential token wherever it appears, at any length; any value
|
|
327
|
+
* following a secret-named key in `KEY=value` / `KEY: value` form; and any
|
|
328
|
+
* remaining bare high-entropy ≥24-char token run anywhere in the message.
|
|
329
|
+
*
|
|
330
|
+
* This is BEST-EFFORT defense-in-depth, NOT a guarantee: a short, prefix-less
|
|
331
|
+
* secret under a non-secret-named key (and embedded credentials in shapes not
|
|
332
|
+
* enumerated here) can still slip through. Treat it as a backstop — prefer
|
|
333
|
+
* structured logging that never serializes raw env/secret fields in the first
|
|
334
|
+
* place over relying on post-hoc scrubbing of untrusted data.
|
|
335
|
+
*
|
|
336
|
+
* Exported because it is independently useful — call it before logging anything
|
|
337
|
+
* derived from `env`, request bodies, or thrown errors.
|
|
338
|
+
*/
|
|
214
339
|
declare const redactSecrets: (message: string) => string;
|
|
215
340
|
/** One key's validation failure, secrets already redacted out of `message`. */
|
|
216
341
|
interface EnvKeyFailure {
|
|
@@ -220,32 +345,31 @@ interface EnvKeyFailure {
|
|
|
220
345
|
message: string;
|
|
221
346
|
}
|
|
222
347
|
/**
|
|
223
|
-
* Thrown when one or more env keys are missing or fail validation. Carries the
|
|
224
|
-
* structured list of `failures` (each with the offending `key`) so callers can
|
|
225
|
-
* react programmatically; `message` is the joined, secret-redacted summary.
|
|
226
|
-
*
|
|
227
|
-
* Named export only (no default) per the repo export convention.
|
|
228
|
-
*/
|
|
229
|
-
declare class LunoraEnvError extends
|
|
230
|
-
override readonly name = "LunoraEnvError";
|
|
348
|
+
* Thrown when one or more env keys are missing or fail validation. Carries the
|
|
349
|
+
* structured list of `failures` (each with the offending `key`) so callers can
|
|
350
|
+
* react programmatically; `message` is the joined, secret-redacted summary.
|
|
351
|
+
*
|
|
352
|
+
* Named export only (no default) per the repo export convention.
|
|
353
|
+
*/
|
|
354
|
+
declare class LunoraEnvError extends LunoraError$1 {
|
|
231
355
|
readonly failures: ReadonlyArray<EnvKeyFailure>;
|
|
232
356
|
constructor(failures: ReadonlyArray<EnvKeyFailure>);
|
|
233
357
|
}
|
|
234
358
|
/** A record of `v.*` validators describing the expected env shape. */
|
|
235
359
|
type EnvShape = Record<string, Validator>;
|
|
236
360
|
/**
|
|
237
|
-
* The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
|
|
238
|
-
* become optional keys; everything else is required. Mirrors how `InferArgs`
|
|
239
|
-
* derives an args object from a validator map.
|
|
240
|
-
*/
|
|
241
|
-
type InferEnv<S extends EnvShape> = { [K in keyof S as undefined extends Infer<S[K]> ? K : never]?: Infer<S[K]
|
|
242
|
-
/**
|
|
243
|
-
* The accessor returned by {@link defineEnv}. A typed view over an `env` object
|
|
244
|
-
* plus a `.parse(env)` escape hatch that validates every key eagerly.
|
|
245
|
-
*
|
|
246
|
-
* Call the accessor with the worker's `env` to get the typed, lazily-validated
|
|
247
|
-
* proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
|
|
248
|
-
*/
|
|
361
|
+
* The typed output of {@link defineEnv}. Optional validators (`v.optional(...)`)
|
|
362
|
+
* become optional keys; everything else is required. Mirrors how `InferArgs`
|
|
363
|
+
* derives an args object from a validator map.
|
|
364
|
+
*/
|
|
365
|
+
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]>; };
|
|
366
|
+
/**
|
|
367
|
+
* The accessor returned by {@link defineEnv}. A typed view over an `env` object
|
|
368
|
+
* plus a `.parse(env)` escape hatch that validates every key eagerly.
|
|
369
|
+
*
|
|
370
|
+
* Call the accessor with the worker's `env` to get the typed, lazily-validated
|
|
371
|
+
* proxy: `const config = defineEnv({ … }); const { PORT } = config(env);`.
|
|
372
|
+
*/
|
|
249
373
|
interface EnvAccessor<S extends EnvShape> {
|
|
250
374
|
/** Validate every key eagerly and return the typed, plain (non-proxy) object. Use for fail-fast-at-boot. */
|
|
251
375
|
parse: (env: unknown) => InferEnv<S>;
|
|
@@ -253,91 +377,40 @@ interface EnvAccessor<S extends EnvShape> {
|
|
|
253
377
|
(env: unknown): InferEnv<S>;
|
|
254
378
|
}
|
|
255
379
|
/**
|
|
256
|
-
* Define a typed, validated accessor over a Worker's `env`. Pass a record of
|
|
257
|
-
* `v.*` validators; receive an accessor that validates lazily per key (cached
|
|
258
|
-
* per `env` identity) and infers its output type from the validators.
|
|
259
|
-
*
|
|
260
|
-
* ```ts
|
|
261
|
-
* import { defineEnv, v } from "@lunora/server";
|
|
262
|
-
*
|
|
263
|
-
* const config = defineEnv({
|
|
264
|
-
* STRIPE_KEY: v.string(),
|
|
265
|
-
* PORT: v.optional(v.number()),
|
|
266
|
-
* });
|
|
267
|
-
*
|
|
268
|
-
* export default {
|
|
269
|
-
* fetch(request, env) {
|
|
270
|
-
* const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
|
|
271
|
-
* // …
|
|
272
|
-
* },
|
|
273
|
-
* };
|
|
274
|
-
* ```
|
|
275
|
-
*
|
|
276
|
-
* Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
|
|
277
|
-
* invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
|
|
278
|
-
*/
|
|
380
|
+
* Define a typed, validated accessor over a Worker's `env`. Pass a record of
|
|
381
|
+
* `v.*` validators; receive an accessor that validates lazily per key (cached
|
|
382
|
+
* per `env` identity) and infers its output type from the validators.
|
|
383
|
+
*
|
|
384
|
+
* ```ts
|
|
385
|
+
* import { defineEnv, v } from "@lunora/server";
|
|
386
|
+
*
|
|
387
|
+
* const config = defineEnv({
|
|
388
|
+
* STRIPE_KEY: v.string(),
|
|
389
|
+
* PORT: v.optional(v.number()),
|
|
390
|
+
* });
|
|
391
|
+
*
|
|
392
|
+
* export default {
|
|
393
|
+
* fetch(request, env) {
|
|
394
|
+
* const { STRIPE_KEY, PORT } = config(env); // STRIPE_KEY: string, PORT?: number
|
|
395
|
+
* // …
|
|
396
|
+
* },
|
|
397
|
+
* };
|
|
398
|
+
* ```
|
|
399
|
+
*
|
|
400
|
+
* Throws {@link LunoraEnvError} (secrets redacted) when a key is missing or
|
|
401
|
+
* invalid — lazily on first access of that key, or eagerly via `config.parse(env)`.
|
|
402
|
+
*/
|
|
279
403
|
declare const defineEnv: <S extends EnvShape>(shape: S) => EnvAccessor<S>;
|
|
404
|
+
declare class LunoraError extends LunoraError$1 {
|
|
405
|
+
constructor(code: LunoraErrorCode, message?: string, data?: unknown);
|
|
406
|
+
}
|
|
280
407
|
/**
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
286
|
-
|
|
287
|
-
*/
|
|
288
|
-
declare const CODE_STATUS: {
|
|
289
|
-
readonly BAD_REQUEST: 400;
|
|
290
|
-
readonly CONFLICT: 409;
|
|
291
|
-
/**
|
|
292
|
-
* `count()` invoked against a table whose context carries an active RLS
|
|
293
|
-
* policy. The operation itself is unsupported in an RLS-restricted reader
|
|
294
|
-
* (kitcn's documented constraint) — the request is well-formed and the
|
|
295
|
-
* caller is authorized, so this is a 422 (semantic conflict) rather than a
|
|
296
|
-
* 403 (policy denial).
|
|
297
|
-
*/
|
|
298
|
-
readonly COUNT_RLS_UNSUPPORTED: 422;
|
|
299
|
-
readonly FORBIDDEN: 403;
|
|
300
|
-
readonly INTERNAL_SERVER_ERROR: 500;
|
|
301
|
-
/**
|
|
302
|
-
* An analytical reduction (`aggregate` / `groupBy`) was invoked over a
|
|
303
|
-
* column that the procedure's `mask()` middleware redacts. A masked column
|
|
304
|
-
* can't be summed, averaged, or grouped without leaking the very values the
|
|
305
|
-
* mask hides (a group key *is* the raw value; an aggregate is computed from
|
|
306
|
-
* it), so the operation fails closed. The request is well-formed and the
|
|
307
|
-
* caller is authorized — this is a 422 (semantic conflict), mirroring
|
|
308
|
-
* `COUNT_RLS_UNSUPPORTED`.
|
|
309
|
-
*/
|
|
310
|
-
readonly MASK_UNSUPPORTED: 422;
|
|
311
|
-
readonly NOT_FOUND: 404;
|
|
312
|
-
readonly NOT_IMPLEMENTED: 501;
|
|
313
|
-
/**
|
|
314
|
-
* A write policy's `when` returned a relation-crossing predicate
|
|
315
|
-
* (`some`/`none`/`every`/`is`/`isNot`). The in-memory write-policy evaluator
|
|
316
|
-
* has no child fetcher and cannot resolve a relation node, so the policy is
|
|
317
|
-
* unsupported as written. Relation predicates are valid in *read* policies
|
|
318
|
-
* and query `where` clauses (the pre-resolver handles them there). The
|
|
319
|
-
* request is well-formed; this is a 422 (semantic conflict), mirroring the
|
|
320
|
-
* sibling `*_UNSUPPORTED` codes.
|
|
321
|
-
*/
|
|
322
|
-
readonly RELATION_PREDICATE_UNSUPPORTED: 422;
|
|
323
|
-
readonly TOO_MANY_REQUESTS: 429;
|
|
324
|
-
readonly UNAUTHORIZED: 401;
|
|
325
|
-
readonly UNPROCESSABLE: 422;
|
|
326
|
-
};
|
|
327
|
-
type LunoraErrorCode = keyof typeof CODE_STATUS;
|
|
328
|
-
declare class LunoraError extends Error {
|
|
329
|
-
override readonly name = "LunoraError";
|
|
330
|
-
readonly code: LunoraErrorCode;
|
|
331
|
-
readonly status: number;
|
|
332
|
-
constructor(code: LunoraErrorCode, message?: string);
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* Minimal structural writer the facade binds over. Declared with **method**
|
|
336
|
-
* syntax (not arrow properties) so a more-specifically-typed writer — both
|
|
337
|
-
* `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
|
|
338
|
-
* stays assignable under bivariant parameter checking. That is the whole reason
|
|
339
|
-
* the shared helper can serve both callers, hence the rule exemption.
|
|
340
|
-
*/
|
|
408
|
+
* Minimal structural writer the facade binds over. Declared with **method**
|
|
409
|
+
* syntax (not arrow properties) so a more-specifically-typed writer — both
|
|
410
|
+
* `@lunora/do`'s `DatabaseWriterLike` and the RLS middleware's wrapped writer —
|
|
411
|
+
* stays assignable under bivariant parameter checking. That is the whole reason
|
|
412
|
+
* the shared helper can serve both callers, hence the rule exemption.
|
|
413
|
+
*/
|
|
341
414
|
interface FacadeWriterLike {
|
|
342
415
|
aggregate(tableName: string, options: unknown): Promise<unknown>;
|
|
343
416
|
count(tableName: string, where?: unknown): Promise<number>;
|
|
@@ -349,6 +422,11 @@ interface FacadeWriterLike {
|
|
|
349
422
|
}, expectedTable?: string): Promise<{
|
|
350
423
|
deleted: number;
|
|
351
424
|
}>;
|
|
425
|
+
deleteWhere?(tableName: string, where: Record<string, unknown>, options?: {
|
|
426
|
+
limit?: number;
|
|
427
|
+
}): Promise<{
|
|
428
|
+
deleted: number;
|
|
429
|
+
}>;
|
|
352
430
|
findFirst(tableName: string, args?: unknown): Promise<unknown>;
|
|
353
431
|
findFirstOrThrow(tableName: string, args?: unknown): Promise<unknown>;
|
|
354
432
|
findMany(tableName: string, args?: unknown): Promise<unknown>;
|
|
@@ -357,15 +435,27 @@ interface FacadeWriterLike {
|
|
|
357
435
|
insert(tableName: string, document: Record<string, unknown>): Promise<string>;
|
|
358
436
|
insertMany?(tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
359
437
|
limit?: number;
|
|
360
|
-
|
|
438
|
+
skipDuplicates?: boolean;
|
|
439
|
+
}): Promise<(string | null)[]>;
|
|
361
440
|
patch(id: string, patch: Record<string, unknown>, expectedTable?: string): Promise<void>;
|
|
362
441
|
patchMany?(patches: ReadonlyArray<{
|
|
363
442
|
id: string;
|
|
364
443
|
patch: Record<string, unknown>;
|
|
365
444
|
}>, options?: {
|
|
366
445
|
limit?: number;
|
|
367
|
-
}, expectedTable?: string): Promise<
|
|
446
|
+
}, expectedTable?: string): Promise<{
|
|
447
|
+
patched: number;
|
|
448
|
+
}>;
|
|
449
|
+
patchWhere?(tableName: string, args: {
|
|
450
|
+
patch: Record<string, unknown>;
|
|
451
|
+
where: Record<string, unknown>;
|
|
452
|
+
}, options?: {
|
|
453
|
+
limit?: number;
|
|
454
|
+
}): Promise<{
|
|
455
|
+
patched: number;
|
|
456
|
+
}>;
|
|
368
457
|
query(tableName: string): {
|
|
458
|
+
withGeoIndex(indexName: string, build: (q: unknown) => unknown): unknown;
|
|
369
459
|
withSearchIndex(indexName: string, search: (q: unknown) => unknown): unknown;
|
|
370
460
|
};
|
|
371
461
|
rank(tableName: string, indexName: string, options: unknown): Promise<unknown>;
|
|
@@ -378,11 +468,19 @@ interface FacadeEntry {
|
|
|
378
468
|
aggregate: (options: unknown) => Promise<unknown>;
|
|
379
469
|
count: (where?: unknown) => Promise<number>;
|
|
380
470
|
delete: (id: string) => Promise<void>;
|
|
381
|
-
deleteMany:
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
471
|
+
deleteMany: {
|
|
472
|
+
(ids: ReadonlyArray<string>, options?: {
|
|
473
|
+
limit?: number;
|
|
474
|
+
}): Promise<{
|
|
475
|
+
deleted: number;
|
|
476
|
+
}>;
|
|
477
|
+
(args: {
|
|
478
|
+
limit?: number;
|
|
479
|
+
where: Record<string, unknown>;
|
|
480
|
+
}): Promise<{
|
|
481
|
+
deleted: number;
|
|
482
|
+
}>;
|
|
483
|
+
};
|
|
386
484
|
/** `true` when at least one row matches `where` (or any row exists when omitted). Honors RLS like `findFirst`. */
|
|
387
485
|
exists: (where?: unknown) => Promise<boolean>;
|
|
388
486
|
findFirst: (args?: unknown) => Promise<unknown>;
|
|
@@ -393,16 +491,34 @@ interface FacadeEntry {
|
|
|
393
491
|
/** Physically remove a row (and physically cascade), bypassing `.softDelete()`. */
|
|
394
492
|
hardDelete: (id: string) => Promise<void>;
|
|
395
493
|
insert: (document: Record<string, unknown>, options?: FacadeInsertOptions) => Promise<null | string>;
|
|
494
|
+
/**
|
|
495
|
+
* Insert many documents into this table in one call. With
|
|
496
|
+
* `{ skipDuplicates: true }`, UNIQUE breaches resolve to `null` for that row
|
|
497
|
+
* instead of failing the batch. The typed facade narrows the return to
|
|
498
|
+
* `Id<T>[]` when skipDuplicates is not requested.
|
|
499
|
+
*/
|
|
396
500
|
insertMany: (documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
397
501
|
limit?: number;
|
|
398
|
-
|
|
502
|
+
skipDuplicates?: boolean;
|
|
503
|
+
}) => Promise<(string | null)[]>;
|
|
399
504
|
patch: (id: string, patch: Record<string, unknown>) => Promise<void>;
|
|
400
|
-
patchMany:
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
505
|
+
patchMany: {
|
|
506
|
+
(patches: ReadonlyArray<{
|
|
507
|
+
id: string;
|
|
508
|
+
values: Record<string, unknown>;
|
|
509
|
+
}>, options?: {
|
|
510
|
+
limit?: number;
|
|
511
|
+
}): Promise<{
|
|
512
|
+
patched: number;
|
|
513
|
+
}>;
|
|
514
|
+
(args: {
|
|
515
|
+
limit?: number;
|
|
516
|
+
values: Record<string, unknown>;
|
|
517
|
+
where: Record<string, unknown>;
|
|
518
|
+
}): Promise<{
|
|
519
|
+
patched: number;
|
|
520
|
+
}>;
|
|
521
|
+
};
|
|
406
522
|
rank: (indexName: string, options: unknown) => Promise<unknown>;
|
|
407
523
|
rankPage: (indexName: string, options?: unknown) => Promise<unknown>;
|
|
408
524
|
replace: (id: string, document: Record<string, unknown>) => Promise<void>;
|
|
@@ -412,15 +528,16 @@ interface FacadeEntry {
|
|
|
412
528
|
upsert: (args: UpsertArgs) => Promise<UpsertResult>;
|
|
413
529
|
/** Sequential `upsert` over many rows sharing one `target`; returns one result per input row in order. */
|
|
414
530
|
upsertMany: (args: UpsertManyArgs) => Promise<UpsertResult[]>;
|
|
531
|
+
withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => unknown;
|
|
415
532
|
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => unknown;
|
|
416
533
|
}
|
|
417
534
|
/** Options accepted by the per-table `insert` accessor. */
|
|
418
535
|
interface FacadeInsertOptions {
|
|
419
536
|
/**
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
537
|
+
* When `true`, a UNIQUE-constraint breach is swallowed: the insert becomes a
|
|
538
|
+
* silent no-op and resolves to `null` instead of throwing a `CONFLICT`. Any
|
|
539
|
+
* other error still propagates. Mirrors better-drizzle's `create({ skipDuplicates })`.
|
|
540
|
+
*/
|
|
424
541
|
skipDuplicates?: boolean;
|
|
425
542
|
}
|
|
426
543
|
/** The conflict target for `upsert`/`upsertMany`: one field name or a tuple of them. */
|
|
@@ -448,17 +565,17 @@ interface UpsertManyArgs {
|
|
|
448
565
|
target: UpsertTarget;
|
|
449
566
|
}
|
|
450
567
|
/**
|
|
451
|
-
* Bind a structural writer to one table, producing its `ctx.db` table accessor.
|
|
452
|
-
*
|
|
453
|
-
* The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
|
|
454
|
-
* `tableName` as `expectedTable` so the underlying writer scopes its id lookup
|
|
455
|
-
* to this table. Without it, a branded `Id
|
|
456
|
-
* id would resolve cross-table (the writer probes every table by id), letting
|
|
457
|
-
* `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
|
|
458
|
-
* mutate — a row in an unrelated table (IDOR). Writers that ignore the second
|
|
459
|
-
* argument keep their previous global behaviour; the scoping is opt-in via this
|
|
460
|
-
* forwarded name.
|
|
461
|
-
*/
|
|
568
|
+
* Bind a structural writer to one table, producing its `ctx.db` table accessor.
|
|
569
|
+
*
|
|
570
|
+
* The by-id accessors (`get`/`delete`/`patch`/`replace`) forward the bound
|
|
571
|
+
* `tableName` as `expectedTable` so the underlying writer scopes its id lookup
|
|
572
|
+
* to this table. Without it, a branded `Id<"posts">` carrying another table's
|
|
573
|
+
* id would resolve cross-table (the writer probes every table by id), letting
|
|
574
|
+
* `ctx.db.posts.get(foreignId)` read — or `.delete`/`.patch`/`.replace`
|
|
575
|
+
* mutate — a row in an unrelated table (IDOR). Writers that ignore the second
|
|
576
|
+
* argument keep their previous global behaviour; the scoping is opt-in via this
|
|
577
|
+
* forwarded name.
|
|
578
|
+
*/
|
|
462
579
|
declare const bindTableFacade: (writer: FacadeWriterLike, tableName: string) => FacadeEntry;
|
|
463
580
|
/** The kitcn-style `ctx.orm` namespace over a per-table facade map. */
|
|
464
581
|
interface OrmLike {
|
|
@@ -479,21 +596,40 @@ declare const bindOrm: (facade: Record<string, FacadeEntry>) => OrmLike;
|
|
|
479
596
|
/** HTTP verbs the typed {@link httpRoute} builder can bind to. */
|
|
480
597
|
type HttpMethod = "DELETE" | "GET" | "HEAD" | "OPTIONS" | "PATCH" | "POST" | "PUT";
|
|
481
598
|
/**
|
|
482
|
-
* Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
|
|
483
|
-
* HTTP actions run in the worker (the "action runtime"), separate from the
|
|
484
|
-
* transactional store, so there is no direct `db` / `vectors`
|
|
485
|
-
*
|
|
486
|
-
* `
|
|
487
|
-
|
|
488
|
-
|
|
599
|
+
* Context handed to an HTTP action handler. A narrower view of {@link ActionContext}:
|
|
600
|
+
* HTTP actions run in the worker (the "action runtime"), separate from the
|
|
601
|
+
* transactional store, so there is no direct `db` / `vectors` surface — reach the
|
|
602
|
+
* data layer through `runQuery` / `runMutation` / `runAction`, which forward to
|
|
603
|
+
* the owning shard. `db`'s absence is principled: an HTTP handler is not
|
|
604
|
+
* transactional.
|
|
605
|
+
*
|
|
606
|
+
* `scheduler` and `storage` ARE present, because neither needs the shard — the
|
|
607
|
+
* scheduler talks to the scheduler DO, and R2 is a worker binding an HTTP
|
|
608
|
+
* handler can reach where an action does. Both are optional: each exists only
|
|
609
|
+
* when the app declared the matching capability (`.scheduler(...)` /
|
|
610
|
+
* `.storage(...)`) on the generated app builder.
|
|
611
|
+
*
|
|
612
|
+
* Omitting them was costly out of proportion to the gap. Without `scheduler`,
|
|
613
|
+
* "receive webhook → enqueue the real work → return 200" — the shape HTTP
|
|
614
|
+
* actions exist for — forced a hop through a mutation plus a closed allow-list
|
|
615
|
+
* of target strings, because a function reference cannot cross the RPC boundary
|
|
616
|
+
* and a free-form target on an unauthenticated endpoint is a "call any internal
|
|
617
|
+
* function" primitive. Without `storage`, any helper the ctx was threaded into
|
|
618
|
+
* had to be typed for its storage-touching branch, so a handler was barred from
|
|
619
|
+
* the helper even on the branches that never went near storage.
|
|
620
|
+
*/
|
|
621
|
+
type HttpActionCtx = Pick<ActionCtx, "auth" | "cache" | "fetch" | "runAction" | "runMutation" | "runQuery"> & {
|
|
622
|
+
readonly scheduler?: ActionCtx["scheduler"];
|
|
623
|
+
readonly storage?: ActionCtx["storage"];
|
|
624
|
+
};
|
|
489
625
|
/** A raw handler wrapped by {@link httpAction}. Receives the raw request, returns the raw response. */
|
|
490
626
|
type HttpActionHandler = (context: HttpActionCtx, request: Request) => Promise<Response> | Response;
|
|
491
627
|
/**
|
|
492
|
-
* The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
|
|
493
|
-
* The runtime injects the per-request {@link HttpActionCtx} on the private
|
|
494
|
-
* `__lunoraCtx` binding; the router's lifting middleware promotes it to
|
|
495
|
-
* `c.var.lunora` so handlers can read it as a typed variable.
|
|
496
|
-
*/
|
|
628
|
+
* The hono {@link https://hono.dev | Hono} environment used by {@link httpRouter}.
|
|
629
|
+
* The runtime injects the per-request {@link HttpActionCtx} on the private
|
|
630
|
+
* `__lunoraCtx` binding; the router's lifting middleware promotes it to
|
|
631
|
+
* `c.var.lunora` so handlers can read it as a typed variable.
|
|
632
|
+
*/
|
|
497
633
|
interface LunoraHttpEnv {
|
|
498
634
|
Bindings: Record<string, unknown> & {
|
|
499
635
|
__lunoraCtx?: HttpActionCtx;
|
|
@@ -507,31 +643,31 @@ type LunoraHttpApp = Hono<LunoraHttpEnv>;
|
|
|
507
643
|
/** A compiled route handler: a hono handler that resolves to a raw {@link Response}. */
|
|
508
644
|
type LunoraRouteHandler = (c: Context<LunoraHttpEnv>) => Promise<Response>;
|
|
509
645
|
/**
|
|
510
|
-
* Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
|
|
511
|
-
* hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
|
|
512
|
-
* runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
|
|
513
|
-
* {@link httpRouter}; `request` is the underlying `c.req.raw`.
|
|
514
|
-
*/
|
|
646
|
+
* Wrap a `(ctx, request) => Response` handler as a hono handler. The raw escape
|
|
647
|
+
* hatch — mount it with `app.all(path, httpAction(fn))`. `ctx` is the
|
|
648
|
+
* runtime-injected {@link HttpActionCtx} lifted into `c.var.lunora` by
|
|
649
|
+
* {@link httpRouter}; `request` is the underlying `c.req.raw`.
|
|
650
|
+
*/
|
|
515
651
|
declare const httpAction: (handler: HttpActionHandler) => LunoraRouteHandler;
|
|
516
652
|
/**
|
|
517
|
-
* Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
|
|
518
|
-
* the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
|
|
519
|
-
* {@link httpAction} and the typed {@link httpRoute} builder can read the action
|
|
520
|
-
* context. The full hono surface is available — plugins, path params, `.route`:
|
|
521
|
-
*
|
|
522
|
-
* ```ts
|
|
523
|
-
* const app = httpRouter();
|
|
524
|
-
* app.use("*", cors());
|
|
525
|
-
* app.post("/webhook", httpAction(onWebhook));
|
|
526
|
-
* app.get("/users/:id", getUser);
|
|
527
|
-
* export default createWorker({ httpRouter: app, ... });
|
|
528
|
-
* ```
|
|
529
|
-
*
|
|
530
|
-
* The lifting middleware throws if the context is absent. `createWorker` injects
|
|
531
|
-
* it on every request the router sees, so this only trips when the app is run
|
|
532
|
-
* outside the runtime — a misconfiguration we surface loudly rather than let
|
|
533
|
-
* `c.var.lunora` be silently `undefined` despite its non-optional type.
|
|
534
|
-
*/
|
|
653
|
+
* Create the hono app for HTTP actions. Pre-wired with a middleware that lifts
|
|
654
|
+
* the runtime-injected `c.env.__lunoraCtx` into `c.var.lunora`, so both
|
|
655
|
+
* {@link httpAction} and the typed {@link httpRoute} builder can read the action
|
|
656
|
+
* context. The full hono surface is available — plugins, path params, `.route`:
|
|
657
|
+
*
|
|
658
|
+
* ```ts
|
|
659
|
+
* const app = httpRouter();
|
|
660
|
+
* app.use("*", cors());
|
|
661
|
+
* app.post("/webhook", httpAction(onWebhook));
|
|
662
|
+
* app.get("/users/:id", getUser);
|
|
663
|
+
* export default createWorker({ httpRouter: app, ... });
|
|
664
|
+
* ```
|
|
665
|
+
*
|
|
666
|
+
* The lifting middleware throws if the context is absent. `createWorker` injects
|
|
667
|
+
* it on every request the router sees, so this only trips when the app is run
|
|
668
|
+
* outside the runtime — a misconfiguration we surface loudly rather than let
|
|
669
|
+
* `c.var.lunora` be silently `undefined` despite its non-optional type.
|
|
670
|
+
*/
|
|
535
671
|
declare const httpRouter: () => LunoraHttpApp;
|
|
536
672
|
/** The `{ ctx, searchParams, body, params }` a typed route handler receives. */
|
|
537
673
|
interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator> {
|
|
@@ -541,11 +677,12 @@ interface HttpRouteHandlerOptions<SearchParams extends ArgsValidator, Body exten
|
|
|
541
677
|
searchParams: InferArgs<SearchParams>;
|
|
542
678
|
}
|
|
543
679
|
/**
|
|
544
|
-
* The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
|
|
545
|
-
* handler receives. There is no parsed `body` — streams are typically GET, and
|
|
546
|
-
* the raw `request` is exposed if a handler needs to read the body itself.
|
|
547
|
-
* `signal` is tripped when the client disconnects.
|
|
548
|
-
|
|
680
|
+
* The `{ ctx, searchParams, params, request, signal }` a streaming HTTP
|
|
681
|
+
* handler receives. There is no parsed `body` — streams are typically GET, and
|
|
682
|
+
* the raw `request` is exposed if a handler needs to read the body itself.
|
|
683
|
+
* `signal` is tripped when the client disconnects.
|
|
684
|
+
* @experimental Part of the HTTP-SSE stream surface; reconnect/POST-body design questions are still open.
|
|
685
|
+
*/
|
|
549
686
|
interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params extends ArgsValidator> {
|
|
550
687
|
ctx: HttpActionCtx;
|
|
551
688
|
params: InferArgs<Params>;
|
|
@@ -554,35 +691,51 @@ interface HttpStreamHandlerOptions<SearchParams extends ArgsValidator, Params ex
|
|
|
554
691
|
signal: AbortSignal;
|
|
555
692
|
}
|
|
556
693
|
/**
|
|
557
|
-
* A typed REST route under construction. `.searchParams()` / `.body()` /
|
|
558
|
-
* `.params()` accumulate validator maps (later calls merge, a colliding key
|
|
559
|
-
* wins) that decode the URL query, JSON body, and hono path params into the
|
|
560
|
-
* handler's typed `searchParams` / `body` / `params`. Like the procedure
|
|
561
|
-
* builder, `.output(validator)` defaults to the `undefined` sentinel — while
|
|
562
|
-
* unset the handler is generic over its own return; once set the handler must
|
|
563
|
-
* return that type and the result is parsed through the validator before
|
|
564
|
-
* serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
|
|
565
|
-
* `Output` doesn't distribute and the test is for the exact sentinel.
|
|
566
|
-
*
|
|
567
|
-
* The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
|
|
568
|
-
* directly with `app.get(path, route)`.
|
|
569
|
-
*/
|
|
694
|
+
* A typed REST route under construction. `.searchParams()` / `.body()` /
|
|
695
|
+
* `.params()` accumulate validator maps (later calls merge, a colliding key
|
|
696
|
+
* wins) that decode the URL query, JSON body, and hono path params into the
|
|
697
|
+
* handler's typed `searchParams` / `body` / `params`. Like the procedure
|
|
698
|
+
* builder, `.output(validator)` defaults to the `undefined` sentinel — while
|
|
699
|
+
* unset the handler is generic over its own return; once set the handler must
|
|
700
|
+
* return that type and the result is parsed through the validator before
|
|
701
|
+
* serialization. `[Output] extends [undefined]` is tuple-wrapped so a union
|
|
702
|
+
* `Output` doesn't distribute and the test is for the exact sentinel.
|
|
703
|
+
*
|
|
704
|
+
* The terminal `.handler()` yields a {@link LunoraRouteHandler} — mount it
|
|
705
|
+
* directly with `app.get(path, route)`.
|
|
706
|
+
*/
|
|
570
707
|
interface HttpRouteBuilder<SearchParams extends ArgsValidator, Body extends ArgsValidator, Params extends ArgsValidator, Output = undefined> {
|
|
571
708
|
body: <B extends ArgsValidator>(validators: B) => HttpRouteBuilder<SearchParams, B & Body, Params, Output>;
|
|
709
|
+
/**
|
|
710
|
+
* Attach a `Cache-Control` header to the response. Only meaningful when
|
|
711
|
+
* Workers Cache is enabled in `wrangler.jsonc` (`"cache": { "enabled": true }`).
|
|
712
|
+
*/
|
|
713
|
+
cacheControl: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
|
|
714
|
+
/**
|
|
715
|
+
* Attach a `Cache-Tag` header to the response for tag-based purging via
|
|
716
|
+
* `ctx.cache.purge({ tags: [...] })`.
|
|
717
|
+
*/
|
|
718
|
+
cacheTag: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
|
|
572
719
|
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;
|
|
573
720
|
output: <V extends Validator>(validator: V) => HttpRouteBuilder<SearchParams, Body, Params, Infer<V>>;
|
|
574
721
|
params: <P extends ArgsValidator>(validators: P) => HttpRouteBuilder<SearchParams, Body, P & Params, Output>;
|
|
575
722
|
searchParams: <S extends ArgsValidator>(validators: S) => HttpRouteBuilder<S & SearchParams, Body, Params, Output>;
|
|
576
723
|
/**
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
724
|
+
* Terminal: declare this route as a streaming Server-Sent Events endpoint.
|
|
725
|
+
* The handler is an async generator (or any function returning an
|
|
726
|
+
* `AsyncIterable<R>`) that yields one chunk per SSE `data:` frame; on
|
|
727
|
+
* iterator completion the route writes a final `event: complete` frame; on
|
|
728
|
+
* throw, an `event: error` frame is written with `{code, message}` before
|
|
729
|
+
* the stream closes. The chunks are JSON-encoded; `R` is inferred from the
|
|
730
|
+
* handler's yielded type.
|
|
731
|
+
* @experimental Reconnect/POST-body/wire-fidelity design questions are still open, so the shape may change.
|
|
732
|
+
*/
|
|
585
733
|
stream: <R>(handler: (options: HttpStreamHandlerOptions<SearchParams, Params>) => AsyncGenerator<R, void, void> | AsyncIterable<R>) => LunoraRouteHandler;
|
|
734
|
+
/**
|
|
735
|
+
* Attach a `Vary` header to the response so Cloudflare stores separate
|
|
736
|
+
* cached variants per distinct value of the listed request headers.
|
|
737
|
+
*/
|
|
738
|
+
vary: (value: string) => HttpRouteBuilder<SearchParams, Body, Params, Output>;
|
|
586
739
|
}
|
|
587
740
|
/** Opens a fresh {@link HttpRouteBuilder}. The `path` documents intent; hono owns the actual routing at mount. */
|
|
588
741
|
type HttpRouteFactory = (path: string) => HttpRouteBuilder<EmptyArgs, EmptyArgs, EmptyArgs>;
|
|
@@ -597,33 +750,33 @@ interface HttpRoute {
|
|
|
597
750
|
put: HttpRouteFactory;
|
|
598
751
|
}
|
|
599
752
|
/**
|
|
600
|
-
* Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
|
|
601
|
-
* typed route and a hand-written {@link httpAction} are interchangeable when
|
|
602
|
-
* mounted on {@link httpRouter}:
|
|
603
|
-
*
|
|
604
|
-
* ```ts
|
|
605
|
-
* export const listTodos = httpRoute
|
|
606
|
-
* .get("/api/todos")
|
|
607
|
-
* .searchParams({ limit: v.number(), q: v.optional(v.string()) })
|
|
608
|
-
* .output(v.array(v.object({ id: v.string(), text: v.string() })))
|
|
609
|
-
* .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
|
|
610
|
-
*
|
|
611
|
-
* export const getTodo = httpRoute
|
|
612
|
-
* .get("/api/todos/:id")
|
|
613
|
-
* .params({ id: v.string() })
|
|
614
|
-
* .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
|
|
615
|
-
*
|
|
616
|
-
* const app = httpRouter();
|
|
617
|
-
* app.get("/api/todos", listTodos);
|
|
618
|
-
* app.get("/api/todos/:id", getTodo);
|
|
619
|
-
* ```
|
|
620
|
-
*/
|
|
753
|
+
* Typed REST route builder. Compiles down to a {@link LunoraRouteHandler}, so a
|
|
754
|
+
* typed route and a hand-written {@link httpAction} are interchangeable when
|
|
755
|
+
* mounted on {@link httpRouter}:
|
|
756
|
+
*
|
|
757
|
+
* ```ts
|
|
758
|
+
* export const listTodos = httpRoute
|
|
759
|
+
* .get("/api/todos")
|
|
760
|
+
* .searchParams({ limit: v.number(), q: v.optional(v.string()) })
|
|
761
|
+
* .output(v.array(v.object({ id: v.string(), text: v.string() })))
|
|
762
|
+
* .handler(async ({ ctx, searchParams }) => ctx.runQuery(api.todos.list, searchParams));
|
|
763
|
+
*
|
|
764
|
+
* export const getTodo = httpRoute
|
|
765
|
+
* .get("/api/todos/:id")
|
|
766
|
+
* .params({ id: v.string() })
|
|
767
|
+
* .handler(async ({ ctx, params }) => ctx.runQuery(api.todos.get, params));
|
|
768
|
+
*
|
|
769
|
+
* const app = httpRouter();
|
|
770
|
+
* app.get("/api/todos", listTodos);
|
|
771
|
+
* app.get("/api/todos/:id", getTodo);
|
|
772
|
+
* ```
|
|
773
|
+
*/
|
|
621
774
|
declare const httpRoute: HttpRoute;
|
|
622
775
|
/**
|
|
623
|
-
* Structural view of an R2 object body, as returned by `@lunora/storage`'s
|
|
624
|
-
* `download()`. Re-declared here (not imported) so `@lunora/server` takes no
|
|
625
|
-
* runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
|
|
626
|
-
*/
|
|
776
|
+
* Structural view of an R2 object body, as returned by `@lunora/storage`'s
|
|
777
|
+
* `download()`. Re-declared here (not imported) so `@lunora/server` takes no
|
|
778
|
+
* runtime dependency on `@lunora/storage`; the real binding satisfies the shape.
|
|
779
|
+
*/
|
|
627
780
|
interface StorageObjectBody {
|
|
628
781
|
/** The object body stream (`null` for a zero-byte object). */
|
|
629
782
|
body: ReadableStream | null;
|
|
@@ -654,148 +807,398 @@ interface ContextWithStorage {
|
|
|
654
807
|
storage: StorageDownloader;
|
|
655
808
|
}
|
|
656
809
|
/**
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
660
|
-
* `Content-
|
|
661
|
-
*
|
|
662
|
-
* `
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
*
|
|
667
|
-
*
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
|
|
810
|
+
* True when `value` is safe to use as an HTTP header field-value: no CR, LF, or
|
|
811
|
+
* NUL. Guards against response-header injection / `Headers`-construction throws
|
|
812
|
+
* when reflecting attacker-influenced object metadata (e.g. a stored
|
|
813
|
+
* `Content-Type`). Exported (see the `export {}` at the file end) so an `httpAction`
|
|
814
|
+
* handler can guard a request-derived header value before writing it — the fix the
|
|
815
|
+
* `http_action_response_header_injection` advisor lint points to.
|
|
816
|
+
*/
|
|
817
|
+
declare const isSafeHeaderValue: (value: string) => boolean;
|
|
818
|
+
/**
|
|
819
|
+
* Stream a stored object as an HTTP {@link Response} from an `httpAction`
|
|
820
|
+
* handler, with correct `Content-Type`, `ETag`, and `Accept-Ranges: bytes`.
|
|
821
|
+
* Honors a single-range `Range` request → **206 Partial Content** with
|
|
822
|
+
* `Content-Range` + `Content-Length`; otherwise **200**. A missing object is a
|
|
823
|
+
* **404**; an out-of-bounds range is a **416** with a `Content-Range` of
|
|
824
|
+
* `bytes` star-slash-size.
|
|
825
|
+
*
|
|
826
|
+
* A range request re-issues the `download()` with the resolved `{ offset, length }`
|
|
827
|
+
* window so R2 streams only those bytes back to the Worker — the slice is never
|
|
828
|
+
* buffered in the isolate. The first `download()` is used only for the object's
|
|
829
|
+
* size + metadata (its body is left unread and cancelled). For very large
|
|
830
|
+
* objects a signed URL (`ctx.storage.getSignedUrl`) is still cheaper since the
|
|
831
|
+
* client then ranges against R2/CDN directly with no Worker hop.
|
|
832
|
+
*/
|
|
671
833
|
declare const serveStorageObject: (context: ContextWithStorage, key: string, request: Request) => Promise<Response>;
|
|
834
|
+
/**
|
|
835
|
+
* What the worker does with a resolver's identity when it fails contract
|
|
836
|
+
* validation (a forged / malformed claim set arriving from an untrusted token).
|
|
837
|
+
* `"anonymous"` (default, safe) treats the request as anonymous, so the bad
|
|
838
|
+
* identity never reaches a policy as a valid identity (`ctx.auth.userId`
|
|
839
|
+
* becomes `undefined`). `"reject"` fails the request closed (a `401`) — use
|
|
840
|
+
* when a malformed credential should be a hard error, not a silent downgrade.
|
|
841
|
+
*/
|
|
842
|
+
type IdentityRejectMode = "anonymous" | "reject";
|
|
843
|
+
/** Options for {@link defineIdentity}. */
|
|
844
|
+
interface DefineIdentityOptions {
|
|
845
|
+
/**
|
|
846
|
+
* How to handle a resolver identity that violates the contract at the trust
|
|
847
|
+
* boundary. Defaults to `"anonymous"` (a forged claim set is downgraded to
|
|
848
|
+
* anonymous rather than flowing in as an unchecked cast).
|
|
849
|
+
*/
|
|
850
|
+
readonly onInvalid?: IdentityRejectMode;
|
|
851
|
+
}
|
|
852
|
+
/** Result of validating a candidate identity against the contract. */
|
|
853
|
+
type IdentityValidation = {
|
|
854
|
+
ok: true;
|
|
855
|
+
} | {
|
|
856
|
+
error: string;
|
|
857
|
+
ok: false;
|
|
858
|
+
};
|
|
859
|
+
/**
|
|
860
|
+
* A declared identity claim contract. Carries the codegen discovery brand, the
|
|
861
|
+
* declared claim validators, the reject policy, and a runtime `validate`. The
|
|
862
|
+
* `TClaims` type parameter is the inferred claim shape (always extending
|
|
863
|
+
* `{ userId: string }`); it is phantom (no runtime field) and exists so
|
|
864
|
+
* `@lunora/codegen` and {@link InferIdentity} can recover the type.
|
|
865
|
+
*/
|
|
866
|
+
interface IdentityContract<TClaims extends {
|
|
867
|
+
userId: string;
|
|
868
|
+
} = {
|
|
869
|
+
userId: string;
|
|
870
|
+
}> {
|
|
871
|
+
/**
|
|
872
|
+
* Phantom carrier for the inferred claim type. Never populated at runtime
|
|
873
|
+
* (`undefined`); present only so the type flows to codegen / {@link InferIdentity}.
|
|
874
|
+
*/
|
|
875
|
+
readonly __claimType?: TClaims;
|
|
876
|
+
readonly __lunoraIdentity: true;
|
|
877
|
+
/** The declared claim validators (a `@lunora/values` validator map). */
|
|
878
|
+
readonly claims: ValidatorMap;
|
|
879
|
+
/** Reject policy applied at the trust boundary. See {@link IdentityRejectMode}. */
|
|
880
|
+
readonly onInvalid: IdentityRejectMode;
|
|
881
|
+
/**
|
|
882
|
+
* Validate a resolver's returned identity against the declared claims. On
|
|
883
|
+
* success the caller keeps the original identity untouched (so undeclared
|
|
884
|
+
* claims are forwarded verbatim, preserving today's behaviour); on failure
|
|
885
|
+
* the worker applies the `onInvalid` policy.
|
|
886
|
+
*/
|
|
887
|
+
validate: (identity: Record<string, unknown>) => IdentityValidation;
|
|
888
|
+
}
|
|
889
|
+
/** Recover the declared claim type from a {@link defineIdentity} contract. */
|
|
890
|
+
type InferIdentity<T> = T extends IdentityContract<infer TClaims> ? TClaims : never;
|
|
891
|
+
/**
|
|
892
|
+
* Declare the identity claim contract. `claims` is a `@lunora/values` validator
|
|
893
|
+
* map whose inferred type must extend `{ userId: string }` — if it does not
|
|
894
|
+
* (e.g. `userId` is missing or not a required string), the argument type
|
|
895
|
+
* collapses to `never` and the call fails to typecheck.
|
|
896
|
+
* @example
|
|
897
|
+
* export const identity = defineIdentity({ userId: v.string(), tenantId: v.optional(v.string()), scopes: v.optional(v.array(v.string())) });
|
|
898
|
+
*/
|
|
899
|
+
declare const defineIdentity: <A extends ValidatorMap>(claims: InferValidatorMap<A> extends {
|
|
900
|
+
userId: string;
|
|
901
|
+
} ? A : never, options?: DefineIdentityOptions) => IdentityContract<InferValidatorMap<A> & {
|
|
902
|
+
userId: string;
|
|
903
|
+
}>;
|
|
672
904
|
/** Handler for a connection-lifecycle hook. */
|
|
673
905
|
type LifecycleHandler = (context: MutationCtx, event: LifecycleEvent) => Promise<void> | void;
|
|
906
|
+
/** Handler for a shard-init hook. */
|
|
907
|
+
type ShardInitHandler = (context: MutationCtx, event: ShardInitEvent) => Promise<void> | void;
|
|
674
908
|
/** Register a hook that fires once when a client's WebSocket connects. */
|
|
675
909
|
declare const onConnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
|
|
676
910
|
/** Register a hook that fires once when a client's WebSocket disconnects. */
|
|
677
911
|
declare const onDisconnect: (handler: LifecycleHandler) => RegisteredLifecycleHook;
|
|
678
912
|
/**
|
|
679
|
-
*
|
|
680
|
-
*
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
913
|
+
* Register a hook that fires ONCE per Durable Object instance, before any
|
|
914
|
+
* handler on that instance can run — the re-init half of `.memory()` tables.
|
|
915
|
+
*
|
|
916
|
+
* A shard is not a process that stays up. Cloudflare reconstructs the Durable
|
|
917
|
+
* Object after every eviction, and a shard whose sockets are hibernating is
|
|
918
|
+
* evicted routinely, so "cold start" is a steady-state event rather than a rare
|
|
919
|
+
* one. Everything the shard held in memory is gone at that moment: the JS heap,
|
|
920
|
+
* and every `.memory()` table, which the framework has already cleared by the
|
|
921
|
+
* time this hook runs.
|
|
922
|
+
*
|
|
923
|
+
* ```ts
|
|
924
|
+
* // lunora/init.ts
|
|
925
|
+
* import { onShardInit } from "@lunora/server";
|
|
926
|
+
*
|
|
927
|
+
* export const warm = onShardInit(async (ctx, event) => {
|
|
928
|
+
* // Rebuild ephemeral state from the durable tables that outlived us.
|
|
929
|
+
* for await (const member of ctx.db.roomMembers.iterate({ where: { roomId: event.shardKey } })) {
|
|
930
|
+
* await ctx.db.presence.insert({ userId: member.userId, status: "away" });
|
|
931
|
+
* }
|
|
932
|
+
* });
|
|
933
|
+
* ```
|
|
934
|
+
*
|
|
935
|
+
* **Ordering is the guarantee.** Memory tables are cleared, then every init hook
|
|
936
|
+
* runs to completion, and only then does the dispatch that triggered the cold
|
|
937
|
+
* start proceed. No handler, subscription refresh, alarm, or shape poke can
|
|
938
|
+
* observe a memory table in the gap. Hooks run sequentially in manifest order,
|
|
939
|
+
* so one may depend on state an earlier one wrote.
|
|
940
|
+
*
|
|
941
|
+
* **It is a mutation, and it runs on every cold start.** Keep it cheap and keep
|
|
942
|
+
* it idempotent: it is on the latency path of the request that woke the shard,
|
|
943
|
+
* and it will run again — many times — over the shard's life. Writing to durable
|
|
944
|
+
* tables from here is legal and occasionally right, but remember it is a
|
|
945
|
+
* rebuild, not a migration; use `defineMigration` for anything that should
|
|
946
|
+
* happen once.
|
|
947
|
+
*
|
|
948
|
+
* **No caller identity.** The hook dispatches as a trusted system call with no
|
|
949
|
+
* request identity — `ctx.auth` is anonymous and RLS does not apply even under
|
|
950
|
+
* `.rls("required")`, exactly as for a cron tick or a migration. RLS scopes rows
|
|
951
|
+
* to a user and an init hook has none, so `ctx.db` here sees every row: scope
|
|
952
|
+
* your reads yourself. (`onConnect`/`onDisconnect` are the opposite case — they
|
|
953
|
+
* carry the socket's verified identity and stay RLS-guarded.)
|
|
954
|
+
*
|
|
955
|
+
* A throw is logged and does NOT fail the dispatch that woke the shard — an init
|
|
956
|
+
* hook that cannot rebuild presence must not take the whole shard down with it.
|
|
957
|
+
* The table is then cleared but not refilled, so reads see nothing. A failure
|
|
958
|
+
* EARLIER, before the framework's clear runs, instead leaves the previous
|
|
959
|
+
* instance's rows in place: a memory table's rows live in SQLite until they are
|
|
960
|
+
* deleted, so an eviction on its own does not remove them.
|
|
961
|
+
*/
|
|
962
|
+
declare const onShardInit: (handler: ShardInitHandler) => RegisteredLifecycleHook;
|
|
963
|
+
/** Default `limit` when the caller doesn't ask for one. */
|
|
964
|
+
declare const DEFAULT_LIMIT = 25;
|
|
965
|
+
/** Default ceiling on `limit`, so one request can't ask for an unbounded page. */
|
|
966
|
+
declare const DEFAULT_MAX_LIMIT = 100;
|
|
967
|
+
/**
|
|
968
|
+
* Per-field predicate accepted for a declared filter column. An alias of the
|
|
969
|
+
* `ctx.db` `where` DSL's own operator type rather than a copy, so the two cannot
|
|
970
|
+
* drift as operators are added.
|
|
971
|
+
*/
|
|
972
|
+
type ListFilterOperators<T> = WhereOperators<T>;
|
|
973
|
+
/**
|
|
974
|
+
* The filter allow-list a caller may declare: a subset of the document's own
|
|
975
|
+
* columns, each with a validator for that column's type. Constraining the KEYS to
|
|
976
|
+
* `keyof Doc` is what turns a typo'd or renamed column into a compile error
|
|
977
|
+
* instead of a predicate that silently never matches.
|
|
978
|
+
*/
|
|
979
|
+
type ListFilterShape<TDocument> = { [K in keyof TDocument & string]?: Validator<TDocument[K]>; };
|
|
980
|
+
/** The `where` argument: each declared filter column, optionally, as a bare value or an operator object. */
|
|
981
|
+
type ListWhere<F> = { [K in keyof F]?: Infer<NonNullable<F[K]>> | ListFilterOperators<Infer<NonNullable<F[K]>>>; };
|
|
982
|
+
/** One `orderBy` entry. `direction` defaults to `"asc"`. */
|
|
983
|
+
interface ListOrderByEntry<O extends string> {
|
|
984
|
+
direction?: "asc" | "desc";
|
|
985
|
+
field: O;
|
|
986
|
+
}
|
|
987
|
+
/** The decoded arguments a {@link defineListArgs} endpoint receives. */
|
|
988
|
+
interface ListArgsValue<F, O extends string> {
|
|
989
|
+
cursor?: null | number | string;
|
|
990
|
+
limit?: number;
|
|
991
|
+
orderBy?: ListOrderByEntry<O>[];
|
|
992
|
+
where?: ListWhere<F>;
|
|
700
993
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*/
|
|
708
|
-
|
|
709
|
-
/**
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
|
|
728
|
-
/**
|
|
729
|
-
*
|
|
730
|
-
*
|
|
731
|
-
*
|
|
732
|
-
*
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
994
|
+
interface DefineListArgsConfig<F, O extends string> {
|
|
995
|
+
/** `limit` applied when the caller omits one. Defaults to 25. */
|
|
996
|
+
readonly defaultLimit?: number;
|
|
997
|
+
/**
|
|
998
|
+
* Allow-list of filterable columns. Publish only columns an index can serve;
|
|
999
|
+
* anything absent here is unreachable from the client.
|
|
1000
|
+
*/
|
|
1001
|
+
readonly filter: F;
|
|
1002
|
+
/** Ceiling on `in` / `notIn` array length — one bound parameter each. Defaults to 100. */
|
|
1003
|
+
readonly maxInValues?: number;
|
|
1004
|
+
/** Ceiling on `limit`; a larger request is clamped down, not rejected. Defaults to 100. */
|
|
1005
|
+
readonly maxLimit?: number;
|
|
1006
|
+
/** Ceiling on how many `orderBy` entries a request may ask for. Defaults to 8. */
|
|
1007
|
+
readonly maxOrderBy?: number;
|
|
1008
|
+
/** Allow-list of sortable columns. Pass `[]` to fix the order server-side. */
|
|
1009
|
+
readonly orderBy: ReadonlyArray<O>;
|
|
1010
|
+
}
|
|
1011
|
+
/** The validator map handed to `.input()`. Typed precisely so `args` infers end-to-end. */
|
|
1012
|
+
interface ListArgsValidators<F, O extends string> {
|
|
1013
|
+
cursor: ColumnValidator<null | number | string | undefined, null | number | string | undefined>;
|
|
1014
|
+
limit: ColumnValidator<number | undefined, number | undefined>;
|
|
1015
|
+
orderBy: ColumnValidator<ListOrderByEntry<O>[] | undefined, ListOrderByEntry<O>[] | undefined>;
|
|
1016
|
+
where: ColumnValidator<ListWhere<F> | undefined, ListWhere<F> | undefined>;
|
|
1017
|
+
}
|
|
1018
|
+
interface ListArgsSpec<TDocument, F, O extends string> {
|
|
1019
|
+
/** Spread into `.input(...)` — `{ cursor, limit, orderBy, where }`. */
|
|
1020
|
+
readonly args: ListArgsValidators<F, O>;
|
|
1021
|
+
/**
|
|
1022
|
+
* Translate the decoded arguments into the `findMany` options object:
|
|
1023
|
+
* `limit` clamped into `[1, maxLimit]`, `orderBy` reshaped from
|
|
1024
|
+
* `{ field, direction }[]` into `ctx.db`'s `{ column: direction }[]`.
|
|
1025
|
+
*
|
|
1026
|
+
* Returns `QueryArgs<Doc>` — bound to the table, not free — so a mismatch
|
|
1027
|
+
* between what this helper declares and what the table actually holds is a
|
|
1028
|
+
* compile error at the `findMany` call site.
|
|
1029
|
+
*/
|
|
1030
|
+
readonly toQueryArgs: (args: ListArgsValue<F, O>) => QueryArgs$2<TDocument>;
|
|
1031
|
+
}
|
|
1032
|
+
/** Clamp a caller-supplied `limit` into `[1, maxLimit]`; a non-finite value falls back to `fallback`. */
|
|
1033
|
+
declare const clampLimit: (limit: number | undefined, fallback: number, maxLimit: number) => number;
|
|
1034
|
+
/**
|
|
1035
|
+
* Declare the filter / sort / page arguments for a list endpoint, plus the
|
|
1036
|
+
* translation into `ctx.db.<table>.findMany(...)` options. See the module docs
|
|
1037
|
+
* for the shape and the reasoning behind it.
|
|
1038
|
+
*
|
|
1039
|
+
* Curried on the document type: `defineListArgs<Doc<"messages">>()({ … })`. The
|
|
1040
|
+
* extra `()` buys the thing that matters — with `Doc` bound, `filter` keys and
|
|
1041
|
+
* `orderBy` entries are checked against the table's real columns, so a typo or a
|
|
1042
|
+
* column renamed out from under the endpoint is a COMPILE error instead of a
|
|
1043
|
+
* predicate that silently matches nothing. TypeScript has no partial type-argument
|
|
1044
|
+
* inference, so binding `Doc` explicitly while still inferring `F` and `O` from
|
|
1045
|
+
* the config requires the second call.
|
|
1046
|
+
*/
|
|
1047
|
+
declare const defineListArgs: <TDocument>() => <F extends ListFilterShape<TDocument>, O extends keyof TDocument & string>(config: DefineListArgsConfig<F, O>) => ListArgsSpec<TDocument, F, O>;
|
|
1048
|
+
/**
|
|
1049
|
+
* Structural mirrors of `@lunora/shard-engine`'s rank-page-row shapes
|
|
1050
|
+
* (`RankPageRowKey` / `RankPageRow` / `ShardRankPageResult`) — the return type
|
|
1051
|
+
* of the writer's `rankPageRows` seam, the cross-shard companion to
|
|
1052
|
+
* `rankPage`.
|
|
1053
|
+
*
|
|
1054
|
+
* Shared by `../rls/middleware` and `../mask/middleware`: both wrap
|
|
1055
|
+
* `rankPageRows` structurally (no `@lunora/shard-engine` import, mirroring how
|
|
1056
|
+
* every other method on their `DatabaseWriterLike`/`MaskDatabase` projections
|
|
1057
|
+
* is hand-mirrored rather than imported) and both need the exact same result
|
|
1058
|
+
* shape to type their overrides. A single copy here means the two wrappers
|
|
1059
|
+
* can't drift out of lockstep with each other — see AGENTS.md's platform
|
|
1060
|
+
* parity note on `ShardSqlExec` and the canonical binding `*Like` projections
|
|
1061
|
+
* shipping wrong for exactly this reason (two hand-maintained mirrors of one
|
|
1062
|
+
* upstream type).
|
|
1063
|
+
*/
|
|
1064
|
+
/** Structural mirror of `@lunora/shard-engine`'s `RankPageRowKey`. */
|
|
1065
|
+
interface RankPageRowKeyLike {
|
|
1066
|
+
partitionKey: string;
|
|
1067
|
+
rowId: string;
|
|
1068
|
+
sortValues: ReadonlyArray<unknown>;
|
|
749
1069
|
}
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
1070
|
+
/** Structural mirror of `@lunora/shard-engine`'s `RankPageRow`. */
|
|
1071
|
+
interface RankPageRowLike {
|
|
1072
|
+
doc: Record<string, unknown>;
|
|
1073
|
+
key: RankPageRowKeyLike;
|
|
1074
|
+
}
|
|
1075
|
+
/** Structural mirror of `@lunora/shard-engine`'s `ShardRankPageResult` — the `rankPageRows` return shape. */
|
|
1076
|
+
interface ShardRankPageResultLike {
|
|
1077
|
+
directions: ReadonlyArray<"asc" | "desc">;
|
|
1078
|
+
hasMore: boolean;
|
|
1079
|
+
rows: ReadonlyArray<RankPageRowLike>;
|
|
754
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Structural mirror of `@lunora/do`'s `QueryArgs` and `CountArgs`. The
|
|
1083
|
+
* runtime ORM in `@lunora/do`/`@lunora/d1` reads `baseWhere` /
|
|
1084
|
+
* `restrictsCounts` straight off these option objects, so as long as the
|
|
1085
|
+
* fields here stay name-compatible the wrapper is portable across the two
|
|
1086
|
+
* dialects without an inter-package dependency.
|
|
1087
|
+
*/
|
|
755
1088
|
interface QueryArgs$1 {
|
|
756
|
-
baseWhere?:
|
|
1089
|
+
baseWhere?: WhereInput;
|
|
757
1090
|
cursor?: null | string;
|
|
758
1091
|
limit?: number;
|
|
759
|
-
|
|
1092
|
+
orderBy?: ReadonlyArray<unknown>;
|
|
1093
|
+
/**
|
|
1094
|
+
* Per-target-table read filter the RLS wrapper attaches so a `with` relation
|
|
1095
|
+
* is policy-filtered on its own hop (see `@lunora/do`'s `QueryArgs`). Mirrors
|
|
1096
|
+
* the top-level read: `(table) => readBase(table).baseWhere`.
|
|
1097
|
+
*/
|
|
1098
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1099
|
+
restrictsCounts?: boolean;
|
|
1100
|
+
where?: WhereInput;
|
|
760
1101
|
with?: Record<string, unknown>;
|
|
761
1102
|
}
|
|
1103
|
+
interface CountArgs {
|
|
1104
|
+
baseWhere?: WhereInput;
|
|
1105
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1106
|
+
restrictsCounts?: boolean;
|
|
1107
|
+
where?: WhereInput;
|
|
1108
|
+
}
|
|
1109
|
+
/** Structural mirror of `@lunora/do`'s `AggregateOptions` — only the fields the wrapper touches. */
|
|
762
1110
|
interface AggregateArgs$1 {
|
|
1111
|
+
baseWhere?: WhereInput;
|
|
763
1112
|
field?: string;
|
|
764
1113
|
op: string;
|
|
1114
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1115
|
+
restrictsCounts?: boolean;
|
|
1116
|
+
where?: WhereInput;
|
|
765
1117
|
}
|
|
1118
|
+
/** Structural mirror of `@lunora/do`'s `GroupByOptions`. */
|
|
766
1119
|
interface GroupByArgs$1 {
|
|
767
1120
|
agg?: {
|
|
768
1121
|
field?: string;
|
|
769
1122
|
op: string;
|
|
770
1123
|
};
|
|
1124
|
+
baseWhere?: WhereInput;
|
|
771
1125
|
by: ReadonlyArray<string>;
|
|
1126
|
+
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1127
|
+
restrictsCounts?: boolean;
|
|
1128
|
+
where?: WhereInput;
|
|
1129
|
+
}
|
|
1130
|
+
/** Structural mirror of `@lunora/do`'s `RankOptions`. */
|
|
1131
|
+
interface RankArgs {
|
|
1132
|
+
baseWhere?: WhereInput;
|
|
1133
|
+
restrictsCounts?: boolean;
|
|
1134
|
+
row: Record<string, unknown> | string;
|
|
1135
|
+
where?: WhereInput;
|
|
1136
|
+
}
|
|
1137
|
+
/** Structural mirror of `@lunora/do`'s `RankBeforeOptions`. */
|
|
1138
|
+
interface RankBeforeArgs {
|
|
1139
|
+
partitionKey: string;
|
|
1140
|
+
restrictsCounts?: boolean;
|
|
1141
|
+
rowId: string;
|
|
1142
|
+
sortValues: ReadonlyArray<unknown>;
|
|
1143
|
+
}
|
|
1144
|
+
/** Structural mirror of `@lunora/do`'s `RankPageOptions`. */
|
|
1145
|
+
interface RankPageArgs {
|
|
1146
|
+
baseWhere?: WhereInput;
|
|
1147
|
+
cursor?: null | string;
|
|
1148
|
+
restrictsCounts?: boolean;
|
|
1149
|
+
take?: number;
|
|
1150
|
+
where?: WhereInput;
|
|
1151
|
+
}
|
|
1152
|
+
interface QueryPage$1 {
|
|
1153
|
+
continueCursor: null | string;
|
|
1154
|
+
isDone: boolean;
|
|
1155
|
+
page: Record<string, unknown>[];
|
|
772
1156
|
}
|
|
773
1157
|
interface TableReaderLike$1 {
|
|
774
1158
|
collect: () => Promise<Record<string, unknown>[]>;
|
|
775
1159
|
filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike$1;
|
|
776
1160
|
first: () => Promise<Record<string, unknown> | null>;
|
|
777
|
-
order: (direction: "asc" | "desc") => TableReaderLike$1;
|
|
778
1161
|
paginate: (options: {
|
|
779
1162
|
cursor?: null | string;
|
|
780
1163
|
numItems: number;
|
|
781
1164
|
}) => Promise<QueryPage$1>;
|
|
782
1165
|
take: (limit: number) => Promise<Record<string, unknown>[]>;
|
|
783
|
-
|
|
1166
|
+
withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike$1;
|
|
784
1167
|
withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike$1;
|
|
785
1168
|
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike$1;
|
|
786
1169
|
}
|
|
787
1170
|
/**
|
|
788
|
-
* Structural projection of the runtime ORM writer
|
|
789
|
-
*
|
|
790
|
-
*
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
1171
|
+
* Structural projection of the runtime ORM writer. The wrapper relies only
|
|
1172
|
+
* on these fields, so it's interchangeable between `@lunora/do`'s
|
|
1173
|
+
* `DatabaseWriterLike` and `@lunora/d1`'s `DatabaseWriterLike`.
|
|
1174
|
+
*/
|
|
1175
|
+
interface DatabaseWriterLike {
|
|
1176
|
+
/**
|
|
1177
|
+
* Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
|
|
1178
|
+
* `baseWhere` into `options` so the reduction only sees policy-visible rows
|
|
1179
|
+
* (safe: an aggregate scoped to `where` never reveals a hidden row — see
|
|
1180
|
+
* `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
|
|
1181
|
+
* wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
|
|
1182
|
+
*/
|
|
794
1183
|
aggregate: (tableName: string, options: AggregateArgs$1) => Promise<null | number>;
|
|
795
|
-
count: (tableName: string, whereOrArgs?:
|
|
796
|
-
delete: (id: string, expectedTable?: string
|
|
1184
|
+
count: (tableName: string, whereOrArgs?: CountArgs | WhereInput) => Promise<number>;
|
|
1185
|
+
delete: (id: string, expectedTable?: string, options?: {
|
|
1186
|
+
hard?: boolean;
|
|
1187
|
+
}) => Promise<void>;
|
|
1188
|
+
/** Uncapped, chunked erase of a whole table. The RLS wrapper gates each row like a single delete. */
|
|
1189
|
+
deleteAll?: (tableName: string, options?: {
|
|
1190
|
+
chunkSize?: number;
|
|
1191
|
+
hard?: boolean;
|
|
1192
|
+
}) => Promise<{
|
|
1193
|
+
deleted: number;
|
|
1194
|
+
}>;
|
|
797
1195
|
deleteMany: (ids: ReadonlyArray<string>, options?: {
|
|
798
1196
|
limit?: number;
|
|
1197
|
+
}, expectedTable?: string) => Promise<{
|
|
1198
|
+
deleted: number;
|
|
1199
|
+
}>;
|
|
1200
|
+
deleteWhere?: (tableName: string, where: WhereInput, options?: {
|
|
1201
|
+
limit?: number;
|
|
799
1202
|
}) => Promise<{
|
|
800
1203
|
deleted: number;
|
|
801
1204
|
}>;
|
|
@@ -803,6 +1206,11 @@ interface MaskDatabase {
|
|
|
803
1206
|
findFirstOrThrow: (tableName: string, args?: QueryArgs$1) => Promise<Record<string, unknown>>;
|
|
804
1207
|
findMany: (tableName: string, args?: QueryArgs$1) => Promise<QueryPage$1>;
|
|
805
1208
|
get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
|
|
1209
|
+
/**
|
|
1210
|
+
* Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
|
|
1211
|
+
* reduction is scoped to policy-visible rows, so a group count tallies only
|
|
1212
|
+
* rows the caller may read. Required for the same reason as `aggregate`.
|
|
1213
|
+
*/
|
|
806
1214
|
groupBy: (tableName: string, options: GroupByArgs$1) => Promise<ReadonlyArray<{
|
|
807
1215
|
key: Record<string, unknown>;
|
|
808
1216
|
value: null | number;
|
|
@@ -810,7 +1218,20 @@ interface MaskDatabase {
|
|
|
810
1218
|
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
811
1219
|
insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
812
1220
|
limit?: number;
|
|
1221
|
+
skipDuplicates?: boolean;
|
|
1222
|
+
}) => Promise<(string | null)[]>;
|
|
1223
|
+
insertManyUnsafe: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
1224
|
+
allowExplicitId?: boolean;
|
|
1225
|
+
limit?: number;
|
|
813
1226
|
}) => Promise<string[]>;
|
|
1227
|
+
/**
|
|
1228
|
+
* Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
|
|
1229
|
+
* already knows the owning table of an id internally, so it can return
|
|
1230
|
+
* `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
|
|
1231
|
+
* uses it to collapse the per-call membership-probe fan-out (1 `get` + N
|
|
1232
|
+
* `findFirst` across every policy table) down to one lookup. Writers that
|
|
1233
|
+
* don't implement it fall back to the probe path.
|
|
1234
|
+
*/
|
|
814
1235
|
lookupById?: (id: string, expectedTable?: string) => Promise<null | {
|
|
815
1236
|
row: Record<string, unknown>;
|
|
816
1237
|
tableName: string;
|
|
@@ -821,207 +1242,162 @@ interface MaskDatabase {
|
|
|
821
1242
|
patch: Record<string, unknown>;
|
|
822
1243
|
}>, options?: {
|
|
823
1244
|
limit?: number;
|
|
824
|
-
}) => Promise<
|
|
1245
|
+
}, expectedTable?: string) => Promise<{
|
|
1246
|
+
patched: number;
|
|
1247
|
+
}>;
|
|
1248
|
+
patchWhere?: (tableName: string, args: {
|
|
1249
|
+
patch: Record<string, unknown>;
|
|
1250
|
+
where: WhereInput;
|
|
1251
|
+
}, options?: {
|
|
1252
|
+
limit?: number;
|
|
1253
|
+
}) => Promise<{
|
|
1254
|
+
patched: number;
|
|
1255
|
+
}>;
|
|
825
1256
|
query: (tableName: string) => TableReaderLike$1;
|
|
826
|
-
|
|
1257
|
+
/**
|
|
1258
|
+
* Rank a row within its partition. A position is a count-of-rows-before, so
|
|
1259
|
+
* — exactly like `count()` — it can't be trusted in an RLS-restricted
|
|
1260
|
+
* reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
|
|
1261
|
+
* for the same reason as `aggregate`.
|
|
1262
|
+
*/
|
|
1263
|
+
rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
|
|
827
1264
|
position: number;
|
|
828
1265
|
total: number;
|
|
829
1266
|
}>;
|
|
830
|
-
|
|
1267
|
+
/** Cross-shard rank primitive — same count-of-before RLS hazard as `rank`; failed closed under a read policy. */
|
|
1268
|
+
rankBefore?: (tableName: string, indexName: string, options: RankBeforeArgs) => Promise<{
|
|
831
1269
|
before: number;
|
|
832
1270
|
total: number;
|
|
833
1271
|
}>;
|
|
834
|
-
|
|
1272
|
+
/**
|
|
1273
|
+
* Sorted pagination over a rank companion. The companion stores only the
|
|
1274
|
+
* partition + sort keys + id, so an arbitrary read `baseWhere` can't be
|
|
1275
|
+
* enforced against it (and re-filtering the fetched rows would break page
|
|
1276
|
+
* sizing). RLS therefore fails it closed rather than leak hidden rows.
|
|
1277
|
+
* Required for the same reason as `aggregate`.
|
|
1278
|
+
*/
|
|
1279
|
+
rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage$1>;
|
|
1280
|
+
/**
|
|
1281
|
+
* Cross-shard companion to `rankPage`: same ranked slice, but each row
|
|
1282
|
+
* keeps its rank-key tuple for the query coordinator's k-way merge. Same
|
|
1283
|
+
* count-of-partition RLS hazard as `rankPage` — failed closed under a read
|
|
1284
|
+
* policy for the identical reason (see `rankPage` above).
|
|
1285
|
+
*/
|
|
1286
|
+
rankPageRows?: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<ShardRankPageResultLike>;
|
|
835
1287
|
replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
1288
|
+
restore?: (id: string, expectedTable?: string) => Promise<void>;
|
|
1289
|
+
/**
|
|
1290
|
+
* Whole-shard erase. The RLS wrapper deliberately **fails this closed** rather
|
|
1291
|
+
* than wrapping it — see the wrapper's `wipeShard`.
|
|
1292
|
+
*/
|
|
1293
|
+
wipeShard?: (options?: {
|
|
1294
|
+
chunkSize?: number;
|
|
1295
|
+
exclude?: ReadonlyArray<string>;
|
|
1296
|
+
tables?: ReadonlyArray<string>;
|
|
1297
|
+
}) => Promise<{
|
|
1298
|
+
deleted: number;
|
|
1299
|
+
tables: Record<string, number>;
|
|
1300
|
+
}>;
|
|
836
1301
|
}
|
|
837
|
-
/**
|
|
838
|
-
|
|
1302
|
+
/**
|
|
1303
|
+
* What a procedure's `ctx.db` must structurally satisfy for the middleware
|
|
1304
|
+
* to wrap it. We deliberately mirror `@lunora/do`'s `DatabaseWriterLike`
|
|
1305
|
+
* rather than `@lunora/server`'s nominal `DatabaseWriter`/`DatabaseReader`:
|
|
1306
|
+
* the runtime adapter that flows in is the `DatabaseWriterLike`-shaped one,
|
|
1307
|
+
* and structural matching keeps this module free of an `@lunora/do`-typed
|
|
1308
|
+
* `ctx`.
|
|
1309
|
+
*/
|
|
1310
|
+
type RlsDatabase = DatabaseWriterLike;
|
|
1311
|
+
/** Roles list source on the context. Tolerant of older auth states. */
|
|
1312
|
+
type AuthLike = {
|
|
839
1313
|
getIdentity?: () => Promise<Record<string, unknown> | null>;
|
|
840
1314
|
roles?: ReadonlyArray<string>;
|
|
841
1315
|
userId?: null | string;
|
|
842
1316
|
};
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
/**
|
|
848
|
-
* Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
|
|
849
|
-
* Closes over the policy map at builder-construction time; resolves identity +
|
|
850
|
-
* the `bypass` decision per call against the live ctx.
|
|
851
|
-
*
|
|
852
|
-
* IMPORTANT: a mask is in scope only for procedures whose builder chain
|
|
853
|
-
* includes this middleware — opt-in, never global (the same invariant as RLS).
|
|
854
|
-
*/
|
|
855
|
-
declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
|
|
856
|
-
/**
|
|
857
|
-
* Online data-migration authoring API.
|
|
858
|
-
*
|
|
859
|
-
* `defineMigration` declares a per-document backfill over one table: `up`
|
|
860
|
-
* transforms every existing row, `down` (optional) reverses it. Unlike the D1
|
|
861
|
-
* SQL schema migrations in `@lunora/d1`, these run *inside each shard's*
|
|
862
|
-
* Durable Object against live documents, in keyset batches, and are resumable —
|
|
863
|
-
* the per-shard runner in `@lunora/do` tracks progress in a reserved
|
|
864
|
-
* `__lunora_migrations` table so an interrupted run picks up where it stopped.
|
|
865
|
-
*
|
|
866
|
-
* The returned object carries a `__lunoraMigration` brand so codegen can
|
|
867
|
-
* discover declarations through the type checker (mirroring the procedure
|
|
868
|
-
* builder's `__lunoraProcedure` brand) and emit them into a `LUNORA_MIGRATIONS`
|
|
869
|
-
* registry the DO and CLI look migrations up by id.
|
|
870
|
-
*/
|
|
871
|
-
/** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
|
|
872
|
-
type MigrationDocument = Record<string, unknown>;
|
|
873
|
-
/**
|
|
874
|
-
* Transform applied to one document. Return a new document to rewrite the row,
|
|
875
|
-
* or `undefined` to leave it untouched (skipped, not counted as changed). The
|
|
876
|
-
* runner always preserves the original `_id` and `_creationTime`, so the
|
|
877
|
-
* returned document neither needs to nor should change row identity.
|
|
878
|
-
*/
|
|
879
|
-
type MigrationTransform = (document: MigrationDocument) => MigrationDocument | undefined | void;
|
|
880
|
-
interface MigrationDefinition {
|
|
881
|
-
/** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
|
|
882
|
-
readonly batchSize?: number;
|
|
883
|
-
/** Optional reverse transform, applied by `migrate down`. */
|
|
884
|
-
readonly down?: MigrationTransform;
|
|
885
|
-
/** Stable, unique identifier — the key per-shard run-state is tracked under. */
|
|
886
|
-
readonly id: string;
|
|
887
|
-
/** Table whose documents this migration iterates. */
|
|
888
|
-
readonly table: string;
|
|
889
|
-
/** Forward transform, applied to every row by `migrate up`. */
|
|
890
|
-
readonly up: MigrationTransform;
|
|
891
|
-
}
|
|
892
|
-
/** A {@link MigrationDefinition} plus the codegen discovery marker. */
|
|
893
|
-
interface RegisteredMigration extends MigrationDefinition {
|
|
894
|
-
readonly __lunoraMigration: true;
|
|
895
|
-
}
|
|
896
|
-
/** Declare an online data migration. See the module docs for runtime semantics. */
|
|
897
|
-
declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
|
|
898
|
-
/**
|
|
899
|
-
* A mutator declaration. `server` is authoritative; `client` is the optimistic
|
|
900
|
-
* twin (optional — omit it to let the optimistic write fall through to the
|
|
901
|
-
* server round-trip with no local preview). Both receive the same validated
|
|
902
|
-
* `args`.
|
|
903
|
-
*/
|
|
904
|
-
interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
|
|
905
|
-
/**
|
|
906
|
-
* Validator for the mutator's arguments. Validated on the DO before `server`
|
|
907
|
-
* runs and (when present) on the client before `client` runs, so both impls
|
|
908
|
-
* see the same parsed shape. Omit for a parameterless mutator.
|
|
909
|
-
*/
|
|
910
|
-
readonly args?: Args;
|
|
911
|
-
/**
|
|
912
|
-
* Optimistic client implementation. Runs in a TanStack DB transaction
|
|
913
|
-
* against the local collections; its writes are applied immediately and
|
|
914
|
-
* automatically rolled back / rebased as the authoritative result syncs
|
|
915
|
-
* back. Pure and side-effect-free beyond the local store. Omit to skip the
|
|
916
|
-
* local preview.
|
|
917
|
-
*/
|
|
918
|
-
readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
|
|
919
|
-
/**
|
|
920
|
-
* Authoritative server implementation. Runs inside the shard DO with a full
|
|
921
|
-
* {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
|
|
922
|
-
* and poke back to subscribers. This is the source of truth — the client
|
|
923
|
-
* impl is only a prediction of it.
|
|
924
|
-
*/
|
|
925
|
-
readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
|
|
926
|
-
}
|
|
927
|
-
/**
|
|
928
|
-
* A {@link MutatorDefinition} plus the codegen discovery marker and a
|
|
929
|
-
* dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
|
|
930
|
-
* invokes a mutator exactly like a registered procedure.
|
|
931
|
-
*/
|
|
932
|
-
interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
|
|
933
|
-
readonly __lunoraMutator: true;
|
|
934
|
-
/** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
|
|
935
|
-
readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
|
|
936
|
-
/**
|
|
937
|
-
* Marks the dispatch kind so codegen can register the mutator in the same
|
|
938
|
-
* `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
|
|
939
|
-
* reads `kind === "mutation"` to wrap the authoritative `server` impl in the
|
|
940
|
-
* shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
|
|
941
|
-
* ordinary `mutation`.
|
|
942
|
-
*/
|
|
943
|
-
readonly kind: "mutation";
|
|
1317
|
+
/** Minimal shape the middleware needs on the incoming ctx. */
|
|
1318
|
+
interface RlsContextIn {
|
|
1319
|
+
auth?: AuthLike;
|
|
1320
|
+
db: RlsDatabase;
|
|
944
1321
|
}
|
|
945
|
-
|
|
946
|
-
declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
|
|
1322
|
+
declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
|
|
947
1323
|
/**
|
|
948
|
-
* The prefixed tables a single plugin `P` contributes, or an empty map when it
|
|
949
|
-
* ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
|
|
950
|
-
* so {@link InstalledTables} can fold a tuple of plugins.
|
|
951
|
-
*/
|
|
1324
|
+
* The prefixed tables a single plugin `P` contributes, or an empty map when it
|
|
1325
|
+
* ships no schema extension. Mirrors {@link PrefixedTables} at the plugin level
|
|
1326
|
+
* so {@link InstalledTables} can fold a tuple of plugins.
|
|
1327
|
+
*/
|
|
952
1328
|
type ExtensionTablesOf<P> = P extends {
|
|
953
1329
|
readonly extension: SchemaExtension<infer X> & {
|
|
954
1330
|
readonly key: infer K;
|
|
955
1331
|
};
|
|
956
1332
|
} ? K extends string ? PrefixedTables<X, K> : Record<never, never> : Record<never, never>;
|
|
957
1333
|
/**
|
|
958
|
-
* Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
|
|
959
|
-
* auto-prefixed extension tables left-to-right — the type-level mirror of
|
|
960
|
-
* {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
|
|
961
|
-
*/
|
|
1334
|
+
* Fold a tuple of plugins onto a base table map `T`, accumulating each plugin's
|
|
1335
|
+
* auto-prefixed extension tables left-to-right — the type-level mirror of
|
|
1336
|
+
* {@link installPlugins} applying `mergeSchemaExtension` for each plugin in turn.
|
|
1337
|
+
*/
|
|
962
1338
|
type InstalledTables<T extends Record<string, TableDefinition>, Plugins extends ReadonlyArray<unknown>> = Plugins extends readonly [infer Head, ...infer Rest] ? InstalledTables<ExtensionTablesOf<Head> & T, Rest> : T;
|
|
963
1339
|
/**
|
|
964
|
-
* Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
|
|
965
|
-
* `ctx.api
|
|
966
|
-
* plugin middleware runs. Independent of the incoming context, which the builder
|
|
967
|
-
* infers at the `.use(...)` site.
|
|
968
|
-
*/
|
|
1340
|
+
* Union every plugin's `ContextOut` in a tuple — the type-level mirror of the
|
|
1341
|
+
* `ctx.api.<key>` additions {@link composePluginMiddleware} accumulates as each
|
|
1342
|
+
* plugin middleware runs. Independent of the incoming context, which the builder
|
|
1343
|
+
* infers at the `.use(...)` site.
|
|
1344
|
+
*/
|
|
969
1345
|
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;
|
|
970
1346
|
/**
|
|
971
|
-
* Schema fragment a plugin contributes. Same shape as the `tables` map
|
|
972
|
-
* passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
|
|
973
|
-
* `defineSchema` argument so a plugin can ship vector decls alongside its
|
|
974
|
-
* tables.
|
|
975
|
-
*/
|
|
1347
|
+
* Schema fragment a plugin contributes. Same shape as the `tables` map
|
|
1348
|
+
* passed to `defineSchema`. Optional `vectorIndexes` mirror the top-level
|
|
1349
|
+
* `defineSchema` argument so a plugin can ship vector decls alongside its
|
|
1350
|
+
* tables.
|
|
1351
|
+
*/
|
|
976
1352
|
interface SchemaExtension<T extends Record<string, TableDefinition> = Record<string, TableDefinition>> {
|
|
977
1353
|
/** Stable key identifying the plugin that owns this extension. */
|
|
978
1354
|
readonly key: string;
|
|
979
1355
|
/**
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1356
|
+
* Extension tables, keyed by **bare** name (e.g. `buckets`). At merge time
|
|
1357
|
+
* each is auto-prefixed with `key` (`ratelimit_buckets`) so it can't
|
|
1358
|
+
* collide with an app table; do **not** namespace manually.
|
|
1359
|
+
*/
|
|
984
1360
|
readonly tables: T;
|
|
985
1361
|
/**
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1362
|
+
* Optional standalone vector indexes the plugin ships, keyed by index
|
|
1363
|
+
* name. Merged into the host schema's `vectorIndexes`; a key collision
|
|
1364
|
+
* with the base schema is a hard error (same policy as tables).
|
|
1365
|
+
*/
|
|
990
1366
|
readonly vectorIndexes?: Record<string, VectorIndexDefinition>;
|
|
991
1367
|
}
|
|
992
1368
|
/**
|
|
993
|
-
* Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
|
|
994
|
-
* error messages on collision) and a type-level brand.
|
|
995
|
-
*/
|
|
1369
|
+
* Build a {@link SchemaExtension}. The `key` is a runtime tag (used for
|
|
1370
|
+
* error messages on collision) and a type-level brand.
|
|
1371
|
+
*/
|
|
996
1372
|
declare const defineSchemaExtension: <T extends Record<string, TableDefinition>>(key: string, options: {
|
|
997
1373
|
tables: T;
|
|
998
1374
|
vectorIndexes?: Record<string, VectorIndexDefinition>;
|
|
999
1375
|
}) => SchemaExtension<T>;
|
|
1000
1376
|
/**
|
|
1001
|
-
* A plugin packages an optional schema extension and optional middleware.
|
|
1002
|
-
* Both are independently usable: an app can install only the schema (e.g.
|
|
1003
|
-
* for plugins that ship background workers but no per-request behavior)
|
|
1004
|
-
* or only the middleware (plugins that augment ctx without persistent
|
|
1005
|
-
* state).
|
|
1006
|
-
*/
|
|
1377
|
+
* A plugin packages an optional schema extension and optional middleware.
|
|
1378
|
+
* Both are independently usable: an app can install only the schema (e.g.
|
|
1379
|
+
* for plugins that ship background workers but no per-request behavior)
|
|
1380
|
+
* or only the middleware (plugins that augment ctx without persistent
|
|
1381
|
+
* state).
|
|
1382
|
+
*/
|
|
1007
1383
|
interface Plugin<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn> {
|
|
1008
1384
|
/**
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1385
|
+
* Optional schema extension. Apps install via
|
|
1386
|
+
* `defineSchema(...).extend(plugin.extension)`.
|
|
1387
|
+
*/
|
|
1012
1388
|
readonly extension?: SchemaExtension<TExtension>;
|
|
1013
1389
|
/** Stable key identifying the plugin. Matches `extension.key` when set. */
|
|
1014
1390
|
readonly key: string;
|
|
1015
1391
|
/**
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1392
|
+
* Optional middleware. Users attach with `c.query.use(plugin.middleware)`.
|
|
1393
|
+
* The middleware can extend `ctx`; convention is to attach helpers under
|
|
1394
|
+
* `ctx.api.<key>`, e.g.
|
|
1395
|
+
*
|
|
1396
|
+
* ```ts
|
|
1397
|
+
* middleware: ({ ctx, next }) =>
|
|
1398
|
+
* next({ ctx: { api: { ...ctx.api, ratelimit: api } } })
|
|
1399
|
+
* ```
|
|
1400
|
+
*/
|
|
1025
1401
|
readonly middleware?: Middleware<TContextIn, TContextOut>;
|
|
1026
1402
|
}
|
|
1027
1403
|
/** Options to {@link definePlugin}. */
|
|
@@ -1030,35 +1406,51 @@ interface DefinePluginOptions<TExtension extends Record<string, TableDefinition>
|
|
|
1030
1406
|
middleware?: Middleware<TContextIn, TContextOut>;
|
|
1031
1407
|
}
|
|
1032
1408
|
/**
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
*
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
*
|
|
1050
|
-
*
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1409
|
+
* Call signatures for {@link definePlugin}. When `extension` is supplied the
|
|
1410
|
+
* returned plugin's `extension` is typed as PRESENT (not `?`), so the
|
|
1411
|
+
* canonical install pattern `defineSchema(...).extend(plugin.extension)`
|
|
1412
|
+
* typechecks without a non-null assertion — the shape every scaffold template
|
|
1413
|
+
* ships. The bare-options signature keeps `extension` optional for plugins
|
|
1414
|
+
* that carry only middleware.
|
|
1415
|
+
*/
|
|
1416
|
+
interface DefinePluginFunction {
|
|
1417
|
+
<TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut> & {
|
|
1418
|
+
extension: SchemaExtension<TExtension>;
|
|
1419
|
+
}): Plugin<TExtension, TContextIn, TContextOut> & {
|
|
1420
|
+
readonly extension: SchemaExtension<TExtension>;
|
|
1421
|
+
};
|
|
1422
|
+
<TExtension extends Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn>(key: string, options: DefinePluginOptions<TExtension, TContextIn, TContextOut>): Plugin<TExtension, TContextIn, TContextOut>;
|
|
1423
|
+
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Package a schema extension + middleware as a reusable plugin. Either
|
|
1426
|
+
* field is optional — `definePlugin("foo", {})` is valid but degenerate.
|
|
1427
|
+
*/
|
|
1428
|
+
declare const definePlugin: DefinePluginFunction;
|
|
1429
|
+
/**
|
|
1430
|
+
* Bundle of registered functions a {@link Component} ships. Keys are the
|
|
1431
|
+
* function's local name (e.g. `check`, `reset`); the registered function
|
|
1432
|
+
* value carries its own kind / args / handler.
|
|
1433
|
+
*
|
|
1434
|
+
* Users re-export from their own lunora module so codegen picks them up:
|
|
1435
|
+
*
|
|
1436
|
+
* ```ts
|
|
1437
|
+
* // lunora/ratelimit.ts
|
|
1438
|
+
* import { ratelimit } from "@vendor/ratelimit-component";
|
|
1439
|
+
* export const { check, reset } = ratelimit.functions;
|
|
1440
|
+
* // Emits as `ratelimit:check` / `ratelimit:reset` in the generated `api`.
|
|
1441
|
+
* ```
|
|
1442
|
+
*
|
|
1443
|
+
* Codegen follows the re-export back to the bundled `query/mutation/action`
|
|
1444
|
+
* call (property access or destructuring both work), so the functions land in
|
|
1445
|
+
* the generated `api` under the re-exporting file's namespace.
|
|
1446
|
+
*/
|
|
1055
1447
|
type ComponentFunctions = Readonly<Record<string, RegisteredFunction<any, any, FunctionKind>>>;
|
|
1056
1448
|
/**
|
|
1057
|
-
* Component = {@link Plugin} with a bundle of registered functions. The
|
|
1058
|
-
* extension + middleware + functions are independent: a component can ship
|
|
1059
|
-
* functions without a schema (e.g. a stateless utility), or a schema
|
|
1060
|
-
* without functions (e.g. shared table definitions), and any combination.
|
|
1061
|
-
*/
|
|
1449
|
+
* Component = {@link Plugin} with a bundle of registered functions. The
|
|
1450
|
+
* extension + middleware + functions are independent: a component can ship
|
|
1451
|
+
* functions without a schema (e.g. a stateless utility), or a schema
|
|
1452
|
+
* without functions (e.g. shared table definitions), and any combination.
|
|
1453
|
+
*/
|
|
1062
1454
|
interface Component<TExtension extends Record<string, TableDefinition> = Record<string, TableDefinition>, TContextIn = unknown, TContextOut = TContextIn, F extends ComponentFunctions = ComponentFunctions> extends Plugin<TExtension, TContextIn, TContextOut> {
|
|
1063
1455
|
readonly functions: F;
|
|
1064
1456
|
}
|
|
@@ -1067,81 +1459,91 @@ interface DefineComponentOptions<TExtension extends Record<string, TableDefiniti
|
|
|
1067
1459
|
functions?: F;
|
|
1068
1460
|
}
|
|
1069
1461
|
/**
|
|
1070
|
-
* Convenience wrapper around {@link definePlugin} that also bundles a set
|
|
1071
|
-
* of registered functions. The resulting `component.functions` object is a
|
|
1072
|
-
* record of `name → registered query/mutation/action`; consumers
|
|
1073
|
-
* re-export entries so codegen discovers them as user functions:
|
|
1074
|
-
*
|
|
1075
|
-
* ```ts
|
|
1076
|
-
* export const ratelimit = defineComponent("ratelimit", {
|
|
1077
|
-
* // Bare `buckets` merges in as `ratelimit_buckets`.
|
|
1078
|
-
* extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
|
|
1079
|
-
* middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
|
|
1080
|
-
* functions: {
|
|
1081
|
-
* check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
|
|
1082
|
-
* reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
|
|
1083
|
-
* },
|
|
1084
|
-
* });
|
|
1085
|
-
* ```
|
|
1086
|
-
*
|
|
1087
|
-
* Re-exporting an entry (by property access or destructuring) is enough for
|
|
1088
|
-
* codegen to discover it in the host app's namespace — the discovery resolver
|
|
1089
|
-
* chases the re-export back to the bundled registration call.
|
|
1090
|
-
*/
|
|
1462
|
+
* Convenience wrapper around {@link definePlugin} that also bundles a set
|
|
1463
|
+
* of registered functions. The resulting `component.functions` object is a
|
|
1464
|
+
* record of `name → registered query/mutation/action`; consumers
|
|
1465
|
+
* re-export entries so codegen discovers them as user functions:
|
|
1466
|
+
*
|
|
1467
|
+
* ```ts
|
|
1468
|
+
* export const ratelimit = defineComponent("ratelimit", {
|
|
1469
|
+
* // Bare `buckets` merges in as `ratelimit_buckets`.
|
|
1470
|
+
* extension: defineSchemaExtension("ratelimit", { tables: { buckets } }),
|
|
1471
|
+
* middleware: ({ ctx, next }) => next({ ctx: { ...ctx, ratelimit: api(ctx) } }),
|
|
1472
|
+
* functions: {
|
|
1473
|
+
* check: query.input({ key: v.string() }).query(async ({ ctx, args }) => ...),
|
|
1474
|
+
* reset: mutation.input({ key: v.string() }).mutation(async ({ ctx, args }) => ...),
|
|
1475
|
+
* },
|
|
1476
|
+
* });
|
|
1477
|
+
* ```
|
|
1478
|
+
*
|
|
1479
|
+
* Re-exporting an entry (by property access or destructuring) is enough for
|
|
1480
|
+
* codegen to discover it in the host app's namespace — the discovery resolver
|
|
1481
|
+
* chases the re-export back to the bundled registration call.
|
|
1482
|
+
*/
|
|
1091
1483
|
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>;
|
|
1092
1484
|
/**
|
|
1093
|
-
* Map every key `K` of an extension's table map `X` to its auto-prefixed name
|
|
1094
|
-
* `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
|
|
1095
|
-
* so the typed `.extend(...)` chain reflects the real merged table names.
|
|
1096
|
-
*/
|
|
1097
|
-
type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K] };
|
|
1098
|
-
/**
|
|
1099
|
-
* Merge a {@link SchemaExtension} into an existing schema. Returns a new
|
|
1100
|
-
* schema object — never mutates the input.
|
|
1101
|
-
*
|
|
1102
|
-
* Extension tables are auto-namespaced: each bare table name is prefixed with
|
|
1103
|
-
* the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
|
|
1104
|
-
* style, and every intra-extension reference (relation targets, aggregate /
|
|
1105
|
-
* rank index `on`, standalone vector index `table`) is rewritten to match.
|
|
1106
|
-
* References to base/app tables are left untouched.
|
|
1107
|
-
*
|
|
1108
|
-
* Because each extension lives in its own `key` namespace, app↔component
|
|
1109
|
-
* collisions are impossible. The only remaining hard error is two extensions
|
|
1110
|
-
* sharing the same `key` and producing the same prefixed table (or vector
|
|
1111
|
-
* index) name — silent shadow would let one plugin hijack another's data.
|
|
1112
|
-
|
|
1485
|
+
* Map every key `K` of an extension's table map `X` to its auto-prefixed name
|
|
1486
|
+
* `${Key}_${K}`. Mirrors the runtime prefixing in {@link mergeSchemaExtension}
|
|
1487
|
+
* so the typed `.extend(...)` chain reflects the real merged table names.
|
|
1488
|
+
*/
|
|
1489
|
+
type PrefixedTables<X extends Record<string, TableDefinition>, Key extends string> = { [K in keyof X as K extends string ? `${Key}_${K}` : K]: X[K]; };
|
|
1490
|
+
/**
|
|
1491
|
+
* Merge a {@link SchemaExtension} into an existing schema. Returns a new
|
|
1492
|
+
* schema object — never mutates the input.
|
|
1493
|
+
*
|
|
1494
|
+
* Extension tables are auto-namespaced: each bare table name is prefixed with
|
|
1495
|
+
* the extension `key` (`buckets` → `ratelimit_buckets`), Convex-Components
|
|
1496
|
+
* style, and every intra-extension reference (relation targets, aggregate /
|
|
1497
|
+
* rank index `on`, standalone vector index `table`) is rewritten to match.
|
|
1498
|
+
* References to base/app tables are left untouched.
|
|
1499
|
+
*
|
|
1500
|
+
* Because each extension lives in its own `key` namespace, app↔component
|
|
1501
|
+
* collisions are impossible. The only remaining hard error is two extensions
|
|
1502
|
+
* sharing the same `key` and producing the same prefixed table (or vector
|
|
1503
|
+
* index) name — silent shadow would let one plugin hijack another's data.
|
|
1504
|
+
*
|
|
1505
|
+
* Re-runs {@link validateIndexFields} against the merged table set before
|
|
1506
|
+
* returning: `defineSchema` only validates the tables it was called with, so
|
|
1507
|
+
* without this an extension-contributed index with a typo'd/out-of-shape
|
|
1508
|
+
* field (or a duplicate name within one kind) would never be checked at all.
|
|
1509
|
+
* Re-validating the whole merged set (base + prefixed extension tables) is
|
|
1510
|
+
* cheap and idempotent for the base tables, which already passed this same
|
|
1511
|
+
* check when the base schema was built. Both callers of this function —
|
|
1512
|
+
* `withExtend.extend()` (`./schema`) and `installPlugins` (below) — get the
|
|
1513
|
+
* re-validation for free from this single call site (plan 258 §4/§9 Q3).
|
|
1514
|
+
*/
|
|
1113
1515
|
declare const mergeSchemaExtension: <T extends Record<string, TableDefinition>, X extends Record<string, TableDefinition>, Key extends string = string>(base: Schema<T>, extension: SchemaExtension<X> & {
|
|
1114
1516
|
readonly key: Key;
|
|
1115
1517
|
}) => Schema<PrefixedTables<X, Key> & T>;
|
|
1116
1518
|
/**
|
|
1117
|
-
* Install several plugins' schema extensions in one call — the one-shot
|
|
1118
|
-
* counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
|
|
1119
|
-
* without an `extension` (middleware-only) are skipped; tables from those that
|
|
1120
|
-
* do are auto-prefixed and reference-rewritten exactly as
|
|
1121
|
-
* {@link mergeSchemaExtension} does for a single `.extend(...)`.
|
|
1122
|
-
*
|
|
1123
|
-
* ```ts
|
|
1124
|
-
* const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
|
|
1125
|
-
* // → todos + ratelimit_* + audit_*
|
|
1126
|
-
* ```
|
|
1127
|
-
*
|
|
1128
|
-
* Pair it with {@link composePluginMiddleware} to attach every plugin's
|
|
1129
|
-
* middleware in a single `.use(...)`, so installing N plugins is two calls
|
|
1130
|
-
* rather than N `.extend(...)` + N `.use(...)`.
|
|
1131
|
-
*/
|
|
1519
|
+
* Install several plugins' schema extensions in one call — the one-shot
|
|
1520
|
+
* counterpart to chaining `defineSchema(...).extend(a).extend(b)`. Plugins
|
|
1521
|
+
* without an `extension` (middleware-only) are skipped; tables from those that
|
|
1522
|
+
* do are auto-prefixed and reference-rewritten exactly as
|
|
1523
|
+
* {@link mergeSchemaExtension} does for a single `.extend(...)`.
|
|
1524
|
+
*
|
|
1525
|
+
* ```ts
|
|
1526
|
+
* const schema = installPlugins(defineSchema({ todos }), [ratelimit, audit]);
|
|
1527
|
+
* // → todos + ratelimit_* + audit_*
|
|
1528
|
+
* ```
|
|
1529
|
+
*
|
|
1530
|
+
* Pair it with {@link composePluginMiddleware} to attach every plugin's
|
|
1531
|
+
* middleware in a single `.use(...)`, so installing N plugins is two calls
|
|
1532
|
+
* rather than N `.extend(...)` + N `.use(...)`.
|
|
1533
|
+
*/
|
|
1132
1534
|
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>>;
|
|
1133
1535
|
/**
|
|
1134
|
-
* Compose every plugin's middleware into a single middleware you attach with one
|
|
1135
|
-
* `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
|
|
1136
|
-
* in array order, each seeing the context the previous one widened, so the final
|
|
1137
|
-
* `next({ ctx })` the builder receives carries every plugin's `ctx.api
|
|
1138
|
-
* additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
|
|
1139
|
-
* value, the middleware sibling of {@link installPlugins}.
|
|
1140
|
-
*
|
|
1141
|
-
* `ContextIn` is left free so the builder infers it from the context at the
|
|
1142
|
-
* `.use(...)` site; the result type widens it by the union of the plugins'
|
|
1143
|
-
* outputs.
|
|
1144
|
-
*/
|
|
1536
|
+
* Compose every plugin's middleware into a single middleware you attach with one
|
|
1537
|
+
* `.use(...)`. Plugins without middleware (schema-only) are skipped; the rest run
|
|
1538
|
+
* in array order, each seeing the context the previous one widened, so the final
|
|
1539
|
+
* `next({ ctx })` the builder receives carries every plugin's `ctx.api.<key>`
|
|
1540
|
+
* additions. Equivalent to `.use(a.middleware).use(b.middleware)…` but as one
|
|
1541
|
+
* value, the middleware sibling of {@link installPlugins}.
|
|
1542
|
+
*
|
|
1543
|
+
* `ContextIn` is left free so the builder infers it from the context at the
|
|
1544
|
+
* `.use(...)` site; the result type widens it by the union of the plugins'
|
|
1545
|
+
* outputs.
|
|
1546
|
+
*/
|
|
1145
1547
|
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>;
|
|
1146
1548
|
/** Options for `.vectorize(field, opts)` (DSL Shape A). */
|
|
1147
1549
|
interface VectorizeOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
@@ -1178,10 +1580,10 @@ interface RelationBuilder {
|
|
|
1178
1580
|
}) => OneRelation<Target>;
|
|
1179
1581
|
}
|
|
1180
1582
|
/**
|
|
1181
|
-
* Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
|
|
1182
|
-
* `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
|
|
1183
|
-
* `COUNT(*) GROUP BY userId` accelerator.
|
|
1184
|
-
*/
|
|
1583
|
+
* Options for the inline `.aggregateIndex(name, opts)` builder. `op` defaults to
|
|
1584
|
+
* `count` so `aggregateIndex("byUser", { by: ["userId"] })` is a single-line
|
|
1585
|
+
* `COUNT(*) GROUP BY userId` accelerator.
|
|
1586
|
+
*/
|
|
1185
1587
|
interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1186
1588
|
/** Group keys; counter rows are one per distinct tuple. Omitted = single-row aggregate over the whole table. */
|
|
1187
1589
|
by?: ReadonlyArray<keyof Shape & string>;
|
|
@@ -1193,11 +1595,11 @@ interface InlineAggregateIndexOptions<Shape extends Record<string, Validator> =
|
|
|
1193
1595
|
where?: Record<string, unknown>;
|
|
1194
1596
|
}
|
|
1195
1597
|
/**
|
|
1196
|
-
* Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
|
|
1197
|
-
* accepts either an array of `{ field, direction }` keys, or the shorthand
|
|
1198
|
-
* `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
|
|
1199
|
-
* rank — omitted ⇒ one global rank over the whole table.
|
|
1200
|
-
*/
|
|
1598
|
+
* Options for the inline `.rankIndex(name, opts)` builder. `sortBy` is required;
|
|
1599
|
+
* accepts either an array of `{ field, direction }` keys, or the shorthand
|
|
1600
|
+
* `["field"]` (asc) / `{ field: "desc" }` map entries. `partitionBy` scopes the
|
|
1601
|
+
* rank — omitted ⇒ one global rank over the whole table.
|
|
1602
|
+
*/
|
|
1201
1603
|
interface InlineRankIndexOptions<Shape extends Record<string, Validator> = Record<string, Validator>> {
|
|
1202
1604
|
/** Columns that scope each ranking; omitted ⇒ one global rank. */
|
|
1203
1605
|
partitionBy?: ReadonlyArray<keyof Shape & string>;
|
|
@@ -1213,79 +1615,187 @@ interface TableBuilder<Shape extends Record<string, Validator> = Record<string,
|
|
|
1213
1615
|
/** Declare an aggregate (counter/sum/…) maintained by triggers for O(1) reads. */
|
|
1214
1616
|
aggregateIndex: (name: string, options?: InlineAggregateIndexOptions<Shape>) => TableBuilder<Shape>;
|
|
1215
1617
|
/**
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1618
|
+
* Stamp every row with `_commitSeq` — a per-shard integer, allocated once
|
|
1619
|
+
* per mutation and strictly increasing in **commit order**, refreshed on
|
|
1620
|
+
* every write to the row (insert, patch, replace, and the marker flip a
|
|
1621
|
+
* `.softDelete()` performs).
|
|
1622
|
+
*
|
|
1623
|
+
* `_creationTime` is wall-clock and therefore cannot order commits: the
|
|
1624
|
+
* clock is read when the handler runs, the write lands when the transaction
|
|
1625
|
+
* commits, and nothing ties those instants together. A changefeed paging on
|
|
1626
|
+
* `_creationTime` can skip a row permanently. Paging on `_commitSeq`
|
|
1627
|
+
* (`where: { _commitSeq: { gt: cursor } }, orderBy: ["_commitSeq"]`) cannot.
|
|
1628
|
+
*
|
|
1629
|
+
* It orders COMMITS, not rows: one mutation's rows share a value. A bounded
|
|
1630
|
+
* page can therefore end mid-group, so a consumer must checkpoint at a
|
|
1631
|
+
* sequence it has seen the whole of, never at the last row of a full page.
|
|
1632
|
+
* An action's writes are the exception to the grouping — they commit
|
|
1633
|
+
* independently, so each gets its own sequence.
|
|
1634
|
+
*
|
|
1635
|
+
* Ordered, not contiguous — read a gap as "nothing to see", never as loss.
|
|
1636
|
+
* Per-shard, not global: two shards allocate independently, so a cursor is
|
|
1637
|
+
* only meaningful against the shard it came from. Rejected on `.global()`
|
|
1638
|
+
* tables, which have no shard-local transaction to allocate inside.
|
|
1639
|
+
*
|
|
1640
|
+
* **A hard delete is invisible to the feed.** The sequence lives on the row,
|
|
1641
|
+
* so a physically removed row takes it along: the row stops appearing, but
|
|
1642
|
+
* no event says it went away. Pair `.commitOrdered()` with `.softDelete()`
|
|
1643
|
+
* when the feed must observe deletes — the tombstone flip is an UPDATE, so
|
|
1644
|
+
* it advances the sequence and pages through like any other change.
|
|
1645
|
+
*/
|
|
1646
|
+
commitOrdered: () => TableBuilder<Shape>;
|
|
1647
|
+
/**
|
|
1648
|
+
* Mark this table as written outside Lunora's discoverable insert path —
|
|
1649
|
+
* by an adapter, a migration, or framework middleware (e.g. `@lunora/auth`'s
|
|
1650
|
+
* better-auth tables, `@lunora/ratelimit`'s store). Advisor insert-path lints
|
|
1651
|
+
* (`table_without_insert`) then skip it instead of flagging the absent
|
|
1652
|
+
* `ctx.db.insert(...)`.
|
|
1653
|
+
*/
|
|
1222
1654
|
externallyManaged: () => TableBuilder<Shape>;
|
|
1223
1655
|
/**
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1656
|
+
* Declare a geospatial index over a `v.geoPoint()` column. The runtime keeps
|
|
1657
|
+
* a geohash companion so `withGeoIndex(name, q => q.near(point, radius))` and
|
|
1658
|
+
* `.within(bbox)` resolve as a geohash-prefix range scan + Haversine
|
|
1659
|
+
* refine/sort. `options.precision` tunes the geohash length (default 9).
|
|
1660
|
+
*/
|
|
1661
|
+
geoIndex: (name: string, options: {
|
|
1662
|
+
field: keyof Shape & string;
|
|
1663
|
+
precision?: number;
|
|
1664
|
+
}) => TableBuilder<Shape>;
|
|
1665
|
+
/**
|
|
1666
|
+
* Mark this table as global (cross-shard). Backed by **D1** by default;
|
|
1667
|
+
* pass `{ backend: "hyperdrive" }` to store it in a Postgres/MySQL database
|
|
1668
|
+
* via Cloudflare Hyperdrive (PlanetScale, Neon, …) instead. Either way the
|
|
1669
|
+
* table stays reactive — live queries re-run on write.
|
|
1670
|
+
*/
|
|
1229
1671
|
global: (options?: {
|
|
1230
1672
|
backend?: GlobalBackend;
|
|
1231
1673
|
}) => TableBuilder<Shape>;
|
|
1232
1674
|
/** Add a secondary index. */
|
|
1233
|
-
index: (name: string, fields: ReadonlyArray<string>, options?: {
|
|
1675
|
+
index: (name: string, fields: ReadonlyArray<(keyof Shape & string) | (typeof SYSTEM_INDEX_FIELDS)[number]>, options?: {
|
|
1234
1676
|
unique?: boolean;
|
|
1235
1677
|
}) => TableBuilder<Shape>;
|
|
1236
1678
|
/**
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1679
|
+
* Declare this table EPHEMERAL — state the shard rebuilds rather than
|
|
1680
|
+
* remembers.
|
|
1681
|
+
*
|
|
1682
|
+
* A memory table is a full `ctx.db` table: indexes, `where`, `orderBy`,
|
|
1683
|
+
* pagination, relations, live queries. What it is not is durable. Its rows
|
|
1684
|
+
* are wiped the moment the Durable Object is reconstructed — which happens
|
|
1685
|
+
* on every eviction, and a WebSocket-hibernating shard is evicted often — so
|
|
1686
|
+
* a memory table holds only what can be derived again: presence and cursors,
|
|
1687
|
+
* a live participant list, a rate-limit window, an actor's scratch state.
|
|
1688
|
+
*
|
|
1689
|
+
* Pair it with `onShardInit` to rebuild whatever the app needs present.
|
|
1690
|
+
* The framework guarantees the ordering: every memory table is cleared, and
|
|
1691
|
+
* every init hook has run, before any handler can read one. Without a hook a
|
|
1692
|
+
* memory table simply comes back empty, which is a correct state for
|
|
1693
|
+
* presence and a wrong one for a cache someone is treating as authoritative.
|
|
1694
|
+
*
|
|
1695
|
+
* **On Cloudflare the rows still transit the DO's SQLite.** workerd exposes
|
|
1696
|
+
* exactly one SQL handle and no memory-backed database, so `.memory()` buys
|
|
1697
|
+
* the LIFETIME (and skips the CDC changelog, so an append-heavy presence
|
|
1698
|
+
* table does not grow the op-log), not the write. Treat it as "state I am
|
|
1699
|
+
* happy to lose", not as "state that is free to write" — see
|
|
1700
|
+
* `PlatformCapabilities.memoryTables`, rated `emulated` for exactly this
|
|
1701
|
+
* reason.
|
|
1702
|
+
*
|
|
1703
|
+
* Rejected alongside `.global()` (a D1 table is not this shard's to clear),
|
|
1704
|
+
* `.commitOrdered()` (a sequence that resets is not a sequence), and
|
|
1705
|
+
* `.source()` (an externally-materialized table is not ours to wipe).
|
|
1706
|
+
*/
|
|
1707
|
+
memory: () => TableBuilder<Shape>;
|
|
1708
|
+
/**
|
|
1709
|
+
* Name the column holding the owning user's id, so "only the owner sees these
|
|
1710
|
+
* rows" is declared once here rather than restated in every shape.
|
|
1711
|
+
*
|
|
1712
|
+
* A `defineShape({ table, owner: true })` over this table derives its predicate
|
|
1713
|
+
* from the field: the subscriber's verified `ctx.auth.userId` must match, and an
|
|
1714
|
+
* anonymous subscriber is denied. Pairs naturally with `.shardBy(field)` on the
|
|
1715
|
+
* same column — the shard key routes the storage, `ownedBy` states who the rows
|
|
1716
|
+
* belong to — but the two are independent and either can be used alone.
|
|
1717
|
+
*
|
|
1718
|
+
* This is a *shape* declaration, not an RLS policy: it narrows what a shape
|
|
1719
|
+
* replicates. Guarding procedure reads/writes is still `rls(...)`'s job.
|
|
1720
|
+
*/
|
|
1721
|
+
ownedBy: (field: keyof Shape & string) => TableBuilder<Shape>;
|
|
1722
|
+
/**
|
|
1723
|
+
* Opt this table OUT of secure-by-default RLS. Under a schema marked
|
|
1724
|
+
* `.rls("required")`, every table is protected (the write path denies raw,
|
|
1725
|
+
* non-RLS `ctx.db` access); calling `.public()` exempts this one table so a
|
|
1726
|
+
* plain `query`/`mutation` may read/write it without an RLS policy. No effect
|
|
1727
|
+
* when the schema does not require RLS.
|
|
1728
|
+
*/
|
|
1243
1729
|
public: () => TableBuilder<Shape>;
|
|
1244
1730
|
/**
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1731
|
+
* Declare a rank index (sorted companion table, btree-backed) for
|
|
1732
|
+
* `rank(row)` / `rankPage()` reads in O(log n). See {@link RankIndexDefinition}.
|
|
1733
|
+
*/
|
|
1248
1734
|
rankIndex: (name: string, options: InlineRankIndexOptions<Shape>) => TableBuilder<Shape>;
|
|
1249
1735
|
/** Declare relations to other tables, loaded via `findMany({ with })`. */
|
|
1250
1736
|
relations: (build: (r: RelationBuilder) => Record<string, RelationDefinition>) => TableBuilder<Shape>;
|
|
1251
|
-
/**
|
|
1737
|
+
/**
|
|
1738
|
+
* Add a full-text search index over `field`, queried with
|
|
1739
|
+
* `.withSearchIndex(name, q => q.search(field, term))`. `field` may be a
|
|
1740
|
+
* dot-separated path into a nested object (`"properties.name"`).
|
|
1741
|
+
* `filterFields` (at most 16) lists the columns `.eq()` may narrow by inside
|
|
1742
|
+
* the search. `language` selects the text analysis (accent folding always,
|
|
1743
|
+
* plus that language's stopwords). `staged: true` skips the migration-time
|
|
1744
|
+
* backfill on a large existing table. `strategy: "native"` uses the engine's
|
|
1745
|
+
* own full-text index where it has one (Postgres) — faster on large corpora,
|
|
1746
|
+
* at the cost of the engine ranking rather than the shared scorer.
|
|
1747
|
+
*/
|
|
1252
1748
|
searchIndex: (name: string, options: {
|
|
1253
1749
|
field: string;
|
|
1254
1750
|
filterFields?: ReadonlyArray<string>;
|
|
1751
|
+
language?: SearchLanguage;
|
|
1752
|
+
staged?: boolean;
|
|
1753
|
+
strategy?: SearchStrategy;
|
|
1255
1754
|
}) => TableBuilder<Shape>;
|
|
1256
1755
|
/** Route storage by the named field — one DO per distinct value. */
|
|
1257
1756
|
shardBy: (field: keyof Shape & string) => TableBuilder<Shape>;
|
|
1258
1757
|
/**
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1758
|
+
* Turn on soft delete. Adds a nullable timestamp column (`options.field`,
|
|
1759
|
+
* default `deletedAt`) and changes `ctx.db.<table>.delete()` to **set** it
|
|
1760
|
+
* instead of removing the row; `onDelete: "cascade"` children are recursively
|
|
1761
|
+
* soft-deleted too. **List reads** (`findMany`/`findFirst`/`query()`/`count`/
|
|
1762
|
+
* `aggregate`/relation loads) then hide soft-deleted rows unless they pass
|
|
1763
|
+
* `includeDeleted: true`; by-id `get`/`patch`/`replace` and the new
|
|
1764
|
+
* `restore()` still address the row directly. `hardDelete()` physically
|
|
1765
|
+
* removes it (cascading as a real delete). Note: `includeDeleted` is a read
|
|
1766
|
+
* scope, not access control — anyone who can run the read can set it; a unique
|
|
1767
|
+
* index still rejects a new row that collides with a soft-deleted one (the row
|
|
1768
|
+
* physically persists).
|
|
1769
|
+
*/
|
|
1271
1770
|
softDelete: (options?: {
|
|
1272
1771
|
field?: string;
|
|
1273
1772
|
}) => TableBuilder<Shape>;
|
|
1274
1773
|
/**
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1774
|
+
* Materialize this table from an external Postgres/MySQL behind Cloudflare
|
|
1775
|
+
* Hyperdrive (plan 077). A system-driven poll loop reads the tenant slice
|
|
1776
|
+
* (`query`, with params bound from `tenantBy`) and lands it in the DO's SQLite,
|
|
1777
|
+
* after which `defineShape` carries it to clients unchanged. Implies
|
|
1778
|
+
* `.externallyManaged()` (rows come from the ingest loop, not user mutations).
|
|
1779
|
+
*
|
|
1780
|
+
* Orthogonal to `.shardBy()` — combine them for per-tenant DOs. **Under
|
|
1781
|
+
* `.shardBy()` `tenantBy` is mandatory** (the tenant-isolation boundary); the
|
|
1782
|
+
* `external_source_unscoped` advisor lint fails the build when it is absent, and
|
|
1783
|
+
* `external_source_on_global` rejects combining `.source()` with `.global()`.
|
|
1784
|
+
*/
|
|
1286
1785
|
source: (definition: ExternalSourceDefinition) => TableBuilder<Shape>;
|
|
1287
1786
|
/** Declare named lifecycle triggers fired inline within the write path. */
|
|
1288
1787
|
triggers: (build: (t: TriggerBuilder<Shape>) => Record<string, TriggerDefinition>) => TableBuilder<Shape>;
|
|
1788
|
+
/**
|
|
1789
|
+
* Declare a table-level TTL: a DO alarm-driven sweep auto-deletes rows whose
|
|
1790
|
+
* expiry has passed (or soft-deletes them when the table also
|
|
1791
|
+
* `.softDelete()`s). `field` is an epoch-millisecond column; without
|
|
1792
|
+
* `options.after` its value is the absolute expiry instant, with `after` the
|
|
1793
|
+
* row expires `after` ms past `field` (`field + after`). Coarse, cheap,
|
|
1794
|
+
* table-level — for per-row schedules use `@lunora/scheduler`.
|
|
1795
|
+
*/
|
|
1796
|
+
ttl: (field: keyof Shape & string, options?: {
|
|
1797
|
+
after?: number;
|
|
1798
|
+
}) => TableBuilder<Shape>;
|
|
1289
1799
|
/** Declare a vector index over a single text field on this table. */
|
|
1290
1800
|
vectorize: (field: keyof Shape & string, options: VectorizeOptions<Shape>) => TableBuilder<Shape>;
|
|
1291
1801
|
}
|
|
@@ -1303,22 +1813,22 @@ interface VectorIndexOptions {
|
|
|
1303
1813
|
};
|
|
1304
1814
|
}
|
|
1305
1815
|
/**
|
|
1306
|
-
* Build a table definition. Returned object is both the table definition (for
|
|
1307
|
-
* `defineSchema`) and a fluent builder for indexes + sharding metadata.
|
|
1308
|
-
*/
|
|
1816
|
+
* Build a table definition. Returned object is both the table definition (for
|
|
1817
|
+
* `defineSchema`) and a fluent builder for indexes + sharding metadata.
|
|
1818
|
+
*/
|
|
1309
1819
|
declare const defineTable: <Shape extends Record<string, Validator>>(inputShape: Shape) => TableBuilder<Shape>;
|
|
1310
1820
|
/**
|
|
1311
|
-
* Declare a standalone vector index (DSL Shape B). Pass the returned value in
|
|
1312
|
-
* the `vectorIndexes` map of {@link defineSchema} when the source is derived
|
|
1313
|
-
* from multiple fields or a computation rather than a single column.
|
|
1314
|
-
*/
|
|
1821
|
+
* Declare a standalone vector index (DSL Shape B). Pass the returned value in
|
|
1822
|
+
* the `vectorIndexes` map of {@link defineSchema} when the source is derived
|
|
1823
|
+
* from multiple fields or a computation rather than a single column.
|
|
1824
|
+
*/
|
|
1315
1825
|
declare const defineVectorIndex: (options: VectorIndexOptions) => VectorIndexDefinition;
|
|
1316
1826
|
/**
|
|
1317
|
-
* Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
|
|
1318
|
-
* Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
|
|
1319
|
-
* the owning table explicitly via `on` — handy when a single counter wants to
|
|
1320
|
-
* live next to the schema map rather than inside a table chain.
|
|
1321
|
-
*/
|
|
1827
|
+
* Options for the standalone `defineAggregateIndex(name, opts)` helper (DSL
|
|
1828
|
+
* Shape B). Unlike the inline `.aggregateIndex(...)` builder, this form takes
|
|
1829
|
+
* the owning table explicitly via `on` — handy when a single counter wants to
|
|
1830
|
+
* live next to the schema map rather than inside a table chain.
|
|
1831
|
+
*/
|
|
1322
1832
|
interface AggregateIndexOptions {
|
|
1323
1833
|
by?: ReadonlyArray<string>;
|
|
1324
1834
|
field?: string;
|
|
@@ -1327,17 +1837,17 @@ interface AggregateIndexOptions {
|
|
|
1327
1837
|
where?: Record<string, unknown>;
|
|
1328
1838
|
}
|
|
1329
1839
|
/**
|
|
1330
|
-
* Declare a standalone aggregate index. Pass the returned value to
|
|
1331
|
-
* `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
|
|
1332
|
-
* the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
|
|
1333
|
-
* (DO + D1) read every index uniformly off the table definition.
|
|
1334
|
-
*/
|
|
1840
|
+
* Declare a standalone aggregate index. Pass the returned value to
|
|
1841
|
+
* `defineSchema(tables, vectorIndexes, aggregateIndexes)` keyed by index name —
|
|
1842
|
+
* the schema attaches it to `tables[on].aggregateIndexes` so runtime consumers
|
|
1843
|
+
* (DO + D1) read every index uniformly off the table definition.
|
|
1844
|
+
*/
|
|
1335
1845
|
declare const defineAggregateIndex: (name: string, options: AggregateIndexOptions) => AggregateIndexDefinition;
|
|
1336
1846
|
/**
|
|
1337
|
-
* Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
|
|
1338
|
-
* Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
|
|
1339
|
-
* `table` so it can sit next to the schema map.
|
|
1340
|
-
*/
|
|
1847
|
+
* Options for the standalone `defineRankIndex(name, opts)` helper (DSL Shape B).
|
|
1848
|
+
* Mirrors the inline `.rankIndex(...)` builder but takes the owning table via
|
|
1849
|
+
* `table` so it can sit next to the schema map.
|
|
1850
|
+
*/
|
|
1341
1851
|
interface RankIndexOptions {
|
|
1342
1852
|
partitionBy?: ReadonlyArray<string>;
|
|
1343
1853
|
sortBy: ReadonlyArray<{
|
|
@@ -1348,375 +1858,276 @@ interface RankIndexOptions {
|
|
|
1348
1858
|
where?: Record<string, unknown>;
|
|
1349
1859
|
}
|
|
1350
1860
|
/**
|
|
1351
|
-
* Declare a standalone rank index. Pass the returned value to
|
|
1352
|
-
* `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
|
|
1353
|
-
* by index name — the schema attaches it to `tables[on].rankIndexes`.
|
|
1354
|
-
*/
|
|
1861
|
+
* Declare a standalone rank index. Pass the returned value to
|
|
1862
|
+
* `defineSchema(tables, vectorIndexes, aggregateIndexes, rankIndexes)` keyed
|
|
1863
|
+
* by index name — the schema attaches it to `tables[on].rankIndexes`.
|
|
1864
|
+
*/
|
|
1355
1865
|
declare const defineRankIndex: (name: string, options: RankIndexOptions) => RankIndexDefinition;
|
|
1356
1866
|
/**
|
|
1357
|
-
* Build the application schema. The first argument is the table map; the
|
|
1358
|
-
* optional second argument registers standalone `defineVectorIndex(...)`
|
|
1359
|
-
* declarations (DSL Shape B) keyed by index name. The optional third argument
|
|
1360
|
-
* registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
|
|
1361
|
-
* the optional fourth argument registers standalone `defineRankIndex(...)`
|
|
1362
|
-
* declarations. Both are folded into the matching `tables[on].*Indexes` array
|
|
1363
|
-
* so runtime backends read every index uniformly off the table definition.
|
|
1364
|
-
*/
|
|
1365
|
-
/**
|
|
1366
|
-
* Schema with an in-place `.extend(plugin.extension)` method. Used so apps
|
|
1367
|
-
* can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
|
|
1368
|
-
*
|
|
1369
|
-
* `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
|
|
1370
|
-
* the merged tables. Extension tables are auto-namespaced by the extension
|
|
1371
|
-
* `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
|
|
1372
|
-
* prefixed names via {@link PrefixedTables}. Chains:
|
|
1373
|
-
* `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
|
|
1374
|
-
* `a`'s prefixed tables then `b`'s.
|
|
1375
|
-
*/
|
|
1867
|
+
* Build the application schema. The first argument is the table map; the
|
|
1868
|
+
* optional second argument registers standalone `defineVectorIndex(...)`
|
|
1869
|
+
* declarations (DSL Shape B) keyed by index name. The optional third argument
|
|
1870
|
+
* registers standalone `defineAggregateIndex(...)` declarations (DSL Shape B);
|
|
1871
|
+
* the optional fourth argument registers standalone `defineRankIndex(...)`
|
|
1872
|
+
* declarations. Both are folded into the matching `tables[on].*Indexes` array
|
|
1873
|
+
* so runtime backends read every index uniformly off the table definition.
|
|
1874
|
+
*/
|
|
1875
|
+
/**
|
|
1876
|
+
* Schema with an in-place `.extend(plugin.extension)` method. Used so apps
|
|
1877
|
+
* can compose plugin schemas: `defineSchema({...}).extend(authPlugin.extension)`.
|
|
1878
|
+
*
|
|
1879
|
+
* `extend` is non-mutating — returns a fresh `ExtendableSchema` containing
|
|
1880
|
+
* the merged tables. Extension tables are auto-namespaced by the extension
|
|
1881
|
+
* `key` (`buckets` → `ratelimit_buckets`), so the merged type carries the
|
|
1882
|
+
* prefixed names via {@link PrefixedTables}. Chains:
|
|
1883
|
+
* `defineSchema(...).extend(a).extend(b)` is the typed equivalent of merging
|
|
1884
|
+
* `a`'s prefixed tables then `b`'s.
|
|
1885
|
+
*/
|
|
1376
1886
|
type ExtendableSchema<T extends Record<string, TableDefinition>> = {
|
|
1377
1887
|
extend: <X extends Record<string, TableDefinition>, Key extends string>(extension: SchemaExtension<X> & {
|
|
1378
1888
|
readonly key: Key;
|
|
1379
1889
|
}) => ExtendableSchema<PrefixedTables<X, Key> & T>;
|
|
1380
1890
|
/**
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1891
|
+
* Pin every Durable Object the app reaches — shards, fan-out, subscriptions,
|
|
1892
|
+
* the scheduler, and `ctx.containers` — to a Cloudflare data-residency
|
|
1893
|
+
* jurisdiction (`"eu"`, `"us"`, `"fedramp"`). Codegen reads this off the
|
|
1894
|
+
* schema and emits it into the generated worker's `createWorker({ jurisdiction })`
|
|
1895
|
+
* (and `ctx.scheduler` / `ctx.containers`). Non-mutating: returns a fresh
|
|
1896
|
+
* `ExtendableSchema`, so it composes with `.rls(...)` / `.extend(...)` in any order.
|
|
1897
|
+
*
|
|
1898
|
+
* ⚠️ **Set this once, before your first deploy — changing or removing it
|
|
1899
|
+
* strands data.** A Durable Object name maps to a *different* ID in each
|
|
1900
|
+
* jurisdiction, so toggling this on an existing app makes every shard, scheduler
|
|
1901
|
+
* job, and session DO resolve to a NEW, empty DO; the previous data stays in the
|
|
1902
|
+
* old jurisdiction's DOs and is no longer reachable. There is no in-place
|
|
1903
|
+
* migration — you would have to export from the old jurisdiction and import
|
|
1904
|
+
* into the new one.
|
|
1905
|
+
*
|
|
1906
|
+
* Note: this pins **DO-backed** state only. D1-backed state — `.global()`
|
|
1907
|
+
* tables and `@lunora/auth` sessions alike — is governed by D1's own location
|
|
1908
|
+
* settings, not this option.
|
|
1909
|
+
* @see https://developers.cloudflare.com/durable-objects/reference/data-location/
|
|
1910
|
+
*/
|
|
1401
1911
|
jurisdiction: (jurisdiction: DurableObjectJurisdiction) => ExtendableSchema<T>;
|
|
1402
1912
|
/**
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1913
|
+
* Turn on secure-by-default RLS for the whole schema. Every table is then
|
|
1914
|
+
* protected — the DO/D1 write path denies raw, non-RLS `ctx.db` access, so a
|
|
1915
|
+
* procedure that forgets `.use(rls(...))` fails closed. Opt a table out with
|
|
1916
|
+
* `.public()`. Non-mutating: returns a fresh `ExtendableSchema` carrying the
|
|
1917
|
+
* mode, so `.rls("required")` composes with `.extend(...)` either order.
|
|
1918
|
+
*/
|
|
1409
1919
|
rls: (mode: "required") => ExtendableSchema<T>;
|
|
1410
1920
|
} & Schema<T>;
|
|
1411
|
-
declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
|
|
1412
|
-
/** Default time-to-live for a presence row: a heartbeat keeps a member "present" for this long. */
|
|
1413
|
-
declare const DEFAULT_TTL_MS = 3e4;
|
|
1414
|
-
declare const PRESENCE_BARE_TABLE = "present";
|
|
1415
1921
|
/**
|
|
1416
|
-
*
|
|
1417
|
-
*
|
|
1418
|
-
|
|
1419
|
-
|
|
1922
|
+
* Columns every row carries implicitly (never part of a table's declared
|
|
1923
|
+
* `shape`), so `.index()` may legitimately name them. The single source for
|
|
1924
|
+
* both the compile-time allow-list (`TableBuilder["index"]`'s `fields` type,
|
|
1925
|
+
* via `(typeof SYSTEM_INDEX_FIELDS)[number]`) and the runtime cross-check
|
|
1926
|
+
* below (via `SYSTEM_INDEX_FIELDS_SET`) — declared once so the two can't
|
|
1927
|
+
* drift apart.
|
|
1928
|
+
*/
|
|
1929
|
+
declare const SYSTEM_INDEX_FIELDS: readonly ["_commitSeq", "_creationTime", "_id"];
|
|
1930
|
+
/**
|
|
1931
|
+
* Per-table, per-KIND index→declared-fields map: for each table, each index
|
|
1932
|
+
* KIND (`index` | `rank` | `geo`) that has at least one declared index maps
|
|
1933
|
+
* to a name→fields record for that kind only. Distilled by
|
|
1934
|
+
* {@link indexFieldsFromSchema}; this is the shape `mask()`'s
|
|
1935
|
+
* `MaskOptions.indexFields` expects (see `./mask/types`), so a table not
|
|
1936
|
+
* present here (no declared indexes of any kind) is simply absent from the
|
|
1937
|
+
* map rather than mapped to `{}`, and a kind with no declared indexes on a
|
|
1938
|
+
* table that HAS other kinds is simply absent from that table's entry.
|
|
1939
|
+
*
|
|
1940
|
+
* Kept per kind (rather than one flat name→fields record) because the engine
|
|
1941
|
+
* resolves `withIndex`/`withGeoIndex`/rank reads in THREE separate
|
|
1942
|
+
* namespaces (`tableDefinition.indexes` / `.geoIndexes` / `.rankIndexes` —
|
|
1943
|
+
* see `@lunora/shard-engine`'s `ctx-db.ts`), so the same name can legally and
|
|
1944
|
+
* unambiguously denote a different index per kind. A flat map would let one
|
|
1945
|
+
* kind's fields silently shadow another's for a colliding name, producing a
|
|
1946
|
+
* wrong-namespace answer from the mask guard (checking the wrong index's
|
|
1947
|
+
* fields) instead of the documented fail-open (missing lookup) — see plan 258.
|
|
1948
|
+
*/
|
|
1949
|
+
type IndexFieldsByTable = Readonly<Record<string, {
|
|
1950
|
+
readonly geo?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
1951
|
+
readonly index?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
1952
|
+
readonly rank?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
1953
|
+
}>>;
|
|
1954
|
+
declare const indexFieldsFromSchema: (schema: Schema) => IndexFieldsByTable;
|
|
1955
|
+
declare const defineSchema: <T extends Record<string, TableDefinition>>(tables: T, vectorIndexes?: Record<string, VectorIndexDefinition>, aggregateIndexes?: Record<string, AggregateIndexDefinition>, rankIndexes?: Record<string, RankIndexDefinition>) => ExtendableSchema<T>;
|
|
1420
1956
|
/**
|
|
1421
|
-
*
|
|
1422
|
-
*
|
|
1423
|
-
*
|
|
1424
|
-
*
|
|
1425
|
-
*
|
|
1426
|
-
*
|
|
1427
|
-
*
|
|
1428
|
-
*/
|
|
1429
|
-
interface
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
* Grace window (ms) before a gracefully-closed session is dropped from the
|
|
1443
|
-
* present list. When `0` (the default), `onDisconnect` hard-deletes the
|
|
1444
|
-
* session's row the instant its socket closes. When `> 0`, the row is
|
|
1445
|
-
* instead aged so the read-time TTL filter hides it `disconnectGraceMs`
|
|
1446
|
-
* from now — a reconnect with the same `sessionId` within the window
|
|
1447
|
-
* re-heartbeats and restores full presence with no visible flicker (the
|
|
1448
|
-
* AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
|
|
1449
|
-
*/
|
|
1450
|
-
disconnectGraceMs?: number;
|
|
1451
|
-
/**
|
|
1452
|
-
* How long (ms) a heartbeat keeps a member present. `listPresent` excludes
|
|
1453
|
-
* rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
|
|
1454
|
-
*/
|
|
1455
|
-
ttlMs?: number;
|
|
1456
|
-
}
|
|
1457
|
-
/** The registered functions a presence component ships. */
|
|
1458
|
-
interface PresenceFunctions {
|
|
1459
|
-
/**
|
|
1460
|
-
* Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
|
|
1461
|
-
* delete its presence row so it disappears from `listPresent` with no TTL
|
|
1462
|
-
* lag. Targets the row by the `{ roomId, sessionId }` the client passed as
|
|
1463
|
-
* the connection `context`, and only deletes it when the disconnecting
|
|
1464
|
-
* VERIFIED identity owns the row (so a forged context can't evict another
|
|
1465
|
-
* member). The TTL filter + `sweep` remain the fallback for ungraceful drops
|
|
1466
|
-
* where no `context` was recorded.
|
|
1467
|
-
*/
|
|
1468
|
-
disconnect: RegisteredLifecycleHook;
|
|
1469
|
-
/**
|
|
1470
|
-
* Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
|
|
1471
|
-
* Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
|
|
1472
|
-
* subscribers receive a single-row delta, not a churn of insert/delete. A
|
|
1473
|
-
* heartbeat may only patch a row owned by the same identity (an existing row
|
|
1474
|
-
* held by a different `userId` is refused with `FORBIDDEN`), so a client
|
|
1475
|
-
* can't overwrite another member's awareness data via a guessed `sessionId`.
|
|
1476
|
-
*/
|
|
1477
|
-
heartbeat: RegisteredMutation<{
|
|
1478
|
-
data: ReturnType<typeof v.optional>;
|
|
1479
|
-
roomId: ReturnType<typeof v.string>;
|
|
1480
|
-
sessionId: ReturnType<typeof v.string>;
|
|
1481
|
-
}, {
|
|
1482
|
-
lastSeen: number;
|
|
1483
|
-
}>;
|
|
1484
|
-
/**
|
|
1485
|
-
* Live query returning the non-expired members of `roomId`, newest heartbeat
|
|
1486
|
-
* first. Subscribe to it for a reactive "who's here" list.
|
|
1487
|
-
*/
|
|
1488
|
-
listPresent: RegisteredQuery<{
|
|
1489
|
-
roomId: ReturnType<typeof v.string>;
|
|
1490
|
-
}, PresenceMember[]>;
|
|
1491
|
-
/**
|
|
1492
|
-
* Internal mutation that hard-deletes every expired row for `roomId`. Stale
|
|
1493
|
-
* rows already vanish from `listPresent` via the read-time TTL filter; this
|
|
1494
|
-
* only reclaims storage. Schedule it (cron / `runAfter`) if you care.
|
|
1495
|
-
*/
|
|
1496
|
-
sweep: RegisteredMutation<{
|
|
1497
|
-
roomId: ReturnType<typeof v.string>;
|
|
1498
|
-
}, {
|
|
1499
|
-
deleted: number;
|
|
1500
|
-
}>;
|
|
1501
|
-
}
|
|
1502
|
-
/** The component shape `definePresence` returns: the presence extension + typed functions. */
|
|
1503
|
-
type PresenceComponent = Component<{
|
|
1504
|
-
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
1505
|
-
}> & {
|
|
1506
|
-
functions: PresenceFunctions;
|
|
1507
|
-
};
|
|
1508
|
-
/**
|
|
1509
|
-
* The presence schema extension: a single `present` table, auto-namespaced to
|
|
1510
|
-
* `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
|
|
1511
|
-
* heartbeat upsert and by `roomId` for `listPresent`.
|
|
1512
|
-
*/
|
|
1513
|
-
declare const presenceExtension: SchemaExtension<{
|
|
1514
|
-
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
1515
|
-
}>;
|
|
1516
|
-
declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
|
|
1517
|
-
/**
|
|
1518
|
-
* The middlewares `protectPublic` chains, in the order they run. Every field is
|
|
1519
|
-
* optional, so a bundle can be just a rate limit, just a captcha, or any mix —
|
|
1520
|
-
* pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
|
|
1521
|
-
* from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
|
|
1522
|
-
* `@lunora/auth`). They are accepted as values rather than imported here so
|
|
1523
|
-
* `@lunora/server` keeps no dependency on those packages (which depend on it).
|
|
1524
|
-
*/
|
|
1525
|
-
interface ProtectPublicOptions<Context> {
|
|
1526
|
-
/**
|
|
1527
|
-
* A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
|
|
1528
|
-
* an obvious flood is cheaper to reject with the in-memory limiter than with
|
|
1529
|
-
* a Turnstile siteverify round-trip.
|
|
1530
|
-
*/
|
|
1531
|
-
captcha?: Middleware<Context, Context>;
|
|
1532
|
-
/**
|
|
1533
|
-
* A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
|
|
1534
|
-
* any network-bound check below it runs.
|
|
1535
|
-
*/
|
|
1536
|
-
rateLimit?: Middleware<Context, Context>;
|
|
1537
|
-
/** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
|
|
1538
|
-
use?: ReadonlyArray<Middleware<Context, Context>>;
|
|
1957
|
+
* Context handed to a {@link MaskFn} (and to {@link MaskOptions.bypass}). The
|
|
1958
|
+
* `auth` shape mirrors RLS's `PolicyContext.auth` one-for-one — same identity
|
|
1959
|
+
* resolver, same `can(...)` permission check — so an author can branch a mask
|
|
1960
|
+
* on the caller's role/permission. `row` is the full pre-mask row the column
|
|
1961
|
+
* belongs to; `column` is the column currently being masked. Both are absent
|
|
1962
|
+
* when the context is used for the procedure-wide `bypass` check (no specific
|
|
1963
|
+
* cell is in play yet).
|
|
1964
|
+
*/
|
|
1965
|
+
interface MaskContext<Context = unknown> {
|
|
1966
|
+
readonly auth: {
|
|
1967
|
+
/** `true` when any of the request's `roles` grants `permission` (see {@link MaskOptions.roles}). Fails closed for unregistered roles. */
|
|
1968
|
+
readonly can: (permission: Permission | string) => boolean;
|
|
1969
|
+
readonly identity?: Record<string, unknown> | null;
|
|
1970
|
+
readonly roles: ReadonlyArray<string>;
|
|
1971
|
+
readonly userId: null | string;
|
|
1972
|
+
};
|
|
1973
|
+
/** The column currently being masked. Present only inside a per-cell {@link MaskFn}. */
|
|
1974
|
+
readonly column?: string;
|
|
1975
|
+
readonly ctx: Context;
|
|
1976
|
+
/** The full pre-mask row the masked cell belongs to. Present only inside a per-cell {@link MaskFn}. */
|
|
1977
|
+
readonly row?: Record<string, unknown>;
|
|
1539
1978
|
}
|
|
1540
1979
|
/**
|
|
1541
|
-
*
|
|
1542
|
-
*
|
|
1543
|
-
*
|
|
1544
|
-
*
|
|
1545
|
-
*
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
* export const signUp = mutation
|
|
1549
|
-
* .use(protectPublic({
|
|
1550
|
-
* rateLimit: rateLimit(limiter, "signup"),
|
|
1551
|
-
* captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
|
|
1552
|
-
* }))
|
|
1553
|
-
* .handler(async (ctx, args) => { ... });
|
|
1554
|
-
* ```
|
|
1555
|
-
*
|
|
1556
|
-
* The bundle is context-preserving — each inner middleware leaves the context
|
|
1557
|
-
* unchanged — so it slots into any `.use()` chain without reshaping the
|
|
1558
|
-
* procedure context. Omitted fields are skipped; an empty bundle is a
|
|
1559
|
-
* transparent pass-through.
|
|
1560
|
-
*/
|
|
1561
|
-
declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
|
|
1562
|
-
declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
|
|
1980
|
+
* A custom masking function. Receives the raw cell value and the
|
|
1981
|
+
* {@link MaskContext}, returns the value to surface. Use it for partial masks
|
|
1982
|
+
* (`maskMiddle(phone)`), role-aware reveals (`ctx.auth.can(...) ? value : null`),
|
|
1983
|
+
* or format-preserving tokens. A function that **throws** fails closed — the
|
|
1984
|
+
* cell is redacted to `null`, never leaked raw.
|
|
1985
|
+
*/
|
|
1986
|
+
type MaskFn<Context = unknown> = (value: unknown, context: MaskContext<Context>) => unknown;
|
|
1563
1987
|
/**
|
|
1564
|
-
*
|
|
1565
|
-
*
|
|
1566
|
-
* `
|
|
1567
|
-
*
|
|
1568
|
-
*
|
|
1569
|
-
*
|
|
1570
|
-
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
|
|
1988
|
+
* How a column is masked:
|
|
1989
|
+
*
|
|
1990
|
+
* - `"redact"` — drop the value to `null`. The simplest, safest strategy, and
|
|
1991
|
+
* the right choice for any value that must actually be kept secret.
|
|
1992
|
+
* - `"hash"` — replace with a stable token (unsalted 32-bit FNV-1a hex) so the
|
|
1993
|
+
* same input always yields the same token (joinable/groupable client-side).
|
|
1994
|
+
* **This is NOT a confidentiality control.** It is a non-cryptographic,
|
|
1995
|
+
* unsalted, deterministic, narrow (~2^32) digest: low-entropy values (emails,
|
|
1996
|
+
* phone numbers, SSNs) are brute-force-recoverable by the very caller you are
|
|
1997
|
+
* masking from, and identical values always produce identical tokens across
|
|
1998
|
+
* rows/columns/tenants (enabling correlation). Use `"hash"` ONLY when you want a
|
|
1999
|
+
* stable pseudonym for grouping/joining and leaking the value is acceptable —
|
|
2000
|
+
* never to hide sensitive PII. For PII that must stay hidden, use `"redact"`.
|
|
2001
|
+
* - a {@link MaskFn} — author-defined transform (partial mask, role-aware reveal).
|
|
2002
|
+
*/
|
|
2003
|
+
type MaskStrategy<Context = unknown> = "hash" | "redact" | MaskFn<Context>;
|
|
2004
|
+
/** Per-column strategy map for one table: `{ email: "redact", phone: maskMiddle }`. */
|
|
2005
|
+
type MaskColumns<Context = unknown> = Record<string, MaskStrategy<Context>>;
|
|
1583
2006
|
/**
|
|
1584
|
-
*
|
|
1585
|
-
*
|
|
1586
|
-
*
|
|
1587
|
-
*
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
* Validates against an **accidentally duplicated policy** — the same
|
|
1591
|
-
* `(table, on)` registered with the *same* decision function (a copy-paste, or
|
|
1592
|
-
* the same policy object spread in twice). Because multiple DISTINCT policies
|
|
1593
|
-
* per `(table, on)` are intentional, the check keys on the `when` reference too:
|
|
1594
|
-
* only a reference-identical `when` for the same `(table, on)` is a real
|
|
1595
|
-
* duplicate. Throws at module load so the misconfiguration surfaces immediately
|
|
1596
|
-
* rather than as a silently double-evaluated predicate at request time.
|
|
1597
|
-
*/
|
|
1598
|
-
declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
|
|
1599
|
-
declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
|
|
2007
|
+
* The mask declaration passed to `mask(...)`: a table → column → strategy map.
|
|
2008
|
+
* Deliberately a plain object literal so the codegen feeder can statically read
|
|
2009
|
+
* which columns a procedure masks (powering the `mask_uncovered_pii_column`
|
|
2010
|
+
* advisor lint), exactly as the RLS feeder reads policy tables.
|
|
2011
|
+
*/
|
|
2012
|
+
type MaskPolicies<Context = unknown> = Record<string, MaskColumns<Context>>;
|
|
1600
2013
|
/**
|
|
1601
|
-
*
|
|
1602
|
-
*
|
|
1603
|
-
* `
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
|
|
2014
|
+
* Options for `mask(policies, options)`.
|
|
2015
|
+
*
|
|
2016
|
+
* - `roles` registers the role→permission grants that back `ctx.auth.can(...)`
|
|
2017
|
+
* inside a {@link MaskFn} — identical to `rls(policies, { roles })`. A role
|
|
2018
|
+
* not listed grants no permissions (fails closed for unknown roles).
|
|
2019
|
+
* - `bypass` is a procedure-wide escape hatch: when it returns `true` the whole
|
|
2020
|
+
* mask is skipped (the caller sees raw values). Use it for a privileged
|
|
2021
|
+
* viewer — `bypass: ({ auth }) => auth.can("pii:view")`. Prefer this over
|
|
2022
|
+
* branching every column when an entire class of caller should see clear data.
|
|
2023
|
+
* - `indexFields` closes the bare-index-scan / rank / geo position oracle (see
|
|
2024
|
+
* the `mask/middleware` module docblock's "Residual read-position oracles" section).
|
|
2025
|
+
*/
|
|
2026
|
+
interface MaskOptions<Context = unknown> {
|
|
2027
|
+
readonly bypass?: (context: MaskContext<Context>) => boolean;
|
|
2028
|
+
/**
|
|
2029
|
+
* Per-table, per-KIND index→declared-fields map (regular index `fields`
|
|
2030
|
+
* under `index`; rank index `sortBy` ∪ `partitionBy` under `rank`; geo
|
|
2031
|
+
* index `field` under `geo`). Supplied to close the bare-index-scan /
|
|
2032
|
+
* rank / geo position oracle: a `withIndex(name)` with no range callback,
|
|
2033
|
+
* a `rank`/`rankPage`/`rankBefore` read, or a `withGeoIndex` read, over an
|
|
2034
|
+
* index whose DECLARED fields (for that read's own kind) intersect a
|
|
2035
|
+
* masked column, is rejected. Kept per kind — rather than one flat
|
|
2036
|
+
* name→fields map — because the engine resolves `withIndex` /
|
|
2037
|
+
* `withGeoIndex` / rank reads in three separate namespaces, so the same
|
|
2038
|
+
* index name can legally denote a different index per kind; a flat map
|
|
2039
|
+
* would let one kind's fields shadow another's for a colliding name.
|
|
2040
|
+
* OPTIONAL and additive — omit it and behaviour is unchanged (the oracle
|
|
2041
|
+
* stays open, exactly as before this option existed). Build it with
|
|
2042
|
+
* `indexFieldsFromSchema` (exported from `@lunora/server`):
|
|
2043
|
+
* `mask(policies, { indexFields: indexFieldsFromSchema(schema) })`.
|
|
2044
|
+
*/
|
|
2045
|
+
readonly indexFields?: IndexFieldsByTable;
|
|
2046
|
+
readonly roles?: ReadonlyArray<Role>;
|
|
2047
|
+
}
|
|
2048
|
+
interface QueryPage {
|
|
2049
|
+
continueCursor: null | string;
|
|
2050
|
+
isDone: boolean;
|
|
2051
|
+
page: Record<string, unknown>[];
|
|
2052
|
+
}
|
|
1607
2053
|
interface QueryArgs {
|
|
1608
|
-
baseWhere?:
|
|
2054
|
+
baseWhere?: unknown;
|
|
1609
2055
|
cursor?: null | string;
|
|
1610
2056
|
limit?: number;
|
|
1611
|
-
orderBy?: ReadonlyArray<unknown
|
|
1612
|
-
/**
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
* the top-level read: `(table) => readBase(table).baseWhere`.
|
|
1616
|
-
*/
|
|
1617
|
-
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1618
|
-
restrictsCounts?: boolean;
|
|
1619
|
-
where?: WhereInput;
|
|
2057
|
+
orderBy?: ReadonlyArray<Record<string, unknown>>;
|
|
2058
|
+
/** Per-target-table mask applied to `with`-hydrated children — mirrors `@lunora/shard-engine`'s `RelationMask`. */
|
|
2059
|
+
relationMask?: (table: string, rows: Record<string, unknown>[]) => Record<string, unknown>[];
|
|
2060
|
+
where?: unknown;
|
|
1620
2061
|
with?: Record<string, unknown>;
|
|
1621
2062
|
}
|
|
1622
|
-
interface CountArgs {
|
|
1623
|
-
baseWhere?: WhereInput;
|
|
1624
|
-
relationBaseWhere?: (table: string) => undefined | WhereInput;
|
|
1625
|
-
restrictsCounts?: boolean;
|
|
1626
|
-
where?: WhereInput;
|
|
1627
|
-
}
|
|
1628
|
-
/** Structural mirror of `@lunora/do`'s `AggregateOptions` — only the fields the wrapper touches. */
|
|
1629
2063
|
interface AggregateArgs {
|
|
1630
|
-
baseWhere?: WhereInput;
|
|
1631
2064
|
field?: string;
|
|
1632
2065
|
op: string;
|
|
1633
|
-
|
|
1634
|
-
restrictsCounts?: boolean;
|
|
1635
|
-
where?: WhereInput;
|
|
2066
|
+
where?: unknown;
|
|
1636
2067
|
}
|
|
1637
|
-
/** Structural mirror of `@lunora/do`'s `GroupByOptions`. */
|
|
1638
2068
|
interface GroupByArgs {
|
|
1639
2069
|
agg?: {
|
|
1640
2070
|
field?: string;
|
|
1641
2071
|
op: string;
|
|
1642
2072
|
};
|
|
1643
|
-
baseWhere?: WhereInput;
|
|
1644
2073
|
by: ReadonlyArray<string>;
|
|
1645
|
-
|
|
1646
|
-
restrictsCounts?: boolean;
|
|
1647
|
-
where?: WhereInput;
|
|
1648
|
-
}
|
|
1649
|
-
/** Structural mirror of `@lunora/do`'s `RankOptions`. */
|
|
1650
|
-
interface RankArgs {
|
|
1651
|
-
baseWhere?: WhereInput;
|
|
1652
|
-
restrictsCounts?: boolean;
|
|
1653
|
-
row: Record<string, unknown> | string;
|
|
1654
|
-
where?: WhereInput;
|
|
2074
|
+
where?: unknown;
|
|
1655
2075
|
}
|
|
1656
|
-
/**
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
2076
|
+
/** One row of a `.collectWithScores()` result — mirrors `@lunora/shard-engine`'s `ScoredDocument`. */
|
|
2077
|
+
type ScoredDocument = GeoScoredDocument | SearchScoredDocument;
|
|
2078
|
+
/** A `.withGeoIndex()` row — mirrors `@lunora/shard-engine`'s `GeoScoredDocument`. */
|
|
2079
|
+
interface GeoScoredDocument {
|
|
2080
|
+
distanceMeters: null | number;
|
|
2081
|
+
document: Record<string, unknown>;
|
|
2082
|
+
score?: never;
|
|
1662
2083
|
}
|
|
1663
|
-
/**
|
|
1664
|
-
interface
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
take?: number;
|
|
1669
|
-
where?: WhereInput;
|
|
1670
|
-
}
|
|
1671
|
-
interface QueryPage {
|
|
1672
|
-
continueCursor: null | string;
|
|
1673
|
-
isDone: boolean;
|
|
1674
|
-
page: Record<string, unknown>[];
|
|
2084
|
+
/** A `.withSearchIndex()` row — mirrors `@lunora/shard-engine`'s `SearchScoredDocument`. */
|
|
2085
|
+
interface SearchScoredDocument {
|
|
2086
|
+
distanceMeters?: never;
|
|
2087
|
+
document: Record<string, unknown>;
|
|
2088
|
+
score: number;
|
|
1675
2089
|
}
|
|
1676
2090
|
interface TableReaderLike {
|
|
2091
|
+
[Symbol.asyncIterator]: () => AsyncIterator<Record<string, unknown>>;
|
|
1677
2092
|
collect: () => Promise<Record<string, unknown>[]>;
|
|
2093
|
+
collectWithScores: () => Promise<ScoredDocument[]>;
|
|
1678
2094
|
filter: (predicate: (document: Record<string, unknown>) => boolean) => TableReaderLike;
|
|
1679
2095
|
first: () => Promise<Record<string, unknown> | null>;
|
|
2096
|
+
order: (direction: "asc" | "desc") => TableReaderLike;
|
|
1680
2097
|
paginate: (options: {
|
|
1681
2098
|
cursor?: null | string;
|
|
1682
2099
|
numItems: number;
|
|
1683
2100
|
}) => Promise<QueryPage>;
|
|
1684
2101
|
take: (limit: number) => Promise<Record<string, unknown>[]>;
|
|
2102
|
+
unique: () => Promise<Record<string, unknown> | null>;
|
|
2103
|
+
withGeoIndex: (indexName: string, build: (q: unknown) => unknown) => TableReaderLike;
|
|
1685
2104
|
withIndex: (indexName: string, range?: (q: unknown) => unknown) => TableReaderLike;
|
|
1686
2105
|
withSearchIndex: (indexName: string, search: (q: unknown) => unknown) => TableReaderLike;
|
|
1687
2106
|
}
|
|
1688
2107
|
/**
|
|
1689
|
-
* Structural projection of the runtime ORM writer
|
|
1690
|
-
*
|
|
1691
|
-
* `
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
* Reduce matching rows to a scalar. The RLS wrapper AND-merges the read
|
|
1696
|
-
* `baseWhere` into `options` so the reduction only sees policy-visible rows
|
|
1697
|
-
* (safe: an aggregate scoped to `where` never reveals a hidden row — see
|
|
1698
|
-
* `@lunora/do`'s `RestrictableQueryOptions`). Required: the only writer ever
|
|
1699
|
-
* wrapped is `@lunora/do`'s `createShardCtxDb`, which always implements it.
|
|
1700
|
-
*/
|
|
2108
|
+
* Structural projection of the runtime ORM writer — the same subset
|
|
2109
|
+
* `../rls/middleware` mirrors, so the wrapper is interchangeable between
|
|
2110
|
+
* `@lunora/do`'s and `@lunora/d1`'s `DatabaseWriterLike` without an
|
|
2111
|
+
* inter-package dependency. `rankBefore` is optional (the D1 twin omits it).
|
|
2112
|
+
*/
|
|
2113
|
+
interface MaskDatabase {
|
|
1701
2114
|
aggregate: (tableName: string, options: AggregateArgs) => Promise<null | number>;
|
|
1702
|
-
count: (tableName: string, whereOrArgs?:
|
|
1703
|
-
delete: (id: string, expectedTable?: string
|
|
1704
|
-
hard?: boolean;
|
|
1705
|
-
}) => Promise<void>;
|
|
2115
|
+
count: (tableName: string, whereOrArgs?: unknown) => Promise<number>;
|
|
2116
|
+
delete: (id: string, expectedTable?: string) => Promise<void>;
|
|
1706
2117
|
deleteMany: (ids: ReadonlyArray<string>, options?: {
|
|
1707
2118
|
limit?: number;
|
|
1708
|
-
}
|
|
2119
|
+
}) => Promise<{
|
|
2120
|
+
deleted: number;
|
|
2121
|
+
}>;
|
|
2122
|
+
deleteWhere?: (tableName: string, where: Record<string, unknown>, options?: {
|
|
2123
|
+
limit?: number;
|
|
2124
|
+
}) => Promise<{
|
|
1709
2125
|
deleted: number;
|
|
1710
2126
|
}>;
|
|
1711
2127
|
findFirst: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown> | null>;
|
|
1712
2128
|
findFirstOrThrow: (tableName: string, args?: QueryArgs) => Promise<Record<string, unknown>>;
|
|
1713
2129
|
findMany: (tableName: string, args?: QueryArgs) => Promise<QueryPage>;
|
|
1714
2130
|
get: (id: string, expectedTable?: string) => Promise<Record<string, unknown> | null>;
|
|
1715
|
-
/**
|
|
1716
|
-
* Group + reduce. Same `baseWhere` injection as `aggregate`: the per-group
|
|
1717
|
-
* reduction is scoped to policy-visible rows, so a group count tallies only
|
|
1718
|
-
* rows the caller may read. Required for the same reason as `aggregate`.
|
|
1719
|
-
*/
|
|
1720
2131
|
groupBy: (tableName: string, options: GroupByArgs) => Promise<ReadonlyArray<{
|
|
1721
2132
|
key: Record<string, unknown>;
|
|
1722
2133
|
value: null | number;
|
|
@@ -1724,19 +2135,8 @@ interface DatabaseWriterLike {
|
|
|
1724
2135
|
insert: (tableName: string, document: Record<string, unknown>) => Promise<string>;
|
|
1725
2136
|
insertMany: (tableName: string, documents: ReadonlyArray<Record<string, unknown>>, options?: {
|
|
1726
2137
|
limit?: number;
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
allowExplicitId?: boolean;
|
|
1730
|
-
limit?: number;
|
|
1731
|
-
}) => Promise<string[]>;
|
|
1732
|
-
/**
|
|
1733
|
-
* Optional table-aware lookup. The underlying writer (e.g. `@lunora/do`)
|
|
1734
|
-
* already knows the owning table of an id internally, so it can return
|
|
1735
|
-
* `{ row, tableName }` in a single round-trip. When present, the RLS wrapper
|
|
1736
|
-
* uses it to collapse the per-call membership-probe fan-out (1 `get` + N
|
|
1737
|
-
* `findFirst` across every policy table) down to one lookup. Writers that
|
|
1738
|
-
* don't implement it fall back to the probe path.
|
|
1739
|
-
*/
|
|
2138
|
+
skipDuplicates?: boolean;
|
|
2139
|
+
}) => Promise<(string | null)[]>;
|
|
1740
2140
|
lookupById?: (id: string, expectedTable?: string) => Promise<null | {
|
|
1741
2141
|
row: Record<string, unknown>;
|
|
1742
2142
|
tableName: string;
|
|
@@ -1747,61 +2147,454 @@ interface DatabaseWriterLike {
|
|
|
1747
2147
|
patch: Record<string, unknown>;
|
|
1748
2148
|
}>, options?: {
|
|
1749
2149
|
limit?: number;
|
|
1750
|
-
}
|
|
2150
|
+
}) => Promise<{
|
|
2151
|
+
patched: number;
|
|
2152
|
+
}>;
|
|
2153
|
+
patchWhere?: (tableName: string, args: {
|
|
2154
|
+
patch: Record<string, unknown>;
|
|
2155
|
+
where: Record<string, unknown>;
|
|
2156
|
+
}, options?: {
|
|
2157
|
+
limit?: number;
|
|
2158
|
+
}) => Promise<{
|
|
2159
|
+
patched: number;
|
|
2160
|
+
}>;
|
|
1751
2161
|
query: (tableName: string) => TableReaderLike;
|
|
1752
|
-
|
|
1753
|
-
* Rank a row within its partition. A position is a count-of-rows-before, so
|
|
1754
|
-
* — exactly like `count()` — it can't be trusted in an RLS-restricted
|
|
1755
|
-
* reader: the wrapper fails it closed with `COUNT_RLS_UNSUPPORTED`. Required
|
|
1756
|
-
* for the same reason as `aggregate`.
|
|
1757
|
-
*/
|
|
1758
|
-
rank: (tableName: string, indexName: string, options: RankArgs) => Promise<null | {
|
|
2162
|
+
rank: (tableName: string, indexName: string, options: unknown) => Promise<null | {
|
|
1759
2163
|
position: number;
|
|
1760
2164
|
total: number;
|
|
1761
2165
|
}>;
|
|
1762
|
-
|
|
1763
|
-
rankBefore?: (tableName: string, indexName: string, options: RankBeforeArgs) => Promise<{
|
|
2166
|
+
rankBefore?: (tableName: string, indexName: string, options: unknown) => Promise<{
|
|
1764
2167
|
before: number;
|
|
1765
2168
|
total: number;
|
|
1766
2169
|
}>;
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
* enforced against it (and re-filtering the fetched rows would break page
|
|
1771
|
-
* sizing). RLS therefore fails it closed rather than leak hidden rows.
|
|
1772
|
-
* Required for the same reason as `aggregate`.
|
|
1773
|
-
*/
|
|
1774
|
-
rankPage: (tableName: string, indexName: string, options?: RankPageArgs) => Promise<QueryPage>;
|
|
2170
|
+
rankPage: (tableName: string, indexName: string, options?: unknown) => Promise<QueryPage>;
|
|
2171
|
+
/** Cross-shard companion to `rankPage`, gated the same way `rankPage` is masked below. */
|
|
2172
|
+
rankPageRows?: (tableName: string, indexName: string, options?: unknown) => Promise<ShardRankPageResultLike>;
|
|
1775
2173
|
replace: (id: string, document: Record<string, unknown>, expectedTable?: string) => Promise<void>;
|
|
1776
|
-
|
|
2174
|
+
}
|
|
2175
|
+
interface MaskContextIn {
|
|
2176
|
+
auth?: AuthLike;
|
|
2177
|
+
db: MaskDatabase;
|
|
1777
2178
|
}
|
|
1778
2179
|
/**
|
|
1779
|
-
*
|
|
1780
|
-
*
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1785
|
-
*/
|
|
1786
|
-
|
|
1787
|
-
/**
|
|
1788
|
-
type
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
2180
|
+
* Procedure-builder middleware. Apply per-request via `.use(mask(policies))`.
|
|
2181
|
+
* Closes over the policy map at builder-construction time; resolves identity +
|
|
2182
|
+
* the `bypass` decision per call against the live ctx.
|
|
2183
|
+
*
|
|
2184
|
+
* IMPORTANT: a mask is in scope only for procedures whose builder chain
|
|
2185
|
+
* includes this middleware — opt-in, never global (the same invariant as RLS).
|
|
2186
|
+
*/
|
|
2187
|
+
declare const mask: <Context extends MaskContextIn = MaskContextIn>(policies: MaskPolicies<Context>, options?: MaskOptions<Context>) => Middleware<Context, Context>;
|
|
2188
|
+
/** Project-wide masked-column registry: every table any registered function masks, unioned. */
|
|
2189
|
+
type MaskRegistry = ReadonlyMap<string, ReadonlySet<string>>;
|
|
2190
|
+
/**
|
|
2191
|
+
* Build the project-wide masked-column registry from the registered functions
|
|
2192
|
+
* (pass `Object.values(LUNORA_FUNCTIONS)`) — the mask-column twin of
|
|
2193
|
+
* `buildRlsReadRegistry`. Unions every function's `.use(mask(...))` columns
|
|
2194
|
+
* per table: a column masked by ANY registered function counts as masked in
|
|
2195
|
+
* the registry — there is no "which procedure would this shape have gone
|
|
2196
|
+
* through" question to narrow by, so the union is the only safe answer.
|
|
2197
|
+
*/
|
|
2198
|
+
declare const buildMaskRegistry: (functions: Iterable<unknown>) => MaskRegistry;
|
|
2199
|
+
/** A document handed to a migration transform: the stored row including `_id`/`_creationTime`. */
|
|
2200
|
+
type MigrationDocument = Record<string, unknown>;
|
|
2201
|
+
/**
|
|
2202
|
+
* The read surface a transform reaches through its `ctx`.
|
|
2203
|
+
*
|
|
2204
|
+
* Read-only by design: the runner accounts for exactly one rewrite per row read,
|
|
2205
|
+
* and a transform writing directly would make that count describe something
|
|
2206
|
+
* other than what happened. Scoped to the shard the runner is walking.
|
|
2207
|
+
*/
|
|
2208
|
+
interface MigrationReader {
|
|
2209
|
+
count: (table: string, where?: Record<string, unknown>) => Promise<number>;
|
|
2210
|
+
findFirst: (table: string, args?: Record<string, unknown>) => Promise<MigrationDocument | null>;
|
|
2211
|
+
findMany: (table: string, args?: Record<string, unknown>) => Promise<{
|
|
2212
|
+
isDone: boolean;
|
|
2213
|
+
page: MigrationDocument[];
|
|
2214
|
+
}>;
|
|
2215
|
+
get: (id: string, expectedTable?: string) => Promise<MigrationDocument | null>;
|
|
2216
|
+
}
|
|
2217
|
+
/** The context handed to a transform alongside the row. */
|
|
2218
|
+
interface MigrationCtx {
|
|
2219
|
+
db: MigrationReader;
|
|
2220
|
+
}
|
|
2221
|
+
/**
|
|
2222
|
+
* Transform applied to one document. Return a new document to rewrite the row,
|
|
2223
|
+
* or `undefined` to leave it untouched (skipped, not counted as changed). The
|
|
2224
|
+
* runner always preserves the original `_id` and `_creationTime`, so the
|
|
2225
|
+
* returned document neither needs to nor should change row identity.
|
|
2226
|
+
*
|
|
2227
|
+
* The second parameter carries a shard-scoped reader. Without it a transform
|
|
2228
|
+
* could only rewrite the row it was handed — enough for a backfill whose new
|
|
2229
|
+
* value is a pure function of the old row (`displayName = name ?? "Anonymous"`),
|
|
2230
|
+
* but not for the shape people actually write: read the parent, copy a field
|
|
2231
|
+
* down onto its children.
|
|
2232
|
+
*
|
|
2233
|
+
* May return a promise, since a cross-table read is asynchronous.
|
|
2234
|
+
*
|
|
2235
|
+
* **A shard key cannot be backfilled this way, even with a reader.** A row whose
|
|
2236
|
+
* shard-key field is unset does not belong to any shard, so a shard-scoped query
|
|
2237
|
+
* will not enumerate it; and writing the key would have to MOVE the row to a
|
|
2238
|
+
* different Durable Object, which a per-shard runner cannot do. Re-keying is an
|
|
2239
|
+
* export → transform → import, not a migration.
|
|
2240
|
+
*/
|
|
2241
|
+
type MigrationTransform = (document: MigrationDocument, ctx: MigrationCtx) => MigrationDocument | Promise<MigrationDocument | undefined | void> | undefined | void;
|
|
2242
|
+
interface MigrationDefinition {
|
|
2243
|
+
/** Rows fetched and rewritten per batch. Defaults to the runner's batch size when omitted. */
|
|
2244
|
+
readonly batchSize?: number;
|
|
2245
|
+
/** Optional reverse transform, applied by `migrate down`. */
|
|
2246
|
+
readonly down?: MigrationTransform;
|
|
2247
|
+
/** Stable, unique identifier — the key per-shard run-state is tracked under. */
|
|
2248
|
+
readonly id: string;
|
|
2249
|
+
/** Table whose documents this migration iterates. */
|
|
2250
|
+
readonly table: string;
|
|
2251
|
+
/** Forward transform, applied to every row by `migrate up`. */
|
|
2252
|
+
readonly up: MigrationTransform;
|
|
2253
|
+
}
|
|
2254
|
+
/** A {@link MigrationDefinition} plus the codegen discovery marker. */
|
|
2255
|
+
interface RegisteredMigration extends MigrationDefinition {
|
|
2256
|
+
readonly __lunoraMigration: true;
|
|
2257
|
+
}
|
|
2258
|
+
/** Declare an online data migration. See the module docs for runtime semantics. */
|
|
2259
|
+
declare const defineMigration: (definition: MigrationDefinition) => RegisteredMigration;
|
|
2260
|
+
/**
|
|
2261
|
+
* A mutator declaration. `server` is authoritative; `client` is the optimistic
|
|
2262
|
+
* twin (optional — omit it to let the optimistic write fall through to the
|
|
2263
|
+
* server round-trip with no local preview). Both receive the same validated
|
|
2264
|
+
* `args`.
|
|
2265
|
+
*/
|
|
2266
|
+
interface MutatorDefinition<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> {
|
|
2267
|
+
/**
|
|
2268
|
+
* Validator for the mutator's arguments. Validated on the DO before `server`
|
|
2269
|
+
* runs and (when present) on the client before `client` runs, so both impls
|
|
2270
|
+
* see the same parsed shape. Omit for a parameterless mutator.
|
|
2271
|
+
*/
|
|
2272
|
+
readonly args?: Args;
|
|
2273
|
+
/**
|
|
2274
|
+
* Optimistic client implementation. Runs in a TanStack DB transaction
|
|
2275
|
+
* against the local collections; its writes are applied immediately and
|
|
2276
|
+
* automatically rolled back / rebased as the authoritative result syncs
|
|
2277
|
+
* back. Pure and side-effect-free beyond the local store. Omit to skip the
|
|
2278
|
+
* local preview.
|
|
2279
|
+
*/
|
|
2280
|
+
readonly client?: (tx: ClientTx, args: InferValidatorMap<Args>) => Promise<void> | void;
|
|
2281
|
+
/**
|
|
2282
|
+
* Owner-scope the write: names the column carrying the row owner (e.g.
|
|
2283
|
+
* `owner: "userId"`). Before `server` runs, the mutator requires a verified
|
|
2284
|
+
* identity, rejects a client-supplied owner that disagrees with it, and sets
|
|
2285
|
+
* the column to the verified value — so the impl reads `args[owner]` without
|
|
2286
|
+
* trusting the client and never repeats the check by hand.
|
|
2287
|
+
*
|
|
2288
|
+
* This replaces the "every mutator opens with `assertOwner(ctx, args.userId)`"
|
|
2289
|
+
* pattern, and is the write-side counterpart to an `owner`-scoped
|
|
2290
|
+
* {@link import("./shapes").ShapeDefinition}. Unlike a shape it takes the column
|
|
2291
|
+
* NAME rather than `true`: a shape is bound to one `table`, so the table's
|
|
2292
|
+
* `.ownedBy(field)` resolves unambiguously, whereas one mutator may write
|
|
2293
|
+
* several tables and has no single owning table to read it from.
|
|
2294
|
+
*
|
|
2295
|
+
* Declare the column `v.optional(...)` to leave it off the wire entirely; it is
|
|
2296
|
+
* injected either way.
|
|
2297
|
+
*/
|
|
2298
|
+
readonly owner?: string;
|
|
2299
|
+
/**
|
|
2300
|
+
* Authoritative server implementation. Runs inside the shard DO with a full
|
|
2301
|
+
* {@link MutationContext} (`ctx.db` writer); its writes append to `__cdc_log`
|
|
2302
|
+
* and poke back to subscribers. This is the source of truth — the client
|
|
2303
|
+
* impl is only a prediction of it.
|
|
2304
|
+
*/
|
|
2305
|
+
readonly server: (context: ServerContext, args: InferValidatorMap<Args>) => Promise<R> | R;
|
|
2306
|
+
}
|
|
2307
|
+
/**
|
|
2308
|
+
* A {@link MutatorDefinition} plus the codegen discovery marker and a
|
|
2309
|
+
* dispatch-shaped `handler` (validates `args`, then runs `server`) so the DO
|
|
2310
|
+
* invokes a mutator exactly like a registered procedure.
|
|
2311
|
+
*/
|
|
2312
|
+
interface RegisteredMutator<Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown> extends MutatorDefinition<Args, ServerContext, ClientTx, R> {
|
|
2313
|
+
readonly __lunoraMutator: true;
|
|
2314
|
+
/** Validate `rawArgs`, then run the authoritative `server` impl. Used by the DO push path. */
|
|
2315
|
+
readonly handler: (context: ServerContext, rawArgs: Record<string, unknown>) => Promise<R>;
|
|
2316
|
+
/**
|
|
2317
|
+
* Marks the dispatch kind so codegen can register the mutator in the same
|
|
2318
|
+
* `LUNORA_FUNCTIONS` table queries/mutations use — the DO's `handleRpc`
|
|
2319
|
+
* reads `kind === "mutation"` to wrap the authoritative `server` impl in the
|
|
2320
|
+
* shard's BEGIN/COMMIT span (all-or-nothing writes), exactly like an
|
|
2321
|
+
* ordinary `mutation`.
|
|
2322
|
+
*/
|
|
2323
|
+
readonly kind: "mutation";
|
|
2324
|
+
}
|
|
2325
|
+
/** Declare a custom mutator. See the module docs for runtime semantics. */
|
|
2326
|
+
declare const defineMutator: <Args extends ValidatorMap = ValidatorMap, ServerContext = MutationCtx, ClientTx = unknown, R = unknown>(definition: MutatorDefinition<Args, ServerContext, ClientTx, R>) => RegisteredMutator<Args, ServerContext, ClientTx, R>;
|
|
2327
|
+
/** Default time-to-live for a presence row: a heartbeat keeps a member "present" for this long. */
|
|
2328
|
+
declare const DEFAULT_TTL_MS = 3e4;
|
|
2329
|
+
declare const PRESENCE_BARE_TABLE = "present";
|
|
2330
|
+
/**
|
|
2331
|
+
* The prefixed table name the extension produces at merge time. The handlers
|
|
2332
|
+
* read/write this name directly so they always agree with the merged schema.
|
|
2333
|
+
*/
|
|
2334
|
+
declare const PRESENCE_TABLE: "presence_present";
|
|
2335
|
+
/**
|
|
2336
|
+
* A single present member as returned by `listPresent`.
|
|
2337
|
+
*
|
|
2338
|
+
* Note: the raw client-chosen `sessionId` is deliberately NOT surfaced. It is a
|
|
2339
|
+
* connection secret — disclosing every member's `sessionId` would let any
|
|
2340
|
+
* subscriber enumerate them and target the heartbeat / disconnect write paths.
|
|
2341
|
+
* A "who's here" UI needs only `userId` + awareness `data`; the caller already
|
|
2342
|
+
* knows its own session id locally (the `usePresence` hook returns it).
|
|
2343
|
+
*/
|
|
2344
|
+
interface PresenceMember {
|
|
2345
|
+
/** Opaque awareness blob (selection, cursor, name, color…). */
|
|
2346
|
+
data?: Record<string, unknown>;
|
|
2347
|
+
/** Last heartbeat time (epoch ms). */
|
|
2348
|
+
lastSeen: number;
|
|
2349
|
+
/** The room / channel / document this presence is scoped to. */
|
|
2350
|
+
roomId: string;
|
|
2351
|
+
/** Authenticated user id, when known. */
|
|
2352
|
+
userId?: string;
|
|
2353
|
+
}
|
|
2354
|
+
/** Options for {@link definePresence}. */
|
|
2355
|
+
interface DefinePresenceOptions {
|
|
2356
|
+
/**
|
|
2357
|
+
* Grace window (ms) before a gracefully-closed session is dropped from the
|
|
2358
|
+
* present list. When `0` (the default), `onDisconnect` hard-deletes the
|
|
2359
|
+
* session's row the instant its socket closes. When `> 0`, the row is
|
|
2360
|
+
* instead aged so the read-time TTL filter hides it `disconnectGraceMs`
|
|
2361
|
+
* from now — a reconnect with the same `sessionId` within the window
|
|
2362
|
+
* re-heartbeats and restores full presence with no visible flicker (the
|
|
2363
|
+
* AnyCable `presence_ttl` behaviour). Clamped to `ttlMs`.
|
|
2364
|
+
*/
|
|
2365
|
+
disconnectGraceMs?: number;
|
|
2366
|
+
/**
|
|
2367
|
+
* How long (ms) a heartbeat keeps a member present. `listPresent` excludes
|
|
2368
|
+
* rows whose `lastSeen` is older than `now - ttlMs`. Defaults to 30s.
|
|
2369
|
+
*/
|
|
2370
|
+
ttlMs?: number;
|
|
2371
|
+
}
|
|
2372
|
+
/** The registered functions a presence component ships. */
|
|
2373
|
+
interface PresenceFunctions {
|
|
2374
|
+
/**
|
|
2375
|
+
* Connection-lifecycle hook: the instant a client's WebSocket drops, hard-
|
|
2376
|
+
* delete its presence row so it disappears from `listPresent` with no TTL
|
|
2377
|
+
* lag. Targets the row by the `{ roomId, sessionId }` the client passed as
|
|
2378
|
+
* the connection `context`, and only deletes it when the disconnecting
|
|
2379
|
+
* VERIFIED identity owns the row (so a forged context can't evict another
|
|
2380
|
+
* member). The TTL filter + `sweep` remain the fallback for ungraceful drops
|
|
2381
|
+
* where no `context` was recorded.
|
|
2382
|
+
*/
|
|
2383
|
+
disconnect: RegisteredLifecycleHook;
|
|
2384
|
+
/**
|
|
2385
|
+
* Upsert the caller's presence row for `roomId` and stamp `lastSeen = now`.
|
|
2386
|
+
* Keyed by `(roomId, sessionId)` — re-heartbeats patch the existing row so
|
|
2387
|
+
* subscribers receive a single-row delta, not a churn of insert/delete. A
|
|
2388
|
+
* heartbeat may only patch a row owned by the same identity (an existing row
|
|
2389
|
+
* held by a different `userId` is refused with `FORBIDDEN`), so a client
|
|
2390
|
+
* can't overwrite another member's awareness data via a guessed `sessionId`.
|
|
2391
|
+
*/
|
|
2392
|
+
heartbeat: RegisteredMutation<{
|
|
2393
|
+
data: ReturnType<typeof v.optional>;
|
|
2394
|
+
roomId: ReturnType<typeof v.string>;
|
|
2395
|
+
sessionId: ReturnType<typeof v.string>;
|
|
2396
|
+
}, {
|
|
2397
|
+
lastSeen: number;
|
|
2398
|
+
}>;
|
|
2399
|
+
/**
|
|
2400
|
+
* Live query returning the non-expired members of `roomId`, newest heartbeat
|
|
2401
|
+
* first. Subscribe to it for a reactive "who's here" list.
|
|
2402
|
+
*/
|
|
2403
|
+
listPresent: RegisteredQuery<{
|
|
2404
|
+
roomId: ReturnType<typeof v.string>;
|
|
2405
|
+
}, PresenceMember[]>;
|
|
2406
|
+
/**
|
|
2407
|
+
* Internal mutation that hard-deletes every expired row for `roomId`. Stale
|
|
2408
|
+
* rows already vanish from `listPresent` via the read-time TTL filter; this
|
|
2409
|
+
* only reclaims storage. Schedule it (cron / `runAfter`) if you care.
|
|
2410
|
+
*/
|
|
2411
|
+
sweep: RegisteredMutation<{
|
|
2412
|
+
roomId: ReturnType<typeof v.string>;
|
|
2413
|
+
}, {
|
|
2414
|
+
deleted: number;
|
|
2415
|
+
}>;
|
|
2416
|
+
}
|
|
2417
|
+
/** The component shape `definePresence` returns: the presence extension + typed functions. */
|
|
2418
|
+
type PresenceComponent = Component<{
|
|
2419
|
+
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
2420
|
+
}> & {
|
|
2421
|
+
functions: PresenceFunctions;
|
|
1792
2422
|
};
|
|
1793
|
-
/**
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
2423
|
+
/**
|
|
2424
|
+
* The presence schema extension: a single `present` table, auto-namespaced to
|
|
2425
|
+
* `presence_present` at merge time, indexed by `(roomId, sessionId)` for the
|
|
2426
|
+
* heartbeat upsert and by `roomId` for `listPresent`.
|
|
2427
|
+
*/
|
|
2428
|
+
declare const presenceExtension: SchemaExtension<{
|
|
2429
|
+
[PRESENCE_BARE_TABLE]: ReturnType<typeof defineTable>;
|
|
2430
|
+
}>;
|
|
2431
|
+
declare const definePresence: (options?: DefinePresenceOptions) => PresenceComponent;
|
|
2432
|
+
/**
|
|
2433
|
+
* The middlewares `protectPublic` chains, in the order they run. Every field is
|
|
2434
|
+
* optional, so a bundle can be just a rate limit, just a captcha, or any mix —
|
|
2435
|
+
* pass the already-constructed middlewares (e.g. `rateLimit(limiter, "signup")`
|
|
2436
|
+
* from `@lunora/ratelimit`, `verifyTurnstileMiddleware({...})` from
|
|
2437
|
+
* `@lunora/auth`). They are accepted as values rather than imported here so
|
|
2438
|
+
* `@lunora/server` keeps no dependency on those packages (which depend on it).
|
|
2439
|
+
*/
|
|
2440
|
+
interface ProtectPublicOptions<Context> {
|
|
2441
|
+
/**
|
|
2442
|
+
* A CAPTCHA / bot check, run after the rate limit. Placed second on purpose:
|
|
2443
|
+
* an obvious flood is cheaper to reject with the in-memory limiter than with
|
|
2444
|
+
* a Turnstile siteverify round-trip.
|
|
2445
|
+
*/
|
|
2446
|
+
captcha?: Middleware<Context, Context>;
|
|
2447
|
+
/**
|
|
2448
|
+
* A rate limit, run first. Cheapest gate, so it sheds obvious abuse before
|
|
2449
|
+
* any network-bound check below it runs.
|
|
2450
|
+
*/
|
|
2451
|
+
rateLimit?: Middleware<Context, Context>;
|
|
2452
|
+
/** Extra middlewares appended after `rateLimit` and `captcha`, in order. */
|
|
2453
|
+
use?: ReadonlyArray<Middleware<Context, Context>>;
|
|
1797
2454
|
}
|
|
1798
|
-
declare const rls: <Context extends RlsContextIn = RlsContextIn>(policies: ReadonlyArray<Policy<Context>>, options?: RlsOptions) => Middleware<Context, Context>;
|
|
1799
2455
|
/**
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
1804
|
-
|
|
2456
|
+
* Compose the recommended public-procedure protections into a single
|
|
2457
|
+
* `.use()`-able middleware. It is thin sugar over middleware composition — no
|
|
2458
|
+
* new enforcement engine — chaining (in order) a rate limit, a CAPTCHA check,
|
|
2459
|
+
* and any extra middlewares so a public mutation that creates users, sends
|
|
2460
|
+
* mail, or consumes credits is guarded in one attachment:
|
|
2461
|
+
*
|
|
2462
|
+
* ```ts
|
|
2463
|
+
* export const signUp = mutation
|
|
2464
|
+
* .use(protectPublic({
|
|
2465
|
+
* rateLimit: rateLimit(limiter, "signup"),
|
|
2466
|
+
* captcha: verifyTurnstileMiddleware({ secret: env.TURNSTILE_SECRET_KEY, token: (c) => c.args.captchaToken }),
|
|
2467
|
+
* }))
|
|
2468
|
+
* .handler(async (ctx, args) => { ... });
|
|
2469
|
+
* ```
|
|
2470
|
+
*
|
|
2471
|
+
* The bundle is context-preserving — each inner middleware leaves the context
|
|
2472
|
+
* unchanged — so it slots into any `.use()` chain without reshaping the
|
|
2473
|
+
* procedure context. Omitted fields are skipped; an empty bundle is a
|
|
2474
|
+
* transparent pass-through.
|
|
2475
|
+
*/
|
|
2476
|
+
declare const protectPublic: <Context>(options: ProtectPublicOptions<Context>) => Middleware<Context, Context>;
|
|
2477
|
+
/**
|
|
2478
|
+
* The read a reactor watches. Runs on every flush that touched a table it read
|
|
2479
|
+
* last time, so keep it indexed and bounded — this is the reactor's steady-state
|
|
2480
|
+
* cost, paid whether or not the handler ends up running.
|
|
2481
|
+
*/
|
|
2482
|
+
type ReactorSelect<T> = (context: QueryCtx) => Promise<T> | T;
|
|
2483
|
+
/** What runs when {@link ReactorSelect}'s result changes. Receives the current result. */
|
|
2484
|
+
type ReactorHandler<T> = (context: MutationCtx, result: T) => Promise<void> | void;
|
|
2485
|
+
/**
|
|
2486
|
+
* A registered reactor — an internal mutation tagged `lifecycle: "reactor"`.
|
|
2487
|
+
*
|
|
2488
|
+
* Typed separately from `RegisteredLifecycleHook` (which returns `void`) because
|
|
2489
|
+
* a reactor dispatch reports a {@link ReactorOutcome} back to the shard: the
|
|
2490
|
+
* digest becomes the next baseline, and the shard cannot compute it itself
|
|
2491
|
+
* without re-running the read.
|
|
2492
|
+
*/
|
|
2493
|
+
type RegisteredReactor = RegisteredFunction<Record<string, never>, ReactorOutcome, "mutation"> & {
|
|
2494
|
+
readonly lifecycle: "reactor";
|
|
2495
|
+
};
|
|
2496
|
+
/** What a reactor dispatch reports back so the shard can update the baseline. */
|
|
2497
|
+
interface ReactorOutcome {
|
|
2498
|
+
/** Digest of the result the handler was given (or would have been). */
|
|
2499
|
+
digest: string;
|
|
2500
|
+
/** Whether the handler actually ran — `false` when the result was unchanged. */
|
|
2501
|
+
ran: boolean;
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Register a reactor: run `handler` after a write flush, whenever `select`'s
|
|
2505
|
+
* result differs from the last result the handler saw. See the module docblock
|
|
2506
|
+
* for the trigger comparison and the convergence contract.
|
|
2507
|
+
*
|
|
2508
|
+
* The read is an inline callback rather than a reference to a registered query
|
|
2509
|
+
* on purpose. It keeps the declaration statically discoverable with no import
|
|
2510
|
+
* resolution and no dependency on `_generated/api`, and it lets the read and the
|
|
2511
|
+
* handler share one transaction and one read footprint.
|
|
2512
|
+
*
|
|
2513
|
+
* That shared footprint is deliberately conservative: it is the union of what
|
|
2514
|
+
* `select` AND `handler` read, so a table the handler merely consults can wake
|
|
2515
|
+
* the reactor later. The cost of that is one extra `select` — whose digest then
|
|
2516
|
+
* matches and suppresses the handler — never a missed reaction.
|
|
2517
|
+
*/
|
|
2518
|
+
declare const onQueryChange: <T>(select: ReactorSelect<T>, handler: ReactorHandler<T>) => RegisteredReactor;
|
|
2519
|
+
declare const definePolicy: <Context = unknown>(input: DefinePolicyInput<Context>) => Policy<Context>;
|
|
2520
|
+
/**
|
|
2521
|
+
* Build a project-bound, relation-aware `definePolicy` typed against the
|
|
2522
|
+
* generated `DataModel` (`DM`) + `Relations` (`REL`) maps. Codegen emits a
|
|
2523
|
+
* `createPolicyDsl<DataModel, Relations>()` binding into `_generated/server.ts`,
|
|
2524
|
+
* so importing `definePolicy` from the generated module constrains `table` to a
|
|
2525
|
+
* real table name and type-checks the `when` predicate — including Prisma-style
|
|
2526
|
+
* relation predicates (`is`/`some`/…) the `@lunora/do` pre-resolver now resolves
|
|
2527
|
+
* on reads. The runtime is byte-for-byte the untyped {@link definePolicy}; only
|
|
2528
|
+
* the compile-time surface narrows, so a policy authored either way is
|
|
2529
|
+
* discovered identically by the `rls()` chain.
|
|
2530
|
+
*/
|
|
2531
|
+
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>;
|
|
2532
|
+
/**
|
|
2533
|
+
* Declare a named permission a policy can check with `ctx.auth.can(...)`. Grant
|
|
2534
|
+
* it to a role through `defineRole`'s `permissions`, register those roles with
|
|
2535
|
+
* the middleware via `rls(policies, { roles })`, then check it in a policy with
|
|
2536
|
+
* `when: ({ auth }) => auth.can(permission)`. See the `./index` JSDoc for a
|
|
2537
|
+
* worked example.
|
|
2538
|
+
*/
|
|
2539
|
+
declare const definePermission: (name: string, options?: Omit<Permission, "name">) => Permission;
|
|
2540
|
+
/**
|
|
2541
|
+
* Collect a list of policies into the structure the `rls()` middleware
|
|
2542
|
+
* consumes. Multiple read policies on the same table OR together (any one
|
|
2543
|
+
* matching reveals the row); multiple write policies for the same `(table,
|
|
2544
|
+
* op)` AND together (every one must allow). The middleware keeps them in order
|
|
2545
|
+
* and decides — see `./middleware`.
|
|
2546
|
+
*
|
|
2547
|
+
* Validates against an **accidentally duplicated policy** — the same
|
|
2548
|
+
* `(table, on)` registered with the *same* decision function (a copy-paste, or
|
|
2549
|
+
* the same policy object spread in twice). Because multiple DISTINCT policies
|
|
2550
|
+
* per `(table, on)` are intentional, the check keys on the `when` reference too:
|
|
2551
|
+
* only a reference-identical `when` for the same `(table, on)` is a real
|
|
2552
|
+
* duplicate. Throws at module load so the misconfiguration surfaces immediately
|
|
2553
|
+
* rather than as a silently double-evaluated predicate at request time.
|
|
2554
|
+
*/
|
|
2555
|
+
declare const definePolicies: <Context = unknown>(policies: ReadonlyArray<Policy<Context>>) => ReadonlyArray<Policy<Context>>;
|
|
2556
|
+
declare const defineRole: (name: string, options?: Omit<Role, "name">) => Role;
|
|
2557
|
+
/**
|
|
2558
|
+
* A predicate matching **no** rows — the deny decision.
|
|
2559
|
+
*
|
|
2560
|
+
* `{ OR: [] }` is a disjunction over zero branches, which the where-compiler
|
|
2561
|
+
* folds to a constant false. Use it (never a bare `{}`) for the unauthorized
|
|
2562
|
+
* branch of a shape or policy predicate:
|
|
2563
|
+
*
|
|
2564
|
+
* ```ts
|
|
2565
|
+
* where: (ctx, { userId }) => (ctx.auth.userId === userId ? { userId } : deny())
|
|
2566
|
+
* ```
|
|
2567
|
+
*
|
|
2568
|
+
* A fresh object per call, so a caller can safely spread or extend the result.
|
|
2569
|
+
*/
|
|
2570
|
+
declare const deny: () => WhereInput;
|
|
2571
|
+
/**
|
|
2572
|
+
* A predicate matching **every** row — no restriction beyond whatever it is
|
|
2573
|
+
* AND-composed with. The honest spelling of "this branch adds no filter", where a
|
|
2574
|
+
* bare `{}` reads like an oversight.
|
|
2575
|
+
*/
|
|
2576
|
+
declare const allowAll: () => WhereInput;
|
|
2577
|
+
/**
|
|
2578
|
+
* `true` when `where` is the vacuously-false deny predicate — i.e. an `OR` over
|
|
2579
|
+
* zero branches, carrying no other constraint. Used by the runtime to short-circuit
|
|
2580
|
+
* a denied read instead of compiling and running SQL that provably matches nothing.
|
|
2581
|
+
*/
|
|
2582
|
+
declare const isDeny: (where: WhereInput) => boolean;
|
|
2583
|
+
/**
|
|
2584
|
+
* Normalize a boolean-or-predicate decision to a {@link WhereInput}.
|
|
2585
|
+
*
|
|
2586
|
+
* The authoring surfaces accept `true`/`false` as sugar — they read far better
|
|
2587
|
+
* than the sentinel objects in an ownership check — and every consumer downstream
|
|
2588
|
+
* wants a plain predicate. `undefined` means "no opinion" and maps to
|
|
2589
|
+
* {@link allowAll}, matching the RLS `PolicyDecision` contract.
|
|
2590
|
+
*/
|
|
2591
|
+
declare const toWhereInput: (decision: WhereInput | boolean | undefined) => WhereInput;
|
|
2592
|
+
/**
|
|
2593
|
+
* One `rls()` tag's read policies for a table, paired with the role→permission
|
|
2594
|
+
* grants of that SAME middleware. Keeping the role map per-group is what lets a
|
|
2595
|
+
* policy's `auth.can(...)` resolve against its own middleware's roles — never a
|
|
2596
|
+
* permission registered on a different `rls()` step.
|
|
2597
|
+
*/
|
|
1805
2598
|
interface ScopedReadPolicies {
|
|
1806
2599
|
readonly policies: ReadonlyArray<Policy>;
|
|
1807
2600
|
readonly rolePermissions: ReadonlyMap<string, ReadonlySet<string>>;
|
|
@@ -1830,86 +2623,115 @@ interface ShapeReadWhereRequest {
|
|
|
1830
2623
|
readonly userId: null | string;
|
|
1831
2624
|
}
|
|
1832
2625
|
/**
|
|
1833
|
-
* Build the read-policy registry from the registered functions (pass
|
|
1834
|
-
* `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
|
|
1835
|
-
* grouped per `rls()` middleware so each group keeps its own role→permission map
|
|
1836
|
-
* (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
|
|
1837
|
-
* several procedures (a shared `const guard = rls(...)`) is folded once. This
|
|
1838
|
-
* mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
|
|
1839
|
-
* resolves against the roles of the middleware that declared it, never a union.
|
|
1840
|
-
*/
|
|
2626
|
+
* Build the read-policy registry from the registered functions (pass
|
|
2627
|
+
* `Object.values(LUNORA_FUNCTIONS)`). Only `on: "read"` policies are collected,
|
|
2628
|
+
* grouped per `rls()` middleware so each group keeps its own role→permission map
|
|
2629
|
+
* (a `(table, when)` pair is de-duplicated within a tag). A tag reused across
|
|
2630
|
+
* several procedures (a shared `const guard = rls(...)`) is folded once. This
|
|
2631
|
+
* mirrors the request-time `rls()` path exactly: a policy's `auth.can(...)`
|
|
2632
|
+
* resolves against the roles of the middleware that declared it, never a union.
|
|
2633
|
+
*/
|
|
1841
2634
|
declare const buildRlsReadRegistry: (functions: Iterable<unknown>) => RlsReadRegistry;
|
|
1842
2635
|
/**
|
|
1843
|
-
* Compute the effective `where` a shape replicates: the table's RLS read
|
|
1844
|
-
* base-where AND the shape's own predicate. Returns the shape predicate
|
|
1845
|
-
* unchanged for a table with no read policy (a `.public()` or non-RLS table),
|
|
1846
|
-
* and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
|
|
1847
|
-
* exposes a protected, policy-less table.
|
|
1848
|
-
*/
|
|
2636
|
+
* Compute the effective `where` a shape replicates: the table's RLS read
|
|
2637
|
+
* base-where AND the shape's own predicate. Returns the shape predicate
|
|
2638
|
+
* unchanged for a table with no read policy (a `.public()` or non-RLS table),
|
|
2639
|
+
* and the FALSE sentinel (replicate nothing) when a `.rls("required")` schema
|
|
2640
|
+
* exposes a protected, policy-less table.
|
|
2641
|
+
*/
|
|
1849
2642
|
declare const composeShapeReadWhere: (registry: RlsReadRegistry, request: ShapeReadWhereRequest) => WhereInput;
|
|
1850
2643
|
/**
|
|
1851
|
-
* A shape declaration. `where` receives the trusted procedure context and the
|
|
1852
|
-
* validated client args and returns the same {@link WhereInput} shape the RLS
|
|
1853
|
-
* DSL uses, so the DO can AND-merge it with the table's read base-where via the
|
|
1854
|
-
* existing where-compiler (zero second predicate implementation).
|
|
1855
|
-
*/
|
|
2644
|
+
* A shape declaration. `where` receives the trusted procedure context and the
|
|
2645
|
+
* validated client args and returns the same {@link WhereInput} shape the RLS
|
|
2646
|
+
* DSL uses, so the DO can AND-merge it with the table's read base-where via the
|
|
2647
|
+
* existing where-compiler (zero second predicate implementation).
|
|
2648
|
+
*/
|
|
1856
2649
|
interface ShapeDefinition<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> {
|
|
1857
2650
|
/**
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
2651
|
+
* Validator for the client-supplied shape parameters. Validated on the DO
|
|
2652
|
+
* before `where` runs, so a malformed `args` envelope is rejected at the
|
|
2653
|
+
* subscription boundary rather than silently widening the partition. Omit
|
|
2654
|
+
* for a parameterless shape.
|
|
2655
|
+
*/
|
|
1863
2656
|
readonly args?: Args;
|
|
1864
2657
|
/**
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
2658
|
+
* Project the replicated rows to these columns (the system columns `_id` and
|
|
2659
|
+
* `_creationTime` are always included). Omit to replicate every column. An
|
|
2660
|
+
* empty array is rejected — it would replicate no data, which is never the
|
|
2661
|
+
* intent.
|
|
2662
|
+
*/
|
|
1870
2663
|
readonly columns?: ReadonlyArray<string>;
|
|
2664
|
+
/**
|
|
2665
|
+
* Restrict this shape to rows the **subscriber owns**, without writing the
|
|
2666
|
+
* predicate by hand.
|
|
2667
|
+
*
|
|
2668
|
+
* `true` uses the owning column declared by `.ownedBy(field)` on the table;
|
|
2669
|
+
* `"field"` names the owning column here instead (for a table that has no
|
|
2670
|
+
* `.ownedBy()`, or a shape that scopes by a different column).
|
|
2671
|
+
*
|
|
2672
|
+
* The derived predicate is `{ [field]: ctx.auth.userId }`, AND-composed with
|
|
2673
|
+
* the shape's own `where` when both are present, and an anonymous subscriber is denied
|
|
2674
|
+
* outright. Because the value comes from the socket's verified identity rather
|
|
2675
|
+
* than from `args`, a client cannot ask for another user's partition at all —
|
|
2676
|
+
* strictly safer than accepting a `userId` arg and comparing it.
|
|
2677
|
+
*
|
|
2678
|
+
* This is the primitive for the "every shape repeats the same ownership check"
|
|
2679
|
+
* pattern: with it, an owner-scoped shape needs no `where` at all.
|
|
2680
|
+
*/
|
|
2681
|
+
readonly owner?: string | true;
|
|
1871
2682
|
/** Logical table this shape replicates a partition of. */
|
|
1872
2683
|
readonly table: string;
|
|
1873
2684
|
/**
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
2685
|
+
* Predicate selecting the rows this shape replicates. AND-composed with the
|
|
2686
|
+
* table's RLS read base-where on the DO. Runs server-side with a trusted
|
|
2687
|
+
* `ctx` (identity/auth the client can't forge) and the validated client
|
|
2688
|
+
* `args`; returns a {@link WhereInput} using the same operator set as the
|
|
2689
|
+
* SQL compiler (`eq`/`in`/`lt`/… + `AND`/`OR`/`NOT`).
|
|
2690
|
+
*
|
|
2691
|
+
* `true`/`false` are accepted as sugar for "no further restriction" / "deny" —
|
|
2692
|
+
* `false` compiles to the vacuously-false predicate, so a denial branch can
|
|
2693
|
+
* never be mistyped as the everything-matches `{}`. Optional when `owner` is
|
|
2694
|
+
* set; otherwise required.
|
|
2695
|
+
*/
|
|
2696
|
+
readonly where?: (context: Context, args: InferValidatorMap<Args>) => WhereInput | boolean;
|
|
1881
2697
|
}
|
|
1882
2698
|
/** A {@link ShapeDefinition} plus the codegen discovery marker and a dispatch-shaped `compileWhere`. */
|
|
1883
2699
|
interface RegisteredShape<Args extends ValidatorMap = ValidatorMap, Context = QueryCtx> extends ShapeDefinition<Args, Context> {
|
|
1884
2700
|
readonly __lunoraShape: true;
|
|
1885
2701
|
/**
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
2702
|
+
* Validate `rawArgs`, then evaluate the shape's predicate under the trusted
|
|
2703
|
+
* `ctx` and return its {@link WhereInput}. Used by the generated DO's
|
|
2704
|
+
* `resolveShape` override: `ctx` is erased to `unknown` at this dispatch
|
|
2705
|
+
* boundary (the DO builds it from the socket's verified identity and hands it
|
|
2706
|
+
* back as the concrete {@link QueryContext} the predicate expects), exactly like
|
|
2707
|
+
* `RegisteredLunoraFunction.handler` erases its context.
|
|
2708
|
+
*
|
|
2709
|
+
* `options.ownerField` carries the table's `.ownedBy(field)` column, which only
|
|
2710
|
+
* the caller (the DO, holding the schema) can look up — it is what resolves an
|
|
2711
|
+
* `owner: true` shape. Omitted for an `owner: "field"` or plain `where` shape.
|
|
2712
|
+
*/
|
|
2713
|
+
readonly compileWhere: (context: unknown, rawArgs: Record<string, unknown>, options?: {
|
|
2714
|
+
ownerField?: string;
|
|
2715
|
+
}) => WhereInput;
|
|
1894
2716
|
}
|
|
1895
2717
|
/** Declare a replication shape. See the module docs for runtime semantics. */
|
|
1896
2718
|
declare const defineShape: <Args extends ValidatorMap = ValidatorMap, Context = QueryCtx>(definition: ShapeDefinition<Args, Context>) => RegisteredShape<Args, Context>;
|
|
1897
2719
|
/**
|
|
1898
|
-
* Operations a storage rule can gate. `read` covers `download` / `getMetadata`
|
|
1899
|
-
* / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
|
|
1900
|
-
* `delete` is `delete`; `list` is a prefix listing (governed via the file
|
|
1901
|
-
* browser / admin path, not `ctx.storage` which has no `list`).
|
|
1902
|
-
*/
|
|
2720
|
+
* Operations a storage rule can gate. `read` covers `download` / `getMetadata`
|
|
2721
|
+
* / `getSignedUrl` / `getUrl`; `write` covers `store` / `generateUploadUrl`;
|
|
2722
|
+
* `delete` is `delete`; `list` is a prefix listing (governed via the file
|
|
2723
|
+
* browser / admin path, not `ctx.storage` which has no `list`).
|
|
2724
|
+
*/
|
|
1903
2725
|
type StorageOperation = "delete" | "list" | "read" | "write";
|
|
1904
2726
|
/** A rule's decision. `true` allows, `false` denies, `undefined` opts this rule out. */
|
|
1905
2727
|
type StorageRuleDecision = boolean | undefined;
|
|
1906
2728
|
/**
|
|
1907
|
-
* Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
|
|
1908
|
-
* (the per-request userId / roles / identity and the `can(permission)` helper),
|
|
1909
|
-
* so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
|
|
1910
|
-
* `key` is the object key the operation targets (for `list`, the listing
|
|
1911
|
-
* prefix). `ctx` is the full procedure context the middleware closed over.
|
|
1912
|
-
*/
|
|
2729
|
+
* Context handed to a storage rule. `auth` mirrors RLS's `PolicyContext.auth`
|
|
2730
|
+
* (the per-request userId / roles / identity and the `can(permission)` helper),
|
|
2731
|
+
* so a rule reads `({ auth, key }) => key.startsWith(`user/${auth.userId}/`)`.
|
|
2732
|
+
* `key` is the object key the operation targets (for `list`, the listing
|
|
2733
|
+
* prefix). `ctx` is the full procedure context the middleware closed over.
|
|
2734
|
+
*/
|
|
1913
2735
|
interface StorageRuleContext<Context = unknown> {
|
|
1914
2736
|
readonly auth: {
|
|
1915
2737
|
readonly can: (permission: Permission | string) => boolean;
|
|
@@ -1924,11 +2746,11 @@ interface StorageRuleContext<Context = unknown> {
|
|
|
1924
2746
|
/** A registered storage rule as stored in the rule table. */
|
|
1925
2747
|
interface StorageRule<Context = unknown> {
|
|
1926
2748
|
/**
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
2749
|
+
* Logical bucket the rule governs — matched against the accessor's bucket
|
|
2750
|
+
* (`ctx.storage.bucketName`, or the bucket selected via `ctx.storage.bucket(name)`).
|
|
2751
|
+
* A rule only applies to operations on its own bucket. The unnamed bucket is
|
|
2752
|
+
* `"default"`. Also surfaced in the studio's access-rules view.
|
|
2753
|
+
*/
|
|
1932
2754
|
readonly bucket: string;
|
|
1933
2755
|
readonly on: StorageOperation;
|
|
1934
2756
|
/** Optional key-prefix scope; the rule only governs keys under it. Absent ⇒ the whole bucket. */
|
|
@@ -1943,38 +2765,32 @@ interface DefineStorageRuleInput<Context = unknown> {
|
|
|
1943
2765
|
when: (context: StorageRuleContext<Context>) => StorageRuleDecision;
|
|
1944
2766
|
}
|
|
1945
2767
|
/**
|
|
1946
|
-
* Options for the `storageRules(rules, options)` middleware. `roles` registers
|
|
1947
|
-
* the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
|
|
1948
|
-
* `RlsOptions.roles` does — fail-closed for unlisted roles.
|
|
1949
|
-
*/
|
|
2768
|
+
* Options for the `storageRules(rules, options)` middleware. `roles` registers
|
|
2769
|
+
* the role→permission grants that back `ctx.auth.can(...)`, exactly as RLS's
|
|
2770
|
+
* `RlsOptions.roles` does — fail-closed for unlisted roles.
|
|
2771
|
+
*/
|
|
1950
2772
|
interface StorageRulesOptions {
|
|
1951
2773
|
readonly roles?: ReadonlyArray<Role>;
|
|
1952
2774
|
}
|
|
1953
2775
|
declare const defineStorageRule: <Context = unknown>(input: DefineStorageRuleInput<Context>) => StorageRule<Context>;
|
|
1954
2776
|
/**
|
|
1955
|
-
* Collect a list of storage rules into the structure the `storageRules()`
|
|
1956
|
-
* middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
|
|
1957
|
-
* any one allowing grants the operation (each rule grants a slice of the
|
|
1958
|
-
* keyspace).
|
|
1959
|
-
*
|
|
1960
|
-
* Validates against an **accidentally duplicated rule** — the same
|
|
1961
|
-
* `(bucket, on, prefix)` registered with the *same* decision function (a
|
|
1962
|
-
* copy-paste, or the same rule object spread in twice). Because multiple
|
|
1963
|
-
* DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
|
|
1964
|
-
* `when` reference too. Throws at module load so the misconfiguration surfaces
|
|
1965
|
-
* immediately rather than as a silently double-evaluated predicate.
|
|
1966
|
-
*/
|
|
2777
|
+
* Collect a list of storage rules into the structure the `storageRules()`
|
|
2778
|
+
* middleware consumes. Multiple rules for the same `(bucket, on)` OR together —
|
|
2779
|
+
* any one allowing grants the operation (each rule grants a slice of the
|
|
2780
|
+
* keyspace).
|
|
2781
|
+
*
|
|
2782
|
+
* Validates against an **accidentally duplicated rule** — the same
|
|
2783
|
+
* `(bucket, on, prefix)` registered with the *same* decision function (a
|
|
2784
|
+
* copy-paste, or the same rule object spread in twice). Because multiple
|
|
2785
|
+
* DISTINCT rules per `(bucket, on)` are intentional, the check keys on the
|
|
2786
|
+
* `when` reference too. Throws at module load so the misconfiguration surfaces
|
|
2787
|
+
* immediately rather than as a silently double-evaluated predicate.
|
|
2788
|
+
*/
|
|
1967
2789
|
declare const defineStorageRules: <Context = unknown>(rules: ReadonlyArray<StorageRule<Context>>) => ReadonlyArray<StorageRule<Context>>;
|
|
1968
|
-
/** 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`). */
|
|
1969
|
-
type StorageAuthLike = {
|
|
1970
|
-
getIdentity?: () => Promise<Record<string, unknown> | null>;
|
|
1971
|
-
roles?: ReadonlyArray<string>;
|
|
1972
|
-
userId?: null | string;
|
|
1973
|
-
};
|
|
1974
2790
|
interface StorageContextIn {
|
|
1975
|
-
auth?:
|
|
2791
|
+
auth?: AuthLike;
|
|
1976
2792
|
storage?: unknown;
|
|
1977
2793
|
}
|
|
1978
2794
|
declare const storageRules: <Context extends StorageContextIn = StorageContextIn>(rules: ReadonlyArray<StorageRule<Context>>, options?: StorageRulesOptions) => Middleware<Context, Context>;
|
|
1979
2795
|
declare const VERSION = "0.0.0";
|
|
1980
|
-
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 DurableObjectJurisdiction, 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
|
|
2796
|
+
export { type ActionBuilder, type ActionCtx, type AggregateIndexDefinition, type AggregateIndexOptions, type AggregateOp, type ArgsValidator, type Component, type ComponentFunctions, type CreateOptions, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, type DataModelInit, type DefineComponentOptions, type DefineIdentityOptions, type DefineListArgsConfig, type DefinePluginOptions, type DefinePolicyInput, type DefinePresenceOptions, type DefineStorageRuleInput, type DurableObjectJurisdiction, type DurableStreamOptions, 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 IndexFieldsByTable, type InferArgs, type InferEnv, type InferIdentity, type InlineAggregateIndexOptions, type InlineRankIndexOptions, type InternalActionBuilder, type InternalMutationBuilder, type InternalQueryBuilder, type LifecycleEvent, type LifecycleHandler, type ListArgsSpec, type ListArgsValidators, type ListArgsValue, type ListFilterOperators, type ListOrderByEntry, type ListWhere, type LunoraBuilders, LunoraEnvError, LunoraError, type LunoraHttpApp, type LunoraHttpEnv, type LunoraRouteHandler, type ManyRelation, type MaskColumns, type MaskContext, type MaskFn, type MaskOptions, type MaskPolicies, type MaskRegistry, type MaskStrategy, type Middleware, type MiddlewareNext, type MigrationCtx, type MigrationDefinition, type MigrationDocument, type MigrationReader, 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 ReactorHandler, type ReactorOutcome, type ReactorSelect, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMigration, type RegisteredMutation, type RegisteredMutator, type RegisteredQuery, type RegisteredReactor, type RegisteredShape, type RegisteredStream, type RelationBuilder, type RelationDefinition, type RlsOptions, type RlsReadRegistry, type Role, type Schema, type SchemaExtension, type ShapeDefinition, type ShapeReadWhereRequest, type ShardInitEvent, type ShardInitHandler, 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, allowAll, asBucketStorage, bindOrm, bindTableFacade, buildMaskRegistry, buildRlsReadRegistry, clampLimit, composePluginMiddleware, composeShapeReadWhere, createPolicyDsl, createSecrets, defineAggregateIndex, defineComponent, defineEnv, defineIdentity, defineListArgs, defineMigration, defineMutator, definePermission, definePlugin, definePolicies, definePolicy, definePresence, defineRankIndex, defineRole, defineSchema, defineSchemaExtension, defineShape, defineStorageRule, defineStorageRules, defineTable, defineVectorIndex, deny, httpAction, httpRoute, httpRouter, indexFieldsFromSchema, initLunora, installPlugins, isDeny, isSafeHeaderValue, mask, mergeSchemaExtension, onConnect, onDisconnect, onQueryChange, onShardInit, presenceExtension, protectPublic, redactSecrets, rls, serveStorageObject, storageRules, toWhereInput };
|