@spine-event-engine/auth 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 (82) hide show
  1. package/README.md +128 -11
  2. package/REFERENCE.md +166 -12
  3. package/dist/gateway/dynamic-unary-forwarder.d.ts +62 -3
  4. package/dist/gateway/dynamic-unary-forwarder.d.ts.map +1 -1
  5. package/dist/gateway/dynamic-unary-forwarder.js +189 -10
  6. package/dist/gateway/dynamic-unary-forwarder.js.map +1 -1
  7. package/dist/gateway/index.d.ts +5 -0
  8. package/dist/gateway/index.d.ts.map +1 -1
  9. package/dist/gateway/index.js +65 -7
  10. package/dist/gateway/index.js.map +1 -1
  11. package/dist/index.d.ts +60 -3
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/native/index.d.ts +43 -3
  16. package/dist/native/index.d.ts.map +1 -1
  17. package/dist/native/index.js +178 -32
  18. package/dist/native/index.js.map +1 -1
  19. package/dist/oidc/contracts.d.ts +18 -4
  20. package/dist/oidc/contracts.d.ts.map +1 -1
  21. package/dist/oidc/flow-store.d.ts +143 -0
  22. package/dist/oidc/flow-store.d.ts.map +1 -0
  23. package/dist/oidc/flow-store.js +184 -0
  24. package/dist/oidc/flow-store.js.map +1 -0
  25. package/dist/oidc/index.d.ts +29 -9
  26. package/dist/oidc/index.d.ts.map +1 -1
  27. package/dist/oidc/index.js +611 -152
  28. package/dist/oidc/index.js.map +1 -1
  29. package/dist/oidc/persistent-flow-store.d.ts +117 -0
  30. package/dist/oidc/persistent-flow-store.d.ts.map +1 -0
  31. package/dist/oidc/persistent-flow-store.js +413 -0
  32. package/dist/oidc/persistent-flow-store.js.map +1 -0
  33. package/dist/oidc/records.d.ts +15 -0
  34. package/dist/oidc/records.d.ts.map +1 -0
  35. package/dist/oidc/records.js +91 -0
  36. package/dist/oidc/records.js.map +1 -0
  37. package/dist/providers/index.d.ts +10 -5
  38. package/dist/providers/index.d.ts.map +1 -1
  39. package/dist/providers/index.js +103 -30
  40. package/dist/providers/index.js.map +1 -1
  41. package/dist/request/index.d.ts +3 -0
  42. package/dist/request/index.d.ts.map +1 -1
  43. package/dist/request/index.js +17 -1
  44. package/dist/request/index.js.map +1 -1
  45. package/dist/sessions/cookies.d.ts +46 -8
  46. package/dist/sessions/cookies.d.ts.map +1 -1
  47. package/dist/sessions/cookies.js +269 -47
  48. package/dist/sessions/cookies.js.map +1 -1
  49. package/dist/sessions/opaque-session-store.d.ts +143 -0
  50. package/dist/sessions/opaque-session-store.d.ts.map +1 -0
  51. package/dist/sessions/opaque-session-store.js +185 -0
  52. package/dist/sessions/opaque-session-store.js.map +1 -0
  53. package/dist/sessions/opaque.d.ts +93 -106
  54. package/dist/sessions/opaque.d.ts.map +1 -1
  55. package/dist/sessions/opaque.js +501 -232
  56. package/dist/sessions/opaque.js.map +1 -1
  57. package/dist/sessions/persistent-opaque-session-store.d.ts +105 -0
  58. package/dist/sessions/persistent-opaque-session-store.d.ts.map +1 -0
  59. package/dist/sessions/persistent-opaque-session-store.js +333 -0
  60. package/dist/sessions/persistent-opaque-session-store.js.map +1 -0
  61. package/dist/sessions/persistent-token-revocations.d.ts +90 -0
  62. package/dist/sessions/persistent-token-revocations.d.ts.map +1 -0
  63. package/dist/sessions/persistent-token-revocations.js +422 -0
  64. package/dist/sessions/persistent-token-revocations.js.map +1 -0
  65. package/dist/sessions/session-lifetime.d.ts +67 -0
  66. package/dist/sessions/session-lifetime.d.ts.map +1 -0
  67. package/dist/sessions/session-lifetime.js +165 -0
  68. package/dist/sessions/session-lifetime.js.map +1 -0
  69. package/dist/sessions/session-record.d.ts +15 -0
  70. package/dist/sessions/session-record.d.ts.map +1 -0
  71. package/dist/sessions/session-record.js +109 -0
  72. package/dist/sessions/session-record.js.map +1 -0
  73. package/dist/sessions/signed.d.ts +5 -4
  74. package/dist/sessions/signed.d.ts.map +1 -1
  75. package/dist/sessions/signed.js +212 -74
  76. package/dist/sessions/signed.js.map +1 -1
  77. package/dist/subscriptions/index.d.ts +35 -3
  78. package/dist/subscriptions/index.d.ts.map +1 -1
  79. package/dist/subscriptions/index.js +680 -134
  80. package/dist/subscriptions/index.js.map +1 -1
  81. package/dist/tsconfig.tsbuildinfo +1 -1
  82. package/package.json +20 -6
package/README.md CHANGED
@@ -24,15 +24,25 @@ is native-only and treats auth as an optional peer.
24
24
 
25
25
  ## First success: create and resolve a local session
26
26
 
27
- `OpaqueSessions` is a bounded, process-local store for local development or a
28
- single process. This creates a credential, resolves it, and closes the store.
27
+ Omitting a store selects process-local memory for development. Configure a seal
28
+ secret and explicit session policy before issuing credentials. A
29
+ `PersistentOpaqueSessionStore` can instead share the same atomic sessions across
30
+ processes; its asynchronous factory prepares selected structures by default.
29
31
 
30
32
  ```ts
31
33
  import { OpaqueSessions } from "@spine-event-engine/auth";
32
-
33
- const sessions = new OpaqueSessions({ ttlMilliseconds: 60 * 60 * 1_000, maxSessions: 100 });
34
+ import { Time } from "@spine-event-engine/core/time";
35
+
36
+ const password = process.env.SESSION_SEAL_PASSWORD;
37
+ if (!password) throw new Error("Configure SESSION_SEAL_PASSWORD");
38
+ const sessions = await OpaqueSessions.create({
39
+ scope: "local-browser",
40
+ password,
41
+ clock: { now: Time.currentTimeMillis },
42
+ ttlMilliseconds: 60 * 60 * 1_000,
43
+ });
34
44
  try {
35
- const created = await sessions.create({ id: "local-user" });
45
+ const created = await sessions.issue({ id: "local-user" });
36
46
  if (created.kind !== "created") throw new Error(`Session creation failed: ${created.reason}`);
37
47
  const resolved = await sessions.resolve(created.credential);
38
48
  if (resolved === undefined) throw new Error("New local session was not resolvable");
@@ -42,6 +52,67 @@ try {
42
52
  }
43
53
  ```
44
54
 
55
+ ## Share opaque sessions through PostgreSQL
56
+
57
+ Install `@spine-event-engine/storage-postgres` alongside the auth package. Give
58
+ the session family a dedicated storage group. The database and schema must
59
+ already exist.
60
+
61
+ ```ts
62
+ import { OpaqueSessions, PersistentOpaqueSessionStore } from "@spine-event-engine/auth";
63
+ import { Time } from "@spine-event-engine/core/time";
64
+ import { StorageGroup } from "@spine-event-engine/storage";
65
+ import { PostgresStorageFactory } from "@spine-event-engine/storage-postgres";
66
+
67
+ const url = process.env.DATABASE_URL;
68
+ const password = process.env.SESSION_SEAL_PASSWORD;
69
+ if (!url || !password) throw new Error("Configure database and session credentials");
70
+ const factory = await PostgresStorageFactory.newBuilder().setOptions({ url }).build();
71
+ let store: PersistentOpaqueSessionStore | undefined;
72
+ try {
73
+ store = await PersistentOpaqueSessionStore.create({
74
+ storageFactory: factory,
75
+ group: new StorageGroup("auth_sessions"),
76
+ });
77
+ const sessions = await OpaqueSessions.create({
78
+ store,
79
+ scope: "orders-browser",
80
+ password,
81
+ clock: { now: Time.currentTimeMillis },
82
+ ttlMilliseconds: 60 * 60 * 1_000,
83
+ });
84
+ try {
85
+ const issued = await sessions.issue({ id: "user-42" });
86
+ if (issued.kind !== "created") throw new Error(`Session issue failed: ${issued.reason}`);
87
+ // Send issued.credential through the application's credential transport.
88
+ } finally {
89
+ await sessions.close();
90
+ }
91
+ } finally {
92
+ try {
93
+ await store?.close();
94
+ } finally {
95
+ await factory.close();
96
+ }
97
+ }
98
+ ```
99
+
100
+ The strategy leaves an injected store open; close the strategy, store, and
101
+ factory in that order. Use the same group and seal policy in each process.
102
+
103
+ ## Signed bearer sessions
104
+
105
+ `SignedSessions` issues and verifies ES256 bearer tokens with an application
106
+ P-256 key pair. Without a revocation source, logout reports expiry-only.
107
+ `PersistentTokenRevocations.create` supplies shared scoped revocation through
108
+ the selected storage factory and group; pass the resulting store as the
109
+ strategy's `revocation`. The store prepares its indexed record family by
110
+ default. Its explicit `purgeExpired` method removes records only after their
111
+ full inclusive acceptance deadline, including configured clock skew. The
112
+ application closes the strategy, store, and factory separately. See the
113
+ [signed session reference](REFERENCE.md) for exact retention and maintenance
114
+ behavior.
115
+
45
116
  For a real gateway, construct `UnaryGateway` with exactly one admission mode:
46
117
  `sessions`, or deliberate `publicAccess: true`. It requires a finite request
47
118
  limit, policy, trusted-context resolver, clock, and forwarder. Every approved
@@ -54,14 +125,52 @@ Events can refer to the exact Command that caused them. Those metadata fields
54
125
  are supplied by the caller; they are not proof of identity. Queries and
55
126
  subscription Topics receive the context supplied by the gateway resolver.
56
127
 
128
+ ## Pass verified reader information to the application node
129
+
130
+ The gateway also forwards the admitted principal and the `authorityFacts`
131
+ returned by `ContextResolver.resolve()`. These facts may contain JSON values,
132
+ including role arrays, booleans, and nested objects. They are detached from the
133
+ session and resolver result before forwarding. The framework does not copy the
134
+ request credential into them or fetch fresh information from an identity provider.
135
+ Your resolver decides which facts to supply on each request.
136
+
137
+ For a browser gateway, supply the issuer through
138
+ `BrowserServerOptions.forwardingProof`. When assembling a native gateway,
139
+ configure `NativeSubscriptionCreator(transport, { issuer, maxBytes })`; a custom
140
+ unary forwarder must produce the same operation-bound proof. See
141
+ [Verified reader forwarding](REFERENCE.md#verified-reader-forwarding) for the forwarding adapters.
142
+
143
+ In either setup, configure `ServerOptions.services.readerAdmission.forwarded`
144
+ on the receiving application node. The issuer produces a proof for the service,
145
+ method, request bytes, and reader facts. The verifier checks that proof and the
146
+ forwarding peer. Both are replaceable application adapters. A header name, actor
147
+ ID, or loopback connection does not establish a verified reader.
148
+
149
+ Authenticated forwarding requires an issuer. Deliberate `publicAccess: true`
150
+ forwards no principal or authority facts; the gateway's public admission marker
151
+ does not become a user identity. See the [server reference](https://github.com/SpineEventEngine/spine-ts/blob/master/packages/server/REFERENCE.md#native-reader-admission)
152
+ for direct native authentication and managed forwarding configuration.
153
+
57
154
  ## Browser and OIDC extensions
58
155
 
59
- The package does not provide HTTP routes, browser pages, users, permissions,
60
- TLS, or a production session database. An application can compose
61
- `OidcFlow` with Google, GitHub, or another verified provider, but it must own
62
- the start, callback, exchange, identity mapping, and session issuance routes.
156
+ `OidcFlow` is a component flow: the application provides its start, callback,
157
+ exchange, identity mapping, and session issuance routes. It does not supply
158
+ browser pages, users, permissions, TLS, or a production session database. An application can compose `OidcFlow` with
159
+ Google, GitHub, or another verified provider.
63
160
  Keep provider access, refresh, and ID tokens server-side.
64
161
 
162
+ `OidcFlow.start()` is asynchronous: await it before redirecting the browser.
163
+ The default transaction and application-grant store is process-local. To share
164
+ one login flow across processes, create `PersistentOidcFlowStore` with a
165
+ `StorageFactory`, `StorageGroup`, and stable `flowId`, then pass it in
166
+ `OidcFlowOptions.store`. The factory prepares both record families by default;
167
+ `createMissingStructures: false` skips preparation. Close the flow, then its
168
+ injected store and factory when the application shuts down. Call
169
+ `purgeExpired(cutoff)` explicitly for expired records; no timer runs it.
170
+
171
+ The application must send the authorization URL returned by `OidcFlow.start()`
172
+ to the browser and supply the actual callback parameters.
173
+
65
174
  For browser-facing hosting, install the server browser entry point and use
66
175
  `BrowserServer` and `DurableSubscriptionBindings` from
67
176
  `@spine-event-engine/server/browser`. That boundary requires exact origins,
@@ -70,8 +179,16 @@ durable authenticated bindings when applicable.
70
179
 
71
180
  ## Limits and next steps
72
181
 
73
- `OpaqueSessions` is not shared or durable. Signed-session revocation and OIDC
74
- identity provisioning are application responsibilities. A gateway protects
182
+ The [Fieldnotes application](https://github.com/SpineEventEngine/spine-ts/blob/master/examples/fieldnotes/README.md)
183
+ connects a Next.js map interface to a single-tenant Bounded Context. Its two launch
184
+ options demonstrate direct Google OIDC and Google sign-in through Firebase Auth.
185
+ Both use persistent opaque application sessions stored through the PostgreSQL
186
+ storage factory. The application guide follows sign-in through authenticated
187
+ Commands, private queries and subscriptions, and logout.
188
+
189
+ Memory-selected `OpaqueSessions` is process-local; select durable shared storage
190
+ for sessions that must survive process restart. Signed-session revocation and
191
+ OIDC identity provisioning are application responsibilities. A gateway protects
75
192
  only traffic that operators route through it; do not expose a backend route
76
193
  around that boundary.
77
194
 
package/REFERENCE.md CHANGED
@@ -7,7 +7,7 @@ Start with the [authentication overview](README.md) when choosing a gateway shap
7
7
 
8
8
  The package supplies provider-neutral gateway building blocks. An application places its gateway in front of a Spine TS or Spine JVM backend, then supplies the listener, credential extraction, identity provider, session storage, authorization rules, actor/tenant resolution, and forwarding implementation. The framework does not enforce a particular proxy, route topology, identity provider, tenant policy, or deployment configuration.
9
9
 
10
- `TransportFacts.from()` exposes only allowlisted request facts. Credentials are passed only to `Authenticator` or `SessionResolver`; they are not included in `IncomingRequest`. `IncomingRequest` is a discriminated union for command, query, subscription creation, activation, and cancellation. Command facts can include a decoded message when a `TypeRegistryLookup` knows its packed type; unknown and malformed `Any` values remain type-URL-only facts.
10
+ `TransportFacts.from()` exposes only allowlisted request facts. Credentials are passed only to `Authenticator` or `SessionResolver`; they are not included in `IncomingRequest`. `IncomingRequest` is a discriminated union for command, entity query, application query, subscription creation, activation, and cancellation. Command facts can include a decoded message when a `TypeRegistryLookup` knows its packed type; unknown and malformed `Any` values remain type-URL-only facts.
11
11
 
12
12
  `AuthorizationPolicy.authorize()` is evaluated separately for each incoming request. `ContextResolver.resolve()` returns the trusted actor, optional tenant, timestamp, zone, and language. The caller-provided `ActorContext` must match that resolved actor and tenant, otherwise `UnaryGateway` rejects the request as `context-stale`. Backends may rely on these identity checks only when the deployment routes traffic through an application-selected gateway.
13
13
 
@@ -40,7 +40,7 @@ a failed remote cleanup remains retryable.
40
40
 
41
41
  ## Unary requests
42
42
 
43
- `UnaryGateway` accepts `CommandService.Post`, `QueryService.Read`, and
43
+ `UnaryGateway` accepts `CommandService.Post`, `QueryService.Read`, `QueryService.Execute`, and
44
44
  `AuthenticationService.ResolveContext`. Construct both `UnaryGateway` and
45
45
  `SubscriptionGateway` with exactly one admission mode: a `SessionResolver`, or
46
46
  `publicAccess: true`. Public access creates no login session and no synthetic
@@ -50,18 +50,167 @@ Gateway, its authorization policy admits the public principal, and its
50
50
  also require finite request limits, an authorization function,
51
51
  `ContextResolver`, clock, and the corresponding forwarding collaborator.
52
52
 
53
- For Post and Read, it bounds and decodes bytes, admits the request through the selected mode, authorizes the decoded request, resolves trusted context, checks requested actor/tenant, and forwards exactly once. In authenticated mode, admission resolves the supplied session. In public mode, it uses the framework-owned public principal and no session. The forwarder receives only service, method, bytes, and optional cancellation signal. It does not receive credentials or extra transport facts. It returns `forwarded`, `resolved`, or a rejection reason: `request-too-large`, `unknown-operation`, `malformed-request`, `unauthenticated`, `forbidden`, or `context-stale`. Mapping a rejection to HTTP or gRPC status is the native transport adapter's job.
53
+ For Post, Read, and Execute, it bounds and decodes bytes, admits the request through the selected mode, authorizes the decoded request, resolves trusted context, checks requested actor/tenant, and forwards exactly once. In authenticated mode, admission resolves the supplied session. In public mode, it uses the public admission marker and no session. The forwarder receives service, method, bytes, detached `reader` facts, and an optional cancellation signal. It does not receive credentials or extra transport facts. It returns `forwarded`, `resolved`, or a rejection reason: `request-too-large`, `unknown-operation`, `malformed-request`, `unauthenticated`, `forbidden`, or `context-stale`. Mapping a rejection to HTTP or gRPC status is the native transport adapter's job.
54
54
 
55
55
  ResolveContext returns informational trusted actor and tenant facts in both modes. In authenticated mode, it validates the current session and also returns its expiry. In public mode, it resolves context for the framework-owned public principal and omits `expiresAt` because no session exists. It neither authorizes a business request nor creates a reusable credential. Later requests are independently admitted and authorized.
56
56
 
57
+ ### Verified reader forwarding
58
+
59
+ `reader.principal` contains the admitted application principal's ID and string
60
+ attributes. `reader.authorityFacts` comes from `AuthorizedRequestContext.authorityFacts`
61
+ and accepts finite JSON values, including nested objects and arrays. The gateway
62
+ copies these values for the invocation. Public admission omits both fields even
63
+ if a context resolver supplies facts for the public admission marker.
64
+ The existing `ContextResolver` remains the replacement point for obtaining
65
+ application-specific authority information; there is no provider polling or
66
+ promise that its information remains current after resolution.
67
+
68
+ `NativeSubscriptionCreator(transport, { issuer, maxBytes })` uses the configured
69
+ issuer to forward that reader with each native operation. `BrowserServerOptions.forwardingProof`
70
+ configures the same issuer in browser composition. Its `issue(bytes)` method
71
+ returns a transport-safe proof. The application node's corresponding
72
+ `services.readerAdmission.forwarded.verify(bytes, proof, peer)` verifies the
73
+ forwarding peer and the assertion. The signed bytes bind the service, method,
74
+ canonical request, and reader facts; an Activate proof cannot authorize Cancel.
75
+ Internal metadata received from a caller is not accepted without that verifier.
76
+
77
+ An authenticated native forwarding call without an issuer fails rather than
78
+ discarding its reader identity. Configure both issuer and verifier for authenticated
79
+ browser forwarding. These are TS forwarding ports; merely sending
80
+ their metadata to a JVM backend does not add verified-reader support there.
81
+ Custom forwarding implementations must establish the equivalent trust at their
82
+ receiving boundary. Credentials and provider tokens are not forwarding fields.
83
+
57
84
  ## Sessions and sign-in
58
85
 
59
- `OpaqueSessions` implements a bounded, process-local `SessionResolver` with random cookie credentials. Defaults are an eight-hour lifetime, 10,000 retained sessions, and three identifier attempts. It lazily removes expired entries and is terminal after `close()`. It suits local development and the supported single-Gateway shape; choose an application session strategy that meets that gateway's restart and revocation requirements.
86
+ `OpaqueSessions.create` requires an application scope, iron-session password
87
+ configuration, trusted clock, and positive lifetime. `issue` seals a random
88
+ stable reference and generation before atomic absent insertion. `resolve`
89
+ checks the current stored generation and exact deadline without renewal.
90
+ The authenticated application deadline is authoritative: any grace period in
91
+ iron-session's seal validation cannot extend it. Rotation seals before
92
+ compare-and-set. An old unexpired seal can revoke the current generation, but
93
+ an expired seal cannot resolve or revoke.
94
+
95
+ Omitting the store selects process-local memory. `PersistentOpaqueSessionStore.create`
96
+ accepts a common storage factory and group; it prepares the indexed session
97
+ family before returning by default. `createMissingStructures: false` opens a
98
+ non-preparing handle for already prepared structures. The strategy leaves an
99
+ injected store open, so the application closes the strategy, store, and factory
100
+ separately. Explicit `purgeExpired(cutoff, signal)` traverses finite indexed
101
+ pages with conditional removal in the persistent store; process-local memory
102
+ scans its own records. If a row changes during conditional removal, the pass
103
+ finishes that expiry group and returns its removed count. The changed row and
104
+ other eligible rows in later groups can remain for another explicit purge.
105
+ Storage errors and cancellation reject, possibly after partial removal. No
106
+ timer calls maintenance automatically.
107
+
108
+ For cancellation after a confirmed insertion or replacement, the strategy
109
+ attempts conditional compensation and returns no credential. A storage error
110
+ that does not confirm whether a commit occurred cannot safely authorize removal:
111
+ another session may already occupy the same stable reference. The operation
112
+ fails without returning a credential, but a committed row may require later
113
+ explicit maintenance. This is a best-effort cleanup guarantee.
114
+
115
+ `SignedSessions` issues and verifies ES256 application bearer tokens using jose.
116
+ Without a `SignedTokenRevocation` source, logout is expiry-only. For shared
117
+ revocation, create `PersistentTokenRevocations` with a storage factory, group,
118
+ issuer, and audience, then inject it as `revocation`. Its asynchronous factory
119
+ prepares the indexed record family by default; `createMissingStructures: false`
120
+ opens a non-preparing handle with no physical schema checks or DDL at creation
121
+ or during later operations. If structures are missing, ordinary storage
122
+ operations fail. Close the strategy, revocation store, and shared
123
+ factory separately. The strategy passes the full inclusive acceptance deadline,
124
+ including clock skew, to the store. Reads ignore records after that deadline
125
+ even before maintenance. Explicit `purgeExpired(cutoff, signal)` removes only
126
+ records strictly before a cutoff no later than the store's trusted clock. It
127
+ uses scoped indexed pages and conditional deletion; a concurrent change can
128
+ leave eligible rows for a later invocation. No automatic cleanup runs.
60
129
 
61
- `SignedSessions` provides signed application session tokens. Local signature validation can delay revocation; shared revocation requires an explicit `SignedTokenRevocation` implementation. `OpaqueSessionCookies` supplies strict cookie parsing and serialization helpers. Neither strategy provisions users nor grants permissions.
130
+ `SessionResolver.resolve(credential, lifetime?)` accepts an optional operation
131
+ signal. Omitting it performs one validation without a retained monitor. With
132
+ one, standard opaque and signed strategies register by stable authenticated
133
+ identity before the authoritative storage or revocation read and return
134
+ `ResolvedSession.invalidated`. Local logout, the captured resolved deadline,
135
+ or strategy close aborts that signal. The supplied operation ending releases
136
+ the listener and expiry timer. Opaque rotation does not extend an existing
137
+ operation's captured deadline; signed token verification skew and durable
138
+ revocation retention do not extend it either. The Gateway links invalidation
139
+ to its backend cancellation and suppresses late updates. Logout errors remain
140
+ visible even when local forwarding has stopped. Another process receives no
141
+ local abort signal and must validate against shared durable state separately.
142
+ `OpaqueSessionCookies` binds its CSRF value to the complete sealed credential,
143
+ parses Cookie fields with cookie 2.0.1 after duplicate checks, and emits separate
144
+ secure `__Host-` session and CSRF `Set-Cookie` fields. Pass the actual issued
145
+ deadline to `issue`; `persistent: true` gives both cookies that deadline while
146
+ the default keeps them as browser-session cookies. `sameSite` accepts `lax`,
147
+ `strict`, or `none`; the session cookie remains `HttpOnly` and the CSRF cookie
148
+ remains readable by the browser.
149
+
150
+ ```ts
151
+ import { OpaqueSessionCookies, type OpaqueSessions } from "@spine-event-engine/auth";
152
+
153
+ declare const sessions: OpaqueSessions;
154
+ const cookies = new OpaqueSessionCookies({
155
+ csrfSecret: crypto.getRandomValues(new Uint8Array(32)),
156
+ origins: ["https://app.example"],
157
+ sameSite: "strict",
158
+ persistent: true,
159
+ });
160
+ const issued = await sessions.issue({ id: "example-user" });
161
+ if (issued.kind === "created") {
162
+ const setCookieFields = cookies.issue(issued.credential.value, issued.session.expiresAt);
163
+ // Append each field separately to the HTTP response.
164
+ void setCookieFields;
165
+ }
166
+ ```
167
+
168
+ After rotation, call `issue` again with the new sealed value and its deadline;
169
+ `clear()` emits matching host-only removal fields. `read` distinguishes missing
170
+ credentials from terminal rejection so browser hosting does not fall back to a
171
+ different reader after invalid input.
172
+ Neither strategy provisions users nor grants permissions.
62
173
 
63
174
  `OidcFlow` is a bounded authorization-code and PKCE transaction manager. It works with `createGoogleProvider`, `createGitHubProvider`, or a discovered or configured OIDC provider. Applications expose start/callback/exchange endpoints, perform HTTP routing, map verified external identity through `IdentityMapping`, and issue an application session through `ApplicationSessionIssuer`. Provider access, refresh, and ID tokens remain sensitive server-side material; do not return them to browser application code.
64
175
 
176
+ `OidcFlow.start()` awaits transaction persistence before returning an
177
+ authorization URL. Callback binding and expiry are validated before the
178
+ transaction is atomically consumed; provider exchange and identity mapping
179
+ follow consumption. Browser PKCE proof and grant expiry are validated before
180
+ the grant is consumed; application session issuance follows consumption. A
181
+ trusted clock check after each confirmed consumption stops the next external
182
+ step if the record reached expiry while storage was pending. A
183
+ failed external step does not restore a consumed record. Independent flow
184
+ instances select the same one-use records through their configured store.
185
+ The optional second `AbortSignal` on start, callback, and exchange carries host
186
+ request cancellation. Ordinary abort rejects with that reason; a distinct
187
+ session issuer failure rejects the pending exchange or, after an abort or
188
+ timeout has returned, is reported by `close()`. Close waits for admitted work
189
+ and the original provider, mapping, and issuer promises to settle. A consumed
190
+ transaction or grant is never restored after cancellation.
191
+
192
+ The default `OidcFlow` store is process-local. `PersistentOidcFlowStore.create()`
193
+ selects two record families from the supplied `StorageFactory` and `StorageGroup`;
194
+ `flowId` qualifies state, grants, and indexed expiry queries. Preparation of
195
+ both families completes by default. `createMissingStructures: false` returns
196
+ non-preparing handles that do not probe for structures later. `OidcFlow.close()`
197
+ drains admitted work and closes only its default memory store. Close an injected
198
+ store and its factory separately. `purgeExpired(cutoff)` is explicit and uses a
199
+ flow-scoped exact expiry index in persistent storage. A concurrent row change
200
+ can leave eligible records for a later invocation. Cancellation or storage
201
+ failure rejects with partial progress possible. A known confirmed late insert
202
+ is conditionally removed after close; if another writer has replaced that
203
+ record, the replacement remains. An ambiguous thrown storage result cannot be
204
+ rolled back safely and may need later explicit maintenance.
205
+
206
+ `discoverOidcProvider()` uses the exact selected discovery URL, including an
207
+ application supplied path and query, and rejects a returned issuer mismatch.
208
+ The selected URL does not replace token or JWKS destinations. Authorization
209
+ URLs use S256 and replace reserved protocol parameters while retaining other
210
+ provider query values. Token exchange uses the configured client authentication
211
+ and exact callback URI; the provider returns an identity only after bounded
212
+ response parsing and JOSE ID-token verification.
213
+
65
214
  ## Subscription and native adapters
66
215
 
67
216
  `SubscriptionGateway` and `InMemorySubscriptionBindings` compose subscription creation, activation, cancellation, and relaying at the gateway boundary. The Gateway accepts a logical `SubscriptionCoordinator`; native `SubscriptionCreator` instances remain a per-node implementation seam. This is an intentional replacement of the former public native-creator input, with no compatibility adapter because Spine TS has no deployed users. In-memory bindings are local to one process and have finite operation limits; they do not provide cross-machine propagation. `createNativeGatewayServices`, `NativeSubscriptionCreator`, and `SubscriptionUpdateRelay` adapt these public contracts to the native service layer. They do not make a deployment secure by themselves: applications choose how their listener accepts only gateway-routed traffic.
@@ -69,10 +218,14 @@ ResolveContext returns informational trusted actor and tenant facts in both mode
69
218
  `BackendSubscriptionEnvelope` is opaque to the Gateway. A custom `SubscriptionCreator` or `DynamicUnaryClient` must validate each forwarded update's subscription ID and Topic against the actual Subscription accepted by its backend. The built-in native creator performs this validation; a custom envelope does not imply the backend reused the requested child ID.
70
219
 
71
220
  `DurableSubscriptionBindings` from `@spine-event-engine/server/browser` retains one approved
72
- `GatewayAuthenticatedSubscription` per public Subscription. The retained Topic
73
- includes the trusted Actor and Tenant, so Activate and Cancel compare that pair
74
- with the newly resolved request context. It is single-Gateway persistence, not
75
- cross-process quota, reservation, lease, fence, or fingerprint coordination.
221
+ `GatewayAuthenticatedSubscription` per public Subscription. The record includes
222
+ the stable principal ID, while its Topic retains the trusted Actor and Tenant.
223
+ Activate and Cancel check all three against the newly admitted request before
224
+ affecting a stream or record. The same principal may reconnect with changed
225
+ attributes or authority facts; those facts are not persisted as its identity.
226
+ Explicit anonymous admission is distinct from a missing legacy binding. A
227
+ missing binding cannot be claimed by the next caller. This remains single-Gateway
228
+ persistence, not cross-process quota, reservation, lease, or fingerprint coordination.
76
229
 
77
230
  Public subscription creation has two stages. `Subscribe` stores a process-local
78
231
  definition and returns its ID; `Activate` must arrive within the shared
@@ -100,9 +253,10 @@ Read the [browser client and gateway guide](../../docs/BROWSER_CLIENT_AUTH_EXTEN
100
253
 
101
254
  `DynamicUnaryForwarder` accepts complete current application-node snapshots. It serializes reconciliation, retains only the latest pending snapshot during churn, starts clients in bounded batches, disposes departed clients, and round-robins commands and queries across the resulting set. A dispatched unary request is never retried. Empty membership reports backend unavailability until a later snapshot restores clients.
102
255
 
103
- Dynamic subscriptions use the same reconciliation owner. Durable bindings retain
104
- the logical subscription definition and trusted Actor/Tenant facts only; native streams are
105
- ephemeral per-node work. Nodes may be added, removed, or re-added without
256
+ Dynamic subscriptions use the same reconciliation process. Durable bindings retain
257
+ the logical subscription definition, trusted Actor/Tenant facts, and stable reader
258
+ identity; native streams and changing authority facts are per-process data.
259
+ Nodes may be added, removed, or re-added without
106
260
  changing a durable definition. Notifications are best effort, may duplicate,
107
261
  and browser clients reconnect and re-query authoritative state after loss.
108
262
 
@@ -52,13 +52,67 @@ export declare class DynamicUnaryForwarder implements UnaryForwarder {
52
52
  * @param options Configures discovery clients and resource bounds.
53
53
  */
54
54
  constructor(options: DynamicUnaryOptions);
55
+ /**
56
+ * Records the native child definition and its admitted logical reader.
57
+ *
58
+ * @param definition The serialized logical subscription definition.
59
+ * @param node Selected managed application node.
60
+ * @returns The child subscription definition.
61
+ */
62
+ private childDefinition;
55
63
  /**
56
64
  * Builds membership operations for one connected gateway client.
57
65
  *
58
66
  * @param client The connected gateway client.
59
67
  * @returns The member's forwarding and subscription operations.
60
68
  */
61
- private static memberClient;
69
+ private memberClient;
70
+ /**
71
+ * Removes one native child and then retires its reader associations.
72
+ *
73
+ * @param client Connected member client that created this child.
74
+ * @param child Backend-issued child envelope.
75
+ * @param signal Cancellation signal for native disposal.
76
+ * @returns Completion after native disposal and reader cleanup.
77
+ */
78
+ private disposeChild;
79
+ /**
80
+ * Starts a native child and tracks an assigned ID for its admitted reader.
81
+ *
82
+ * @param client Native subscription client.
83
+ * @param definition The serialized subscription definition.
84
+ * @param signal The cancellation signal for this operation.
85
+ * @returns The managed-child subscription.
86
+ */
87
+ private subscribeChild;
88
+ /**
89
+ * Finds detached reader facts for one retained logical or native child definition.
90
+ *
91
+ * @param bytes The serialized message bytes.
92
+ * @returns The persisted reader binding field.
93
+ */
94
+ private readerField;
95
+ /**
96
+ * Records a backend-assigned child ID to its admitted logical reader.
97
+ *
98
+ * @param association Native child attempt that produced the backend ID.
99
+ * @param produced Backend update produced for a subscription.
100
+ */
101
+ private rememberProducedChild;
102
+ /**
103
+ * Records one child ID with its attempt and current logical reader.
104
+ *
105
+ * @param association Native child attempt that introduced the ID.
106
+ * @param childId Initial or backend-issued child ID.
107
+ * @param reader Current verified reader facts, if supplied.
108
+ */
109
+ private associateChildId;
110
+ /**
111
+ * Removes only IDs still associated with this retired child attempt.
112
+ *
113
+ * @param association Native child attempt after successful disposal.
114
+ */
115
+ private retireChild;
62
116
  /**
63
117
  * Checks a response-only successful child activation acknowledgement.
64
118
  *
@@ -117,8 +171,7 @@ export declare class DynamicUnaryForwarder implements UnaryForwarder {
117
171
  * @param wire Supplies the public subscription wire.
118
172
  * @param updates Receives relayed subscription updates.
119
173
  * @param signal Cancels update relay.
120
- * @returns Completion after cancellation; rejects on unexpected child failure, possibly
121
- * including bounded cleanup failure.
174
+ * @returns Completion after cancellation; rejects on unexpected child or bounded cleanup failure.
122
175
  */
123
176
  activateDefinition(wire: PublicSubscriptionWire, updates: SubscriptionUpdateSink, signal: AbortSignal): Promise<void>;
124
177
  /**
@@ -129,6 +182,12 @@ export declare class DynamicUnaryForwarder implements UnaryForwarder {
129
182
  * @returns Completion of child cleanup.
130
183
  */
131
184
  cancelDefinition(wire: PublicSubscriptionWire, signal: AbortSignal): Promise<void>;
185
+ /**
186
+ * Updates current reader facts for a retained definition and its native children.
187
+ *
188
+ * @param wire The admitted definition with current verified reader facts.
189
+ */
190
+ private rememberReader;
132
191
  /**
133
192
  * Resolves when the supplied signal aborts.
134
193
  *
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-unary-forwarder.d.ts","sourceRoot":"","sources":["../../src/gateway/dynamic-unary-forwarder.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAMjE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,KAAK,EAEV,sBAAsB,EACtB,mBAAmB,EACnB,sBAAsB,EAEvB,MAAM,2BAA2B,CAAC;AAEnC;;;;GAIG;AACH,MAAM,WAAW,kBAAmB,SAAQ,cAAc,EAAE,mBAAmB;IAG7E;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAGlC;;;;;;OAMG;IACH,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAE7F;;OAEG;IACH,QAAQ,CAAC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAEtC;;OAEG;IACH,QAAQ,CAAC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IAE1C;;OAEG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,CAAC;CAC7B;AAED;;;GAGG;AACH,qBAAa,qBAAsB,YAAW,cAAc;;IAQ1D;;;;OAIG;gBACS,OAAO,EAAE,mBAAmB;IAkCxC;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,YAAY;IA4B3B;;;;;OAKG;IACH,OAAO,CAAC,MAAM,CAAC,eAAe;IAK9B;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,CAAC,oBAAoB;IASnC;;;;;OAKG;IACH,SAAS,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAI3D;;;;;OAKG;IACH,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,cAAc,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,UAAU,CAAC;IAM/E;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAItB;;;;;;;OAOG;IACH,mBAAmB,CACjB,OAAO,EAAE,sBAAsB,EAC/B,MAAM,EAAE,WAAW,EACnB,uBAAuB,CAAC,EAAE,MAAM,GAC/B,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;OAMG;IACG,mBAAmB,CACvB,OAAO,EAAE,sBAAsB,EAC/B,uBAAuB,CAAC,EAAE,MAAM,GAC/B,OAAO,CAAC,IAAI,CAAC;IAWhB;;;;;;;;OAQG;IACH,kBAAkB,CAChB,IAAI,EAAE,sBAAsB,EAC5B,OAAO,EAAE,sBAAsB,EAC/B,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,IAAI,CAAC;IAIhB;;;;;;OAMG;IACH,gBAAgB,CAAC,IAAI,EAAE,sBAAsB,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlF;;;;;OAKG;IACH,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAIvD;;;;;OAKG;IACH,MAAM,CAAC,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS;IAI3D;;;;;;OAMG;IACH,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,GAAG,UAAU;CAiB7E"}
1
+ {"version":3,"file":"dynamic-unary-forwarder.d.ts","sourceRoot":"","sources":["../../src/gateway/dynamic-unary-forwarder.ts"],"names":[],"mappings":"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"}