@spine-event-engine/server 2.0.0-snapshot.26 → 2.0.0-snapshot.29

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 (131) hide show
  1. package/README.md +61 -8
  2. package/REFERENCE.md +267 -45
  3. package/dist/agent/agent-live-ai-runtime.d.ts +1 -1
  4. package/dist/agent/agent-live-ai-runtime.js +1 -1
  5. package/dist/agent/agent-live-budget.d.ts +1 -1
  6. package/dist/agent/agent-live-budget.js +1 -1
  7. package/dist/agent/agent-live-read-runtime.d.ts +1 -1
  8. package/dist/agent/agent-live-read-runtime.js +1 -1
  9. package/dist/browser/browser-admission.d.ts +47 -0
  10. package/dist/browser/browser-admission.d.ts.map +1 -0
  11. package/dist/browser/browser-admission.js +200 -0
  12. package/dist/browser/browser-admission.js.map +1 -0
  13. package/dist/browser/browser-server.d.ts +14 -7
  14. package/dist/browser/browser-server.d.ts.map +1 -1
  15. package/dist/browser/browser-server.js +462 -129
  16. package/dist/browser/browser-server.js.map +1 -1
  17. package/dist/browser/index.d.ts +1 -1
  18. package/dist/browser/index.d.ts.map +1 -1
  19. package/dist/browser/options.d.ts +48 -2
  20. package/dist/browser/options.d.ts.map +1 -1
  21. package/dist/bus/command-bus.d.ts +6 -1
  22. package/dist/bus/command-bus.d.ts.map +1 -1
  23. package/dist/bus/command-bus.js +6 -1
  24. package/dist/bus/command-bus.js.map +1 -1
  25. package/dist/bus/event-bus.d.ts +6 -1
  26. package/dist/bus/event-bus.d.ts.map +1 -1
  27. package/dist/bus/event-bus.js +6 -1
  28. package/dist/bus/event-bus.js.map +1 -1
  29. package/dist/context/bounded-context.d.ts +30 -28
  30. package/dist/context/bounded-context.d.ts.map +1 -1
  31. package/dist/context/bounded-context.js +137 -83
  32. package/dist/context/bounded-context.js.map +1 -1
  33. package/dist/context/entity-inbox.d.ts +12 -17
  34. package/dist/context/entity-inbox.d.ts.map +1 -1
  35. package/dist/context/entity-inbox.js +106 -120
  36. package/dist/context/entity-inbox.js.map +1 -1
  37. package/dist/context/local-inbox-handoff.d.ts +13 -13
  38. package/dist/context/local-inbox-handoff.d.ts.map +1 -1
  39. package/dist/context/local-inbox-handoff.js +60 -32
  40. package/dist/context/local-inbox-handoff.js.map +1 -1
  41. package/dist/context/persistent-tenant-index.d.ts +81 -0
  42. package/dist/context/persistent-tenant-index.d.ts.map +1 -0
  43. package/dist/context/persistent-tenant-index.js +270 -0
  44. package/dist/context/persistent-tenant-index.js.map +1 -0
  45. package/dist/context/projection-handoff.d.ts +13 -11
  46. package/dist/context/projection-handoff.d.ts.map +1 -1
  47. package/dist/context/projection-handoff.js +77 -34
  48. package/dist/context/projection-handoff.js.map +1 -1
  49. package/dist/context/tenant-index.d.ts +7 -6
  50. package/dist/context/tenant-index.d.ts.map +1 -1
  51. package/dist/context/tenant-index.js +2 -188
  52. package/dist/context/tenant-index.js.map +1 -1
  53. package/dist/context/tenant-unknown-error.d.ts +2 -2
  54. package/dist/context/tenant-unknown-error.d.ts.map +1 -1
  55. package/dist/context/tenant-unknown-error.js +4 -1
  56. package/dist/context/tenant-unknown-error.js.map +1 -1
  57. package/dist/delivery/delivery-builder.d.ts +8 -7
  58. package/dist/delivery/delivery-builder.d.ts.map +1 -1
  59. package/dist/delivery/delivery-builder.js +2 -2
  60. package/dist/delivery/delivery-builder.js.map +1 -1
  61. package/dist/delivery/delivery-ports.d.ts +2 -2
  62. package/dist/delivery/delivery-supervisor.d.ts.map +1 -1
  63. package/dist/delivery/delivery-supervisor.js +12 -3
  64. package/dist/delivery/delivery-supervisor.js.map +1 -1
  65. package/dist/delivery/delivery.d.ts +4 -3
  66. package/dist/delivery/delivery.d.ts.map +1 -1
  67. package/dist/delivery/delivery.js +87 -38
  68. package/dist/delivery/delivery.js.map +1 -1
  69. package/dist/delivery/inbox-retention.d.ts +7 -0
  70. package/dist/delivery/inbox-retention.d.ts.map +1 -0
  71. package/dist/delivery/inbox-retention.js +29 -0
  72. package/dist/delivery/inbox-retention.js.map +1 -0
  73. package/dist/delivery/inbox-storage.d.ts +2 -2
  74. package/dist/delivery/inbox-storage.d.ts.map +1 -1
  75. package/dist/delivery/inbox-storage.js +32 -8
  76. package/dist/delivery/inbox-storage.js.map +1 -1
  77. package/dist/delivery/inbox.d.ts +5 -9
  78. package/dist/delivery/inbox.d.ts.map +1 -1
  79. package/dist/delivery/inbox.js +4 -6
  80. package/dist/delivery/inbox.js.map +1 -1
  81. package/dist/delivery/reception-observation.d.ts +3 -3
  82. package/dist/delivery/reception-observation.js +2 -2
  83. package/dist/index.d.ts +4 -1
  84. package/dist/index.d.ts.map +1 -1
  85. package/dist/index.js +2 -0
  86. package/dist/index.js.map +1 -1
  87. package/dist/repository/command-routing.d.ts +1 -1
  88. package/dist/repository/command-routing.js +1 -1
  89. package/dist/repository/event-routing.d.ts +1 -1
  90. package/dist/repository/event-routing.js +1 -1
  91. package/dist/repository/repository.d.ts +25 -33
  92. package/dist/repository/repository.d.ts.map +1 -1
  93. package/dist/repository/repository.js +245 -230
  94. package/dist/repository/repository.js.map +1 -1
  95. package/dist/repository/state-update-routing.d.ts +1 -1
  96. package/dist/repository/state-update-routing.js +1 -1
  97. package/dist/runtime/signal-publisher.d.ts +23 -8
  98. package/dist/runtime/signal-publisher.d.ts.map +1 -1
  99. package/dist/runtime/signal-publisher.js +11 -8
  100. package/dist/runtime/signal-publisher.js.map +1 -1
  101. package/dist/server/node-coordinator.d.ts.map +1 -1
  102. package/dist/server/node-coordinator.js +220 -54
  103. package/dist/server/node-coordinator.js.map +1 -1
  104. package/dist/server/server-environment.d.ts +6 -5
  105. package/dist/server/server-environment.d.ts.map +1 -1
  106. package/dist/server/server-environment.js +13 -9
  107. package/dist/server/server-environment.js.map +1 -1
  108. package/dist/server/server.js +1 -0
  109. package/dist/server/server.js.map +1 -1
  110. package/dist/services/app-query-execution.js +2 -2
  111. package/dist/services/app-query-execution.js.map +1 -1
  112. package/dist/services/native-operation.d.ts +34 -0
  113. package/dist/services/native-operation.d.ts.map +1 -0
  114. package/dist/services/native-operation.js +77 -0
  115. package/dist/services/native-operation.js.map +1 -0
  116. package/dist/services/spine-services.d.ts +6 -0
  117. package/dist/services/spine-services.d.ts.map +1 -1
  118. package/dist/services/spine-services.js +128 -30
  119. package/dist/services/spine-services.js.map +1 -1
  120. package/dist/services/trusted-reader-ingress.d.ts +23 -5
  121. package/dist/services/trusted-reader-ingress.d.ts.map +1 -1
  122. package/dist/services/trusted-reader-ingress.js +18 -5
  123. package/dist/services/trusted-reader-ingress.js.map +1 -1
  124. package/dist/spi/delivery.d.ts +1 -0
  125. package/dist/spi/delivery.d.ts.map +1 -1
  126. package/dist/spi/delivery.js +1 -0
  127. package/dist/spi/delivery.js.map +1 -1
  128. package/dist/stand/stand.d.ts +1 -1
  129. package/dist/stand/stand.js +1 -1
  130. package/dist/tsconfig.tsbuildinfo +1 -1
  131. package/package.json +16 -14
package/README.md CHANGED
@@ -44,7 +44,6 @@ Add generated entities with `BoundedContext.add(...)` and use
44
44
  `build()` to assemble the context and load generated repositories when configured. A
45
45
  context is single-tenant or multitenant by construction; select that mode
46
46
  before registering application handlers and storage.
47
-
48
47
  `build()` prepares the current model storage and awaits the first subscription
49
48
  registry pass. Set `createStorageStructures: false` or
50
49
  `startSubscriptionMonitoring: false` in `build({...})` to defer either phase
@@ -112,6 +111,20 @@ A failed handler leaves previous persisted state unchanged and
112
111
  suppresses produced output. An external HTTP request or other side effect may
113
112
  already have happened.
114
113
 
114
+ Repository-backed Command and Event posting resolves after recipient Inbox
115
+ writes. Delivery starts immediately and handles the stored row separately, so
116
+ successful posting does not mean the handler has completed. Restart durability
117
+ depends on the configured storage provider; custom dispatchers define their own
118
+ completion contract. A committed Entity awaits one outgoing handoff attempt for
119
+ handler-produced domain Commands and Events. A failed attempt is reported without
120
+ rerunning successful source handling. Internal diagnostic publication remains
121
+ best effort.
122
+
123
+ New Inbox rows have no completion deadline. After a recipient is handled, the
124
+ first acknowledgement marks its row delivered and keeps that row for five
125
+ seconds as duplicate evidence. A pending row does not expire during a slow
126
+ handler. Cleanup can remove a delivered row once its deadline has passed.
127
+
115
128
  Handler return declarations can use native unions for one of several results,
116
129
  or fixed tuples for several ordered results. For example,
117
130
  `CreateAccessGrant | ExtendAccessGrant` selects one Command, while
@@ -232,14 +245,36 @@ override. Single-tenant execution uses `SINGLE_TENANT`, even when the request
232
245
  omits a tenant field. A named tenant cannot query a single-tenant context: that
233
246
  fails before reading. See the [query contract](REFERENCE.md#handler-routing-and-operations)
234
247
  for the complete tenant rules and empty-result behavior.
235
- For a multitenant receiving context, explicitly register each accepted tenant
236
- through `context.tenantIndex.keep(tenantId)` or the configured directory.
237
- Provider-backed indexes may only confirm existing membership: fixed SQL catalogs
238
- cannot add tenants, and the Datastore index requires native namespace metadata.
239
- Use an application-managed `TenantIndex` to register tenants before provider
240
- data exists or to add tenants dynamically.
248
+ For a multitenant receiving Bounded Context, supply an application tenant directory and
249
+ register each accepted tenant through its `keep(tenantId)` method before sending
250
+ tenant-scoped work. The optional environment setting defaults to `SINGLE_TENANT`;
251
+ that mode creates no directory or provider catalog. A multitenant Bounded Context
252
+ requires an environment directory before assembly, even when `withTenantIndex()`
253
+ selects another directory for that Bounded Context. In one environment, a
254
+ single-tenant Identity Bounded Context still uses `SINGLE_TENANT` while a
255
+ multitenant Orders Bounded Context uses the configured directory.
256
+ Both public `tenantIndex` getters return `TenantIndexSetting`, so narrow against
257
+ `SINGLE_TENANT` before calling directory methods. A single-tenant Bounded Context always
258
+ returns the marker, even when its environment has a directory for other Bounded Contexts.
259
+ Provider catalogs remain available as storage-provider APIs, but environment
260
+ assembly does not infer membership from them.
261
+ `PersistentTenantIndex.create({ storageFactory, group })` stores registrations
262
+ through the selected factory. Remote providers can retain them across process
263
+ restarts; memory storage remains process-local. Its async factory prepares supported structures by
264
+ default; set `createMissingStructures: false` when the application prepares
265
+ the selected record family separately before data access. Close the index after its admitted work
266
+ finishes; it leaves the supplied storage factory open.
241
267
  Unknown tenants fail before reads or handlers run; input never registers them.
242
268
 
269
+ The [Orders application](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/orders/README.md)
270
+ demonstrates this setup with memory, PostgreSQL, and Datastore. Its PostgreSQL
271
+ launch uses separate shared, Northwind, and Contoso databases, validates persisted
272
+ membership against configured routing, and prepares supported structures before
273
+ opening the listener. Orders focuses on tenant storage and has no sign-in UI.
274
+ The [Fieldnotes application](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/fieldnotes/README.md)
275
+ demonstrates browser authentication and private data in a single-tenant Bounded
276
+ Context without a tenant index.
277
+
243
278
  Use `EntityQuery.all(...)` when every predicate must match and
244
279
  `EntityQuery.either(...)` when any branch may match.
245
280
 
@@ -411,6 +446,21 @@ and backend forwarding. Authenticated durable bindings are browser-host
411
446
  infrastructure, not native `Server` state. See the [browser/auth extension guide](https://github.com/SpineEventEngine/spine-ts/blob/master/docs/BROWSER_CLIENT_AUTH_EXTENSION_GUIDE.md)
412
447
  before exposing a backend to a browser.
413
448
 
449
+ For a standalone Gateway, call `BrowserServer.run` with a canonical
450
+ `backend.baseUrl` (or discovery), `origins`, `sessions`, `authorize`, `contexts`,
451
+ `clock`, and explicit `bindings`. Supply `credentials` with a
452
+ `BrowserCredentialReader` to select a browser auth engine. The reader and each
453
+ `authRoutes` entry declare `csrfHeaderNames`, `credentialCookieNames`, and
454
+ `stateCookieNames`; empty arrays state that a category is unused. The Gateway
455
+ validates those names before it starts listening, then checks raw headers before
456
+ calling the reader or a route. Preflight never calls the credential reader;
457
+ permitted origins receive credentialed CORS headers covering the configured CSRF
458
+ fields and Connect protocol headers. Connect stops waiting for a reader and
459
+ does not forward its late result when the request is cancelled or its deadline
460
+ passes. The reader receives no abort signal, so its underlying work may continue.
461
+ This setup does not construct a
462
+ `ServerEnvironment` or Bounded Context. Close the returned server at shutdown.
463
+
414
464
  ## Native reader admission
415
465
 
416
466
  Native authentication is configured through `ServerOptions.services.readerAdmission`.
@@ -423,7 +473,10 @@ public and supplies no authenticated principal or authority facts.
423
473
 
424
474
  Subscription activation and cancellation also check the admitted principal
425
475
  against the stored reader binding. A different principal cannot reuse a
426
- subscription even with the same ActorContext. See [native reader admission](REFERENCE.md#native-reader-admission)
476
+ subscription even with the same ActorContext. The host supplies an operation
477
+ signal before authentication and closes it after unary work or the complete
478
+ Activate stream. A direct authenticator can return a separately captured
479
+ `invalidated` signal for local logout and session expiry. See [native reader admission](REFERENCE.md#native-reader-admission)
427
480
  for adapter signatures, public admission, and managed application configuration.
428
481
 
429
482
  ## Limits
package/REFERENCE.md CHANGED
@@ -18,6 +18,11 @@ Delivery adapters import `conditionalPickUp` from
18
18
  conditional-pickup capability for a `DeliveryWorkRegistry`; it is not part of
19
19
  the server root API and does not independently create delivery work.
20
20
 
21
+ The same SPI exports `InboxRetention.deadline(nowMs)`. Delivery adapters use it
22
+ when marking a row delivered; it returns a `Date` five seconds after the supplied
23
+ application time in milliseconds. Local storage and remote delivery use this
24
+ one deadline policy without adding a public retention setting.
25
+
21
26
  ## Generated handler-registry SPI
22
27
 
23
28
  Generated model modules import only their registry data types from
@@ -168,7 +173,7 @@ phase without implying that every Entity model has been prepared.
168
173
  awaits both model preparation and the first subscription reconciliation by
169
174
  default. Each option can be set to `false` independently. A deferred phase runs
170
175
  before the first valid Command, Event, query, subscription operation, or Inbox
171
- replay for the receiving context; lazy multitenant preparation selects only that
176
+ delivery for the receiving context; lazy multitenant preparation selects only that
172
177
  input's currently registered tenant. A failed lazy attempt rejects its current
173
178
  input and can be retried by a later input. Monitoring requires the shared
174
179
  subscription registry, so `startSubscriptionMonitoring: true` prepares that
@@ -208,20 +213,33 @@ contexts share one in-memory factory, including in production. Local and test
208
213
  environments default storage in memory and use the core registry only as a
209
214
  fallback; those defaults are not production configuration.
210
215
 
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.
216
+ `tenantIndex` is optional and defaults to `SINGLE_TENANT`, a marker that creates
217
+ no directory and never consults a provider catalog. The environment getter
218
+ returns `TenantIndexSetting`, either that marker or an application-supplied
219
+ `TenantIndex`. Narrow against the marker before calling `contains()`, `keep()`,
220
+ `page()`, or `all()`. A multitenant Bounded Context requires a real environment
221
+ index before business storage opens. A single-tenant Bounded Context always returns the
222
+ marker, including inside a multitenant environment. Provider catalogs remain
223
+ separate storage-provider APIs and do not select application membership.
224
+
225
+ `PersistentTenantIndex.create({ storageFactory, group })` opens a separate,
226
+ storage-backed registration directory without constructing a Bounded Context or
227
+ `ServerEnvironment`. It creates and checks supported storage structures before
228
+ returning. Set `createMissingStructures: false` to skip preparation on creation
229
+ and on later reads or writes. A skipped directory needs structures prepared by
230
+ the application before data access; missing structures cause provider errors.
231
+ Memory storage remains process-local. The injected factory remains usable after the
232
+ index closes. `keep()` registers a complete `TenantId` atomically and retains
233
+ the first registration time; `contains()` reads its primary key. `page()` uses
234
+ finite keyset traversal with an abort signal and a continuation accepted only
235
+ by that index instance. `all()` traverses the complete current directory, so
236
+ its result can be large. Traversal does not promise a snapshot while other
237
+ writers register tenants. Registration does not provision tenant databases or
238
+ authorize users. The environment defaults to the explicit `SINGLE_TENANT` marker.
221
239
 
222
240
  Multitenant receiving contexts check current index membership before Command,
223
241
  Event, query, direct repository/history, and subscription work. Inbox persistence
224
- and later replay check separately, so removing a tenant prevents saved work from
242
+ and later delivery check separately, so removing a tenant prevents saved work from
225
243
  restoring it. Input does not call `keep()` implicitly. A confirmed missing member
226
244
  raises `TenantUnknownError` with the complete tenant ID and context name; native
227
245
  services return `NotFound` with a structured `spine.base.Error` detail whose type
@@ -249,8 +267,8 @@ Direct `repository.routeCommand()` and `repository.routeEvent()` calls return
249
267
  Promises and must be awaited. Commands select exactly one ID; Events and state
250
268
  updates may select any number of distinct IDs. After validation and stable
251
269
  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
270
+ recipient. Inbox delivery uses stored target IDs without rerouting. The local
271
+ `catchUpReadSide()` helper is unrelated to retry: it resets and dispatches stored Events through
254
272
  the whole local read side, and is not Projection catch-up.
255
273
 
256
274
  ## Message-valued Entity IDs
@@ -282,7 +300,7 @@ const id: MessageId = create(CommandIdSchema, { uuid: "shipment-7" });
282
300
  ```
283
301
 
284
302
  Admission validates a message ID against the schema declared by the state
285
- field, including generated validation rules, before it is stored or replayed.
303
+ field, including generated validation rules, before it is stored or delivered.
286
304
  When default routing finds a message but the Entity ID type is primitive, a
287
305
  custom route returns the intended primitive ID.
288
306
 
@@ -596,9 +614,9 @@ repeated declarations fail closed. It is not an alternative routing mechanism.
596
614
  Configure logging through `ServerEnvironment` when assembling the server. The
597
615
  server installs that logger into its runtime components, where warnings and
598
616
  failures carry operation context. Durable facts belong in events or storage.
599
- Ordinary Aggregate, Process Manager, and Projection Inbox replay sends persisted
600
- accepted work through the normal handler path, so those handler effects must be
601
- replay-safe. An accepted Agent interaction is recorded by its receipt and is
617
+ Ordinary Aggregate, Process Manager, and Projection Inbox delivery sends persisted
618
+ accepted work through the normal handler path. Handler effects must tolerate
619
+ redelivery after an uncertain acknowledgement. An accepted Agent interaction is recorded by its receipt and is
602
620
  never re-executed after interruption.
603
621
 
604
622
  ## Context assembly and entities
@@ -607,9 +625,11 @@ Create a context with `BoundedContext.singleTenant(name)` or
607
625
  `BoundedContext.multitenant(name)`. Names are immutable. Use
608
626
  `ServerEnvironment.when(type).use({ storageFactory: factory })` before explicit
609
627
  environment setup to supply record/event storage to all contexts.
610
- `withTenantIndex(index)` selects a different live membership directory for one
611
- context; `context.tenantIndex` and `ServerEnvironment.instance().tenantIndex`
612
- return the configured objects. Closing a context does not close either index.
628
+ `withTenantIndex(index)` selects a different live membership directory for a
629
+ multitenant Bounded Context after its environment has a real index. The Bounded Context getter
630
+ returns its captured selection; single-tenant Bounded Contexts return `SINGLE_TENANT`.
631
+ Both getters have type `TenantIndexSetting` and require narrowing before index
632
+ methods. Closing a Bounded Context does not close the configured directory.
613
633
  The application imports its generated `spine-bootstrap.js` before registering
614
634
  classes with `add(EntityClass)`; `build()` then selects passive descriptions
615
635
  by exact constructor and performs assembly, including
@@ -911,6 +931,12 @@ work to stop using dependencies, then closes contexts. Caller-managed `start()`
911
931
  does not close global process facilities; close `ServerEnvironment.instance()`
912
932
  separately when used. The final run-managed `run()` close closes its environment.
913
933
 
934
+ Native QueryService accepts `BytesValue` equality and range operands for annotated scalar `bytes`
935
+ columns and permits ascending or descending byte ordering. The shared evaluator uses unsigned
936
+ lexicographic comparison, including shorter equal prefixes first. This TypeScript query capability
937
+ does not assert JVM JDBC byte-range compatibility. `QUERY_SERVICE_MAX_RESULTS` from core is the
938
+ current capacity of 1,000 returned and evaluated records for one native read, not a collection-size limit.
939
+
914
940
  `ServerOptions.appQueries` selects concrete generated `AppQueryHandler<Q, R>`
915
941
  instances by exact constructor metadata before listening. Duplicate Q routes
916
942
  or missing metadata fail startup. On direct app-node ingress,
@@ -1110,8 +1136,8 @@ export const scopedOnCreate =
1110
1136
  ```
1111
1137
 
1112
1138
  `Stand` registers entity state for point/list reads, updates current state, and
1113
- offers in-process subscriptions. The legacy-named local reset/replay helper
1114
- `catchUpReadSide(options?)` clears every registered Projection state and replays
1139
+ offers in-process subscriptions. The local read-side reset helper
1140
+ `catchUpReadSide(options?)` clears every registered Projection state and dispatches
1115
1141
  the whole stored event history through matching Projection subscribers; it does
1116
1142
  not append events. Single-tenant contexts reject a tenant option; multitenant
1117
1143
  contexts require one. It cannot target a Projection repository, entity IDs, or
@@ -1263,11 +1289,14 @@ transport responses independently.
1263
1289
 
1264
1290
  Configure app-node admission at `ServerOptions.services.readerAdmission`:
1265
1291
 
1266
- - `direct.authenticate({ service, method, value, headers, peer })` authenticates
1267
- a direct native request. It returns verified `{ principal, authorityFacts }`,
1268
- an empty object for deliberate public access, or `undefined` to deny access.
1292
+ - `direct.authenticate({ service, method, value, headers, peer, signal })` authenticates
1293
+ a direct native request. It returns `{ reader, invalidated? }` with verified
1294
+ `{ principal, authorityFacts }` in `reader`, `{ reader: {} }` for deliberate
1295
+ public access, or `undefined` to deny access. `signal` begins before
1296
+ authentication and ends on terminal unary or complete stream exit.
1269
1297
  The request value contains canonical Proto bytes; `peer` is the Connect
1270
- `HandlerContext` for that invocation.
1298
+ `HandlerContext` for that invocation. `invalidated` is a separate local
1299
+ AbortSignal captured by the host; it never enters reader facts or proofs.
1271
1300
  - `forwarded.verify(bytes, proof, peer)` verifies an assertion from a trusted
1272
1301
  forwarding peer. It must authenticate that peer and validate the supplied
1273
1302
  proof over the exact bytes. `BrowserServerOptions.forwardingProof.issue(bytes)`
@@ -1294,6 +1323,69 @@ child Servers with their verifier as well. Each hop verifies its incoming
1294
1323
  assertion and issues a new proof for the actual outgoing operation and payload.
1295
1324
  Loopback placement alone does not authenticate a forwarding peer.
1296
1325
 
1326
+ A standard session strategy accepts the host signal during `resolve` and
1327
+ returns an invalidation signal for local logout, its captured session deadline,
1328
+ and strategy close. A custom direct authenticator may omit invalidation, but
1329
+ then the host cannot stop an active stream on local logout or expiry. A
1330
+ successful unary request releases its operation even while the transport peer
1331
+ stays connected. Subscribe releases its operation after definition creation;
1332
+ Activate authenticates again and holds its operation through ACK and every
1333
+ update. Normal completion, host handler iterator return, cancellation, and host
1334
+ close stop the operation. Raw Connect clients must abort an explicit
1335
+ `AbortSignal` or call `SubscriptionService.Cancel` to stop remote stream work;
1336
+ returning from the client iterator alone does not signal transport cancellation.
1337
+ An active stream in another process receives no local signal.
1338
+
1339
+ ```ts
1340
+ import type { SignedSessions } from "@spine-event-engine/auth";
1341
+ import { Server, type ReaderAdmissionResult, type ServerOptions } from "@spine-event-engine/server";
1342
+
1343
+ /**
1344
+ * Configures direct native bearer admission with the local session strategy.
1345
+ *
1346
+ * @param sessions Strategy also used by the application's logout action.
1347
+ * @param contexts Application Bounded Contexts exposed by this Server.
1348
+ * @returns Server configured for native authentication.
1349
+ */
1350
+ function nativeServer(
1351
+ sessions: SignedSessions,
1352
+ contexts: NonNullable<ServerOptions["contexts"]>,
1353
+ ): Server {
1354
+ return new Server({
1355
+ contexts,
1356
+ services: {
1357
+ readerAdmission: {
1358
+ direct: {
1359
+ /**
1360
+ * Resolves the bearer credential within the host operation.
1361
+ *
1362
+ * @param request Native peer and lifetime facts.
1363
+ * @returns Verified reader and invalidation, or denial.
1364
+ */
1365
+ async authenticate(request): Promise<ReaderAdmissionResult | undefined> {
1366
+ const authorization = request.peer.requestHeader.get("authorization");
1367
+ const token = /^Bearer ([^\s,]+)$/i.exec(authorization ?? "")?.[1];
1368
+ if (token === undefined) return undefined;
1369
+ const session = await sessions.resolve(
1370
+ { kind: "bearer", value: token },
1371
+ request.signal,
1372
+ );
1373
+ if (session?.invalidated === undefined) return undefined;
1374
+ return { reader: { principal: session.principal }, invalidated: session.invalidated };
1375
+ },
1376
+ },
1377
+ },
1378
+ },
1379
+ });
1380
+ }
1381
+ ```
1382
+
1383
+ The admitted logout action checks `await sessions.logout(credential)` on that
1384
+ same strategy. It reports durable success only for `revoked`; `unavailable`
1385
+ and `expiryOnly` remain distinct. Application shutdown closes both Server and
1386
+ strategy. A custom authenticator still enforces its own credential and policy
1387
+ checks; a token in a header alone does not establish authority.
1388
+
1297
1389
  Authenticated forwarding now requires an issuer; configure both gateway and
1298
1390
  receiving node when updating an existing authenticated deployment. The native
1299
1391
  Server root still does not load the optional auth package. The forwarding ports
@@ -1305,6 +1397,11 @@ For `BrowserServer` from `@spine-event-engine/server/browser`, origins must be u
1305
1397
  fragment, or trailing slash. Requests without an allowed exact `Origin` receive
1306
1398
  403 before RPC handling. Allowed responses include credentialed exact-origin
1307
1399
  CORS, `Vary: Origin`, protocol request headers, and exposed gRPC status headers.
1400
+ RPC and auth-route preflight completes before credential extraction. The host
1401
+ allows configured CSRF headers alongside Connect and gRPC-Web protocol headers;
1402
+ actual responses, including permitted-origin terminal errors, retain exact-origin
1403
+ credentialed CORS. Callback responses cannot replace that host policy with a
1404
+ wildcard origin.
1308
1405
 
1309
1406
  Supplying `browser.backend.baseUrl` selects standalone mode. It is one canonical
1310
1407
  HTTP(S) origin for a Spine TS or JVM backend; the gateway never closes
@@ -1324,7 +1421,8 @@ re-query authoritative state after duplicate updates or generic loss notices.
1324
1421
  Each registration has one exact `GET` or `POST` canonical path, one method per
1325
1422
  path, exact canonical origins, finite request bytes, and a finite timeout. A
1326
1423
  missing Origin is rejected unless that route explicitly permits it for an OAuth
1327
- callback. Unknown paths, method/origin failures, body overflow, timeout, and
1424
+ callback and validates the protocol state in its handler.
1425
+ Unknown paths, method/origin failures, body overflow, timeout, and
1328
1426
  handler failure return fixed 404, 405, 403, 413, 504, and 500 responses without
1329
1427
  calling application code on rejected input. The 404 unknown-path result applies
1330
1428
  only after framework Origin admission. Auth admission is bounded to 64 active
@@ -1335,10 +1433,18 @@ transfer. Response bodies use `writeMaxBytes`; overflow returns 413 before any
1335
1433
  application status or headers are committed. Listener close ultimately aborts
1336
1434
  an active request through its disconnect signal.
1337
1435
 
1338
- With `cookies`, `OpaqueSessionCookies` performs strict bearer-first or
1339
- CSRF-protected cookie extraction. A strict rejection becomes an unusable
1340
- credential and never falls back to permissive header parsing. Without cookies,
1341
- the host accepts exactly one non-whitespace bearer token. Credentials and raw
1436
+ Every credential reader and auth route declares CSRF header names, primary
1437
+ credential-cookie names, and supporting state-cookie names. The host snapshots
1438
+ these declarations before startup, rejects repeated raw Authorization, Origin,
1439
+ configured CSRF headers, and selected cookies, and checks simultaneous primary
1440
+ credentials only for the active reader or selected route. Legal split Cookie
1441
+ fields and repeated unrelated cookies remain available to the reader.
1442
+ `OpaqueSessionCookies` provides a strict reader for a sealed opaque session or
1443
+ bearer credential, including CSRF checks; a primary cookie and bearer field
1444
+ together are rejected. The legacy `cookies` option cannot be combined with
1445
+ `credentials`. Without either, the host reads one valid Bearer field. A reader
1446
+ returns a credential, absence, or terminal rejection; a thrown read is terminal
1447
+ unavailability. Credentials and raw
1342
1448
  transport metadata do not reach the private backend. The application-supplied
1343
1449
  context resolver replaces caller actor and tenant facts before forwarding.
1344
1450
 
@@ -1381,11 +1487,21 @@ delivery, global update ordering, nor cluster-complete notification delivery.
1381
1487
  local durable-row abstraction. Inbox and shard storage require a single-tenant
1382
1488
  Delivery storage context shared by all tenants; the packed signal supplies
1383
1489
  the dispatch tenant. Read limits are 1–1000; work leases are
1384
- 1000–2147483647 ms. Framework replay supports command handling, projection
1385
- updates, and process-manager reactions. Callbacks are at-least-once/replay-safe:
1490
+ 1000–2147483647 ms. Framework delivery supports command handling, projection
1491
+ updates, process-manager reactions, and Aggregate Event reactions. Callbacks must tolerate redelivery:
1386
1492
  lost renewal can prevent stale finalization but cannot undo a callback already
1387
1493
  run. The package exposes no general raw worker callback API.
1388
1494
 
1495
+ Repository-backed Command and Event routing awaits recipient Inbox writes and
1496
+ notifies Delivery immediately. A successful post through this routing means
1497
+ recipient Inbox writes completed; it does not wait for handler execution or row
1498
+ acknowledgement. Restart durability depends on the configured storage provider.
1499
+ Custom dispatchers define their own completion contract. After an Entity commit, the
1500
+ source delivery awaits one outgoing bus handoff attempt through recipient Inbox
1501
+ acceptance. A failed publication is reported and contained without re-executing
1502
+ a successful source Command. Entity storage, recipient Inbox writes, and source
1503
+ acknowledgement remain separate operations.
1504
+
1389
1505
  Inbox reads return `InboxMessage` with a full-precision Protobuf `Timestamp` in
1390
1506
  `whenReceived`. `InboxMessageInput` requires a precise `Timestamp` for new
1391
1507
  receipts. Writes, acknowledgements, and exact removals use an `InboxMessage`
@@ -1402,7 +1518,14 @@ the Entity transaction commit, and before acknowledgement. These checks stop a w
1402
1518
  after it detects that another worker has taken the shard. They do not make
1403
1519
  delivery and handler storage part of one distributed transaction.
1404
1520
 
1405
- `keepUntil` is the optional deduplication-protection deadline, not a second
1521
+ New Inbox receipts have no `keepUntil`. The first successful local acknowledgement
1522
+ stores `DELIVERED` and a deadline five seconds after that transition in one
1523
+ compare-and-set. A repeated acknowledgement returns the first stored
1524
+ deadline. The worker passes that stored completion to delivery observers. This
1525
+ timing applies to Aggregate, Process Manager, and Projection delivery through
1526
+ the local Inbox.
1527
+
1528
+ `keepUntil` is the deduplication-protection deadline on a delivered row, not a second
1406
1529
  retention setting. A delivered row is cleanup-eligible when that deadline is
1407
1530
  absent or elapsed (`keepUntil <= now`). Delivery skips per-record mutation checks
1408
1531
  and deletion attempts for rows whose deadline is still in the future. Cleanup
@@ -1419,7 +1542,7 @@ retryable, non-delivered, and still-protected rows are skipped by cleanup. There
1419
1542
  is no additional retention configuration, timer, or scheduler.
1420
1543
 
1421
1544
  Configure one Delivery strategy for the environment before setup. It maps
1422
- targets to shards across all contexts; the default is one shard. For example:
1545
+ targets to shards across all Bounded Contexts; the default is one shard. For example:
1423
1546
 
1424
1547
  ```ts
1425
1548
  import {
@@ -1436,12 +1559,111 @@ await ServerEnvironment.instance().completeSetup();
1436
1559
  const context = await BoundedContext.singleTenant("Tasks").build();
1437
1560
  ```
1438
1561
 
1439
- Aggregate commands and Process Manager or Agent commands/events derive their target shard
1440
- internally and persist their envelope in the target Entity Inbox before any
1441
- handler runs. Projection input uses the same environment Delivery and shared
1442
- Inbox, with its separate recipient identity.
1562
+ Aggregates, Process Managers, Agents, and Projections derive their Inbox shard
1563
+ from this same strategy. Projection Events and Entity-state subscriptions use
1564
+ the routed Projection ID and Projection state type, just as other receivers use
1565
+ their receiving Entity identity. The framework stores the envelope before
1566
+ invoking the handler. Startup notifications cover every configured shard, and
1567
+ stored rows are checked against the current strategy before dispatch.
1568
+
1569
+ ### Receiver-specific shard policies
1570
+
1571
+ `DeliveryStrategy.shardFor(targetId, targetType)` receives the packed receiver
1572
+ ID and the type URL of its Entity state. `targetType` is neither the signal type
1573
+ nor the TypeScript class name. Routing selects recipients first; the strategy
1574
+ then selects a shard for each routed recipient.
1575
+
1576
+ For example, the Orders application can reserve two of eight shards for its
1577
+ `OrderSummary` and `SalesSummary` Projections. Other receiver types use the first
1578
+ six shards. Place this strategy in the Orders source directory:
1579
+
1580
+ <!-- docs-snippet-path: examples/orders/src/orders-environment.ts -->
1581
+
1582
+ ```ts
1583
+ import type { Any } from "@bufbuild/protobuf/wkt";
1584
+ import { TypeUrls } from "@spine-event-engine/core";
1585
+ import {
1586
+ ShardIndex,
1587
+ UniformAcrossAllShards,
1588
+ type DeliveryStrategy,
1589
+ } from "@spine-event-engine/server";
1590
+ import {
1591
+ OrderSummarySchema,
1592
+ SalesSummarySchema,
1593
+ } from "../generated/spine/examples/orders/read_models_pb.js";
1594
+
1595
+ /**
1596
+ * Reserves two of eight shards for the selected Orders Projections.
1597
+ */
1598
+ class OrdersDeliveryStrategy implements DeliveryStrategy {
1599
+ /**
1600
+ * Number of shards in the shared set.
1601
+ */
1602
+ readonly shardCount = 8;
1603
+
1604
+ readonly #business = UniformAcrossAllShards.forNumber(6);
1605
+
1606
+ readonly #views = UniformAcrossAllShards.forNumber(2);
1607
+
1608
+ readonly #viewTypes = new Set([
1609
+ TypeUrls.derive(OrderSummarySchema),
1610
+ TypeUrls.derive(SalesSummarySchema),
1611
+ ]);
1612
+
1613
+ /**
1614
+ * Selects a shard in the receiver's configured range.
1615
+ *
1616
+ * @param targetId Packed receiving Entity identifier.
1617
+ * @param targetType Receiving Entity state type URL.
1618
+ * @returns A shard in the common eight-shard set.
1619
+ */
1620
+ shardFor(targetId: Any, targetType: string): ShardIndex {
1621
+ if (this.#viewTypes.has(targetType)) {
1622
+ const local = this.#views.shardFor(targetId, targetType);
1623
+ return new ShardIndex(6 + local.index, this.shardCount);
1624
+ }
1625
+ const local = this.#business.shardFor(targetId, targetType);
1626
+ return new ShardIndex(local.index, this.shardCount);
1627
+ }
1628
+ }
1629
+ ```
1630
+
1631
+ Set `deliveryStrategy: new OrdersDeliveryStrategy()` in the existing
1632
+ `ServerEnvironment.when(...).use({...})` call before resolving the environment.
1633
+ For Orders, that call is in `configureOrdersEnvironment()`. Preserve its storage
1634
+ and tenant settings. Select the profile actually used by the process: configuring
1635
+ `Local` does not configure a process whose `NODE_ENV` is `production`.
1636
+
1637
+ The resulting shard sets are:
1638
+
1639
+ ```text
1640
+ Other receivers -> 0/8, 1/8, 2/8, 3/8, 4/8, 5/8
1641
+ OrderSummary and SalesSummary -> 6/8, 7/8
1642
+ ```
1443
1643
 
1444
- Local posting waits for the result of that signal's delivery attempt. For an
1644
+ Only the listed state types use the second range. Add other Projection state
1645
+ types explicitly if they should use it. To place all instances of one receiver
1646
+ type on one shard instead, return a fixed value such as `new ShardIndex(6, 8)`
1647
+ for that state type. All returned totals must equal `shardCount`: `0/1` and `0/8`
1648
+ are distinct shard identities, not aliases.
1649
+
1650
+ Keep the mapping deterministic and identical across application processes
1651
+ sharing delivery storage. Configure it in processes hosting the receivers,
1652
+ before Bounded Context assembly. Shard count is not process count and does not
1653
+ assign a receiver to a particular process. Do not change the policy underneath
1654
+ pending rows, whose stored shard must agree with the configured strategy.
1655
+
1656
+ The built-in uniform strategy hashes the typed ID together with the receiver
1657
+ state type. Equal IDs in different receiver types therefore do not guarantee
1658
+ co-location. A custom strategy can normalize related receiver types to the same
1659
+ hash input when their domain identifier has the same meaning. This does not make
1660
+ their separate signal deliveries transactional. The callback has no tenant ID
1661
+ or Bounded Context name parameter.
1662
+
1663
+ ### Reception and processing
1664
+
1665
+ Local repository-backed posting waits for recipient Inbox writes and scheduling,
1666
+ while handler execution and row acknowledgement continue through Delivery. For an
1445
1667
  Agent, successful acceptance and Inbox acknowledgement let posting finish while
1446
1668
  the live handler continues. If Agent execution capacity is unavailable, the input
1447
1669
  stays pending and the local call reports the existing command-posting error.
@@ -1487,9 +1709,9 @@ scan delivers nothing; it does not restart after each productive page.
1487
1709
  Delivery compares pending rows with delivered rows in the current page
1488
1710
  and a process-local cache of the 1,000 most recent deliveries, then removes
1489
1711
  duplicates. The 30-second Inbox deduplication window controls how long delivered
1490
- rows remain available as duplicate evidence; it is not a replay-retention
1712
+ rows remain available as duplicate evidence; it is not a delivery-retention
1491
1713
  period. Persisted accepted rows follow their delivery lifecycle and may be
1492
- replayed according to that lifecycle after the deduplication window has elapsed.
1714
+ redelivered according to that lifecycle after the deduplication window has elapsed.
1493
1715
 
1494
1716
  Direct delivery uses the configured Inbox storage clock for both retained-row
1495
1717
  duplicate checks and early cleanup filtering. Remote delivery uses local client
@@ -1510,8 +1732,8 @@ reception monitor. A custom reception action that completes normally completes
1510
1732
  delivery and the enclosing stage acknowledges the row. An action that rejects
1511
1733
  propagates its error and leaves the row `TO_DELIVER`. `markDelivered()` does not
1512
1734
  mean domain processing succeeded.
1513
- Managed repository replay sends actual handler failures to the monitor while
1514
- storage and replay infrastructure errors propagate. A direct `onMessage` callback
1735
+ Managed repository delivery sends actual handler failures to the monitor while
1736
+ storage and delivery infrastructure errors propagate. A direct `onMessage` callback
1515
1737
  is treated as the endpoint. Handler effects and acknowledgement are separate,
1516
1738
  so a crash between them may redeliver.
1517
1739
  The framework adds no persisted attempts, quarantine, dead-letter, receipt, or
@@ -280,7 +280,7 @@ export declare class AgentLiveAiRuntime implements AgentAi {
280
280
  */
281
281
  private ticket;
282
282
  /**
283
- * Completes measured response bytes without replay state.
283
+ * Completes measured response bytes without recorded state.
284
284
  *
285
285
  * @param attempts Physical attempt state for this invocation.
286
286
  * @param audit Retained history audit for this invocation.
@@ -578,7 +578,7 @@ export class AgentLiveAiRuntime {
578
578
  return ticket;
579
579
  }
580
580
  /**
581
- * Completes measured response bytes without replay state.
581
+ * Completes measured response bytes without recorded state.
582
582
  *
583
583
  * @param attempts Physical attempt state for this invocation.
584
584
  * @param audit Retained history audit for this invocation.
@@ -13,7 +13,7 @@ export interface AgentPhysicalTicket {
13
13
  readonly maxOutputBytes: number | undefined;
14
14
  }
15
15
  /**
16
- * Counts one live invocation without durable replay or unacknowledged writes.
16
+ * Counts one live invocation without durable restart or unacknowledged writes.
17
17
  */
18
18
  export declare class AgentLiveBudget {
19
19
  #private;
@@ -13,7 +13,7 @@
13
13
  */
14
14
  import { AgentLiveFault } from "./agent-live-fault.js";
15
15
  /**
16
- * Counts one live invocation without durable replay or unacknowledged writes.
16
+ * Counts one live invocation without durable restart or unacknowledged writes.
17
17
  */
18
18
  export class AgentLiveBudget {
19
19
  limits;
@@ -7,7 +7,7 @@ import type { AgentHistoryPage } from "@spine-event-engine/storage/provider";
7
7
  import type { AgentHistoryScope } from "./agent-history.js";
8
8
  import { AgentLiveBudget } from "./agent-live-budget.js";
9
9
  /**
10
- * Sequences live Agent reads without durable snapshots or replay.
10
+ * Sequences live Agent reads without durable snapshots or recorded-operation restart.
11
11
  */
12
12
  export declare class AgentLiveReadRuntime {
13
13
  #private;
@@ -16,7 +16,7 @@ import { TypeUrls } from "@spine-event-engine/core";
16
16
  import { AgentHistoryEntrySchema } from "@spine-event-engine/proto/agent";
17
17
  import { AgentLiveBudget } from "./agent-live-budget.js";
18
18
  /**
19
- * Sequences live Agent reads without durable snapshots or replay.
19
+ * Sequences live Agent reads without durable snapshots or recorded-operation restart.
20
20
  */
21
21
  export class AgentLiveReadRuntime {
22
22
  scope;