// src/backend-contract/primitives.ts import { contractError } from './errors' declare const backendContractBrand: unique symbol export type Brand = Value & { readonly [backendContractBrand]: { readonly kind: Kind; readonly scope: Scope } } export type OpaqueId = Brand export type AttachmentId = OpaqueId<'attachment', Attachment> export type AdapterId = OpaqueId<'adapter', Attachment> export type BackendInstanceId = OpaqueId<'backend-instance', Attachment> export type ManagerId = OpaqueId< 'manager', `${Attachment}:${Manager}` > export type GenerationId = OpaqueId export type PeerId = OpaqueId<'peer', Attachment> export type ConnectionId = OpaqueId< 'connection', `${Attachment}:${Connection}` > export type LeaseId = OpaqueId<'lease', `${Attachment}:${Lease}`> export type ScanShareToken = OpaqueId< 'scan-share-token', `${Attachment}:${Lease}` > export type ScanSessionId = OpaqueId< 'scan-session', `${Attachment}:${Lease}` > export type ClientId = OpaqueId<'client', `${Attachment}:${Client}`> export type OperationCorrelation = OpaqueId< 'core-operation', `${Attachment}:${Operation}` > export type BackendOperationHandle = OpaqueId< 'backend-operation', `${Attachment}:${Operation}` > export type NativeOperationCorrelation = OpaqueId< 'native-operation', `${Attachment}:${Operation}` > export type IpcOperationCorrelation = OpaqueId< 'ipc-operation', `${Attachment}:${Operation}` > export type GattDatabaseId = OpaqueId< 'gatt-database', `${Attachment}:${Connection}:${Database}` > export type SubscriptionId< Attachment extends string, Connection extends string, Database extends string, Service extends string, Characteristic extends string, Subscription extends string > = OpaqueId<'subscription', `${Attachment}:${Connection}:${Database}:${Service}:${Characteristic}:${Subscription}`> export type Uuid = Brand export type OwnedBytes = Brand export type BorrowedBytes = Readonly export type ByteLimit = Brand export type MonotonicTimestamp = Brand export type Deadline = Brand export type Capacity = Brand export type ResourceCount = Brand export type BackendContractAxis = 'backend-contract' export type CapabilitySchemaAxis = 'capability-schema' export type EventSchemaAxis = 'event-schema' export type TraceFormatAxis = 'trace-format' export type NativeProtocolAxis = 'native-protocol' export type IpcProtocolAxis = 'ipc-protocol' export type ProtocolAxis = | BackendContractAxis | CapabilitySchemaAxis | EventSchemaAxis | TraceFormatAxis | NativeProtocolAxis | IpcProtocolAxis export interface VersionNumber { readonly axis: Axis readonly value: number } export interface VersionRange { readonly axis: Axis readonly minimum: VersionNumber readonly maximum: VersionNumber } export interface NegotiatedVersion { readonly axis: Axis readonly selected: VersionNumber readonly localRange: VersionRange readonly remoteRange: VersionRange } export interface BackendCompatibilityOffer { readonly backendContract: VersionRange readonly capabilitySchema: VersionRange readonly eventSchema: VersionRange readonly traceFormat: VersionRange } export interface NativeCompatibilityOffer extends BackendCompatibilityOffer { readonly nativeProtocol: VersionRange } export interface IpcCompatibilityOffer extends BackendCompatibilityOffer { readonly ipcProtocol: VersionRange } export interface CoreVersionAxes { readonly backendContract: NegotiatedVersion readonly capabilitySchema: NegotiatedVersion readonly eventSchema: NegotiatedVersion readonly traceFormat: NegotiatedVersion } export interface NativeVersionAxes extends CoreVersionAxes { readonly nativeProtocol: NegotiatedVersion } export interface IpcVersionAxes extends CoreVersionAxes { readonly ipcProtocol: NegotiatedVersion } export type HostNeutralVersionAxes = CoreVersionAxes export type ApplicableCompatibilityOffer = BackendCompatibilityOffer | NativeCompatibilityOffer | IpcCompatibilityOffer export type ApplicableVersionAxes = HostNeutralVersionAxes | NativeVersionAxes | IpcVersionAxes export interface ByteLimits { readonly maximumOperationBytes: ByteLimit readonly maximumAdvertisementBytes: ByteLimit readonly maximumStreamItemBytes: ByteLimit readonly maximumRetainedBytes: ByteLimit } export interface ByteOwnership { readonly input: 'caller-borrows-until-settlement' readonly retainedInput: 'backend-copies-before-retention' readonly output: 'receiver-owns-independent-copy' readonly boundary: 'copy-or-transfer-with-explicit-owner' } export interface AttachmentBinding { readonly attachmentId: AttachmentId readonly backendInstanceId: BackendInstanceId readonly backendGeneration: GenerationId<'backend-generation', Attachment> readonly adapterId: AdapterId readonly adapterGeneration: GenerationId<'adapter-generation', Attachment> } export interface AttachmentBoundIdFactory { clientId(value: string): ClientId managerId(value: string): ManagerId connectionId(value: string): ConnectionId leaseId(value: string): LeaseId scanShareToken(value: string): ScanShareToken scanSessionId(value: string): ScanSessionId databaseId(value: string): GattDatabaseId subscriptionId(value: string): SubscriptionId operationCorrelation(value: string): OperationCorrelation backendOperationHandle(value: string): BackendOperationHandle } export interface IpcOperationIdFactory { ipcOperationCorrelation(value: string): IpcOperationCorrelation ipcDispatchEpoch(value: string): GenerationId<'ipc-dispatch-epoch', `${Attachment}:${string}`> } type AttachmentFromBoundScope = Scope extends `${infer Attachment}:${string}` ? Attachment : never export type Scalar = boolean | number | string | null export type SerializableValue = Scalar | OwnedBytes | readonly SerializableValue[] | SerializableRecord export interface SerializableRecord { readonly [key: string]: SerializableValue } function assertNonEmptyString(value: string, label: string): void { if (value.length === 0) { throw new Error(`${label} must be non-empty`) } } function assertNonNegativeSafeInteger(value: number, label: string): void { if (!Number.isSafeInteger(value) || value < 0) { throw new Error(`${label} must be a non-negative safe integer`) } } export function opaqueId( value: string, kind: Kind, scope: Scope ): OpaqueId { assertNonEmptyString(value, kind) assertNonEmptyString(scope, `${kind} scope`) return value as OpaqueId } function runtimeScopedOpaqueId( value: string, kind: Kind, scope: string ): OpaqueId { assertNonEmptyString(value, kind) assertNonEmptyString(scope, `${kind} scope`) return value as OpaqueId } function attachmentScope(binding: AttachmentBinding): string { const values = [ binding.attachmentId, binding.backendInstanceId, binding.backendGeneration, binding.adapterId, binding.adapterGeneration ] for (const value of values) { assertNonEmptyString(String(value), 'attachment binding value') } return values.map(value => String(value)).join(':') } /** Creates a peer identifier using the same attachment-bound scope as other runtime identifiers. */ export function createAttachmentBoundPeerId( binding: AttachmentBinding, value: string ): PeerId { return runtimeScopedOpaqueId<'peer', Attachment>(value, 'peer', attachmentScope(binding)) } export function createAttachmentBoundIdFactory( binding: AttachmentBinding ): AttachmentBoundIdFactory { const scope = attachmentScope(binding) return { clientId: value => runtimeScopedOpaqueId<'client', `${Attachment}:${string}`>(value, 'client', scope), managerId: value => runtimeScopedOpaqueId<'manager', `${Attachment}:${string}`>(value, 'manager', scope), connectionId: value => runtimeScopedOpaqueId<'connection', `${Attachment}:${string}`>(value, 'connection', scope), leaseId: value => runtimeScopedOpaqueId<'lease', `${Attachment}:${string}`>(value, 'lease', scope), scanShareToken: value => runtimeScopedOpaqueId<'scan-share-token', `${Attachment}:${string}`>(value, 'scan-share-token', scope), scanSessionId: value => runtimeScopedOpaqueId<'scan-session', `${Attachment}:${string}`>(value, 'scan-session', scope), databaseId: value => runtimeScopedOpaqueId<'gatt-database', `${Attachment}:${string}:${string}`>(value, 'gatt-database', scope), subscriptionId: value => runtimeScopedOpaqueId<'subscription', `${Attachment}:${string}:${string}:${string}:${string}:${string}`>( value, 'subscription', scope ), operationCorrelation: value => runtimeScopedOpaqueId<'core-operation', `${Attachment}:${string}`>(value, 'core-operation', scope), backendOperationHandle: value => runtimeScopedOpaqueId<'backend-operation', `${Attachment}:${string}`>(value, 'backend-operation', scope) } } /** Creates renderer-local IPC correlation IDs scoped to one already-attached backend generation. */ export function createIpcOperationIdFactory( scope: string ): IpcOperationIdFactory { return { ipcOperationCorrelation: value => runtimeScopedOpaqueId<'ipc-operation', `${Attachment}:${string}`>(value, 'ipc-operation', scope), ipcDispatchEpoch: value => runtimeScopedOpaqueId<'ipc-dispatch-epoch', `${Attachment}:${string}`>(value, 'ipc-dispatch-epoch', scope) } } export function rebindAttachmentBoundId( id: OpaqueId, source: AttachmentBinding>, target: AttachmentBinding> ): OpaqueId { if (attachmentScope(source) !== attachmentScope(target)) { throw new Error('attachment rebinding requires the identical attachment tuple') } return id } export function canonicalUuid(value: string): Uuid { const compact = value.replaceAll('-', '').toLowerCase() if (!/^[0-9a-f]+$/.test(compact)) { throw new Error('UUID must contain only hexadecimal digits and hyphens') } if (compact.length === 4) { return `0000${compact}-0000-1000-8000-00805f9b34fb` as Uuid } if (compact.length === 8) { return `${compact}-0000-1000-8000-00805f9b34fb` as Uuid } if (compact.length !== 32) { throw new Error('UUID must be a 16-bit, 32-bit, or 128-bit hexadecimal UUID') } return `${compact.slice(0, 8)}-${compact.slice(8, 12)}-${compact.slice(12, 16)}-${compact.slice(16, 20)}-${compact.slice(20)}` as Uuid } /** * Canonicalizes a 48-bit BLE radio address to uppercase colon-separated octets. * Accepts ':' or '-' separators in any case; rejects every other shape. This * canonical form only identifies peers using public or static random addresses; * peers using resolvable private addresses have no stable radio address and * must be re-entered through a durable `PeerReference` instead. */ export function canonicalBleAddress(value: string): string { if (typeof value !== 'string' || !/^[0-9A-Fa-f]{2}([:-][0-9A-Fa-f]{2}){5}$/.test(value)) { throw new Error('BLE address must be six hexadecimal octets separated by ":" or "-"') } return value.replaceAll('-', ':').toUpperCase() } export function byteLimit(value: number): ByteLimit { assertNonNegativeSafeInteger(value, 'byte limit') return value as ByteLimit } export function capacity(value: number): Capacity { assertNonNegativeSafeInteger(value, 'capacity') if (value === 0) { throw new Error('capacity must be greater than zero') } return value as Capacity } export function resourceCount(value: number): ResourceCount { assertNonNegativeSafeInteger(value, 'resource count') return value as ResourceCount } export function monotonicTimestamp(value: number): MonotonicTimestamp { if (!Number.isFinite(value) || value < 0) { throw new Error('monotonic timestamp must be finite and non-negative') } return value as MonotonicTimestamp } export function deadline(value: number): Deadline { if (!Number.isFinite(value) || value < 0) { throw new Error('deadline must be finite and non-negative') } return value as Deadline } export function ownBytes(bytes: BorrowedBytes, maximumBytes: ByteLimit): OwnedBytes { if (bytes.byteLength > maximumBytes) { throw contractError('bytes.too-large', 'boundary', 'primitives.own-bytes') } return new Uint8Array(bytes) as OwnedBytes } export function version(axis: Axis, value: number): VersionNumber { assertNonNegativeSafeInteger(value, `${axis} version`) return { axis, value } } export function versionRange( minimum: VersionNumber, maximum: VersionNumber ): VersionRange { if (minimum.axis !== maximum.axis) { throw contractError('protocol.malformed', 'core', 'version-range.axes') } if (minimum.value > maximum.value) { throw contractError('protocol.malformed', 'core', 'version-range.minimum') } return { axis: minimum.axis, minimum, maximum } } export function negotiateVersion( localRange: VersionRange, remoteRange: VersionRange ): NegotiatedVersion { assertNegotiationRange(localRange) assertNegotiationRange(remoteRange) if (localRange.axis !== remoteRange.axis) { throw contractError('protocol.malformed', 'core', 'version-negotiate.axes') } const selectedValue = Math.min(localRange.maximum.value, remoteRange.maximum.value) if (selectedValue < localRange.minimum.value || selectedValue < remoteRange.minimum.value) { throw contractError('protocol.incompatible', 'core', `version-negotiate.${localRange.axis}`) } return { axis: localRange.axis, selected: version(localRange.axis, selectedValue), localRange, remoteRange } } function assertNegotiationRange(range: VersionRange): void { if ( range.axis !== range.minimum.axis || range.axis !== range.maximum.axis || !Number.isSafeInteger(range.minimum.value) || range.minimum.value < 0 || !Number.isSafeInteger(range.maximum.value) || range.maximum.value < 0 || range.minimum.value > range.maximum.value ) { throw contractError('protocol.malformed', 'core', `version-negotiate.${range.axis}.range`) } } export function negotiateCoreVersions( local: BackendCompatibilityOffer, remote: BackendCompatibilityOffer ): CoreVersionAxes { return { backendContract: negotiateVersion(local.backendContract, remote.backendContract), capabilitySchema: negotiateVersion(local.capabilitySchema, remote.capabilitySchema), eventSchema: negotiateVersion(local.eventSchema, remote.eventSchema), traceFormat: negotiateVersion(local.traceFormat, remote.traceFormat) } } export function assertCoreVersionsAccepted(versions: CoreVersionAxes, offer: BackendCompatibilityOffer): void { assertVersionAccepted(versions.backendContract, offer.backendContract) assertVersionAccepted(versions.capabilitySchema, offer.capabilitySchema) assertVersionAccepted(versions.eventSchema, offer.eventSchema) assertVersionAccepted(versions.traceFormat, offer.traceFormat) } export function assertIpcVersionsAccepted(versions: IpcVersionAxes, offer: IpcCompatibilityOffer): void { assertCoreVersionsAccepted(versions, offer) assertVersionAccepted(versions.ipcProtocol, offer.ipcProtocol) } export function applicableVersionAxesEqual(left: ApplicableVersionAxes, right: ApplicableVersionAxes): boolean { if ( !negotiatedVersionEqual(left.backendContract, right.backendContract) || !negotiatedVersionEqual(left.capabilitySchema, right.capabilitySchema) || !negotiatedVersionEqual(left.eventSchema, right.eventSchema) || !negotiatedVersionEqual(left.traceFormat, right.traceFormat) ) { return false } if ('nativeProtocol' in left || 'nativeProtocol' in right) { return ( 'nativeProtocol' in left && 'nativeProtocol' in right && negotiatedVersionEqual(left.nativeProtocol, right.nativeProtocol) ) } if ('ipcProtocol' in left || 'ipcProtocol' in right) { return ( 'ipcProtocol' in left && 'ipcProtocol' in right && negotiatedVersionEqual(left.ipcProtocol, right.ipcProtocol) ) } return true } export function snapshotApplicableVersionAxes(versions: ApplicableVersionAxes): ApplicableVersionAxes { const core = { backendContract: snapshotNegotiatedVersion(versions.backendContract), capabilitySchema: snapshotNegotiatedVersion(versions.capabilitySchema), eventSchema: snapshotNegotiatedVersion(versions.eventSchema), traceFormat: snapshotNegotiatedVersion(versions.traceFormat) } if ('nativeProtocol' in versions) { return Object.freeze({ ...core, nativeProtocol: snapshotNegotiatedVersion(versions.nativeProtocol) }) } if ('ipcProtocol' in versions) { return Object.freeze({ ...core, ipcProtocol: snapshotNegotiatedVersion(versions.ipcProtocol) }) } return Object.freeze(core) } function negotiatedVersionEqual( left: NegotiatedVersion, right: NegotiatedVersion ): boolean { return ( left.axis === right.axis && left.selected.axis === right.selected.axis && left.selected.value === right.selected.value && versionRangeEqual(left.localRange, right.localRange) && versionRangeEqual(left.remoteRange, right.remoteRange) ) } function versionRangeEqual(left: VersionRange, right: VersionRange): boolean { return ( left.axis === right.axis && left.minimum.axis === right.minimum.axis && left.minimum.value === right.minimum.value && left.maximum.axis === right.maximum.axis && left.maximum.value === right.maximum.value ) } function snapshotNegotiatedVersion( versionValue: NegotiatedVersion ): NegotiatedVersion { return Object.freeze({ axis: versionValue.axis, selected: Object.freeze({ axis: versionValue.selected.axis, value: versionValue.selected.value }), localRange: snapshotVersionRange(versionValue.localRange), remoteRange: snapshotVersionRange(versionValue.remoteRange) }) } function snapshotVersionRange(range: VersionRange): VersionRange { return Object.freeze({ axis: range.axis, minimum: Object.freeze({ axis: range.minimum.axis, value: range.minimum.value }), maximum: Object.freeze({ axis: range.maximum.axis, value: range.maximum.value }) }) } function assertVersionAccepted( selected: NegotiatedVersion, range: VersionRange ): void { if ( selected.axis !== range.axis || selected.selected.value < range.minimum.value || selected.selected.value > range.maximum.value ) { throw contractError('protocol.incompatible', 'core', `version-accepted.${selected.axis}`) } }