@fitzzero/quickdraw-core 5.0.0-rc.4 → 5.0.0-rc.6
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/README.md +213 -55
- package/dist/{admin-ByHvKBth.d.ts → admin-CsXR7Zwy.d.ts} +2 -2
- package/dist/{adminSchemas-C1U6_voe.d.ts → adminSchemas-Dufu8r7a.d.ts} +1 -1
- package/dist/{auth-Cc_XKBKj.d.ts → auth-CMyRkfjZ.d.ts} +33 -4
- package/dist/{caller-B-7Ib98g.d.ts → caller-N1zmIB8c.d.ts} +26 -4
- package/dist/{chunk-Y5YAG75H.js → chunk-4HOXH4V3.js} +2 -2
- package/dist/{chunk-Y5YAG75H.js.map → chunk-4HOXH4V3.js.map} +1 -1
- package/dist/chunk-6FFNAT7S.js +702 -0
- package/dist/chunk-6FFNAT7S.js.map +1 -0
- package/dist/{chunk-QMCIKLWC.js → chunk-FMEHXFPL.js} +1 -1
- package/dist/chunk-FMEHXFPL.js.map +1 -0
- package/dist/{chunk-IJTZQX6Z.js → chunk-IA2B426W.js} +161 -108
- package/dist/chunk-IA2B426W.js.map +1 -0
- package/dist/{chunk-7EWHLDGN.js → chunk-MGSMCYRN.js} +289 -82
- package/dist/chunk-MGSMCYRN.js.map +1 -0
- package/dist/{chunk-TDK6S6V7.js → chunk-NKVWV3FL.js} +1 -1
- package/dist/chunk-NKVWV3FL.js.map +1 -0
- package/dist/{chunk-RDCIZR4I.js → chunk-O5I72GNW.js} +3 -3
- package/dist/{chunk-4PK6O7VL.js → chunk-PDPV254L.js} +162 -41
- package/dist/chunk-PDPV254L.js.map +1 -0
- package/dist/{chunk-OZ7DJUJY.js → chunk-RLEHORXX.js} +1 -1
- package/dist/chunk-RLEHORXX.js.map +1 -0
- package/dist/{chunk-B4IOFSM6.js → chunk-X2YCUIEK.js} +16 -5
- package/dist/chunk-X2YCUIEK.js.map +1 -0
- package/dist/{chunk-7MPXURIO.js → chunk-ZU6L7PHY.js} +241 -50
- package/dist/chunk-ZU6L7PHY.js.map +1 -0
- package/dist/cli/quickdraw-docs.js +29 -7
- package/dist/cli/quickdraw-docs.js.map +1 -1
- package/dist/client/index.d.ts +59 -8
- package/dist/client/index.js +117 -52
- package/dist/client/index.js.map +1 -1
- package/dist/{clientTypes-CKqjWKdP.d.ts → clientTypes-CBIEo39b.d.ts} +120 -10
- package/dist/{connection-GcBHwKgF.d.ts → connection-DuN8GvcD.d.ts} +9 -2
- package/dist/{createServer-6ZLgE-Vs.d.ts → createServer-BxcXQlk8.d.ts} +6 -6
- package/dist/{createTestApp-BTFzRxV8.d.ts → createTestApp-CY-RpwCu.d.ts} +6 -6
- package/dist/{envelope-B6TUGKsk.d.ts → envelope-nf5eGB-a.d.ts} +17 -4
- package/dist/{http-GTJIh3Hm.d.ts → http-DywMt0Yf.d.ts} +12 -12
- package/dist/index.d.ts +6 -6
- package/dist/index.js +18 -5
- package/dist/index.js.map +1 -1
- package/dist/{infer-Zs7dAWlI.d.ts → infer-Dw21kmLw.d.ts} +18 -5
- package/dist/{metrics-C77VTXEa.d.ts → metrics-C0WvB7-I.d.ts} +2 -2
- package/dist/prisma/index.d.ts +4 -4
- package/dist/prisma/index.js +73 -5
- package/dist/prisma/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +81 -22
- package/dist/server/auth/index.js +151 -14
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/index.d.ts +49 -17
- package/dist/server/index.js +87 -21
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/index.d.ts +7 -7
- package/dist/server/mcp/index.js +2 -2
- package/dist/server/otel.d.ts +3 -3
- package/dist/{sharing-F3blmuVL.d.ts → sharing-BSTPqvgD.d.ts} +3 -3
- package/dist/testing/client.d.ts +13 -13
- package/dist/testing/client.js +5 -5
- package/dist/testing/index.d.ts +11 -11
- package/dist/testing/index.js +6 -6
- package/dist/testing/mock.d.ts +11 -6
- package/dist/testing/mock.js +4 -4
- package/dist/{types-BBU3rSbi.d.ts → types-Bwg3kIEc.d.ts} +1 -1
- package/dist/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/dist/chunk-4PK6O7VL.js.map +0 -1
- package/dist/chunk-7EWHLDGN.js.map +0 -1
- package/dist/chunk-7MPXURIO.js.map +0 -1
- package/dist/chunk-B4IOFSM6.js.map +0 -1
- package/dist/chunk-DMUGWMNJ.js +0 -325
- package/dist/chunk-DMUGWMNJ.js.map +0 -1
- package/dist/chunk-IJTZQX6Z.js.map +0 -1
- package/dist/chunk-OZ7DJUJY.js.map +0 -1
- package/dist/chunk-QMCIKLWC.js.map +0 -1
- package/dist/chunk-TDK6S6V7.js.map +0 -1
- /package/dist/{chunk-RDCIZR4I.js.map → chunk-O5I72GNW.js.map} +0 -0
package/README.md
CHANGED
|
@@ -531,7 +531,9 @@ export type Card = ItemOf<typeof taskContract, "board">;
|
|
|
531
531
|
|
|
532
532
|
`InputOf`, `ParsedInputOf`, `OutputOf`, `EntityOf`, `ProjectionOf`,
|
|
533
533
|
`ItemOf`, `ScopeOf`, `IndexRowOf`, `ViewName` and the rest are exported from
|
|
534
|
-
the package root.
|
|
534
|
+
the package root. `FullProjectionOf<C, P>` is a projection's full row (what
|
|
535
|
+
a handler builds, every tiered field present), and `ReceivedRow<C, Row>`
|
|
536
|
+
makes a row's tiered fields optional, as a reader receives it.
|
|
535
537
|
|
|
536
538
|
## Services
|
|
537
539
|
|
|
@@ -587,9 +589,21 @@ export const taskService = qd.defineService(task, {
|
|
|
587
589
|
- Every call runs a pipeline: look up, concurrency (16 queries in flight per
|
|
588
590
|
socket and 64 queued, then `RATE_LIMITED`; mutations are not queued behind
|
|
589
591
|
queries), input validation, access, "not modified", sharing, the handler
|
|
590
|
-
under a time limit (30 s by default),
|
|
591
|
-
production, the reply, the flush, and
|
|
592
|
-
A mutation ignores the caller's cancel:
|
|
592
|
+
under a time limit (30 s by default), the result shaped to its output
|
|
593
|
+
(below), output validation outside production, the reply, the flush, and
|
|
594
|
+
one completion record (`onCall`). A mutation ignores the caller's cancel:
|
|
595
|
+
only its time limit stops it.
|
|
596
|
+
- A method's output is sent as it declares it, on every transport and in
|
|
597
|
+
production too, so a handler may return the whole row: a projection
|
|
598
|
+
output (`"entity"`, a named projection) sends the projection's keys,
|
|
599
|
+
stripped per caller (below), and an output schema of the method's own
|
|
600
|
+
sends what its JSON Schema declares (Zod 4.2 or later). An object keeps
|
|
601
|
+
the keys its `properties` declare, and every key only where
|
|
602
|
+
`additionalProperties` allows them (`z.looseObject`, `.catchall`) or it is
|
|
603
|
+
a record; an array keeps its items, each reduced by its item schema; a
|
|
604
|
+
union keeps what any branch declares; a value the schema allows to be
|
|
605
|
+
anything (`z.unknown()`, a `Date`) goes as it is. An output schema
|
|
606
|
+
without JSON Schema (Zod 3) is sent as the handler returns it.
|
|
593
607
|
|
|
594
608
|
Errors are `QuickdrawError(code, message, data?)`. Anything else a handler
|
|
595
609
|
throws reaches the caller as `INTERNAL` with a generic message, and is
|
|
@@ -753,6 +767,16 @@ tracked client as `db`; the server finds the rest on it.
|
|
|
753
767
|
recorded with its row ids, merged per row, and handed to the flush sinks
|
|
754
768
|
once the response has been sent, with one revision per flush. A handler
|
|
755
769
|
may return `db.task.update(...)` without awaiting it.
|
|
770
|
+
- A write that certainly changed nothing is not recorded, so it sends no
|
|
771
|
+
frame, no collection delta, no topic change and no `refreshEntry`: an
|
|
772
|
+
`updateMany`, `updateManyAndReturn` or `deleteMany` that matched no row,
|
|
773
|
+
and a `data` (or an upsert's `update`) with nothing to write, `{}`. An
|
|
774
|
+
upsert with `update: {}` ("make sure this row exists") reads the row
|
|
775
|
+
first in its place: one statement when the row is there, and the upsert
|
|
776
|
+
after it when it is not. Every other write is recorded, one that sets a
|
|
777
|
+
column to the value it already held too: the values the framework reads
|
|
778
|
+
before a write cannot prove it changed nothing, since another write may
|
|
779
|
+
land between that read and the write.
|
|
756
780
|
- Writes inside `db.$transaction` join the unit only when it commits; a
|
|
757
781
|
rollback drops them. Prefer the interactive form
|
|
758
782
|
(`db.$transaction(async (tx) => ...)`): an array-form
|
|
@@ -879,10 +903,26 @@ export const taskService = qd.defineService(task, {
|
|
|
879
903
|
- A projection's keys come from its schema's JSON Schema (Zod 4.2 or later),
|
|
880
904
|
or from `project: { <name>: { keys } }`; a service whose projection has
|
|
881
905
|
neither fails when it is defined. A handler returning a projection returns
|
|
882
|
-
rows (a `Date` is fine where the wire has a string,
|
|
883
|
-
|
|
906
|
+
rows (a `Date` is fine where the wire has a string, a Prisma `Json` column
|
|
907
|
+
(`JsonColumnValue`) where it has an object, an array or a record, and
|
|
908
|
+
extra columns are dropped); with `map`, it returns what `map` takes.
|
|
884
909
|
- Fields the contract's `fields` map puts above the caller's level on a row
|
|
885
|
-
are stripped from that caller's copy, after any shared run.
|
|
910
|
+
are stripped from that caller's copy, after any shared run. Only a
|
|
911
|
+
projection output is stripped per caller (`"entity"`, a named projection,
|
|
912
|
+
and `nullable(...)` or `listOf(...)` of one): a method whose output is a
|
|
913
|
+
schema of its own sends what that schema declares to every caller its
|
|
914
|
+
access admits, so a tiered key the schema declares, at any depth (`email`
|
|
915
|
+
in a hand-written `{ id, name, email }`, or in `{ user: { id, email } }`),
|
|
916
|
+
reaches callers below its level. When a server is made, each such method
|
|
917
|
+
raises the development warning `[quickdraw:tiered-field-in-output]`
|
|
918
|
+
(thrown by `createTestApp({ strictWarnings: true })`, so the test app
|
|
919
|
+
fails to start): answer `"entity"` or a projection, or drop the key from
|
|
920
|
+
the schema. Not warned: a method whose access admits no caller below the
|
|
921
|
+
field's level (`{ service: "Admin" }` with the Admin bypass on,
|
|
922
|
+
`{ entry: L }` with `L` at the field's level or above), and a kit's
|
|
923
|
+
methods, which strip their own replies. An output without JSON Schema
|
|
924
|
+
(Zod 3) cannot be reduced, so in development each tiered key its replies
|
|
925
|
+
carry raises the warning when a reply first carries it.
|
|
886
926
|
- `affects` names rows of other services a write changes too
|
|
887
927
|
(`{ service, id: column }`, or `{ service, id: (row) => ids, columns }`);
|
|
888
928
|
they are sent again after the flush, one hop.
|
|
@@ -1009,7 +1049,14 @@ export const taskService = qd.defineService(task, {
|
|
|
1009
1049
|
authorized like a subscribe to that scope, or `service`, which changes
|
|
1010
1050
|
whenever any row of the service does, or a row of a model it lists in
|
|
1011
1051
|
`writes` (a game's high scores, which no service owns). A query declares
|
|
1012
|
-
`watch: "service"` to be invalidated by it.
|
|
1052
|
+
`watch: "service"` to be invalidated by it. A query over some of those
|
|
1053
|
+
models names them, `watch: { service: ["gameScore"] }` (type
|
|
1054
|
+
`ServiceModelsWatch`; the service's `model` and models in its `writes`;
|
|
1055
|
+
`defineService` refuses others), and
|
|
1056
|
+
is invalidated only after a flush that wrote one of them: high scores are
|
|
1057
|
+
not read again when a chat membership the same service writes changes.
|
|
1058
|
+
It is the same topic: its `qd:changed` frame names the models that
|
|
1059
|
+
changed it (`models`). The service topic is closed
|
|
1013
1060
|
(`FORBIDDEN`) unless the service declares `watchAccess` (`"public"`,
|
|
1014
1061
|
`"authenticated"` or `{ service: level }`). A watcher that loses access
|
|
1015
1062
|
leaves the topic after one last `qd:changed`.
|
|
@@ -1041,7 +1088,9 @@ over three transports (design: sections 3, 8 and 10):
|
|
|
1041
1088
|
principal comes from the session cookie (`__Host-session` over HTTPS,
|
|
1042
1089
|
`session` over plain HTTP or with `COOKIE_DOMAIN`, as the auth routes name
|
|
1043
1090
|
it) or an `Authorization: Bearer` token through the same
|
|
1044
|
-
`authenticate
|
|
1091
|
+
`authenticate`, which is told which one it was (`request.credential`):
|
|
1092
|
+
`socketAuth` answers a cookie sent from a page outside `allowedOrigins`
|
|
1093
|
+
with `FORBIDDEN`, as it refuses such a socket; the reply is `{ ok: true, d }` or
|
|
1045
1094
|
`{ ok: false, e: { code, message, data? } }` with the code's HTTP status.
|
|
1046
1095
|
Works on Express 4 and 5, and on a bare Node server. Move it with
|
|
1047
1096
|
`http: { path }`, turn it off with `http: false`, or mount
|
|
@@ -1211,6 +1260,19 @@ export function TaskDetail({ id }: { readonly id: string }) {
|
|
|
1211
1260
|
}
|
|
1212
1261
|
```
|
|
1213
1262
|
|
|
1263
|
+
- A row that does not exist is refused the way a row the reader may not
|
|
1264
|
+
see is: `FORBIDDEN`, for a subscribe and for a method whose access names
|
|
1265
|
+
the row (`{ entry }`), since a policy that cannot find the row grants
|
|
1266
|
+
nothing (fail closed), and telling the two apart would tell a stranger
|
|
1267
|
+
which ids exist (a service-wide `Admin` grant, which needs no row to
|
|
1268
|
+
pass, gets `NOT_FOUND`). So a page cannot tell "deleted" from "no access" by the
|
|
1269
|
+
refusal: it learns of a delete while it holds the row, live: the `r`
|
|
1270
|
+
frame sets `useEntity`'s `isRemoved` (as above), and a collection's
|
|
1271
|
+
`removed` delta takes the item out of `items`. A page opened after the
|
|
1272
|
+
delete (a link, a reload, a reconnect after the row went) gets only
|
|
1273
|
+
`FORBIDDEN`; where the difference matters, say "not found or not shared
|
|
1274
|
+
with you", or ask a method of the parent row (a list of the project's
|
|
1275
|
+
tasks) that can answer without leaking.
|
|
1214
1276
|
- A field the contract's `fields` map tiers (`notes` here) is optional in
|
|
1215
1277
|
every row type a reader gets (`useEntity`, collection items, `"entity"`
|
|
1216
1278
|
outputs, `EntityOf`, `ItemOf`), because a reader below its level receives
|
|
@@ -1243,7 +1305,13 @@ export function TaskDetail({ id }: { readonly id: string }) {
|
|
|
1243
1305
|
items loaded, and `loadMore`/`loadItems`; `view` filters the members by a
|
|
1244
1306
|
view of the contract, for the user the server's hello names; `load: "all"`
|
|
1245
1307
|
keeps every page loaded. A `null` scope holds nothing; `enabled: false`
|
|
1246
|
-
subscribes to nothing.
|
|
1308
|
+
subscribes to nothing. A scope the server refused shows its `error`
|
|
1309
|
+
(`FORBIDDEN`) and is loaded once more each time the user's access may
|
|
1310
|
+
have changed: on a connect, on new service grants (`qd:access`), and when
|
|
1311
|
+
another scope the client holds gets an `added` delta for the scope's
|
|
1312
|
+
anchor row (an invite adds the chat to the user's own list of chats, and
|
|
1313
|
+
the chat's messages open). A row-level grant with neither sends nothing
|
|
1314
|
+
the client could notice: remount the view, or call `refresh()`.
|
|
1247
1315
|
- The cache follows the user: a hello naming another user removes everything
|
|
1248
1316
|
quickdraw cached; new credentials for the same user refetch it; new grants
|
|
1249
1317
|
(`qd:access`) refetch every query.
|
|
@@ -1260,7 +1328,26 @@ A create shows its row before the server answers with `cache.addItem(collection,
|
|
|
1260
1328
|
scope, item)`: the item appears in the scope at once, in its place by the
|
|
1261
1329
|
collection's `order` (give it the order's fields; without them it goes
|
|
1262
1330
|
last), and `useCollection`'s `pending` names it while the call is in flight.
|
|
1263
|
-
A refused call removes it
|
|
1331
|
+
A refused call removes it, unless it was added with `{ onRefused: "keep" }`
|
|
1332
|
+
(`cache.addItem(collection, scope, item, { onRefused: "keep" })`, or the
|
|
1333
|
+
same for `addEntity`): it then leaves `items` for `useCollection`'s
|
|
1334
|
+
`refused`, each `{ item, error, dismiss(), retry() }`, in the render that
|
|
1335
|
+
shows the mutation's error, until the app dismisses it or sends the same
|
|
1336
|
+
call again with `retry()` (through the mutation hook that sent it, so its
|
|
1337
|
+
`isPending` and callbacks follow; it shows the item `pending` again, and
|
|
1338
|
+
never rejects: a second refusal shows in `refused` again), so a chat shows
|
|
1339
|
+
a failed message with "retry" without a copy of its own. A call whose outcome is unknown is not refused: when the
|
|
1340
|
+
connection drops after the call was sent, or it times out, the server may
|
|
1341
|
+
have made the write (`isUnknownOutcome(error)` on `./client` says so). Its
|
|
1342
|
+
items stay, `pending`, and `useCollection`'s `checking` names them until
|
|
1343
|
+
the scope's next load (the reconnect's resume, or a load asked for at once
|
|
1344
|
+
while the socket is up): a load that holds an item's id ends it, its own
|
|
1345
|
+
copy shown, and one sent after the failure that answers without it refuses
|
|
1346
|
+
it (into `refused` with `onRefused: "keep"`). Only an id the client made,
|
|
1347
|
+
which the server keeps, can be found that way, and only with one is
|
|
1348
|
+
`retry()` safe after an unknown outcome: the second call fails `CONFLICT`
|
|
1349
|
+
instead of writing a second row. A refused item ends too once its scope
|
|
1350
|
+
holds its id. The reply's `id` (the created row's) and values
|
|
1264
1351
|
replace the item's own, and once the scope's own copy arrives (its `added`
|
|
1265
1352
|
delta, or a load) that copy shows in its place: never both, never a gap.
|
|
1266
1353
|
`cache.addEntity(row)` adds a row to every collection of entity rows whose
|
|
@@ -1397,8 +1484,10 @@ The methods most services write by hand, as one-line opt-ins (design:
|
|
|
1397
1484
|
section 12). Each kit's contract half comes from the package root and makes
|
|
1398
1485
|
ordinary contract entries; its handlers come from `./server`. Lint's
|
|
1399
1486
|
`prefer-kit` reports a method written by hand that a kit implements (`get`,
|
|
1400
|
-
`list`, `create`, `getTask`, ...) in a service that uses no kit
|
|
1401
|
-
|
|
1487
|
+
`list`, `create`, `getTask`, ...) in a service that uses no kit, and one
|
|
1488
|
+
that duplicates what a kit spread beside it serves (a `getNote` beside
|
|
1489
|
+
`crud.handlers(note, { access: { get } })`); one that must stay
|
|
1490
|
+
hand-written says why in a `// quickdraw: hand-written because ...`
|
|
1402
1491
|
comment above it.
|
|
1403
1492
|
|
|
1404
1493
|
### Read/write kit
|
|
@@ -1943,9 +2032,15 @@ before?, after }, ctx, db)` after each `adminCreate`, `adminUpdate` and
|
|
|
1943
2032
|
transaction's tracked client; a throw undoes the write and fails the
|
|
1944
2033
|
call): what an admin edit must set off, such as a game reloading its
|
|
1945
2034
|
tunables, without wrapping the kit's handlers. The rows are the entity,
|
|
1946
|
-
every field.
|
|
1947
|
-
|
|
1948
|
-
|
|
2035
|
+
every field. `onCommitted({ method, id, before?, after }, ctx)` runs once
|
|
2036
|
+
the write has committed (after `onWrite`'s transaction), in a detached
|
|
2037
|
+
unit of work of its own: the reply does not wait for it, its writes flush
|
|
2038
|
+
on their own, and a throw is logged, the write standing. Apply an edit to
|
|
2039
|
+
what runs in memory (a game's simulation) there, so an edit that rolled
|
|
2040
|
+
back is never applied. Without either hook the kit opens no transaction.
|
|
2041
|
+
Each handler `admin.handlers` returns resolves with its method's output
|
|
2042
|
+
type (`AdminOutputOf<C, M>`), so a wrapper reads the row and returns it
|
|
2043
|
+
with no cast.
|
|
1949
2044
|
- `adminSubscribers({ id })` counts the sockets subscribed to a row per
|
|
1950
2045
|
access level (`{ id, count, levels, complete }`; behind a Redis adapter
|
|
1951
2046
|
the counts are this server's and `complete` is `false`), and
|
|
@@ -2111,8 +2206,8 @@ export function TaskRoom({
|
|
|
2111
2206
|
leaves the feed and gets
|
|
2112
2207
|
`qd:revoked { kind: "stream", reason: "access", s, stream, scope? }`, and
|
|
2113
2208
|
`useStream` shows `FORBIDDEN` until the next connect.
|
|
2114
|
-
- A service's `streams: { <name>: { seed, validate } }`
|
|
2115
|
-
seed per subscriber, `seed: (scope, ctx) => items` (the current state,
|
|
2209
|
+
- A service's `streams: { <name>: { seed, validate } }` (type
|
|
2210
|
+
`StreamImplementation`) computes a stream's seed per subscriber, `seed: (scope, ctx) => items` (the current state,
|
|
2116
2211
|
where the items that follow are deltas; on whichever node the subscriber
|
|
2117
2212
|
is on, under its principal, in the tick it joins the feed; a contract
|
|
2118
2213
|
`seed: n` and a seed function cannot both be declared), and
|
|
@@ -2133,7 +2228,8 @@ export function TaskRoom({
|
|
|
2133
2228
|
limiter does not count channels. `{ room: { prefix: "world:" } }` takes
|
|
2134
2229
|
a socket in any app room whose name starts with the prefix (a game of many
|
|
2135
2230
|
worlds), and every room form gives the handler the room it matched as
|
|
2136
|
-
`ctx.room
|
|
2231
|
+
`ctx.room` (typed `ChannelRoomOf<C, Name>`: `string` for a channel that
|
|
2232
|
+
requires a room, else `undefined`), so the payload need not repeat it. Every requirement is the
|
|
2137
2233
|
sending socket's own: a room another socket of the user joined does not
|
|
2138
2234
|
count, a reconnected socket must join again, and behind a cluster the
|
|
2139
2235
|
check runs on the node the socket is connected to, with no round trip.
|
|
@@ -2158,8 +2254,10 @@ input, { enabled?, onJoined? })` (from `./client`) runs the joining call
|
|
|
2158
2254
|
(`enterBoard` above) on every `qd:hello` and when its input changes by
|
|
2159
2255
|
value, never on a re-render, and shows `status` (`idle` with no socket to
|
|
2160
2256
|
join with, `joining`, `joined`, `error`), `isJoined`, `data` and `error`;
|
|
2161
|
-
a refusal stands until the next hello
|
|
2162
|
-
|
|
2257
|
+
a refusal stands until the next hello or `retry()` (which runs the call
|
|
2258
|
+
again at once on the current socket, and does nothing while there is
|
|
2259
|
+
none or `enabled` is false), `RATE_LIMITED` is tried again after its
|
|
2260
|
+
backoff, and it never leaves the room itself.
|
|
2163
2261
|
`connection.onHello(listener)` is the same hook without React.
|
|
2164
2262
|
|
|
2165
2263
|
Code that is not a handler (a game loop, a timer, a job) reaches rooms
|
|
@@ -2212,8 +2310,9 @@ export function hasAudience(projectId: string): boolean {
|
|
|
2212
2310
|
the node that held the socket: its own `leave` (`reason: "leave"`), a
|
|
2213
2311
|
removal (`"removed"`), or a disconnect, which leaves every app room it was
|
|
2214
2312
|
in (`"disconnect"`; `qd:rotate` reconnects with a new socket). Each room
|
|
2215
|
-
comes with `last`: true when no socket of that user is in
|
|
2216
|
-
more, on any node, which is what a game's `playerLeft` waits
|
|
2313
|
+
(a `RoomLeft`) comes with `last`: true when no socket of that user is in
|
|
2314
|
+
the room any more, on any node, which is what a game's `playerLeft` waits
|
|
2315
|
+
for. It runs
|
|
2217
2316
|
in a unit of work of its own (never the unit of the handler that left), a
|
|
2218
2317
|
throw is logged, and `close()` waits for it. Behind a cluster `last` is
|
|
2219
2318
|
decided by asking every node once the socket left, so two sockets of one
|
|
@@ -2248,8 +2347,8 @@ const allowedOrigins = [env.CLIENT_URL];
|
|
|
2248
2347
|
const sessions = createMemorySessionStore();
|
|
2249
2348
|
|
|
2250
2349
|
export const app: Express = express();
|
|
2251
|
-
// behind a proxy, so the rate limits see the client's IP
|
|
2252
|
-
app.set("trust proxy",
|
|
2350
|
+
// behind a proxy, so the rate limits see the client's IP; with none, a client would pick its own
|
|
2351
|
+
app.set("trust proxy", env.TRUST_PROXY);
|
|
2253
2352
|
// a web app on another origin also needs CORS with credentials on these routes
|
|
2254
2353
|
app.use(
|
|
2255
2354
|
createAuthRoutes({
|
|
@@ -2305,6 +2404,7 @@ nothing is cached:
|
|
|
2305
2404
|
| `GET /{provider}/start?returnTo=<url>` | 302 to the provider |
|
|
2306
2405
|
| `GET /{provider}/callback` | 302 to `{origin}{successPath}` with the session cookie, or to `{origin}{errorPath}?error=state`, `denied` or `failed` |
|
|
2307
2406
|
| `POST /guest` | `createUser(body)`, then `{ userId, name? }` with the session cookie (and `token` with `guest({ token: true })`) |
|
|
2407
|
+
| `GET /providers` | `{ providers: [{ id, name, kind }] }`: the sign-ins served now (`routes.providers()` on the server) |
|
|
2308
2408
|
| `GET /me` | `{ userId }`, or 401 |
|
|
2309
2409
|
| `POST /logout` | 204: revokes the session, clears the cookie |
|
|
2310
2410
|
| `POST /logout-all` | 204: revokes every session of the user; 401 without a live session |
|
|
@@ -2351,8 +2451,17 @@ nothing is cached:
|
|
|
2351
2451
|
any page's handshake). A handshake without `Origin` is refused unless it
|
|
2352
2452
|
is a browser's same-origin request (`Sec-Fetch-Site: same-origin`) or
|
|
2353
2453
|
`allowMissingOrigin: true` is set for native clients that keep cookies.
|
|
2354
|
-
|
|
2355
|
-
|
|
2454
|
+
An HTTP call (`/qd/...`) that sends the cookie is checked the same way
|
|
2455
|
+
and answered `FORBIDDEN` (403) from an `Origin` outside `allowedOrigins`,
|
|
2456
|
+
besides needing its JSON content type. One without `Origin` is accepted:
|
|
2457
|
+
a browser sends `Origin` with every POST, so it comes from curl or a
|
|
2458
|
+
server rendering a page with the user's forwarded cookie (refused only
|
|
2459
|
+
when `Sec-Fetch-Site` names another site). Bearer tokens need no Origin
|
|
2460
|
+
on either transport. An app's own `authenticate` learns where an HTTP
|
|
2461
|
+
call's token came from in `request.credential` (`"cookie"` or
|
|
2462
|
+
`"bearer"`, type `HttpCredentialSource`), applies the same rule with
|
|
2463
|
+
`cookieOriginAllowed(request, allowedOrigins)`, and a
|
|
2464
|
+
`QuickdrawError("FORBIDDEN")` it throws on HTTP is answered as it is.
|
|
2356
2465
|
- `socketAuth({ devCredentials })` signs a socket in by the user id its
|
|
2357
2466
|
handshake names (`auth: { userId }`, no token), as the function answers
|
|
2358
2467
|
(the principal, or `null` to refuse): for a game editor or load-test bots
|
|
@@ -2369,11 +2478,17 @@ nothing is cached:
|
|
|
2369
2478
|
session's token, for clients that keep no cookies (a game engine, a page
|
|
2370
2479
|
in a third-party iframe), at the cost of the token being readable by the
|
|
2371
2480
|
page's scripts.
|
|
2372
|
-
- `requireSession({ sessions, jwtSecret }, { loadPrincipal? })`
|
|
2373
|
-
app's own REST routes (below): the credential is read as `/me`
|
|
2374
|
-
the JWT verified once and the session checked in the store;
|
|
2375
|
-
401 `{ error: "UNAUTHENTICATED", message }`.
|
|
2376
|
-
|
|
2481
|
+
- `requireSession({ sessions, jwtSecret }, { loadPrincipal?, allowedOrigins? })`
|
|
2482
|
+
guards the app's own REST routes (below): the credential is read as `/me`
|
|
2483
|
+
reads it, the JWT verified once and the session checked in the store;
|
|
2484
|
+
otherwise 401 `{ error: "UNAUTHENTICATED", message }`. The session cookie
|
|
2485
|
+
gets the `/qd` calls' Origin rule: from an `Origin` outside
|
|
2486
|
+
`allowedOrigins` (by default the list of the `createAuthRoutes` writing to
|
|
2487
|
+
the same `sessions`) it answers 403 `{ error: "FORBIDDEN", message }`, so
|
|
2488
|
+
another site's form cannot post to a route as the user; a request without
|
|
2489
|
+
`Origin` is accepted unless `Sec-Fetch-Site` names another site, and a
|
|
2490
|
+
bearer token needs none. 4.1's `createRequireAuth` stays for token-keyed
|
|
2491
|
+
sessions.
|
|
2377
2492
|
- Rate limits: the sign-in routes share `createAuthLimiter({ max: 60 })` (60
|
|
2378
2493
|
requests per 15 minutes per IP) and the session routes
|
|
2379
2494
|
`createAuthStatusLimiter()` (120); pass `rateLimit: { signIn, session }`
|
|
@@ -2387,7 +2502,14 @@ nothing is cached:
|
|
|
2387
2502
|
- The mock provider is mounted only while `isMockOAuthEnabled()`
|
|
2388
2503
|
(`ENABLE_MOCK_OAUTH=true` and `NODE_ENV` other than `production`), and
|
|
2389
2504
|
every request checks again. Set `mock({ internalUrl })` where the API
|
|
2390
|
-
cannot reach itself at `publicUrl`.
|
|
2505
|
+
cannot reach itself at `publicUrl`. Routes made with nothing that can
|
|
2506
|
+
sign anyone in (only a mock that is off) warn when they are made.
|
|
2507
|
+
- `publicUrl` is where providers send the browser back, so a deployment
|
|
2508
|
+
must set it to the API's public URL. A loopback `publicUrl`
|
|
2509
|
+
(`http://localhost:4000`, the usual fallback for an unset `API_URL`)
|
|
2510
|
+
warns when the routes are made if every allowed origin is a page on
|
|
2511
|
+
another machine, and the first request that arrives for another host
|
|
2512
|
+
(`X-Forwarded-Host`, else `Host`) logs an error naming it, once.
|
|
2391
2513
|
- One rule names the session cookie, written and read: the routes,
|
|
2392
2514
|
`setSessionCookie`, `socketAuth` and the HTTP transport give a request
|
|
2393
2515
|
the same name, and read first the name they would set on it. Without a
|
|
@@ -2418,10 +2540,13 @@ nothing is cached:
|
|
|
2418
2540
|
session for an app's own sign-in flow (login codes, an embedded activity),
|
|
2419
2541
|
and `liveSession` reads a token back; both work with `socketAuth`.
|
|
2420
2542
|
|
|
2421
|
-
From the browser, `./client` speaks to these routes: `
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2543
|
+
From the browser, `./client` speaks to these routes: `authProviders({
|
|
2544
|
+
apiUrl })` lists the sign-ins the API serves (`GET /providers`: a provider
|
|
2545
|
+
`google.optional` built nothing for is not in it, nor the mock where it is
|
|
2546
|
+
off), so a login page shows only those, whatever the web app was built
|
|
2547
|
+
with; `signInUrl(provider, { apiUrl, returnTo })` is the start route's URL
|
|
2548
|
+
(to navigate to; `returnTo` defaults to the current page's origin), and
|
|
2549
|
+
`signOut()` and `signOutEverywhere()` post to `logout` and `logout-all`. Each POST sends the
|
|
2425
2550
|
session cookie (`credentials: "include"`, so the API's CORS must allow the
|
|
2426
2551
|
web app's origin with credentials) with `Content-Type: application/json`,
|
|
2427
2552
|
and the token `setAuthToken` stored, if any, as a bearer token; the stored
|
|
@@ -2434,8 +2559,23 @@ signing out (with a token in `auth`, clearing it does that):
|
|
|
2434
2559
|
|
|
2435
2560
|
```tsx
|
|
2436
2561
|
export function SignIn() {
|
|
2437
|
-
//
|
|
2438
|
-
|
|
2562
|
+
// GET /auth/providers: only the sign-ins this API serves (no Google button without its keys)
|
|
2563
|
+
const { data: providers = [] } = useQuery({
|
|
2564
|
+
queryKey: ["auth", "providers"],
|
|
2565
|
+
queryFn: () => authProviders({ apiUrl: API_URL }),
|
|
2566
|
+
});
|
|
2567
|
+
return (
|
|
2568
|
+
<nav>
|
|
2569
|
+
{providers
|
|
2570
|
+
.filter((provider) => provider.kind !== "guest")
|
|
2571
|
+
.map((provider) => (
|
|
2572
|
+
// the kit's GET /auth/{id}/start: back to this page's origin with the session cookie
|
|
2573
|
+
<a key={provider.id} href={signInUrl(provider.id, { apiUrl: API_URL })}>
|
|
2574
|
+
{`Sign in with ${provider.name}`}
|
|
2575
|
+
</a>
|
|
2576
|
+
))}
|
|
2577
|
+
</nav>
|
|
2578
|
+
);
|
|
2439
2579
|
}
|
|
2440
2580
|
|
|
2441
2581
|
export function SignOut() {
|
|
@@ -2576,8 +2716,8 @@ it("sends a rename to the other members' boards", async () => {
|
|
|
2576
2716
|
real socket (`{ call, socket, hello, close }`); both are keyed by service
|
|
2577
2717
|
name. `app.frames(match?)` lists every frame the server sent, with its
|
|
2578
2718
|
socket and user; `frames.waitFor(match)` waits for one. A query of one
|
|
2579
|
-
event takes `where`, a predicate over its frames typed by
|
|
2580
|
-
(`{ event: "qd:presence", where: ({ data }) => data.joined === id }`), and
|
|
2719
|
+
event (`EventQuery`) takes `where`, a predicate over its frames typed by
|
|
2720
|
+
the event (`{ event: "qd:presence", where: ({ data }) => data.joined === id }`), and
|
|
2581
2721
|
`streamFrames(contract, stream, where?, scope?)` and
|
|
2582
2722
|
`eventFrames(contract, event, where?)` match one stream's items or one
|
|
2583
2723
|
event's payloads, typed by the contract, to spread beside `socketId` or
|
|
@@ -2615,8 +2755,9 @@ it("lets the owner rename, members read, and nobody else in", async () => {
|
|
|
2615
2755
|
`"deny"` (the default for everyone `allow` does not name) means
|
|
2616
2756
|
`UNAUTHENTICATED` without a principal and `FORBIDDEN` with one. Mutations run
|
|
2617
2757
|
for real, once per allowed principal: give inputs that can run again, or
|
|
2618
|
-
`input` as a function of the cell (`
|
|
2619
|
-
|
|
2758
|
+
`input` as a function of the cell (a `MatrixInputFactory` of the
|
|
2759
|
+
`MatrixCell` `{ name, principal }`, sync or async), called before each
|
|
2760
|
+
cell's call, that makes a row of its own (a task
|
|
2620
2761
|
to delete, an unused name), so no cell depends on the order of the
|
|
2621
2762
|
principals.
|
|
2622
2763
|
|
|
@@ -2689,16 +2830,17 @@ one format and names the method call it happened in:
|
|
|
2689
2830
|
[quickdraw:n-plus-one] taskService.board: task.findUnique by id ran 10 times in one call, once per item (N+1); ...
|
|
2690
2831
|
```
|
|
2691
2832
|
|
|
2692
|
-
| Kind
|
|
2693
|
-
|
|
|
2694
|
-
| `n-plus-one`
|
|
2695
|
-
| `unbounded-read`
|
|
2696
|
-
| `oversized-response`
|
|
2697
|
-
| `nested-write`
|
|
2698
|
-
| `ambient-write`
|
|
2699
|
-
| `batch-read`
|
|
2700
|
-
| `batch-create-many`
|
|
2701
|
-
| `repeated-call`
|
|
2833
|
+
| Kind | Raised when |
|
|
2834
|
+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
2835
|
+
| `n-plus-one` | a call ran 10 statements of one shape (model, operation, `where` keys), outside a `$transaction([])` |
|
|
2836
|
+
| `unbounded-read` | a call ran `findMany` with neither `take` nor ids to read (`id`, `{ in }` or `{ equals }`) |
|
|
2837
|
+
| `oversized-response` | a reply was larger than `maxResponseBytes` (default 1 MiB) |
|
|
2838
|
+
| `nested-write` | a write's `data` wrote a related row, which is not tracked |
|
|
2839
|
+
| `ambient-write` | a tracked write ran outside any unit of work |
|
|
2840
|
+
| `batch-read` | a write in an array-form `$transaction` read its rows outside the batch |
|
|
2841
|
+
| `batch-create-many` | a `createMany` in an array-form `$transaction` could not report its rows |
|
|
2842
|
+
| `repeated-call` | one connection repeated a call or was refused `RATE_LIMITED` again and again (below) |
|
|
2843
|
+
| `tiered-field-in-output` | a method's own output schema declares a field the contract tiers, which only projection outputs strip; raised when the server is made (for an output without JSON Schema, when a reply carries it) |
|
|
2702
2844
|
|
|
2703
2845
|
Updates and deletes by id inside an interactive transaction are not counted
|
|
2704
2846
|
toward `n-plus-one`: that is how per-row writes are written (see tracked
|
|
@@ -2716,7 +2858,9 @@ fails: the call it happened in fails with `INTERNAL` and the error as its
|
|
|
2716
2858
|
once the reply was recorded (over a socket or HTTP the reply was already
|
|
2717
2859
|
sent, so that error is logged, not thrown). Strictness belongs to the app:
|
|
2718
2860
|
warnings outside its calls (an ambient write while seeding, another app's
|
|
2719
|
-
calls) are logged as usual, and `app.close()` ends it.
|
|
2861
|
+
calls) are logged as usual, and `app.close()` ends it. The one raised when
|
|
2862
|
+
the app is made, `tiered-field-in-output`, is thrown from `createTestApp`
|
|
2863
|
+
itself, so a strict app over such a method does not start.
|
|
2720
2864
|
|
|
2721
2865
|
A `repeated-call` warning names a client caught in a loop (a mutation fired
|
|
2722
2866
|
from an effect that its own result runs again, a refetch that triggers
|
|
@@ -2817,6 +2961,13 @@ it("lets a signed-in user through the gate", () => {
|
|
|
2817
2961
|
|
|
2818
2962
|
In Storybook, a decorator renders every story inside `qd.$Provider`, and a
|
|
2819
2963
|
story's `beforeEach` sets its session (`qd.$session(...)`) beside its data.
|
|
2964
|
+
A docs page renders its stories side by side, where one mock session would
|
|
2965
|
+
show the last story's in all of them: give each its own with the
|
|
2966
|
+
provider's `session` prop, `<qd.$Provider session={{ userId: null }}>`
|
|
2967
|
+
(a `MockSession`, the type `$session(...)` takes too), laid over the
|
|
2968
|
+
mock's session field by field for that subtree alone (the
|
|
2969
|
+
real `useQuickdraw()` and `usePresence`, and the mock's views and admin
|
|
2970
|
+
grants, read it there).
|
|
2820
2971
|
|
|
2821
2972
|
### Test databases
|
|
2822
2973
|
|
|
@@ -2916,7 +3067,14 @@ topic (`watchAccess`) and the field levels; each method's access form, in
|
|
|
2916
3067
|
words, and its `rowless`; who may open a collection's scope; a channel's
|
|
2917
3068
|
access; a stream's computed seed and when its items are checked. A
|
|
2918
3069
|
contract the services module has no service for says so, and a service
|
|
2919
|
-
without a contract is an error. Pass the same flag to `--check`.
|
|
3070
|
+
without a contract is an error. Pass the same flag to `--check`. The
|
|
3071
|
+
services module is imported with everything it imports, as the server
|
|
3072
|
+
loads it: the workspace packages it uses (`@project/db`, `@project/shared`)
|
|
3073
|
+
load from their built output when their `package.json` points there, so
|
|
3074
|
+
build them first (`bun run build`, or `turbo run build --filter` for those
|
|
3075
|
+
packages) in a script that runs the docs on a fresh checkout; the error
|
|
3076
|
+
says so when a built file is missing. The contracts module, which imports
|
|
3077
|
+
only the shared package's own sources, needs no build.
|
|
2920
3078
|
|
|
2921
3079
|
The module may export each contract, or a map of them as given to
|
|
2922
3080
|
`createQuickdrawClient`. A TypeScript module loads through Node's type
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { e as StandardSchemaWithJSON, d as StandardSchemaV1, I as InferInput, c as InferOutput,
|
|
2
|
-
import { d as ListSort, c as ListQuery, b as ListPage, u as AdminListInput, v as AdminListQuery, y as AdminPage, r as AdminCreateInput, s as AdminCreateQuery, B as AdminUpdateInput, C as AdminUpdateQuery, w as AdminMetaInput, n as AdminServiceMeta, z as AdminSubscribers } from './adminSchemas-
|
|
1
|
+
import { e as StandardSchemaWithJSON, d as StandardSchemaV1, I as InferInput, c as InferOutput, X as RowSchema, Q as QueryDef, P as ProjectionList, p as MutationDef, J as AnyContract, ap as MethodName } from './infer-Dw21kmLw.js';
|
|
2
|
+
import { d as ListSort, c as ListQuery, b as ListPage, u as AdminListInput, v as AdminListQuery, y as AdminPage, r as AdminCreateInput, s as AdminCreateQuery, B as AdminUpdateInput, C as AdminUpdateQuery, w as AdminMetaInput, n as AdminServiceMeta, z as AdminSubscribers } from './adminSchemas-Dufu8r7a.js';
|
|
3
3
|
|
|
4
4
|
/** A schema a kit generates: validation, and its JSON Schema. */
|
|
5
5
|
type KitSchema<Input, Output = Input> = StandardSchemaWithJSON<Input, Output>;
|
|
@@ -1,7 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { IncomingMessage, IncomingHttpHeaders } from 'node:http';
|
|
2
2
|
import { Socket } from 'socket.io';
|
|
3
|
-
import { A as AccessLevel } from './infer-
|
|
4
|
-
import { P as Principal, M as MaybePromise } from './types-
|
|
3
|
+
import { A as AccessLevel } from './infer-Dw21kmLw.js';
|
|
4
|
+
import { P as Principal, M as MaybePromise } from './types-Bwg3kIEc.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A request as Express leaves it: maybe with `body` from `express.json()`,
|
|
8
|
+
* `cookies` from `cookie-parser`, and Express's own `secure`.
|
|
9
|
+
*/
|
|
10
|
+
type HttpRequest = IncomingMessage & {
|
|
11
|
+
readonly body?: unknown;
|
|
12
|
+
readonly cookies?: unknown;
|
|
13
|
+
readonly secure?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Where an HTTP call's token came from. `"cookie"`: the session cookie, which
|
|
17
|
+
* a browser sends with a request any page makes, so whoever accepts it checks
|
|
18
|
+
* the request's `Origin` (`socketAuth` does). `"bearer"`: an
|
|
19
|
+
* `Authorization: Bearer` header, which a page sends only on purpose.
|
|
20
|
+
*/
|
|
21
|
+
type HttpCredentialSource = "cookie" | "bearer";
|
|
5
22
|
|
|
6
23
|
/** A principal's service-wide grants by service name: `{ taskService: "Admin" }`. */
|
|
7
24
|
type ServiceGrants = Readonly<Record<string, AccessLevel>>;
|
|
@@ -24,6 +41,14 @@ interface SocketAuthenticateRequest extends AuthenticateRequestBase {
|
|
|
24
41
|
interface HttpAuthenticateRequest extends AuthenticateRequestBase {
|
|
25
42
|
readonly transport: "http";
|
|
26
43
|
readonly req: IncomingMessage;
|
|
44
|
+
/**
|
|
45
|
+
* Where `auth.token` came from (finding F7.1): `"cookie"`, the session
|
|
46
|
+
* cookie, which a browser sends with a request any page makes, so an
|
|
47
|
+
* `authenticate` that accepts it checks the request's `Origin` as
|
|
48
|
+
* `socketAuth` does; `"bearer"`, an `Authorization: Bearer` header, which
|
|
49
|
+
* a page sends only on purpose. Absent without a token.
|
|
50
|
+
*/
|
|
51
|
+
readonly credential?: HttpCredentialSource;
|
|
27
52
|
}
|
|
28
53
|
/** What `authenticate` is asked about: a socket connecting, or an HTTP call. */
|
|
29
54
|
type AuthenticateRequest = SocketAuthenticateRequest | HttpAuthenticateRequest;
|
|
@@ -44,6 +69,10 @@ interface ServerAuth<P extends Principal = Principal> {
|
|
|
44
69
|
* caller in anonymously, so only `"public"` methods pass. Throw a
|
|
45
70
|
* `QuickdrawError("UNAUTHENTICATED", ...)` for a refusal that is the
|
|
46
71
|
* caller's doing (it logs at debug; anything else thrown logs at error).
|
|
72
|
+
* On an HTTP call a thrown `QuickdrawError("FORBIDDEN", ...)` is answered
|
|
73
|
+
* as it is, with 403 (logged at debug): a credential the request may not
|
|
74
|
+
* use from where it came, as `socketAuth` answers a session cookie sent
|
|
75
|
+
* from a page outside `allowedOrigins`.
|
|
47
76
|
*/
|
|
48
77
|
readonly authenticate?: (request: AuthenticateRequest) => MaybePromise<AuthenticateResult<P>>;
|
|
49
78
|
/**
|
|
@@ -77,4 +106,4 @@ interface ServiceAccessSource {
|
|
|
77
106
|
*/
|
|
78
107
|
declare function recordSocketSession(socket: object, sessionId: string): void;
|
|
79
108
|
|
|
80
|
-
export { type AuthenticateResult as A, type
|
|
109
|
+
export { type AuthenticateResult as A, type HttpRequest as H, type ServerAuth as S, type ServiceGrants as a, type AuthenticateRequest as b, type HttpAuthenticateRequest as c, type HttpCredentialSource as d, type ServiceAccessSource as e, type SocketAuthenticateRequest as f, recordSocketSession as r };
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { ad as Version, a5 as Revision } from './envelope-
|
|
1
|
+
import { ad as Version, a5 as Revision } from './envelope-nf5eGB-a.js';
|
|
2
2
|
import { L as Logger } from './logger-BY28acW-.js';
|
|
3
|
-
import { A as AccessLevel,
|
|
4
|
-
import { P as Principal, Q as QuickdrawTypes, M as MaybePromise, c as PrincipalOf, a as McpContext, T as Transport, C as ContextExtensionOf, b as McpContextOf, D as DbOf } from './types-
|
|
3
|
+
import { A as AccessLevel, J as AnyContract, y as CollectionWhere, O as OrderBy, k as MethodKind, d as StandardSchemaV1, ag as EventName, ah as EventPayloadOf, aa as ChannelName, ab as ChannelPayloadOf, az as StreamName, am as IsScopedStream, ay as StreamItemOf, ae as ContractMap, ap as MethodName, al as InputOf, ar as OutputOf } from './infer-Dw21kmLw.js';
|
|
4
|
+
import { P as Principal, Q as QuickdrawTypes, M as MaybePromise, c as PrincipalOf, a as McpContext, T as Transport, C as ContextExtensionOf, b as McpContextOf, D as DbOf } from './types-Bwg3kIEc.js';
|
|
5
5
|
|
|
6
6
|
/** What a development warning is about. */
|
|
7
|
-
type DevWarningKind = "n-plus-one" | "unbounded-read" | "oversized-response" | "nested-write" | "ambient-write" | "batch-read" | "batch-create-many" | "repeated-call";
|
|
7
|
+
type DevWarningKind = "n-plus-one" | "unbounded-read" | "oversized-response" | "nested-write" | "ambient-write" | "batch-read" | "batch-create-many" | "repeated-call" | "tiered-field-in-output";
|
|
8
8
|
/** One development warning. */
|
|
9
9
|
interface DevWarning {
|
|
10
10
|
readonly kind: DevWarningKind;
|
|
@@ -340,6 +340,20 @@ type AnyHandler = (args: {
|
|
|
340
340
|
readonly ctx: AnyContext;
|
|
341
341
|
readonly db: unknown;
|
|
342
342
|
}) => unknown;
|
|
343
|
+
/**
|
|
344
|
+
* A method's own output schema, compiled from its JSON Schema when the
|
|
345
|
+
* service is defined (`pipeline/schemaOutput.ts`).
|
|
346
|
+
*/
|
|
347
|
+
interface SchemaOutput {
|
|
348
|
+
/** The value reduced to what the schema declares; the value itself when nothing is dropped. */
|
|
349
|
+
pick(value: unknown): unknown;
|
|
350
|
+
/**
|
|
351
|
+
* Each key the schema declares, at any depth, with the first path that
|
|
352
|
+
* declares it (`email`, `user.email`, `[].email`; `{}` stands for any key
|
|
353
|
+
* of a record): what the `tiered-field-in-output` warning reads.
|
|
354
|
+
*/
|
|
355
|
+
keyPaths(): ReadonlyMap<string, string>;
|
|
356
|
+
}
|
|
343
357
|
/** One method of a defined service, checked and ready to dispatch. */
|
|
344
358
|
interface ServiceMethod {
|
|
345
359
|
readonly name: string;
|
|
@@ -358,6 +372,14 @@ interface ServiceMethod {
|
|
|
358
372
|
* its field tiers are stripped per caller.
|
|
359
373
|
*/
|
|
360
374
|
readonly projection: ProjectedOutput | undefined;
|
|
375
|
+
/**
|
|
376
|
+
* A schema output, compiled from its JSON Schema when the service was
|
|
377
|
+
* defined (`pipeline/schemaOutput.ts`): the handler's result is reduced to
|
|
378
|
+
* what the schema declares before the output check, on every transport.
|
|
379
|
+
* `undefined` for a projection output, and for a schema without JSON
|
|
380
|
+
* Schema (Zod 3), which is sent as the handler returns it.
|
|
381
|
+
*/
|
|
382
|
+
readonly schemaOutput: SchemaOutput | undefined;
|
|
361
383
|
readonly access: AccessForm;
|
|
362
384
|
readonly handler: AnyHandler;
|
|
363
385
|
readonly share: ShareMode | undefined;
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-V3KNCTXJ.js";
|
|
4
4
|
|
|
5
5
|
// src/version.ts
|
|
6
|
-
var QUICKDRAW_VERSION = "5.0.0-rc.
|
|
6
|
+
var QUICKDRAW_VERSION = "5.0.0-rc.6";
|
|
7
7
|
|
|
8
8
|
// src/contract/names.ts
|
|
9
9
|
function entityRoom(service, id, level) {
|
|
@@ -127,4 +127,4 @@ export {
|
|
|
127
127
|
MAX_SUBSCRIBE_IDS,
|
|
128
128
|
MAX_SCOPE_LENGTH
|
|
129
129
|
};
|
|
130
|
-
//# sourceMappingURL=chunk-
|
|
130
|
+
//# sourceMappingURL=chunk-4HOXH4V3.js.map
|