mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-01 12:22:24 +03:00
Compare commits
2 Commits
docs/rever
...
feat/11655
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235235f31c | ||
|
|
806280ec2c |
@@ -0,0 +1,6 @@
|
||||
- Add a tenant-bound Video Bridge drill-down lifecycle on top of the existing secure cache
|
||||
substrate: opaque hashed handles (never raw session/video identifiers), preview/standard/detail
|
||||
multiresolution variants resampled on read, response pagination capped at 8 frames and 32 MiB,
|
||||
and a new authenticated `/api/v1/video-bridge/drilldown` consumer route that stays disabled for
|
||||
remote access by default and denies cross-key access with the same response as a nonexistent
|
||||
handle (no existence oracle).
|
||||
155
src/app/api/v1/video-bridge/drilldown/route.ts
Normal file
155
src/app/api/v1/video-bridge/drilldown/route.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
|
||||
import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy";
|
||||
import { extractApiKey, isValidApiKey } from "@/sse/services/auth";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
import {
|
||||
isVideoBridgeDrilldownRemoteAccessEnabled,
|
||||
VIDEO_DRILLDOWN_VARIANTS,
|
||||
VideoDrilldownLifecycle,
|
||||
type VideoDrilldownVariant,
|
||||
} from "@/lib/guardrails/videoBridgeDrilldownLifecycle";
|
||||
import { VideoDrilldownCache } from "@/lib/guardrails/videoBridgeDrilldown";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
export const revalidate = 0;
|
||||
|
||||
// A single process-wide lifecycle backs both this authenticated consumer route and any
|
||||
// future in-process producer (the video bridge guardrail, once it threads a per-request
|
||||
// opt-in flag through). It is intentionally a *different* cache instance from the internal
|
||||
// loopback broker route's — this route never accepts raw frames from a remote caller, only
|
||||
// opaque handles minted by an in-process producer, so there is no reason to share state with
|
||||
// the loopback-only extraction broker surface.
|
||||
const sharedLifecycle = new VideoDrilldownLifecycle({
|
||||
cache: new VideoDrilldownCache({
|
||||
maxEntries: 64,
|
||||
maxEntriesPerPrincipal: 16,
|
||||
maxBytesPerPrincipal: 64 * 1024 * 1024,
|
||||
maxTotalBytes: 256 * 1024 * 1024,
|
||||
ttlMs: 10 * 60 * 1000,
|
||||
}),
|
||||
});
|
||||
|
||||
const HandleSchema = z
|
||||
.string()
|
||||
.regex(/^[0-9a-f]{64}$/, "handle must be an opaque 64-character hex value");
|
||||
const VariantSchema = z.enum(VIDEO_DRILLDOWN_VARIANTS as [VideoDrilldownVariant, ...VideoDrilldownVariant[]]);
|
||||
const BoundedIntSchema = z
|
||||
.string()
|
||||
.regex(/^\d{1,9}$/)
|
||||
.transform(Number);
|
||||
const NonNegativeNumberSchema = z
|
||||
.string()
|
||||
.refine((value) => value.length > 0 && value.length <= 64 && Number.isFinite(Number(value)))
|
||||
.transform(Number)
|
||||
.refine((value) => value >= 0);
|
||||
|
||||
const ReadQuerySchema = z
|
||||
.object({
|
||||
end: NonNegativeNumberSchema.optional(),
|
||||
frames: BoundedIntSchema.pipe(z.number().int().min(1).max(8)).optional(),
|
||||
handle: HandleSchema,
|
||||
page: BoundedIntSchema.pipe(z.number().int().min(0)).optional(),
|
||||
start: NonNegativeNumberSchema.optional(),
|
||||
variant: VariantSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
const DeleteQuerySchema = z.object({ handle: HandleSchema }).strict();
|
||||
|
||||
function queryRecord(searchParams: URLSearchParams): Record<string, string> {
|
||||
const values: Record<string, string> = {};
|
||||
for (const [key, value] of searchParams) values[key] = value;
|
||||
return values;
|
||||
}
|
||||
|
||||
function corsJson(status: number, body: unknown): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { ...CORS_HEADERS, "Content-Type": "application/json", "Cache-Control": "no-store" },
|
||||
});
|
||||
}
|
||||
|
||||
function corsError(status: number, message: string, type: string): Response {
|
||||
return corsJson(status, buildErrorBody(status, message, null, { type }));
|
||||
}
|
||||
|
||||
export interface VideoBridgeDrilldownRouteDependencies {
|
||||
isRemoteAccessEnabled?: () => boolean;
|
||||
lifecycle?: VideoDrilldownLifecycle;
|
||||
}
|
||||
|
||||
async function resolvePrincipal(
|
||||
request: Request
|
||||
): Promise<{ error?: Response; principalId?: string }> {
|
||||
const apiKey = extractApiKey(request);
|
||||
if (!apiKey) {
|
||||
return { error: corsError(401, "Authentication is required", "authentication_required") };
|
||||
}
|
||||
if (!(await isValidApiKey(apiKey))) {
|
||||
return { error: corsError(401, "The provided API key is invalid", "authentication_required") };
|
||||
}
|
||||
const policy = await enforceApiKeyPolicy(request, null);
|
||||
if (policy.rejection) return { error: policy.rejection };
|
||||
const principalId = policy.apiKeyInfo?.id;
|
||||
if (!principalId) {
|
||||
return { error: corsError(401, "Authentication is required", "authentication_required") };
|
||||
}
|
||||
return { principalId };
|
||||
}
|
||||
|
||||
export const OPTIONS = async (): Promise<Response> => handleCorsOptions();
|
||||
|
||||
export async function handleVideoBridgeDrilldownConsumerRequest(
|
||||
request: Request,
|
||||
dependencies: VideoBridgeDrilldownRouteDependencies = {}
|
||||
): Promise<Response> {
|
||||
const isRemoteAccessEnabled =
|
||||
dependencies.isRemoteAccessEnabled ?? isVideoBridgeDrilldownRemoteAccessEnabled;
|
||||
if (!isRemoteAccessEnabled()) {
|
||||
return corsError(
|
||||
403,
|
||||
"Video Bridge drill-down remote access is disabled",
|
||||
"feature_disabled"
|
||||
);
|
||||
}
|
||||
if (request.method !== "GET" && request.method !== "DELETE") {
|
||||
return corsError(405, "Method not allowed", "invalid_request");
|
||||
}
|
||||
const resolved = await resolvePrincipal(request);
|
||||
if (resolved.error) return resolved.error;
|
||||
const principalId = resolved.principalId!;
|
||||
const lifecycle = dependencies.lifecycle ?? sharedLifecycle;
|
||||
const url = new URL(request.url);
|
||||
const query = queryRecord(url.searchParams);
|
||||
|
||||
if (request.method === "DELETE") {
|
||||
const parsed = DeleteQuerySchema.safeParse(query);
|
||||
if (!parsed.success) return corsError(400, "A valid drill-down handle is required", "invalid_request");
|
||||
const removed = lifecycle.deleteHandle(principalId, parsed.data.handle);
|
||||
return corsJson(200, { removed });
|
||||
}
|
||||
|
||||
const parsed = ReadQuerySchema.safeParse(query);
|
||||
if (!parsed.success) return corsError(400, "Invalid Video Bridge drill-down query", "invalid_request");
|
||||
const page = await lifecycle.resolve(principalId, parsed.data.handle, {
|
||||
endSeconds: parsed.data.end,
|
||||
frameCount: parsed.data.frames,
|
||||
page: parsed.data.page,
|
||||
startSeconds: parsed.data.start,
|
||||
variant: parsed.data.variant,
|
||||
});
|
||||
if (!page) {
|
||||
return corsError(404, "Video Bridge drill-down result was not found", "not_found");
|
||||
}
|
||||
return corsJson(200, page);
|
||||
}
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
return handleVideoBridgeDrilldownConsumerRequest(request);
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request): Promise<Response> {
|
||||
return handleVideoBridgeDrilldownConsumerRequest(request);
|
||||
}
|
||||
401
src/lib/guardrails/videoBridgeDrilldownLifecycle.ts
Normal file
401
src/lib/guardrails/videoBridgeDrilldownLifecycle.ts
Normal file
@@ -0,0 +1,401 @@
|
||||
// Video Bridge drill-down production/consumption lifecycle (FU-08, #11655).
|
||||
//
|
||||
// `videoBridgeDrilldown.ts` is the secure cache substrate: it stores frames keyed by
|
||||
// (principalId, sessionId, videoRef) and already refuses cross-principal reads/deletes
|
||||
// without revealing whether an entry exists. This module adds the missing lifecycle on
|
||||
// top of it, without touching the frozen-shape substrate file:
|
||||
// - opaque hashed handles, so a consumer never needs (and never sees) the raw
|
||||
// sessionId/videoRef the substrate indexes by — both are minted server-side here;
|
||||
// - preview/standard/detail multiresolution variants, resampled on read (never stored
|
||||
// more than once, so producing stays "zero overhead" beyond the opt-in call itself);
|
||||
// - response pagination capped at 8 frames and a bounded response-byte budget;
|
||||
// - a small handle registry with its own TTL/quota, cleaned up alongside the cache.
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
|
||||
import sharp from "sharp";
|
||||
|
||||
import {
|
||||
VIDEO_DRILLDOWN_MAX_ENTRY_BYTES,
|
||||
VideoDrilldownCache,
|
||||
type VideoDrilldownCacheOptions,
|
||||
type VideoDrilldownFrame,
|
||||
type VideoDrilldownPutValue,
|
||||
type VideoDrilldownResult,
|
||||
} from "./videoBridgeDrilldown";
|
||||
|
||||
export type VideoDrilldownVariant = "preview" | "standard" | "detail";
|
||||
|
||||
export const VIDEO_DRILLDOWN_VARIANTS: readonly VideoDrilldownVariant[] = [
|
||||
"preview",
|
||||
"standard",
|
||||
"detail",
|
||||
];
|
||||
|
||||
export interface VideoDrilldownVariantPreset {
|
||||
/** Frames are only ever shrunk toward this ceiling, never upscaled. */
|
||||
maxDimension: number;
|
||||
/** Default page size for this variant when the caller does not ask for a specific count. */
|
||||
defaultPageFrames: number;
|
||||
}
|
||||
|
||||
export const VIDEO_DRILLDOWN_VARIANT_PRESETS: Record<
|
||||
VideoDrilldownVariant,
|
||||
VideoDrilldownVariantPreset
|
||||
> = {
|
||||
preview: { maxDimension: 320, defaultPageFrames: 3 },
|
||||
standard: { maxDimension: 640, defaultPageFrames: 6 },
|
||||
detail: { maxDimension: 1280, defaultPageFrames: 8 },
|
||||
};
|
||||
|
||||
export const VIDEO_DRILLDOWN_MAX_PAGE_FRAMES = 8;
|
||||
export const VIDEO_DRILLDOWN_MAX_PAGE_BYTES = VIDEO_DRILLDOWN_MAX_ENTRY_BYTES;
|
||||
|
||||
const HANDLE_PATTERN = /^[0-9a-f]{64}$/;
|
||||
|
||||
function truthyFlag(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
||||
}
|
||||
|
||||
/** Producing drill-down artifacts stays off unless an operator opts the deployment in. */
|
||||
export function isVideoBridgeDrilldownProductionEnabled(
|
||||
env: Record<string, string | undefined> = process.env
|
||||
): boolean {
|
||||
return truthyFlag(env.OMNIROUTE_VIDEO_BRIDGE_DRILLDOWN_ENABLED);
|
||||
}
|
||||
|
||||
/** Remote (authenticated API-key) consumption stays off unless an operator opts in. */
|
||||
export function isVideoBridgeDrilldownRemoteAccessEnabled(
|
||||
env: Record<string, string | undefined> = process.env
|
||||
): boolean {
|
||||
return truthyFlag(env.OMNIROUTE_VIDEO_BRIDGE_DRILLDOWN_REMOTE_ENABLED);
|
||||
}
|
||||
|
||||
interface HandleEntry {
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
principalId: string;
|
||||
principalKey: string;
|
||||
sessionId: string;
|
||||
videoRef: string;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownProducePayload {
|
||||
derivation: VideoDrilldownPutValue["derivation"];
|
||||
durationSeconds: number;
|
||||
frames: readonly VideoDrilldownPutValue["frames"][number][];
|
||||
}
|
||||
|
||||
export interface VideoDrilldownProduceOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownProduceResult {
|
||||
expiresAt: number;
|
||||
handle: string;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownResolveQuery {
|
||||
endSeconds?: number;
|
||||
frameCount?: number;
|
||||
/** Test/defense-in-depth override; production callers use the exported constant. */
|
||||
maxPageBytes?: number;
|
||||
page?: number;
|
||||
startSeconds?: number;
|
||||
variant?: VideoDrilldownVariant;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownPage {
|
||||
derivation: VideoDrilldownResult["derivation"];
|
||||
durationSeconds: number;
|
||||
focusWindow?: VideoDrilldownResult["focusWindow"];
|
||||
frames: VideoDrilldownFrame[];
|
||||
hasMore: boolean;
|
||||
page: number;
|
||||
variant: VideoDrilldownVariant;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownUsage {
|
||||
bytes: number;
|
||||
entries: number;
|
||||
totalBytes: number;
|
||||
totalEntries: number;
|
||||
}
|
||||
|
||||
export interface VideoDrilldownLifecycleOptions {
|
||||
cache: VideoDrilldownCache;
|
||||
maxHandles?: number;
|
||||
maxHandlesPerPrincipal?: number;
|
||||
now?: () => number;
|
||||
/** Handle bookkeeping TTL. Defaults to the cache's own TTL when it is exposed via `ttlMs`. */
|
||||
ttlMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the per-principal bookkeeping key from an API-key RECORD ID (`apiKeyInfo.id`),
|
||||
* never from the secret itself. SHA-256 is the correct primitive here and a password KDF
|
||||
* would be wrong: this value is a deterministic index key that must be recomputable on
|
||||
* every lookup, not a stored credential verifier. Same construction and rationale as
|
||||
* `src/lib/db/apiKeys.ts` and `videoBridgeDrilldown.ts`; CodeQL/semgrep flag the
|
||||
* `sha256(<something reached from an apiKey-shaped source>)` shape generically and cannot
|
||||
* see that the input is an opaque row id (issue #11655, Hard Rule #14).
|
||||
*/
|
||||
function principalDigest(principalId: string): string {
|
||||
// nosemgrep: insufficient-password-hash
|
||||
return createHash("sha256").update(principalId, "utf8").digest("hex");
|
||||
}
|
||||
|
||||
function frameDataUriBytes(frame: VideoDrilldownFrame): number {
|
||||
const commaIndex = frame.dataUri.indexOf(",");
|
||||
const encoded = commaIndex >= 0 ? frame.dataUri.slice(commaIndex + 1) : "";
|
||||
return Math.floor((encoded.length * 3) / 4);
|
||||
}
|
||||
|
||||
async function shrinkFrameForVariant(
|
||||
frame: VideoDrilldownFrame,
|
||||
preset: VideoDrilldownVariantPreset
|
||||
): Promise<VideoDrilldownFrame> {
|
||||
if (frame.width <= preset.maxDimension && frame.height <= preset.maxDimension) return frame;
|
||||
const commaIndex = frame.dataUri.indexOf(",");
|
||||
const source = Buffer.from(frame.dataUri.slice(commaIndex + 1), "base64");
|
||||
const resized = await sharp(source)
|
||||
.resize({
|
||||
fit: "inside",
|
||||
height: preset.maxDimension,
|
||||
width: preset.maxDimension,
|
||||
withoutEnlargement: true,
|
||||
})
|
||||
.jpeg({ progressive: false })
|
||||
.toBuffer();
|
||||
const metadata = await sharp(resized).metadata();
|
||||
return {
|
||||
dataUri: `data:image/jpeg;base64,${resized.toString("base64")}`,
|
||||
height: metadata.height ?? frame.height,
|
||||
timestampSeconds: frame.timestampSeconds,
|
||||
width: metadata.width ?? frame.width,
|
||||
};
|
||||
}
|
||||
|
||||
function pageFrameSlice(
|
||||
frames: readonly VideoDrilldownFrame[],
|
||||
page: number,
|
||||
requestedFrameCount: number | undefined,
|
||||
variant: VideoDrilldownVariant
|
||||
): { hasMore: boolean; slice: readonly VideoDrilldownFrame[] } {
|
||||
const pageSize = Math.max(
|
||||
1,
|
||||
Math.min(
|
||||
requestedFrameCount ?? VIDEO_DRILLDOWN_VARIANT_PRESETS[variant].defaultPageFrames,
|
||||
VIDEO_DRILLDOWN_MAX_PAGE_FRAMES
|
||||
)
|
||||
);
|
||||
const start = Math.max(0, page) * pageSize;
|
||||
const slice = frames.slice(start, start + pageSize);
|
||||
return { hasMore: start + slice.length < frames.length, slice };
|
||||
}
|
||||
|
||||
function trimToByteBudget(
|
||||
frames: readonly VideoDrilldownFrame[],
|
||||
maxBytes: number
|
||||
): { frames: VideoDrilldownFrame[]; trimmed: boolean } {
|
||||
const kept: VideoDrilldownFrame[] = [];
|
||||
let total = 0;
|
||||
for (const frame of frames) {
|
||||
const bytes = frameDataUriBytes(frame);
|
||||
if (kept.length > 0 && total + bytes > maxBytes) break;
|
||||
kept.push(frame);
|
||||
total += bytes;
|
||||
}
|
||||
// A single oversized frame is still returned alone rather than producing an empty page —
|
||||
// the underlying cache already enforces the 32 MiB per-entry ceiling, so this only trims
|
||||
// multi-frame pages down.
|
||||
return { frames: kept.length > 0 ? kept : frames.slice(0, 1), trimmed: kept.length < frames.length };
|
||||
}
|
||||
|
||||
/**
|
||||
* Lifecycle wrapper around {@link VideoDrilldownCache}: mints opaque handles at produce
|
||||
* time, resolves/deletes strictly by (principal, handle), and applies multiresolution
|
||||
* variants + pagination at read time.
|
||||
*/
|
||||
export class VideoDrilldownLifecycle {
|
||||
private readonly cache: VideoDrilldownCache;
|
||||
private readonly handles = new Map<string, HandleEntry>();
|
||||
private readonly handleCountByPrincipal = new Map<string, number>();
|
||||
private readonly maxHandles: number;
|
||||
private readonly maxHandlesPerPrincipal: number;
|
||||
private readonly now: () => number;
|
||||
private readonly ttlMs: number;
|
||||
|
||||
constructor(options: VideoDrilldownLifecycleOptions) {
|
||||
this.cache = options.cache;
|
||||
this.now = options.now ?? Date.now;
|
||||
this.ttlMs = options.ttlMs ?? 10 * 60 * 1000;
|
||||
this.maxHandles = options.maxHandles ?? 4096;
|
||||
this.maxHandlesPerPrincipal = options.maxHandlesPerPrincipal ?? 64;
|
||||
}
|
||||
|
||||
private sweepExpiredHandles(): number {
|
||||
const now = this.now();
|
||||
let removed = 0;
|
||||
for (const [handle, entry] of this.handles) {
|
||||
if (entry.expiresAt <= now) {
|
||||
this.dropHandle(handle, entry);
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
return removed;
|
||||
}
|
||||
|
||||
private dropHandle(handle: string, entry: HandleEntry): void {
|
||||
this.handles.delete(handle);
|
||||
const count = this.handleCountByPrincipal.get(entry.principalKey) ?? 0;
|
||||
if (count <= 1) this.handleCountByPrincipal.delete(entry.principalKey);
|
||||
else this.handleCountByPrincipal.set(entry.principalKey, count - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* A handle evicted for quota is unreachable forever (its digest can never be re-derived
|
||||
* without the minted sessionId/videoRef), so its cache entry must be released here too —
|
||||
* otherwise it would sit as unreclaimable, invisible quota usage until TTL expiry.
|
||||
*/
|
||||
private releaseEvictedHandle(handle: string, entry: HandleEntry): void {
|
||||
this.cache.clearSession(entry.principalId, entry.sessionId);
|
||||
this.dropHandle(handle, entry);
|
||||
}
|
||||
|
||||
private evictOldestHandleForPrincipal(principalKey: string): void {
|
||||
for (const [handle, entry] of this.handles) {
|
||||
if (entry.principalKey === principalKey) {
|
||||
this.releaseEvictedHandle(handle, entry);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private evictOldestHandleGlobally(): void {
|
||||
const oldest = this.handles.keys().next().value;
|
||||
if (!oldest) return;
|
||||
const entry = this.handles.get(oldest);
|
||||
if (entry) this.releaseEvictedHandle(oldest, entry);
|
||||
}
|
||||
|
||||
private registerHandle(principalId: string, sessionId: string, videoRef: string): string {
|
||||
const principalKey = principalDigest(principalId);
|
||||
const createdAt = this.now();
|
||||
const handle = createHash("sha256")
|
||||
.update(principalKey, "utf8")
|
||||
.update(":", "utf8")
|
||||
.update(sessionId, "utf8")
|
||||
.update(":", "utf8")
|
||||
.update(videoRef, "utf8")
|
||||
.digest("hex");
|
||||
this.handles.set(handle, {
|
||||
createdAt,
|
||||
expiresAt: createdAt + this.ttlMs,
|
||||
principalId,
|
||||
principalKey,
|
||||
sessionId,
|
||||
videoRef,
|
||||
});
|
||||
const count = (this.handleCountByPrincipal.get(principalKey) ?? 0) + 1;
|
||||
this.handleCountByPrincipal.set(principalKey, count);
|
||||
while ((this.handleCountByPrincipal.get(principalKey) ?? 0) > this.maxHandlesPerPrincipal) {
|
||||
const before = this.handles.size;
|
||||
this.evictOldestHandleForPrincipal(principalKey);
|
||||
if (this.handles.size === before) break;
|
||||
}
|
||||
while (this.handles.size > this.maxHandles) {
|
||||
const before = this.handles.size;
|
||||
this.evictOldestHandleGlobally();
|
||||
if (this.handles.size === before) break;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
private resolveHandleEntry(principalId: string, handle: string): HandleEntry | null {
|
||||
if (!HANDLE_PATTERN.test(handle)) return null;
|
||||
this.sweepExpiredHandles();
|
||||
const entry = this.handles.get(handle);
|
||||
if (!entry || entry.principalKey !== principalDigest(principalId)) return null;
|
||||
return entry;
|
||||
}
|
||||
|
||||
async produce(
|
||||
principalId: string,
|
||||
value: VideoDrilldownProducePayload,
|
||||
options: VideoDrilldownProduceOptions = {}
|
||||
): Promise<VideoDrilldownProduceResult> {
|
||||
this.sweepExpiredHandles();
|
||||
const sessionId = randomUUID();
|
||||
const videoRef = randomUUID();
|
||||
await this.cache.put(principalId, sessionId, videoRef, value as VideoDrilldownPutValue, {
|
||||
signal: options.signal,
|
||||
});
|
||||
const handle = this.registerHandle(principalId, sessionId, videoRef);
|
||||
const entry = this.handles.get(handle);
|
||||
return { expiresAt: entry?.expiresAt ?? this.now() + this.ttlMs, handle };
|
||||
}
|
||||
|
||||
async resolve(
|
||||
principalId: string,
|
||||
handle: string,
|
||||
query: VideoDrilldownResolveQuery
|
||||
): Promise<VideoDrilldownPage | null> {
|
||||
const entry = this.resolveHandleEntry(principalId, handle);
|
||||
if (!entry) return null;
|
||||
const stored = this.cache.get(principalId, entry.sessionId, entry.videoRef, {
|
||||
endSeconds: query.endSeconds,
|
||||
frameCount: 16,
|
||||
startSeconds: query.startSeconds,
|
||||
});
|
||||
if (!stored) {
|
||||
this.dropHandle(handle, entry);
|
||||
return null;
|
||||
}
|
||||
const variant = query.variant ?? "detail";
|
||||
const preset = VIDEO_DRILLDOWN_VARIANT_PRESETS[variant];
|
||||
const { slice, hasMore } = pageFrameSlice(
|
||||
stored.frames,
|
||||
query.page ?? 0,
|
||||
query.frameCount,
|
||||
variant
|
||||
);
|
||||
const shrunk = await Promise.all(slice.map((frame) => shrinkFrameForVariant(frame, preset)));
|
||||
const { frames, trimmed } = trimToByteBudget(shrunk, query.maxPageBytes ?? VIDEO_DRILLDOWN_MAX_PAGE_BYTES);
|
||||
return {
|
||||
derivation: stored.derivation,
|
||||
durationSeconds: stored.durationSeconds,
|
||||
...(stored.focusWindow ? { focusWindow: stored.focusWindow } : {}),
|
||||
frames,
|
||||
hasMore: hasMore || trimmed,
|
||||
page: Math.max(0, query.page ?? 0),
|
||||
variant,
|
||||
};
|
||||
}
|
||||
|
||||
deleteHandle(principalId: string, handle: string): number {
|
||||
const entry = this.resolveHandleEntry(principalId, handle);
|
||||
if (!entry) return 0;
|
||||
const removed = this.cache.clearSession(principalId, entry.sessionId);
|
||||
this.dropHandle(handle, entry);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/** Explicit sweep for scheduled cleanup; returns the number of stale handles reclaimed. */
|
||||
cleanup(): number {
|
||||
return this.sweepExpiredHandles();
|
||||
}
|
||||
|
||||
getUsage(principalId: string): VideoDrilldownUsage {
|
||||
return this.cache.getUsage(principalId);
|
||||
}
|
||||
|
||||
clearAll(): void {
|
||||
this.cache.clearAll();
|
||||
this.handles.clear();
|
||||
this.handleCountByPrincipal.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export type { VideoDrilldownCacheOptions };
|
||||
@@ -393,6 +393,7 @@
|
||||
"tests/unit/usage-service-hardening.test.ts",
|
||||
"tests/unit/validate-response-quality.test.ts",
|
||||
"tests/unit/vertex-passthrough-model-lockout.test.ts",
|
||||
"tests/unit/video-bridge-drilldown-consumer-route.test.ts",
|
||||
"tests/unit/video-bridge-drilldown-route.test.ts",
|
||||
"tests/unit/video-bridge-route-security.test.ts",
|
||||
"tests/unit/xai-agent-tools-passthrough.test.ts",
|
||||
|
||||
271
tests/unit/guardrails/videoBridgeDrilldownLifecycle.test.ts
Normal file
271
tests/unit/guardrails/videoBridgeDrilldownLifecycle.test.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import sharp from "sharp";
|
||||
|
||||
import { VideoDrilldownCache } from "../../../src/lib/guardrails/videoBridgeDrilldown";
|
||||
import {
|
||||
isVideoBridgeDrilldownProductionEnabled,
|
||||
isVideoBridgeDrilldownRemoteAccessEnabled,
|
||||
VIDEO_DRILLDOWN_MAX_PAGE_FRAMES,
|
||||
VIDEO_DRILLDOWN_VARIANT_PRESETS,
|
||||
VideoDrilldownLifecycle,
|
||||
} from "../../../src/lib/guardrails/videoBridgeDrilldownLifecycle";
|
||||
|
||||
const derivation = {
|
||||
parentContentHash: `sha256:${"a".repeat(64)}`,
|
||||
policy: "focused-window",
|
||||
version: "video-drilldown/v1",
|
||||
} as const;
|
||||
|
||||
const jpegFixtures = new Map<string, Buffer>();
|
||||
async function jpeg(width: number, height: number, fill: number): Promise<Buffer> {
|
||||
const key = `${width}x${height}x${fill}`;
|
||||
const cached = jpegFixtures.get(key);
|
||||
if (cached) return cached;
|
||||
const buffer = await sharp({
|
||||
create: { width, height, channels: 3, background: { r: fill, g: fill, b: fill } },
|
||||
})
|
||||
.jpeg({ progressive: false })
|
||||
.toBuffer();
|
||||
jpegFixtures.set(key, buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
async function jpegDataUri(width: number, height: number, fill = 1): Promise<string> {
|
||||
const buffer = await jpeg(width, height, fill);
|
||||
return `data:image/jpeg;base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
function framesInput(
|
||||
entries: ReadonlyArray<{ dataUri: string; timestampSeconds: number }>
|
||||
): { derivation: typeof derivation; durationSeconds: number; frames: typeof entries } {
|
||||
return { derivation, durationSeconds: 20, frames: entries };
|
||||
}
|
||||
|
||||
test("video bridge drill-down feature flags default to opt-in / disabled", () => {
|
||||
assert.equal(isVideoBridgeDrilldownProductionEnabled({}), false);
|
||||
assert.equal(isVideoBridgeDrilldownProductionEnabled({ OMNIROUTE_VIDEO_BRIDGE_DRILLDOWN_ENABLED: "true" }), true);
|
||||
assert.equal(isVideoBridgeDrilldownRemoteAccessEnabled({}), false);
|
||||
assert.equal(
|
||||
isVideoBridgeDrilldownRemoteAccessEnabled({
|
||||
OMNIROUTE_VIDEO_BRIDGE_DRILLDOWN_REMOTE_ENABLED: "1",
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("produce mints an opaque hashed handle that reveals no session/video identity", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }) });
|
||||
const { handle, expiresAt } = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(640, 360), timestampSeconds: 1 }])
|
||||
);
|
||||
assert.match(handle, /^[0-9a-f]{64}$/);
|
||||
assert.ok(expiresAt > Date.now());
|
||||
});
|
||||
|
||||
test("resolve returns stored frames to the owning principal by handle only", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }) });
|
||||
const { handle } = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(640, 360), timestampSeconds: 3 }])
|
||||
);
|
||||
const page = await lifecycle.resolve("principal-a", handle, {});
|
||||
assert.ok(page);
|
||||
assert.equal(page?.frames.length, 1);
|
||||
assert.equal(page?.variant, "detail");
|
||||
assert.equal(page?.hasMore, false);
|
||||
});
|
||||
|
||||
test("resolve denies a wrong principal and a nonexistent handle identically (no oracle)", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }) });
|
||||
const { handle } = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(640, 360), timestampSeconds: 3 }])
|
||||
);
|
||||
const wrongPrincipal = await lifecycle.resolve("principal-b", handle, {});
|
||||
const madeUpHandle = await lifecycle.resolve("principal-b", "f".repeat(64), {});
|
||||
assert.equal(wrongPrincipal, null);
|
||||
assert.equal(madeUpHandle, null);
|
||||
|
||||
const deniedDelete = lifecycle.deleteHandle("principal-b", handle);
|
||||
const madeUpDelete = lifecycle.deleteHandle("principal-b", "f".repeat(64));
|
||||
assert.equal(deniedDelete, 0);
|
||||
assert.equal(madeUpDelete, 0);
|
||||
|
||||
const ownerStillWorks = await lifecycle.resolve("principal-a", handle, {});
|
||||
assert.ok(ownerStillWorks);
|
||||
});
|
||||
|
||||
test("delete removes the artifact for its own principal and is idempotent", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }) });
|
||||
const { handle } = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(640, 360), timestampSeconds: 3 }])
|
||||
);
|
||||
assert.equal(lifecycle.deleteHandle("principal-a", handle), 1);
|
||||
assert.equal(await lifecycle.resolve("principal-a", handle, {}), null);
|
||||
assert.equal(lifecycle.deleteHandle("principal-a", handle), 0);
|
||||
});
|
||||
|
||||
test("preview and standard variants shrink frames without ever upscaling", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }) });
|
||||
const big = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(1280, 720), timestampSeconds: 1 }])
|
||||
);
|
||||
const small = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(200, 100), timestampSeconds: 2 }])
|
||||
);
|
||||
|
||||
const preview = await lifecycle.resolve("principal-a", big.handle, { variant: "preview" });
|
||||
assert.ok(preview);
|
||||
for (const frame of preview?.frames ?? []) {
|
||||
assert.ok(frame.width <= VIDEO_DRILLDOWN_VARIANT_PRESETS.preview.maxDimension);
|
||||
assert.ok(frame.height <= VIDEO_DRILLDOWN_VARIANT_PRESETS.preview.maxDimension);
|
||||
}
|
||||
|
||||
// The already-small 200x100 frame must not be upscaled toward the preview ceiling.
|
||||
const smallPreview = await lifecycle.resolve("principal-a", small.handle, { variant: "preview" });
|
||||
const smallFrame = smallPreview?.frames[0];
|
||||
assert.deepEqual(
|
||||
smallFrame && { height: smallFrame.height, width: smallFrame.width },
|
||||
{ height: 100, width: 200 }
|
||||
);
|
||||
|
||||
const standard = await lifecycle.resolve("principal-a", big.handle, { variant: "standard" });
|
||||
for (const frame of standard?.frames ?? []) {
|
||||
assert.ok(frame.width <= VIDEO_DRILLDOWN_VARIANT_PRESETS.standard.maxDimension);
|
||||
assert.ok(frame.height <= VIDEO_DRILLDOWN_VARIANT_PRESETS.standard.maxDimension);
|
||||
}
|
||||
|
||||
const detail = await lifecycle.resolve("principal-a", big.handle, { variant: "detail" });
|
||||
const detailBig = detail?.frames[0];
|
||||
assert.deepEqual(
|
||||
detailBig && { height: detailBig.height, width: detailBig.width },
|
||||
{ height: 720, width: 1280 }
|
||||
);
|
||||
});
|
||||
|
||||
test("pagination never exceeds the 8-frame page cap and reports hasMore across pages", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }) });
|
||||
const entries = [] as Array<{ dataUri: string; timestampSeconds: number }>;
|
||||
for (let index = 0; index < 12; index += 1) {
|
||||
entries.push({ dataUri: await jpegDataUri(64, 64, (index % 250) + 1), timestampSeconds: index });
|
||||
}
|
||||
const { handle } = await lifecycle.produce("principal-a", framesInput(entries.slice(0, 16)));
|
||||
|
||||
const firstPage = await lifecycle.resolve("principal-a", handle, { frameCount: 100 });
|
||||
assert.ok(firstPage);
|
||||
assert.ok((firstPage?.frames.length ?? 0) <= VIDEO_DRILLDOWN_MAX_PAGE_FRAMES);
|
||||
assert.equal(firstPage?.hasMore, entries.length > VIDEO_DRILLDOWN_MAX_PAGE_FRAMES);
|
||||
|
||||
const secondPage = await lifecycle.resolve("principal-a", handle, { page: 1 });
|
||||
assert.ok(secondPage);
|
||||
const seenTimestamps = new Set([
|
||||
...(firstPage?.frames.map((frame) => frame.timestampSeconds) ?? []),
|
||||
...(secondPage?.frames.map((frame) => frame.timestampSeconds) ?? []),
|
||||
]);
|
||||
assert.equal(seenTimestamps.size, entries.length);
|
||||
});
|
||||
|
||||
test("a tight page byte budget trims frames instead of exceeding it", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({
|
||||
cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }),
|
||||
});
|
||||
const frameBytes = (await jpeg(320, 180, 5)).byteLength;
|
||||
const { handle } = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([
|
||||
{ dataUri: await jpegDataUri(320, 180, 5), timestampSeconds: 1 },
|
||||
{ dataUri: await jpegDataUri(320, 180, 5), timestampSeconds: 2 },
|
||||
{ dataUri: await jpegDataUri(320, 180, 5), timestampSeconds: 3 },
|
||||
])
|
||||
);
|
||||
const tight = await lifecycle.resolve("principal-a", handle, {
|
||||
maxPageBytes: Math.floor(frameBytes * 1.5),
|
||||
});
|
||||
assert.ok(tight);
|
||||
assert.equal(tight?.frames.length, 1);
|
||||
assert.equal(tight?.hasMore, true);
|
||||
});
|
||||
|
||||
test("TTL expiry hides the artifact and cleanup reclaims the handle", async () => {
|
||||
let now = 1_000;
|
||||
const lifecycle = new VideoDrilldownLifecycle({
|
||||
cache: new VideoDrilldownCache({ maxEntries: 8, now: () => now, ttlMs: 5_000 }),
|
||||
now: () => now,
|
||||
ttlMs: 5_000,
|
||||
});
|
||||
const { handle } = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 1 }])
|
||||
);
|
||||
now += 10_000;
|
||||
// cleanup() alone (no prior resolve/delete call) must reclaim the stale handle.
|
||||
const removed = lifecycle.cleanup();
|
||||
assert.ok(removed >= 1);
|
||||
assert.equal(await lifecycle.resolve("principal-a", handle, {}), null);
|
||||
assert.equal(lifecycle.deleteHandle("principal-a", handle), 0);
|
||||
});
|
||||
|
||||
test("per-principal and global usage accounting is delegated to the cache substrate", async () => {
|
||||
const cache = new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 });
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache });
|
||||
assert.deepEqual(lifecycle.getUsage("principal-a"), {
|
||||
bytes: 0,
|
||||
entries: 0,
|
||||
totalBytes: 0,
|
||||
totalEntries: 0,
|
||||
});
|
||||
await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 1 }])
|
||||
);
|
||||
const usage = lifecycle.getUsage("principal-a");
|
||||
assert.equal(usage.entries, 1);
|
||||
assert.ok(usage.bytes > 0);
|
||||
assert.equal(usage.totalEntries, 1);
|
||||
});
|
||||
|
||||
test("aborting mid-production leaves no orphaned handle or cache usage", async () => {
|
||||
const cache = new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 });
|
||||
const lifecycle = new VideoDrilldownLifecycle({ cache });
|
||||
const controller = new AbortController();
|
||||
controller.abort();
|
||||
const dataUri = await jpegDataUri(320, 180);
|
||||
await assert.rejects(() =>
|
||||
lifecycle.produce("principal-a", framesInput([{ dataUri, timestampSeconds: 1 }]), {
|
||||
signal: controller.signal,
|
||||
})
|
||||
);
|
||||
assert.deepEqual(lifecycle.getUsage("principal-a"), {
|
||||
bytes: 0,
|
||||
entries: 0,
|
||||
totalBytes: 0,
|
||||
totalEntries: 0,
|
||||
});
|
||||
});
|
||||
|
||||
test("per-principal handle quota evicts the oldest handle instead of growing without bound", async () => {
|
||||
const lifecycle = new VideoDrilldownLifecycle({
|
||||
cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }),
|
||||
maxHandlesPerPrincipal: 2,
|
||||
});
|
||||
const first = await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 1 }])
|
||||
);
|
||||
await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 2 }])
|
||||
);
|
||||
await lifecycle.produce(
|
||||
"principal-a",
|
||||
framesInput([{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 3 }])
|
||||
);
|
||||
assert.equal(await lifecycle.resolve("principal-a", first.handle, {}), null);
|
||||
assert.equal(lifecycle.getUsage("principal-a").entries, 2);
|
||||
});
|
||||
214
tests/unit/video-bridge-drilldown-consumer-route.test.ts
Normal file
214
tests/unit/video-bridge-drilldown-consumer-route.test.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import sharp from "sharp";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(
|
||||
path.join(os.tmpdir(), "omniroute-video-drilldown-consumer-route-")
|
||||
);
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
process.env.API_KEY_SECRET = "video-drilldown-consumer-route-test-secret";
|
||||
process.env.DISABLE_SQLITE_AUTO_BACKUP = "true";
|
||||
|
||||
const core = await import("../../src/lib/db/core.ts");
|
||||
const apiKeysDb = await import("../../src/lib/db/apiKeys.ts");
|
||||
const route = await import("../../src/app/api/v1/video-bridge/drilldown/route.ts");
|
||||
const { VideoDrilldownLifecycle } = await import(
|
||||
"../../src/lib/guardrails/videoBridgeDrilldownLifecycle.ts"
|
||||
);
|
||||
const { VideoDrilldownCache } = await import("../../src/lib/guardrails/videoBridgeDrilldown.ts");
|
||||
const { isLocalOnlyPath } = await import("../../src/server/authz/routeGuard.ts");
|
||||
|
||||
async function resetStorage(): Promise<void> {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
test.beforeEach(resetStorage);
|
||||
test.after(() => {
|
||||
core.resetDbInstance();
|
||||
apiKeysDb.resetApiKeyState();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
|
||||
});
|
||||
|
||||
async function seedKey(): Promise<{ key: string }> {
|
||||
return apiKeysDb.createApiKey("video-drilldown-route-key", "test", []);
|
||||
}
|
||||
|
||||
function newLifecycle() {
|
||||
return new VideoDrilldownLifecycle({
|
||||
cache: new VideoDrilldownCache({ maxEntries: 8, ttlMs: 60_000 }),
|
||||
});
|
||||
}
|
||||
|
||||
async function jpegDataUri(width: number, height: number, fill = 1): Promise<string> {
|
||||
const buffer = await sharp({
|
||||
create: { width, height, channels: 3, background: { r: fill, g: fill, b: fill } },
|
||||
})
|
||||
.jpeg({ progressive: false })
|
||||
.toBuffer();
|
||||
return `data:image/jpeg;base64,${buffer.toString("base64")}`;
|
||||
}
|
||||
|
||||
const derivation = {
|
||||
parentContentHash: `sha256:${"a".repeat(64)}`,
|
||||
policy: "focused-window",
|
||||
version: "video-drilldown/v1",
|
||||
} as const;
|
||||
|
||||
function get(url: string, key?: string): Request {
|
||||
return new Request(`http://omniroute.local${url}`, {
|
||||
headers: key ? { Authorization: `Bearer ${key}` } : {},
|
||||
});
|
||||
}
|
||||
|
||||
function del(url: string, key?: string): Request {
|
||||
return new Request(`http://omniroute.local${url}`, {
|
||||
headers: key ? { Authorization: `Bearer ${key}` } : {},
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
||||
|
||||
test("consumer route requires remote access to be explicitly enabled", async () => {
|
||||
const { key } = await seedKey();
|
||||
const response = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${"a".repeat(64)}`, key),
|
||||
{ isRemoteAccessEnabled: () => false }
|
||||
);
|
||||
assert.equal(response.status, 403);
|
||||
});
|
||||
|
||||
test("consumer route requires an authenticated API key", async () => {
|
||||
const noKey = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${"a".repeat(64)}`),
|
||||
{ isRemoteAccessEnabled: () => true }
|
||||
);
|
||||
assert.equal(noKey.status, 401);
|
||||
|
||||
const invalidKey = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${"a".repeat(64)}`, "not-a-real-key"),
|
||||
{ isRemoteAccessEnabled: () => true }
|
||||
);
|
||||
assert.equal(invalidKey.status, 401);
|
||||
});
|
||||
|
||||
test("consumer route resolves a produced handle for its own API key and paginates/variant-shapes it", async () => {
|
||||
const { key } = await seedKey();
|
||||
const lifecycle = newLifecycle();
|
||||
const policyModule = await import("../../src/shared/utils/apiKeyPolicy.ts");
|
||||
const policy = await policyModule.enforceApiKeyPolicy(get("/x", key), null);
|
||||
const principalId = policy.apiKeyInfo!.id;
|
||||
const { handle } = await lifecycle.produce(principalId, {
|
||||
derivation,
|
||||
durationSeconds: 10,
|
||||
frames: [
|
||||
{ dataUri: await jpegDataUri(640, 360), timestampSeconds: 1 },
|
||||
{ dataUri: await jpegDataUri(640, 360), timestampSeconds: 2 },
|
||||
],
|
||||
});
|
||||
|
||||
const response = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${handle}&variant=preview`, key),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const body = await response.json();
|
||||
assert.equal(body.variant, "preview");
|
||||
assert.equal(body.frames.length, 2);
|
||||
for (const frame of body.frames) {
|
||||
assert.ok(frame.width <= 320);
|
||||
assert.ok(frame.height <= 320);
|
||||
}
|
||||
});
|
||||
|
||||
test("consumer route denies a different API key's handle with the same 404 as a made-up handle", async () => {
|
||||
const { key: ownerKey } = await seedKey();
|
||||
const { key: strangerKey } = await seedKey();
|
||||
const lifecycle = newLifecycle();
|
||||
const policyModule = await import("../../src/shared/utils/apiKeyPolicy.ts");
|
||||
const ownerPolicy = await policyModule.enforceApiKeyPolicy(get("/x", ownerKey), null);
|
||||
const { handle } = await lifecycle.produce(ownerPolicy.apiKeyInfo!.id, {
|
||||
derivation,
|
||||
durationSeconds: 10,
|
||||
frames: [{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 1 }],
|
||||
});
|
||||
|
||||
const strangerRead = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${handle}`, strangerKey),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
const madeUpRead = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${"0".repeat(64)}`, strangerKey),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
assert.equal(strangerRead.status, 404);
|
||||
assert.equal(madeUpRead.status, 404);
|
||||
assert.deepEqual(await strangerRead.json(), await madeUpRead.json());
|
||||
|
||||
const strangerDelete = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
del(`/api/v1/video-bridge/drilldown?handle=${handle}`, strangerKey),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
assert.equal(strangerDelete.status, 200);
|
||||
assert.deepEqual(await strangerDelete.json(), { removed: 0 });
|
||||
|
||||
const ownerRead = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${handle}`, ownerKey),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
assert.equal(ownerRead.status, 200);
|
||||
});
|
||||
|
||||
test("consumer route deletes an owner's handle and it becomes unresolvable afterward", async () => {
|
||||
const { key } = await seedKey();
|
||||
const lifecycle = newLifecycle();
|
||||
const policyModule = await import("../../src/shared/utils/apiKeyPolicy.ts");
|
||||
const policy = await policyModule.enforceApiKeyPolicy(get("/x", key), null);
|
||||
const { handle } = await lifecycle.produce(policy.apiKeyInfo!.id, {
|
||||
derivation,
|
||||
durationSeconds: 10,
|
||||
frames: [{ dataUri: await jpegDataUri(320, 180), timestampSeconds: 1 }],
|
||||
});
|
||||
|
||||
const deleted = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
del(`/api/v1/video-bridge/drilldown?handle=${handle}`, key),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
assert.deepEqual(await deleted.json(), { removed: 1 });
|
||||
|
||||
const afterDelete = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${handle}`, key),
|
||||
{ isRemoteAccessEnabled: () => true, lifecycle }
|
||||
);
|
||||
assert.equal(afterDelete.status, 404);
|
||||
});
|
||||
|
||||
test("consumer route rejects malformed handles and out-of-range pagination before touching the lifecycle", async () => {
|
||||
const { key } = await seedKey();
|
||||
const badHandle = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get("/api/v1/video-bridge/drilldown?handle=not-hex", key),
|
||||
{ isRemoteAccessEnabled: () => true }
|
||||
);
|
||||
assert.equal(badHandle.status, 400);
|
||||
|
||||
const tooManyFrames = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${"a".repeat(64)}&frames=9`, key),
|
||||
{ isRemoteAccessEnabled: () => true }
|
||||
);
|
||||
assert.equal(tooManyFrames.status, 400);
|
||||
|
||||
const badVariant = await route.handleVideoBridgeDrilldownConsumerRequest(
|
||||
get(`/api/v1/video-bridge/drilldown?handle=${"a".repeat(64)}&variant=ultra`, key),
|
||||
{ isRemoteAccessEnabled: () => true }
|
||||
);
|
||||
assert.equal(badVariant.status, 400);
|
||||
});
|
||||
|
||||
test("consumer route is not classified local-only — it is a real remote-authenticated surface, gated by settings instead", () => {
|
||||
assert.equal(isLocalOnlyPath("/api/v1/video-bridge/drilldown", "GET"), false);
|
||||
});
|
||||
Reference in New Issue
Block a user