@abloatai/transaction 0.59.2 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/apiKey.d.ts +10 -11
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +15 -29
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/browserCredentialSafety.js +1 -1
- package/dist/auth/browserCredentialSafety.js.map +1 -1
- package/dist/auth/capability.d.ts +15 -15
- package/dist/auth/capabilityLifecycle.d.ts +3 -3
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/capabilityLifecycle.js +2 -1
- package/dist/auth/capabilityLifecycle.js.map +1 -1
- package/dist/auth/credentialEndpoint.js +1 -1
- package/dist/auth/credentialEndpoint.js.map +1 -1
- package/dist/auth/credentialResult.d.ts +2 -1
- package/dist/auth/credentialResult.d.ts.map +1 -1
- package/dist/auth/credentialResult.js.map +1 -1
- package/dist/auth/credentialSource.js.map +1 -1
- package/dist/auth/identity.js.map +1 -1
- package/dist/auth/schemas.d.ts +5 -5
- package/dist/auth/sessionIssuanceProtocol.d.ts +8 -8
- package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -1
- package/dist/branches.d.ts +38 -38
- package/dist/claims/contract.d.ts +27 -27
- package/dist/claims/eventContract.d.ts +18 -18
- package/dist/claims/events.d.ts +1 -1
- package/dist/claims/events.d.ts.map +1 -1
- package/dist/claims/routes.d.ts +9 -9
- package/dist/claims/routes.d.ts.map +1 -1
- package/dist/claims/trace.d.ts.map +1 -1
- package/dist/claims/trace.js.map +1 -1
- package/dist/client/ablo.d.ts +11 -8
- package/dist/client/ablo.d.ts.map +1 -1
- package/dist/client/ablo.js +5 -6
- package/dist/client/ablo.js.map +1 -1
- package/dist/client/resources/httpResources.d.ts +1 -169
- package/dist/client/resources/httpResources.d.ts.map +1 -1
- package/dist/client/resources/httpResources.js +1 -1
- package/dist/client/resources/httpResources.js.map +1 -1
- package/dist/client/resources/modelOperations.d.ts +6 -15
- package/dist/client/resources/modelOperations.d.ts.map +1 -1
- package/dist/client/resources/where.d.ts +12 -12
- package/dist/client/surface.d.ts +1 -1
- package/dist/client/surface.d.ts.map +1 -1
- package/dist/client/surface.js +2 -4
- package/dist/client/surface.js.map +1 -1
- package/dist/commit/confirmation/commitEnvelope.d.ts +7 -7
- package/dist/commit/confirmation/httpCommitEnvelope.d.ts +1 -1
- package/dist/commit/confirmation/pendingWrite.d.ts +5 -5
- package/dist/commit/contract.d.ts +249 -249
- package/dist/commit/contract.d.ts.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +3 -4
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +57 -101
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -69
- package/dist/coordination/schema.js.map +1 -1
- package/dist/environment.d.ts +3 -3
- package/dist/environment.d.ts.map +1 -1
- package/dist/environment.js.map +1 -1
- package/dist/errorCodes.d.ts +4 -5
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +3 -8
- package/dist/errorCodes.js.map +1 -1
- package/dist/errorObservation.d.ts +14 -14
- package/dist/errors.d.ts +11 -11
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/keys/index.d.ts +1 -1
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js.map +1 -1
- package/dist/observation/contract.d.ts +34 -34
- package/dist/observation/feedContract.d.ts +86 -86
- package/dist/observation/persistence/syncDeltaRow.d.ts +23 -23
- package/dist/observation/persistence/syncDeltaRow.d.ts.map +1 -1
- package/dist/pricing.d.ts +12 -12
- package/dist/schema/audit.js.map +1 -1
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/ddlLock.js.map +1 -1
- package/dist/schema/deployment/contracts.d.ts +64 -64
- package/dist/schema/deployment/index.js.map +1 -1
- package/dist/schema/deployment/reconcile.js.map +1 -1
- package/dist/schema/diff.js.map +1 -1
- package/dist/schema/field.d.ts +1 -1
- package/dist/schema/field.d.ts.map +1 -1
- package/dist/schema/loadStrategy.d.ts +2 -2
- package/dist/schema/loadStrategy.d.ts.map +1 -1
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/relation.d.ts +3 -3
- package/dist/schema/relation.d.ts.map +1 -1
- package/dist/schema/residency.d.ts +1 -1
- package/dist/schema/roles.d.ts +1 -1
- package/dist/schema/roles.d.ts.map +1 -1
- package/dist/schema/roles.js.map +1 -1
- package/dist/schema/schema.d.ts +1 -1
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/serialize.js.map +1 -1
- package/dist/schema/sugar.d.ts.map +1 -1
- package/dist/schema/tenancy.d.ts +2 -2
- package/dist/server/storageMode.d.ts +1 -1
- package/dist/sessions/client.d.ts +29 -0
- package/dist/sessions/client.d.ts.map +1 -0
- package/dist/sessions/client.js +69 -0
- package/dist/sessions/client.js.map +1 -0
- package/dist/sessions/contract.d.ts +97 -0
- package/dist/sessions/contract.d.ts.map +1 -0
- package/dist/sessions/contract.js +5 -0
- package/dist/sessions/contract.js.map +1 -0
- package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
- package/dist/sessions/create.d.ts.map +1 -0
- package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
- package/dist/sessions/create.js.map +1 -0
- package/dist/sessions/handler.d.ts +23 -0
- package/dist/sessions/handler.d.ts.map +1 -0
- package/dist/sessions/handler.js +55 -0
- package/dist/sessions/handler.js.map +1 -0
- package/dist/sessions/index.d.ts +14 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +14 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
- package/dist/sessions/lifecycle.d.ts.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
- package/dist/sessions/lifecycle.js.map +1 -0
- package/dist/sessions/source.d.ts +24 -0
- package/dist/sessions/source.d.ts.map +1 -0
- package/dist/sessions/source.js +56 -0
- package/dist/sessions/source.js.map +1 -0
- package/dist/source/adapters/adapterProfile.d.ts +6 -6
- package/dist/source/adapters/conformance.js.map +1 -1
- package/dist/source/adapters/contract.d.ts +14 -14
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/connector/client.d.ts.map +1 -1
- package/dist/source/connector/client.js.map +1 -1
- package/dist/source/connector/protocol.d.ts.map +1 -1
- package/dist/source/delivery/pushQueue.d.ts.map +1 -1
- package/dist/source/delivery/pushQueue.js.map +1 -1
- package/dist/source/endpoint/signing.d.ts +4 -4
- package/dist/source/endpoint/signing.d.ts.map +1 -1
- package/dist/source/outbox/contract.d.ts +4 -4
- package/dist/testing/fixtures/httpResponses.d.ts +2 -2
- package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
- package/dist/testing/fixtures/httpResponses.js.map +1 -1
- package/dist/transport/connection/index.d.ts +0 -1
- package/dist/transport/connection/index.d.ts.map +1 -1
- package/dist/transport/connection/index.js +0 -1
- package/dist/transport/connection/index.js.map +1 -1
- package/dist/transport/connection/manager.d.ts +1 -1
- package/dist/transport/connection/manager.d.ts.map +1 -1
- package/dist/transport/connection/manager.js.map +1 -1
- package/dist/transport/connection/networkProbe.d.ts +5 -18
- package/dist/transport/connection/networkProbe.d.ts.map +1 -1
- package/dist/transport/connection/networkProbe.js.map +1 -1
- package/dist/transport/http/client.d.ts +11 -19
- package/dist/transport/http/client.d.ts.map +1 -1
- package/dist/transport/http/client.js +12 -14
- package/dist/transport/http/client.js.map +1 -1
- package/dist/transport/http/options.d.ts +4 -1
- package/dist/transport/http/options.d.ts.map +1 -1
- package/dist/transport/http/transport.d.ts +4 -10
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +9 -51
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/commitFrames.d.ts +1 -2
- package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
- package/dist/transport/websocket/commitFrames.js.map +1 -1
- package/dist/transport/websocket/frameHandlers.d.ts +0 -14
- package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
- package/dist/transport/websocket/frameHandlers.js +1 -71
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/heartbeat.d.ts.map +1 -1
- package/dist/transport/websocket/heartbeat.js.map +1 -1
- package/dist/transport/websocket/session.d.ts.map +1 -1
- package/dist/transport/websocket/session.js +72 -32
- package/dist/transport/websocket/session.js.map +1 -1
- package/dist/transport/websocket/sessionContract.d.ts +3 -14
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
- package/dist/transport/websocket/transport.d.ts +5 -42
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js +14 -101
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/accountResponses.d.ts +31 -31
- package/dist/wire/auth.d.ts +3 -3
- package/dist/wire/bootstrapReason.d.ts +2 -2
- package/dist/wire/dataSourceResponses.d.ts +2 -2
- package/dist/wire/dataSourceResponses.d.ts.map +1 -1
- package/dist/wire/frames.d.ts +8 -8
- package/dist/wire/inboundFrames.d.ts +53 -63
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +1 -6
- package/dist/wire/inboundFrames.js.map +1 -1
- package/dist/wire/listEnvelope.js.map +1 -1
- package/dist/wire/modelResponses.d.ts +2 -2
- package/dist/wire/modelShape.d.ts +4 -4
- package/dist/wire/pricing.d.ts +3 -3
- package/dist/wire/protocolVersion.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/auth/apiKey.ts +25 -44
- package/src/auth/browserCredentialSafety.ts +1 -1
- package/src/auth/capabilityLifecycle.ts +3 -2
- package/src/auth/credentialEndpoint.ts +1 -1
- package/src/auth/credentialResult.ts +2 -0
- package/src/claims/events.ts +1 -2
- package/src/client/ablo.ts +15 -10
- package/src/client/resources/httpResources.ts +2 -174
- package/src/client/resources/modelOperations.ts +0 -10
- package/src/client/surface.ts +2 -4
- package/src/coordination/index.ts +2 -9
- package/src/coordination/schema.ts +7 -81
- package/src/errorCodes.ts +3 -12
- package/src/sessions/client.ts +121 -0
- package/src/sessions/contract.ts +116 -0
- package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
- package/src/sessions/handler.ts +96 -0
- package/src/sessions/index.ts +13 -0
- package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
- package/src/sessions/source.ts +94 -0
- package/src/transport/connection/index.ts +0 -1
- package/src/transport/connection/manager.ts +1 -1
- package/src/transport/http/client.ts +21 -31
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +14 -74
- package/src/transport/websocket/commitFrames.ts +0 -2
- package/src/transport/websocket/frameHandlers.ts +0 -102
- package/src/transport/websocket/session.ts +88 -33
- package/src/transport/websocket/sessionContract.ts +3 -12
- package/src/transport/websocket/transport.ts +13 -137
- package/src/wire/inboundFrames.ts +0 -6
- package/dist/auth/sessionMint.d.ts.map +0 -1
- package/dist/auth/sessionMint.js.map +0 -1
- package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
- package/dist/transport/connection/credentialLifecycle.js.map +0 -1
|
@@ -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';
|
|
@@ -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
|
|
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 '
|
|
11
|
-
import { noopLogger, type Logger } from '
|
|
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 '
|
|
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
|
+
}
|
|
@@ -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 '
|
|
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
|
|
95
|
-
readonly
|
|
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
|
|
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
|
|
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
|
-
|
|
244
|
+
groups: readonly string[],
|
|
256
245
|
options?: { timeoutMs?: number },
|
|
257
|
-
): Promise<{
|
|
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
|
|
542
|
-
*
|
|
543
|
-
*
|
|
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(
|
|
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
|
-
|
|
601
|
-
syncGroups: options.
|
|
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(
|
|
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
|
-
/**
|
|
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: {
|