Wire new routes; identity, summarize-url, dashboard, admin
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
// Request identity resolution (core-decoupling).
|
||||
//
|
||||
// Two kinds of caller authenticate to the relay:
|
||||
//
|
||||
// "cloud" — the operator's cloud Recaps server (recaps.cc) acting on
|
||||
// behalf of one of its signed-in users. It authenticates
|
||||
// ONCE with a shared operator key (X-Recap-Operator-Key) and
|
||||
// names the user via X-Recap-User-Id. The credit pool is
|
||||
// keyed by `user:<id>`, and the tier is whatever the relay
|
||||
// has STORED for that user (operator-set) — NOT read from a
|
||||
// license. This is the path that removes the per-user Keysat
|
||||
// license from cloud requests.
|
||||
//
|
||||
// "license" — a license bearer (a self-hosted install using the
|
||||
// operator's relay, or the operator's own single-mode app).
|
||||
// Unchanged legacy behavior: tier + pool come from the
|
||||
// resolved Keysat license / install id.
|
||||
//
|
||||
// resolveIdentity(req) returns a uniform shape the routes thread into the
|
||||
// credits/job-credits/envelope helpers:
|
||||
// { kind, creditKey, userId|null, installId|null, license|null }
|
||||
//
|
||||
// A route bills against `creditKey`; for tier it uses the stored row tier
|
||||
// (cloud) or `license.tier` (license) — see identityTier().
|
||||
|
||||
import { getConfigSnapshot } from "./config.js";
|
||||
import { resolveLicense } from "./keysat-client.js";
|
||||
import { getCreditKey } from "./credits.js";
|
||||
|
||||
// user-ids come from Recaps (base64url / hex account ids). Constrain the
|
||||
// charset so a header can't smuggle a path-ish or oversized key into the
|
||||
// ledger.
|
||||
const USER_ID_RE = /^[A-Za-z0-9_-]{1,128}$/;
|
||||
|
||||
export async function resolveIdentity(req) {
|
||||
const userId = (req.header("X-Recap-User-Id") || "").trim();
|
||||
|
||||
if (userId) {
|
||||
// Cloud path — must present a valid operator key.
|
||||
const cfg = await getConfigSnapshot();
|
||||
const expected = (cfg.relay_cloud_operator_key || "").trim();
|
||||
const presented = (req.header("X-Recap-Operator-Key") || "").trim();
|
||||
if (!expected || !presented || presented !== expected) {
|
||||
const e = new Error(
|
||||
"X-Recap-User-Id requires a valid X-Recap-Operator-Key"
|
||||
);
|
||||
e.status = 401;
|
||||
throw e;
|
||||
}
|
||||
if (!USER_ID_RE.test(userId)) {
|
||||
const e = new Error("invalid X-Recap-User-Id");
|
||||
e.status = 400;
|
||||
throw e;
|
||||
}
|
||||
return {
|
||||
kind: "cloud",
|
||||
creditKey: `user:${userId}`,
|
||||
userId,
|
||||
installId: null,
|
||||
license: null,
|
||||
};
|
||||
}
|
||||
|
||||
// License / install path — unchanged legacy behavior.
|
||||
const installId = req.header("X-Recap-Install-Id") || null;
|
||||
const license = await resolveLicense(req.header("Authorization"));
|
||||
// getCreditKey throws only when there's neither a license fingerprint
|
||||
// nor an installId; routes guard "missing install id" themselves, so
|
||||
// tolerate it here and leave creditKey null.
|
||||
let creditKey = null;
|
||||
try {
|
||||
creditKey = getCreditKey({ installId, license });
|
||||
} catch {}
|
||||
return { kind: "license", creditKey, userId: null, installId, license };
|
||||
}
|
||||
|
||||
// Is this request the operator's cloud server (valid operator key)?
|
||||
// Used by operator-only endpoints (e.g. setting a user's tier) that
|
||||
// aren't tied to a specific X-Recap-User-Id.
|
||||
export async function verifyOperatorKey(req) {
|
||||
const cfg = await getConfigSnapshot();
|
||||
const expected = (cfg.relay_cloud_operator_key || "").trim();
|
||||
const presented = (req.header("X-Recap-Operator-Key") || "").trim();
|
||||
return !!expected && !!presented && presented === expected;
|
||||
}
|
||||
|
||||
// The tier to bill/quota at for a resolved identity.
|
||||
// cloud → the relay's stored row tier (operator-set; default "core")
|
||||
// license → the resolved license tier
|
||||
export function identityTier(identity, row) {
|
||||
if (identity.kind === "cloud") {
|
||||
// Prepaid-period enforcement (self-serve subscriptions): once the paid
|
||||
// period lapses, the user is effectively Core until they renew. An
|
||||
// operator comp grant leaves subscription_expires_at null, so this is a
|
||||
// no-op for those — only dated purchases expire.
|
||||
if (isSubscriptionExpired(row)) return "core";
|
||||
return row?.tier_snapshot || "core";
|
||||
}
|
||||
return identity.license?.tier || "core";
|
||||
}
|
||||
|
||||
// True when a cloud user's paid period has a set expiry that's in the past.
|
||||
// Null/absent expiry = never expires (e.g. an operator comp grant).
|
||||
export function isSubscriptionExpired(row) {
|
||||
const exp = row?.subscription_expires_at;
|
||||
if (!exp) return false;
|
||||
const t = new Date(exp).getTime();
|
||||
return Number.isFinite(t) && t < Date.now();
|
||||
}
|
||||
Reference in New Issue
Block a user