mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
* refactor(cloud): share PostgreSQL schema startup between services * feat(cloud): add durable native push notification gateway * infra(push): define dedicated gateway resources and operational checks * fix(push): bound cross-host admission and simplify gateway configuration * fix(push): validate deploy configuration and preserve topic-error registrations
91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
const textEncoder = new TextEncoder()
|
|
|
|
export const PUSH_HOST_PROOF_TRANSCRIPT_DOMAIN = 'orca-push-host-proof/v1'
|
|
export const PUSH_HOST_CHALLENGE_PLAINTEXT_DOMAIN = 'orca-push-host-challenge/v1'
|
|
export const PUSH_HOST_CHALLENGE_BOX_ALGORITHM = 'Curve25519-XSalsa20-Poly1305'
|
|
export const PUSH_HOST_PROOF_ALGORITHM = 'HMAC-SHA-256'
|
|
|
|
export interface PushHostProofTranscriptInput {
|
|
gatewayOrigin: string
|
|
gatewayEphemeralPublicKey: Uint8Array
|
|
challengeNonce: Uint8Array
|
|
challengeId: string
|
|
issuedAt: number
|
|
expiresAt: number
|
|
hostFingerprint: string
|
|
hostPublicKey: Uint8Array
|
|
}
|
|
|
|
export const PUSH_HOST_PROOF_TRANSCRIPT_FIELD_COUNT = 10
|
|
|
|
function uint32(value: number): Uint8Array {
|
|
const bytes = new Uint8Array(4)
|
|
new DataView(bytes.buffer).setUint32(0, value, false)
|
|
return bytes
|
|
}
|
|
|
|
function uint64(value: number): Uint8Array {
|
|
const bytes = new Uint8Array(8)
|
|
new DataView(bytes.buffer).setBigUint64(0, BigInt(value), false)
|
|
return bytes
|
|
}
|
|
|
|
function concat(parts: readonly Uint8Array[]): Uint8Array {
|
|
const output = new Uint8Array(parts.reduce((total, part) => total + part.byteLength, 0))
|
|
let offset = 0
|
|
for (const part of parts) {
|
|
output.set(part, offset)
|
|
offset += part.byteLength
|
|
}
|
|
return output
|
|
}
|
|
|
|
function field(name: string, value: Uint8Array): Uint8Array {
|
|
const encodedName = textEncoder.encode(name)
|
|
return concat([uint32(encodedName.byteLength), encodedName, uint32(value.byteLength), value])
|
|
}
|
|
|
|
function text(value: string): Uint8Array {
|
|
return textEncoder.encode(value)
|
|
}
|
|
|
|
function requireByteLength(value: Uint8Array, expected: number, name: string): void {
|
|
if (value.byteLength !== expected) throw new Error(`${name} must be ${expected} bytes`)
|
|
}
|
|
|
|
export function buildPushHostProofTranscript(input: PushHostProofTranscriptInput): Uint8Array {
|
|
requireByteLength(input.gatewayEphemeralPublicKey, 32, 'gatewayEphemeralPublicKey')
|
|
requireByteLength(input.challengeNonce, 24, 'challengeNonce')
|
|
requireByteLength(input.hostPublicKey, 32, 'hostPublicKey')
|
|
return concat([
|
|
field('protocol', text(PUSH_HOST_PROOF_TRANSCRIPT_DOMAIN)),
|
|
field('version', new Uint8Array([1])),
|
|
field('gatewayOrigin', text(input.gatewayOrigin)),
|
|
field('gatewayEphemeralPublicKey', input.gatewayEphemeralPublicKey),
|
|
field('challengeNonce', input.challengeNonce),
|
|
field('challengeId', text(input.challengeId)),
|
|
field('issuedAt', uint64(input.issuedAt)),
|
|
field('expiresAt', uint64(input.expiresAt)),
|
|
field('hostFingerprint', text(input.hostFingerprint)),
|
|
field('hostPublicKey', input.hostPublicKey)
|
|
])
|
|
}
|
|
|
|
export function buildPushHostChallengePlaintext(
|
|
transcript: Uint8Array,
|
|
challengeSecret: Uint8Array
|
|
): Uint8Array {
|
|
if (challengeSecret.byteLength !== 32) throw new Error('challengeSecret must be 32 bytes')
|
|
// Why: the encrypted random secret makes the public transcript insufficient to forge the ack.
|
|
return concat([
|
|
text(`${PUSH_HOST_CHALLENGE_PLAINTEXT_DOMAIN}\0`),
|
|
uint32(transcript.byteLength),
|
|
transcript,
|
|
challengeSecret
|
|
])
|
|
}
|
|
|
|
export function buildPushHostProofMacInput(transcript: Uint8Array): Uint8Array {
|
|
return concat([text(`${PUSH_HOST_PROOF_TRANSCRIPT_DOMAIN}\0ack\0`), transcript])
|
|
}
|