Files
zpan/server/platform/node.ts
T
Jasper VanandClaude Opus 4.8 e6e67c5fe4 feat(licensing): harden cloud pairing — env keys, clear errors, confirm handshake (#424)
Three related robustness fixes for the cloud pairing flow:

1. Trusted license public keys are env-configurable (ZPAN_LICENSE_PUBLIC_KEYS)
   instead of hardcoding dev keys in source — a leaked dev key is rotated via
   config and never baked into production builds. Registered in all platform
   factories.
2. Certificate verification surfaces a specific rejection reason
   (signature/issuer/instance/expired/host), and the pairing modal distinguishes
   a cert-verification failure from a genuine timeout instead of showing both as
   "expired". On failure the poll handler rolls back the orphaned cloud binding.
3. After verifying + storing the certificate, the instance confirms the binding
   to the cloud (zpan-cloud-sdk 2.1.0's POST /licenses/:id/confirm) so the cloud
   pairing page resolves to success only once the instance actually accepted it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 14:43:46 -04:00

33 lines
1.1 KiB
TypeScript

import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'
import * as authSchema from '../db/auth-schema'
import * as schema from '../db/schema'
import { registerEnvPublicKeys } from '../licensing/public-keys'
import type { Platform } from './interface'
export function createNodePlatform(): Platform {
const dbPath = process.env.DATABASE_URL || './zpan.db'
const migrationsFolder = process.env.MIGRATIONS_DIR || './migrations'
const sqlite = new Database(dbPath)
sqlite.pragma('journal_mode = WAL')
const db = drizzle(sqlite, { schema: { ...schema, ...authSchema } })
migrate(db, { migrationsFolder })
registerEnvPublicKeys(process.env.ZPAN_LICENSE_PUBLIC_KEYS)
return {
db,
getEnv(key: string) {
return process.env[key]
},
// Node has no platform-native bindings — callers always fall back to
// whatever the non-binding path does (e.g. DB-configured S3 storage).
getBinding<T = unknown>(_key: string): T | undefined {
return undefined
},
}
}