feat(api): board self-register + attendee claim (onboarding slice 1)
Adds the server side of the "preloaded + self-register + claim" board
onboarding so attendees bring a board online with no operator and no
admin code.
- claim.ts: an unclaimed pool keyed by kitId, holding {url, token,
claimCode}; single-use claim with a constant-time code compare and a
per-kit sliding-window rate limit (MAX_FAILS/WINDOW_MS) to blunt code
guessing.
- POST /nodes/self-register — a booting board announces itself; gated
by a shared FLEET_SECRET (baked into the image), not the admin code.
- POST /claim {kit, teamId, code} — public + rate-limited; validates
the claim code, moves the bearer token straight from the pool into
the node bridge (never touches the browser), binds the board to the
team without clobbering an existing name/members, and emits the
node:status + team:update the live feeds already consume.
- index.ts: wire the pool + FLEET_SECRET env.
22 new tests (claim unit + endpoint integration).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
88789b3f5c
commit
14e08b3623
+62
-2
@@ -1,9 +1,10 @@
|
|||||||
import express, { type Express } from 'express'
|
import express, { type Express } from 'express'
|
||||||
import cors from 'cors'
|
import cors from 'cors'
|
||||||
import type { Store } from './db'
|
import type { Store } from './db'
|
||||||
import { requireCode, requireAnyCode } from './auth'
|
import { requireCode, requireAnyCode, matches } from './auth'
|
||||||
import type { TeamSnapshot, SubmissionDTO, WsEvent } from './types'
|
import type { TeamSnapshot, SubmissionDTO, WsEvent } from './types'
|
||||||
import type { NodeBridge } from './nodes'
|
import type { NodeBridge } from './nodes'
|
||||||
|
import type { UnclaimedPool } from './claim'
|
||||||
|
|
||||||
export interface AppOptions {
|
export interface AppOptions {
|
||||||
store: Store
|
store: Store
|
||||||
@@ -14,6 +15,10 @@ export interface AppOptions {
|
|||||||
now?: () => string
|
now?: () => string
|
||||||
/** Optional ZeroClaw node bridge; when absent, /nodes routes 503. */
|
/** Optional ZeroClaw node bridge; when absent, /nodes routes 503. */
|
||||||
nodes?: NodeBridge
|
nodes?: NodeBridge
|
||||||
|
/** Optional pool of self-registered, not-yet-claimed boards. */
|
||||||
|
unclaimed?: UnclaimedPool
|
||||||
|
/** Shared fleet secret boards present when self-registering. */
|
||||||
|
fleetSecret?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyPhases = { reg: false, setup: false, m1: false, m2: false, add: false }
|
const emptyPhases = { reg: false, setup: false, m1: false, m2: false, add: false }
|
||||||
@@ -21,7 +26,7 @@ const emptyStats = { calls: 0, nominal: 0, anomalous: 0, critical: 0 }
|
|||||||
|
|
||||||
/** Build the collective REST app. Pure of I/O wiring (db + broadcast injected). */
|
/** Build the collective REST app. Pure of I/O wiring (db + broadcast injected). */
|
||||||
export function createApp(opts: AppOptions): Express {
|
export function createApp(opts: AppOptions): Express {
|
||||||
const { store, broadcast, adminCode, judgeCode, nodes } = opts
|
const { store, broadcast, adminCode, judgeCode, nodes, unclaimed, fleetSecret } = opts
|
||||||
const now = opts.now ?? (() => new Date().toISOString())
|
const now = opts.now ?? (() => new Date().toISOString())
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(cors({ origin: opts.corsOrigin ?? true }))
|
app.use(cors({ origin: opts.corsOrigin ?? true }))
|
||||||
@@ -122,6 +127,61 @@ export function createApp(opts: AppOptions): Express {
|
|||||||
res.status(201).json({ teamId: b.teamId, url: b.url })
|
res.status(201).json({ teamId: b.teamId, url: b.url })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// A board announces itself on boot into the unclaimed pool. Gated by the
|
||||||
|
// shared fleet secret (baked into the board image) — not the admin code —
|
||||||
|
// so a booting board needs no operator, but randoms can't seed the pool.
|
||||||
|
app.post('/nodes/self-register', (req, res) => {
|
||||||
|
if (!unclaimed) return res.status(503).json({ error: 'claim unavailable' })
|
||||||
|
if (!fleetSecret || !matches(req.header('x-fleet-secret') ?? '', fleetSecret)) {
|
||||||
|
return res.status(401).json({ error: 'unauthorized' })
|
||||||
|
}
|
||||||
|
const b = req.body ?? {}
|
||||||
|
if (![b.kitId, b.url, b.token, b.claimCode].every((v) => typeof v === 'string' && v)) {
|
||||||
|
return res.status(400).json({ error: 'kitId, url, token and claimCode are required' })
|
||||||
|
}
|
||||||
|
unclaimed.announce({ kitId: b.kitId, url: b.url, token: b.token, claimCode: b.claimCode })
|
||||||
|
res.status(201).json({ kitId: b.kitId })
|
||||||
|
})
|
||||||
|
|
||||||
|
// A participant claims their powered-on board to their team by proving
|
||||||
|
// possession of the kit's claim code. Public + rate-limited (no operator in
|
||||||
|
// the loop); the bearer token moves straight from the pool into the bridge
|
||||||
|
// and never touches the browser.
|
||||||
|
app.post('/claim', async (req, res) => {
|
||||||
|
if (!unclaimed || !nodes) return res.status(503).json({ error: 'claim unavailable' })
|
||||||
|
const b = req.body ?? {}
|
||||||
|
if (typeof b.kit !== 'string' || typeof b.teamId !== 'string' || typeof b.code !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'kit, teamId and code are required' })
|
||||||
|
}
|
||||||
|
const result = unclaimed.claim(b.kit, b.code, Date.parse(now()))
|
||||||
|
if (!result.ok) {
|
||||||
|
if (result.reason === 'unknown') {
|
||||||
|
return res.status(404).json({ error: 'no board found for that kit — is it powered on?' })
|
||||||
|
}
|
||||||
|
if (result.reason === 'rate_limited') {
|
||||||
|
return res.status(429).json({ error: 'too many attempts — wait a minute and try again' })
|
||||||
|
}
|
||||||
|
return res.status(401).json({ error: 'wrong claim code' })
|
||||||
|
}
|
||||||
|
await nodes.register({ teamId: b.teamId, url: result.node.url, token: result.node.token })
|
||||||
|
// Bind the board to the team without clobbering an existing registration.
|
||||||
|
const prev = store.getTeam(b.teamId)
|
||||||
|
const team: TeamSnapshot = {
|
||||||
|
id: b.teamId,
|
||||||
|
name: typeof b.teamName === 'string' && b.teamName ? b.teamName : prev?.name ?? '',
|
||||||
|
kit: b.kit,
|
||||||
|
members: Array.isArray(b.members) ? b.members : prev?.members ?? [],
|
||||||
|
phases: { ...emptyPhases, ...(prev?.phases ?? {}) },
|
||||||
|
stats: { ...emptyStats, ...(prev?.stats ?? {}) },
|
||||||
|
deviceConnected: true,
|
||||||
|
updatedAt: now(),
|
||||||
|
}
|
||||||
|
store.upsertTeam(team)
|
||||||
|
broadcast({ type: 'team:update', team })
|
||||||
|
const online = nodes.list().find((n) => n.teamId === b.teamId)?.online ?? false
|
||||||
|
res.status(201).json({ teamId: b.teamId, kit: b.kit, online })
|
||||||
|
})
|
||||||
|
|
||||||
app.delete('/nodes/:teamId', requireCode(adminCode), (req, res) => {
|
app.delete('/nodes/:teamId', requireCode(adminCode), (req, res) => {
|
||||||
if (!nodes) return res.status(503).json({ error: 'node bridge unavailable' })
|
if (!nodes) return res.status(503).json({ error: 'node bridge unavailable' })
|
||||||
nodes.remove(String(req.params.teamId))
|
nodes.remove(String(req.params.teamId))
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from 'vitest'
|
||||||
|
import request from 'supertest'
|
||||||
|
import { openStore, type Store } from './db'
|
||||||
|
import { createApp } from './app'
|
||||||
|
import { createNodeBridge } from './nodes'
|
||||||
|
import { createUnclaimedPool } from './claim'
|
||||||
|
import { MAX_FAILS } from './claim'
|
||||||
|
import type { WsEvent } from './types'
|
||||||
|
|
||||||
|
const ADMIN = 'admin-code'
|
||||||
|
const JUDGE = 'judge-code'
|
||||||
|
const FLEET = 'fleet-secret'
|
||||||
|
|
||||||
|
const board = { kitId: 'KIT-07', url: 'http://192.168.1.7:8080', token: 'zc_secret_token', claimCode: '418302' }
|
||||||
|
|
||||||
|
describe('board self-register + claim', () => {
|
||||||
|
let store: Store
|
||||||
|
let events: WsEvent[]
|
||||||
|
let now: string
|
||||||
|
let app: ReturnType<typeof createApp>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = openStore(':memory:')
|
||||||
|
events = []
|
||||||
|
now = '2026-07-27T14:00:00.000Z'
|
||||||
|
const broadcast = (e: WsEvent) => events.push(e)
|
||||||
|
const nodes = createNodeBridge({
|
||||||
|
broadcast,
|
||||||
|
ping: async () => true,
|
||||||
|
send: async () => {},
|
||||||
|
subscribe: () => () => {},
|
||||||
|
})
|
||||||
|
app = createApp({
|
||||||
|
store,
|
||||||
|
broadcast,
|
||||||
|
adminCode: ADMIN,
|
||||||
|
judgeCode: JUDGE,
|
||||||
|
nodes,
|
||||||
|
unclaimed: createUnclaimedPool(),
|
||||||
|
fleetSecret: FLEET,
|
||||||
|
now: () => now,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const selfRegister = (over: Partial<typeof board> = {}, secret = FLEET) =>
|
||||||
|
request(app).post('/nodes/self-register').set('x-fleet-secret', secret).send({ ...board, ...over })
|
||||||
|
|
||||||
|
describe('self-register', () => {
|
||||||
|
it('rejects a board without the fleet secret', async () => {
|
||||||
|
await request(app).post('/nodes/self-register').send(board).expect(401)
|
||||||
|
await selfRegister({}, 'wrong-secret').expect(401)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('validates the body', async () => {
|
||||||
|
await selfRegister({ token: '' }).expect(400)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('accepts a board presenting the fleet secret', async () => {
|
||||||
|
const res = await selfRegister().expect(201)
|
||||||
|
expect(res.body).toEqual({ kitId: 'KIT-07' })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('claim', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await selfRegister().expect(201)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects an unknown kit', async () => {
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-99', teamId: 'team-07', code: '418302' }).expect(404)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a wrong claim code', async () => {
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '000000' }).expect(401)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('binds the board to the team, brings it online, and never leaks the token', async () => {
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/claim')
|
||||||
|
.send({ kit: 'KIT-07', teamId: 'team-07', teamName: 'team_resonance', code: '418302' })
|
||||||
|
.expect(201)
|
||||||
|
expect(res.body).toEqual({ teamId: 'team-07', kit: 'KIT-07', online: true })
|
||||||
|
|
||||||
|
// the node is now registered + online for the team
|
||||||
|
const list = await request(app).get('/nodes').set('x-access-code', ADMIN).expect(200)
|
||||||
|
expect(list.body).toEqual([{ teamId: 'team-07', url: board.url, online: true }])
|
||||||
|
expect(JSON.stringify(list.body)).not.toContain('zc_secret_token')
|
||||||
|
|
||||||
|
// the team now exists, device-connected, with its kit + name
|
||||||
|
const teams = await request(app).get('/teams').set('x-access-code', ADMIN).expect(200)
|
||||||
|
expect(teams.body).toContainEqual(
|
||||||
|
expect.objectContaining({ id: 'team-07', name: 'team_resonance', kit: 'KIT-07', deviceConnected: true }),
|
||||||
|
)
|
||||||
|
|
||||||
|
// participants + judges get the live updates
|
||||||
|
expect(events).toContainEqual({ type: 'node:status', teamId: 'team-07', online: true })
|
||||||
|
expect(events.some((e) => e.type === 'team:update' && e.team.id === 'team-07')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is single-use — a second claim of the same kit 404s', async () => {
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '418302' }).expect(201)
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-08', code: '418302' }).expect(404)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not clobber an existing team name/members on re-claim', async () => {
|
||||||
|
await request(app)
|
||||||
|
.put('/teams/team-07')
|
||||||
|
.send({ name: 'team_resonance', kit: 'KIT-07', members: ['ada', 'linus'] })
|
||||||
|
.expect(204)
|
||||||
|
await selfRegister().expect(201) // board rebooted, back in the pool
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '418302' }).expect(201)
|
||||||
|
const teams = await request(app).get('/teams').set('x-access-code', ADMIN).expect(200)
|
||||||
|
expect(teams.body).toContainEqual(
|
||||||
|
expect.objectContaining({ id: 'team-07', name: 'team_resonance', members: ['ada', 'linus'] }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rate-limits repeated wrong codes, then recovers after the window', async () => {
|
||||||
|
for (let i = 0; i < MAX_FAILS; i++) {
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '000000' }).expect(401)
|
||||||
|
}
|
||||||
|
// locked — even the right code is refused
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '418302' }).expect(429)
|
||||||
|
// a minute later the window has passed
|
||||||
|
now = '2026-07-27T14:02:00.000Z'
|
||||||
|
await request(app).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '418302' }).expect(201)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('503s the claim routes when the pool is not configured', async () => {
|
||||||
|
const bare = createApp({ store, broadcast: () => {}, adminCode: ADMIN, judgeCode: JUDGE })
|
||||||
|
await request(bare).post('/nodes/self-register').set('x-fleet-secret', FLEET).send(board).expect(503)
|
||||||
|
await request(bare).post('/claim').send({ kit: 'KIT-07', teamId: 'team-07', code: '418302' }).expect(503)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect } from 'vitest'
|
||||||
|
import { createUnclaimedPool, MAX_FAILS, WINDOW_MS } from './claim'
|
||||||
|
|
||||||
|
const node = (over: Partial<Parameters<ReturnType<typeof createUnclaimedPool>['announce']>[0]> = {}) => ({
|
||||||
|
kitId: 'KIT-07',
|
||||||
|
url: 'http://192.168.1.7:8080',
|
||||||
|
token: 'zc_secret_token',
|
||||||
|
claimCode: '418302',
|
||||||
|
...over,
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unclaimed pool', () => {
|
||||||
|
it('announces a board and lists only its kit id (no secrets)', () => {
|
||||||
|
const pool = createUnclaimedPool()
|
||||||
|
pool.announce(node())
|
||||||
|
expect(pool.has('KIT-07')).toBe(true)
|
||||||
|
expect(pool.list()).toEqual([{ kitId: 'KIT-07' }])
|
||||||
|
expect(JSON.stringify(pool.list())).not.toContain('zc_secret_token')
|
||||||
|
expect(JSON.stringify(pool.list())).not.toContain('418302')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('claims with the right code, returns the node, and removes it from the pool', () => {
|
||||||
|
const pool = createUnclaimedPool()
|
||||||
|
pool.announce(node())
|
||||||
|
const r = pool.claim('KIT-07', '418302', 0)
|
||||||
|
expect(r).toEqual({ ok: true, node: node() })
|
||||||
|
expect(pool.has('KIT-07')).toBe(false) // single-use: gone after a successful claim
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects an unknown kit', () => {
|
||||||
|
const pool = createUnclaimedPool()
|
||||||
|
expect(pool.claim('KIT-99', 'x', 0)).toEqual({ ok: false, reason: 'unknown' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('rejects a wrong code without consuming the board', () => {
|
||||||
|
const pool = createUnclaimedPool()
|
||||||
|
pool.announce(node())
|
||||||
|
expect(pool.claim('KIT-07', '000000', 0)).toEqual({ ok: false, reason: 'bad_code' })
|
||||||
|
expect(pool.has('KIT-07')).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('locks a kit after too many wrong codes, then recovers after the window', () => {
|
||||||
|
const pool = createUnclaimedPool()
|
||||||
|
pool.announce(node())
|
||||||
|
for (let i = 0; i < MAX_FAILS; i++) {
|
||||||
|
expect(pool.claim('KIT-07', '000000', 0).ok).toBe(false)
|
||||||
|
}
|
||||||
|
// further attempts are rate-limited — even the correct code is refused while locked
|
||||||
|
expect(pool.claim('KIT-07', '418302', 100)).toEqual({ ok: false, reason: 'rate_limited' })
|
||||||
|
// once the window passes, the correct code works again
|
||||||
|
expect(pool.claim('KIT-07', '418302', WINDOW_MS + 1)).toEqual({ ok: true, node: node() })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('re-announcing (a reboot) clears the fail counter', () => {
|
||||||
|
const pool = createUnclaimedPool()
|
||||||
|
pool.announce(node())
|
||||||
|
for (let i = 0; i < MAX_FAILS; i++) pool.claim('KIT-07', '000000', 0)
|
||||||
|
pool.announce(node()) // board rebooted and re-registered
|
||||||
|
expect(pool.claim('KIT-07', '418302', 0)).toEqual({ ok: true, node: node() })
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { matches } from './auth'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A board that has powered on and announced itself, but is not yet bound to a
|
||||||
|
* team. Holds the gateway url + bearer token (server-side, never sent to a
|
||||||
|
* browser) plus the per-board claim code an attendee proves possession with
|
||||||
|
* (it is printed / QR-encoded on the physical kit).
|
||||||
|
*/
|
||||||
|
export interface UnclaimedNode {
|
||||||
|
kitId: string
|
||||||
|
url: string
|
||||||
|
token: string
|
||||||
|
claimCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ClaimResult =
|
||||||
|
| { ok: true; node: UnclaimedNode }
|
||||||
|
| { ok: false; reason: 'unknown' | 'bad_code' | 'rate_limited' }
|
||||||
|
|
||||||
|
export interface UnclaimedPool {
|
||||||
|
/** A board announces itself on boot (idempotent per kit; resets its fails). */
|
||||||
|
announce(n: UnclaimedNode): void
|
||||||
|
/** Kit ids only — never the url/token/claimCode. */
|
||||||
|
list(): { kitId: string }[]
|
||||||
|
has(kitId: string): boolean
|
||||||
|
/**
|
||||||
|
* Attempt a claim. On success removes the entry and returns the node so the
|
||||||
|
* caller can bind it to a team. Rate-limited per kit to blunt code guessing.
|
||||||
|
*/
|
||||||
|
claim(kitId: string, code: string, nowMs: number): ClaimResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Wrong-code attempts allowed per kit inside {@link WINDOW_MS} before lockout. */
|
||||||
|
export const MAX_FAILS = 5
|
||||||
|
export const WINDOW_MS = 60_000
|
||||||
|
|
||||||
|
export function createUnclaimedPool(seed: UnclaimedNode[] = []): UnclaimedPool {
|
||||||
|
const pool = new Map<string, UnclaimedNode>()
|
||||||
|
for (const n of seed) pool.set(n.kitId, n)
|
||||||
|
const fails = new Map<string, number[]>() // kitId -> recent failed-attempt timestamps (ms)
|
||||||
|
|
||||||
|
// Count fails still inside the sliding window, pruning older ones in place.
|
||||||
|
const recentFails = (kitId: string, nowMs: number): number => {
|
||||||
|
const arr = (fails.get(kitId) ?? []).filter((t) => nowMs - t < WINDOW_MS)
|
||||||
|
if (arr.length) fails.set(kitId, arr)
|
||||||
|
else fails.delete(kitId)
|
||||||
|
return arr.length
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
announce(n) {
|
||||||
|
pool.set(n.kitId, n)
|
||||||
|
fails.delete(n.kitId)
|
||||||
|
},
|
||||||
|
list() {
|
||||||
|
return [...pool.keys()].map((kitId) => ({ kitId }))
|
||||||
|
},
|
||||||
|
has(kitId) {
|
||||||
|
return pool.has(kitId)
|
||||||
|
},
|
||||||
|
claim(kitId, code, nowMs) {
|
||||||
|
const node = pool.get(kitId)
|
||||||
|
if (!node) return { ok: false, reason: 'unknown' }
|
||||||
|
if (recentFails(kitId, nowMs) >= MAX_FAILS) return { ok: false, reason: 'rate_limited' }
|
||||||
|
if (!matches(code, node.claimCode)) {
|
||||||
|
fails.set(kitId, [...(fails.get(kitId) ?? []), nowMs])
|
||||||
|
return { ok: false, reason: 'bad_code' }
|
||||||
|
}
|
||||||
|
pool.delete(kitId)
|
||||||
|
fails.delete(kitId)
|
||||||
|
return { ok: true, node }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,20 +4,26 @@ import { createHub } from './hub'
|
|||||||
import { createApp } from './app'
|
import { createApp } from './app'
|
||||||
import { attachWs } from './ws'
|
import { attachWs } from './ws'
|
||||||
import { createNodeBridge } from './nodes'
|
import { createNodeBridge } from './nodes'
|
||||||
|
import { createUnclaimedPool } from './claim'
|
||||||
|
|
||||||
const PORT = Number(process.env.PORT ?? 3000)
|
const PORT = Number(process.env.PORT ?? 3000)
|
||||||
const DB_PATH = process.env.DB_PATH ?? '/data/apess.db'
|
const DB_PATH = process.env.DB_PATH ?? '/data/apess.db'
|
||||||
const ADMIN_CODE = process.env.ADMIN_CODE ?? ''
|
const ADMIN_CODE = process.env.ADMIN_CODE ?? ''
|
||||||
const JUDGE_CODE = process.env.JUDGE_CODE ?? ''
|
const JUDGE_CODE = process.env.JUDGE_CODE ?? ''
|
||||||
|
const FLEET_SECRET = process.env.FLEET_SECRET ?? ''
|
||||||
const CORS_ORIGIN = process.env.CORS_ORIGIN
|
const CORS_ORIGIN = process.env.CORS_ORIGIN
|
||||||
|
|
||||||
if (!ADMIN_CODE || !JUDGE_CODE) {
|
if (!ADMIN_CODE || !JUDGE_CODE) {
|
||||||
console.warn('[apess-api] ADMIN_CODE / JUDGE_CODE not set — protected routes will reject all requests')
|
console.warn('[apess-api] ADMIN_CODE / JUDGE_CODE not set — protected routes will reject all requests')
|
||||||
}
|
}
|
||||||
|
if (!FLEET_SECRET) {
|
||||||
|
console.warn('[apess-api] FLEET_SECRET not set — board self-registration (/nodes/self-register) will reject all boards')
|
||||||
|
}
|
||||||
|
|
||||||
const store = openStore(DB_PATH)
|
const store = openStore(DB_PATH)
|
||||||
const hub = createHub()
|
const hub = createHub()
|
||||||
const nodes = createNodeBridge({ broadcast: hub.broadcast })
|
const nodes = createNodeBridge({ broadcast: hub.broadcast })
|
||||||
|
const unclaimed = createUnclaimedPool()
|
||||||
const app = createApp({
|
const app = createApp({
|
||||||
store,
|
store,
|
||||||
broadcast: hub.broadcast,
|
broadcast: hub.broadcast,
|
||||||
@@ -25,6 +31,8 @@ const app = createApp({
|
|||||||
judgeCode: JUDGE_CODE,
|
judgeCode: JUDGE_CODE,
|
||||||
corsOrigin: CORS_ORIGIN,
|
corsOrigin: CORS_ORIGIN,
|
||||||
nodes,
|
nodes,
|
||||||
|
unclaimed,
|
||||||
|
fleetSecret: FLEET_SECRET,
|
||||||
})
|
})
|
||||||
|
|
||||||
const server = http.createServer(app)
|
const server = http.createServer(app)
|
||||||
|
|||||||
Reference in New Issue
Block a user