mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
303 lines
11 KiB
TypeScript
303 lines
11 KiB
TypeScript
import { PushAuthAdmission } from './push-auth-admission.js'
|
||
import { createAdaptorServer } from '@hono/node-server'
|
||
import {
|
||
PUSH_LIMITS,
|
||
PushDeviceRegistrationRequestSchema,
|
||
PushHostChallengeRequestSchema,
|
||
PushHostSessionRequestSchema,
|
||
PushSendRequestSchema,
|
||
type PushSendResult
|
||
} from '@orca-cloud/push-contract'
|
||
import { Hono, type MiddlewareHandler } from 'hono'
|
||
import { bodyLimit } from 'hono/body-limit'
|
||
import { ApnsClient } from './apns-client.js'
|
||
import { createApnsHttp2Transport, type ApnsTransport } from './apns-http2-transport.js'
|
||
import { clientIpRateLimit, ClientIpRateLimiter, readClientIp } from './client-ip-rate-limit.js'
|
||
import { DurablePushStore } from './durable-push-store.js'
|
||
import { DurablePushWorker } from './durable-push-worker.js'
|
||
import type { PushConfig } from './config.js'
|
||
import { PushDeviceRegistryStore } from './device-registry-store.js'
|
||
import { createFcmAccessTokenProvider } from './fcm-access-token.js'
|
||
import { createFcmFetchTransport, FcmClient, type FcmTransport } from './fcm-client.js'
|
||
import { PushHostChallengeStore } from './host-challenge-store.js'
|
||
import { PushHostSessionStore } from './host-session-store.js'
|
||
import type { PushDatabase } from './push-database.js'
|
||
import { PushDispatcher } from './push-dispatcher.js'
|
||
import { PushObservability } from './push-observability.js'
|
||
import { reserveRequestConnection } from './push-background-database.js'
|
||
import { createPushReadiness } from './push-readiness.js'
|
||
import { PushRequestDrain } from './push-request-drain.js'
|
||
|
||
export type PushServerOptions = {
|
||
now?: () => number
|
||
apnsTransport?: ApnsTransport
|
||
fcmTransport?: FcmTransport
|
||
fcmAccessToken?: () => Promise<string>
|
||
}
|
||
|
||
type PushVariables = { hostFingerprint: string }
|
||
|
||
export function readBearer(header: string | undefined): string | null {
|
||
if (!header) return null
|
||
const [scheme, ...rest] = header.split(' ')
|
||
const token = rest.join(' ').trim()
|
||
return scheme?.toLowerCase() === 'bearer' && token.length > 0 ? token : null
|
||
}
|
||
|
||
// Enforce Hono’s body limit: Content-Length misses chunked bodies that req.json would fully buffer.
|
||
const limitBody = bodyLimit({
|
||
maxSize: PUSH_LIMITS.maxHttpBodyBytes,
|
||
onError: (context) => context.json({ error: 'request_too_large' }, 413)
|
||
})
|
||
|
||
export function createPushServer(
|
||
config: PushConfig,
|
||
database: PushDatabase,
|
||
options: PushServerOptions = {}
|
||
) {
|
||
const now = options.now ?? Date.now
|
||
const observability = new PushObservability()
|
||
const challenges = new PushHostChallengeStore(database, config.publicUrl, now)
|
||
const sessions = new PushHostSessionStore(database, now)
|
||
const devices = new PushDeviceRegistryStore(database, now)
|
||
const deliveryStore = new DurablePushStore(
|
||
database,
|
||
now,
|
||
reserveRequestConnection(database, config.databasePoolMax)
|
||
)
|
||
const apnsTransport = options.apnsTransport ?? (config.apns ? createApnsHttp2Transport() : null)
|
||
const dispatcher = new PushDispatcher({
|
||
devices,
|
||
...(config.apns && apnsTransport
|
||
? {
|
||
apns: new ApnsClient({
|
||
topic: config.apnsTopic,
|
||
credentials: config.apns,
|
||
transport: apnsTransport,
|
||
now
|
||
})
|
||
}
|
||
: {}),
|
||
fcm: new FcmClient({
|
||
now,
|
||
projectId: config.fcmProjectId,
|
||
accessToken: options.fcmAccessToken ?? createFcmAccessTokenProvider(),
|
||
transport: options.fcmTransport ?? createFcmFetchTransport()
|
||
}),
|
||
onOutcome: (status) =>
|
||
observability.record(
|
||
status === 'sent' ? 'delivery_sent' : status === 'dead' ? 'delivery_dead' : 'delivery_error'
|
||
)
|
||
})
|
||
const worker = new DurablePushWorker(deliveryStore, dispatcher, {
|
||
now,
|
||
onRetry: () => observability.record('delivery_retry')
|
||
})
|
||
const ready = createPushReadiness(database, { now })
|
||
const unauthenticatedIps = new ClientIpRateLimiter({ now })
|
||
const limitUnauthenticatedIp = clientIpRateLimit(unauthenticatedIps, {
|
||
trustedProxyHops: config.trustedProxyHops,
|
||
onLimited: () => observability.record('ip_rate_limited')
|
||
})
|
||
const limitAuthenticatedIp = clientIpRateLimit(
|
||
new ClientIpRateLimiter({
|
||
now,
|
||
capacity: PUSH_LIMITS.authenticatedRequestsPerMinutePerIp
|
||
}),
|
||
{
|
||
trustedProxyHops: config.trustedProxyHops,
|
||
onLimited: () => observability.record('ip_rate_limited')
|
||
}
|
||
)
|
||
const authAdmission = new PushAuthAdmission()
|
||
const invalidBearerIps = new ClientIpRateLimiter({ now })
|
||
const authenticatedHosts = new ClientIpRateLimiter({
|
||
now,
|
||
capacity: PUSH_LIMITS.authenticatedRequestsPerMinutePerHost
|
||
})
|
||
const app = new Hono<{ Variables: PushVariables }>()
|
||
const requestDrain = new PushRequestDrain()
|
||
app.use('*', requestDrain.middleware)
|
||
// Default Hono logging exposes pg row detail; log only error names.
|
||
app.onError((error, context) => {
|
||
observability.record('request_error')
|
||
console.warn(
|
||
JSON.stringify({
|
||
event: 'orca_push_request_failed',
|
||
error: error instanceof Error ? error.name : 'unknown'
|
||
})
|
||
)
|
||
return context.json({ error: 'internal' }, 500)
|
||
})
|
||
|
||
app.get('/health', (context) =>
|
||
context.json({ ok: true, pushProtocol: 1, deliveryProtocol: 2, mode: config.mode })
|
||
)
|
||
app.get('/ready', limitUnauthenticatedIp, async (context) =>
|
||
(await ready())
|
||
? context.json({ ok: true })
|
||
: context.json({ error: 'dependency_unavailable' }, 503)
|
||
)
|
||
|
||
if (config.mode === 'validation') {
|
||
app.use('*', async (context) => context.json({ error: 'validation_only' }, 503))
|
||
}
|
||
|
||
const bearerSession: MiddlewareHandler<{ Variables: PushVariables }> = async (context, next) => {
|
||
const ip = readClientIp(context, config.trustedProxyHops)
|
||
if (!invalidBearerIps.available(ip)) return context.json({ error: 'rate_limited' }, 429)
|
||
const bearer = readBearer(context.req.header('authorization'))
|
||
if (!bearer) {
|
||
invalidBearerIps.allow(ip)
|
||
return context.json({ error: 'invalid_token' }, 401)
|
||
}
|
||
const session = await authAdmission.run(async () => {
|
||
if (!invalidBearerIps.available(ip)) return null
|
||
const result = await sessions.resolve(bearer)
|
||
if (!result.ok) invalidBearerIps.allow(ip)
|
||
return result
|
||
})
|
||
if (!session) {
|
||
context.header('Retry-After', '1')
|
||
return context.json({ error: 'busy' }, 503)
|
||
}
|
||
if (!session.ok) {
|
||
return context.json(
|
||
{ error: session.reason === 'session_expired' ? 'session_expired' : 'invalid_token' },
|
||
401
|
||
)
|
||
}
|
||
if (!authenticatedHosts.allow(session.hostFingerprint))
|
||
return context.json({ error: 'rate_limited' }, 429)
|
||
context.set('hostFingerprint', session.hostFingerprint)
|
||
await next()
|
||
return
|
||
}
|
||
// `/v1/devices/*` matches `/v1/devices` itself; a second registration for the
|
||
// bare path would run both middlewares twice on it.
|
||
app.use('/v1/devices/*', limitAuthenticatedIp, bearerSession)
|
||
app.use('/v1/send', limitAuthenticatedIp, bearerSession)
|
||
|
||
app.post('/v1/host/challenge', limitUnauthenticatedIp, limitBody, async (context) => {
|
||
const body = PushHostChallengeRequestSchema.safeParse(
|
||
await context.req.json().catch(() => null)
|
||
)
|
||
if (!body.success) return context.json({ error: 'invalid_request' }, 400)
|
||
const issued = await challenges.issue(body.data.hostPublicKeyB64)
|
||
if (!issued) {
|
||
observability.record('challenge_rejected')
|
||
return context.json({ error: 'invalid_request' }, 400)
|
||
}
|
||
observability.record('challenge_issued')
|
||
const { hostFingerprint: _bound, ...response } = issued
|
||
return context.json(response)
|
||
})
|
||
|
||
app.post('/v1/host/session', limitUnauthenticatedIp, limitBody, async (context) => {
|
||
const body = PushHostSessionRequestSchema.safeParse(await context.req.json().catch(() => null))
|
||
if (!body.success) return context.json({ error: 'invalid_request' }, 400)
|
||
const verification = await challenges.verify(body.data.challengeId, body.data.proofB64)
|
||
if (!verification.ok) {
|
||
observability.record('session_rejected')
|
||
return context.json(
|
||
{
|
||
error: verification.reason === 'unknown_challenge' ? 'invalid_challenge' : 'invalid_proof'
|
||
},
|
||
401
|
||
)
|
||
}
|
||
observability.record('session_issued')
|
||
return context.json(await sessions.create(verification.hostFingerprint))
|
||
})
|
||
|
||
app.post('/v1/devices', limitBody, async (context) => {
|
||
const body = PushDeviceRegistrationRequestSchema.safeParse(
|
||
await context.req.json().catch(() => null)
|
||
)
|
||
if (!body.success) return context.json({ error: 'invalid_request' }, 400)
|
||
const registered = await devices.upsert({
|
||
hostFingerprint: context.get('hostFingerprint'),
|
||
deviceId: body.data.deviceId,
|
||
platform: body.data.platform,
|
||
token: body.data.token,
|
||
...(body.data.apnsEnvironment === undefined
|
||
? {}
|
||
: { apnsEnvironment: body.data.apnsEnvironment })
|
||
})
|
||
if (!registered.ok) {
|
||
observability.record('device_rejected')
|
||
return context.json({ error: 'too_many_devices' }, 409)
|
||
}
|
||
observability.record('device_registered')
|
||
return context.json({ registrationId: registered.registrationId })
|
||
})
|
||
|
||
app.delete('/v1/devices/:registrationId', async (context) => {
|
||
const deleted = await devices.deleteOwned(
|
||
context.get('hostFingerprint'),
|
||
context.req.param('registrationId')
|
||
)
|
||
if (!deleted) return context.json({ error: 'not_found' }, 404)
|
||
observability.record('device_deleted')
|
||
return context.body(null, 204)
|
||
})
|
||
|
||
app.get('/v1/devices', async (context) =>
|
||
context.json({ devices: await devices.list(context.get('hostFingerprint')) })
|
||
)
|
||
|
||
app.post('/v1/send', limitBody, async (context) => {
|
||
const body = PushSendRequestSchema.safeParse(await context.req.json().catch(() => null))
|
||
if (!body.success) return context.json({ error: 'invalid_request' }, 400)
|
||
const hostFingerprint = context.get('hostFingerprint')
|
||
const owned = await devices.findOwned(hostFingerprint, body.data.registrationIds)
|
||
const results: PushSendResult[] = []
|
||
for (const registrationId of body.data.registrationIds) {
|
||
const device = owned.get(registrationId)
|
||
if (!device) {
|
||
observability.record('send_error')
|
||
results.push({ registrationId, status: 'error' })
|
||
continue
|
||
}
|
||
if (device.dead) {
|
||
observability.record('send_dead')
|
||
results.push({ registrationId, status: 'dead' })
|
||
continue
|
||
}
|
||
const reservation = await deliveryStore.accept(
|
||
hostFingerprint,
|
||
registrationId,
|
||
body.data.notification
|
||
)
|
||
if (reservation !== 'queued') {
|
||
observability.record(reservation === 'rate_limited' ? 'send_rate_limited' : 'send_error')
|
||
results.push({ registrationId, status: reservation })
|
||
continue
|
||
}
|
||
observability.record('send_queued')
|
||
results.push({ registrationId, status: 'queued' })
|
||
}
|
||
if (results.some((result) => result.status === 'queued')) worker.wake()
|
||
return context.json({ results })
|
||
})
|
||
|
||
return {
|
||
app,
|
||
requestDrain,
|
||
server: createAdaptorServer(app),
|
||
challenges,
|
||
sessions,
|
||
devices,
|
||
deliveryStore,
|
||
unauthenticatedIps,
|
||
worker,
|
||
observability,
|
||
ready,
|
||
closeTransports: (): void => {
|
||
if (apnsTransport && 'close' in apnsTransport) {
|
||
;(apnsTransport as { close: () => void }).close()
|
||
}
|
||
}
|
||
}
|
||
}
|