@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.
Files changed (235) hide show
  1. package/dist/auth/apiKey.d.ts +10 -11
  2. package/dist/auth/apiKey.d.ts.map +1 -1
  3. package/dist/auth/apiKey.js +15 -29
  4. package/dist/auth/apiKey.js.map +1 -1
  5. package/dist/auth/browserCredentialSafety.js +1 -1
  6. package/dist/auth/browserCredentialSafety.js.map +1 -1
  7. package/dist/auth/capability.d.ts +15 -15
  8. package/dist/auth/capabilityLifecycle.d.ts +3 -3
  9. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  10. package/dist/auth/capabilityLifecycle.js +2 -1
  11. package/dist/auth/capabilityLifecycle.js.map +1 -1
  12. package/dist/auth/credentialEndpoint.js +1 -1
  13. package/dist/auth/credentialEndpoint.js.map +1 -1
  14. package/dist/auth/credentialResult.d.ts +2 -1
  15. package/dist/auth/credentialResult.d.ts.map +1 -1
  16. package/dist/auth/credentialResult.js.map +1 -1
  17. package/dist/auth/credentialSource.js.map +1 -1
  18. package/dist/auth/identity.js.map +1 -1
  19. package/dist/auth/schemas.d.ts +5 -5
  20. package/dist/auth/sessionIssuanceProtocol.d.ts +8 -8
  21. package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -1
  22. package/dist/branches.d.ts +38 -38
  23. package/dist/claims/contract.d.ts +27 -27
  24. package/dist/claims/eventContract.d.ts +18 -18
  25. package/dist/claims/events.d.ts +1 -1
  26. package/dist/claims/events.d.ts.map +1 -1
  27. package/dist/claims/routes.d.ts +9 -9
  28. package/dist/claims/routes.d.ts.map +1 -1
  29. package/dist/claims/trace.d.ts.map +1 -1
  30. package/dist/claims/trace.js.map +1 -1
  31. package/dist/client/ablo.d.ts +11 -8
  32. package/dist/client/ablo.d.ts.map +1 -1
  33. package/dist/client/ablo.js +5 -6
  34. package/dist/client/ablo.js.map +1 -1
  35. package/dist/client/resources/httpResources.d.ts +1 -169
  36. package/dist/client/resources/httpResources.d.ts.map +1 -1
  37. package/dist/client/resources/httpResources.js +1 -1
  38. package/dist/client/resources/httpResources.js.map +1 -1
  39. package/dist/client/resources/modelOperations.d.ts +6 -15
  40. package/dist/client/resources/modelOperations.d.ts.map +1 -1
  41. package/dist/client/resources/where.d.ts +12 -12
  42. package/dist/client/surface.d.ts +1 -1
  43. package/dist/client/surface.d.ts.map +1 -1
  44. package/dist/client/surface.js +2 -4
  45. package/dist/client/surface.js.map +1 -1
  46. package/dist/commit/confirmation/commitEnvelope.d.ts +7 -7
  47. package/dist/commit/confirmation/httpCommitEnvelope.d.ts +1 -1
  48. package/dist/commit/confirmation/pendingWrite.d.ts +5 -5
  49. package/dist/commit/contract.d.ts +249 -249
  50. package/dist/commit/contract.d.ts.map +1 -1
  51. package/dist/coordination/index.d.ts +2 -2
  52. package/dist/coordination/index.d.ts.map +1 -1
  53. package/dist/coordination/index.js +3 -4
  54. package/dist/coordination/index.js.map +1 -1
  55. package/dist/coordination/schema.d.ts +57 -101
  56. package/dist/coordination/schema.d.ts.map +1 -1
  57. package/dist/coordination/schema.js +7 -69
  58. package/dist/coordination/schema.js.map +1 -1
  59. package/dist/environment.d.ts +3 -3
  60. package/dist/environment.d.ts.map +1 -1
  61. package/dist/environment.js.map +1 -1
  62. package/dist/errorCodes.d.ts +4 -5
  63. package/dist/errorCodes.d.ts.map +1 -1
  64. package/dist/errorCodes.js +3 -8
  65. package/dist/errorCodes.js.map +1 -1
  66. package/dist/errorObservation.d.ts +14 -14
  67. package/dist/errors.d.ts +11 -11
  68. package/dist/errors.d.ts.map +1 -1
  69. package/dist/errors.js.map +1 -1
  70. package/dist/keys/index.d.ts +1 -1
  71. package/dist/keys/index.d.ts.map +1 -1
  72. package/dist/keys/index.js.map +1 -1
  73. package/dist/observation/contract.d.ts +34 -34
  74. package/dist/observation/feedContract.d.ts +86 -86
  75. package/dist/observation/persistence/syncDeltaRow.d.ts +23 -23
  76. package/dist/observation/persistence/syncDeltaRow.d.ts.map +1 -1
  77. package/dist/pricing.d.ts +12 -12
  78. package/dist/schema/audit.js.map +1 -1
  79. package/dist/schema/ddl.js.map +1 -1
  80. package/dist/schema/ddlLock.js.map +1 -1
  81. package/dist/schema/deployment/contracts.d.ts +64 -64
  82. package/dist/schema/deployment/index.js.map +1 -1
  83. package/dist/schema/deployment/reconcile.js.map +1 -1
  84. package/dist/schema/diff.js.map +1 -1
  85. package/dist/schema/field.d.ts +1 -1
  86. package/dist/schema/field.d.ts.map +1 -1
  87. package/dist/schema/loadStrategy.d.ts +2 -2
  88. package/dist/schema/loadStrategy.d.ts.map +1 -1
  89. package/dist/schema/openapi.js.map +1 -1
  90. package/dist/schema/relation.d.ts +3 -3
  91. package/dist/schema/relation.d.ts.map +1 -1
  92. package/dist/schema/residency.d.ts +1 -1
  93. package/dist/schema/roles.d.ts +1 -1
  94. package/dist/schema/roles.d.ts.map +1 -1
  95. package/dist/schema/roles.js.map +1 -1
  96. package/dist/schema/schema.d.ts +1 -1
  97. package/dist/schema/schema.d.ts.map +1 -1
  98. package/dist/schema/serialize.js.map +1 -1
  99. package/dist/schema/sugar.d.ts.map +1 -1
  100. package/dist/schema/tenancy.d.ts +2 -2
  101. package/dist/server/storageMode.d.ts +1 -1
  102. package/dist/sessions/client.d.ts +29 -0
  103. package/dist/sessions/client.d.ts.map +1 -0
  104. package/dist/sessions/client.js +69 -0
  105. package/dist/sessions/client.js.map +1 -0
  106. package/dist/sessions/contract.d.ts +97 -0
  107. package/dist/sessions/contract.d.ts.map +1 -0
  108. package/dist/sessions/contract.js +5 -0
  109. package/dist/sessions/contract.js.map +1 -0
  110. package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
  111. package/dist/sessions/create.d.ts.map +1 -0
  112. package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
  113. package/dist/sessions/create.js.map +1 -0
  114. package/dist/sessions/handler.d.ts +23 -0
  115. package/dist/sessions/handler.d.ts.map +1 -0
  116. package/dist/sessions/handler.js +55 -0
  117. package/dist/sessions/handler.js.map +1 -0
  118. package/dist/sessions/index.d.ts +14 -0
  119. package/dist/sessions/index.d.ts.map +1 -0
  120. package/dist/sessions/index.js +14 -0
  121. package/dist/sessions/index.js.map +1 -0
  122. package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
  123. package/dist/sessions/lifecycle.d.ts.map +1 -0
  124. package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
  125. package/dist/sessions/lifecycle.js.map +1 -0
  126. package/dist/sessions/source.d.ts +24 -0
  127. package/dist/sessions/source.d.ts.map +1 -0
  128. package/dist/sessions/source.js +56 -0
  129. package/dist/sessions/source.js.map +1 -0
  130. package/dist/source/adapters/adapterProfile.d.ts +6 -6
  131. package/dist/source/adapters/conformance.js.map +1 -1
  132. package/dist/source/adapters/contract.d.ts +14 -14
  133. package/dist/source/adapters/kysely.js.map +1 -1
  134. package/dist/source/adapters/memory.js.map +1 -1
  135. package/dist/source/adapters/prisma.js.map +1 -1
  136. package/dist/source/connector/client.d.ts.map +1 -1
  137. package/dist/source/connector/client.js.map +1 -1
  138. package/dist/source/connector/protocol.d.ts.map +1 -1
  139. package/dist/source/delivery/pushQueue.d.ts.map +1 -1
  140. package/dist/source/delivery/pushQueue.js.map +1 -1
  141. package/dist/source/endpoint/signing.d.ts +4 -4
  142. package/dist/source/endpoint/signing.d.ts.map +1 -1
  143. package/dist/source/outbox/contract.d.ts +4 -4
  144. package/dist/testing/fixtures/httpResponses.d.ts +2 -2
  145. package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
  146. package/dist/testing/fixtures/httpResponses.js.map +1 -1
  147. package/dist/transport/connection/index.d.ts +0 -1
  148. package/dist/transport/connection/index.d.ts.map +1 -1
  149. package/dist/transport/connection/index.js +0 -1
  150. package/dist/transport/connection/index.js.map +1 -1
  151. package/dist/transport/connection/manager.d.ts +1 -1
  152. package/dist/transport/connection/manager.d.ts.map +1 -1
  153. package/dist/transport/connection/manager.js.map +1 -1
  154. package/dist/transport/connection/networkProbe.d.ts +5 -18
  155. package/dist/transport/connection/networkProbe.d.ts.map +1 -1
  156. package/dist/transport/connection/networkProbe.js.map +1 -1
  157. package/dist/transport/http/client.d.ts +11 -19
  158. package/dist/transport/http/client.d.ts.map +1 -1
  159. package/dist/transport/http/client.js +12 -14
  160. package/dist/transport/http/client.js.map +1 -1
  161. package/dist/transport/http/options.d.ts +4 -1
  162. package/dist/transport/http/options.d.ts.map +1 -1
  163. package/dist/transport/http/transport.d.ts +4 -10
  164. package/dist/transport/http/transport.d.ts.map +1 -1
  165. package/dist/transport/http/transport.js +9 -51
  166. package/dist/transport/http/transport.js.map +1 -1
  167. package/dist/transport/websocket/commitFrames.d.ts +1 -2
  168. package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
  169. package/dist/transport/websocket/commitFrames.js.map +1 -1
  170. package/dist/transport/websocket/frameHandlers.d.ts +0 -14
  171. package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
  172. package/dist/transport/websocket/frameHandlers.js +1 -71
  173. package/dist/transport/websocket/frameHandlers.js.map +1 -1
  174. package/dist/transport/websocket/heartbeat.d.ts.map +1 -1
  175. package/dist/transport/websocket/heartbeat.js.map +1 -1
  176. package/dist/transport/websocket/session.d.ts.map +1 -1
  177. package/dist/transport/websocket/session.js +72 -32
  178. package/dist/transport/websocket/session.js.map +1 -1
  179. package/dist/transport/websocket/sessionContract.d.ts +3 -14
  180. package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
  181. package/dist/transport/websocket/transport.d.ts +5 -42
  182. package/dist/transport/websocket/transport.d.ts.map +1 -1
  183. package/dist/transport/websocket/transport.js +14 -101
  184. package/dist/transport/websocket/transport.js.map +1 -1
  185. package/dist/wire/accountResponses.d.ts +31 -31
  186. package/dist/wire/auth.d.ts +3 -3
  187. package/dist/wire/bootstrapReason.d.ts +2 -2
  188. package/dist/wire/dataSourceResponses.d.ts +2 -2
  189. package/dist/wire/dataSourceResponses.d.ts.map +1 -1
  190. package/dist/wire/frames.d.ts +8 -8
  191. package/dist/wire/inboundFrames.d.ts +53 -63
  192. package/dist/wire/inboundFrames.d.ts.map +1 -1
  193. package/dist/wire/inboundFrames.js +1 -6
  194. package/dist/wire/inboundFrames.js.map +1 -1
  195. package/dist/wire/listEnvelope.js.map +1 -1
  196. package/dist/wire/modelResponses.d.ts +2 -2
  197. package/dist/wire/modelShape.d.ts +4 -4
  198. package/dist/wire/pricing.d.ts +3 -3
  199. package/dist/wire/protocolVersion.d.ts.map +1 -1
  200. package/package.json +6 -1
  201. package/src/auth/apiKey.ts +25 -44
  202. package/src/auth/browserCredentialSafety.ts +1 -1
  203. package/src/auth/capabilityLifecycle.ts +3 -2
  204. package/src/auth/credentialEndpoint.ts +1 -1
  205. package/src/auth/credentialResult.ts +2 -0
  206. package/src/claims/events.ts +1 -2
  207. package/src/client/ablo.ts +15 -10
  208. package/src/client/resources/httpResources.ts +2 -174
  209. package/src/client/resources/modelOperations.ts +0 -10
  210. package/src/client/surface.ts +2 -4
  211. package/src/coordination/index.ts +2 -9
  212. package/src/coordination/schema.ts +7 -81
  213. package/src/errorCodes.ts +3 -12
  214. package/src/sessions/client.ts +121 -0
  215. package/src/sessions/contract.ts +116 -0
  216. package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
  217. package/src/sessions/handler.ts +96 -0
  218. package/src/sessions/index.ts +13 -0
  219. package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
  220. package/src/sessions/source.ts +94 -0
  221. package/src/transport/connection/index.ts +0 -1
  222. package/src/transport/connection/manager.ts +1 -1
  223. package/src/transport/http/client.ts +21 -31
  224. package/src/transport/http/options.ts +4 -1
  225. package/src/transport/http/transport.ts +14 -74
  226. package/src/transport/websocket/commitFrames.ts +0 -2
  227. package/src/transport/websocket/frameHandlers.ts +0 -102
  228. package/src/transport/websocket/session.ts +88 -33
  229. package/src/transport/websocket/sessionContract.ts +3 -12
  230. package/src/transport/websocket/transport.ts +13 -137
  231. package/src/wire/inboundFrames.ts +0 -6
  232. package/dist/auth/sessionMint.d.ts.map +0 -1
  233. package/dist/auth/sessionMint.js.map +0 -1
  234. package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
  235. 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 { mintSession } from '../../auth/sessionMint.js';
94
- import {
95
- revokeCapability,
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
- * Mint a short-lived scoped session. Minting is a control-plane HTTP call (no
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 resolveApiKeyValue(configuredApiKey)) ?? configuredAuthToken ?? null;
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(options: WebSocketSessionOptions, position: StoredPosition, token: string) {
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
- void this.refreshExpiredCredential();
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 refreshExpiredCredential(): Promise<void> {
157
- if (this.closed) return;
158
- try {
159
- const next = await this.options.getAuthToken();
160
- if (!next || next === this.currentToken) return;
161
- this.currentToken = next;
162
- this.socket.clearSessionError();
163
- this.socket.connect();
164
- } catch {
165
- // The terminal session_error remains the public signal. A later explicit
166
- // ready() call can retry after the caller repairs its credential source.
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.socket.subscribe('session_error', (error) => {
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.socket.subscribe('session_error', (error) => {
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 token = await options.getAuthToken();
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>(options, parsePosition(stored), token);
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
- readonly getAuthToken: () => string | undefined | Promise<string | undefined>;
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,