@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.
- package/dist/auth/apiKey.d.ts +10 -11
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +15 -29
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/browserCredentialSafety.js +1 -1
- package/dist/auth/browserCredentialSafety.js.map +1 -1
- package/dist/auth/capabilityLifecycle.d.ts +1 -1
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/capabilityLifecycle.js +2 -1
- package/dist/auth/capabilityLifecycle.js.map +1 -1
- package/dist/auth/credentialEndpoint.js +1 -1
- package/dist/auth/credentialEndpoint.js.map +1 -1
- package/dist/auth/credentialResult.d.ts +2 -1
- package/dist/auth/credentialResult.d.ts.map +1 -1
- package/dist/auth/credentialResult.js.map +1 -1
- package/dist/claims/events.d.ts +1 -1
- package/dist/claims/events.d.ts.map +1 -1
- package/dist/client/ablo.d.ts +15 -11
- package/dist/client/ablo.d.ts.map +1 -1
- package/dist/client/ablo.js +5 -14
- package/dist/client/ablo.js.map +1 -1
- package/dist/client/resources/httpResources.d.ts +1 -169
- package/dist/client/resources/httpResources.d.ts.map +1 -1
- package/dist/client/resources/httpResources.js +1 -1
- package/dist/client/resources/httpResources.js.map +1 -1
- package/dist/client/resources/modelOperations.d.ts +0 -9
- package/dist/client/resources/modelOperations.d.ts.map +1 -1
- package/dist/client/surface.d.ts +2 -2
- package/dist/client/surface.d.ts.map +1 -1
- package/dist/client/surface.js +8 -3
- package/dist/client/surface.js.map +1 -1
- package/dist/commit/contract.d.ts +10 -10
- package/dist/commit/recordRuntime.d.ts +3 -2
- package/dist/commit/recordRuntime.d.ts.map +1 -1
- package/dist/commit/recordRuntime.js +1 -0
- package/dist/commit/recordRuntime.js.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +3 -4
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +3 -47
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -69
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +2 -2
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +5 -9
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sessions/client.d.ts +29 -0
- package/dist/sessions/client.d.ts.map +1 -0
- package/dist/sessions/client.js +69 -0
- package/dist/sessions/client.js.map +1 -0
- package/dist/sessions/contract.d.ts +97 -0
- package/dist/sessions/contract.d.ts.map +1 -0
- package/dist/sessions/contract.js +5 -0
- package/dist/sessions/contract.js.map +1 -0
- package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
- package/dist/sessions/create.d.ts.map +1 -0
- package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
- package/dist/sessions/create.js.map +1 -0
- package/dist/sessions/handler.d.ts +23 -0
- package/dist/sessions/handler.d.ts.map +1 -0
- package/dist/sessions/handler.js +55 -0
- package/dist/sessions/handler.js.map +1 -0
- package/dist/sessions/index.d.ts +14 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +14 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
- package/dist/sessions/lifecycle.d.ts.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
- package/dist/sessions/lifecycle.js.map +1 -0
- package/dist/sessions/source.d.ts +24 -0
- package/dist/sessions/source.d.ts.map +1 -0
- package/dist/sessions/source.js +56 -0
- package/dist/sessions/source.js.map +1 -0
- package/dist/transport/connection/index.d.ts +0 -1
- package/dist/transport/connection/index.d.ts.map +1 -1
- package/dist/transport/connection/index.js +0 -1
- package/dist/transport/connection/index.js.map +1 -1
- package/dist/transport/connection/manager.d.ts +1 -1
- package/dist/transport/connection/manager.d.ts.map +1 -1
- package/dist/transport/http/client.d.ts +44 -16
- package/dist/transport/http/client.d.ts.map +1 -1
- package/dist/transport/http/client.js +125 -12
- package/dist/transport/http/client.js.map +1 -1
- package/dist/transport/http/options.d.ts +4 -1
- package/dist/transport/http/options.d.ts.map +1 -1
- package/dist/transport/http/transport.d.ts +25 -10
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +102 -56
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/commitFrames.d.ts +5 -5
- package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
- package/dist/transport/websocket/commitFrames.js +1 -0
- package/dist/transport/websocket/commitFrames.js.map +1 -1
- package/dist/transport/websocket/contextSubscription.d.ts +6 -0
- package/dist/transport/websocket/contextSubscription.d.ts.map +1 -0
- package/dist/transport/websocket/contextSubscription.js +45 -0
- package/dist/transport/websocket/contextSubscription.js.map +1 -0
- package/dist/transport/websocket/frameHandlers.d.ts +0 -14
- package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
- package/dist/transport/websocket/frameHandlers.js +1 -71
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/session.d.ts +4 -0
- package/dist/transport/websocket/session.d.ts.map +1 -0
- package/dist/transport/websocket/session.js +425 -0
- package/dist/transport/websocket/session.js.map +1 -0
- package/dist/transport/websocket/sessionContract.d.ts +78 -0
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -0
- package/dist/transport/websocket/sessionContract.js +2 -0
- package/dist/transport/websocket/sessionContract.js.map +1 -0
- package/dist/transport/websocket/transport.d.ts +27 -42
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js +77 -102
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/inboundFrames.d.ts +0 -10
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +1 -6
- package/dist/wire/inboundFrames.js.map +1 -1
- package/package.json +7 -1
- package/src/auth/apiKey.ts +25 -44
- package/src/auth/browserCredentialSafety.ts +1 -1
- package/src/auth/capabilityLifecycle.ts +3 -2
- package/src/auth/credentialEndpoint.ts +1 -1
- package/src/auth/credentialResult.ts +2 -0
- package/src/claims/events.ts +1 -2
- package/src/client/ablo.ts +23 -13
- package/src/client/resources/httpResources.ts +2 -174
- package/src/client/resources/modelOperations.ts +0 -10
- package/src/client/surface.ts +9 -4
- package/src/commit/recordRuntime.ts +4 -2
- package/src/coordination/index.ts +2 -9
- package/src/coordination/schema.ts +7 -81
- package/src/errorCodes.ts +8 -13
- package/src/errors.ts +18 -1
- package/src/index.ts +1 -0
- package/src/sessions/client.ts +121 -0
- package/src/sessions/contract.ts +116 -0
- package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
- package/src/sessions/handler.ts +96 -0
- package/src/sessions/index.ts +13 -0
- package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
- package/src/sessions/source.ts +94 -0
- package/src/transport/connection/index.ts +0 -1
- package/src/transport/connection/manager.ts +1 -1
- package/src/transport/http/client.ts +183 -26
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +140 -78
- package/src/transport/websocket/commitFrames.ts +5 -5
- package/src/transport/websocket/contextSubscription.ts +55 -0
- package/src/transport/websocket/frameHandlers.ts +0 -102
- package/src/transport/websocket/session.ts +522 -0
- package/src/transport/websocket/sessionContract.ts +106 -0
- package/src/transport/websocket/transport.ts +85 -137
- package/src/wire/inboundFrames.ts +0 -6
- package/dist/auth/sessionMint.d.ts.map +0 -1
- package/dist/auth/sessionMint.js.map +0 -1
- package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
- package/dist/transport/connection/credentialLifecycle.js.map +0 -1
|
@@ -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
|
|
471
|
-
*
|
|
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
|
-
//
|
|
985
|
-
//
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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:
|
|
938
|
+
details: Object.keys(details).length > 0 ? details : undefined,
|
|
922
939
|
});
|
|
923
940
|
}
|
|
924
941
|
|
package/src/index.ts
CHANGED
|
@@ -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
|
|
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 '
|
|
28
|
+
} from '../auth/runtime.js';
|
|
29
29
|
import {
|
|
30
30
|
capabilityCanSchemaFor,
|
|
31
31
|
grantedOperations,
|
|
32
|
-
} from '
|
|
32
|
+
} from '../auth/capability.js';
|
|
33
33
|
import type { SchemaRecord } from '../schema/schema.js';
|
|
34
|
-
import type { AbloSession, CreateSessionParams } from '
|
|
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
|
|
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
|
|
63
|
+
export async function createSession<S extends SchemaRecord>(
|
|
63
64
|
params: CreateSessionParams<S>,
|
|
64
|
-
ctx:
|
|
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.
|
|
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.
|
|
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';
|