@abloatai/transaction 0.59.2 → 0.60.0
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/dist/auth/apiKey.d.ts +10 -11
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +15 -29
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/browserCredentialSafety.js +1 -1
- package/dist/auth/browserCredentialSafety.js.map +1 -1
- package/dist/auth/capability.d.ts +15 -15
- package/dist/auth/capabilityLifecycle.d.ts +3 -3
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/capabilityLifecycle.js +2 -1
- package/dist/auth/capabilityLifecycle.js.map +1 -1
- package/dist/auth/credentialEndpoint.js +1 -1
- package/dist/auth/credentialEndpoint.js.map +1 -1
- package/dist/auth/credentialResult.d.ts +2 -1
- package/dist/auth/credentialResult.d.ts.map +1 -1
- package/dist/auth/credentialResult.js.map +1 -1
- package/dist/auth/credentialSource.js.map +1 -1
- package/dist/auth/identity.js.map +1 -1
- package/dist/auth/schemas.d.ts +5 -5
- package/dist/auth/sessionIssuanceProtocol.d.ts +8 -8
- package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -1
- package/dist/branches.d.ts +38 -38
- package/dist/claims/contract.d.ts +27 -27
- package/dist/claims/eventContract.d.ts +18 -18
- package/dist/claims/events.d.ts +1 -1
- package/dist/claims/events.d.ts.map +1 -1
- package/dist/claims/routes.d.ts +9 -9
- package/dist/claims/routes.d.ts.map +1 -1
- package/dist/claims/trace.d.ts.map +1 -1
- package/dist/claims/trace.js.map +1 -1
- package/dist/client/ablo.d.ts +11 -8
- package/dist/client/ablo.d.ts.map +1 -1
- package/dist/client/ablo.js +5 -6
- package/dist/client/ablo.js.map +1 -1
- package/dist/client/resources/httpResources.d.ts +1 -169
- package/dist/client/resources/httpResources.d.ts.map +1 -1
- package/dist/client/resources/httpResources.js +1 -1
- package/dist/client/resources/httpResources.js.map +1 -1
- package/dist/client/resources/modelOperations.d.ts +6 -15
- package/dist/client/resources/modelOperations.d.ts.map +1 -1
- package/dist/client/resources/where.d.ts +12 -12
- package/dist/client/surface.d.ts +1 -1
- package/dist/client/surface.d.ts.map +1 -1
- package/dist/client/surface.js +2 -4
- package/dist/client/surface.js.map +1 -1
- package/dist/commit/confirmation/commitEnvelope.d.ts +7 -7
- package/dist/commit/confirmation/httpCommitEnvelope.d.ts +1 -1
- package/dist/commit/confirmation/pendingWrite.d.ts +5 -5
- package/dist/commit/contract.d.ts +249 -249
- package/dist/commit/contract.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +3 -4
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +57 -101
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -69
- package/dist/coordination/schema.js.map +1 -1
- package/dist/environment.d.ts +3 -3
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/errorCodes.d.ts +4 -5
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +3 -8
- package/dist/errorCodes.js.map +1 -1
- package/dist/errorObservation.d.ts +14 -14
- package/dist/errors.d.ts +11 -11
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/keys/index.d.ts +1 -1
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js.map +1 -1
- package/dist/observation/contract.d.ts +34 -34
- package/dist/observation/feedContract.d.ts +86 -86
- package/dist/observation/persistence/syncDeltaRow.d.ts +23 -23
- package/dist/observation/persistence/syncDeltaRow.d.ts.map +1 -1
- package/dist/pricing.d.ts +12 -12
- package/dist/schema/audit.js.map +1 -1
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/ddlLock.js.map +1 -1
- package/dist/schema/deployment/contracts.d.ts +64 -64
- package/dist/schema/deployment/index.js.map +1 -1
- package/dist/schema/deployment/reconcile.js.map +1 -1
- package/dist/schema/diff.js.map +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.d.ts.map +1 -1
- package/dist/schema/loadStrategy.d.ts +2 -2
- package/dist/schema/loadStrategy.d.ts.map +1 -1
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/relation.d.ts +3 -3
- package/dist/schema/relation.d.ts.map +1 -1
- package/dist/schema/residency.d.ts +1 -1
- package/dist/schema/roles.d.ts +1 -1
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/roles.js.map +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/serialize.js.map +1 -1
- package/dist/schema/sugar.d.ts.map +1 -1
- package/dist/schema/tenancy.d.ts +2 -2
- package/dist/server/storageMode.d.ts +1 -1
- package/dist/sessions/client.d.ts +29 -0
- package/dist/sessions/client.d.ts.map +1 -0
- package/dist/sessions/client.js +69 -0
- package/dist/sessions/client.js.map +1 -0
- package/dist/sessions/contract.d.ts +97 -0
- package/dist/sessions/contract.d.ts.map +1 -0
- package/dist/sessions/contract.js +5 -0
- package/dist/sessions/contract.js.map +1 -0
- package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
- package/dist/sessions/create.d.ts.map +1 -0
- package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
- package/dist/sessions/create.js.map +1 -0
- package/dist/sessions/handler.d.ts +23 -0
- package/dist/sessions/handler.d.ts.map +1 -0
- package/dist/sessions/handler.js +55 -0
- package/dist/sessions/handler.js.map +1 -0
- package/dist/sessions/index.d.ts +14 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +14 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
- package/dist/sessions/lifecycle.d.ts.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
- package/dist/sessions/lifecycle.js.map +1 -0
- package/dist/sessions/source.d.ts +24 -0
- package/dist/sessions/source.d.ts.map +1 -0
- package/dist/sessions/source.js +56 -0
- package/dist/sessions/source.js.map +1 -0
- package/dist/source/adapters/adapterProfile.d.ts +6 -6
- package/dist/source/adapters/conformance.js.map +1 -1
- package/dist/source/adapters/contract.d.ts +14 -14
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/connector/client.d.ts.map +1 -1
- package/dist/source/connector/client.js.map +1 -1
- package/dist/source/connector/protocol.d.ts.map +1 -1
- package/dist/source/delivery/pushQueue.d.ts.map +1 -1
- package/dist/source/delivery/pushQueue.js.map +1 -1
- package/dist/source/endpoint/signing.d.ts +4 -4
- package/dist/source/endpoint/signing.d.ts.map +1 -1
- package/dist/source/outbox/contract.d.ts +4 -4
- package/dist/testing/fixtures/httpResponses.d.ts +2 -2
- package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
- package/dist/testing/fixtures/httpResponses.js.map +1 -1
- package/dist/transport/connection/index.d.ts +0 -1
- package/dist/transport/connection/index.d.ts.map +1 -1
- package/dist/transport/connection/index.js +0 -1
- package/dist/transport/connection/index.js.map +1 -1
- package/dist/transport/connection/manager.d.ts +1 -1
- package/dist/transport/connection/manager.d.ts.map +1 -1
- package/dist/transport/connection/manager.js.map +1 -1
- package/dist/transport/connection/networkProbe.d.ts +5 -18
- package/dist/transport/connection/networkProbe.d.ts.map +1 -1
- package/dist/transport/connection/networkProbe.js.map +1 -1
- package/dist/transport/http/client.d.ts +11 -19
- package/dist/transport/http/client.d.ts.map +1 -1
- package/dist/transport/http/client.js +12 -14
- package/dist/transport/http/client.js.map +1 -1
- package/dist/transport/http/options.d.ts +4 -1
- package/dist/transport/http/options.d.ts.map +1 -1
- package/dist/transport/http/transport.d.ts +4 -10
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +9 -51
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/commitFrames.d.ts +1 -2
- package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
- package/dist/transport/websocket/commitFrames.js.map +1 -1
- package/dist/transport/websocket/frameHandlers.d.ts +0 -14
- package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
- package/dist/transport/websocket/frameHandlers.js +1 -71
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/heartbeat.d.ts.map +1 -1
- package/dist/transport/websocket/heartbeat.js.map +1 -1
- package/dist/transport/websocket/session.d.ts.map +1 -1
- package/dist/transport/websocket/session.js +72 -32
- package/dist/transport/websocket/session.js.map +1 -1
- package/dist/transport/websocket/sessionContract.d.ts +3 -14
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
- package/dist/transport/websocket/transport.d.ts +5 -42
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js +14 -101
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/accountResponses.d.ts +31 -31
- package/dist/wire/auth.d.ts +3 -3
- package/dist/wire/bootstrapReason.d.ts +2 -2
- package/dist/wire/dataSourceResponses.d.ts +2 -2
- package/dist/wire/dataSourceResponses.d.ts.map +1 -1
- package/dist/wire/frames.d.ts +8 -8
- package/dist/wire/inboundFrames.d.ts +53 -63
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +1 -6
- package/dist/wire/inboundFrames.js.map +1 -1
- package/dist/wire/listEnvelope.js.map +1 -1
- package/dist/wire/modelResponses.d.ts +2 -2
- package/dist/wire/modelShape.d.ts +4 -4
- package/dist/wire/pricing.d.ts +3 -3
- package/dist/wire/protocolVersion.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/auth/apiKey.ts +25 -44
- package/src/auth/browserCredentialSafety.ts +1 -1
- package/src/auth/capabilityLifecycle.ts +3 -2
- package/src/auth/credentialEndpoint.ts +1 -1
- package/src/auth/credentialResult.ts +2 -0
- package/src/claims/events.ts +1 -2
- package/src/client/ablo.ts +15 -10
- package/src/client/resources/httpResources.ts +2 -174
- package/src/client/resources/modelOperations.ts +0 -10
- package/src/client/surface.ts +2 -4
- package/src/coordination/index.ts +2 -9
- package/src/coordination/schema.ts +7 -81
- package/src/errorCodes.ts +3 -12
- package/src/sessions/client.ts +121 -0
- package/src/sessions/contract.ts +116 -0
- package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
- package/src/sessions/handler.ts +96 -0
- package/src/sessions/index.ts +13 -0
- package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
- package/src/sessions/source.ts +94 -0
- package/src/transport/connection/index.ts +0 -1
- package/src/transport/connection/manager.ts +1 -1
- package/src/transport/http/client.ts +21 -31
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +14 -74
- package/src/transport/websocket/commitFrames.ts +0 -2
- package/src/transport/websocket/frameHandlers.ts +0 -102
- package/src/transport/websocket/session.ts +88 -33
- package/src/transport/websocket/sessionContract.ts +3 -12
- package/src/transport/websocket/transport.ts +13 -137
- package/src/wire/inboundFrames.ts +0 -6
- package/dist/auth/sessionMint.d.ts.map +0 -1
- package/dist/auth/sessionMint.js.map +0 -1
- package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
- package/dist/transport/connection/credentialLifecycle.js.map +0 -1
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
readProcessEnv,
|
|
29
29
|
resolveApiKey,
|
|
30
30
|
resolveApiKeyValue,
|
|
31
|
+
resolveCredentialValue,
|
|
31
32
|
resolveAuthToken,
|
|
32
33
|
resolveBaseURL,
|
|
33
34
|
resolveBootstrapBaseUrl,
|
|
@@ -86,15 +87,10 @@ import type {
|
|
|
86
87
|
HttpTransportList,
|
|
87
88
|
HttpLogsResource,
|
|
88
89
|
ModelTarget,
|
|
89
|
-
CreateSessionParams,
|
|
90
|
-
AbloSession,
|
|
91
|
-
SessionResource,
|
|
92
90
|
} from '../../client/resources/httpResources.js';
|
|
93
|
-
import {
|
|
94
|
-
import {
|
|
95
|
-
|
|
96
|
-
rotateCapability,
|
|
97
|
-
} from '../../auth/capabilityLifecycle.js';
|
|
91
|
+
import type { CredentialProviderResult } from '../../auth/credentialResult.js';
|
|
92
|
+
import { credentialToken } from '../../auth/credentialResult.js';
|
|
93
|
+
import { createSessionAccess, type SessionAccess } from '../../sessions/index.js';
|
|
98
94
|
import { parseIdentityResolveResponse } from '../../auth/schemas.js';
|
|
99
95
|
import type { EffectiveAuthority } from '../../auth/capability.js';
|
|
100
96
|
|
|
@@ -103,7 +99,6 @@ import {
|
|
|
103
99
|
heldHeartbeatReply,
|
|
104
100
|
parseSuccessfulCommitResponse,
|
|
105
101
|
} from './helpers.js';
|
|
106
|
-
import type { SchemaRecord } from '../../schema/schema.js';
|
|
107
102
|
import type {
|
|
108
103
|
ClaimLookupParams,
|
|
109
104
|
ClaimOptions,
|
|
@@ -180,8 +175,6 @@ import type { CommitFrameOperation } from '../websocket/commitFrames.js';
|
|
|
180
175
|
/** @internal Private options for the schema-agnostic HTTP protocol transport. */
|
|
181
176
|
export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
|
|
182
177
|
readonly bootstrapBaseUrl?: string | undefined;
|
|
183
|
-
/** Schema-key to wire-typename mapping used only when minting agent sessions. */
|
|
184
|
-
readonly modelTypenames?: Readonly<Record<string, string>> | undefined;
|
|
185
178
|
/**
|
|
186
179
|
* The observability provider forwarded from `Ablo({ observability })`. The HTTP
|
|
187
180
|
* transport emits the same claim and conflict events as the WebSocket transport,
|
|
@@ -221,9 +214,6 @@ export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
|
|
|
221
214
|
/** @internal Default per-request deadline for the private HTTP transport. */
|
|
222
215
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
223
216
|
const HTTP_CONFIRMATION_POLL_INTERVAL_MS = 250;
|
|
224
|
-
// NOTE: end-user / agent session minting is `ablo.sessions.create(...)` (typed
|
|
225
|
-
// against the schema, see Ablo.ts `CreateSessionParams`). There is no separate
|
|
226
|
-
// `ephemeralKeys` resource — `sessions` is the one front door for both.
|
|
227
217
|
|
|
228
218
|
/** @internal Private protocol surface wrapped by `AbloHttpClient`. */
|
|
229
219
|
export interface HttpTransport {
|
|
@@ -255,12 +245,8 @@ export interface HttpTransport {
|
|
|
255
245
|
* server with the credential this client already holds — no re-mint.
|
|
256
246
|
*/
|
|
257
247
|
getAuthToken(): Promise<string | null>;
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
* socket), so it lives on this stateless client too, not only the realtime one.
|
|
261
|
-
* `{ user }` mints an `ek_`; `{ agent, can }` mints an `rk_`.
|
|
262
|
-
*/
|
|
263
|
-
readonly sessions: SessionResource<SchemaRecord>;
|
|
248
|
+
/** @internal One normalized source shared by HTTP bootstrap and live transport. */
|
|
249
|
+
readonly access: SessionAccess;
|
|
264
250
|
}
|
|
265
251
|
|
|
266
252
|
type CommitResponse = CommitReceiptWire;
|
|
@@ -466,6 +452,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
466
452
|
return headers;
|
|
467
453
|
}
|
|
468
454
|
|
|
455
|
+
async function activeCredential(): Promise<CredentialProviderResult> {
|
|
456
|
+
return (await resolveCredentialValue(configuredApiKey)) ?? configuredAuthToken ?? null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const access = createSessionAccess(options.session, activeCredential);
|
|
460
|
+
|
|
469
461
|
function endpoint(path: string): string {
|
|
470
462
|
const target = new URL(`${apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
471
463
|
for (const [key, value] of Object.entries(options.defaultQuery ?? {})) {
|
|
@@ -2222,64 +2214,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
2222
2214
|
claims,
|
|
2223
2215
|
logs,
|
|
2224
2216
|
model,
|
|
2225
|
-
sessions: {
|
|
2226
|
-
async create(params: CreateSessionParams<SchemaRecord>): Promise<AbloSession> {
|
|
2227
|
-
// Stateless mint: the configured key is the control-plane credential here
|
|
2228
|
-
// (no startup `rk_` exchange runs on this client). It reuses the resolved
|
|
2229
|
-
// base URL and fetch; the shared `mintSession` handles the two server routes.
|
|
2230
|
-
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
2231
|
-
if (!apiKey) {
|
|
2232
|
-
throw new AbloAuthenticationError(
|
|
2233
|
-
'sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.',
|
|
2234
|
-
{ code: 'apikey_missing' }
|
|
2235
|
-
);
|
|
2236
|
-
}
|
|
2237
|
-
// A transport built without a schema has no way to translate `can`'s
|
|
2238
|
-
// schema keys into the type names the server gates on. Minting anyway
|
|
2239
|
-
// would spell every override wrong and surface as
|
|
2240
|
-
// `capability_scope_denied` on the agent's first write, so refuse here
|
|
2241
|
-
// instead of guessing.
|
|
2242
|
-
if (!options.modelTypenames) {
|
|
2243
|
-
throw new AbloValidationError(
|
|
2244
|
-
'sessions.create needs the schema this client is bound to. Construct it ' +
|
|
2245
|
-
"through Ablo({ schema, apiKey, transport: 'http' }) rather than the " +
|
|
2246
|
-
'bare transport.',
|
|
2247
|
-
{ code: 'invalid_options', param: 'schema' },
|
|
2248
|
-
);
|
|
2249
|
-
}
|
|
2250
|
-
return mintSession(params, {
|
|
2251
|
-
apiKey,
|
|
2252
|
-
baseUrl: apiBaseUrl,
|
|
2253
|
-
modelTypenames: options.modelTypenames,
|
|
2254
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
2255
|
-
});
|
|
2256
|
-
},
|
|
2257
|
-
async revoke({ id }) {
|
|
2258
|
-
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
2259
|
-
return revokeCapability({
|
|
2260
|
-
apiKey: apiKey ?? '',
|
|
2261
|
-
baseUrl: apiBaseUrl,
|
|
2262
|
-
id,
|
|
2263
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
2264
|
-
});
|
|
2265
|
-
},
|
|
2266
|
-
async rotate({ id, graceSeconds, ttlSeconds }) {
|
|
2267
|
-
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
2268
|
-
return rotateCapability({
|
|
2269
|
-
apiKey: apiKey ?? '',
|
|
2270
|
-
baseUrl: apiBaseUrl,
|
|
2271
|
-
id,
|
|
2272
|
-
...(graceSeconds !== undefined ? { graceSeconds } : {}),
|
|
2273
|
-
...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
|
|
2274
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
2275
|
-
});
|
|
2276
|
-
},
|
|
2277
|
-
},
|
|
2278
2217
|
async getAuthToken(): Promise<string | null> {
|
|
2279
2218
|
// Mirror `authHeaders()`: a configured API key wins, else the
|
|
2280
2219
|
// construction-time auth token. Resolve the (possibly async) key setter.
|
|
2281
|
-
return (await
|
|
2220
|
+
return credentialToken(await activeCredential());
|
|
2282
2221
|
},
|
|
2222
|
+
access,
|
|
2283
2223
|
};
|
|
2284
2224
|
}
|
|
2285
2225
|
|
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
ClaimQueued,
|
|
18
18
|
ClaimLost,
|
|
19
19
|
ClaimRejection,
|
|
20
|
-
ClaimExpired,
|
|
21
20
|
} from '../../coordination/schema.js';
|
|
22
21
|
import type { ClaimEvent, ClaimCounterparty } from '../../claims/events.js';
|
|
23
22
|
import type { AssertExact } from '../../types/assertExact.js';
|
|
@@ -105,7 +104,6 @@ interface ClaimFrameByPhase {
|
|
|
105
104
|
queued: ClaimQueued;
|
|
106
105
|
lost: ClaimLost;
|
|
107
106
|
rejected: ClaimRejection;
|
|
108
|
-
expired: ClaimExpired;
|
|
109
107
|
}
|
|
110
108
|
|
|
111
109
|
// The phases this map covers and the phases a ClaimEvent can report are the
|
|
@@ -45,16 +45,6 @@ export interface PendingCommit {
|
|
|
45
45
|
readonly returnReceipt?: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* In-flight `claim` request record, keyed by claimId. Resolved when the
|
|
50
|
-
* matching `claim_ack` arrives, or rejected on timeout/disconnect.
|
|
51
|
-
*/
|
|
52
|
-
export interface PendingClaim {
|
|
53
|
-
resolve: (value: { syncGroups: string[]; ttlSeconds?: number }) => void;
|
|
54
|
-
reject: (err: Error) => void;
|
|
55
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
48
|
/**
|
|
59
49
|
* An in-flight `update_subscription` request awaiting its
|
|
60
50
|
* `subscription_ack`. The wire carries no correlation id, so requests are
|
|
@@ -109,8 +99,6 @@ export interface WsSession {
|
|
|
109
99
|
observability: SocketObservability;
|
|
110
100
|
/** In-flight commit acks keyed by clientTxId. */
|
|
111
101
|
pendingMutations: Map<string, PendingCommit>;
|
|
112
|
-
/** In-flight claim acks keyed by claimId. */
|
|
113
|
-
pendingClaims: Map<string, PendingClaim>;
|
|
114
102
|
/** Removes and returns the oldest in-flight `update_subscription` request. */
|
|
115
103
|
shiftPendingSubscription(): PendingSubscription | undefined;
|
|
116
104
|
/** Connection options subset the handlers write back (acked sync groups). */
|
|
@@ -290,84 +278,6 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
|
|
|
290
278
|
}
|
|
291
279
|
};
|
|
292
280
|
|
|
293
|
-
/**
|
|
294
|
-
* Handles the acknowledgement of a `claim` request. The frame has the shape
|
|
295
|
-
* `{ type: 'claim_ack', payload: { claimId, success, syncGroups?,
|
|
296
|
-
* ttlSeconds?, error? } }`.
|
|
297
|
-
*/
|
|
298
|
-
const handleClaimAck: WsFrameHandler = (session, message) => {
|
|
299
|
-
const p = (message.payload ?? {}) as Record<string, unknown>;
|
|
300
|
-
const { claimId, success, syncGroups, ttlSeconds, error } = p;
|
|
301
|
-
const pending =
|
|
302
|
-
typeof claimId === 'string'
|
|
303
|
-
? session.pendingClaims.get(claimId)
|
|
304
|
-
: undefined;
|
|
305
|
-
if (!pending) return;
|
|
306
|
-
clearTimeout(pending.timeout);
|
|
307
|
-
// `pending` exists ⇒ claimId was a string key (the guard above).
|
|
308
|
-
session.pendingClaims.delete(claimId as string);
|
|
309
|
-
if (success) {
|
|
310
|
-
pending.resolve({
|
|
311
|
-
syncGroups: Array.isArray(syncGroups) ? syncGroups : [],
|
|
312
|
-
ttlSeconds: typeof ttlSeconds === 'number' ? ttlSeconds : undefined,
|
|
313
|
-
});
|
|
314
|
-
} else {
|
|
315
|
-
const err = error as
|
|
316
|
-
| {
|
|
317
|
-
code?: unknown;
|
|
318
|
-
message?: unknown;
|
|
319
|
-
request_id?: unknown;
|
|
320
|
-
event_id?: unknown;
|
|
321
|
-
}
|
|
322
|
-
| undefined;
|
|
323
|
-
const code =
|
|
324
|
-
err?.code && typeof err.code === 'string'
|
|
325
|
-
? err.code
|
|
326
|
-
: 'claim_rejected';
|
|
327
|
-
const msg =
|
|
328
|
-
err?.message && typeof err.message === 'string'
|
|
329
|
-
? err.message
|
|
330
|
-
: 'claim rejected by server';
|
|
331
|
-
const requestId =
|
|
332
|
-
typeof err?.request_id === 'string' ? err.request_id : undefined;
|
|
333
|
-
const eventId =
|
|
334
|
-
typeof err?.event_id === 'string' ? err.event_id : undefined;
|
|
335
|
-
const details = eventId === undefined ? undefined : { event_id: eventId };
|
|
336
|
-
// Capability denials get the typed CapabilityError so
|
|
337
|
-
// callers can read `.requiredCapability` and attenuate-
|
|
338
|
-
// and-retry the claim with a narrower token.
|
|
339
|
-
if (
|
|
340
|
-
code === 'capability_scope_denied' ||
|
|
341
|
-
code === 'capability_invalid'
|
|
342
|
-
) {
|
|
343
|
-
const rc = (error as { requiredCapability?: unknown } | undefined)
|
|
344
|
-
?.requiredCapability;
|
|
345
|
-
const requiredCapability =
|
|
346
|
-
rc != null &&
|
|
347
|
-
typeof rc === 'object' &&
|
|
348
|
-
typeof (rc as { scope?: unknown }).scope === 'string'
|
|
349
|
-
? (rc as RequiredCapability)
|
|
350
|
-
: undefined;
|
|
351
|
-
pending.reject(
|
|
352
|
-
new CapabilityError(code, msg, requiredCapability, {
|
|
353
|
-
...(requestId !== undefined ? { requestId } : {}),
|
|
354
|
-
...(details !== undefined ? { details } : {}),
|
|
355
|
-
}),
|
|
356
|
-
);
|
|
357
|
-
} else {
|
|
358
|
-
// Route through the shared factory so a failed claim_ack is a
|
|
359
|
-
// typed AbloError (registry code → right subclass), symmetric
|
|
360
|
-
// with the commit `mutation_result` path — never a bare Error.
|
|
361
|
-
pending.reject(
|
|
362
|
-
errorFromWire(msg, {
|
|
363
|
-
code,
|
|
364
|
-
requestId,
|
|
365
|
-
details,
|
|
366
|
-
}),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
281
|
|
|
372
282
|
/**
|
|
373
283
|
* Handles the acknowledgement of an `update_subscription` request. The wire
|
|
@@ -448,7 +358,6 @@ export const wsFrameHandlers: Record<string, WsFrameHandler> = {
|
|
|
448
358
|
bootstrap_response: (session, message) => { session.handleBootstrapResponse(message.payload); },
|
|
449
359
|
presence_update: (session, message) => { session.handlePresenceUpdate(message); },
|
|
450
360
|
mutation_result: handleMutationResult,
|
|
451
|
-
claim_ack: handleClaimAck,
|
|
452
361
|
subscription_ack: handleSubscriptionAck,
|
|
453
362
|
delta: handleDeltaFrame,
|
|
454
363
|
};
|
|
@@ -497,17 +406,6 @@ const validatedFrameHandlers: Record<
|
|
|
497
406
|
session.handlePresenceUpdate({ payload });
|
|
498
407
|
},
|
|
499
408
|
),
|
|
500
|
-
claim_expired: validating(
|
|
501
|
-
WS_INBOUND_FRAMES.claim_expired.payload,
|
|
502
|
-
'claim_expired',
|
|
503
|
-
(session, payload) => {
|
|
504
|
-
// Server-initiated expiry. The claim is already inactive by the time
|
|
505
|
-
// this arrives, so a consumer either re-claims with a fresh credential
|
|
506
|
-
// or accepts the drop.
|
|
507
|
-
recordClaim(tracePorts(session), 'expired', payload);
|
|
508
|
-
session.emit('claim_expired', payload);
|
|
509
|
-
},
|
|
510
|
-
),
|
|
511
409
|
claim_rejected: validating(
|
|
512
410
|
WS_INBOUND_FRAMES.claim_rejected.payload,
|
|
513
411
|
'claim_rejected',
|
|
@@ -3,7 +3,14 @@ import {
|
|
|
3
3
|
AbloConnectionError,
|
|
4
4
|
AbloSessionError,
|
|
5
5
|
errorFromWire,
|
|
6
|
+
isAccessCredentialExpiryCloseReason,
|
|
6
7
|
} from '../../errors.js';
|
|
8
|
+
import { classifyCredentialKind } from '../../auth/credentialKind.js';
|
|
9
|
+
import {
|
|
10
|
+
credentialToken,
|
|
11
|
+
type CredentialProviderResult,
|
|
12
|
+
} from '../../auth/credentialResult.js';
|
|
13
|
+
import { CredentialLifecycle } from '../../sessions/lifecycle.js';
|
|
7
14
|
import {
|
|
8
15
|
clientSyncDeltaSchema,
|
|
9
16
|
type ClientSyncDelta,
|
|
@@ -21,7 +28,6 @@ import type {
|
|
|
21
28
|
WebSocketObserveOptions,
|
|
22
29
|
WebSocketObservedDelta,
|
|
23
30
|
WebSocketPresence,
|
|
24
|
-
WebSocketJoinInput,
|
|
25
31
|
WebSocketSessionOptions,
|
|
26
32
|
OpenCollaborationEvents,
|
|
27
33
|
} from './sessionContract.js';
|
|
@@ -125,17 +131,29 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
125
131
|
private closed = false;
|
|
126
132
|
private currentToken: string;
|
|
127
133
|
private acknowledgeLane: Promise<void> = Promise.resolve();
|
|
134
|
+
private terminalError: Error | null = null;
|
|
135
|
+
private readonly terminalListeners = new Set<(error: Error) => void>();
|
|
136
|
+
private readonly credentialLifecycle: CredentialLifecycle | null;
|
|
137
|
+
private pendingSessionError: Error | null = null;
|
|
128
138
|
|
|
129
139
|
readonly presence: WebSocketPresence;
|
|
130
140
|
readonly collaboration: WebSocketCollaboration<TEvents>;
|
|
131
141
|
|
|
132
|
-
constructor(
|
|
142
|
+
constructor(
|
|
143
|
+
options: WebSocketSessionOptions,
|
|
144
|
+
position: StoredPosition,
|
|
145
|
+
credential: CredentialProviderResult,
|
|
146
|
+
) {
|
|
133
147
|
this.options = options;
|
|
148
|
+
const token = credentialToken(credential);
|
|
149
|
+
if (!token) {
|
|
150
|
+
throw new AbloSessionError('The WebSocket transport requires an authenticated credential.');
|
|
151
|
+
}
|
|
134
152
|
this.currentToken = token;
|
|
135
153
|
this.cursorKey = options.cursorKey ?? 'websocket';
|
|
136
154
|
this.socket = new AgentWebSocket<TEvents>({
|
|
137
155
|
baseUrl: options.baseUrl,
|
|
138
|
-
kind: 'agent',
|
|
156
|
+
kind: classifyCredentialKind(token) === 'ephemeral' ? 'user' : 'agent',
|
|
139
157
|
getAuthToken: () => this.currentToken,
|
|
140
158
|
syncGroups: [...(options.syncGroups ?? [])],
|
|
141
159
|
collaborationEvents: [...(options.collaborationEvents ?? [])],
|
|
@@ -148,23 +166,65 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
148
166
|
this.collaboration = {
|
|
149
167
|
send: (event, payload) => this.socket.sendCollaborationEvent(event, payload),
|
|
150
168
|
} as WebSocketCollaboration<TEvents>;
|
|
151
|
-
this.socket.subscribe('session_error', () => {
|
|
152
|
-
|
|
169
|
+
this.socket.subscribe('session_error', (error) => {
|
|
170
|
+
this.handleSessionError(error);
|
|
153
171
|
});
|
|
172
|
+
this.credentialLifecycle = options.access.renewable
|
|
173
|
+
? new CredentialLifecycle({
|
|
174
|
+
setAuthToken: (freshToken) => {
|
|
175
|
+
this.currentToken = freshToken;
|
|
176
|
+
},
|
|
177
|
+
nudgeReconnect: () => {
|
|
178
|
+
if (this.closed || this.terminalError) return;
|
|
179
|
+
this.socket.clearSessionError();
|
|
180
|
+
this.socket.resetReconnectAttempts();
|
|
181
|
+
this.socket.connect();
|
|
182
|
+
},
|
|
183
|
+
reportSessionExpired: () => {
|
|
184
|
+
this.failTerminal(this.pendingSessionError ?? new AbloSessionError(
|
|
185
|
+
'The application session ended while renewing its Ablo session.',
|
|
186
|
+
));
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
: null;
|
|
190
|
+
if (this.credentialLifecycle) {
|
|
191
|
+
this.credentialLifecycle.start(() => this.loadCredential(), { proactiveInNode: true });
|
|
192
|
+
this.credentialLifecycle.accept(credential);
|
|
193
|
+
}
|
|
154
194
|
}
|
|
155
195
|
|
|
156
|
-
private async
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
196
|
+
private async loadCredential(): Promise<CredentialProviderResult> {
|
|
197
|
+
return this.options.access.credential();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
private handleSessionError(error: Error): void {
|
|
201
|
+
if (
|
|
202
|
+
!this.credentialLifecycle
|
|
203
|
+
|| !isAccessCredentialExpiryCloseReason(error.message)
|
|
204
|
+
) {
|
|
205
|
+
this.failTerminal(error);
|
|
206
|
+
return;
|
|
167
207
|
}
|
|
208
|
+
this.pendingSessionError = error;
|
|
209
|
+
void this.credentialLifecycle.recoverFromAuthRejection('access_credential_expiry');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private failTerminal(error: Error): void {
|
|
213
|
+
if (this.terminalError || this.closed) return;
|
|
214
|
+
this.terminalError = error;
|
|
215
|
+
this.socket.setSessionErrorDetected();
|
|
216
|
+
this.socket.disconnect();
|
|
217
|
+
for (const listener of this.terminalListeners) listener(error);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private onTerminal(listener: (error: Error) => void): () => void {
|
|
221
|
+
if (this.terminalError) {
|
|
222
|
+
const error = this.terminalError;
|
|
223
|
+
queueMicrotask(() => listener(error));
|
|
224
|
+
return () => undefined;
|
|
225
|
+
}
|
|
226
|
+
this.terminalListeners.add(listener);
|
|
227
|
+
return () => this.terminalListeners.delete(listener);
|
|
168
228
|
}
|
|
169
229
|
|
|
170
230
|
get connected(): boolean {
|
|
@@ -175,6 +235,7 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
175
235
|
if (this.closed) {
|
|
176
236
|
return Promise.reject(new AbloConnectionError('The WebSocket session is closed.'));
|
|
177
237
|
}
|
|
238
|
+
if (this.terminalError) return Promise.reject(this.terminalError);
|
|
178
239
|
if (this.socket.isConnected()) return Promise.resolve();
|
|
179
240
|
if (this.readyPromise) return this.readyPromise;
|
|
180
241
|
this.readyPromise = new Promise<void>((resolve, reject) => {
|
|
@@ -190,7 +251,7 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
190
251
|
cleanup();
|
|
191
252
|
resolve();
|
|
192
253
|
});
|
|
193
|
-
const failed = this.
|
|
254
|
+
const failed = this.onTerminal((error) => {
|
|
194
255
|
rejectConnection(error);
|
|
195
256
|
});
|
|
196
257
|
const mismatch = this.socket.subscribe('protocol_mismatch', () => {
|
|
@@ -311,19 +372,6 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
311
372
|
this.socket.send({ type: 'claim_abandon', payload: input });
|
|
312
373
|
}
|
|
313
374
|
|
|
314
|
-
async join(input: WebSocketJoinInput) {
|
|
315
|
-
await this.ready();
|
|
316
|
-
return this.socket.sendClaim(input.claimId, input.syncGroups, {
|
|
317
|
-
capabilityToken: input.capabilityToken,
|
|
318
|
-
ttlSeconds: input.ttlSeconds,
|
|
319
|
-
timeoutMs: input.timeoutMs,
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
leave(claimId: string): void {
|
|
324
|
-
this.socket.sendRelease(claimId);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
375
|
subscribe<K extends keyof SyncWebSocketEventMap<TEvents>>(
|
|
328
376
|
event: K,
|
|
329
377
|
listener: (...args: SyncWebSocketEventMap<TEvents>[K]) => void,
|
|
@@ -357,7 +405,7 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
357
405
|
wake?.();
|
|
358
406
|
wake = undefined;
|
|
359
407
|
});
|
|
360
|
-
const offError = this.
|
|
408
|
+
const offError = this.onTerminal((error) => {
|
|
361
409
|
failure = error;
|
|
362
410
|
wake?.();
|
|
363
411
|
wake = undefined;
|
|
@@ -421,6 +469,8 @@ class WebSocketSession<TEvents extends EventMap<TEvents>>
|
|
|
421
469
|
async close(): Promise<void> {
|
|
422
470
|
if (this.closed) return;
|
|
423
471
|
this.closed = true;
|
|
472
|
+
this.credentialLifecycle?.stop();
|
|
473
|
+
this.terminalListeners.clear();
|
|
424
474
|
this.rejectReady?.(new AbloConnectionError('The WebSocket session was disposed.'));
|
|
425
475
|
await this.acknowledgeLane;
|
|
426
476
|
this.socket.disconnect();
|
|
@@ -439,11 +489,16 @@ export async function createWebSocketSession<
|
|
|
439
489
|
): Promise<AbloWebSocketSession<TEvents>> {
|
|
440
490
|
const cursorKey = options.cursorKey ?? 'websocket';
|
|
441
491
|
const stored = await options.cursorStore?.load(cursorKey) ?? null;
|
|
442
|
-
const
|
|
492
|
+
const credential = await options.access.credential();
|
|
493
|
+
const token = credentialToken(credential);
|
|
443
494
|
if (!token) {
|
|
444
495
|
throw new AbloSessionError('The WebSocket transport requires an authenticated credential.');
|
|
445
496
|
}
|
|
446
|
-
const session = new WebSocketSession<TEvents>(
|
|
497
|
+
const session = new WebSocketSession<TEvents>(
|
|
498
|
+
options,
|
|
499
|
+
parsePosition(stored),
|
|
500
|
+
credential,
|
|
501
|
+
);
|
|
447
502
|
let rejectOpening: ((error: Error) => void) | undefined;
|
|
448
503
|
const aborted = new Promise<never>((_resolve, reject) => {
|
|
449
504
|
rejectOpening = reject;
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from '../../coordination/schema.js';
|
|
9
9
|
import type { ClientSyncDelta } from '../../observation/contract.js';
|
|
10
10
|
import type { ObserveCursorStore } from '../../client/contract.js';
|
|
11
|
+
import type { SessionAccess } from '../../sessions/source.js';
|
|
11
12
|
import type { CommitFrameOperation } from '../websocket/commitFrames.js';
|
|
12
13
|
import type {
|
|
13
14
|
CoreSyncEventMap,
|
|
@@ -18,7 +19,8 @@ import type {
|
|
|
18
19
|
|
|
19
20
|
export interface WebSocketSessionOptions {
|
|
20
21
|
readonly baseUrl?: string;
|
|
21
|
-
|
|
22
|
+
/** Normalized once at the client boundary; transports do not infer policy. */
|
|
23
|
+
readonly access: SessionAccess;
|
|
22
24
|
readonly syncGroups?: readonly string[];
|
|
23
25
|
readonly collaborationEvents?: readonly string[];
|
|
24
26
|
readonly cursorKey?: string;
|
|
@@ -44,15 +46,6 @@ export interface WebSocketObserveOptions {
|
|
|
44
46
|
readonly signal?: AbortSignal;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
/** A multiplexed participant-scope registration on the shared socket. */
|
|
48
|
-
export interface WebSocketJoinInput {
|
|
49
|
-
readonly claimId: string;
|
|
50
|
-
readonly syncGroups: readonly string[];
|
|
51
|
-
readonly capabilityToken?: string;
|
|
52
|
-
readonly ttlSeconds?: number;
|
|
53
|
-
readonly timeoutMs?: number;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
49
|
/** A durable row/field claim carried over the same live protocol. */
|
|
57
50
|
export type WebSocketClaimInput = ClaimBeginPayload & {
|
|
58
51
|
readonly timeoutMs?: number;
|
|
@@ -95,8 +88,6 @@ export interface AbloWebSocketSession<
|
|
|
95
88
|
readonly entityType?: string;
|
|
96
89
|
readonly entityId?: string;
|
|
97
90
|
}): void;
|
|
98
|
-
join(input: WebSocketJoinInput): Promise<{ syncGroups: string[]; ttlSeconds?: number }>;
|
|
99
|
-
leave(claimId: string): void;
|
|
100
91
|
subscribe<K extends keyof SyncWebSocketEventMap<TEvents>>(
|
|
101
92
|
event: K,
|
|
102
93
|
listener: (...args: SyncWebSocketEventMap<TEvents>[K]) => void,
|