mirror of
https://github.com/saltbo/zpan.git
synced 2026-09-21 13:20:33 +08:00
* feat(licensing): cloud client + binding API (pair, poll, refresh, disconnect) - server/licensing/public-keys.ts — DEV PASERK placeholder (production key lands via C5 cross-repo PR) - server/licensing/verify.ts — verifyCertificate() using paseto-ts/v4, returns LicenseEntitlement | null - server/licensing/entitlement.ts — loadEntitlement() with 60s in-process memoization + invalidateEntitlementCache() - server/licensing/has-feature.ts — loadBindingState() + hasFeature() pure sync check - server/licensing/instance-id.ts — getOrCreateInstanceId() lazily persisted in systemOptions under 'instance_id' - server/licensing/refresh.ts — performRefresh(): calls cloud, verifies cert, rotates DB row; handles CloudUnboundError (clear binding) and CloudNetworkError (update error log, keep cached cert) - server/services/licensing-cloud.ts — createPairing(), pollPairing(), refreshEntitlement() with 10s timeout; CloudUnboundError + CloudNetworkError for typed error handling - server/routes/licensing.ts — public GET /api/licensing/status (no auth required) - server/routes/licensing-admin.ts — admin-only: POST /pair, GET /pair/:code/poll, POST /refresh, DELETE /binding - server/middleware/require-feature.ts — requireFeature(name) middleware, returns 402 when feature missing - server/app.ts — mount /api/licensing (public) + /api/licensing (admin) + export route types - src/lib/rpc.ts — licensingApi + licensingAdminApi RPC clients - src/lib/api.ts — getLicensingStatus(), connectCloud(), pollPairing(), refreshLicense(), disconnectCloud() - src/lib/api.test.ts — 17 new tests covering all 5 new api.ts wrappers - shared/types/licensing.ts — update LicenseEntitlement.issued_at/expires_at to string (ISO-8601) - paseto-ts dependency added for PASETO v4 public verification Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * fix(licensing): fix biome lint issues — remove unused imports, format test file Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * fix(licensing): apply biome format fixes to refresh, require-feature, licensing-cloud Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * test(licensing): add unit and integration tests for all new licensing modules - server/licensing/public-keys.test.ts — PUBLIC_KEYS format validation - server/licensing/verify.test.ts — verifyCertificate: valid cert, invalid sig, expired, instance mismatch, key rotation - server/licensing/has-feature.test.ts — hasFeature: null/unbound/empty/expired/future states - server/services/licensing-cloud.test.ts — createPairing, pollPairing, refreshEntitlement: success, 401 Unbound, network error - server/routes/licensing.integration.test.ts — GET /api/licensing/status: unbound, bound+cert, bound+no-cert, public access - server/routes/licensing-admin.integration.test.ts — auth guards (401/403) + POST /pair, GET /pair/:code/poll, POST /refresh, DELETE /binding - server/test/setup.ts — add license_binding table to in-memory schema Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f * test(licensing): add entitlement cache and refresh orchestration unit tests - entitlement.test.ts — loadEntitlement: no row, no cert, valid PASETO cert, expired cert; invalidateEntitlementCache: re-reads from DB after invalidation - refresh.test.ts — performRefresh: no-op when unbound, rotates token (pre-C5 object), rotates token (PASETO string), clears binding on 401 Unbound, updates error log on network failure Agent-Profile: https://agent-kanban.dev/agents/a6bb038c4226a87f --------- Co-authored-by: Bob <aibob@mails.agent-kanban.dev>
27 lines
842 B
TypeScript
27 lines
842 B
TypeScript
import { eq } from 'drizzle-orm'
|
|
import { nanoid } from 'nanoid'
|
|
import { systemOptions } from '../db/schema'
|
|
import type { Database } from '../platform/interface'
|
|
|
|
const INSTANCE_ID_KEY = 'instance_id'
|
|
|
|
// Returns the instance UUID, creating and persisting one if it does not exist.
|
|
// The ID is stored in systemOptions under key 'instance_id'.
|
|
export async function getOrCreateInstanceId(db: Database): Promise<string> {
|
|
const rows = await db
|
|
.select({ value: systemOptions.value })
|
|
.from(systemOptions)
|
|
.where(eq(systemOptions.key, INSTANCE_ID_KEY))
|
|
.limit(1)
|
|
|
|
if (rows[0]?.value) return rows[0].value
|
|
|
|
const id = nanoid(21)
|
|
await db
|
|
.insert(systemOptions)
|
|
.values({ key: INSTANCE_ID_KEY, value: id, public: false })
|
|
.onConflictDoUpdate({ target: systemOptions.key, set: { value: id } })
|
|
|
|
return id
|
|
}
|