feat(ql3): establish 3.0 incubation baseline

This commit is contained in:
whyour
2026-08-12 00:25:26 +08:00
parent 4bf92dcfeb
commit c699c32461
2817 changed files with 779642 additions and 653 deletions
@@ -0,0 +1,256 @@
import {
normalizeSecurityAuditRecord,
type SecurityAuditRecord,
} from '../security/audit/securityAudit';
import type { SecuritySubject } from '../security/security';
export const WORKER_CREDENTIAL_STATES = ['active', 'revoked'] as const;
export type WorkerCredentialState = (typeof WORKER_CREDENTIAL_STATES)[number];
export interface WorkerCredentialRecord {
readonly credentialId: string;
readonly version: number;
readonly state: WorkerCredentialState;
readonly workerId: string;
readonly secretDigest: string;
readonly createdAtMs: number;
readonly notBeforeAtMs: number;
readonly expiresAtMs: number;
}
export interface WorkerCredentialRepository {
resolve(credentialId: string): Promise<WorkerCredentialRecord | null>;
}
export type WorkerCredentialAdministrationOperation =
| 'issue'
| 'rotate'
| 'revoke';
export interface WorkerCredentialMutationRecord {
readonly mutationId: string;
readonly operation: WorkerCredentialAdministrationOperation;
readonly credentialId: string;
readonly credentialVersion: number;
readonly expectedPreviousVersion: number;
readonly changedBy: SecuritySubject;
readonly createdAtMs: number;
}
export interface AppendWorkerCredentialCommand {
readonly expectedCurrentVersion: number;
readonly credential: WorkerCredentialRecord;
readonly mutation: WorkerCredentialMutationRecord;
readonly audit: SecurityAuditRecord;
}
export interface AppendWorkerCredentialResult {
readonly status: 'created' | 'existing';
readonly credential: Readonly<WorkerCredentialRecord>;
readonly mutation: Readonly<WorkerCredentialMutationRecord>;
}
export interface ResolvedWorkerCredentialMutation {
readonly credential: Readonly<WorkerCredentialRecord>;
readonly mutation: Readonly<WorkerCredentialMutationRecord>;
readonly audit: Readonly<SecurityAuditRecord>;
}
export interface WorkerCredentialAdministrationRepository {
resolveMutation(
mutationId: string,
): Promise<ResolvedWorkerCredentialMutation | null>;
append(
command: AppendWorkerCredentialCommand,
): Promise<AppendWorkerCredentialResult>;
}
export class WorkerCredentialUnavailableError extends Error {
readonly code = 'WORKER_CREDENTIAL_UNAVAILABLE';
constructor() {
super('Worker credential storage is unavailable');
this.name = 'WorkerCredentialUnavailableError';
}
}
export class WorkerCredentialMutationConflictError extends Error {
readonly code = 'WORKER_CREDENTIAL_MUTATION_CONFLICT';
constructor() {
super('Worker credential mutation conflicts with an existing fact');
this.name = 'WorkerCredentialMutationConflictError';
}
}
export class WorkerCredentialVersionConflictError extends Error {
readonly code = 'WORKER_CREDENTIAL_VERSION_CONFLICT';
constructor() {
super('Worker credential version fence was rejected');
this.name = 'WorkerCredentialVersionConflictError';
}
}
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const CREDENTIAL_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/;
const UUID_V4 =
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
function exact(value: object, keys: readonly string[], name: string): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new TypeError(`${name} shape is invalid`);
}
}
function safeTime(name: string, value: number): number {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`${name} is invalid`);
}
return value;
}
export function normalizeWorkerCredentialId(value: string): string {
if (typeof value !== 'string' || !CREDENTIAL_ID.test(value)) {
throw new TypeError('Worker credential ID is invalid');
}
return value;
}
export function normalizeWorkerCredentialRecord(
value: WorkerCredentialRecord,
): Readonly<WorkerCredentialRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential record is invalid');
}
exact(
value,
[
'credentialId',
'version',
'state',
'workerId',
'secretDigest',
'createdAtMs',
'notBeforeAtMs',
'expiresAtMs',
],
'Worker credential record',
);
normalizeWorkerCredentialId(value.credentialId);
if (!Number.isSafeInteger(value.version) || value.version < 1) {
throw new RangeError('Worker credential version is invalid');
}
if (!WORKER_CREDENTIAL_STATES.includes(value.state)) {
throw new TypeError('Worker credential state is invalid');
}
if (!SAFE_ID.test(value.workerId)) {
throw new TypeError('Worker credential worker ID is invalid');
}
if (!/^[0-9a-f]{64}$/.test(value.secretDigest)) {
throw new TypeError('Worker credential digest is invalid');
}
const createdAtMs = safeTime('Worker credential createdAtMs', value.createdAtMs);
const notBeforeAtMs = safeTime(
'Worker credential notBeforeAtMs',
value.notBeforeAtMs,
);
const expiresAtMs = safeTime(
'Worker credential expiresAtMs',
value.expiresAtMs,
);
if (expiresAtMs <= Math.max(createdAtMs, notBeforeAtMs)) {
throw new RangeError('Worker credential lifetime is invalid');
}
return Object.freeze({ ...value });
}
export function normalizeWorkerCredentialMutationId(value: string): string {
if (typeof value !== 'string' || !UUID_V4.test(value)) {
throw new TypeError('Worker credential mutation ID is invalid');
}
return value;
}
export function normalizeAppendWorkerCredentialCommand(
value: AppendWorkerCredentialCommand,
): Readonly<AppendWorkerCredentialCommand> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential append command is invalid');
}
exact(
value,
['expectedCurrentVersion', 'credential', 'mutation', 'audit'],
'Worker credential append command',
);
const credential = normalizeWorkerCredentialRecord(value.credential);
const mutation = value.mutation;
if (!mutation || typeof mutation !== 'object' || Array.isArray(mutation)) {
throw new TypeError('Worker credential mutation is invalid');
}
exact(
mutation,
[
'mutationId',
'operation',
'credentialId',
'credentialVersion',
'expectedPreviousVersion',
'changedBy',
'createdAtMs',
],
'Worker credential mutation',
);
normalizeWorkerCredentialMutationId(mutation.mutationId);
if (!['issue', 'rotate', 'revoke'].includes(mutation.operation)) {
throw new TypeError('Worker credential mutation operation is invalid');
}
if (
!Number.isSafeInteger(value.expectedCurrentVersion) ||
value.expectedCurrentVersion < 0 ||
mutation.expectedPreviousVersion !== value.expectedCurrentVersion ||
mutation.credentialVersion !== value.expectedCurrentVersion + 1 ||
mutation.credentialId !== credential.credentialId ||
mutation.createdAtMs !== credential.createdAtMs ||
credential.state !== (mutation.operation === 'revoke' ? 'revoked' : 'active')
) {
throw new RangeError('Worker credential mutation fence is invalid');
}
if (
!mutation.changedBy ||
typeof mutation.changedBy !== 'object' ||
Array.isArray(mutation.changedBy)
) {
throw new TypeError('Worker credential mutation actor is invalid');
}
exact(mutation.changedBy, ['type', 'id'], 'Worker credential mutation actor');
if (!['user', 'system'].includes(mutation.changedBy.type)) {
throw new TypeError('Worker credential mutation actor is invalid');
}
const audit = normalizeSecurityAuditRecord(value.audit);
if (
audit.eventId !== mutation.mutationId ||
audit.occurredAtMs !== mutation.createdAtMs ||
audit.operationId !== `worker_credential.${mutation.operation}` ||
audit.projectId !== null ||
audit.subject?.type !== mutation.changedBy.type ||
audit.subject.id !== mutation.changedBy.id ||
audit.outcome !== 'allowed' ||
audit.reasons.length !== 1 ||
audit.reasons[0] !== 'worker_credential_admin' ||
audit.fence !== null
) {
throw new TypeError('Worker credential mutation audit is invalid');
}
return Object.freeze({
expectedCurrentVersion: value.expectedCurrentVersion,
credential,
mutation: Object.freeze({
...mutation,
changedBy: Object.freeze({ ...mutation.changedBy }),
}),
audit,
});
}
@@ -0,0 +1,708 @@
import { createHash } from 'node:crypto';
import {
normalizeAppendWorkerCredentialCommand,
normalizeWorkerCredentialId,
normalizeWorkerCredentialMutationId,
type AppendWorkerCredentialCommand,
type AppendWorkerCredentialResult,
type ResolvedWorkerCredentialMutation,
type WorkerCredentialAdministrationRepository,
} from './workerCredential';
import type {
HeartbeatWorkerSessionCommand,
TransitionWorkerSessionCommand,
WorkerSessionRecord,
WorkerSessionRepository,
} from './workerSession';
export const WORKER_CREDENTIAL_DELIVERY_STATES = [
'credential_committed',
'published',
'observed',
'previous_revoked',
] as const;
export type WorkerCredentialDeliveryState =
(typeof WORKER_CREDENTIAL_DELIVERY_STATES)[number];
export interface WorkerCredentialDeliveryIntent {
readonly deliveryId: string;
readonly workerId: string;
readonly credentialId: string;
readonly credentialVersion: number;
readonly previousCredentialId: string | null;
readonly secretDigest: string;
readonly tokenDigest: string;
readonly deploymentTargetDigest: string;
readonly deploymentGeneration: string;
readonly stagedAtMs: number;
}
export interface WorkerCredentialDeliveryRecord {
readonly deliveryId: string;
readonly version: number;
readonly state: WorkerCredentialDeliveryState;
readonly workerId: string;
readonly credentialId: string;
readonly credentialVersion: number;
readonly previousCredentialId: string | null;
readonly secretDigest: string;
readonly tokenDigest: string;
readonly deploymentTargetDigest: string;
readonly deploymentGeneration: string;
readonly stagedAtMs: number;
readonly credentialCommittedAtMs: number;
readonly publishedAtMs: number | null;
readonly publicationDigest: string | null;
readonly observedAtMs: number | null;
readonly observedSessionId: string | null;
readonly observedSessionVersion: number | null;
readonly previousRevokedAtMs: number | null;
}
export interface CommitWorkerCredentialDeliveryCommand {
readonly credential: AppendWorkerCredentialCommand;
readonly delivery: WorkerCredentialDeliveryRecord;
}
export interface PublishWorkerCredentialDeliveryCommand {
readonly deliveryId: string;
readonly expectedVersion: number;
readonly publicationDigest: string;
readonly publishedAtMs: number;
}
export interface RevokePreviousWorkerCredentialDeliveryCommand {
readonly credential: AppendWorkerCredentialCommand;
readonly delivery: WorkerCredentialDeliveryRecord;
}
export const MAX_WORKER_CREDENTIAL_DELIVERY_RECOVERY_PAGE_SIZE = 64;
export const MAX_WORKER_CREDENTIAL_STAGE_DISCARD_PAGE_SIZE = 64;
export const WORKER_CREDENTIAL_STAGE_DISCARD_STATES = [
'discard_authorized',
'discarded',
] as const;
export type WorkerCredentialStageDiscardState =
(typeof WORKER_CREDENTIAL_STAGE_DISCARD_STATES)[number];
export interface WorkerCredentialStageDiscardRecord
extends WorkerCredentialDeliveryIntent {
readonly version: number;
readonly state: WorkerCredentialStageDiscardState;
readonly authorizedAtMs: number;
readonly discardedAtMs: number | null;
}
export interface MarkWorkerCredentialStageDiscardedCommand {
readonly deliveryId: string;
readonly expectedVersion: number;
}
export interface WorkerCredentialStageDiscardRecoveryPage {
readonly observedAtMs: number;
readonly discards: readonly Readonly<WorkerCredentialStageDiscardRecord>[];
readonly truncated: boolean;
readonly nextCursor?: string;
}
const WORKER_CREDENTIAL_DELIVERY_TOKEN_DIGEST_DOMAIN = Buffer.from(
'qinglong/worker-credential-delivery-token@v1\0',
'utf8',
);
export function workerCredentialDeliveryTokenDigest(
token: Uint8Array,
): string {
if (
!(token instanceof Uint8Array) ||
token.byteLength < 1 ||
token.byteLength > 256
) {
throw new TypeError('Worker credential delivery token bytes are invalid');
}
return createHash('sha256')
.update(WORKER_CREDENTIAL_DELIVERY_TOKEN_DIGEST_DOMAIN)
.update(token)
.digest('hex');
}
export interface WorkerCredentialDeliveryRecoveryPage {
readonly observedAtMs: number;
readonly deliveries: readonly Readonly<WorkerCredentialDeliveryRecord>[];
readonly truncated: boolean;
readonly nextCursor?: string;
}
export interface AuthenticatedWorkerCredentialIdentity {
readonly workerId: string;
readonly credentialId: string;
readonly credentialVersion: number;
}
export interface AuthenticatedWorkerSessionRepository
extends WorkerSessionRepository {
heartbeatAuthenticated(
command: HeartbeatWorkerSessionCommand,
credential: AuthenticatedWorkerCredentialIdentity,
): Promise<WorkerSessionRecord>;
transitionAuthenticated(
command: TransitionWorkerSessionCommand,
credential: AuthenticatedWorkerCredentialIdentity,
): Promise<WorkerSessionRecord>;
}
export interface ResolvedWorkerCredentialDelivery
extends ResolvedWorkerCredentialMutation {
readonly delivery: Readonly<WorkerCredentialDeliveryRecord>;
}
export interface WorkerCredentialDeliveryAdministrationRepository
extends WorkerCredentialAdministrationRepository {
resolveDelivery(
deliveryId: string,
): Promise<Readonly<WorkerCredentialDeliveryRecord> | null>;
resolveDelivered(
deliveryId: string,
): Promise<ResolvedWorkerCredentialDelivery | null>;
commitDelivered(
command: CommitWorkerCredentialDeliveryCommand,
): Promise<AppendWorkerCredentialResult>;
markPublished(
command: PublishWorkerCredentialDeliveryCommand,
): Promise<Readonly<WorkerCredentialDeliveryRecord>>;
listRecoveryPage(options?: Readonly<{
afterDeliveryId?: string;
limit?: number;
}>): Promise<Readonly<WorkerCredentialDeliveryRecoveryPage>>;
revokePreviousDelivered(
command: RevokePreviousWorkerCredentialDeliveryCommand,
): Promise<AppendWorkerCredentialResult>;
resolveStageDiscard(
deliveryId: string,
): Promise<Readonly<WorkerCredentialStageDiscardRecord> | null>;
authorizeStageDiscard(
intent: WorkerCredentialDeliveryIntent,
): Promise<Readonly<WorkerCredentialStageDiscardRecord>>;
markStageDiscarded(
command: MarkWorkerCredentialStageDiscardedCommand,
): Promise<Readonly<WorkerCredentialStageDiscardRecord>>;
listStageDiscardRecoveryPage(options?: Readonly<{
afterDeliveryId?: string;
limit?: number;
}>): Promise<Readonly<WorkerCredentialStageDiscardRecoveryPage>>;
}
export class WorkerCredentialDeliveryConflictError extends Error {
readonly code = 'WORKER_CREDENTIAL_DELIVERY_CONFLICT';
constructor() {
super('Worker credential delivery conflicts with an existing fact');
this.name = 'WorkerCredentialDeliveryConflictError';
}
}
export class WorkerCredentialDeliveryUnavailableError extends Error {
readonly code = 'WORKER_CREDENTIAL_DELIVERY_UNAVAILABLE';
constructor() {
super('Worker credential delivery is unavailable');
this.name = 'WorkerCredentialDeliveryUnavailableError';
}
}
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SHA256 = /^[0-9a-f]{64}$/;
const UUID_V7 =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
function exact(value: object, keys: readonly string[], name: string): void {
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new TypeError(`${name} shape is invalid`);
}
}
function time(name: string, value: number): number {
if (!Number.isSafeInteger(value) || value < 0) {
throw new RangeError(`${name} is invalid`);
}
return value;
}
function nullableTime(name: string, value: number | null): number | null {
return value === null ? null : time(name, value);
}
function digest(name: string, value: string): string {
if (typeof value !== 'string' || !SHA256.test(value)) {
throw new TypeError(`${name} is invalid`);
}
return value;
}
export function normalizeAuthenticatedWorkerCredentialIdentity(
value: AuthenticatedWorkerCredentialIdentity,
): Readonly<AuthenticatedWorkerCredentialIdentity> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Authenticated Worker credential identity is invalid');
}
exact(
value,
['workerId', 'credentialId', 'credentialVersion'],
'Authenticated Worker credential identity',
);
if (typeof value.workerId !== 'string' || !SAFE_ID.test(value.workerId)) {
throw new TypeError('Authenticated Worker identity is invalid');
}
normalizeWorkerCredentialId(value.credentialId);
if (
!Number.isSafeInteger(value.credentialVersion) ||
value.credentialVersion < 1 ||
value.credentialVersion > 2_147_483_647
) {
throw new RangeError('Authenticated Worker credential version is invalid');
}
return Object.freeze({ ...value });
}
export function normalizeWorkerCredentialDeliveryIntent(
value: WorkerCredentialDeliveryIntent,
): Readonly<WorkerCredentialDeliveryIntent> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential delivery intent is invalid');
}
exact(
value,
[
'deliveryId',
'workerId',
'credentialId',
'credentialVersion',
'previousCredentialId',
'secretDigest',
'tokenDigest',
'deploymentTargetDigest',
'deploymentGeneration',
'stagedAtMs',
],
'Worker credential delivery intent',
);
normalizeWorkerCredentialMutationId(value.deliveryId);
normalizeWorkerCredentialId(value.credentialId);
if (value.previousCredentialId !== null) {
normalizeWorkerCredentialId(value.previousCredentialId);
if (value.previousCredentialId === value.credentialId) {
throw new TypeError('Worker credential delivery requires a new credential ID');
}
}
if (
typeof value.workerId !== 'string' ||
!SAFE_ID.test(value.workerId) ||
typeof value.deploymentGeneration !== 'string' ||
!SAFE_ID.test(value.deploymentGeneration) ||
value.credentialVersion !== 1
) {
throw new TypeError('Worker credential delivery intent identity is invalid');
}
digest('Worker credential delivery secret digest', value.secretDigest);
digest('Worker credential delivery token digest', value.tokenDigest);
digest(
'Worker credential delivery deployment target digest',
value.deploymentTargetDigest,
);
time('Worker credential delivery stagedAtMs', value.stagedAtMs);
return Object.freeze({ ...value });
}
export function normalizeWorkerCredentialStageDiscardRecord(
value: WorkerCredentialStageDiscardRecord,
): Readonly<WorkerCredentialStageDiscardRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential stage discard record is invalid');
}
exact(
value,
[
'deliveryId',
'version',
'state',
'workerId',
'credentialId',
'credentialVersion',
'previousCredentialId',
'secretDigest',
'tokenDigest',
'deploymentTargetDigest',
'deploymentGeneration',
'stagedAtMs',
'authorizedAtMs',
'discardedAtMs',
],
'Worker credential stage discard record',
);
const intent = normalizeWorkerCredentialDeliveryIntent({
deliveryId: value.deliveryId,
workerId: value.workerId,
credentialId: value.credentialId,
credentialVersion: value.credentialVersion,
previousCredentialId: value.previousCredentialId,
secretDigest: value.secretDigest,
tokenDigest: value.tokenDigest,
deploymentTargetDigest: value.deploymentTargetDigest,
deploymentGeneration: value.deploymentGeneration,
stagedAtMs: value.stagedAtMs,
});
const authorizedAtMs = time(
'Worker credential stage discard authorization',
value.authorizedAtMs,
);
const discardedAtMs = nullableTime(
'Worker credential stage discard completion',
value.discardedAtMs,
);
if (
!WORKER_CREDENTIAL_STAGE_DISCARD_STATES.includes(value.state) ||
value.version !==
WORKER_CREDENTIAL_STAGE_DISCARD_STATES.indexOf(value.state) + 1 ||
(value.state === 'discard_authorized' && discardedAtMs !== null) ||
(value.state === 'discarded' &&
(discardedAtMs === null || discardedAtMs < authorizedAtMs))
) {
throw new TypeError('Worker credential stage discard state is invalid');
}
return Object.freeze({
...intent,
version: value.version,
state: value.state,
authorizedAtMs,
discardedAtMs,
});
}
export function normalizeMarkWorkerCredentialStageDiscardedCommand(
value: MarkWorkerCredentialStageDiscardedCommand,
): Readonly<MarkWorkerCredentialStageDiscardedCommand> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential stage discard completion is invalid');
}
exact(
value,
['deliveryId', 'expectedVersion'],
'Worker credential stage discard completion',
);
normalizeWorkerCredentialMutationId(value.deliveryId);
if (value.expectedVersion !== 1) {
throw new RangeError('Worker credential stage discard fence is invalid');
}
return Object.freeze({ ...value });
}
export function normalizeWorkerCredentialStageDiscardRecoveryPage(
value: WorkerCredentialStageDiscardRecoveryPage,
): Readonly<WorkerCredentialStageDiscardRecoveryPage> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential stage discard recovery page is invalid');
}
exact(
value,
[
'observedAtMs',
'discards',
'truncated',
...(value.nextCursor === undefined ? [] : ['nextCursor']),
],
'Worker credential stage discard recovery page',
);
time('Worker credential stage discard recovery observation', value.observedAtMs);
if (
!Array.isArray(value.discards) ||
value.discards.length > MAX_WORKER_CREDENTIAL_STAGE_DISCARD_PAGE_SIZE ||
typeof value.truncated !== 'boolean'
) {
throw new TypeError('Worker credential stage discard recovery bound is invalid');
}
const discards = value.discards.map((record) =>
normalizeWorkerCredentialStageDiscardRecord(record));
for (let index = 0; index < discards.length; index += 1) {
if (
discards[index]!.state !== 'discard_authorized' ||
value.observedAtMs < discards[index]!.authorizedAtMs ||
(index > 0 && discards[index - 1]!.deliveryId >= discards[index]!.deliveryId)
) {
throw new TypeError('Worker credential stage discard recovery order is invalid');
}
}
const last = discards.at(-1);
if (
value.truncated !== (value.nextCursor !== undefined) ||
(value.nextCursor !== undefined &&
(!last || value.nextCursor !== last.deliveryId))
) {
throw new TypeError('Worker credential stage discard recovery cursor is invalid');
}
return Object.freeze({
observedAtMs: value.observedAtMs,
discards: Object.freeze(discards),
truncated: value.truncated,
...(value.nextCursor === undefined
? {}
: { nextCursor: value.nextCursor }),
});
}
export function normalizeWorkerCredentialDeliveryRecord(
value: WorkerCredentialDeliveryRecord,
): Readonly<WorkerCredentialDeliveryRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential delivery record is invalid');
}
exact(
value,
[
'deliveryId',
'version',
'state',
'workerId',
'credentialId',
'credentialVersion',
'previousCredentialId',
'secretDigest',
'tokenDigest',
'deploymentTargetDigest',
'deploymentGeneration',
'stagedAtMs',
'credentialCommittedAtMs',
'publishedAtMs',
'publicationDigest',
'observedAtMs',
'observedSessionId',
'observedSessionVersion',
'previousRevokedAtMs',
],
'Worker credential delivery record',
);
normalizeWorkerCredentialMutationId(value.deliveryId);
normalizeWorkerCredentialDeliveryIntent({
deliveryId: value.deliveryId,
workerId: value.workerId,
credentialId: value.credentialId,
credentialVersion: value.credentialVersion,
previousCredentialId: value.previousCredentialId,
secretDigest: value.secretDigest,
tokenDigest: value.tokenDigest,
deploymentTargetDigest: value.deploymentTargetDigest,
deploymentGeneration: value.deploymentGeneration,
stagedAtMs: value.stagedAtMs,
});
if (!WORKER_CREDENTIAL_DELIVERY_STATES.includes(value.state)) {
throw new TypeError('Worker credential delivery state is invalid');
}
const stagedAtMs = time(
'Worker credential delivery stagedAtMs',
value.stagedAtMs,
);
const credentialCommittedAtMs = time(
'Worker credential delivery credentialCommittedAtMs',
value.credentialCommittedAtMs,
);
const publishedAtMs = nullableTime(
'Worker credential delivery publishedAtMs',
value.publishedAtMs,
);
const observedAtMs = nullableTime(
'Worker credential delivery observedAtMs',
value.observedAtMs,
);
const previousRevokedAtMs = nullableTime(
'Worker credential delivery previousRevokedAtMs',
value.previousRevokedAtMs,
);
if (credentialCommittedAtMs < stagedAtMs) {
throw new RangeError('Worker credential delivery commit precedes staging');
}
const expectedVersion =
WORKER_CREDENTIAL_DELIVERY_STATES.indexOf(value.state) + 1;
if (value.version !== expectedVersion) {
throw new RangeError('Worker credential delivery version is invalid');
}
const published = expectedVersion >= 2;
const observed = expectedVersion >= 3;
const revoked = expectedVersion >= 4;
if (
published !== (publishedAtMs !== null) ||
published !== (value.publicationDigest !== null) ||
observed !== (observedAtMs !== null) ||
observed !== (value.observedSessionId !== null) ||
observed !== (value.observedSessionVersion !== null) ||
revoked !== (previousRevokedAtMs !== null)
) {
throw new TypeError('Worker credential delivery state evidence is incomplete');
}
if (value.publicationDigest !== null) {
digest(
'Worker credential delivery publication digest',
value.publicationDigest,
);
}
if (
value.observedSessionId !== null &&
!UUID_V7.test(value.observedSessionId)
) {
throw new TypeError('Worker credential delivery observed Session is invalid');
}
if (
value.observedSessionVersion !== null &&
(!Number.isSafeInteger(value.observedSessionVersion) ||
value.observedSessionVersion < 1)
) {
throw new RangeError('Worker credential delivery observed version is invalid');
}
if (
(publishedAtMs !== null && publishedAtMs < credentialCommittedAtMs) ||
(observedAtMs !== null &&
(publishedAtMs === null || observedAtMs < publishedAtMs)) ||
(previousRevokedAtMs !== null &&
(observedAtMs === null || previousRevokedAtMs < observedAtMs))
) {
throw new RangeError('Worker credential delivery evidence time is invalid');
}
if (revoked && value.previousCredentialId === null) {
throw new TypeError('Worker credential delivery has no previous credential');
}
return Object.freeze({ ...value });
}
export function normalizeCommitWorkerCredentialDeliveryCommand(
value: CommitWorkerCredentialDeliveryCommand,
): Readonly<CommitWorkerCredentialDeliveryCommand> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential delivery commit is invalid');
}
exact(value, ['credential', 'delivery'], 'Worker credential delivery commit');
const credential = normalizeAppendWorkerCredentialCommand(value.credential);
const delivery = normalizeWorkerCredentialDeliveryRecord(value.delivery);
if (
credential.mutation.operation !== 'issue' ||
credential.expectedCurrentVersion !== 0 ||
delivery.state !== 'credential_committed' ||
delivery.deliveryId !== credential.mutation.mutationId ||
delivery.credentialId !== credential.credential.credentialId ||
delivery.credentialVersion !== credential.credential.version ||
delivery.workerId !== credential.credential.workerId ||
delivery.secretDigest !== credential.credential.secretDigest ||
delivery.credentialCommittedAtMs !== credential.credential.createdAtMs
) {
throw new TypeError('Worker credential delivery commit authority is invalid');
}
return Object.freeze({ credential, delivery });
}
export function normalizePublishWorkerCredentialDeliveryCommand(
value: PublishWorkerCredentialDeliveryCommand,
): Readonly<PublishWorkerCredentialDeliveryCommand> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential delivery publication is invalid');
}
exact(
value,
['deliveryId', 'expectedVersion', 'publicationDigest', 'publishedAtMs'],
'Worker credential delivery publication',
);
normalizeWorkerCredentialMutationId(value.deliveryId);
if (value.expectedVersion !== 1) {
throw new RangeError('Worker credential delivery publication fence is invalid');
}
digest(
'Worker credential delivery publication digest',
value.publicationDigest,
);
time('Worker credential delivery publishedAtMs', value.publishedAtMs);
return Object.freeze({ ...value });
}
export function normalizeRevokePreviousWorkerCredentialDeliveryCommand(
value: RevokePreviousWorkerCredentialDeliveryCommand,
): Readonly<RevokePreviousWorkerCredentialDeliveryCommand> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential delivery revoke is invalid');
}
exact(value, ['credential', 'delivery'], 'Worker credential delivery revoke');
const credential = normalizeAppendWorkerCredentialCommand(value.credential);
const delivery = normalizeWorkerCredentialDeliveryRecord(value.delivery);
if (
credential.mutation.operation !== 'revoke' ||
credential.expectedCurrentVersion !== 1 ||
credential.credential.version !== 2 ||
credential.credential.state !== 'revoked' ||
delivery.version !== 4 ||
delivery.state !== 'previous_revoked' ||
delivery.previousCredentialId === null ||
delivery.previousCredentialId !== credential.credential.credentialId ||
delivery.workerId !== credential.credential.workerId ||
delivery.previousRevokedAtMs !== credential.credential.createdAtMs
) {
throw new TypeError('Worker credential delivery revoke authority is invalid');
}
return Object.freeze({ credential, delivery });
}
export function normalizeWorkerCredentialDeliveryRecoveryPage(
value: WorkerCredentialDeliveryRecoveryPage,
): Readonly<WorkerCredentialDeliveryRecoveryPage> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker credential delivery recovery page is invalid');
}
const expected = [
'observedAtMs',
'deliveries',
'truncated',
...(value.nextCursor === undefined ? [] : ['nextCursor']),
];
exact(value, expected, 'Worker credential delivery recovery page');
time('Worker credential delivery recovery observation', value.observedAtMs);
if (
!Array.isArray(value.deliveries) ||
value.deliveries.length > MAX_WORKER_CREDENTIAL_DELIVERY_RECOVERY_PAGE_SIZE ||
typeof value.truncated !== 'boolean'
) {
throw new TypeError('Worker credential delivery recovery page bound is invalid');
}
const deliveries = value.deliveries.map(
(delivery) => normalizeWorkerCredentialDeliveryRecord(delivery),
);
for (let index = 0; index < deliveries.length; index += 1) {
const delivery = deliveries[index]!;
if (
delivery.state === 'previous_revoked' ||
(delivery.state === 'observed' && delivery.previousCredentialId === null) ||
value.observedAtMs <
(delivery.observedAtMs ??
delivery.publishedAtMs ??
delivery.credentialCommittedAtMs) ||
(index > 0 && deliveries[index - 1]!.deliveryId >= delivery.deliveryId)
) {
throw new TypeError('Worker credential delivery recovery page order is invalid');
}
}
const last = deliveries.at(-1);
if (
value.truncated !== (value.nextCursor !== undefined) ||
(value.nextCursor !== undefined &&
(!last || value.nextCursor !== last.deliveryId))
) {
throw new TypeError('Worker credential delivery recovery cursor is invalid');
}
return Object.freeze({
observedAtMs: value.observedAtMs,
deliveries: Object.freeze(deliveries),
truncated: value.truncated,
...(value.nextCursor === undefined
? {}
: { nextCursor: value.nextCursor }),
});
}
@@ -0,0 +1,449 @@
import { createHash } from 'node:crypto';
import {
normalizeWorkerCredentialId,
normalizeWorkerCredentialMutationId,
} from './workerCredential';
import type { SecuritySubject } from '../security/security';
export const WORKER_CREDENTIAL_MANAGEMENT_PLAN_SCHEMA =
'qinglong/worker-credential-management-plan@v1' as const;
export const WORKER_CREDENTIAL_MANAGEMENT_ACTIONS = [
'issue',
'rotate',
] as const;
export const MAX_WORKER_CREDENTIAL_MANAGEMENT_PLAN_LIFETIME_MS =
15 * 60 * 1000;
export const MAX_WORKER_CREDENTIAL_MANAGEMENT_PLAN_BYTES = 16 * 1024;
export type WorkerCredentialManagementAction =
(typeof WORKER_CREDENTIAL_MANAGEMENT_ACTIONS)[number];
export interface WorkerCredentialManagementTarget {
readonly deliveryId: string;
readonly workerId: string;
readonly credentialId: string;
readonly previousCredentialId: string | null;
readonly credentialNotBeforeAtMs: number;
readonly credentialExpiresAtMs: number;
readonly deploymentTargetDigest: string;
readonly deploymentGeneration: string;
}
export interface WorkerCredentialManagementPlan {
readonly schema: typeof WORKER_CREDENTIAL_MANAGEMENT_PLAN_SCHEMA;
readonly actionRef: string;
readonly authorityProjectId: string;
readonly action: WorkerCredentialManagementAction;
readonly target: Readonly<WorkerCredentialManagementTarget>;
readonly requestedBy: Readonly<SecuritySubject>;
readonly plannedAtMs: number;
readonly expiresAtMs: number;
readonly planDigest: string;
readonly previewDigest: string;
}
export interface CreateWorkerCredentialManagementPlanInput {
readonly actionRef: string;
readonly authorityProjectId: string;
readonly action: WorkerCredentialManagementAction;
readonly target: WorkerCredentialManagementTarget;
readonly requestedBy: SecuritySubject;
readonly plannedAtMs: number;
readonly expiresAtMs: number;
}
export interface CreateWorkerCredentialManagementPlanResult {
readonly status: 'created' | 'existing';
readonly plan: Readonly<WorkerCredentialManagementPlan>;
}
export interface WorkerCredentialManagementPlanRepository {
create(
plan: Readonly<WorkerCredentialManagementPlan>,
): Promise<Readonly<CreateWorkerCredentialManagementPlanResult>>;
findByActionRef(
actionRef: string,
): Promise<Readonly<WorkerCredentialManagementPlan> | null>;
}
export class InvalidWorkerCredentialManagementPlanError extends TypeError {
readonly code = 'WORKER_CREDENTIAL_MANAGEMENT_PLAN_INVALID';
constructor(message: string) {
super(`Worker credential management plan is invalid: ${message}`);
this.name = 'InvalidWorkerCredentialManagementPlanError';
}
}
export class WorkerCredentialManagementPlanConflictError extends Error {
readonly code = 'WORKER_CREDENTIAL_MANAGEMENT_PLAN_CONFLICT';
constructor(message: string) {
super(
`Worker credential management plan conflicts with durable state: ${message}`,
);
this.name = 'WorkerCredentialManagementPlanConflictError';
}
}
export class WorkerCredentialManagementPlanUnavailableError extends Error {
readonly code = 'WORKER_CREDENTIAL_MANAGEMENT_PLAN_UNAVAILABLE';
constructor(options?: ErrorOptions) {
super('Worker credential management plan is unavailable', options);
this.name = 'WorkerCredentialManagementPlanUnavailableError';
}
}
const ACTION_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,254}$/;
const PROJECT_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const WORKER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const GENERATION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
const SUBJECT_CONTROL_PATTERN = /[\u0000-\u001f\u007f]/;
const DIGEST_PATTERN = /^[0-9a-f]{64}$/;
const PLAN_DIGEST_DOMAIN =
'qinglong/worker-credential-management-plan-digest@v1\0';
const PREVIEW_DIGEST_DOMAIN =
'qinglong/worker-credential-management-preview-digest@v1\0';
const MAX_CREDENTIAL_LIFETIME_MS = 2 * 365 * 24 * 60 * 60 * 1000;
function invalid(message: string): never {
throw new InvalidWorkerCredentialManagementPlanError(message);
}
function record(value: unknown, label: string): Record<string, unknown> {
if (
!value ||
typeof value !== 'object' ||
Array.isArray(value) ||
(Object.getPrototypeOf(value) !== Object.prototype &&
Object.getPrototypeOf(value) !== null)
) {
return invalid(`${label} must be an object`);
}
const descriptors = Object.getOwnPropertyDescriptors(value);
if (
Object.values(descriptors).some(
(descriptor) =>
descriptor.get !== undefined ||
descriptor.set !== undefined ||
descriptor.enumerable !== true,
)
) {
return invalid(`${label} must contain enumerable data properties`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: object,
expected: readonly string[],
label: string,
): void {
const keys = Reflect.ownKeys(value);
const canonical = [...expected].sort();
if (
keys.some((key) => typeof key !== 'string') ||
keys.length !== canonical.length ||
keys
.map(String)
.sort()
.some((key, index) => key !== canonical[index])
) {
invalid(`${label} shape is invalid`);
}
}
function timestamp(value: unknown, label: string): number {
if (!Number.isSafeInteger(value) || (value as number) < 0) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function identifier(
value: unknown,
pattern: RegExp,
label: string,
): string {
if (typeof value !== 'string' || !pattern.test(value)) {
return invalid(`${label} is invalid`);
}
return value;
}
function credentialId(value: unknown, label: string): string {
try {
return normalizeWorkerCredentialId(value as string);
} catch {
return invalid(`${label} is invalid`);
}
}
function deliveryId(value: unknown): string {
try {
return normalizeWorkerCredentialMutationId(value as string);
} catch {
return invalid('deliveryId is invalid');
}
}
function userSubject(value: SecuritySubject): Readonly<SecuritySubject> {
const candidate = record(value, 'requestedBy');
exactKeys(candidate, ['id', 'type'], 'requestedBy');
if (
value.type !== 'user' ||
typeof value.id !== 'string' ||
value.id.length < 1 ||
Buffer.byteLength(value.id, 'utf8') > 255 ||
SUBJECT_CONTROL_PATTERN.test(value.id)
) {
return invalid('requestedBy must be a User subject');
}
return Object.freeze({ type: 'user', id: value.id });
}
function target(
value: WorkerCredentialManagementTarget,
action: WorkerCredentialManagementAction,
plannedAtMs: number,
): Readonly<WorkerCredentialManagementTarget> {
const candidate = record(value, 'target');
exactKeys(
candidate,
[
'credentialExpiresAtMs',
'credentialId',
'credentialNotBeforeAtMs',
'deliveryId',
'deploymentGeneration',
'deploymentTargetDigest',
'previousCredentialId',
'workerId',
],
'target',
);
const normalizedCredentialId = credentialId(
value.credentialId,
'credentialId',
);
const previousCredentialId =
value.previousCredentialId === null
? null
: credentialId(value.previousCredentialId, 'previousCredentialId');
const credentialNotBeforeAtMs = timestamp(
value.credentialNotBeforeAtMs,
'credentialNotBeforeAtMs',
);
const credentialExpiresAtMs = timestamp(
value.credentialExpiresAtMs,
'credentialExpiresAtMs',
);
if (
(action === 'issue' && previousCredentialId !== null) ||
(action === 'rotate' && previousCredentialId === null) ||
previousCredentialId === normalizedCredentialId
) {
return invalid('action and previousCredentialId do not agree');
}
if (
credentialNotBeforeAtMs < plannedAtMs ||
credentialExpiresAtMs <= credentialNotBeforeAtMs ||
credentialExpiresAtMs - credentialNotBeforeAtMs >
MAX_CREDENTIAL_LIFETIME_MS
) {
return invalid('credential lifetime is invalid');
}
if (
typeof value.deploymentTargetDigest !== 'string' ||
!DIGEST_PATTERN.test(value.deploymentTargetDigest)
) {
return invalid('deploymentTargetDigest is invalid');
}
return Object.freeze({
deliveryId: deliveryId(value.deliveryId),
workerId: identifier(value.workerId, WORKER_ID_PATTERN, 'workerId'),
credentialId: normalizedCredentialId,
previousCredentialId,
credentialNotBeforeAtMs,
credentialExpiresAtMs,
deploymentTargetDigest: value.deploymentTargetDigest,
deploymentGeneration: identifier(
value.deploymentGeneration,
GENERATION_PATTERN,
'deploymentGeneration',
),
});
}
type UnsignedPlan = Omit<
WorkerCredentialManagementPlan,
'planDigest' | 'previewDigest'
>;
function planFields(value: UnsignedPlan): object {
return {
schema: value.schema,
actionRef: value.actionRef,
authorityProjectId: value.authorityProjectId,
action: value.action,
target: value.target,
requestedBy: value.requestedBy,
plannedAtMs: value.plannedAtMs,
expiresAtMs: value.expiresAtMs,
};
}
function previewFields(value: UnsignedPlan): object {
return {
schema: value.schema,
authorityProjectId: value.authorityProjectId,
action: value.action,
target: value.target,
};
}
function digest(domain: string, value: object): string {
return createHash('sha256')
.update(domain)
.update(JSON.stringify(value))
.digest('hex');
}
export function workerCredentialManagementPlanDigest(
value: UnsignedPlan,
): string {
return digest(PLAN_DIGEST_DOMAIN, planFields(value));
}
export function workerCredentialManagementPreviewDigest(
value: UnsignedPlan,
): string {
return digest(PREVIEW_DIGEST_DOMAIN, previewFields(value));
}
function normalizeUnsigned(value: UnsignedPlan): UnsignedPlan {
if (value.schema !== WORKER_CREDENTIAL_MANAGEMENT_PLAN_SCHEMA) {
return invalid('schema is invalid');
}
if (!WORKER_CREDENTIAL_MANAGEMENT_ACTIONS.includes(value.action)) {
return invalid('action is invalid');
}
const plannedAtMs = timestamp(value.plannedAtMs, 'plannedAtMs');
const expiresAtMs = timestamp(value.expiresAtMs, 'expiresAtMs');
if (
expiresAtMs <= plannedAtMs ||
expiresAtMs - plannedAtMs >
MAX_WORKER_CREDENTIAL_MANAGEMENT_PLAN_LIFETIME_MS
) {
return invalid('plan lifetime is invalid');
}
return Object.freeze({
schema: WORKER_CREDENTIAL_MANAGEMENT_PLAN_SCHEMA,
actionRef: identifier(value.actionRef, ACTION_REF_PATTERN, 'actionRef'),
authorityProjectId: identifier(
value.authorityProjectId,
PROJECT_ID_PATTERN,
'authorityProjectId',
),
action: value.action,
target: target(value.target, value.action, plannedAtMs),
requestedBy: userSubject(value.requestedBy),
plannedAtMs,
expiresAtMs,
});
}
function boundedPlan(
value: Readonly<WorkerCredentialManagementPlan>,
): Readonly<WorkerCredentialManagementPlan> {
if (
Buffer.byteLength(JSON.stringify(value), 'utf8') >
MAX_WORKER_CREDENTIAL_MANAGEMENT_PLAN_BYTES
) {
return invalid('encoded plan exceeds the size limit');
}
return value;
}
export function createWorkerCredentialManagementPlan(
input: CreateWorkerCredentialManagementPlanInput,
): Readonly<WorkerCredentialManagementPlan> {
const value = record(input, 'plan input');
exactKeys(
value,
[
'action',
'actionRef',
'authorityProjectId',
'expiresAtMs',
'plannedAtMs',
'requestedBy',
'target',
],
'plan input',
);
const unsigned = normalizeUnsigned({
schema: WORKER_CREDENTIAL_MANAGEMENT_PLAN_SCHEMA,
actionRef: input.actionRef,
authorityProjectId: input.authorityProjectId,
action: input.action,
target: input.target,
requestedBy: input.requestedBy,
plannedAtMs: input.plannedAtMs,
expiresAtMs: input.expiresAtMs,
});
return boundedPlan(Object.freeze({
...unsigned,
planDigest: workerCredentialManagementPlanDigest(unsigned),
previewDigest: workerCredentialManagementPreviewDigest(unsigned),
}));
}
export function normalizeWorkerCredentialManagementPlan(
value: WorkerCredentialManagementPlan,
): Readonly<WorkerCredentialManagementPlan> {
const candidate = record(value, 'plan');
exactKeys(
candidate,
[
'action',
'actionRef',
'authorityProjectId',
'expiresAtMs',
'planDigest',
'plannedAtMs',
'previewDigest',
'requestedBy',
'schema',
'target',
],
'plan',
);
const unsigned = normalizeUnsigned({
schema: value.schema,
actionRef: value.actionRef,
authorityProjectId: value.authorityProjectId,
action: value.action,
target: value.target,
requestedBy: value.requestedBy,
plannedAtMs: value.plannedAtMs,
expiresAtMs: value.expiresAtMs,
});
if (
typeof value.planDigest !== 'string' ||
!DIGEST_PATTERN.test(value.planDigest) ||
value.planDigest !== workerCredentialManagementPlanDigest(unsigned) ||
typeof value.previewDigest !== 'string' ||
!DIGEST_PATTERN.test(value.previewDigest) ||
value.previewDigest !== workerCredentialManagementPreviewDigest(unsigned)
) {
return invalid('digest is invalid');
}
return boundedPlan(Object.freeze({
...unsigned,
planDigest: value.planDigest,
previewDigest: value.previewDigest,
}));
}
@@ -0,0 +1,60 @@
import { createHmac } from 'node:crypto';
import { normalizeWorkerCredentialId } from './workerCredential';
export const WORKER_CREDENTIAL_TOKEN_LIMITS = Object.freeze({
secretBytes: 32,
});
const CANONICAL_SECRET = /^[A-Za-z0-9_-]{43}$/;
const DOMAIN = Buffer.from('qinglong-worker-credential-v1\0', 'utf8');
function decode(name: string, value: string): Buffer {
if (typeof value !== 'string' || !CANONICAL_SECRET.test(value)) {
throw new TypeError(`${name} must be canonical base64url for 32 bytes`);
}
const result = Buffer.from(value, 'base64url');
if (result.byteLength !== 32 || result.toString('base64url') !== value) {
result.fill(0);
throw new TypeError(`${name} must be canonical base64url for 32 bytes`);
}
return result;
}
export function assertWorkerCredentialPepper(value: string): void {
const pepper = decode('Worker credential pepper', value);
pepper.fill(0);
}
export function workerCredentialSecretDigest(
pepperBase64Url: string,
credentialId: string,
secretBase64Url: string,
): string {
normalizeWorkerCredentialId(credentialId);
const pepper = decode('Worker credential pepper', pepperBase64Url);
const secret = decode('Worker credential secret', secretBase64Url);
let digest: Buffer | undefined;
try {
digest = createHmac('sha256', pepper)
.update(DOMAIN)
.update(credentialId, 'utf8')
.update('\0', 'utf8')
.update(secret)
.digest();
return digest.toString('hex');
} finally {
digest?.fill(0);
pepper.fill(0);
secret.fill(0);
}
}
export function formatWorkerCredentialToken(
credentialId: string,
secretBase64Url: string,
): string {
normalizeWorkerCredentialId(credentialId);
const secret = decode('Worker credential secret', secretBase64Url);
secret.fill(0);
return `ql3w_${credentialId}_${secretBase64Url}`;
}
@@ -0,0 +1,146 @@
export const WORKER_EXECUTION_ATTESTATION_STATES = [
'running',
'stopped',
] as const;
export type WorkerExecutionAttestationState =
(typeof WORKER_EXECUTION_ATTESTATION_STATES)[number];
export interface WorkerExecutionAttestationRecord {
readonly attestationId: string;
readonly runId: string;
readonly attemptId: string;
readonly sequence: number;
readonly state: WorkerExecutionAttestationState;
readonly workerId: string;
readonly workerSessionId: string;
readonly workerGeneration: number;
readonly leaseTokenDigest: string;
readonly leaseGeneration: number;
readonly leaseVersion: number;
readonly offerId: string;
readonly callbackSequence: number;
readonly executorHandle: string;
readonly journalRevision: number;
readonly receivedAtMs: number;
}
export interface SubmitWorkerExecutionAttestationCommand
extends Omit<WorkerExecutionAttestationRecord, 'receivedAtMs'> {}
export interface SubmitWorkerExecutionAttestationResult {
readonly status: 'created' | 'existing';
readonly attestation: Readonly<WorkerExecutionAttestationRecord>;
}
export interface WorkerExecutionAttestationRepository {
submit(
command: SubmitWorkerExecutionAttestationCommand,
): Promise<SubmitWorkerExecutionAttestationResult>;
findLatestExact(
target: Readonly<{
runId: string;
attemptId: string;
workerId: string;
workerSessionId: string;
workerGeneration: number;
leaseTokenDigest: string;
leaseGeneration: number;
leaseVersion: number;
offerId: string;
callbackSequence: number;
executorHandle: string;
}>,
): Promise<Readonly<WorkerExecutionAttestationRecord> | null>;
}
export class WorkerExecutionAttestationFenceRejectedError extends Error {
readonly code = 'WORKER_EXECUTION_ATTESTATION_FENCE_REJECTED';
constructor(readonly reason: string) {
super(`Worker execution attestation fence was rejected: ${reason}`);
this.name = 'WorkerExecutionAttestationFenceRejectedError';
}
}
export class WorkerExecutionAttestationUnavailableError extends Error {
readonly code = 'WORKER_EXECUTION_ATTESTATION_UNAVAILABLE';
constructor() {
super('Worker execution attestation storage is unavailable');
this.name = 'WorkerExecutionAttestationUnavailableError';
}
}
const UUID_V7 =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
function bounded(name: string, value: string, maximum: number): string {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > maximum ||
value.includes('\0')
) {
throw new TypeError(`${name} is invalid`);
}
return value;
}
function integer(name: string, value: number, minimum: number): number {
if (!Number.isSafeInteger(value) || value < minimum) {
throw new RangeError(`${name} is invalid`);
}
return value;
}
export function normalizeWorkerExecutionAttestation(
value: WorkerExecutionAttestationRecord,
): Readonly<WorkerExecutionAttestationRecord> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new TypeError('Worker execution attestation is invalid');
}
const expected = [
'attestationId', 'runId', 'attemptId', 'sequence', 'state', 'workerId',
'workerSessionId', 'workerGeneration', 'leaseTokenDigest',
'leaseGeneration', 'leaseVersion', 'offerId', 'callbackSequence',
'executorHandle', 'journalRevision', 'receivedAtMs',
].sort();
const actual = Object.keys(value).sort();
if (
actual.length !== expected.length ||
actual.some((key, index) => key !== expected[index])
) {
throw new TypeError('Worker execution attestation shape is invalid');
}
if (!UUID_V7.test(value.attestationId)) {
throw new TypeError('Worker execution attestation ID is invalid');
}
bounded('Worker execution attestation runId', value.runId, 64);
bounded('Worker execution attestation attemptId', value.attemptId, 64);
integer('Worker execution attestation sequence', value.sequence, 1);
if (!WORKER_EXECUTION_ATTESTATION_STATES.includes(value.state)) {
throw new TypeError('Worker execution attestation state is invalid');
}
if (!SAFE_ID.test(value.workerId) || !UUID_V7.test(value.workerSessionId)) {
throw new TypeError('Worker execution attestation Worker identity is invalid');
}
integer('Worker execution attestation Worker generation', value.workerGeneration, 1);
if (!/^[0-9a-f]{64}$/.test(value.leaseTokenDigest)) {
throw new TypeError('Worker execution attestation lease digest is invalid');
}
integer('Worker execution attestation lease generation', value.leaseGeneration, 1);
integer('Worker execution attestation lease version', value.leaseVersion, 0);
bounded('Worker execution attestation offerId', value.offerId, 128);
integer('Worker execution attestation callback sequence', value.callbackSequence, 0);
bounded('Worker execution attestation executor handle', value.executorHandle, 512);
integer('Worker execution attestation journal revision', value.journalRevision, 1);
integer('Worker execution attestation receivedAtMs', value.receivedAtMs, 0);
return Object.freeze({ ...value });
}
export function normalizeSubmitWorkerExecutionAttestationCommand(
command: SubmitWorkerExecutionAttestationCommand,
): Readonly<SubmitWorkerExecutionAttestationCommand> {
const { receivedAtMs: _receivedAtMs, ...normalized } =
normalizeWorkerExecutionAttestation({ ...command, receivedAtMs: 0 });
return Object.freeze(normalized);
}
@@ -0,0 +1,232 @@
import { createHash } from 'node:crypto';
export const WORKER_SESSION_STATUSES = ['online', 'draining', 'offline'] as const;
export type WorkerSessionStatus = (typeof WORKER_SESSION_STATUSES)[number];
export const MAX_WORKER_ID_LENGTH = 128;
export const MAX_WORKER_CAPABILITIES_BYTES = 16 * 1024;
export const MAX_WORKER_CONCURRENT_RUNS = 1024;
export const MAX_WORKER_SESSION_LEASE_MS = 10 * 60_000;
export const MIN_WORKER_SESSION_LEASE_MS = 5_000;
export const MAX_AVAILABLE_WORKER_PAGE_SIZE = 64;
const WORKER_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
const UUID_V7_PATTERN =
/^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const SHA256_PATTERN = /^[0-9a-f]{64}$/;
export interface WorkerSessionRecord {
readonly workerId: string;
readonly sessionId: string;
readonly generation: number;
readonly status: WorkerSessionStatus;
readonly version: number;
/** Canonical, bounded JSON. Parsing/placement belongs to the Worker domain. */
readonly capabilitiesJson: string;
readonly capabilitiesHash: string;
readonly maxConcurrentRuns: number;
readonly availableSlots: number;
readonly registeredAtMs: number;
readonly lastHeartbeatAtMs: number;
readonly leaseExpiresAtMs: number;
readonly updatedAtMs: number;
}
export class InvalidWorkerSessionValueError extends TypeError {
constructor(message: string) {
super(`Worker session value is invalid: ${message}`);
this.name = 'InvalidWorkerSessionValueError';
}
}
export type WorkerSessionFenceReason =
| 'missing'
| 'session_mismatch'
| 'generation_mismatch'
| 'version_mismatch'
| 'lease_expired'
| 'offline';
export class WorkerSessionFenceRejectedError extends Error {
constructor(
readonly workerId: string,
readonly reason: WorkerSessionFenceReason,
) {
super(`Worker session ${workerId} was fenced: ${reason}`);
this.name = 'WorkerSessionFenceRejectedError';
}
}
export class WorkerSessionConflictError extends Error {
constructor(readonly workerId: string) {
super(`Worker session ${workerId} replay conflicts with persisted data`);
this.name = 'WorkerSessionConflictError';
}
}
function invalid(message: string): never {
throw new InvalidWorkerSessionValueError(message);
}
function safeInteger(name: string, value: number, minimum = 0): void {
if (!Number.isSafeInteger(value) || value < minimum) {
invalid(`${name} must be a safe integer greater than or equal to ${minimum}`);
}
}
export function assertWorkerId(value: string): void {
if (
typeof value !== 'string' ||
value.length < 1 ||
value.length > MAX_WORKER_ID_LENGTH ||
!WORKER_ID_PATTERN.test(value)
) {
invalid('workerId is invalid');
}
}
export function assertWorkerSessionId(value: string): void {
if (typeof value !== 'string' || !UUID_V7_PATTERN.test(value)) {
invalid('sessionId must be a lowercase UUIDv7');
}
}
export function assertWorkerSessionLeaseDuration(value: number): void {
if (
!Number.isSafeInteger(value) ||
value < MIN_WORKER_SESSION_LEASE_MS ||
value > MAX_WORKER_SESSION_LEASE_MS
) {
invalid(
`leaseDurationMs must be between ${MIN_WORKER_SESSION_LEASE_MS} and ${MAX_WORKER_SESSION_LEASE_MS}`,
);
}
}
export function assertWorkerConcurrency(
maxConcurrentRuns: number,
availableSlots: number,
): void {
if (
!Number.isSafeInteger(maxConcurrentRuns) ||
maxConcurrentRuns < 1 ||
maxConcurrentRuns > MAX_WORKER_CONCURRENT_RUNS ||
!Number.isSafeInteger(availableSlots) ||
availableSlots < 0 ||
availableSlots > maxConcurrentRuns
) {
invalid('Worker concurrency is invalid');
}
}
export function assertWorkerCapabilitiesSnapshot(
capabilitiesJson: string,
capabilitiesHash: string,
): void {
if (
typeof capabilitiesJson !== 'string' ||
Buffer.byteLength(capabilitiesJson, 'utf8') < 2 ||
Buffer.byteLength(capabilitiesJson, 'utf8') >
MAX_WORKER_CAPABILITIES_BYTES ||
!SHA256_PATTERN.test(capabilitiesHash)
) {
invalid('Worker capabilities snapshot is invalid');
}
let parsed: unknown;
try {
parsed = JSON.parse(capabilitiesJson);
} catch {
return invalid('Worker capabilities snapshot is not JSON');
}
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
invalid('Worker capabilities snapshot must be an object');
}
if (
createHash('sha256').update(capabilitiesJson, 'utf8').digest('hex') !==
capabilitiesHash
) {
invalid('Worker capabilities hash does not match its canonical JSON');
}
}
export function assertWorkerSessionRecord(value: WorkerSessionRecord): void {
assertWorkerId(value.workerId);
assertWorkerSessionId(value.sessionId);
safeInteger('generation', value.generation, 1);
safeInteger('version', value.version);
if (!WORKER_SESSION_STATUSES.includes(value.status)) invalid('status is invalid');
assertWorkerCapabilitiesSnapshot(
value.capabilitiesJson,
value.capabilitiesHash,
);
assertWorkerConcurrency(value.maxConcurrentRuns, value.availableSlots);
for (const [name, timestamp] of [
['registeredAtMs', value.registeredAtMs],
['lastHeartbeatAtMs', value.lastHeartbeatAtMs],
['leaseExpiresAtMs', value.leaseExpiresAtMs],
['updatedAtMs', value.updatedAtMs],
] as const) {
safeInteger(name, timestamp);
}
if (
value.lastHeartbeatAtMs < value.registeredAtMs ||
(value.status !== 'offline' &&
value.leaseExpiresAtMs <= value.lastHeartbeatAtMs) ||
value.updatedAtMs < value.lastHeartbeatAtMs ||
(value.status !== 'online' && value.availableSlots !== 0)
) {
invalid('timestamps or status capacity are inconsistent');
}
}
export interface RegisterWorkerSessionCommand {
readonly workerId: string;
readonly sessionId: string;
readonly capabilitiesJson: string;
readonly capabilitiesHash: string;
readonly maxConcurrentRuns: number;
readonly availableSlots: number;
readonly leaseDurationMs: number;
}
export interface HeartbeatWorkerSessionCommand {
readonly workerId: string;
readonly sessionId: string;
readonly generation: number;
readonly expectedVersion: number;
readonly availableSlots: number;
readonly leaseDurationMs: number;
}
export interface TransitionWorkerSessionCommand {
readonly workerId: string;
readonly sessionId: string;
readonly generation: number;
readonly expectedVersion: number;
readonly status: 'draining' | 'offline';
}
export interface RegisterWorkerSessionResult {
readonly worker: WorkerSessionRecord;
readonly replacedSession: boolean;
}
export interface AvailableWorkerSessionPage {
readonly observedAtMs: number;
readonly workers: readonly WorkerSessionRecord[];
readonly truncated: boolean;
readonly nextCursor?: string;
}
export interface WorkerSessionRepository {
findById(workerId: string): Promise<WorkerSessionRecord | null>;
register(
command: RegisterWorkerSessionCommand,
): Promise<RegisterWorkerSessionResult>;
heartbeat(command: HeartbeatWorkerSessionCommand): Promise<WorkerSessionRecord>;
transition(command: TransitionWorkerSessionCommand): Promise<WorkerSessionRecord>;
listAvailable(options?: Readonly<{
afterWorkerId?: string;
limit?: number;
}>): Promise<AvailableWorkerSessionPage>;
}
@@ -0,0 +1,417 @@
import {
assertWorkerCapabilitiesSnapshot,
assertWorkerConcurrency,
assertWorkerId,
assertWorkerSessionId,
assertWorkerSessionLeaseDuration,
assertWorkerSessionRecord,
MAX_WORKER_CONCURRENT_RUNS,
type HeartbeatWorkerSessionCommand,
type RegisterWorkerSessionCommand,
type RegisterWorkerSessionResult,
type TransitionWorkerSessionCommand,
type WorkerSessionRecord,
type WorkerSessionStatus,
} from './workerSession';
export const WORKER_SESSION_REGISTER_SCHEMA =
'qinglong/worker-session-register@v1';
export const WORKER_SESSION_HEARTBEAT_SCHEMA =
'qinglong/worker-session-heartbeat@v1';
export const WORKER_SESSION_TRANSITION_SCHEMA =
'qinglong/worker-session-transition@v1';
export const MAX_WORKER_SESSION_REGISTER_REQUEST_BYTES = 20 * 1024;
export const MAX_WORKER_SESSION_REQUEST_BYTES = 4 * 1024;
export const MAX_WORKER_SESSION_RESPONSE_BYTES = 4 * 1024;
export interface WorkerSessionPathAuthority {
readonly workerId: string;
readonly sessionId: string;
}
export type WorkerSessionRegisterRequestBody = Readonly<{
readonly schema: typeof WORKER_SESSION_REGISTER_SCHEMA;
readonly capabilitiesJson: string;
readonly capabilitiesHash: string;
readonly maxConcurrentRuns: number;
readonly availableSlots: number;
readonly leaseDurationMs: number;
}>;
export type WorkerSessionHeartbeatRequestBody = Readonly<{
readonly schema: typeof WORKER_SESSION_HEARTBEAT_SCHEMA;
readonly generation: number;
readonly expectedVersion: number;
readonly availableSlots: number;
readonly leaseDurationMs: number;
}>;
export type WorkerSessionTransitionRequestBody = Readonly<{
readonly schema: typeof WORKER_SESSION_TRANSITION_SCHEMA;
readonly generation: number;
readonly expectedVersion: number;
readonly status: 'draining' | 'offline';
}>;
export interface WorkerSessionWireProjection {
readonly workerId: string;
readonly sessionId: string;
readonly generation: number;
readonly version: number;
readonly status: WorkerSessionStatus;
readonly leaseExpiresAtMs: number;
}
export type WorkerSessionRegisterResponseBody = Readonly<
WorkerSessionWireProjection & {
readonly schema: typeof WORKER_SESSION_REGISTER_SCHEMA;
readonly replacedSession: boolean;
}
>;
export type WorkerSessionHeartbeatResponseBody = Readonly<
WorkerSessionWireProjection & {
readonly schema: typeof WORKER_SESSION_HEARTBEAT_SCHEMA;
}
>;
export type WorkerSessionTransitionResponseBody = Readonly<
WorkerSessionWireProjection & {
readonly schema: typeof WORKER_SESSION_TRANSITION_SCHEMA;
}
>;
export class InvalidWorkerSessionTransportError extends TypeError {
readonly code = 'WORKER_SESSION_TRANSPORT_INVALID';
constructor(message: string) {
super(`Worker Session transport is invalid: ${message}`);
this.name = 'InvalidWorkerSessionTransportError';
}
}
function invalid(message: string): never {
throw new InvalidWorkerSessionTransportError(message);
}
function object(value: unknown, label: string): Record<string, unknown> {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return invalid(`${label} is not an object`);
}
return value as Record<string, unknown>;
}
function exactKeys(
value: Record<string, unknown>,
expected: readonly string[],
label: string,
): void {
const actual = Object.keys(value).sort();
const sorted = [...expected].sort();
if (
actual.length !== sorted.length ||
actual.some((key, index) => key !== sorted[index])
) invalid(`${label} shape is invalid`);
}
function integer(
value: unknown,
label: string,
minimum = 0,
maximum = Number.MAX_SAFE_INTEGER,
): number {
if (
!Number.isSafeInteger(value) ||
(value as number) < minimum ||
(value as number) > maximum
) {
return invalid(`${label} is invalid`);
}
return value as number;
}
function authority(value: WorkerSessionPathAuthority): WorkerSessionPathAuthority {
try {
assertWorkerId(value?.workerId);
assertWorkerSessionId(value?.sessionId);
} catch {
return invalid('path authority is invalid');
}
return Object.freeze({
workerId: value.workerId,
sessionId: value.sessionId,
});
}
function validateRegister(
command: RegisterWorkerSessionCommand,
): Readonly<RegisterWorkerSessionCommand> {
try {
assertWorkerId(command?.workerId);
assertWorkerSessionId(command?.sessionId);
assertWorkerCapabilitiesSnapshot(
command?.capabilitiesJson,
command?.capabilitiesHash,
);
assertWorkerConcurrency(
command?.maxConcurrentRuns,
command?.availableSlots,
);
assertWorkerSessionLeaseDuration(command?.leaseDurationMs);
} catch {
return invalid('register command is invalid');
}
return Object.freeze({ ...command });
}
function validateHeartbeat(
command: HeartbeatWorkerSessionCommand,
): Readonly<HeartbeatWorkerSessionCommand> {
try {
assertWorkerId(command?.workerId);
assertWorkerSessionId(command?.sessionId);
assertWorkerSessionLeaseDuration(command?.leaseDurationMs);
} catch {
return invalid('heartbeat command is invalid');
}
integer(command.generation, 'generation', 1);
integer(command.expectedVersion, 'expectedVersion');
integer(
command.availableSlots,
'availableSlots',
0,
MAX_WORKER_CONCURRENT_RUNS,
);
return Object.freeze({ ...command });
}
function validateTransition(
command: TransitionWorkerSessionCommand,
): Readonly<TransitionWorkerSessionCommand> {
try {
assertWorkerId(command?.workerId);
assertWorkerSessionId(command?.sessionId);
} catch {
return invalid('transition command is invalid');
}
integer(command.generation, 'generation', 1);
integer(command.expectedVersion, 'expectedVersion');
if (command.status !== 'draining' && command.status !== 'offline') {
return invalid('transition status is invalid');
}
return Object.freeze({ ...command });
}
export function createWorkerSessionRegisterRequestBody(
value: RegisterWorkerSessionCommand,
): WorkerSessionRegisterRequestBody {
const command = validateRegister(value);
return Object.freeze({
schema: WORKER_SESSION_REGISTER_SCHEMA,
capabilitiesJson: command.capabilitiesJson,
capabilitiesHash: command.capabilitiesHash,
maxConcurrentRuns: command.maxConcurrentRuns,
availableSlots: command.availableSlots,
leaseDurationMs: command.leaseDurationMs,
});
}
export function parseWorkerSessionRegisterRequestBody(
value: unknown,
path: WorkerSessionPathAuthority,
): Readonly<RegisterWorkerSessionCommand> {
const body = object(value, 'register request');
exactKeys(body, [
'schema', 'capabilitiesJson', 'capabilitiesHash', 'maxConcurrentRuns',
'availableSlots', 'leaseDurationMs',
], 'register request');
if (body.schema !== WORKER_SESSION_REGISTER_SCHEMA) {
return invalid('register schema is invalid');
}
const resolved = authority(path);
return validateRegister({
...resolved,
capabilitiesJson: body.capabilitiesJson as string,
capabilitiesHash: body.capabilitiesHash as string,
maxConcurrentRuns: body.maxConcurrentRuns as number,
availableSlots: body.availableSlots as number,
leaseDurationMs: body.leaseDurationMs as number,
});
}
export function createWorkerSessionHeartbeatRequestBody(
value: HeartbeatWorkerSessionCommand,
): WorkerSessionHeartbeatRequestBody {
const command = validateHeartbeat(value);
return Object.freeze({
schema: WORKER_SESSION_HEARTBEAT_SCHEMA,
generation: command.generation,
expectedVersion: command.expectedVersion,
availableSlots: command.availableSlots,
leaseDurationMs: command.leaseDurationMs,
});
}
export function parseWorkerSessionHeartbeatRequestBody(
value: unknown,
path: WorkerSessionPathAuthority,
): Readonly<HeartbeatWorkerSessionCommand> {
const body = object(value, 'heartbeat request');
exactKeys(body, [
'schema', 'generation', 'expectedVersion', 'availableSlots',
'leaseDurationMs',
], 'heartbeat request');
if (body.schema !== WORKER_SESSION_HEARTBEAT_SCHEMA) {
return invalid('heartbeat schema is invalid');
}
return validateHeartbeat({
...authority(path),
generation: body.generation as number,
expectedVersion: body.expectedVersion as number,
availableSlots: body.availableSlots as number,
leaseDurationMs: body.leaseDurationMs as number,
});
}
export function createWorkerSessionTransitionRequestBody(
value: TransitionWorkerSessionCommand,
): WorkerSessionTransitionRequestBody {
const command = validateTransition(value);
return Object.freeze({
schema: WORKER_SESSION_TRANSITION_SCHEMA,
generation: command.generation,
expectedVersion: command.expectedVersion,
status: command.status,
});
}
export function parseWorkerSessionTransitionRequestBody(
value: unknown,
path: WorkerSessionPathAuthority,
): Readonly<TransitionWorkerSessionCommand> {
const body = object(value, 'transition request');
exactKeys(body, [
'schema', 'generation', 'expectedVersion', 'status',
], 'transition request');
if (body.schema !== WORKER_SESSION_TRANSITION_SCHEMA) {
return invalid('transition schema is invalid');
}
return validateTransition({
...authority(path),
generation: body.generation as number,
expectedVersion: body.expectedVersion as number,
status: body.status as 'draining' | 'offline',
});
}
function projection(record: WorkerSessionRecord): WorkerSessionWireProjection {
try {
assertWorkerSessionRecord(record);
} catch {
return invalid('Session record is invalid');
}
return Object.freeze({
workerId: record.workerId,
sessionId: record.sessionId,
generation: record.generation,
version: record.version,
status: record.status,
leaseExpiresAtMs: record.leaseExpiresAtMs,
});
}
export function createWorkerSessionRegisterResponseBody(
value: RegisterWorkerSessionResult,
): WorkerSessionRegisterResponseBody {
if (typeof value?.replacedSession !== 'boolean') {
return invalid('register result is invalid');
}
return Object.freeze({
schema: WORKER_SESSION_REGISTER_SCHEMA,
...projection(value.worker),
replacedSession: value.replacedSession,
});
}
export function createWorkerSessionHeartbeatResponseBody(
value: WorkerSessionRecord,
): WorkerSessionHeartbeatResponseBody {
return Object.freeze({
schema: WORKER_SESSION_HEARTBEAT_SCHEMA,
...projection(value),
});
}
export function createWorkerSessionTransitionResponseBody(
value: WorkerSessionRecord,
): WorkerSessionTransitionResponseBody {
return Object.freeze({
schema: WORKER_SESSION_TRANSITION_SCHEMA,
...projection(value),
});
}
function parseProjection(
value: unknown,
schema: string,
includeReplacement: boolean,
): WorkerSessionWireProjection & { readonly replacedSession?: boolean } {
const body = object(value, 'Session response');
exactKeys(body, [
'schema', 'workerId', 'sessionId', 'generation', 'version', 'status',
'leaseExpiresAtMs', ...(includeReplacement ? ['replacedSession'] : []),
], 'Session response');
if (body.schema !== schema) return invalid('response schema is invalid');
try {
assertWorkerId(body.workerId as string);
assertWorkerSessionId(body.sessionId as string);
} catch {
return invalid('response authority is invalid');
}
if (!['online', 'draining', 'offline'].includes(body.status as string)) {
return invalid('response status is invalid');
}
if (includeReplacement && typeof body.replacedSession !== 'boolean') {
return invalid('replacement fact is invalid');
}
return Object.freeze({
workerId: body.workerId as string,
sessionId: body.sessionId as string,
generation: integer(body.generation, 'generation', 1),
version: integer(body.version, 'version'),
status: body.status as WorkerSessionStatus,
leaseExpiresAtMs: integer(body.leaseExpiresAtMs, 'leaseExpiresAtMs'),
...(includeReplacement
? { replacedSession: body.replacedSession as boolean }
: {}),
});
}
export function parseWorkerSessionRegisterResponseBody(
value: unknown,
): WorkerSessionRegisterResponseBody {
const result = parseProjection(value, WORKER_SESSION_REGISTER_SCHEMA, true);
return Object.freeze({
schema: WORKER_SESSION_REGISTER_SCHEMA,
...result,
replacedSession: result.replacedSession!,
});
}
export function parseWorkerSessionHeartbeatResponseBody(
value: unknown,
): WorkerSessionHeartbeatResponseBody {
return Object.freeze({
schema: WORKER_SESSION_HEARTBEAT_SCHEMA,
...parseProjection(value, WORKER_SESSION_HEARTBEAT_SCHEMA, false),
});
}
export function parseWorkerSessionTransitionResponseBody(
value: unknown,
): WorkerSessionTransitionResponseBody {
return Object.freeze({
schema: WORKER_SESSION_TRANSITION_SCHEMA,
...parseProjection(value, WORKER_SESSION_TRANSITION_SCHEMA, false),
});
}