@spine-event-engine/server 2.0.0-snapshot.25 → 2.0.0-snapshot.28

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.
Files changed (207) hide show
  1. package/README.md +104 -16
  2. package/REFERENCE.md +486 -57
  3. package/dist/agent/agent-history.d.ts +19 -3
  4. package/dist/agent/agent-history.d.ts.map +1 -1
  5. package/dist/agent/agent-history.js +41 -10
  6. package/dist/agent/agent-history.js.map +1 -1
  7. package/dist/agent/agent-live-ai-runtime.d.ts +1 -1
  8. package/dist/agent/agent-live-ai-runtime.js +1 -1
  9. package/dist/agent/agent-live-budget.d.ts +1 -1
  10. package/dist/agent/agent-live-budget.js +1 -1
  11. package/dist/agent/agent-live-read-runtime.d.ts +13 -2
  12. package/dist/agent/agent-live-read-runtime.d.ts.map +1 -1
  13. package/dist/agent/agent-live-read-runtime.js +6 -5
  14. package/dist/agent/agent-live-read-runtime.js.map +1 -1
  15. package/dist/browser/browser-admission.d.ts +47 -0
  16. package/dist/browser/browser-admission.d.ts.map +1 -0
  17. package/dist/browser/browser-admission.js +200 -0
  18. package/dist/browser/browser-admission.js.map +1 -0
  19. package/dist/browser/browser-server.d.ts +171 -1
  20. package/dist/browser/browser-server.d.ts.map +1 -1
  21. package/dist/browser/browser-server.js +1027 -303
  22. package/dist/browser/browser-server.js.map +1 -1
  23. package/dist/browser/durable-subscription-bindings.d.ts +5 -0
  24. package/dist/browser/durable-subscription-bindings.d.ts.map +1 -1
  25. package/dist/browser/durable-subscription-bindings.js +226 -38
  26. package/dist/browser/durable-subscription-bindings.js.map +1 -1
  27. package/dist/browser/index.d.ts +1 -1
  28. package/dist/browser/index.d.ts.map +1 -1
  29. package/dist/browser/options.d.ts +53 -2
  30. package/dist/browser/options.d.ts.map +1 -1
  31. package/dist/bus/command-bus.d.ts +9 -1
  32. package/dist/bus/command-bus.d.ts.map +1 -1
  33. package/dist/bus/command-bus.js +75 -10
  34. package/dist/bus/command-bus.js.map +1 -1
  35. package/dist/bus/command-errors.d.ts +20 -4
  36. package/dist/bus/command-errors.d.ts.map +1 -1
  37. package/dist/bus/command-errors.js +47 -7
  38. package/dist/bus/command-errors.js.map +1 -1
  39. package/dist/bus/event-bus.d.ts +9 -1
  40. package/dist/bus/event-bus.d.ts.map +1 -1
  41. package/dist/bus/event-bus.js +136 -9
  42. package/dist/bus/event-bus.js.map +1 -1
  43. package/dist/context/bounded-context.d.ts +59 -39
  44. package/dist/context/bounded-context.d.ts.map +1 -1
  45. package/dist/context/bounded-context.js +277 -262
  46. package/dist/context/bounded-context.js.map +1 -1
  47. package/dist/context/entity-inbox.d.ts +12 -17
  48. package/dist/context/entity-inbox.d.ts.map +1 -1
  49. package/dist/context/entity-inbox.js +106 -119
  50. package/dist/context/entity-inbox.js.map +1 -1
  51. package/dist/context/local-inbox-handoff.d.ts +13 -13
  52. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  53. package/dist/context/local-inbox-handoff.js +60 -32
  54. package/dist/context/local-inbox-handoff.js.map +1 -1
  55. package/dist/context/persistent-tenant-index.d.ts +81 -0
  56. package/dist/context/persistent-tenant-index.d.ts.map +1 -0
  57. package/dist/context/persistent-tenant-index.js +270 -0
  58. package/dist/context/persistent-tenant-index.js.map +1 -0
  59. package/dist/context/projection-handoff.d.ts +9 -9
  60. package/dist/context/projection-handoff.d.ts.map +1 -1
  61. package/dist/context/projection-handoff.js +31 -23
  62. package/dist/context/projection-handoff.js.map +1 -1
  63. package/dist/context/tenant-index.d.ts +7 -6
  64. package/dist/context/tenant-index.d.ts.map +1 -1
  65. package/dist/context/tenant-index.js +2 -188
  66. package/dist/context/tenant-index.js.map +1 -1
  67. package/dist/context/tenant-unknown-error.d.ts +2 -2
  68. package/dist/context/tenant-unknown-error.d.ts.map +1 -1
  69. package/dist/context/tenant-unknown-error.js +4 -1
  70. package/dist/context/tenant-unknown-error.js.map +1 -1
  71. package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
  72. package/dist/delivery/delivery-supervisor.js +12 -3
  73. package/dist/delivery/delivery-supervisor.js.map +1 -1
  74. package/dist/delivery/delivery.d.ts +4 -3
  75. package/dist/delivery/delivery.d.ts.map +1 -1
  76. package/dist/delivery/delivery.js +82 -34
  77. package/dist/delivery/delivery.js.map +1 -1
  78. package/dist/delivery/reception-observation.d.ts +3 -3
  79. package/dist/delivery/reception-observation.js +2 -2
  80. package/dist/entity/entity.d.ts +59 -0
  81. package/dist/entity/entity.d.ts.map +1 -1
  82. package/dist/entity/entity.js +60 -0
  83. package/dist/entity/entity.js.map +1 -1
  84. package/dist/handler/app-query-handler.d.ts +60 -0
  85. package/dist/handler/app-query-handler.d.ts.map +1 -0
  86. package/dist/handler/app-query-handler.js +23 -0
  87. package/dist/handler/app-query-handler.js.map +1 -0
  88. package/dist/handler/generated-registry-discovery.d.ts +6 -0
  89. package/dist/handler/generated-registry-discovery.d.ts.map +1 -1
  90. package/dist/handler/generated-registry-discovery.js +71 -2
  91. package/dist/handler/generated-registry-discovery.js.map +1 -1
  92. package/dist/handler/handler-decorators.d.ts +8 -29
  93. package/dist/handler/handler-decorators.d.ts.map +1 -1
  94. package/dist/handler/handler-decorators.js +33 -210
  95. package/dist/handler/handler-decorators.js.map +1 -1
  96. package/dist/handler/spine-metadata-inventory.d.ts +99 -0
  97. package/dist/handler/spine-metadata-inventory.d.ts.map +1 -0
  98. package/dist/handler/spine-metadata-inventory.js +162 -0
  99. package/dist/handler/spine-metadata-inventory.js.map +1 -0
  100. package/dist/index.d.ts +9 -4
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +6 -2
  103. package/dist/index.js.map +1 -1
  104. package/dist/repository/command-routing.d.ts +1 -1
  105. package/dist/repository/command-routing.js +1 -1
  106. package/dist/repository/event-routing.d.ts +1 -1
  107. package/dist/repository/event-routing.js +1 -1
  108. package/dist/repository/repository.d.ts +61 -26
  109. package/dist/repository/repository.d.ts.map +1 -1
  110. package/dist/repository/repository.js +612 -306
  111. package/dist/repository/repository.js.map +1 -1
  112. package/dist/repository/state-update-routing.d.ts +1 -1
  113. package/dist/repository/state-update-routing.js +1 -1
  114. package/dist/runtime/signal-publisher.d.ts +23 -8
  115. package/dist/runtime/signal-publisher.d.ts.map +1 -1
  116. package/dist/runtime/signal-publisher.js +11 -8
  117. package/dist/runtime/signal-publisher.js.map +1 -1
  118. package/dist/server/managed-server-application.d.ts +12 -11
  119. package/dist/server/managed-server-application.d.ts.map +1 -1
  120. package/dist/server/managed-server-application.js +41 -25
  121. package/dist/server/managed-server-application.js.map +1 -1
  122. package/dist/server/node-coordinator.d.ts +18 -5
  123. package/dist/server/node-coordinator.d.ts.map +1 -1
  124. package/dist/server/node-coordinator.js +744 -108
  125. package/dist/server/node-coordinator.js.map +1 -1
  126. package/dist/server/reserved-spine-rpc-paths.json +1 -0
  127. package/dist/server/server-environment.d.ts +23 -5
  128. package/dist/server/server-environment.d.ts.map +1 -1
  129. package/dist/server/server-environment.js +89 -24
  130. package/dist/server/server-environment.js.map +1 -1
  131. package/dist/server/server.d.ts +45 -0
  132. package/dist/server/server.d.ts.map +1 -1
  133. package/dist/server/server.js +112 -3
  134. package/dist/server/server.js.map +1 -1
  135. package/dist/services/app-query-execution.d.ts +53 -0
  136. package/dist/services/app-query-execution.d.ts.map +1 -0
  137. package/dist/services/app-query-execution.js +408 -0
  138. package/dist/services/app-query-execution.js.map +1 -0
  139. package/dist/services/native-operation.d.ts +34 -0
  140. package/dist/services/native-operation.d.ts.map +1 -0
  141. package/dist/services/native-operation.js +77 -0
  142. package/dist/services/native-operation.js.map +1 -0
  143. package/dist/services/query-reader.d.ts +32 -7
  144. package/dist/services/query-reader.d.ts.map +1 -1
  145. package/dist/services/query-reader.js +22 -2
  146. package/dist/services/query-reader.js.map +1 -1
  147. package/dist/services/read-request-status.d.ts +65 -0
  148. package/dist/services/read-request-status.d.ts.map +1 -0
  149. package/dist/services/read-request-status.js +151 -0
  150. package/dist/services/read-request-status.js.map +1 -0
  151. package/dist/services/spine-services.d.ts +42 -7
  152. package/dist/services/spine-services.d.ts.map +1 -1
  153. package/dist/services/spine-services.js +1005 -175
  154. package/dist/services/spine-services.js.map +1 -1
  155. package/dist/services/trusted-reader-ingress.d.ts +110 -0
  156. package/dist/services/trusted-reader-ingress.d.ts.map +1 -0
  157. package/dist/services/trusted-reader-ingress.js +102 -0
  158. package/dist/services/trusted-reader-ingress.js.map +1 -0
  159. package/dist/spi/handler-registry.d.ts +2 -0
  160. package/dist/spi/handler-registry.d.ts.map +1 -1
  161. package/dist/spi/handler-registry.js +1 -1
  162. package/dist/spi/handler-registry.js.map +1 -1
  163. package/dist/stand/read-side-filter-engine.d.ts +46 -0
  164. package/dist/stand/read-side-filter-engine.d.ts.map +1 -0
  165. package/dist/stand/read-side-filter-engine.js +356 -0
  166. package/dist/stand/read-side-filter-engine.js.map +1 -0
  167. package/dist/stand/read-side-filter.d.ts +247 -0
  168. package/dist/stand/read-side-filter.d.ts.map +1 -0
  169. package/dist/stand/read-side-filter.js +39 -0
  170. package/dist/stand/read-side-filter.js.map +1 -0
  171. package/dist/stand/read-side-message.d.ts +66 -0
  172. package/dist/stand/read-side-message.d.ts.map +1 -0
  173. package/dist/stand/read-side-message.js +167 -0
  174. package/dist/stand/read-side-message.js.map +1 -0
  175. package/dist/stand/stand-direct-delivery.d.ts +161 -0
  176. package/dist/stand/stand-direct-delivery.d.ts.map +1 -0
  177. package/dist/stand/stand-direct-delivery.js +210 -0
  178. package/dist/stand/stand-direct-delivery.js.map +1 -0
  179. package/dist/stand/stand.d.ts +81 -43
  180. package/dist/stand/stand.d.ts.map +1 -1
  181. package/dist/stand/stand.js +411 -55
  182. package/dist/stand/stand.js.map +1 -1
  183. package/dist/stand/subscription-observer.d.ts.map +1 -1
  184. package/dist/stand/subscription-observer.js +103 -0
  185. package/dist/stand/subscription-observer.js.map +1 -1
  186. package/dist/stand/subscription-read-filters.d.ts +40 -0
  187. package/dist/stand/subscription-read-filters.d.ts.map +1 -0
  188. package/dist/stand/subscription-read-filters.js +91 -0
  189. package/dist/stand/subscription-read-filters.js.map +1 -0
  190. package/dist/stand/subscription-reader-binding.d.ts +20 -0
  191. package/dist/stand/subscription-reader-binding.d.ts.map +1 -0
  192. package/dist/stand/subscription-reader-binding.js +56 -0
  193. package/dist/stand/subscription-reader-binding.js.map +1 -0
  194. package/dist/stand/subscription-records.d.ts +30 -3
  195. package/dist/stand/subscription-records.d.ts.map +1 -1
  196. package/dist/stand/subscription-records.js +61 -16
  197. package/dist/stand/subscription-records.js.map +1 -1
  198. package/dist/stand/subscription-registry.d.ts +49 -14
  199. package/dist/stand/subscription-registry.d.ts.map +1 -1
  200. package/dist/stand/subscription-registry.js +150 -31
  201. package/dist/stand/subscription-registry.js.map +1 -1
  202. package/dist/stand/subscription-runtime.d.ts +7 -2
  203. package/dist/stand/subscription-runtime.d.ts.map +1 -1
  204. package/dist/stand/subscription-runtime.js +131 -25
  205. package/dist/stand/subscription-runtime.js.map +1 -1
  206. package/dist/tsconfig.tsbuildinfo +1 -1
  207. package/package.json +16 -14
package/REFERENCE.md CHANGED
@@ -168,7 +168,7 @@ phase without implying that every Entity model has been prepared.
168
168
  awaits both model preparation and the first subscription reconciliation by
169
169
  default. Each option can be set to `false` independently. A deferred phase runs
170
170
  before the first valid Command, Event, query, subscription operation, or Inbox
171
- replay for the receiving context; lazy multitenant preparation selects only that
171
+ delivery for the receiving context; lazy multitenant preparation selects only that
172
172
  input's currently registered tenant. A failed lazy attempt rejects its current
173
173
  input and can be retried by a later input. Monitoring requires the shared
174
174
  subscription registry, so `startSubscriptionMonitoring: true` prepares that
@@ -208,20 +208,33 @@ contexts share one in-memory factory, including in production. Local and test
208
208
  environments default storage in memory and use the core registry only as a
209
209
  fallback; those defaults are not production configuration.
210
210
 
211
- `tenantIndex` selects a live application membership directory. When omitted,
212
- the environment uses its storage factory's native catalog where available, or
213
- a transient in-memory index for a custom factory without a catalog. The
214
- transient index lasts only for this environment instance. `contains()` checks
215
- membership without preparing storage; `keep()` makes a registration visible to
216
- later reads and may reject for a read-only catalog. PostgreSQL and MySQL
217
- catalogs expose only configured tenant entries: `keep()` is idempotent for
218
- those entries and rejects unknown IDs. A dynamic SQL tenant resolver does not
219
- register or enumerate tenants. Single-tenant contexts do not insert a synthetic
220
- member into this directory.
211
+ `tenantIndex` is optional and defaults to `SINGLE_TENANT`, a marker that creates
212
+ no directory and never consults a provider catalog. The environment getter
213
+ returns `TenantIndexSetting`, either that marker or an application-supplied
214
+ `TenantIndex`. Narrow against the marker before calling `contains()`, `keep()`,
215
+ `page()`, or `all()`. A multitenant Bounded Context requires a real environment
216
+ index before business storage opens. A single-tenant Bounded Context always returns the
217
+ marker, including inside a multitenant environment. Provider catalogs remain
218
+ separate storage-provider APIs and do not select application membership.
219
+
220
+ `PersistentTenantIndex.create({ storageFactory, group })` opens a separate,
221
+ storage-backed registration directory without constructing a Bounded Context or
222
+ `ServerEnvironment`. It creates and checks supported storage structures before
223
+ returning. Set `createMissingStructures: false` to skip preparation on creation
224
+ and on later reads or writes. A skipped directory needs structures prepared by
225
+ the application before data access; missing structures cause provider errors.
226
+ Memory storage remains process-local. The injected factory remains usable after the
227
+ index closes. `keep()` registers a complete `TenantId` atomically and retains
228
+ the first registration time; `contains()` reads its primary key. `page()` uses
229
+ finite keyset traversal with an abort signal and a continuation accepted only
230
+ by that index instance. `all()` traverses the complete current directory, so
231
+ its result can be large. Traversal does not promise a snapshot while other
232
+ writers register tenants. Registration does not provision tenant databases or
233
+ authorize users. The environment defaults to the explicit `SINGLE_TENANT` marker.
221
234
 
222
235
  Multitenant receiving contexts check current index membership before Command,
223
236
  Event, query, direct repository/history, and subscription work. Inbox persistence
224
- and later replay check separately, so removing a tenant prevents saved work from
237
+ and later delivery check separately, so removing a tenant prevents saved work from
225
238
  restoring it. Input does not call `keep()` implicitly. A confirmed missing member
226
239
  raises `TenantUnknownError` with the complete tenant ID and context name; native
227
240
  services return `NotFound` with a structured `spine.base.Error` detail whose type
@@ -249,8 +262,8 @@ Direct `repository.routeCommand()` and `repository.routeEvent()` calls return
249
262
  Promises and must be awaited. Commands select exactly one ID; Events and state
250
263
  updates may select any number of distinct IDs. After validation and stable
251
264
  deduplication, routes above 1,000 recipients emit one warning and retain every
252
- recipient. Inbox replay uses stored target IDs without rerouting. The legacy-named
253
- local `catchUpReadSide()` helper is unrelated to retry: it resets and replays
265
+ recipient. Inbox delivery uses stored target IDs without rerouting. The local
266
+ `catchUpReadSide()` helper is unrelated to retry: it resets and dispatches stored Events through
254
267
  the whole local read side, and is not Projection catch-up.
255
268
 
256
269
  ## Message-valued Entity IDs
@@ -282,7 +295,7 @@ const id: MessageId = create(CommandIdSchema, { uuid: "shipment-7" });
282
295
  ```
283
296
 
284
297
  Admission validates a message ID against the schema declared by the state
285
- field, including generated validation rules, before it is stored or replayed.
298
+ field, including generated validation rules, before it is stored or delivered.
286
299
  When default routing finds a message but the Entity ID type is primitive, a
287
300
  custom route returns the intended primitive ID.
288
301
 
@@ -585,9 +598,8 @@ await context.commandBus().post(
585
598
  The generated record declares `ApproveProjectSchema` as the input and
586
599
  `ScheduleProjectSchema` as the emitted Command schema; registry ingestion, not
587
600
  manual decorator materialization, constructs the output-bearing metadata.
588
- Application builds emit the record to
589
- `generated/handler/generated-handler-registry.js`; those applications use
590
- `build()` with their compiled package root.
601
+ Application builds emit the record to `generated/spine-metadata.js` and import
602
+ `generated/spine-bootstrap.js` before `build()` selects registered classes.
591
603
 
592
604
  One `@Where({ eventField, equals })` equality filter may narrow an event- or
593
605
  rejection-consuming `@Subscribe`, `@React`, or `@Command` handler after type
@@ -597,9 +609,9 @@ repeated declarations fail closed. It is not an alternative routing mechanism.
597
609
  Configure logging through `ServerEnvironment` when assembling the server. The
598
610
  server installs that logger into its runtime components, where warnings and
599
611
  failures carry operation context. Durable facts belong in events or storage.
600
- Ordinary Aggregate, Process Manager, and Projection Inbox replay sends persisted
601
- accepted work through the normal handler path, so those handler effects must be
602
- replay-safe. An accepted Agent interaction is recorded by its receipt and is
612
+ Ordinary Aggregate, Process Manager, and Projection Inbox delivery sends persisted
613
+ accepted work through the normal handler path. Handler effects must tolerate
614
+ redelivery after an uncertain acknowledgement. An accepted Agent interaction is recorded by its receipt and is
603
615
  never re-executed after interruption.
604
616
 
605
617
  ## Context assembly and entities
@@ -608,12 +620,14 @@ Create a context with `BoundedContext.singleTenant(name)` or
608
620
  `BoundedContext.multitenant(name)`. Names are immutable. Use
609
621
  `ServerEnvironment.when(type).use({ storageFactory: factory })` before explicit
610
622
  environment setup to supply record/event storage to all contexts.
611
- `withTenantIndex(index)` selects a different live membership directory for one
612
- context; `context.tenantIndex` and `ServerEnvironment.instance().tenantIndex`
613
- return the configured objects. Closing a context does not close either index.
614
- `withGeneratedRegistryRoot(root)` enables framework discovery of the generated
615
- handler registry for classes registered with `add(EntityClass)`;
616
- `build()` asynchronously performs that discovery and assembly, including
623
+ `withTenantIndex(index)` selects a different live membership directory for a
624
+ multitenant Bounded Context after its environment has a real index. The Bounded Context getter
625
+ returns its captured selection; single-tenant Bounded Contexts return `SINGLE_TENANT`.
626
+ Both getters have type `TenantIndexSetting` and require narrowing before index
627
+ methods. Closing a Bounded Context does not close the configured directory.
628
+ The application imports its generated `spine-bootstrap.js` before registering
629
+ classes with `add(EntityClass)`; `build()` then selects passive descriptions
630
+ by exact constructor and performs assembly, including
617
631
  explicit `Repository` registration. Await it before using the context. A built context contains `CommandBus`,
618
632
  `EventBus`, `Stand`, repositories, and its storage lifecycle.
619
633
  Trusted server-side code can call `context.getRepository(EntityClass)` to obtain
@@ -631,13 +645,12 @@ registry shapes with an instruction to regenerate.
631
645
  Each built context also has one `StandSubscriptionRegistry`. By default the
632
646
  builder creates a storage-backed registry from the resolved context
633
647
  `StorageFactory` (including the `ServerEnvironment` factory used for a builder
634
- added to a server). It stores one `spine.client.SubscriptionRecord` under each
648
+ added to a server). Its physical `StorageGroup` derives from the logical Bounded Context name, so two contexts using one factory keep distinct definitions. An injected `StorageSubscriptionRegistry` takes an explicit group as its third constructor argument and prepares its captured factory and context. It stores one `spine.client.SubscriptionRecord` under each
635
649
  explicit `SubscriptionId`. A complete custom implementation can instead be
636
650
  supplied with `withSubscriptionRegistry(registry)`; responsibility transfers to
637
651
  the builder on the first build attempt.
638
652
 
639
- `SubscriptionService` streams are live best-effort notifications, not replay,
640
- ordering, or completeness contracts. A standalone Server defaults to the
653
+ `SubscriptionService` streams are live notifications. Each subscriber receives its admitted filtered updates in order, but there is no replay, exactly-once, or cluster-wide completeness guarantee. A standalone Server defaults to the
641
654
  persistent registry. In a group of managed server replicas, the framework starts
642
655
  and closes each child Server and accepts only every context's actual
643
656
  `InMemorySubscriptionRegistry`; a persistent or custom registry is rejected
@@ -645,7 +658,12 @@ and the assembled child is closed before READY. Durable logical bindings remain
645
658
  Gateway-only. The Coordinator fans each logical definition to ready children,
646
659
  merges updates, propagates cancellation, and carries no subscription payload on
647
660
  parent/child IPC. A late or replacement child synchronizes retained definitions
648
- before it is eligible for normal requests. Active definitions attach their
661
+ before it is eligible for normal requests. The Coordinator resolves the same
662
+ `subscriptionQueueCapacity` before binding its HTTP/2 listener. Its activation
663
+ ACK is separate from the update queue, which reserves one active DATA update
664
+ plus the configured waiting count for each logical subscriber. Overflow closes
665
+ that subscriber's relay and aborts its child streams.
666
+ Active definitions attach their
649
667
  child streams before admission; subscribed but inactive definitions do not
650
668
  block readiness. Each child observes application-configured remote/shared
651
669
  Delivery directly. During DRAINING, new unary and shard admission stops while
@@ -678,12 +696,21 @@ completed cycle. This is best-effort per-node convergence, not a
678
696
  cluster-completeness, replay, ordering, gap-repair, or exactly-once guarantee.
679
697
 
680
698
  The registry accepts only a generated `SubscriptionId` for activate, get, and
681
- delete. `create(subscription)` returns `{ kind: "created", entry }` or
699
+ delete. `create(subscription, reader?)` returns `{ kind: "created", entry }` or
682
700
  `{ kind: "existing", entry }`; the latter requires byte-equivalent canonical
683
- content and a different definition with the same ID throws `StandConflictError`.
684
- `activate(id)` returns `activated`, `active`, `missing`, or `expired`.
685
- `delete(id)` returns `deleted` or `missing`. Blank IDs or topics and malformed
686
- durable records fail closed.
701
+ content and the same reader binding. A different definition or binding with the
702
+ same ID throws `StandConflictError`. `activate(id, reader?)` returns `activated`,
703
+ `active`, `missing`, `expired`, or `denied`. `delete(id, reader?)` returns `deleted`,
704
+ `missing`, or `denied`. Blank IDs or topics and malformed durable records fail closed.
705
+
706
+ Service requests always supply the admitted principal ID or an explicit anonymous
707
+ binding. When supplied, the registry checks the binding again after every failed
708
+ compare-and-set, before changing a reread record. The omitted argument is for
709
+ trusted internal lifecycle work; it is not a way to admit an unauthenticated RPC.
710
+ Reader identity is stored atomically with the subscription definition and phase.
711
+ Attributes, authority facts, and credentials are not part of the stored identity.
712
+ A missing legacy binding is not anonymous and cannot be claimed by a caller;
713
+ the client must create a new subscription.
687
714
 
688
715
  Entries and nested subscriptions returned by create, activate, get, and
689
716
  snapshot are clone-safe views. Protobuf byte arrays remain mutable runtime
@@ -840,6 +867,11 @@ The scope is explicit: pass `{}` in a single-tenant Bounded Context or
840
867
  `{ tenantId }` in a multitenant Bounded Context. Reads use indexed storage
841
868
  without restoring an Agent. The application bridge must authorize access;
842
869
  actor metadata does not grant read permission.
870
+ For a trusted in-process caller, the scope can also include detached
871
+ `actorContext`, a verified `reader` with principal and authority facts, and a
872
+ `signal`. Read-side filters receive those facts for that history call; omitting
873
+ `reader` leaves principal and authority facts absent. The bridge supplies
874
+ verified facts explicitly and cannot derive them from `actorContext`.
843
875
 
844
876
  If a selected model's `authorizeUse` callback explicitly returns `false`
845
877
  before the handler, the turn records one `AgentInvocationTerminated` System
@@ -849,10 +881,15 @@ may correlate this fact with the original source through paged Agent history.
849
881
 
850
882
  ## Signals, validation, and rejection behavior
851
883
 
852
- `CommandBus` validates every accepted command from Proto validation options
853
- before dispatcher or repository handler code runs. It raises the framework's
854
- command-validation failure, which `SpineServices` maps to
855
- `COMMAND_VALIDATION_ERROR` with a packed `spine.validation.ValidationError`.
884
+ `CommandBus` checks the nonempty Command ID, nondefault message and context,
885
+ and declared payload constraints before admission or dispatcher code runs.
886
+ Annotated packed messages use generated Proto modules registered in the process.
887
+ `SpineServices` reports invalid Commands in an error Ack with type
888
+ `spine.core.CommandValidationError`, numeric `INVALID_COMMAND` code, and a
889
+ packed `spine.validation.ValidationError`. Unsupported Commands use the same
890
+ enum type and numeric `UNSUPPORTED_COMMAND` code.
891
+ Technical validator failures use a sanitized `COMMAND_POST_ERROR` Ack without
892
+ constraint details; they are distinct from invalid Command input.
856
893
  Entity transition validation failures that escape service processing map to
857
894
  `COMMAND_STATE_TRANSITION_VALIDATION_FAILED` with the same detail type. Managed
858
895
  reception of ordinary Entity work sends post-handler transition rejection to `DeliveryMonitor`;
@@ -871,6 +908,8 @@ an internal diagnostic, not a retry guarantee.
871
908
  Every domain context has an internal paired System Context. Domain events use
872
909
  the domain `EventBus` and domain `EventStore`. System events use only the
873
910
  System Context `EventBus`, so they never enter the domain EventStore.
911
+ Each EventBus validates the Event envelope and domain payload before admission,
912
+ storage, or delivery.
874
913
  `persistSystemEvents()` enables the paired System Context's separate storage.
875
914
  Bounded Contexts with Agents must enable it. For other Bounded Contexts it is optional; without
876
915
  it, the bus validates, dispatches and notifies without appending. Schemas may come from an external dispatcher or
@@ -887,9 +926,213 @@ work to stop using dependencies, then closes contexts. Caller-managed `start()`
887
926
  does not close global process facilities; close `ServerEnvironment.instance()`
888
927
  separately when used. The final run-managed `run()` close closes its environment.
889
928
 
929
+ Native QueryService accepts `BytesValue` equality and range operands for annotated scalar `bytes`
930
+ columns and permits ascending or descending byte ordering. The shared evaluator uses unsigned
931
+ lexicographic comparison, including shorter equal prefixes first. This TypeScript query capability
932
+ does not assert JVM JDBC byte-range compatibility. `QUERY_SERVICE_MAX_RESULTS` from core is the
933
+ current capacity of 1,000 returned and evaluated records for one native read, not a collection-size limit.
934
+
935
+ `ServerOptions.appQueries` selects concrete generated `AppQueryHandler<Q, R>`
936
+ instances by exact constructor metadata before listening. Duplicate Q routes
937
+ or missing metadata fail startup. On direct app-node ingress,
938
+ `QueryService.Execute` validates a complete AppQuery envelope and Q, then
939
+ returns one packed paired R. Native and browser Gateways and managed Coordinators
940
+ forward Execute to an app node; `ClientRequest.query(generatedQ)` decodes its paired R.
941
+ The internal
942
+ `Server.query(generatedQ, executionContext)` accepts a generated Q and returns
943
+ its paired generated R, using a detached request and the effective actor,
944
+ verified reader facts, and cancellation signal supplied by the current
945
+ framework invocation. `ProcessManager.currentActorContext` and
946
+ `Agent.currentActorContext` expose only the current signal's detached actor and
947
+ tenant during a handler; neither establishes a verified principal. Constructor
948
+ injection can bind the query function to a Server. Scoped Entity reads require
949
+ query/full visibility and destination tenant membership. Direct calls do not
950
+ cross the Server's gRPC output-filter boundary.
951
+
952
+ `ReadSideFilter<typeof MessageSchema>` declares a typed policy for a registered
953
+ Proto message. Its protected constructor takes the generated schema and optional
954
+ `{ nested: true }`; `filter(message, context)` receives a detached mutable value
955
+ and may return the same type or `null` to remove that occurrence. The context
956
+ separates actor metadata from verified reader facts and identifies the current
957
+ operation, plus a detached containing message and field for nested calls.
958
+ Read-only packed bytes remain available to Buf decoding through a detached
959
+ `value.slice()` passed to `fromBinary`.
960
+ Call `addReadSideFilter(filter)` on a `BoundedContextBuilder` before `build()` to apply it to that
961
+ Bounded Context's outward Entity reads and retained histories. Pass `readSideFilters`
962
+ to `new Server(...)` to apply policies to Entity Query and App Query results
963
+ leaving that Server over gRPC. The same instance can be installed in both
964
+ places; each boundary invokes it once. Direct `Server.query()` runs BC source
965
+ policies but does not cross the Server output boundary. An Entity root returned
966
+ as `null` is omitted without another storage read; an App Query root returned
967
+ as `null` produces OK with zero outer `AppQueryResponse.message` entries. A
968
+ non-null response whose child field is absent remains one response entry.
969
+ History removals retain the
970
+ original continuation. Retained storage, hydration, and replay remain raw.
971
+ Direct Stand and retained-history callbacks identify the producing Bounded
972
+ Context. Protected history called from a handler carries its current signal
973
+ actor; an out-of-handler diagnostic read has no actor or authenticated reader.
974
+ Native SubscriptionService delivery filters already matched Entity states and
975
+ Event payloads through the producing Bounded Context policy and then the
976
+ Server policy. The `subscription` operation includes the original Topic and
977
+ admitted subscription; its context carries the current verified reader facts,
978
+ effective tenant, actor metadata, producing Bounded Context, and cancellation
979
+ signal. Removing a state emits `no_longer_matching` with its original Entity
980
+ ID; removing every Event payload suppresses that data batch. ACKs and existing
981
+ removal controls remain unchanged. Policy failures end only that reader stream
982
+ with a sanitized technical status.
983
+
984
+ The operation describes the call that produced the value. `query` includes the
985
+ actual Query and, during an App Query child read, the enclosing AppQuery. `app-query`
986
+ identifies the final application result. `direct` identifies either a typed
987
+ Stand state read by type URL and Entity ID, a caller-supplied RecordQuery, or a
988
+ normalized query plan. `history` distinguishes an Entity state-at timestamp,
989
+ Entity state/Event history depth, and an Agent view with its page request and
990
+ optional cursor. Direct callers supply actor metadata, verified reader facts,
991
+ and cancellation separately; they cannot override the operation.
992
+ Direct subscription delivery uses `kind: "direct"` with
993
+ `operation.subscription.typeUrl`, distinct from a one-off Stand read's
994
+ `operation.read`.
995
+
996
+ This filter uses the generated Project state and can be installed on the Bounded
997
+ Context builder that contains its Repository:
998
+
999
+ <!-- docs-snippet-path: packages/server/test/server/app-query-execution.test.ts -->
1000
+
1001
+ ```ts
1002
+ import {
1003
+ type BoundedContextBuilder,
1004
+ ReadSideFilter,
1005
+ type ReadSideContext,
1006
+ } from "@spine-event-engine/server";
1007
+ import {
1008
+ type ProjectOverviewState,
1009
+ ProjectOverviewStateSchema,
1010
+ } from "../../test-fixtures/generated/repository-routing/project_states_pb.js";
1011
+
1012
+ /**
1013
+ * Replaces a project name in all ordinary Entity Query results.
1014
+ */
1015
+ class PublicProjectName extends ReadSideFilter<typeof ProjectOverviewStateSchema> {
1016
+ /**
1017
+ * Registers the filter for project overview state.
1018
+ */
1019
+ constructor() {
1020
+ super(ProjectOverviewStateSchema);
1021
+ }
1022
+
1023
+ /**
1024
+ * Replaces the name only for ordinary Entity Query responses.
1025
+ *
1026
+ * @param state Project overview returned by the read side.
1027
+ * @param context Describes the read operation and verified reader.
1028
+ * @returns The project overview with its name replaced for ordinary Entity Queries.
1029
+ */
1030
+ filter(state: ProjectOverviewState, context: ReadSideContext): ProjectOverviewState {
1031
+ if (context.operation.kind === "query") state.name = "Public project";
1032
+ return state;
1033
+ }
1034
+ }
1035
+
1036
+ const projectPolicy = new PublicProjectName();
1037
+
1038
+ /**
1039
+ * Registers the project name policy in its supplying Bounded Context.
1040
+ *
1041
+ * @param builder Configures the project Bounded Context.
1042
+ * @returns The builder with the read policy installed.
1043
+ */
1044
+ function installProjectPolicy(builder: BoundedContextBuilder): BoundedContextBuilder {
1045
+ return builder.addReadSideFilter(projectPolicy);
1046
+ }
1047
+ ```
1048
+
1049
+ The repository `onCreate` callback can inject `(q, context) => server.query(q, context)`
1050
+ into a Process Manager or Agent. Within an active handler, pass a generated Q
1051
+ with `{ actorContext: this.currentActorContext }`; the actor/tenant snapshot
1052
+ does not supply verified principal or provider facts. The
1053
+ [scoped integration test](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/server/test/entity/process-manager-querying.test.ts) includes
1054
+ the full callback and handler invocation.
1055
+
1056
+ This example uses the generated Project query and the current Process Manager
1057
+ scope; pass `scopedOnCreate(server)` as the repository's `onCreate` callback.
1058
+ Ordinary generation and bootstrap register the decorated handler description:
1059
+
1060
+ <!-- docs-snippet-path: packages/server/test/entity/process-manager-querying.test.ts -->
1061
+
1062
+ ```ts
1063
+ import { create } from "@bufbuild/protobuf";
1064
+ import {
1065
+ Assign,
1066
+ ProcessManager,
1067
+ type EntityOptions,
1068
+ type Server,
1069
+ type ServerQueryExecutionContext,
1070
+ } from "@spine-event-engine/server";
1071
+ import { ProcessManagerStateSchema } from "../../test-fixtures/generated/entity-metadata/visibility_pb.js";
1072
+ import { ProjectIdSchema } from "../../test-fixtures/generated/repository-routing/project_identifiers_pb.js";
1073
+ import {
1074
+ type ProjectCreated as ProjectionEvent,
1075
+ ProjectCreatedSchema as ProjectionEventSchema,
1076
+ } from "../../test-fixtures/generated/repository-routing/project_events_pb.js";
1077
+ import { type CreateReviewProject } from "../../test-fixtures/generated/validation-refusal/project_commands_pb.js";
1078
+ import {
1079
+ type GetProjectOverview_Query,
1080
+ type GetProjectOverview_Response,
1081
+ } from "../../test-fixtures/generated/repository-routing/project_queries_pb.js";
1082
+ import { GetProjectOverview } from "../../test-fixtures/generated/repository-routing/project_queries_app_query.js";
1083
+
1084
+ /**
1085
+ * Runs a typed App Query from an active Process Manager handler.
1086
+ */
1087
+ class ScopedAppQueryManager extends ProcessManager<string, typeof ProcessManagerStateSchema> {
1088
+ /**
1089
+ * Captures the scoped query callback supplied during repository construction.
1090
+ *
1091
+ * @param options Configures this Process Manager instance.
1092
+ * @param ask Executes a generated App Query with trusted handler context.
1093
+ */
1094
+ constructor(
1095
+ options: EntityOptions<string, typeof ProcessManagerStateSchema>,
1096
+ private readonly ask: (
1097
+ query: GetProjectOverview_Query,
1098
+ context: ServerQueryExecutionContext,
1099
+ ) => Promise<GetProjectOverview_Response>,
1100
+ ) {
1101
+ super(options);
1102
+ }
1103
+
1104
+ /**
1105
+ * Reads the current project overview while handling the creation Command.
1106
+ *
1107
+ * @param command Project creation request being assigned.
1108
+ * @returns Event carrying the selected overview name.
1109
+ */
1110
+ @Assign
1111
+ async assign(command: CreateReviewProject): Promise<ProjectionEvent> {
1112
+ const result = await this.ask(
1113
+ create(GetProjectOverview.querySchema, {
1114
+ project: create(ProjectIdSchema, { value: command.id }),
1115
+ }),
1116
+ { actorContext: this.currentActorContext },
1117
+ );
1118
+ return create(ProjectionEventSchema, {
1119
+ id: command.id,
1120
+ name: result.project?.name ?? "",
1121
+ });
1122
+ }
1123
+ }
1124
+
1125
+ /**
1126
+ * Injects the Server's typed App Query route into new Process Manager instances.
1127
+ */
1128
+ export const scopedOnCreate =
1129
+ (server: Server) => (options: EntityOptions<string, typeof ProcessManagerStateSchema>) =>
1130
+ new ScopedAppQueryManager(options, (q, context) => server.query(q, context));
1131
+ ```
1132
+
890
1133
  `Stand` registers entity state for point/list reads, updates current state, and
891
- offers in-process subscriptions. The legacy-named local reset/replay helper
892
- `catchUpReadSide(options?)` clears every registered Projection state and replays
1134
+ offers in-process subscriptions. The local read-side reset helper
1135
+ `catchUpReadSide(options?)` clears every registered Projection state and dispatches
893
1136
  the whole stored event history through matching Projection subscribers; it does
894
1137
  not append events. Single-tenant contexts reject a tenant option; multitenant
895
1138
  contexts require one. It cannot target a Projection repository, entity IDs, or
@@ -917,8 +1160,57 @@ authoritative queries from current state and Entity subscriptions observe
917
1160
  committed `EntityStateChanged` messages on the paired System Context EventBus,
918
1161
  so a direct Stand write alone does not produce a subscription update. Consumer
919
1162
  callback failures are isolated from post-commit observation and other
920
- consumers. The package does not guarantee cluster-complete observations,
921
- subscription replay, event-gap repair, or exactly-once effects.
1163
+ consumers. Native subscription updates enter the existing bounded delivery
1164
+ queue before asynchronous read policies run; a full queue closes that stream.
1165
+ For a surviving Event, policy replaces only its domain payload and retains the original envelope.
1166
+ Direct `Stand.subscribe(schema, { next, error }, options)` applies only its
1167
+ producing Bounded Context's read policies, independently for each local
1168
+ subscriber. `next` receives either `{ kind: "state", state, previousState? }`
1169
+ or `{ kind: "removal", noLongerMatching: true }`; both retain the original type
1170
+ URL, Entity ID, version, and tenant metadata. A removed prior state is omitted
1171
+ from a surviving current state. `error` receives one terminal local policy,
1172
+ callback, or capacity fault. There is no raw callback overload. Options accept
1173
+ trusted `reader` facts, `actorContext` metadata, tenant, and cancellation;
1174
+ `ServerEnvironmentSettings.subscriptionQueueCapacity` sets the waiting count
1175
+ for direct and server-served subscriptions. Configure it before constructing
1176
+ the Stand or Bounded Context. It defaults to 100 waiting updates in addition to
1177
+ one active update; zero allows the active update with no waiting updates. Filter calls
1178
+ and `next` completion are sequential per subscriber. Overflow closes only that
1179
+ subscriber, clears waiting updates, invalidates the active result, and reports
1180
+ `StandSubscriptionCapacityError`; committed writes and other subscribers
1181
+ continue. Unsubscribe, cancellation, and Stand close invalidate outstanding
1182
+ results without waiting for a nonsettling policy. A producer does not await
1183
+ asynchronous policy or observer completion, although CPU-heavy synchronous
1184
+ policy work still runs on the producer's Node event loop.
1185
+ Direct subscribers do not cross a Server output-filter boundary. The package
1186
+ does not guarantee cluster-complete observations, subscription replay,
1187
+ event-gap repair, or exactly-once effects.
1188
+
1189
+ This typed local observer handles visible states, removals, and terminal faults.
1190
+ Configure `subscriptionQueueCapacity: 250` through `ServerEnvironment.when(...).use(...)`
1191
+ before creating the Stand to allow 250 waiting updates beyond the active one:
1192
+
1193
+ <!-- docs-snippet-path: packages/server/test/stand/stand-direct-subscription.test.ts -->
1194
+
1195
+ ```ts
1196
+ import { type Stand, type StandSubscription } from "@spine-event-engine/server";
1197
+ import { ProjectOverviewStateSchema } from "../../test-fixtures/generated/repository-routing/project_states_pb.js";
1198
+
1199
+ export function observeProjects(
1200
+ stand: Stand,
1201
+ onState: (name: string) => void,
1202
+ onRemoval: (id: unknown) => void,
1203
+ onError: (error: Error) => void,
1204
+ ): StandSubscription {
1205
+ return stand.subscribe(ProjectOverviewStateSchema, {
1206
+ next(update) {
1207
+ if (update.kind === "state") onState(update.state.name);
1208
+ else onRemoval(update.id);
1209
+ },
1210
+ error: onError,
1211
+ });
1212
+ }
1213
+ ```
922
1214
 
923
1215
  ### Native and managed hosting
924
1216
 
@@ -988,10 +1280,123 @@ gRPC-Web transport message accepted by the public listener, so the effective
988
1280
  inbound limit is the stricter applicable limit. `writeMaxBytes` limits public
989
1281
  transport responses independently.
990
1282
 
1283
+ ### Native reader admission
1284
+
1285
+ Configure app-node admission at `ServerOptions.services.readerAdmission`:
1286
+
1287
+ - `direct.authenticate({ service, method, value, headers, peer, signal })` authenticates
1288
+ a direct native request. It returns `{ reader, invalidated? }` with verified
1289
+ `{ principal, authorityFacts }` in `reader`, `{ reader: {} }` for deliberate
1290
+ public access, or `undefined` to deny access. `signal` begins before
1291
+ authentication and ends on terminal unary or complete stream exit.
1292
+ The request value contains canonical Proto bytes; `peer` is the Connect
1293
+ `HandlerContext` for that invocation. `invalidated` is a separate local
1294
+ AbortSignal captured by the host; it never enters reader facts or proofs.
1295
+ - `forwarded.verify(bytes, proof, peer)` verifies an assertion from a trusted
1296
+ forwarding peer. It must authenticate that peer and validate the supplied
1297
+ proof over the exact bytes. `BrowserServerOptions.forwardingProof.issue(bytes)`
1298
+ supplies the corresponding gateway adapter.
1299
+
1300
+ Admission completes before service validation, routing, or subscription mutation.
1301
+ Principal IDs and authority facts are copied per invocation. `principal.attributes`
1302
+ remains a string map; `authorityFacts` accepts finite JSON values, including
1303
+ arrays and nested objects. ActorContext remains separate from authentication.
1304
+ Neither the framework nor these ports promise live provider information.
1305
+
1306
+ With no admission adapters, native requests remain public. Internal identity
1307
+ headers are still rejected without a configured verifier. With a forwarding
1308
+ verifier but no direct adapter, a request without a forwarding assertion is
1309
+ rejected as unauthenticated. An application that also permits public direct
1310
+ requests can explicitly return an empty reader from its direct adapter.
1311
+ Malformed assertions and denied authentication return `UNAUTHENTICATED`;
1312
+ unexpected adapter faults return sanitized `INTERNAL`. Explicit cancellation,
1313
+ availability, and permission statuses retain their meaning.
1314
+
1315
+ `ManagedServerApplication` accepts `readerAdmission` for its Coordinator listener
1316
+ and `forwardingProof` for Coordinator-to-child requests. Configure the receiving
1317
+ child Servers with their verifier as well. Each hop verifies its incoming
1318
+ assertion and issues a new proof for the actual outgoing operation and payload.
1319
+ Loopback placement alone does not authenticate a forwarding peer.
1320
+
1321
+ A standard session strategy accepts the host signal during `resolve` and
1322
+ returns an invalidation signal for local logout, its captured session deadline,
1323
+ and strategy close. A custom direct authenticator may omit invalidation, but
1324
+ then the host cannot stop an active stream on local logout or expiry. A
1325
+ successful unary request releases its operation even while the transport peer
1326
+ stays connected. Subscribe releases its operation after definition creation;
1327
+ Activate authenticates again and holds its operation through ACK and every
1328
+ update. Normal completion, host handler iterator return, cancellation, and host
1329
+ close stop the operation. Raw Connect clients must abort an explicit
1330
+ `AbortSignal` or call `SubscriptionService.Cancel` to stop remote stream work;
1331
+ returning from the client iterator alone does not signal transport cancellation.
1332
+ An active stream in another process receives no local signal.
1333
+
1334
+ ```ts
1335
+ import type { SignedSessions } from "@spine-event-engine/auth";
1336
+ import { Server, type ReaderAdmissionResult, type ServerOptions } from "@spine-event-engine/server";
1337
+
1338
+ /**
1339
+ * Configures direct native bearer admission with the local session strategy.
1340
+ *
1341
+ * @param sessions Strategy also used by the application's logout action.
1342
+ * @param contexts Application Bounded Contexts exposed by this Server.
1343
+ * @returns Server configured for native authentication.
1344
+ */
1345
+ function nativeServer(
1346
+ sessions: SignedSessions,
1347
+ contexts: NonNullable<ServerOptions["contexts"]>,
1348
+ ): Server {
1349
+ return new Server({
1350
+ contexts,
1351
+ services: {
1352
+ readerAdmission: {
1353
+ direct: {
1354
+ /**
1355
+ * Resolves the bearer credential within the host operation.
1356
+ *
1357
+ * @param request Native peer and lifetime facts.
1358
+ * @returns Verified reader and invalidation, or denial.
1359
+ */
1360
+ async authenticate(request): Promise<ReaderAdmissionResult | undefined> {
1361
+ const authorization = request.peer.requestHeader.get("authorization");
1362
+ const token = /^Bearer ([^\s,]+)$/i.exec(authorization ?? "")?.[1];
1363
+ if (token === undefined) return undefined;
1364
+ const session = await sessions.resolve(
1365
+ { kind: "bearer", value: token },
1366
+ request.signal,
1367
+ );
1368
+ if (session?.invalidated === undefined) return undefined;
1369
+ return { reader: { principal: session.principal }, invalidated: session.invalidated };
1370
+ },
1371
+ },
1372
+ },
1373
+ },
1374
+ });
1375
+ }
1376
+ ```
1377
+
1378
+ The admitted logout action checks `await sessions.logout(credential)` on that
1379
+ same strategy. It reports durable success only for `revoked`; `unavailable`
1380
+ and `expiryOnly` remain distinct. Application shutdown closes both Server and
1381
+ strategy. A custom authenticator still enforces its own credential and policy
1382
+ checks; a token in a header alone does not establish authority.
1383
+
1384
+ Authenticated forwarding now requires an issuer; configure both gateway and
1385
+ receiving node when updating an existing authenticated deployment. The native
1386
+ Server root still does not load the optional auth package. The forwarding ports
1387
+ do not make a JVM endpoint understand the TS internal reader assertion.
1388
+
1389
+ ### Browser hosting
1390
+
991
1391
  For `BrowserServer` from `@spine-event-engine/server/browser`, origins must be unique canonical HTTP(S) origins with no path, query,
992
1392
  fragment, or trailing slash. Requests without an allowed exact `Origin` receive
993
1393
  403 before RPC handling. Allowed responses include credentialed exact-origin
994
1394
  CORS, `Vary: Origin`, protocol request headers, and exposed gRPC status headers.
1395
+ RPC and auth-route preflight completes before credential extraction. The host
1396
+ allows configured CSRF headers alongside Connect and gRPC-Web protocol headers;
1397
+ actual responses, including permitted-origin terminal errors, retain exact-origin
1398
+ credentialed CORS. Callback responses cannot replace that host policy with a
1399
+ wildcard origin.
995
1400
 
996
1401
  Supplying `browser.backend.baseUrl` selects standalone mode. It is one canonical
997
1402
  HTTP(S) origin for a Spine TS or JVM backend; the gateway never closes
@@ -1011,7 +1416,8 @@ re-query authoritative state after duplicate updates or generic loss notices.
1011
1416
  Each registration has one exact `GET` or `POST` canonical path, one method per
1012
1417
  path, exact canonical origins, finite request bytes, and a finite timeout. A
1013
1418
  missing Origin is rejected unless that route explicitly permits it for an OAuth
1014
- callback. Unknown paths, method/origin failures, body overflow, timeout, and
1419
+ callback and validates the protocol state in its handler.
1420
+ Unknown paths, method/origin failures, body overflow, timeout, and
1015
1421
  handler failure return fixed 404, 405, 403, 413, 504, and 500 responses without
1016
1422
  calling application code on rejected input. The 404 unknown-path result applies
1017
1423
  only after framework Origin admission. Auth admission is bounded to 64 active
@@ -1022,10 +1428,18 @@ transfer. Response bodies use `writeMaxBytes`; overflow returns 413 before any
1022
1428
  application status or headers are committed. Listener close ultimately aborts
1023
1429
  an active request through its disconnect signal.
1024
1430
 
1025
- With `cookies`, `OpaqueSessionCookies` performs strict bearer-first or
1026
- CSRF-protected cookie extraction. A strict rejection becomes an unusable
1027
- credential and never falls back to permissive header parsing. Without cookies,
1028
- the host accepts exactly one non-whitespace bearer token. Credentials and raw
1431
+ Every credential reader and auth route declares CSRF header names, primary
1432
+ credential-cookie names, and supporting state-cookie names. The host snapshots
1433
+ these declarations before startup, rejects repeated raw Authorization, Origin,
1434
+ configured CSRF headers, and selected cookies, and checks simultaneous primary
1435
+ credentials only for the active reader or selected route. Legal split Cookie
1436
+ fields and repeated unrelated cookies remain available to the reader.
1437
+ `OpaqueSessionCookies` provides a strict reader for a sealed opaque session or
1438
+ bearer credential, including CSRF checks; a primary cookie and bearer field
1439
+ together are rejected. The legacy `cookies` option cannot be combined with
1440
+ `credentials`. Without either, the host reads one valid Bearer field. A reader
1441
+ returns a credential, absence, or terminal rejection; a thrown read is terminal
1442
+ unavailability. Credentials and raw
1029
1443
  transport metadata do not reach the private backend. The application-supplied
1030
1444
  context resolver replaces caller actor and tenant facts before forwarding.
1031
1445
 
@@ -1050,7 +1464,11 @@ never returns private native data through public subscription responses.
1050
1464
  The durable registry preserves one approved `GatewayAuthenticatedSubscription`
1051
1465
  per public subscription through a process restart. It validates the record ID,
1052
1466
  full Subscription, and expiry before use. The trusted Actor and Tenant remain
1053
- inside the stored Topic and are checked for Activate and Cancel. Create assigns
1467
+ inside the stored Topic; a stable principal ID is stored beside it. Activate and
1468
+ Cancel check the newly admitted reader and Actor/Tenant before stream effects
1469
+ or record mutation. Changed authority facts do not change the principal ID.
1470
+ An explicit anonymous binding remains distinct from missing legacy identity;
1471
+ records with missing identity cannot be claimed by a caller. Create assigns
1054
1472
  the public ID directly; no quota, reservation, fingerprint, or lease is
1055
1473
  persisted. Cleanup remains bounded and restart-safe at the record level. It
1056
1474
  cleans the backend definition before deleting its record. It is not a cleaner
@@ -1064,11 +1482,21 @@ delivery, global update ordering, nor cluster-complete notification delivery.
1064
1482
  local durable-row abstraction. Inbox and shard storage require a single-tenant
1065
1483
  Delivery storage context shared by all tenants; the packed signal supplies
1066
1484
  the dispatch tenant. Read limits are 1–1000; work leases are
1067
- 1000–2147483647 ms. Framework replay supports command handling, projection
1068
- updates, and process-manager reactions. Callbacks are at-least-once/replay-safe:
1485
+ 1000–2147483647 ms. Framework delivery supports command handling, projection
1486
+ updates, process-manager reactions, and Aggregate Event reactions. Callbacks must tolerate redelivery:
1069
1487
  lost renewal can prevent stale finalization but cannot undo a callback already
1070
1488
  run. The package exposes no general raw worker callback API.
1071
1489
 
1490
+ Repository-backed Command and Event routing awaits recipient Inbox writes and
1491
+ notifies Delivery immediately. A successful post through this routing means
1492
+ recipient Inbox writes completed; it does not wait for handler execution or row
1493
+ acknowledgement. Restart durability depends on the configured storage provider.
1494
+ Custom dispatchers define their own completion contract. After an Entity commit, the
1495
+ source delivery awaits one outgoing bus handoff attempt through recipient Inbox
1496
+ acceptance. A failed publication is reported and contained without re-executing
1497
+ a successful source Command. Entity storage, recipient Inbox writes, and source
1498
+ acknowledgement remain separate operations.
1499
+
1072
1500
  Inbox reads return `InboxMessage` with a full-precision Protobuf `Timestamp` in
1073
1501
  `whenReceived`. `InboxMessageInput` requires a precise `Timestamp` for new
1074
1502
  receipts. Writes, acknowledgements, and exact removals use an `InboxMessage`
@@ -1124,7 +1552,8 @@ internally and persist their envelope in the target Entity Inbox before any
1124
1552
  handler runs. Projection input uses the same environment Delivery and shared
1125
1553
  Inbox, with its separate recipient identity.
1126
1554
 
1127
- Local posting waits for the result of that signal's delivery attempt. For an
1555
+ Local repository-backed posting waits for recipient Inbox writes and scheduling,
1556
+ while handler execution and row acknowledgement continue through Delivery. For an
1128
1557
  Agent, successful acceptance and Inbox acknowledgement let posting finish while
1129
1558
  the live handler continues. If Agent execution capacity is unavailable, the input
1130
1559
  stays pending and the local call reports the existing command-posting error.
@@ -1170,9 +1599,9 @@ scan delivers nothing; it does not restart after each productive page.
1170
1599
  Delivery compares pending rows with delivered rows in the current page
1171
1600
  and a process-local cache of the 1,000 most recent deliveries, then removes
1172
1601
  duplicates. The 30-second Inbox deduplication window controls how long delivered
1173
- rows remain available as duplicate evidence; it is not a replay-retention
1602
+ rows remain available as duplicate evidence; it is not a delivery-retention
1174
1603
  period. Persisted accepted rows follow their delivery lifecycle and may be
1175
- replayed according to that lifecycle after the deduplication window has elapsed.
1604
+ redelivered according to that lifecycle after the deduplication window has elapsed.
1176
1605
 
1177
1606
  Direct delivery uses the configured Inbox storage clock for both retained-row
1178
1607
  duplicate checks and early cleanup filtering. Remote delivery uses local client
@@ -1193,8 +1622,8 @@ reception monitor. A custom reception action that completes normally completes
1193
1622
  delivery and the enclosing stage acknowledges the row. An action that rejects
1194
1623
  propagates its error and leaves the row `TO_DELIVER`. `markDelivered()` does not
1195
1624
  mean domain processing succeeded.
1196
- Managed repository replay sends actual handler failures to the monitor while
1197
- storage and replay infrastructure errors propagate. A direct `onMessage` callback
1625
+ Managed repository delivery sends actual handler failures to the monitor while
1626
+ storage and delivery infrastructure errors propagate. A direct `onMessage` callback
1198
1627
  is treated as the endpoint. Handler effects and acknowledgement are separate,
1199
1628
  so a crash between them may redeliver.
1200
1629
  The framework adds no persisted attempts, quarantine, dead-letter, receipt, or