@abloatai/transaction 0.59.1 → 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 (166) 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/capabilityLifecycle.d.ts +1 -1
  8. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  9. package/dist/auth/capabilityLifecycle.js +2 -1
  10. package/dist/auth/capabilityLifecycle.js.map +1 -1
  11. package/dist/auth/credentialEndpoint.js +1 -1
  12. package/dist/auth/credentialEndpoint.js.map +1 -1
  13. package/dist/auth/credentialResult.d.ts +2 -1
  14. package/dist/auth/credentialResult.d.ts.map +1 -1
  15. package/dist/auth/credentialResult.js.map +1 -1
  16. package/dist/claims/events.d.ts +1 -1
  17. package/dist/claims/events.d.ts.map +1 -1
  18. package/dist/client/ablo.d.ts +15 -11
  19. package/dist/client/ablo.d.ts.map +1 -1
  20. package/dist/client/ablo.js +5 -14
  21. package/dist/client/ablo.js.map +1 -1
  22. package/dist/client/resources/httpResources.d.ts +1 -169
  23. package/dist/client/resources/httpResources.d.ts.map +1 -1
  24. package/dist/client/resources/httpResources.js +1 -1
  25. package/dist/client/resources/httpResources.js.map +1 -1
  26. package/dist/client/resources/modelOperations.d.ts +0 -9
  27. package/dist/client/resources/modelOperations.d.ts.map +1 -1
  28. package/dist/client/surface.d.ts +2 -2
  29. package/dist/client/surface.d.ts.map +1 -1
  30. package/dist/client/surface.js +8 -3
  31. package/dist/client/surface.js.map +1 -1
  32. package/dist/commit/contract.d.ts +10 -10
  33. package/dist/commit/recordRuntime.d.ts +3 -2
  34. package/dist/commit/recordRuntime.d.ts.map +1 -1
  35. package/dist/commit/recordRuntime.js +1 -0
  36. package/dist/commit/recordRuntime.js.map +1 -1
  37. package/dist/coordination/index.d.ts +2 -2
  38. package/dist/coordination/index.d.ts.map +1 -1
  39. package/dist/coordination/index.js +3 -4
  40. package/dist/coordination/index.js.map +1 -1
  41. package/dist/coordination/schema.d.ts +3 -47
  42. package/dist/coordination/schema.d.ts.map +1 -1
  43. package/dist/coordination/schema.js +7 -69
  44. package/dist/coordination/schema.js.map +1 -1
  45. package/dist/errorCodes.d.ts +2 -2
  46. package/dist/errorCodes.d.ts.map +1 -1
  47. package/dist/errorCodes.js +5 -9
  48. package/dist/errorCodes.js.map +1 -1
  49. package/dist/errors.d.ts.map +1 -1
  50. package/dist/errors.js +16 -1
  51. package/dist/errors.js.map +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js.map +1 -1
  55. package/dist/sessions/client.d.ts +29 -0
  56. package/dist/sessions/client.d.ts.map +1 -0
  57. package/dist/sessions/client.js +69 -0
  58. package/dist/sessions/client.js.map +1 -0
  59. package/dist/sessions/contract.d.ts +97 -0
  60. package/dist/sessions/contract.d.ts.map +1 -0
  61. package/dist/sessions/contract.js +5 -0
  62. package/dist/sessions/contract.js.map +1 -0
  63. package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
  64. package/dist/sessions/create.d.ts.map +1 -0
  65. package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
  66. package/dist/sessions/create.js.map +1 -0
  67. package/dist/sessions/handler.d.ts +23 -0
  68. package/dist/sessions/handler.d.ts.map +1 -0
  69. package/dist/sessions/handler.js +55 -0
  70. package/dist/sessions/handler.js.map +1 -0
  71. package/dist/sessions/index.d.ts +14 -0
  72. package/dist/sessions/index.d.ts.map +1 -0
  73. package/dist/sessions/index.js +14 -0
  74. package/dist/sessions/index.js.map +1 -0
  75. package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
  76. package/dist/sessions/lifecycle.d.ts.map +1 -0
  77. package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
  78. package/dist/sessions/lifecycle.js.map +1 -0
  79. package/dist/sessions/source.d.ts +24 -0
  80. package/dist/sessions/source.d.ts.map +1 -0
  81. package/dist/sessions/source.js +56 -0
  82. package/dist/sessions/source.js.map +1 -0
  83. package/dist/transport/connection/index.d.ts +0 -1
  84. package/dist/transport/connection/index.d.ts.map +1 -1
  85. package/dist/transport/connection/index.js +0 -1
  86. package/dist/transport/connection/index.js.map +1 -1
  87. package/dist/transport/connection/manager.d.ts +1 -1
  88. package/dist/transport/connection/manager.d.ts.map +1 -1
  89. package/dist/transport/http/client.d.ts +44 -16
  90. package/dist/transport/http/client.d.ts.map +1 -1
  91. package/dist/transport/http/client.js +125 -12
  92. package/dist/transport/http/client.js.map +1 -1
  93. package/dist/transport/http/options.d.ts +4 -1
  94. package/dist/transport/http/options.d.ts.map +1 -1
  95. package/dist/transport/http/transport.d.ts +25 -10
  96. package/dist/transport/http/transport.d.ts.map +1 -1
  97. package/dist/transport/http/transport.js +102 -56
  98. package/dist/transport/http/transport.js.map +1 -1
  99. package/dist/transport/websocket/commitFrames.d.ts +5 -5
  100. package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
  101. package/dist/transport/websocket/commitFrames.js +1 -0
  102. package/dist/transport/websocket/commitFrames.js.map +1 -1
  103. package/dist/transport/websocket/contextSubscription.d.ts +6 -0
  104. package/dist/transport/websocket/contextSubscription.d.ts.map +1 -0
  105. package/dist/transport/websocket/contextSubscription.js +45 -0
  106. package/dist/transport/websocket/contextSubscription.js.map +1 -0
  107. package/dist/transport/websocket/frameHandlers.d.ts +0 -14
  108. package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
  109. package/dist/transport/websocket/frameHandlers.js +1 -71
  110. package/dist/transport/websocket/frameHandlers.js.map +1 -1
  111. package/dist/transport/websocket/session.d.ts +4 -0
  112. package/dist/transport/websocket/session.d.ts.map +1 -0
  113. package/dist/transport/websocket/session.js +425 -0
  114. package/dist/transport/websocket/session.js.map +1 -0
  115. package/dist/transport/websocket/sessionContract.d.ts +78 -0
  116. package/dist/transport/websocket/sessionContract.d.ts.map +1 -0
  117. package/dist/transport/websocket/sessionContract.js +2 -0
  118. package/dist/transport/websocket/sessionContract.js.map +1 -0
  119. package/dist/transport/websocket/transport.d.ts +27 -42
  120. package/dist/transport/websocket/transport.d.ts.map +1 -1
  121. package/dist/transport/websocket/transport.js +77 -102
  122. package/dist/transport/websocket/transport.js.map +1 -1
  123. package/dist/wire/inboundFrames.d.ts +0 -10
  124. package/dist/wire/inboundFrames.d.ts.map +1 -1
  125. package/dist/wire/inboundFrames.js +1 -6
  126. package/dist/wire/inboundFrames.js.map +1 -1
  127. package/package.json +7 -1
  128. package/src/auth/apiKey.ts +25 -44
  129. package/src/auth/browserCredentialSafety.ts +1 -1
  130. package/src/auth/capabilityLifecycle.ts +3 -2
  131. package/src/auth/credentialEndpoint.ts +1 -1
  132. package/src/auth/credentialResult.ts +2 -0
  133. package/src/claims/events.ts +1 -2
  134. package/src/client/ablo.ts +23 -13
  135. package/src/client/resources/httpResources.ts +2 -174
  136. package/src/client/resources/modelOperations.ts +0 -10
  137. package/src/client/surface.ts +9 -4
  138. package/src/commit/recordRuntime.ts +4 -2
  139. package/src/coordination/index.ts +2 -9
  140. package/src/coordination/schema.ts +7 -81
  141. package/src/errorCodes.ts +8 -13
  142. package/src/errors.ts +18 -1
  143. package/src/index.ts +1 -0
  144. package/src/sessions/client.ts +121 -0
  145. package/src/sessions/contract.ts +116 -0
  146. package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
  147. package/src/sessions/handler.ts +96 -0
  148. package/src/sessions/index.ts +13 -0
  149. package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
  150. package/src/sessions/source.ts +94 -0
  151. package/src/transport/connection/index.ts +0 -1
  152. package/src/transport/connection/manager.ts +1 -1
  153. package/src/transport/http/client.ts +183 -26
  154. package/src/transport/http/options.ts +4 -1
  155. package/src/transport/http/transport.ts +140 -78
  156. package/src/transport/websocket/commitFrames.ts +5 -5
  157. package/src/transport/websocket/contextSubscription.ts +55 -0
  158. package/src/transport/websocket/frameHandlers.ts +0 -102
  159. package/src/transport/websocket/session.ts +522 -0
  160. package/src/transport/websocket/sessionContract.ts +106 -0
  161. package/src/transport/websocket/transport.ts +85 -137
  162. package/src/wire/inboundFrames.ts +0 -6
  163. package/dist/auth/sessionMint.d.ts.map +0 -1
  164. package/dist/auth/sessionMint.js.map +0 -1
  165. package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
  166. package/dist/transport/connection/credentialLifecycle.js.map +0 -1
@@ -467,8 +467,8 @@ export type ClaimError = z.infer<typeof claimErrorSchema>;
467
467
  * frozen, and an older server may send a word not listed here. This is the
468
468
  * reader's side of it: a value that parses becomes the typed reason, and one
469
469
  * that does not is simply absent rather than smuggled through as prose.
470
- * {@link claimExpiredSchema} and {@link claimLostSchema} already spelled their
471
- * reasons as enums; this brings the refusals into line.
470
+ * {@link claimLostSchema} already spells its reasons as an enum; this brings
471
+ * the refusals into line.
472
472
  */
473
473
  export const claimEventReasonSchema = z.enum([
474
474
  'conflict',
@@ -644,17 +644,6 @@ export const claimQueueSchema = z.object({
644
644
  });
645
645
  export type ClaimQueue = z.infer<typeof claimQueueSchema>;
646
646
 
647
- /**
648
- * A held claim's TTL lapsed server-side. The claim is already inactive by the
649
- * time this arrives, so a consumer either re-claims with a fresh credential or
650
- * accepts the drop; there is nothing to release.
651
- */
652
- export const claimExpiredSchema = z.object({
653
- claimId: z.string(),
654
- });
655
- export type ClaimExpired = z.infer<typeof claimExpiredSchema>;
656
-
657
-
658
647
  /**
659
648
  * The two states a claim can be observed in while it still exists.
660
649
  *
@@ -981,20 +970,9 @@ export type ClaimHeartbeatBatchAckPayload = z.infer<
981
970
  // ─────────────────────────────────────────────────────────────────────────
982
971
  // Read interest — what a connection receives
983
972
  //
984
- // Two frames set it, and they differ in exactly one way: whether the
985
- // interest is leased.
986
- //
987
- // • `claim` — a PARTICIPANT claim. Adds a scope under a handle, with a
988
- // TTL and an optional capability token, and announces the sender into
989
- // that scope's roster. This is the frame `ablo.<model>.join(...)` sends;
990
- // `release` drops it. Several may be open on one connection at once.
991
- // • `update_subscription` — REPLACES the connection's whole read set. No
992
- // handle, no lease, no roster entry.
993
- //
994
- // Both are bounded by the connection credential's grant, and both name their
995
- // groups the same way, so both parse their `syncGroups` through the same
996
- // element schema. Neither is the row lease — that is `claim_begin`, in the
997
- // pessimistic-claims block above, which shares only a word.
973
+ // `update_subscription` replaces the connection's whole read set. It is
974
+ // bounded by the connection credential's grant and is not a row lease;
975
+ // row leases use `claim_begin` in the pessimistic-claims block above.
998
976
  // ─────────────────────────────────────────────────────────────────────────
999
977
 
1000
978
  /**
@@ -1012,67 +990,15 @@ export const MAX_FRAME_SYNC_GROUPS = 200;
1012
990
  * that does not parse matches nothing, and subscribing to nothing quietly is
1013
991
  * the failure this element type exists to prevent.
1014
992
  *
1015
- * Strict because this is untrusted client input, and shared because the two
1016
- * frames below carry the same value: when they disagreed, `claim` accepted a
1017
- * malformed group that `update_subscription` refused, and the connection
1018
- * ended up leased to a scope it could never receive.
993
+ * Strict because this is untrusted client input.
1019
994
  */
1020
995
  const frameSyncGroupsSchema = z
1021
996
  .array(syncGroupInputSchema)
1022
997
  .max(MAX_FRAME_SYNC_GROUPS);
1023
998
 
1024
- /**
1025
- * The `claim` payload a client sends — the frame behind `join`.
1026
- *
1027
- * It opens one participant claim: the connection is added to each named
1028
- * scope's fan-out under `claimId`, announced into its presence roster, and
1029
- * holds that interest until `release`, the TTL lapses, or the socket closes.
1030
- * The handle is client-chosen because the client must be able to `release`
1031
- * the exact claim it opened while others stay open.
1032
- *
1033
- * This shape was, for a long time, written three times — built as a literal in
1034
- * the transport, restated as an interface on the server, and read back through
1035
- * a cast in the frame handler — which is how the frame came to be the only
1036
- * coordination message with no runtime check on the way in.
1037
- */
1038
- export const participantClaimPayloadSchema = z.object({
1039
- /** Client-chosen handle. Echoed on `claim_ack`; names the claim to `release`. */
1040
- claimId: z.string().min(1),
1041
- syncGroups: frameSyncGroupsSchema,
1042
- /**
1043
- * A narrower capability to present for this claim than the connection's own.
1044
- * Absent means the connection's credential governs it.
1045
- */
1046
- capabilityToken: z.string().optional(),
1047
- /**
1048
- * Crash cleanup, in seconds. The server caps it at the capability's own TTL;
1049
- * absent means the claim lives until `release` or disconnect.
1050
- */
1051
- ttlSeconds: z.number().optional(),
1052
- });
1053
- export type ParticipantClaimPayload = z.infer<
1054
- typeof participantClaimPayloadSchema
1055
- >;
1056
-
1057
- /**
1058
- * The `release` payload — drop one participant claim by its handle.
1059
- *
1060
- * A projection of the claim it releases rather than a second object, so the
1061
- * handle cannot be spelled one way when opened and another when dropped.
1062
- * Idempotent by contract: the server accepts an unknown handle silently, so a
1063
- * client releasing everything at shutdown never has to check what is still open.
1064
- */
1065
- export const participantReleasePayloadSchema =
1066
- participantClaimPayloadSchema.pick({ claimId: true });
1067
- export type ParticipantReleasePayload = z.infer<
1068
- typeof participantReleasePayloadSchema
1069
- >;
1070
-
1071
999
  /**
1072
1000
  * The `update_subscription` payload a client sends. It replaces the
1073
- * connection's read interest with the complete set of sync groups — the
1074
- * unleased counterpart to {@link participantClaimPayloadSchema}, with no
1075
- * handle, no TTL, and no roster entry.
1001
+ * connection's read interest with the complete set of sync groups.
1076
1002
  */
1077
1003
  export const updateSubscriptionPayloadSchema = z.object({
1078
1004
  syncGroups: frameSyncGroupsSchema,
package/src/errorCodes.ts CHANGED
@@ -39,7 +39,7 @@ import { z } from 'zod';
39
39
  * error documentation and returned on the `Ablo-Version` response header, so a
40
40
  * consumer can detect when its expected contract has drifted from the server's.
41
41
  */
42
- export const ERROR_CONTRACT_VERSION = '2026-08-15';
42
+ export const ERROR_CONTRACT_VERSION = '2026-08-30';
43
43
 
44
44
  /** A coarse grouping of error codes, used to organize metrics and documentation. */
45
45
  export type ErrorCategory =
@@ -511,17 +511,13 @@ export const ERROR_CODES = {
511
511
  // A claim payload that cannot be parsed is a malformed request, not
512
512
  // contention — it is filed with `malformed_subscription` below rather than
513
513
  // with the 409s above, so the claim category stays purely about a target
514
- // being held. This is the code for a claim that fails to name its target,
515
- // over either transport — and for the participant claim behind `join`, which
516
- // names scopes rather than a row but fails the same way and to the same
517
- // caller. The copy covers both because the caller sees one word, `claim`, and
518
- // needs to be told which shape was expected without being handed a lecture on
519
- // the two frames.
514
+ // being held. This is the code for a row claim that fails to name its target,
515
+ // over either transport.
520
516
  malformed_claim: wire(
521
517
  'validation',
522
518
  400,
523
519
  false,
524
- 'The claim payload could not be parsed. A claim on a row must name the model and the entity it targets; a claim on a scope, which is what `join` opens, must name sync groups spelled `kind:id` or `default`. Check the payload shape and resend.'
520
+ 'The claim payload could not be parsed. A row claim must name the model and entity it targets. Check the payload shape and resend.'
525
521
  ),
526
522
  malformed_subscription: wire(
527
523
  'validation',
@@ -558,11 +554,6 @@ export const ERROR_CODES = {
558
554
  'claim',
559
555
  'Claiming is unavailable on this model client. Construct it through the standard Ablo({ schema, apiKey }) client and retry.'
560
556
  ),
561
- model_join_not_configured: client(
562
- 'claim',
563
- 'join() opens a presence/claim subscription and needs a live WebSocket, so it is unavailable on the HTTP transport and on model proxies built without a socket. Use the standard Ablo({ schema, apiKey }) client (default WebSocket transport).'
564
- ),
565
-
566
557
  // ── stale context / idempotency (409) ──────────────────────────────
567
558
  // Not retryable at the transport: the rejected request carries its frozen
568
559
  // `readAt`, so resending the identical payload can never succeed. Recovery
@@ -1238,6 +1229,10 @@ export const ERROR_CODES = {
1238
1229
  false,
1239
1230
  'The offline grace window expired before this commit could be sent, so it was not applied. Re-apply the change once the connection returns.'
1240
1231
  ),
1232
+ observation_buffer_overflow: client(
1233
+ 'transport',
1234
+ 'A WebSocket observer fell behind its bounded in-memory backlog. Restart observation to replay from the last durable checkpoint.'
1235
+ ),
1241
1236
  queue_too_deep: wire(
1242
1237
  'transport',
1243
1238
  503,
package/src/errors.ts CHANGED
@@ -871,6 +871,19 @@ function detailsFromBody(body: unknown): Record<string, unknown> | undefined {
871
871
  return Object.keys(rest).length > 0 ? rest : undefined;
872
872
  }
873
873
 
874
+ function detailsFromNestedError(
875
+ nested: NestedErrorShape | undefined,
876
+ ): Record<string, unknown> | undefined {
877
+ if (!nested) return undefined;
878
+ const extras = detailsFromBody(nested) ?? {};
879
+ delete extras.details;
880
+ const direct = typeof nested.details === 'object' && nested.details !== null
881
+ ? nested.details as Record<string, unknown>
882
+ : {};
883
+ const merged = { ...extras, ...direct };
884
+ return Object.keys(merged).length > 0 ? merged : undefined;
885
+ }
886
+
874
887
  export function translateHttpError(
875
888
  status: number,
876
889
  body: unknown,
@@ -911,6 +924,10 @@ export function translateHttpError(
911
924
  typeof body === 'object' && body !== null && 'request_id' in body
912
925
  ? (body as { request_id?: unknown }).request_id
913
926
  : undefined;
927
+ const details = {
928
+ ...(detailsFromBody(body) ?? {}),
929
+ ...(detailsFromNestedError(nested) ?? {}),
930
+ };
914
931
 
915
932
  return errorFromWire(message, {
916
933
  code,
@@ -918,7 +935,7 @@ export function translateHttpError(
918
935
  requestId: requestId ?? (typeof bodyRequestId === 'string' ? bodyRequestId : undefined),
919
936
  requiredCapability,
920
937
  claims,
921
- details: detailsFromBody(body),
938
+ details: Object.keys(details).length > 0 ? details : undefined,
922
939
  });
923
940
  }
924
941
 
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ export { Ablo } from './client/ablo.js';
31
31
  export type {
32
32
  AbloHttpClient,
33
33
  AbloHttpClientOptions,
34
+ AbloWebSocketClient,
34
35
  CapturedRow,
35
36
  HttpModelClient,
36
37
  HttpModelMutationParams,
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Server-side session issuer.
3
+ *
4
+ * This is deliberately separate from `Ablo(...)`: the participant client owns
5
+ * schema model properties, while this client owns credential issuance and
6
+ * lifecycle administration. A schema model named `sessions` therefore remains
7
+ * available as `ablo.sessions` like every other model.
8
+ */
9
+
10
+ import type { CredentialProvider } from '../auth/credentialResult.js';
11
+ import {
12
+ assertBrowserSafety,
13
+ readProcessEnv,
14
+ rejectRemovedDatabaseUrlOption,
15
+ resolveApiKey,
16
+ resolveApiKeyValue,
17
+ resolveBaseURL,
18
+ resolveBootstrapBaseUrl,
19
+ warnIfCliKeyMismatch,
20
+ } from '../auth/apiKey.js';
21
+ import { modelWireNames } from '../auth/capability.js';
22
+ import {
23
+ revokeCapability,
24
+ rotateCapability,
25
+ } from '../auth/capabilityLifecycle.js';
26
+ import { AbloAuthenticationError } from '../errors.js';
27
+ import type { Schema, SchemaRecord } from '../schema/schema.js';
28
+ import type {
29
+ AbloSession,
30
+ CreateSessionParams,
31
+ RevokeSessionParams,
32
+ RotateSessionParams,
33
+ SessionRevocation,
34
+ SessionRotation,
35
+ } from './contract.js';
36
+ import { createSession } from './create.js';
37
+ import {
38
+ createSessionHandler,
39
+ type SessionHandler,
40
+ type SessionHandlerOptions,
41
+ } from './handler.js';
42
+
43
+ export interface SessionsOptions<S extends SchemaRecord> {
44
+ readonly schema: Schema<S>;
45
+ readonly apiKey?: string | CredentialProvider | null;
46
+ readonly baseURL?: string | null;
47
+ readonly bootstrapBaseUrl?: string;
48
+ readonly fetch?: typeof globalThis.fetch;
49
+ readonly dangerouslyAllowBrowser?: boolean;
50
+ }
51
+
52
+ export interface SessionsClient<S extends SchemaRecord> {
53
+ create(params: CreateSessionParams<S>): Promise<AbloSession>;
54
+ handler<Principal>(options: SessionHandlerOptions<S, Principal>): SessionHandler;
55
+ revoke(params: RevokeSessionParams): Promise<SessionRevocation>;
56
+ rotate(params: RotateSessionParams): Promise<SessionRotation>;
57
+ }
58
+
59
+ /** Construct the server-side issuer for user and agent sessions. */
60
+ export function Sessions<const S extends SchemaRecord>(
61
+ options: SessionsOptions<S>,
62
+ ): SessionsClient<S> {
63
+ const env = readProcessEnv();
64
+ const authInput = { options, env };
65
+ const configuredApiKey = resolveApiKey(authInput);
66
+ void warnIfCliKeyMismatch(authInput);
67
+ rejectRemovedDatabaseUrlOption(options);
68
+ assertBrowserSafety({
69
+ apiKey: configuredApiKey,
70
+ dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
71
+ });
72
+
73
+ const baseUrl = resolveBootstrapBaseUrl({
74
+ url: resolveBaseURL(authInput),
75
+ bootstrapBaseUrl: options.bootstrapBaseUrl,
76
+ }).replace(/\/+$/, '');
77
+ const modelTypenames = modelWireNames(options.schema.models);
78
+
79
+ async function secret(): Promise<string> {
80
+ const apiKey = await resolveApiKeyValue(configuredApiKey);
81
+ if (!apiKey) {
82
+ throw new AbloAuthenticationError(
83
+ 'Sessions requires a secret (sk_) API key and must be constructed on the backend.',
84
+ { code: 'apikey_missing' },
85
+ );
86
+ }
87
+ return apiKey;
88
+ }
89
+
90
+ async function create(params: CreateSessionParams<S>): Promise<AbloSession> {
91
+ return createSession(params, {
92
+ apiKey: await secret(),
93
+ baseUrl,
94
+ modelTypenames,
95
+ ...(options.fetch ? { fetch: options.fetch } : {}),
96
+ });
97
+ }
98
+
99
+ return {
100
+ create,
101
+ handler: (handlerOptions) => createSessionHandler(create, handlerOptions),
102
+ async revoke({ id }) {
103
+ return revokeCapability({
104
+ apiKey: await secret(),
105
+ baseUrl,
106
+ id,
107
+ ...(options.fetch ? { fetch: options.fetch } : {}),
108
+ });
109
+ },
110
+ async rotate({ id, graceSeconds, ttlSeconds }) {
111
+ return rotateCapability({
112
+ apiKey: await secret(),
113
+ baseUrl,
114
+ id,
115
+ ...(graceSeconds !== undefined ? { graceSeconds } : {}),
116
+ ...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
117
+ ...(options.fetch ? { fetch: options.fetch } : {}),
118
+ });
119
+ },
120
+ };
121
+ }
@@ -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';