import { createHash, timingSafeEqual } from 'node:crypto'; import { normalizeApiCredentialRecord, type ApiCredentialRecord, } from '../security/identity-credential/apiCredential'; import { normalizeIdentitySubjectRecord, type IdentitySubjectRecord, } from '../security/identity-credential/identityAdministration'; import { assertProjectPolicyProjectId, normalizeProjectRoleBinding, type ProjectRoleBindingRecord, } from '../security/project-policy/projectPolicy'; import { normalizeSecurityPrincipal, type SecurityPrincipal } from '../security/security'; import { normalizeSecurityAuditRecord, type SecurityAuditRecord, } from '../security/audit/securityAudit'; export const LOCAL_OWNER_BOOTSTRAP_TOKEN_BYTES = 32; export const LOCAL_OWNER_BOOTSTRAP_CHALLENGE_ID_BYTES = 16; export const LOCAL_OWNER_BOOTSTRAP_DEFAULT_TTL_MS = 10 * 60 * 1000; export const LOCAL_OWNER_BOOTSTRAP_MIN_TTL_MS = 60 * 1000; export const LOCAL_OWNER_BOOTSTRAP_MAX_TTL_MS = 30 * 60 * 1000; export const LOCAL_OWNER_BOOTSTRAP_SYSTEM_SUBJECT = Object.freeze({ type: 'system' as const, id: 'owner-bootstrap', }); const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/; const CHALLENGE_ID_PATTERN = /^[A-Za-z0-9_-]{22}$/; const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/; const DIGEST_PATTERN = /^[0-9a-f]{64}$/; const DIGEST_DOMAIN = 'qinglong3-local-owner-bootstrap-v1\0'; const MAX_VERSION = 2_147_483_647; export interface LocalIdentityProvisioningRecord { readonly mutationId: string; readonly requestId: string; readonly identity: IdentitySubjectRecord; readonly credential: ApiCredentialRecord; readonly issuer: SecurityPrincipal; readonly audit: SecurityAuditRecord; readonly createdAtMs: number; } export interface ProvisionLocalIdentityCommand extends LocalIdentityProvisioningRecord {} export interface ProvisionLocalIdentityResult { readonly status: 'inserted' | 'existing'; readonly provisioning: Readonly; } export interface LocalOwnerBootstrapChallengeRecord { readonly projectId: string; readonly version: number; readonly issueMutationId: string; readonly issueRequestId: string; readonly challengeId: string; readonly tokenDigest: string; readonly issuer: SecurityPrincipal; readonly issuedAtMs: number; readonly expiresAtMs: number; readonly issueAudit: SecurityAuditRecord; readonly consumedAtMs?: number; readonly claimMutationId?: string; readonly claimRequestId?: string; readonly claimedPrincipal?: SecurityPrincipal; readonly credentialId?: string; readonly credentialVersion?: number; readonly binding?: ProjectRoleBindingRecord; readonly claimAudit?: SecurityAuditRecord; } export interface IssueLocalOwnerBootstrapChallengeCommand { readonly projectId: string; readonly mutationId: string; readonly requestId: string; readonly challengeId: string; readonly tokenDigest: string; readonly issuer: SecurityPrincipal; readonly issuedAtMs: number; readonly expiresAtMs: number; readonly audit: SecurityAuditRecord; } export interface IssueLocalOwnerBootstrapChallengeResult { readonly status: 'inserted' | 'existing'; readonly challenge: Readonly; } export interface ClaimLocalOwnerCommand { readonly projectId: string; readonly mutationId: string; readonly requestId: string; readonly challengeId: string; readonly tokenDigest: string; readonly principal: SecurityPrincipal; readonly credentialId: string; readonly credentialVersion: number; readonly claimedAtMs: number; readonly audit: SecurityAuditRecord; } export interface ClaimLocalOwnerResult { readonly status: 'inserted' | 'existing'; readonly challenge: Readonly; readonly binding: Readonly; } export interface LocalOwnerCredentialDeliveryAcknowledgementRecord { readonly kind: 'credential'; readonly mutationId: string; readonly requestId: string; readonly subjectId: string; readonly credentialId: string; readonly factDigest: string; readonly ttlMs: number; readonly deliveryDigest: string; readonly acknowledgedAtMs: number; } export interface LocalOwnerChallengeDeliveryAcknowledgementRecord { readonly kind: 'challenge'; readonly projectId: string; readonly mutationId: string; readonly requestId: string; readonly challengeId: string; readonly factDigest: string; readonly ttlMs: number; readonly deliveryDigest: string; readonly acknowledgedAtMs: number; } export type LocalOwnerSecretDeliveryAcknowledgementRecord = | LocalOwnerCredentialDeliveryAcknowledgementRecord | LocalOwnerChallengeDeliveryAcknowledgementRecord; export interface RecordLocalOwnerSecretDeliveryAcknowledgementResult { readonly status: 'inserted' | 'existing'; readonly acknowledgement: Readonly; } export interface LocalOwnerBootstrapRepository { resolveProjectVersion(projectId: string): Promise; resolveProvisioning( mutationId: string, ): Promise | null>; resolveIssuedChallenge( mutationId: string, ): Promise | null>; resolveDeliveryAcknowledgement( mutationId: string, ): Promise | null>; recordDeliveryAcknowledgement( acknowledgement: LocalOwnerSecretDeliveryAcknowledgementRecord, ): Promise; provision( command: ProvisionLocalIdentityCommand, ): Promise; issue( command: IssueLocalOwnerBootstrapChallengeCommand, ): Promise; claim(command: ClaimLocalOwnerCommand): Promise; recordAudit(audit: SecurityAuditRecord): Promise; } export class InvalidLocalOwnerBootstrapValueError extends TypeError { constructor(message: string) { super(`Local owner bootstrap value is invalid: ${message}`); this.name = 'InvalidLocalOwnerBootstrapValueError'; } } export class LocalOwnerBootstrapNotPristineError extends Error { readonly code = 'LOCAL_OWNER_BOOTSTRAP_NOT_PRISTINE'; constructor() { super('Local owner bootstrap is no longer available'); this.name = 'LocalOwnerBootstrapNotPristineError'; } } export class LocalOwnerBootstrapChallengeActiveError extends Error { readonly code = 'LOCAL_OWNER_BOOTSTRAP_CHALLENGE_ACTIVE'; constructor() { super('A local owner bootstrap challenge is already active'); this.name = 'LocalOwnerBootstrapChallengeActiveError'; } } export class LocalOwnerBootstrapIdentityRequiredError extends Error { readonly code = 'LOCAL_OWNER_BOOTSTRAP_IDENTITY_REQUIRED'; constructor() { super('A provisioned local identity is required'); this.name = 'LocalOwnerBootstrapIdentityRequiredError'; } } export class LocalOwnerBootstrapClaimRejectedError extends Error { readonly code = 'LOCAL_OWNER_BOOTSTRAP_CLAIM_REJECTED'; constructor() { super('Local owner bootstrap claim was rejected'); this.name = 'LocalOwnerBootstrapClaimRejectedError'; } } export class LocalOwnerBootstrapMutationConflictError extends Error { readonly code = 'LOCAL_OWNER_BOOTSTRAP_MUTATION_CONFLICT'; constructor() { super('Local owner bootstrap mutation conflicts with previous use'); this.name = 'LocalOwnerBootstrapMutationConflictError'; } } export class LocalOwnerBootstrapUnavailableError extends Error { readonly code = 'LOCAL_OWNER_BOOTSTRAP_UNAVAILABLE'; constructor() { super('Local owner bootstrap is unavailable'); this.name = 'LocalOwnerBootstrapUnavailableError'; } } function exactKeys( value: object, expected: readonly string[], name: string, ): void { const actual = Object.keys(value).sort(); const canonical = [...expected].sort(); if ( actual.length !== canonical.length || actual.some((key, index) => key !== canonical[index]) ) { throw new InvalidLocalOwnerBootstrapValueError(`${name} shape is invalid`); } } function timestamp(value: number, name: string): number { if (!Number.isSafeInteger(value) || value < 0) { throw new InvalidLocalOwnerBootstrapValueError(`${name} is invalid`); } return value; } export function assertLocalOwnerBootstrapMutationId(value: string): void { if (typeof value !== 'string' || !UUID_V4_PATTERN.test(value)) { throw new InvalidLocalOwnerBootstrapValueError('mutationId is invalid'); } } export function assertLocalOwnerBootstrapRequestId(value: string): void { if (typeof value !== 'string' || !REQUEST_ID_PATTERN.test(value)) { throw new InvalidLocalOwnerBootstrapValueError('requestId is invalid'); } } export function assertLocalOwnerBootstrapChallengeId(value: string): void { if (typeof value !== 'string' || !CHALLENGE_ID_PATTERN.test(value)) { throw new InvalidLocalOwnerBootstrapValueError('challengeId is invalid'); } } export function assertLocalOwnerBootstrapToken(value: string): void { if (typeof value !== 'string' || !TOKEN_PATTERN.test(value)) { throw new InvalidLocalOwnerBootstrapValueError('token is invalid'); } } export function assertLocalOwnerBootstrapTokenDigest(value: string): void { if (typeof value !== 'string' || !DIGEST_PATTERN.test(value)) { throw new InvalidLocalOwnerBootstrapValueError('tokenDigest is invalid'); } } export function assertLocalOwnerBootstrapTtl(value: number): void { if ( !Number.isSafeInteger(value) || value < LOCAL_OWNER_BOOTSTRAP_MIN_TTL_MS || value > LOCAL_OWNER_BOOTSTRAP_MAX_TTL_MS ) { throw new InvalidLocalOwnerBootstrapValueError('ttlMs is invalid'); } } export function normalizeLocalOwnerSecretDeliveryAcknowledgementRecord( value: LocalOwnerSecretDeliveryAcknowledgementRecord, ): Readonly { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new InvalidLocalOwnerBootstrapValueError( 'delivery acknowledgement must be an object', ); } assertLocalOwnerBootstrapMutationId(value.mutationId); assertLocalOwnerBootstrapRequestId(value.requestId); assertLocalOwnerBootstrapTokenDigest(value.factDigest); assertLocalOwnerBootstrapTokenDigest(value.deliveryDigest); if ( !Number.isSafeInteger(value.ttlMs) || value.ttlMs < 1 || !Number.isSafeInteger(value.acknowledgedAtMs) || value.acknowledgedAtMs < 0 ) { throw new InvalidLocalOwnerBootstrapValueError( 'delivery acknowledgement lifetime is invalid', ); } if (value.kind === 'credential') { exactKeys( value, [ 'kind', 'mutationId', 'requestId', 'subjectId', 'credentialId', 'factDigest', 'ttlMs', 'deliveryDigest', 'acknowledgedAtMs', ], 'credential delivery acknowledgement', ); if ( !/^usr_[A-Za-z0-9_-]{22}$/.test(value.subjectId) || !/^own_[A-Za-z0-9_-]{22}$/.test(value.credentialId) ) { throw new InvalidLocalOwnerBootstrapValueError( 'credential delivery acknowledgement identity is invalid', ); } return Object.freeze({ ...value }); } if (value.kind !== 'challenge') { throw new InvalidLocalOwnerBootstrapValueError( 'delivery acknowledgement kind is invalid', ); } exactKeys( value, [ 'kind', 'projectId', 'mutationId', 'requestId', 'challengeId', 'factDigest', 'ttlMs', 'deliveryDigest', 'acknowledgedAtMs', ], 'challenge delivery acknowledgement', ); assertProjectPolicyProjectId(value.projectId); assertLocalOwnerBootstrapChallengeId(value.challengeId); return Object.freeze({ ...value }); } export function localOwnerSecretDeliveryAcknowledgementSemanticDigest( value: LocalOwnerSecretDeliveryAcknowledgementRecord, ): string { const record = normalizeLocalOwnerSecretDeliveryAcknowledgementRecord(value); const hash = createHash('sha256') .update('qinglong.local-owner-delivery-acknowledgement.v1\0', 'utf8') .update(record.kind, 'utf8') .update('\0', 'utf8') .update(record.mutationId, 'utf8') .update('\0', 'utf8') .update(record.requestId, 'utf8') .update('\0', 'utf8'); if (record.kind === 'credential') { hash .update(record.subjectId, 'utf8') .update('\0', 'utf8') .update(record.credentialId, 'utf8'); } else { hash .update(record.projectId, 'utf8') .update('\0', 'utf8') .update(record.challengeId, 'utf8'); } return hash .update('\0', 'utf8') .update(record.factDigest, 'utf8') .update('\0', 'utf8') .update(record.deliveryDigest, 'utf8') .update('\0', 'utf8') .update(String(record.ttlMs), 'utf8') .update('\0', 'utf8') .update(String(record.acknowledgedAtMs), 'utf8') .digest('hex'); } export function localOwnerBootstrapTokenDigest( projectId: string, challengeId: string, token: string, ): string { if (typeof projectId !== 'string' || !REQUEST_ID_PATTERN.test(projectId)) { throw new InvalidLocalOwnerBootstrapValueError('projectId is invalid'); } assertLocalOwnerBootstrapChallengeId(challengeId); assertLocalOwnerBootstrapToken(token); return createHash('sha256') .update(DIGEST_DOMAIN, 'utf8') .update(projectId, 'utf8') .update('\0', 'utf8') .update(challengeId, 'utf8') .update('\0', 'utf8') .update(token, 'utf8') .digest('hex'); } export function localOwnerBootstrapDigestMatches( expected: string, actual: string, ): boolean { assertLocalOwnerBootstrapTokenDigest(expected); assertLocalOwnerBootstrapTokenDigest(actual); const left = Buffer.from(expected, 'hex'); const right = Buffer.from(actual, 'hex'); try { return timingSafeEqual(left, right); } finally { left.fill(0); right.fill(0); } } function localConsolePrincipal( value: SecurityPrincipal, nowMs: number, ): Readonly { let principal: Readonly; try { principal = normalizeSecurityPrincipal(value, nowMs); } catch { throw new InvalidLocalOwnerBootstrapValueError('issuer is invalid'); } if ( principal.subject.type !== LOCAL_OWNER_BOOTSTRAP_SYSTEM_SUBJECT.type || principal.subject.id !== LOCAL_OWNER_BOOTSTRAP_SYSTEM_SUBJECT.id || principal.assurance !== 'local_console' ) { throw new InvalidLocalOwnerBootstrapValueError('issuer is invalid'); } return principal; } function userPrincipal( value: SecurityPrincipal, nowMs: number, ): Readonly { let principal: Readonly; try { principal = normalizeSecurityPrincipal(value, nowMs); } catch { throw new InvalidLocalOwnerBootstrapValueError('principal is invalid'); } if (principal.subject.type !== 'user') { throw new InvalidLocalOwnerBootstrapValueError('principal is invalid'); } return principal; } export function normalizeLocalIdentityProvisioningRecord( value: LocalIdentityProvisioningRecord, ): Readonly { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new InvalidLocalOwnerBootstrapValueError( 'provisioning must be an object', ); } exactKeys( value, [ 'mutationId', 'requestId', 'identity', 'credential', 'issuer', 'audit', 'createdAtMs', ], 'provisioning', ); assertLocalOwnerBootstrapMutationId(value.mutationId); assertLocalOwnerBootstrapRequestId(value.requestId); const createdAtMs = timestamp(value.createdAtMs, 'createdAtMs'); const identity = normalizeIdentitySubjectRecord(value.identity); const credential = normalizeApiCredentialRecord(value.credential); const issuer = localConsolePrincipal(value.issuer, createdAtMs); const audit = normalizeSecurityAuditRecord(value.audit); if ( identity.subject.type !== 'user' || identity.status !== 'active' || identity.version !== 1 || identity.createdAtMs !== createdAtMs || identity.updatedAtMs !== createdAtMs || credential.subject.type !== identity.subject.type || credential.subject.id !== identity.subject.id || credential.subjectStatus !== 'active' || credential.version !== 1 || credential.state !== 'active' || credential.createdAtMs !== createdAtMs || credential.notBeforeAtMs !== createdAtMs || audit.eventId !== value.mutationId || audit.requestId !== value.requestId || audit.operationId !== 'identity.bootstrap_provision' || audit.projectId !== null || audit.subject?.type !== issuer.subject.type || audit.subject.id !== issuer.subject.id || audit.authenticationId !== issuer.authenticationId || audit.outcome !== 'allowed' || audit.reasons.length !== 1 || audit.reasons[0] !== 'local_console_provisioning' || audit.fence !== null || audit.occurredAtMs !== createdAtMs ) { throw new InvalidLocalOwnerBootstrapValueError( 'provisioning semantics are invalid', ); } return Object.freeze({ mutationId: value.mutationId, requestId: value.requestId, identity, credential, issuer, audit, createdAtMs, }); } export function normalizeIssueLocalOwnerBootstrapChallengeCommand( value: IssueLocalOwnerBootstrapChallengeCommand, ): Readonly { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new InvalidLocalOwnerBootstrapValueError('issue must be an object'); } exactKeys( value, [ 'projectId', 'mutationId', 'requestId', 'challengeId', 'tokenDigest', 'issuer', 'issuedAtMs', 'expiresAtMs', 'audit', ], 'issue', ); if ( typeof value.projectId !== 'string' || !REQUEST_ID_PATTERN.test(value.projectId) ) { throw new InvalidLocalOwnerBootstrapValueError('projectId is invalid'); } assertLocalOwnerBootstrapMutationId(value.mutationId); assertLocalOwnerBootstrapRequestId(value.requestId); assertLocalOwnerBootstrapChallengeId(value.challengeId); assertLocalOwnerBootstrapTokenDigest(value.tokenDigest); const issuedAtMs = timestamp(value.issuedAtMs, 'issuedAtMs'); const expiresAtMs = timestamp(value.expiresAtMs, 'expiresAtMs'); assertLocalOwnerBootstrapTtl(expiresAtMs - issuedAtMs); const issuer = localConsolePrincipal(value.issuer, issuedAtMs); const audit = normalizeSecurityAuditRecord(value.audit); if ( audit.eventId !== value.mutationId || audit.requestId !== value.requestId || audit.operationId !== 'project.owner_bootstrap_issue' || audit.projectId !== value.projectId || audit.subject?.type !== issuer.subject.type || audit.subject.id !== issuer.subject.id || audit.authenticationId !== issuer.authenticationId || audit.outcome !== 'allowed' || audit.reasons.length !== 1 || audit.reasons[0] !== 'local_console_challenge' || audit.fence !== null || audit.occurredAtMs !== issuedAtMs ) { throw new InvalidLocalOwnerBootstrapValueError('issue audit is invalid'); } return Object.freeze({ ...value, issuer, audit, issuedAtMs, expiresAtMs }); } export function normalizeClaimLocalOwnerCommand( value: ClaimLocalOwnerCommand, ): Readonly { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new InvalidLocalOwnerBootstrapValueError('claim must be an object'); } exactKeys( value, [ 'projectId', 'mutationId', 'requestId', 'challengeId', 'tokenDigest', 'principal', 'credentialId', 'credentialVersion', 'claimedAtMs', 'audit', ], 'claim', ); if ( typeof value.projectId !== 'string' || !REQUEST_ID_PATTERN.test(value.projectId) ) { throw new InvalidLocalOwnerBootstrapValueError('projectId is invalid'); } assertLocalOwnerBootstrapMutationId(value.mutationId); assertLocalOwnerBootstrapRequestId(value.requestId); assertLocalOwnerBootstrapChallengeId(value.challengeId); assertLocalOwnerBootstrapTokenDigest(value.tokenDigest); const claimedAtMs = timestamp(value.claimedAtMs, 'claimedAtMs'); const principal = userPrincipal(value.principal, claimedAtMs); if (principal.assurance !== 'single_factor') { throw new InvalidLocalOwnerBootstrapValueError( 'claim principal assurance is invalid', ); } if ( typeof value.credentialId !== 'string' || value.credentialId.length < 1 || value.credentialId.length > 64 || !Number.isSafeInteger(value.credentialVersion) || value.credentialVersion < 1 || value.credentialVersion > MAX_VERSION ) { throw new InvalidLocalOwnerBootstrapValueError( 'credential fence is invalid', ); } const audit = normalizeSecurityAuditRecord(value.audit); if ( audit.eventId !== value.mutationId || audit.requestId !== value.requestId || audit.operationId !== 'project.owner_bootstrap_claim' || audit.projectId !== value.projectId || audit.subject?.type !== principal.subject.type || audit.subject.id !== principal.subject.id || audit.authenticationId !== principal.authenticationId || audit.outcome !== 'allowed' || audit.reasons.length !== 1 || audit.reasons[0] !== 'owner_bootstrap_claim' || audit.fence?.bindingVersion !== 1 || audit.occurredAtMs !== claimedAtMs ) { throw new InvalidLocalOwnerBootstrapValueError('claim audit is invalid'); } return Object.freeze({ ...value, principal, audit, claimedAtMs }); } export function normalizeLocalOwnerBootstrapChallengeRecord( value: LocalOwnerBootstrapChallengeRecord, ): Readonly { if (!value || typeof value !== 'object' || Array.isArray(value)) { throw new InvalidLocalOwnerBootstrapValueError( 'challenge must be an object', ); } const consumed = value.consumedAtMs !== undefined; exactKeys( value, consumed ? [ 'projectId', 'version', 'issueMutationId', 'issueRequestId', 'challengeId', 'tokenDigest', 'issuer', 'issuedAtMs', 'expiresAtMs', 'issueAudit', 'consumedAtMs', 'claimMutationId', 'claimRequestId', 'claimedPrincipal', 'credentialId', 'credentialVersion', 'binding', 'claimAudit', ] : [ 'projectId', 'version', 'issueMutationId', 'issueRequestId', 'challengeId', 'tokenDigest', 'issuer', 'issuedAtMs', 'expiresAtMs', 'issueAudit', ], 'challenge', ); const issue = normalizeIssueLocalOwnerBootstrapChallengeCommand({ projectId: value.projectId, mutationId: value.issueMutationId, requestId: value.issueRequestId, challengeId: value.challengeId, tokenDigest: value.tokenDigest, issuer: value.issuer, issuedAtMs: value.issuedAtMs, expiresAtMs: value.expiresAtMs, audit: value.issueAudit, }); if ( !Number.isSafeInteger(value.version) || value.version < 1 || value.version > MAX_VERSION ) { throw new InvalidLocalOwnerBootstrapValueError('version is invalid'); } if (!consumed) { return Object.freeze({ projectId: issue.projectId, version: value.version, issueMutationId: issue.mutationId, issueRequestId: issue.requestId, challengeId: issue.challengeId, tokenDigest: issue.tokenDigest, issuer: issue.issuer, issuedAtMs: issue.issuedAtMs, expiresAtMs: issue.expiresAtMs, issueAudit: issue.audit, }); } const claim = normalizeClaimLocalOwnerCommand({ projectId: value.projectId, mutationId: value.claimMutationId!, requestId: value.claimRequestId!, challengeId: value.challengeId, tokenDigest: value.tokenDigest, principal: value.claimedPrincipal!, credentialId: value.credentialId!, credentialVersion: value.credentialVersion!, claimedAtMs: value.consumedAtMs!, audit: value.claimAudit!, }); const binding = normalizeProjectRoleBinding(value.binding!); if ( binding.projectId !== value.projectId || binding.subject.type !== claim.principal.subject.type || binding.subject.id !== claim.principal.subject.id || binding.version !== 1 || binding.state !== 'active' || binding.role !== 'owner' || binding.mutationId !== claim.mutationId || binding.changedBy.type !== LOCAL_OWNER_BOOTSTRAP_SYSTEM_SUBJECT.type || binding.changedBy.id !== LOCAL_OWNER_BOOTSTRAP_SYSTEM_SUBJECT.id || binding.createdAtMs !== claim.claimedAtMs ) { throw new InvalidLocalOwnerBootstrapValueError( 'claimed binding is invalid', ); } return Object.freeze({ projectId: issue.projectId, version: value.version, issueMutationId: issue.mutationId, issueRequestId: issue.requestId, challengeId: issue.challengeId, tokenDigest: issue.tokenDigest, issuer: issue.issuer, issuedAtMs: issue.issuedAtMs, expiresAtMs: issue.expiresAtMs, issueAudit: issue.audit, consumedAtMs: claim.claimedAtMs, claimMutationId: claim.mutationId, claimRequestId: claim.requestId, claimedPrincipal: claim.principal, credentialId: claim.credentialId, credentialVersion: claim.credentialVersion, binding, claimAudit: claim.audit, }); }