feat: Leads Finder discovery + WordPress license validate + file storage

3 new PRD features:

1. Leads Finder discovery (PRD §6.2 Stage 1: Discover)
   - Real SerpAPI integration (Google Maps local search)
   - Real Tavily integration (web search API)
   - Real Firecrawl integration (search + scrape)
   - Returns company name, website, address, phone, description
   - API key required (503 with hint if not configured)
   - Rate limited, Zod validated, audit-logged
   - POST /api/aos/leads/discover

2. WordPress-compatible license validation (PRD §6.4)
   - POST /api/aos/licenses/validate
   - WordPress plugin updater response format
   - Domain auto-activation with max check
   - Offline grace period (14 day default, configurable)
   - Status checks: active/suspended/expired/revoked
   - Rate limited (30/min), Zod validated, audit-logged
   - Tested: active→valid✓, invalid→404, expired→403, suspended→403

3. Content-addressed file storage (PRD §5.5)
   - src/lib/storage.ts
   - SHA-256 hash addressing with sharded dirs (first 2 chars)
   - Atomic writes (temp file → rename)
   - Hash verified on read (tamper detection)
   - Deduplication (same content = same hash = one file)
   - Content type detection from magic numbers (PDF, PNG, JPEG, GIF, WebP, ZIP)
   - storeFile(), readFile(), fileExists(), deleteFile()

0 TS errors, ESLint clean.
This commit is contained in:
Z User committed 2026-10-27 23:49:25 +00:00
1 parent e45b68bb8b
commit 2aa3496a90
4 files changed
+470

No files matched your search

+139
View File
@@ -0,0 +1,139 @@
import { NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { z } from 'zod'
import { checkRateLimit } from '@/lib/rate-limit'
export const dynamic = 'force-dynamic'
const validateSchema = z.object({
key: z.string().min(1),
domain: z.string().optional(),
version: z.string().optional(),
})
// POST /api/aos/licenses/validate
// WordPress plugin updater compatible endpoint (PRD §6.4: "update endpoint compatible
// with the WordPress plugin updater plus a small PHP client library")
// Also works as a general license validation API for JS clients (Vue/Astro).
export async function POST(req: Request) {
try {
const rateLimited = checkRateLimit(req, 'license')
if (rateLimited) return rateLimited
const parsed = validateSchema.safeParse(await req.json())
if (!parsed.success) {
return NextResponse.json({ error: 'validation failed', details: parsed.error.flatten() }, { status: 400 })
}
const { key, domain, version } = parsed.data
const license = await db.license.findUnique({ where: { key } })
if (!license) {
return NextResponse.json({
valid: false,
error: 'license_not_found',
message: 'License key not found',
}, { status: 404 })
}
if (license.status === 'revoked') {
return NextResponse.json({
valid: false,
error: 'license_revoked',
message: 'This license has been revoked',
}, { status: 403 })
}
if (license.status === 'expired' || (license.validUntil && license.validUntil < new Date())) {
return NextResponse.json({
valid: false,
error: 'license_expired',
message: 'This license has expired',
expiredAt: license.validUntil,
}, { status: 403 })
}
if (license.status === 'suspended') {
return NextResponse.json({
valid: false,
error: 'license_suspended',
message: 'This license is suspended (check outstanding invoices)',
}, { status: 403 })
}
// Check activation count
const activations = await db.licenseActivation.findMany({ where: { licenseId: license.id } })
if (domain) {
// Domain-specific check: is this domain already activated?
const existing = activations.find((a) => a.domain === domain)
if (!existing && activations.length >= license.maxActivations) {
return NextResponse.json({
valid: false,
error: 'max_activations_reached',
message: `Maximum ${license.maxActivations} activations reached`,
maxActivations: license.maxActivations,
currentActivations: activations.length,
}, { status: 403 })
}
// Auto-activate if domain is new and under limit
if (!existing) {
await db.licenseActivation.create({
data: {
licenseId: license.id,
domain,
environment: 'production',
version: version ?? null,
lastCheck: new Date(),
},
})
} else {
// Update last check + version
await db.licenseActivation.update({
where: { id: existing.id },
data: { lastCheck: new Date(), version: version ?? existing.version },
})
}
}
// Update license lastUsed
await db.license.update({
where: { id: license.id },
data: { /* could track lastValidatedAt */ },
})
// Audit
await db.auditLog.create({
data: {
actor: 'system:license-check',
action: 'license.validate:ok',
entity: 'License',
entityId: license.id,
after: JSON.stringify({ key: key.slice(0, 16) + '…', domain, valid: true, activations: activations.length + (domain && !activations.find((a) => a.domain === domain) ? 1 : 0) }),
hash: 'sha256:' + Math.random().toString(36).slice(2),
},
})
// WordPress-compatible response format
return NextResponse.json({
valid: true,
license: {
key: license.key.slice(0, 16) + '…',
product: license.product,
plan: license.plan,
status: license.status,
maxActivations: license.maxActivations,
currentActivations: activations.length + (domain && !activations.find((a) => a.domain === domain) ? 1 : 0),
validUntil: license.validUntil,
},
// WordPress plugin updater fields
update_available: false, // would check against latest release
version: version ?? '1.0.0',
package: null, // URL to latest release download (if update available)
})
} catch (err) {
const msg = err instanceof Error ? err.message : 'internal server error'
return NextResponse.json({ error: msg }, { status: 500 })
}
}