@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
@@ -0,0 +1,116 @@
1
+ import type {
2
+ CapabilityCan,
3
+ CapabilityOperation,
4
+ EffectiveAuthority,
5
+ } from '../auth/capability.js';
6
+ import type { CredentialEndpointSuccess } from '../auth/credentialEndpointProtocol.js';
7
+ import type { SchemaRecord } from '../schema/schema.js';
8
+ import type { SyncGroupInput } from '../schema/roles.js';
9
+
10
+ /** The bearer-bearing part of a session returned by `sessions.create()`. */
11
+ export interface SessionCredential {
12
+ readonly object: 'session';
13
+ /** The short-lived `ek_` or `rk_` bearer used by the scoped client. */
14
+ readonly token: string;
15
+ /** ISO-8601 hard expiry for this credential. */
16
+ readonly expiresAt: string;
17
+ }
18
+
19
+ /** Result accepted from a session provider or canonical browser mint endpoint. */
20
+ export type SessionProviderResult =
21
+ | SessionCredential
22
+ | CredentialEndpointSuccess
23
+ | null;
24
+
25
+ /** Re-mints the same logical actor's scoped session for a long-lived client. */
26
+ export type SessionProvider = () => Promise<SessionProviderResult>;
27
+
28
+ /** Browser-safe route that mints the signed-in actor's short-lived session. */
29
+ export interface SessionEndpoint {
30
+ readonly endpoint: string;
31
+ readonly timeoutMs?: number;
32
+ readonly allowCrossOrigin?: boolean;
33
+ }
34
+
35
+ /** Every supported source for `Ablo({ session })`. */
36
+ export type SessionSource = SessionCredential | SessionProvider | SessionEndpoint;
37
+
38
+ /** Public session scope; transport-specific sync naming stays below this boundary. */
39
+ export type SessionScope = Omit<EffectiveAuthority, 'syncGroups'> & {
40
+ readonly groups: readonly string[];
41
+ };
42
+
43
+ export function sessionScope(authority: EffectiveAuthority): SessionScope {
44
+ const { syncGroups, ...scope } = authority;
45
+ return { ...scope, groups: syncGroups };
46
+ }
47
+
48
+ /** A single data operation a scoped session may perform on a model. */
49
+ export type SessionOperation = CapabilityOperation;
50
+
51
+ /** Parameters for issuing a short-lived end-user (`ek_`) session. */
52
+ export interface CreateUserSessionParams<S extends SchemaRecord> {
53
+ readonly user: { readonly id: string };
54
+ readonly organizationId?: string;
55
+ readonly schemaProject?: {
56
+ readonly organizationId: string;
57
+ readonly projectId: string;
58
+ };
59
+ readonly groups?: readonly SyncGroupInput[];
60
+ readonly can: CapabilityCan<S>;
61
+ readonly ttlSeconds?: number;
62
+ readonly userMeta?: Record<string, unknown>;
63
+ readonly agent?: never;
64
+ }
65
+
66
+ /** Parameters for issuing a short-lived scoped agent (`rk_`) session. */
67
+ export interface CreateAgentSessionParams<S extends SchemaRecord> {
68
+ readonly agent: { readonly id: string };
69
+ readonly onBehalfOf?: { readonly user: { readonly id: string } };
70
+ readonly can: CapabilityCan<S>;
71
+ readonly groups?: readonly SyncGroupInput[];
72
+ readonly ttlSeconds?: number;
73
+ readonly userMeta?: Record<string, unknown>;
74
+ readonly user?: never;
75
+ }
76
+
77
+ /** The one typed issuance input; its subject selects user or agent identity. */
78
+ export type CreateSessionParams<S extends SchemaRecord> =
79
+ | CreateUserSessionParams<S>
80
+ | CreateAgentSessionParams<S>;
81
+
82
+ /** A minted session. `token` is the secret the holder presents as its bearer. */
83
+ export interface AbloSession extends SessionCredential {
84
+ readonly id: string;
85
+ readonly organizationId: string;
86
+ readonly scope: SessionScope;
87
+ readonly userMeta: Record<string, unknown>;
88
+ }
89
+
90
+ export interface SessionRevocation {
91
+ readonly id: string;
92
+ readonly deleted: true;
93
+ readonly activeSessionsClosed: number;
94
+ }
95
+
96
+ export interface SessionRotation {
97
+ readonly id: string;
98
+ readonly token: string;
99
+ readonly expiresAt: string | null;
100
+ readonly organizationId: string;
101
+ readonly scope: SessionScope;
102
+ readonly rotatedFrom: {
103
+ readonly id: string;
104
+ readonly expiresAt: string;
105
+ };
106
+ }
107
+
108
+ export interface RevokeSessionParams {
109
+ readonly id: string;
110
+ }
111
+
112
+ export interface RotateSessionParams {
113
+ readonly id: string;
114
+ readonly graceSeconds?: number;
115
+ readonly ttlSeconds?: number;
116
+ }
@@ -18,27 +18,28 @@
18
18
  * registered names.
19
19
  *
20
20
  * The caller supplies the already-resolved secret key and base URL in
21
- * {@link MintSessionContext}. Choosing which key to pass — the original secret
21
+ * {@link CreateSessionContext}. Choosing which key to pass — the original secret
22
22
  * key, not a derived key that an earlier exchange may have produced — is the
23
23
  * caller's responsibility.
24
24
  */
25
25
  import {
26
26
  exchangeApiKey,
27
27
  mintUserSessionKey,
28
- } from './runtime.js';
28
+ } from '../auth/runtime.js';
29
29
  import {
30
30
  capabilityCanSchemaFor,
31
31
  grantedOperations,
32
- } from './capability.js';
32
+ } from '../auth/capability.js';
33
33
  import type { SchemaRecord } from '../schema/schema.js';
34
- import type { AbloSession, CreateSessionParams } from '../client/resources/httpResources.js';
34
+ import type { AbloSession, CreateSessionParams } from './contract.js';
35
+ import { sessionScope } from './contract.js';
35
36
 
36
37
  /**
37
38
  * The resolved control-plane details a mint needs: a secret key, a base URL,
38
39
  * and an optional `fetch`. When `fetch` is omitted, the auth helpers fall back
39
40
  * to the runtime's global `fetch`.
40
41
  */
41
- export interface MintSessionContext {
42
+ export interface CreateSessionContext {
42
43
  readonly apiKey: string;
43
44
  readonly baseUrl: string;
44
45
  readonly fetch?: typeof fetch;
@@ -59,9 +60,9 @@ export interface MintSessionContext {
59
60
  * Routes the `{ user }` or `{ agent }` request to the matching mint endpoint
60
61
  * and reshapes the response into an {@link AbloSession}.
61
62
  */
62
- export async function mintSession<S extends SchemaRecord>(
63
+ export async function createSession<S extends SchemaRecord>(
63
64
  params: CreateSessionParams<S>,
64
- ctx: MintSessionContext,
65
+ ctx: CreateSessionContext,
65
66
  ): Promise<AbloSession> {
66
67
  const { apiKey, baseUrl } = ctx;
67
68
  // Static typing and runtime validation consume the same schema-bound grant
@@ -77,7 +78,7 @@ export async function mintSession<S extends SchemaRecord>(
77
78
  userId: params.user.id,
78
79
  ...(params.organizationId ? { organizationId: params.organizationId } : {}),
79
80
  ...(params.schemaProject ? { schemaProject: params.schemaProject } : {}),
80
- ...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
81
+ ...(params.groups ? { syncGroups: [...params.groups] } : {}),
81
82
  operations,
82
83
  ttlSeconds: params.ttlSeconds ?? 900,
83
84
  ...(ctx.fetch ? { fetch: ctx.fetch } : {}),
@@ -90,7 +91,7 @@ export async function mintSession<S extends SchemaRecord>(
90
91
  organizationId: res.organizationId,
91
92
  // The ephemeral mint stores scope on the key row; reshape its flat
92
93
  // response into the session resource's scope block.
93
- scope: {
94
+ scope: sessionScope({
94
95
  organizationId: res.organizationId,
95
96
  projectId: res.projectId,
96
97
  branchId: res.branchId,
@@ -99,7 +100,7 @@ export async function mintSession<S extends SchemaRecord>(
99
100
  participantKind: 'user',
100
101
  participantId: res.participantId,
101
102
  deliveryPartition: res.deliveryPartition,
102
- },
103
+ }),
103
104
  userMeta: params.userMeta ?? { id: res.participantId },
104
105
  };
105
106
  }
@@ -113,7 +114,7 @@ export async function mintSession<S extends SchemaRecord>(
113
114
  participantKind: 'agent',
114
115
  participantId: params.agent.id,
115
116
  ...(params.onBehalfOf ? { onBehalfOf: params.onBehalfOf } : {}),
116
- ...(params.syncGroups ? { syncGroups: [...params.syncGroups] } : {}),
117
+ ...(params.groups ? { syncGroups: [...params.groups] } : {}),
117
118
  operations,
118
119
  ttlSeconds: params.ttlSeconds ?? 900,
119
120
  ...(params.userMeta ? { userMeta: params.userMeta } : {}),
@@ -125,7 +126,7 @@ export async function mintSession<S extends SchemaRecord>(
125
126
  token: res.token,
126
127
  expiresAt: res.expiresAt,
127
128
  organizationId: res.organizationId,
128
- scope: res.scope,
129
+ scope: sessionScope(res.scope),
129
130
  userMeta: res.userMeta,
130
131
  };
131
132
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Server-owned browser session exchange.
3
+ *
4
+ * Authentication providers prove who is calling; this boundary converts that
5
+ * proof into the one Ablo session resource consumed by browser clients. It
6
+ * owns the HTTP protocol so applications do not repeat origin, cache, status,
7
+ * or credential-envelope handling in every framework route.
8
+ */
9
+
10
+ import type { SchemaRecord } from '../schema/schema.js';
11
+ import type {
12
+ AbloSession,
13
+ CreateSessionParams,
14
+ } from './contract.js';
15
+ import {
16
+ credentialEndpointErrorSchema,
17
+ credentialEndpointSuccessSchema,
18
+ } from '../auth/credentialEndpointProtocol.js';
19
+ import { classifyCredentialKind } from '../auth/credentialKind.js';
20
+
21
+ export interface SessionHandlerOptions<
22
+ S extends SchemaRecord,
23
+ Principal,
24
+ > {
25
+ /** Verify the application's cookie, bearer, or framework auth session. */
26
+ authenticate(request: Request): Principal | null | Promise<Principal | null>;
27
+ /** Derive Ablo identity, groups, and access exclusively on the server. */
28
+ grant(input: {
29
+ readonly principal: Principal;
30
+ readonly request: Request;
31
+ }): CreateSessionParams<S> | null | Promise<CreateSessionParams<S> | null>;
32
+ }
33
+
34
+ export type SessionHandler = (request: Request) => Promise<Response>;
35
+
36
+ const NO_STORE_HEADERS = { 'Cache-Control': 'no-store' } as const;
37
+
38
+ function errorResponse(
39
+ status: number,
40
+ code: string,
41
+ message: string,
42
+ ): Response {
43
+ return Response.json(
44
+ credentialEndpointErrorSchema.parse({ error: { code, message } }),
45
+ { status, headers: NO_STORE_HEADERS },
46
+ );
47
+ }
48
+
49
+ function isSameOrigin(request: Request): boolean {
50
+ const origin = request.headers.get('origin');
51
+ if (!origin) return request.headers.get('sec-fetch-site') !== 'cross-site';
52
+ try {
53
+ return new URL(origin).origin === new URL(request.url).origin;
54
+ } catch {
55
+ return false;
56
+ }
57
+ }
58
+
59
+ /** Build a framework-neutral POST handler around one authenticated session grant. */
60
+ export function createSessionHandler<S extends SchemaRecord, Principal>(
61
+ create: (params: CreateSessionParams<S>) => Promise<AbloSession>,
62
+ options: SessionHandlerOptions<S, Principal>,
63
+ ): SessionHandler {
64
+ return async (request) => {
65
+ if (request.method !== 'POST') {
66
+ return errorResponse(405, 'method_not_allowed', 'Use POST to create a session.');
67
+ }
68
+ if (!isSameOrigin(request)) {
69
+ return errorResponse(403, 'origin_mismatch', 'Cross-origin session creation rejected.');
70
+ }
71
+
72
+ const principal = await options.authenticate(request);
73
+ if (principal === null) {
74
+ return errorResponse(401, 'session_expired', 'Sign in again.');
75
+ }
76
+
77
+ const grant = await options.grant({ principal, request });
78
+ if (grant === null) {
79
+ return errorResponse(403, 'policy_denied', 'This identity is not authorized.');
80
+ }
81
+
82
+ const session = await create(grant);
83
+ const credentialKind = classifyCredentialKind(session.token);
84
+ if (credentialKind !== 'ephemeral' && credentialKind !== 'restricted') {
85
+ throw new Error('sessions.create returned a credential that cannot authenticate a session.');
86
+ }
87
+ return Response.json(
88
+ credentialEndpointSuccessSchema.parse({
89
+ token: session.token,
90
+ expiresAt: session.expiresAt,
91
+ credentialKind,
92
+ }),
93
+ { headers: NO_STORE_HEADERS },
94
+ );
95
+ };
96
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Session subsystem.
3
+ *
4
+ * Start here and descend into the public contract, issuance, server handler,
5
+ * source normalization, or credential lifecycle. Transports consume the
6
+ * normalized access contract and do not reconstruct session policy.
7
+ */
8
+ export * from './contract.js';
9
+ export * from './client.js';
10
+ export * from './create.js';
11
+ export * from './handler.js';
12
+ export * from './source.js';
13
+ export * from './lifecycle.js';
@@ -1,20 +1,20 @@
1
1
  /**
2
2
  * Keeps the short-lived access credential fresh. It owns the re-mint hook, a
3
3
  * single-flight guard that stops concurrent triggers from minting more than
4
- * once, and a browser-only proactive refresh — a timer plus an OS-wake listener
5
- * — that renews the credential ahead of expiry. It reaches the rest of the
4
+ * once, and proactive refresh — an expiry-aware timer plus a browser OS-wake
5
+ * listener — that renews the credential ahead of expiry. It reaches the rest of the
6
6
  * client only through the small {@link CredentialLifecycleContext} interface,
7
7
  * so the two can reference each other without an import cycle.
8
8
  */
9
9
 
10
- import type { RecoveryClass } from '../../errorCodes.js';
11
- import { noopLogger, type Logger } from '../../logger.js';
10
+ import type { RecoveryClass } from '../errorCodes.js';
11
+ import { noopLogger, type Logger } from '../logger.js';
12
12
  import {
13
13
  credentialExpiry,
14
14
  credentialToken,
15
15
  type CredentialProvider,
16
16
  type CredentialProviderResult,
17
- } from '../../auth/credentialResult.js';
17
+ } from '../auth/credentialResult.js';
18
18
 
19
19
  /**
20
20
  * Tri-state outcome of a credential re-mint, mirroring the `getToken`
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Runtime lifecycle for a renewable session supplied to `Ablo({ session })`.
3
+ *
4
+ * A session provider mints a credential, while this boundary decides when to
5
+ * reuse it. Keeping that policy here prevents HTTP requests and the WebSocket
6
+ * bootstrap from independently minting duplicate sessions for the same actor.
7
+ */
8
+
9
+ import type {
10
+ CredentialProvider,
11
+ CredentialProviderResult,
12
+ } from '../auth/credentialResult.js';
13
+ import type {
14
+ SessionProvider,
15
+ SessionProviderResult,
16
+ SessionSource,
17
+ } from './contract.js';
18
+ import { AbloValidationError } from '../errors.js';
19
+ import {
20
+ createEndpointCredentialResolver,
21
+ isCredentialEndpoint,
22
+ } from '../auth/credentialEndpoint.js';
23
+ import { protectBrowserCredentialProvider } from '../auth/browserCredentialSafety.js';
24
+
25
+ const SESSION_REFRESH_SKEW_MS = 30_000;
26
+
27
+ function remainsUsable(session: Exclude<SessionProviderResult, null>, now: number): boolean {
28
+ const expiresAt = Date.parse(session.expiresAt);
29
+ return Number.isFinite(expiresAt) && expiresAt - now > SESSION_REFRESH_SKEW_MS;
30
+ }
31
+
32
+ /** Cache one minted session until it approaches expiry; concurrent refreshes share one mint. */
33
+ export function cachedSessionProvider(provider: SessionProvider): CredentialProvider {
34
+ let current: Exclude<SessionProviderResult, null> | null = null;
35
+ let refreshing: Promise<SessionProviderResult> | null = null;
36
+
37
+ return async () => {
38
+ if (current && remainsUsable(current, Date.now())) return current;
39
+ refreshing ??= provider().then((next) => {
40
+ current = next;
41
+ return next;
42
+ }).finally(() => {
43
+ refreshing = null;
44
+ });
45
+ return refreshing;
46
+ };
47
+ }
48
+
49
+ /** The one credential input a live transport consumes. */
50
+ export interface SessionAccess {
51
+ readonly renewable: boolean;
52
+ credential(): Promise<CredentialProviderResult>;
53
+ }
54
+
55
+ /** Normalize a static or renewable session before it reaches a transport. */
56
+ export function createSessionAccess(
57
+ source: SessionSource | null | undefined,
58
+ credential: () => Promise<CredentialProviderResult>,
59
+ ): SessionAccess {
60
+ return {
61
+ renewable:
62
+ typeof source === 'function'
63
+ || (typeof source === 'object' && source !== null && 'endpoint' in source),
64
+ credential,
65
+ };
66
+ }
67
+
68
+ /** Resolve the public session forms into the credential shape auth consumes. */
69
+ export function resolveSessionCredential(
70
+ source: SessionSource,
71
+ options: {
72
+ readonly fetch?: typeof globalThis.fetch;
73
+ readonly dangerouslyAllowBrowser?: boolean;
74
+ },
75
+ ): string | CredentialProvider {
76
+ if (typeof source === 'function') {
77
+ return protectBrowserCredentialProvider(
78
+ cachedSessionProvider(source),
79
+ options.dangerouslyAllowBrowser,
80
+ );
81
+ }
82
+ if (!('endpoint' in source)) return source.token;
83
+ if (!isCredentialEndpoint(source.endpoint)) {
84
+ throw new AbloValidationError(
85
+ '`session.endpoint` expects a URL or path such as \'/api/ablo-session\'.',
86
+ { code: 'invalid_options', param: 'session.endpoint' },
87
+ );
88
+ }
89
+ return protectBrowserCredentialProvider(createEndpointCredentialResolver(source.endpoint, {
90
+ fetch: options.fetch,
91
+ timeoutMs: source.timeoutMs,
92
+ allowCrossOrigin: source.allowCrossOrigin,
93
+ }), options.dangerouslyAllowBrowser);
94
+ }
@@ -1,4 +1,3 @@
1
1
  /** Shared connection lifecycle boundary. */
2
2
  export * from './manager.js';
3
- export * from './credentialLifecycle.js';
4
3
  export * from './networkProbe.js';
@@ -42,7 +42,7 @@
42
42
 
43
43
  import { probeNetwork, type ProbeResult } from './networkProbe.js';
44
44
  import type { AuthTokenGetter } from '../../auth/credentialSource.js';
45
- import type { CredentialRefreshOutcome } from './credentialLifecycle.js';
45
+ import type { CredentialRefreshOutcome } from '../../sessions/lifecycle.js';
46
46
  import { noopLogger, type Logger } from '../../logger.js';
47
47
  import {
48
48
  noopSocketObservability,
@@ -21,7 +21,6 @@ import {
21
21
  createHttpTransport,
22
22
  type HttpTransport,
23
23
  } from './transport.js';
24
- import { modelWireNames } from '../../auth/capability.js';
25
24
  import type { HttpClientConfig } from './options.js';
26
25
  import type {
27
26
  CommitResource,
@@ -33,9 +32,6 @@ import type {
33
32
  ModelReadOptions,
34
33
  ModelMutationOptions,
35
34
  ModelList,
36
- CreateSessionParams,
37
- AbloSession,
38
- SessionResource,
39
35
  } from '../../client/resources/httpResources.js';
40
36
  import { collectModelList, modelList } from '../../client/resources/httpResources.js';
41
37
  import { resolveCreateId } from '../../client/resources/modelCreate.js';
@@ -91,8 +87,8 @@ export interface AbloHttpClientOptions<S extends SchemaRecord>
91
87
  * @default 30_000
92
88
  */
93
89
  readonly timeoutMs?: number;
94
- /** Initial area of interest for `transport: 'websocket'`. */
95
- readonly syncGroups?: readonly string[];
90
+ /** Initial groups observed by a session-backed WebSocket client. */
91
+ readonly groups?: readonly string[];
96
92
  /** Application frame names accepted by `subscribe` on the WebSocket transport. */
97
93
  readonly collaborationEvents?: readonly string[];
98
94
  /** Durable resume position for WebSocket observation. */
@@ -123,7 +119,7 @@ export type HttpModelMutationParams<P> = Omit<P, 'reads'> & {
123
119
  * coordinated writes.
124
120
  * It deliberately omits the stateful client's local-only
125
121
  * reads (the `local` namespace) and live subscriptions (`onChange`), which need
126
- * a resident graph and a persistent socket; those are absent from the type, so
122
+ * a local graph and a persistent socket; those are absent from the type, so
127
123
  * reaching for one is a compile error rather than a runtime gap.
128
124
  *
129
125
  * This is also the base the reactive per-model surface is composed from, rather
@@ -199,7 +195,7 @@ export interface HttpModelClient<T, C = T> {
199
195
 
200
196
  /**
201
197
  * The type of the stateless HTTP client: a typed {@link HttpModelClient} per
202
- * schema model, plus `commits`, `dispose`, and the session-mint surface. It
198
+ * schema model, plus `commits` and lifecycle operations. It
203
199
  * exposes only what request/response transport can do, so reaching for a
204
200
  * stateful-only capability — the `local` reads, `onChange`, or the synchronous
205
201
  * `claim.state`/`queue`/`reorder` reads — is a compile error rather than a value
@@ -235,13 +231,6 @@ export type AbloHttpClient<S extends SchemaRecord> = {
235
231
  dispose(): Promise<void>;
236
232
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
237
233
  getAuthToken(): Promise<string | null>;
238
- /**
239
- * Mints a short-lived, scoped session token. Minting is itself a stateless
240
- * request, so it is available here even though the local-cache reads are not.
241
- * Pass `{ user }` to mint an end-user key (`ek_`) or `{ agent, can }` to mint a
242
- * scoped agent key (`rk_`). See {@link CreateSessionParams}.
243
- */
244
- readonly sessions: SessionResource<S>;
245
234
  };
246
235
 
247
236
  /** The same typed resource client, with server-pushed capabilities carried by WebSocket. */
@@ -252,9 +241,9 @@ export type AbloWebSocketClient<S extends SchemaRecord> = AbloHttpClient<S> & {
252
241
  listener: (...args: CoreSyncEventMap[K]) => void,
253
242
  ): () => void;
254
243
  updateSubscription(
255
- syncGroups: readonly string[],
244
+ groups: readonly string[],
256
245
  options?: { timeoutMs?: number },
257
- ): Promise<{ syncGroups: string[] }>;
246
+ ): Promise<{ groups: string[] }>;
258
247
  readonly presence: {
259
248
  update(input?: {
260
249
  readonly status?: 'online' | 'away' | 'offline';
@@ -281,7 +270,6 @@ const PROTOCOL_MEMBERS = new Set<string>([
281
270
  'logs',
282
271
  'identity',
283
272
  'getAuthToken',
284
- 'sessions',
285
273
  ]);
286
274
 
287
275
  /** Narrows a bare property name to a transport key so the facade can index it typed. */
@@ -538,23 +526,20 @@ function createHttpModelClient<T, C = T>(
538
526
  }
539
527
 
540
528
  /**
541
- * Builds the stateless, typed HTTP client. Each `client.<model>` resolves to the
542
- * protocol client's model accessor, while `commits`, `dispose`, and the other
543
- * protocol members pass through unchanged. No socket is ever opened; the bearer
544
- * credential is the identity.
529
+ * Builds the typed headless client. An API-key identity uses HTTP. A session
530
+ * identity uses one lazily opened WebSocket by default for commits and live
531
+ * coordination until `dispose()`; point reads and administration remain HTTP.
545
532
  */
546
- /** @internal Constructed only through the public `Ablo({ transport: 'http' })` factory. */
533
+ /** @internal Constructed only through the public `Ablo()` factory. */
547
534
  export function createAbloHttpClient<S extends SchemaRecord>(
548
535
  options: AbloHttpClientOptions<S>,
549
536
  ): AbloHttpClient<S> | AbloWebSocketClient<S> {
550
537
  const { schema, onCommitReceipt, ...rest } = options;
551
- const usesWebSocket = options.transport === 'websocket';
538
+ const usesWebSocket = options.transport === 'websocket'
539
+ || (options.transport === undefined && options.session != null);
552
540
  const readSetContext = createReadSetContext();
553
541
  const transport: HttpTransport = createHttpTransport({
554
542
  ...rest,
555
- // Derived from the schema this client is bound to, never assembled by hand
556
- // — see `auth/capability.ts`.
557
- modelTypenames: modelWireNames(schema.models),
558
543
  onCommitReceipt: (observation) => {
559
544
  recordHttpCommitReceipt(readSetContext, observation);
560
545
  onCommitReceipt?.(observation);
@@ -597,8 +582,8 @@ export function createAbloHttpClient<S extends SchemaRecord>(
597
582
  await transport.ready();
598
583
  const session = await createWebSocketSession({
599
584
  baseUrl: rest.baseURL ?? undefined,
600
- getAuthToken: async () => (await transport.getAuthToken()) ?? undefined,
601
- syncGroups: options.syncGroups,
585
+ access: transport.access,
586
+ syncGroups: options.groups,
602
587
  collaborationEvents: options.collaborationEvents,
603
588
  cursorStore: options.cursorStore,
604
589
  cursorKey: options.cursorKey,
@@ -707,8 +692,13 @@ export function createAbloHttpClient<S extends SchemaRecord>(
707
692
  };
708
693
  }
709
694
  if (usesWebSocket && prop === 'updateSubscription') {
710
- return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) =>
711
- (await webSocketSession()).updateSubscription(groups, subscriptionOptions);
695
+ return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) => {
696
+ const result = await (await webSocketSession()).updateSubscription(
697
+ groups,
698
+ subscriptionOptions,
699
+ );
700
+ return { groups: result.syncGroups };
701
+ };
712
702
  }
713
703
  if (usesWebSocket && prop === 'presence') {
714
704
  return {
@@ -37,7 +37,10 @@ export interface HttpClientConfig<S extends SchemaRecord = SchemaRecord>
37
37
  commitOutbox?: DurableWriteStore;
38
38
  /** Compatibility input and internal child-client identity seam. */
39
39
  commitOutboxScope?: CommitOutboxScope;
40
- /** Which transport carries this client's traffic. */
40
+ /**
41
+ * Which transport carries this client's traffic. Defaults to WebSocket for
42
+ * a scoped `session` and HTTP for an API key or direct auth token.
43
+ */
41
44
  transport?: 'websocket' | 'http' | undefined;
42
45
  /** @internal Exact receipt phases observed while dispatching one HTTP commit. */
43
46
  onCommitReceipt?: (observation: {