@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.
- package/README.md +128 -11
- package/REFERENCE.md +166 -12
- package/dist/gateway/dynamic-unary-forwarder.d.ts +62 -3
- package/dist/gateway/dynamic-unary-forwarder.d.ts.map +1 -1
- package/dist/gateway/dynamic-unary-forwarder.js +189 -10
- package/dist/gateway/dynamic-unary-forwarder.js.map +1 -1
- package/dist/gateway/index.d.ts +5 -0
- package/dist/gateway/index.d.ts.map +1 -1
- package/dist/gateway/index.js +65 -7
- package/dist/gateway/index.js.map +1 -1
- package/dist/index.d.ts +60 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/native/index.d.ts +43 -3
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +178 -32
- package/dist/native/index.js.map +1 -1
- package/dist/oidc/contracts.d.ts +18 -4
- package/dist/oidc/contracts.d.ts.map +1 -1
- package/dist/oidc/flow-store.d.ts +143 -0
- package/dist/oidc/flow-store.d.ts.map +1 -0
- package/dist/oidc/flow-store.js +184 -0
- package/dist/oidc/flow-store.js.map +1 -0
- package/dist/oidc/index.d.ts +29 -9
- package/dist/oidc/index.d.ts.map +1 -1
- package/dist/oidc/index.js +611 -152
- package/dist/oidc/index.js.map +1 -1
- package/dist/oidc/persistent-flow-store.d.ts +117 -0
- package/dist/oidc/persistent-flow-store.d.ts.map +1 -0
- package/dist/oidc/persistent-flow-store.js +413 -0
- package/dist/oidc/persistent-flow-store.js.map +1 -0
- package/dist/oidc/records.d.ts +15 -0
- package/dist/oidc/records.d.ts.map +1 -0
- package/dist/oidc/records.js +91 -0
- package/dist/oidc/records.js.map +1 -0
- package/dist/providers/index.d.ts +10 -5
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +103 -30
- package/dist/providers/index.js.map +1 -1
- package/dist/request/index.d.ts +3 -0
- package/dist/request/index.d.ts.map +1 -1
- package/dist/request/index.js +17 -1
- package/dist/request/index.js.map +1 -1
- package/dist/sessions/cookies.d.ts +46 -8
- package/dist/sessions/cookies.d.ts.map +1 -1
- package/dist/sessions/cookies.js +269 -47
- package/dist/sessions/cookies.js.map +1 -1
- package/dist/sessions/opaque-session-store.d.ts +143 -0
- package/dist/sessions/opaque-session-store.d.ts.map +1 -0
- package/dist/sessions/opaque-session-store.js +185 -0
- package/dist/sessions/opaque-session-store.js.map +1 -0
- package/dist/sessions/opaque.d.ts +93 -106
- package/dist/sessions/opaque.d.ts.map +1 -1
- package/dist/sessions/opaque.js +501 -232
- package/dist/sessions/opaque.js.map +1 -1
- package/dist/sessions/persistent-opaque-session-store.d.ts +105 -0
- package/dist/sessions/persistent-opaque-session-store.d.ts.map +1 -0
- package/dist/sessions/persistent-opaque-session-store.js +333 -0
- package/dist/sessions/persistent-opaque-session-store.js.map +1 -0
- package/dist/sessions/persistent-token-revocations.d.ts +90 -0
- package/dist/sessions/persistent-token-revocations.d.ts.map +1 -0
- package/dist/sessions/persistent-token-revocations.js +422 -0
- package/dist/sessions/persistent-token-revocations.js.map +1 -0
- package/dist/sessions/session-lifetime.d.ts +67 -0
- package/dist/sessions/session-lifetime.d.ts.map +1 -0
- package/dist/sessions/session-lifetime.js +165 -0
- package/dist/sessions/session-lifetime.js.map +1 -0
- package/dist/sessions/session-record.d.ts +15 -0
- package/dist/sessions/session-record.d.ts.map +1 -0
- package/dist/sessions/session-record.js +109 -0
- package/dist/sessions/session-record.js.map +1 -0
- package/dist/sessions/signed.d.ts +5 -4
- package/dist/sessions/signed.d.ts.map +1 -1
- package/dist/sessions/signed.js +212 -74
- package/dist/sessions/signed.js.map +1 -1
- package/dist/subscriptions/index.d.ts +35 -3
- package/dist/subscriptions/index.d.ts.map +1 -1
- package/dist/subscriptions/index.js +680 -134
- package/dist/subscriptions/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- 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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
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`
|
|
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
|
-
`
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
104
|
-
the logical subscription definition
|
|
105
|
-
|
|
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
|
|
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
|
|
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;
|
|
1
|
+
{"version":3,"file":"dynamic-unary-forwarder.d.ts","sourceRoot":"","sources":["../../src/gateway/dynamic-unary-forwarder.ts"],"names":[],"mappings":"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"}
|