@abloatai/transaction 0.59.2 → 0.61.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/admission.d.ts +4 -0
- package/dist/claims/admission.d.ts.map +1 -0
- package/dist/claims/admission.js +32 -0
- package/dist/claims/admission.js.map +1 -0
- package/dist/claims/awaitGrant.d.ts.map +1 -1
- package/dist/claims/awaitGrant.js +4 -38
- package/dist/claims/awaitGrant.js.map +1 -1
- package/dist/claims/conflict.d.ts +55 -0
- package/dist/claims/conflict.d.ts.map +1 -0
- package/dist/claims/conflict.js +71 -0
- package/dist/claims/conflict.js.map +1 -0
- package/dist/claims/contract.d.ts +39 -27
- package/dist/claims/contract.d.ts.map +1 -1
- package/dist/claims/contract.js +2 -0
- package/dist/claims/contract.js.map +1 -1
- 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/index.d.ts +2 -0
- package/dist/claims/index.d.ts.map +1 -1
- package/dist/claims/index.js +1 -0
- package/dist/claims/index.js.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 +4 -5
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +64 -101
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +14 -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 +15 -9
- package/dist/errorCodes.js.map +1 -1
- package/dist/errorObservation.d.ts +14 -14
- package/dist/errors.d.ts +17 -11
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +101 -5
- 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/claimWait.d.ts +9 -0
- package/dist/transport/http/claimWait.d.ts.map +1 -0
- package/dist/transport/http/claimWait.js +74 -0
- package/dist/transport/http/claimWait.js.map +1 -0
- 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 +15 -18
- package/dist/transport/http/client.js.map +1 -1
- package/dist/transport/http/contract.d.ts +57 -0
- package/dist/transport/http/contract.d.ts.map +1 -0
- package/dist/transport/http/contract.js +3 -0
- package/dist/transport/http/contract.js.map +1 -0
- package/dist/transport/http/index.d.ts +1 -0
- package/dist/transport/http/index.d.ts.map +1 -1
- package/dist/transport/http/index.js +1 -0
- package/dist/transport/http/index.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 +1 -84
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +16 -134
- 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 +4 -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 +137 -64
- package/dist/transport/websocket/session.js.map +1 -1
- package/dist/transport/websocket/sessionContract.d.ts +4 -15
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
- package/dist/transport/websocket/transport.d.ts +6 -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 +60 -63
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +5 -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/admission.ts +49 -0
- package/src/claims/awaitGrant.ts +3 -52
- package/src/claims/conflict.ts +103 -0
- package/src/claims/contract.ts +2 -0
- package/src/claims/events.ts +1 -2
- package/src/claims/index.ts +11 -0
- 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 +4 -9
- package/src/coordination/schema.ts +18 -81
- package/src/errorCodes.ts +21 -13
- package/src/errors.ts +116 -4
- 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/claimWait.ts +108 -0
- package/src/transport/http/client.ts +27 -36
- package/src/transport/http/contract.ts +71 -0
- package/src/transport/http/index.ts +1 -0
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +22 -275
- package/src/transport/websocket/commitFrames.ts +0 -2
- package/src/transport/websocket/frameHandlers.ts +7 -102
- package/src/transport/websocket/session.ts +162 -73
- package/src/transport/websocket/sessionContract.ts +4 -13
- package/src/transport/websocket/transport.ts +15 -137
- package/src/wire/inboundFrames.ts +5 -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
package/src/errors.ts
CHANGED
|
@@ -23,12 +23,24 @@ import { ABLO_DOCS_BASE_URL } from './auth/hostedEndpoints.js';
|
|
|
23
23
|
import { errorCodeSpec, classifyRecovery } from './errorCodes.js';
|
|
24
24
|
import {
|
|
25
25
|
wireClaimSummarySchema,
|
|
26
|
+
wireParticipantKindSchema,
|
|
27
|
+
modelTargetSchema,
|
|
26
28
|
descriptionFromMeta,
|
|
27
29
|
type WireClaimSummary,
|
|
28
30
|
type ModelClaim,
|
|
29
31
|
type ModelTarget,
|
|
30
32
|
type ParticipantKind,
|
|
31
33
|
} from './coordination/schema.js';
|
|
34
|
+
import {
|
|
35
|
+
claimConflictContextSchema,
|
|
36
|
+
normalizeClaimConflict,
|
|
37
|
+
type ClaimConflictContext,
|
|
38
|
+
} from './claims/conflict.js';
|
|
39
|
+
|
|
40
|
+
export type {
|
|
41
|
+
ClaimConflictContext,
|
|
42
|
+
ClaimConflictCounterparty,
|
|
43
|
+
} from './claims/conflict.js';
|
|
32
44
|
|
|
33
45
|
export type { ErrorCode, WireErrorCode, ErrorCategory, ErrorCodeSpec, RecoveryClass } from './errorCodes.js';
|
|
34
46
|
export {
|
|
@@ -368,6 +380,44 @@ export interface ClaimContext {
|
|
|
368
380
|
|
|
369
381
|
export type ClaimErrorClaim = WireClaimSummary | ClaimContext;
|
|
370
382
|
|
|
383
|
+
function conflictFromClaims(
|
|
384
|
+
claims: readonly ClaimErrorClaim[] | undefined,
|
|
385
|
+
): ClaimConflictContext | undefined {
|
|
386
|
+
const claim = claims?.[0];
|
|
387
|
+
if (!claim) return undefined;
|
|
388
|
+
const summary = wireClaimSummarySchema.safeParse(claim);
|
|
389
|
+
if (summary.success) {
|
|
390
|
+
const actor = 'actor' in claim && typeof claim.actor === 'string'
|
|
391
|
+
? claim.actor
|
|
392
|
+
: undefined;
|
|
393
|
+
return normalizeClaimConflict({
|
|
394
|
+
heldByClaim: summary.data,
|
|
395
|
+
...(actor !== undefined ? { heldBy: actor } : {}),
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
const context = claim as ClaimContext;
|
|
399
|
+
return normalizeClaimConflict({
|
|
400
|
+
...(context.actor !== undefined ? { heldBy: context.actor } : {}),
|
|
401
|
+
...(context.participantKind !== undefined
|
|
402
|
+
? { heldByKind: context.participantKind }
|
|
403
|
+
: {}),
|
|
404
|
+
...(context.claimId !== undefined ? { heldByClaimId: context.claimId } : {}),
|
|
405
|
+
...(context.description !== undefined ? { description: context.description } : {}),
|
|
406
|
+
...(context.expiresAt !== undefined ? { expiresAt: context.expiresAt } : {}),
|
|
407
|
+
...(context.target !== undefined
|
|
408
|
+
? { target: context.target }
|
|
409
|
+
: context.entityType !== undefined || context.entityId !== undefined
|
|
410
|
+
? {
|
|
411
|
+
target: {
|
|
412
|
+
...(context.entityType !== undefined ? { model: context.entityType } : {}),
|
|
413
|
+
...(context.entityId !== undefined ? { id: context.entityId } : {}),
|
|
414
|
+
...(context.field !== undefined ? { field: context.field } : {}),
|
|
415
|
+
},
|
|
416
|
+
}
|
|
417
|
+
: {}),
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
371
421
|
function claimDescription(claim: ClaimErrorClaim | undefined): string | undefined {
|
|
372
422
|
if (!claim) return undefined;
|
|
373
423
|
if ('description' in claim && typeof claim.description === 'string') {
|
|
@@ -434,6 +484,7 @@ export function formatClaimedErrorMessage(args: {
|
|
|
434
484
|
export class AbloClaimedError extends AbloError {
|
|
435
485
|
override readonly type = 'AbloClaimedError' as const;
|
|
436
486
|
readonly claims?: readonly ClaimErrorClaim[];
|
|
487
|
+
readonly conflict?: ClaimConflictContext;
|
|
437
488
|
|
|
438
489
|
constructor(
|
|
439
490
|
message: string,
|
|
@@ -443,10 +494,22 @@ export class AbloClaimedError extends AbloError {
|
|
|
443
494
|
requestId?: string;
|
|
444
495
|
cause?: unknown;
|
|
445
496
|
claims?: readonly ClaimErrorClaim[];
|
|
497
|
+
conflict?: ClaimConflictContext;
|
|
498
|
+
details?: Readonly<Record<string, unknown>>;
|
|
446
499
|
},
|
|
447
500
|
) {
|
|
448
|
-
|
|
501
|
+
const conflict = options?.conflict ?? conflictFromClaims(options?.claims);
|
|
502
|
+
const details = {
|
|
503
|
+
...(options?.details ?? {}),
|
|
504
|
+
...(options?.claims !== undefined ? { claims: options.claims } : {}),
|
|
505
|
+
...(conflict !== undefined ? { conflict } : {}),
|
|
506
|
+
};
|
|
507
|
+
super(message, {
|
|
508
|
+
...options,
|
|
509
|
+
...(Object.keys(details).length > 0 ? { details } : {}),
|
|
510
|
+
});
|
|
449
511
|
if (options?.claims !== undefined) this.claims = options.claims;
|
|
512
|
+
if (conflict !== undefined) this.conflict = conflict;
|
|
450
513
|
}
|
|
451
514
|
}
|
|
452
515
|
|
|
@@ -681,8 +744,13 @@ const NestedErrorShapeSchema = z
|
|
|
681
744
|
field: OptionalWireStringSchema,
|
|
682
745
|
requiredCapability: requiredCapabilityWireSchema.optional().catch(undefined),
|
|
683
746
|
heldBy: OptionalWireStringSchema,
|
|
747
|
+
heldByKind: wireParticipantKindSchema.optional().catch(undefined),
|
|
748
|
+
heldByClaimId: OptionalWireStringSchema,
|
|
749
|
+
heldByExpiresAt: z.number().optional().catch(undefined),
|
|
684
750
|
heldByClaim: wireClaimSummarySchema.optional().catch(undefined),
|
|
685
751
|
claims: z.array(wireClaimSummarySchema).optional().catch(undefined),
|
|
752
|
+
conflict: claimConflictContextSchema.optional().catch(undefined),
|
|
753
|
+
target: modelTargetSchema.unwrap().partial().optional().catch(undefined),
|
|
686
754
|
})
|
|
687
755
|
.loose();
|
|
688
756
|
|
|
@@ -706,8 +774,13 @@ const ErrorBodyShapeSchema = z
|
|
|
706
774
|
message: OptionalWireStringSchema,
|
|
707
775
|
requiredCapability: requiredCapabilityWireSchema.optional().catch(undefined),
|
|
708
776
|
heldBy: OptionalWireStringSchema,
|
|
777
|
+
heldByKind: wireParticipantKindSchema.optional().catch(undefined),
|
|
778
|
+
heldByClaimId: OptionalWireStringSchema,
|
|
779
|
+
heldByExpiresAt: z.number().optional().catch(undefined),
|
|
709
780
|
heldByClaim: wireClaimSummarySchema.optional().catch(undefined),
|
|
710
781
|
claims: z.array(wireClaimSummarySchema).optional().catch(undefined),
|
|
782
|
+
conflict: claimConflictContextSchema.optional().catch(undefined),
|
|
783
|
+
target: modelTargetSchema.unwrap().partial().optional().catch(undefined),
|
|
711
784
|
})
|
|
712
785
|
.loose();
|
|
713
786
|
|
|
@@ -763,11 +836,12 @@ export function errorFromWire(
|
|
|
763
836
|
requestId?: string;
|
|
764
837
|
requiredCapability?: RequiredCapability;
|
|
765
838
|
claims?: readonly ClaimErrorClaim[];
|
|
839
|
+
conflict?: ClaimConflictContext;
|
|
766
840
|
/** Everything the producer attached beyond the envelope's own fields. */
|
|
767
841
|
details?: Readonly<Record<string, unknown>>;
|
|
768
842
|
} = {},
|
|
769
843
|
): AbloError {
|
|
770
|
-
const { code, requestId, requiredCapability, claims, details } = opts;
|
|
844
|
+
const { code, requestId, requiredCapability, claims, conflict, details } = opts;
|
|
771
845
|
// Effective status: an explicit HTTP status wins; otherwise fall back to
|
|
772
846
|
// the code's canonical status from the registry (undefined for unknown /
|
|
773
847
|
// forward-compat codes, which then map to the base AbloError).
|
|
@@ -802,9 +876,16 @@ export function errorFromWire(
|
|
|
802
876
|
if (
|
|
803
877
|
code === 'claim_conflict' ||
|
|
804
878
|
code === 'entity_claimed' ||
|
|
805
|
-
code === 'claim_lost'
|
|
879
|
+
code === 'claim_lost' ||
|
|
880
|
+
code === 'fence_token_stale' ||
|
|
881
|
+
code === 'model_claimed' ||
|
|
882
|
+
code === 'model_claimed_timeout' ||
|
|
883
|
+
code === 'claim_lease_unavailable' ||
|
|
884
|
+
code === 'claim_wait_aborted' ||
|
|
885
|
+
code === 'grant_timeout' ||
|
|
886
|
+
code === 'claim_queued'
|
|
806
887
|
) {
|
|
807
|
-
return new AbloClaimedError(message, { ...baseOpts, claims });
|
|
888
|
+
return new AbloClaimedError(message, { ...baseOpts, claims, conflict });
|
|
808
889
|
}
|
|
809
890
|
// A write whose `readAt` watermark went stale — callers re-read and retry.
|
|
810
891
|
if (code === 'stale_context') {
|
|
@@ -854,6 +935,12 @@ const ENVELOPE_KEYS: ReadonlySet<string> = new Set([
|
|
|
854
935
|
'requiredCapability',
|
|
855
936
|
'claims',
|
|
856
937
|
'heldByClaim',
|
|
938
|
+
'heldBy',
|
|
939
|
+
'heldByKind',
|
|
940
|
+
'heldByClaimId',
|
|
941
|
+
'heldByExpiresAt',
|
|
942
|
+
'conflict',
|
|
943
|
+
'target',
|
|
857
944
|
// `reason` is deliberately NOT here: it is domain detail (a driver's words
|
|
858
945
|
// beside the server's message), not an envelope field — the canonical
|
|
859
946
|
// envelope schema has no such key — so it must survive into `details` for a
|
|
@@ -916,6 +1003,30 @@ export function translateHttpError(
|
|
|
916
1003
|
: nested?.heldByClaim
|
|
917
1004
|
? [nested.heldByClaim]
|
|
918
1005
|
: undefined);
|
|
1006
|
+
const conflict =
|
|
1007
|
+
parsed.conflict ??
|
|
1008
|
+
nested?.conflict ??
|
|
1009
|
+
normalizeClaimConflict({
|
|
1010
|
+
...(parsed.heldBy ?? nested?.heldBy
|
|
1011
|
+
? { heldBy: parsed.heldBy ?? nested?.heldBy }
|
|
1012
|
+
: {}),
|
|
1013
|
+
...(parsed.heldByKind ?? nested?.heldByKind
|
|
1014
|
+
? { heldByKind: parsed.heldByKind ?? nested?.heldByKind }
|
|
1015
|
+
: {}),
|
|
1016
|
+
...(parsed.heldByClaimId ?? nested?.heldByClaimId
|
|
1017
|
+
? { heldByClaimId: parsed.heldByClaimId ?? nested?.heldByClaimId }
|
|
1018
|
+
: {}),
|
|
1019
|
+
...(parsed.heldByExpiresAt ?? nested?.heldByExpiresAt
|
|
1020
|
+
? { heldByExpiresAt: parsed.heldByExpiresAt ?? nested?.heldByExpiresAt }
|
|
1021
|
+
: {}),
|
|
1022
|
+
...(parsed.heldByClaim ?? nested?.heldByClaim
|
|
1023
|
+
? { heldByClaim: parsed.heldByClaim ?? nested?.heldByClaim }
|
|
1024
|
+
: {}),
|
|
1025
|
+
...(parsed.target ?? nested?.target
|
|
1026
|
+
? { target: parsed.target ?? nested?.target }
|
|
1027
|
+
: {}),
|
|
1028
|
+
}) ??
|
|
1029
|
+
conflictFromClaims(claims);
|
|
919
1030
|
|
|
920
1031
|
// The envelope carries `request_id`, so a caller that has only the body still
|
|
921
1032
|
// gets the correlation id — without this it was dropped unless the caller
|
|
@@ -935,6 +1046,7 @@ export function translateHttpError(
|
|
|
935
1046
|
requestId: requestId ?? (typeof bodyRequestId === 'string' ? bodyRequestId : undefined),
|
|
936
1047
|
requiredCapability,
|
|
937
1048
|
claims,
|
|
1049
|
+
conflict,
|
|
938
1050
|
details: Object.keys(details).length > 0 ? details : undefined,
|
|
939
1051
|
});
|
|
940
1052
|
}
|
|
@@ -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';
|
|
@@ -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`
|