feat: YOUreka Tools express app — landing page + fpagent enrollment wizard
- tools/: express host (hardcoded config, YOUreka Auth cookie session, OpenFGA domain filtering with 5-min cache, a2loft proxies) - /fingerprint wizard: domain picker, two-finger capture via fpagent Socket.IO, OTP-validated lookup/save, domain refresh control - node --test suite (27 tests, fake portal/PocketBase/a2loft upstreams) - Dockerfile (node:22-alpine, non-root, healthcheck) - README rewritten; node_modules gitignored Field-verified end to end: portal login, permitted-domain filtering, real U.are.U 4500 captures, cancel-preserves-capture, a2loft save.
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
// YOUreka Auth cookie session → PocketBase client. Ported from
|
||||
// one-school-chat (src/lib/server/yrka-auth.ts): the portal returns a
|
||||
// PocketBase session cookie from `GET /api/web/auth/{sessionId}/cookie`; we
|
||||
// store its encoded value under our own `session` cookie and reload it into a
|
||||
// PocketBase SDK client (pb.itcass.net), refreshing when invalid or near
|
||||
// expiry. `createPb` is injectable for tests; production always uses the
|
||||
// hardcoded pbUrl default.
|
||||
import PocketBase from 'pocketbase';
|
||||
|
||||
export const PB_URL = 'https://pb.itcass.net';
|
||||
|
||||
/** Refresh proactively when the token expires within this window. */
|
||||
const REFRESH_BUFFER_MS = 5 * 60 * 1000;
|
||||
|
||||
/** The auth cookie name used by the YRKA-auth PocketBase instance. */
|
||||
const PB_COOKIE_KEY = 'pb_auth';
|
||||
|
||||
/** Extract the bare `pb_auth` value from a Set-Cookie-style string. */
|
||||
export function extractPbCookieValue(raw) {
|
||||
const match = new RegExp(`${PB_COOKIE_KEY}=([^;]+)`).exec(raw);
|
||||
if (match) return match[1];
|
||||
// Already a bare value (no attributes / no name prefix).
|
||||
return raw.split(';')[0];
|
||||
}
|
||||
|
||||
/** Decode a JWT's `exp` (seconds) → ms, or 0 if unreadable. */
|
||||
export function tokenExpiresAtMs(token) {
|
||||
if (!token) return 0;
|
||||
try {
|
||||
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf8'));
|
||||
return typeof payload.exp === 'number' ? payload.exp * 1000 : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function rebuildCookieHeader(encodedValue) {
|
||||
return `${PB_COOKIE_KEY}=${encodedValue}`;
|
||||
}
|
||||
|
||||
function recordCollectionName(pb) {
|
||||
return pb.authStore.record?.collectionName ?? 'users';
|
||||
}
|
||||
|
||||
function exportCookieValue(pb) {
|
||||
return extractPbCookieValue(pb.authStore.exportToCookie({}, PB_COOKIE_KEY));
|
||||
}
|
||||
|
||||
async function tryRefresh(pb) {
|
||||
try {
|
||||
await pb.collection(recordCollectionName(pb)).authRefresh();
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a YRKA-auth PocketBase session from the persisted cookie value,
|
||||
* refreshing the token when it is expired or about to expire. Returns
|
||||
* { pb, refreshed, cookie? } or null when the session cannot be recovered
|
||||
* (caller treats the operator as anonymous).
|
||||
*/
|
||||
export async function loadYrkaSession(
|
||||
encodedCookieValue,
|
||||
{ pbUrl = PB_URL, createPb = (url) => new PocketBase(url) } = {}
|
||||
) {
|
||||
if (!encodedCookieValue) return null;
|
||||
|
||||
// Normalize: tolerate stale values that still carry a `pb_auth=` prefix.
|
||||
const normalized = extractPbCookieValue(encodedCookieValue);
|
||||
|
||||
const pb = createPb(pbUrl);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
try {
|
||||
pb.authStore.loadFromCookie(rebuildCookieHeader(normalized));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pb.authStore.token) return null;
|
||||
|
||||
// Invalid / expired → attempt one refresh (the stored token may redeem).
|
||||
if (!pb.authStore.isValid) {
|
||||
if (await tryRefresh(pb)) {
|
||||
return { pb, refreshed: true, cookie: exportCookieValue(pb) };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Valid but nearing expiry → refresh proactively and persist the new token.
|
||||
const expiresAt = tokenExpiresAtMs(pb.authStore.token);
|
||||
if (expiresAt > 0 && Date.now() + REFRESH_BUFFER_MS >= expiresAt) {
|
||||
if (await tryRefresh(pb)) {
|
||||
return { pb, refreshed: true, cookie: exportCookieValue(pb) };
|
||||
}
|
||||
}
|
||||
|
||||
return { pb, refreshed: false };
|
||||
}
|
||||
Reference in New Issue
Block a user