fix(coding): remediate data service review findings
This commit is contained in:
@@ -187,7 +187,6 @@ export interface AgentBrowserReadPayloadInput {
|
||||
export interface AgentBrowserModuleOptions {
|
||||
payloadStore?: AgentBrowserPayloadStore;
|
||||
cdpGuard?: AgentBrowserCdpGuard;
|
||||
onLifecycle?(event: AgentBrowserLifecycleEvent): void;
|
||||
previewDataSession?: PreviewDataSession;
|
||||
}
|
||||
|
||||
@@ -230,7 +229,6 @@ export class AgentBrowserModule {
|
||||
) {
|
||||
this.payloadStore = options.payloadStore ?? new AgentBrowserPayloadStore();
|
||||
this.cdpGuard = options.cdpGuard ?? new AgentBrowserCdpGuard();
|
||||
if (options.onLifecycle) this.lifecycleListeners.add(options.onLifecycle);
|
||||
if (options.previewDataSession) this.configurePreviewDataSession(options.previewDataSession);
|
||||
}
|
||||
|
||||
|
||||
177
electron/api/routes/data-service-parsers.ts
Normal file
177
electron/api/routes/data-service-parsers.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import type { IncomingMessage } from 'node:http';
|
||||
|
||||
export const MAX_CURSOR_LENGTH = 1_024;
|
||||
export const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
|
||||
export const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
||||
|
||||
export type ParserFailure = (
|
||||
status: number,
|
||||
code: string,
|
||||
message: string,
|
||||
) => never;
|
||||
|
||||
export type ParserMessages = Readonly<{
|
||||
invalidRequest: string;
|
||||
invalidRevision: string;
|
||||
requestTooLarge: string;
|
||||
}>;
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function boundedString(value: unknown, maximum: number): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return value && value.length <= maximum ? value : null;
|
||||
}
|
||||
|
||||
export async function readBoundedJson(
|
||||
req: IncomingMessage,
|
||||
maxBytes: number,
|
||||
fail: ParserFailure,
|
||||
messages: ParserMessages,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const declared = req.headers['content-length'];
|
||||
const declaredValue = Array.isArray(declared) ? declared[0] : declared;
|
||||
if (declaredValue && /^\d+$/.test(declaredValue) && Number(declaredValue) > maxBytes) {
|
||||
return fail(413, 'request_too_large', messages.requestTooLarge);
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
for await (const chunk of req) {
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
size += buffer.byteLength;
|
||||
if (size > maxBytes) {
|
||||
return fail(413, 'request_too_large', messages.requestTooLarge);
|
||||
}
|
||||
chunks.push(buffer);
|
||||
}
|
||||
try {
|
||||
const value = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;
|
||||
if (!isRecord(value)) throw new Error('object required');
|
||||
return value;
|
||||
} catch {
|
||||
return fail(422, 'invalid_request', messages.invalidRequest);
|
||||
}
|
||||
}
|
||||
|
||||
export function requireExactKeys(
|
||||
value: Record<string, unknown>,
|
||||
keys: readonly string[],
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): void {
|
||||
const expected = new Set(keys);
|
||||
if (Object.keys(value).some((key) => !expected.has(key))
|
||||
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
|
||||
fail(422, 'invalid_request', message);
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSegment(
|
||||
value: string,
|
||||
maximum: number,
|
||||
pattern: RegExp,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): string {
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURIComponent(value);
|
||||
} catch {
|
||||
return fail(422, 'invalid_request', message);
|
||||
}
|
||||
if (decoded.length > maximum || !pattern.test(decoded)) {
|
||||
return fail(422, 'invalid_request', message);
|
||||
}
|
||||
return decoded;
|
||||
}
|
||||
|
||||
export function parseCollection(
|
||||
value: string,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): string {
|
||||
return decodeSegment(value, 48, COLLECTION_PATTERN, fail, message);
|
||||
}
|
||||
|
||||
export function parseDocumentId(
|
||||
value: string,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): string {
|
||||
const documentId = decodeSegment(value, 128, DOCUMENT_ID_PATTERN, fail, message);
|
||||
if (documentId === '.' || documentId === '..') {
|
||||
return fail(422, 'invalid_request', message);
|
||||
}
|
||||
return documentId;
|
||||
}
|
||||
|
||||
export function requireQueryKeys(
|
||||
url: URL,
|
||||
allowed: readonly string[],
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): void {
|
||||
const accepted = new Set(allowed);
|
||||
if ([...url.searchParams.keys()].some((key) => !accepted.has(key))) {
|
||||
fail(422, 'invalid_request', message);
|
||||
}
|
||||
}
|
||||
|
||||
export function parseLimit(
|
||||
url: URL,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): number | undefined {
|
||||
const values = url.searchParams.getAll('limit');
|
||||
if (values.length === 0) return undefined;
|
||||
if (values.length !== 1 || !/^[1-9]\d*$/.test(values[0])) {
|
||||
return fail(422, 'invalid_request', message);
|
||||
}
|
||||
const limit = Number(values[0]);
|
||||
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
||||
return fail(422, 'invalid_request', message);
|
||||
}
|
||||
return limit;
|
||||
}
|
||||
|
||||
export function parseCursor(
|
||||
url: URL,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): string | undefined {
|
||||
const values = url.searchParams.getAll('cursor');
|
||||
if (values.length === 0) return undefined;
|
||||
const cursor = values.length === 1 ? boundedString(values[0], MAX_CURSOR_LENGTH) : null;
|
||||
if (!cursor) return fail(422, 'invalid_request', message);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
export function parseIfMatch(
|
||||
req: IncomingMessage,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): number | undefined {
|
||||
const value = req.headers['if-match'];
|
||||
if (value === undefined) return undefined;
|
||||
if (Array.isArray(value) || !/^"[1-9]\d*"$/.test(value)) {
|
||||
return fail(422, 'invalid_revision', message);
|
||||
}
|
||||
const revision = Number(value.slice(1, -1));
|
||||
if (!Number.isSafeInteger(revision) || revision < 1) {
|
||||
return fail(422, 'invalid_revision', message);
|
||||
}
|
||||
return revision;
|
||||
}
|
||||
|
||||
export function parseData(
|
||||
value: unknown,
|
||||
fail: ParserFailure,
|
||||
message: string,
|
||||
): Record<string, unknown> {
|
||||
if (!isRecord(value)) {
|
||||
return fail(422, 'invalid_request', message);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
@@ -5,12 +5,27 @@ import {
|
||||
type DataServiceErrorContext,
|
||||
type DataServiceHostResult,
|
||||
} from '../../../shared/data-service';
|
||||
import {
|
||||
COLLECTION_PATTERN,
|
||||
parseCollection as parseCollectionSegment,
|
||||
parseCursor as parseCursorValue,
|
||||
parseData as parseDataValue,
|
||||
parseDocumentId as parseDocumentIdValue,
|
||||
parseIfMatch as parseIfMatchValue,
|
||||
parseLimit as parseLimitValue,
|
||||
readBoundedJson as readBoundedJsonBody,
|
||||
requireExactKeys as requireExactKeysValue,
|
||||
requireQueryKeys as requireQueryKeysValue,
|
||||
type ParserMessages,
|
||||
} from './data-service-parsers';
|
||||
|
||||
const LOCAL_ROOT = '/api/works/data-service';
|
||||
const MAX_REQUEST_BYTES = 98_304;
|
||||
const MAX_CURSOR_LENGTH = 1_024;
|
||||
const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
|
||||
const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
||||
const PARSER_MESSAGES: ParserMessages = {
|
||||
invalidRequest: 'Data Service request is invalid',
|
||||
invalidRevision: 'Data Service document revision is invalid',
|
||||
requestTooLarge: 'Data Service request is too large',
|
||||
};
|
||||
|
||||
class DataServiceRouteError extends Error {
|
||||
constructor(
|
||||
@@ -23,15 +38,6 @@ class DataServiceRouteError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function boundedString(value: unknown, maximum: number): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return value && value.length <= maximum ? value : null;
|
||||
}
|
||||
|
||||
function routeFailure<T>(
|
||||
status: number,
|
||||
code: string,
|
||||
@@ -59,69 +65,28 @@ function methodNotAllowed<T>(): DataServiceHostResult<T> {
|
||||
return routeFailure(405, 'method_not_allowed', 'Data Service method is not allowed');
|
||||
}
|
||||
|
||||
function parserFailure(status: number, code: string, message: string): never {
|
||||
throw new DataServiceRouteError(status, code, message);
|
||||
}
|
||||
|
||||
async function readBoundedJson(req: IncomingMessage): Promise<Record<string, unknown>> {
|
||||
const declared = req.headers['content-length'];
|
||||
const declaredValue = Array.isArray(declared) ? declared[0] : declared;
|
||||
if (declaredValue && /^\d+$/.test(declaredValue) && Number(declaredValue) > MAX_REQUEST_BYTES) {
|
||||
throw new DataServiceRouteError(413, 'request_too_large', 'Data Service request is too large');
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
for await (const chunk of req) {
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
size += buffer.byteLength;
|
||||
if (size > MAX_REQUEST_BYTES) {
|
||||
throw new DataServiceRouteError(413, 'request_too_large', 'Data Service request is too large');
|
||||
}
|
||||
chunks.push(buffer);
|
||||
}
|
||||
try {
|
||||
const value = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;
|
||||
if (!isRecord(value)) throw new Error('object required');
|
||||
return value;
|
||||
} catch {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
return readBoundedJsonBody(req, MAX_REQUEST_BYTES, parserFailure, PARSER_MESSAGES);
|
||||
}
|
||||
|
||||
function requireExactKeys(value: Record<string, unknown>, keys: readonly string[]): void {
|
||||
const expected = new Set(keys);
|
||||
if (Object.keys(value).some((key) => !expected.has(key))
|
||||
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSegment(value: string, maximum: number, pattern: RegExp): string {
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURIComponent(value);
|
||||
} catch {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
if (decoded.length > maximum || !pattern.test(decoded)) {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
return decoded;
|
||||
requireExactKeysValue(value, keys, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseCollection(value: string): string {
|
||||
return decodeSegment(value, 48, COLLECTION_PATTERN);
|
||||
return parseCollectionSegment(value, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseDocumentId(value: string): string {
|
||||
const documentId = decodeSegment(value, 128, DOCUMENT_ID_PATTERN);
|
||||
if (documentId === '.' || documentId === '..') {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
return documentId;
|
||||
return parseDocumentIdValue(value, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function requireQueryKeys(url: URL, allowed: readonly string[]): void {
|
||||
const accepted = new Set(allowed);
|
||||
if ([...url.searchParams.keys()].some((key) => !accepted.has(key))) {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
requireQueryKeysValue(url, allowed, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function requireConfirmed(url: URL): void {
|
||||
@@ -137,37 +102,15 @@ function requireConfirmed(url: URL): void {
|
||||
}
|
||||
|
||||
function parseLimit(url: URL): number | undefined {
|
||||
const values = url.searchParams.getAll('limit');
|
||||
if (values.length === 0) return undefined;
|
||||
if (values.length !== 1 || !/^[1-9]\d*$/.test(values[0])) {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
const limit = Number(values[0]);
|
||||
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
return limit;
|
||||
return parseLimitValue(url, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseCursor(url: URL): string | undefined {
|
||||
const values = url.searchParams.getAll('cursor');
|
||||
if (values.length === 0) return undefined;
|
||||
const cursor = values.length === 1 ? boundedString(values[0], MAX_CURSOR_LENGTH) : null;
|
||||
if (!cursor) throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
return cursor;
|
||||
return parseCursorValue(url, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseIfMatch(req: IncomingMessage): number | undefined {
|
||||
const value = req.headers['if-match'];
|
||||
if (value === undefined) return undefined;
|
||||
if (Array.isArray(value) || !/^"[1-9]\d*"$/.test(value)) {
|
||||
throw new DataServiceRouteError(422, 'invalid_revision', 'Data Service document revision is invalid');
|
||||
}
|
||||
const revision = Number(value.slice(1, -1));
|
||||
if (!Number.isSafeInteger(revision) || revision < 1) {
|
||||
throw new DataServiceRouteError(422, 'invalid_revision', 'Data Service document revision is invalid');
|
||||
}
|
||||
return revision;
|
||||
return parseIfMatchValue(req, parserFailure, PARSER_MESSAGES.invalidRevision);
|
||||
}
|
||||
|
||||
function parseCollections(value: unknown): string[] {
|
||||
@@ -179,10 +122,7 @@ function parseCollections(value: unknown): string[] {
|
||||
}
|
||||
|
||||
function parseData(value: unknown): Record<string, unknown> {
|
||||
if (!isRecord(value)) {
|
||||
throw new DataServiceRouteError(422, 'invalid_request', 'Data Service request is invalid');
|
||||
}
|
||||
return value;
|
||||
return parseDataValue(value, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function sendResult<T>(res: ServerResponse, result: DataServiceHostResult<T>): void {
|
||||
@@ -331,5 +271,3 @@ export async function handleDataServiceRoutes(
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const handleDataServiceRoute = handleDataServiceRoutes;
|
||||
|
||||
@@ -6,12 +6,27 @@ import type {
|
||||
DataServiceErrorContext,
|
||||
DataServiceHostResult,
|
||||
} from '../../../shared/data-service';
|
||||
import {
|
||||
isRecord,
|
||||
parseCollection as parseCollectionSegment,
|
||||
parseCursor as parseCursorValue,
|
||||
parseData as parseDataValue,
|
||||
parseDocumentId as parseDocumentIdValue,
|
||||
parseIfMatch as parseIfMatchValue,
|
||||
parseLimit as parseLimitValue,
|
||||
readBoundedJson as readBoundedJsonBody,
|
||||
requireExactKeys as requireExactKeysValue,
|
||||
requireQueryKeys as requireQueryKeysValue,
|
||||
type ParserMessages,
|
||||
} from './data-service-parsers';
|
||||
|
||||
const MAX_CURSOR_LENGTH = 1_024;
|
||||
const COLLECTION_PATTERN = /^[a-z][a-z0-9_-]{0,47}$/;
|
||||
const DOCUMENT_ID_PATTERN = /^[A-Za-z0-9._~-]{1,128}$/;
|
||||
const CORS_METHODS = 'GET, PUT, DELETE, OPTIONS';
|
||||
const CORS_HEADERS = 'Authorization, Content-Type, If-Match';
|
||||
const PARSER_MESSAGES: ParserMessages = {
|
||||
invalidRequest: 'Preview data request is invalid',
|
||||
invalidRevision: 'Data Service document revision is invalid',
|
||||
requestTooLarge: 'Preview data request is too large',
|
||||
};
|
||||
|
||||
class PreviewDataRouteError extends Error {
|
||||
constructor(
|
||||
@@ -25,15 +40,6 @@ class PreviewDataRouteError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function boundedString(value: unknown, maximum: number): string | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
return value && value.length <= maximum ? value : null;
|
||||
}
|
||||
|
||||
function routeFailure<T>(
|
||||
status: number,
|
||||
code: string,
|
||||
@@ -144,110 +150,44 @@ function sendResult<T>(res: ServerResponse, result: DataServiceHostResult<T>): v
|
||||
}
|
||||
}
|
||||
|
||||
function parserFailure(status: number, code: string, message: string): never {
|
||||
throw new PreviewDataRouteError(status, code, message);
|
||||
}
|
||||
|
||||
async function readBoundedJson(req: IncomingMessage): Promise<Record<string, unknown>> {
|
||||
const declared = req.headers['content-length'];
|
||||
const declaredValue = Array.isArray(declared) ? declared[0] : declared;
|
||||
if (declaredValue && /^\d+$/.test(declaredValue) && Number(declaredValue) > PREVIEW_DATA_MAX_REQUEST_BYTES) {
|
||||
throw new PreviewDataRouteError(413, 'request_too_large', 'Preview data request is too large');
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
let size = 0;
|
||||
for await (const chunk of req) {
|
||||
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
||||
size += buffer.byteLength;
|
||||
if (size > PREVIEW_DATA_MAX_REQUEST_BYTES) {
|
||||
throw new PreviewDataRouteError(413, 'request_too_large', 'Preview data request is too large');
|
||||
}
|
||||
chunks.push(buffer);
|
||||
}
|
||||
try {
|
||||
const value = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;
|
||||
if (!isRecord(value)) throw new Error('object required');
|
||||
return value;
|
||||
} catch {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
return readBoundedJsonBody(req, PREVIEW_DATA_MAX_REQUEST_BYTES, parserFailure, PARSER_MESSAGES);
|
||||
}
|
||||
|
||||
function requireExactKeys(value: Record<string, unknown>, keys: readonly string[]): void {
|
||||
const expected = new Set(keys);
|
||||
if (Object.keys(value).some((key) => !expected.has(key))
|
||||
|| keys.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
}
|
||||
|
||||
function decodeSegment(value: string, maximum: number, pattern: RegExp): string {
|
||||
let decoded: string;
|
||||
try {
|
||||
decoded = decodeURIComponent(value);
|
||||
} catch {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
if (decoded.length > maximum || !pattern.test(decoded)) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
return decoded;
|
||||
requireExactKeysValue(value, keys, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseCollection(value: string): string {
|
||||
return decodeSegment(value, 48, COLLECTION_PATTERN);
|
||||
return parseCollectionSegment(value, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseDocumentId(value: string): string {
|
||||
const documentId = decodeSegment(value, 128, DOCUMENT_ID_PATTERN);
|
||||
if (documentId === '.' || documentId === '..') {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
return documentId;
|
||||
return parseDocumentIdValue(value, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function requireQueryKeys(url: URL, allowed: readonly string[]): void {
|
||||
const accepted = new Set(allowed);
|
||||
if ([...url.searchParams.keys()].some((key) => !accepted.has(key))) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
requireQueryKeysValue(url, allowed, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseLimit(url: URL): number | undefined {
|
||||
const values = url.searchParams.getAll('limit');
|
||||
if (values.length === 0) return undefined;
|
||||
if (values.length !== 1 || !/^[1-9]\d*$/.test(values[0])) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
const limit = Number(values[0]);
|
||||
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
return limit;
|
||||
return parseLimitValue(url, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseCursor(url: URL): string | undefined {
|
||||
const values = url.searchParams.getAll('cursor');
|
||||
if (values.length === 0) return undefined;
|
||||
const cursor = values.length === 1 ? boundedString(values[0], MAX_CURSOR_LENGTH) : null;
|
||||
if (!cursor) throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
return cursor;
|
||||
return parseCursorValue(url, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function parseIfMatch(req: IncomingMessage): number | undefined {
|
||||
const value = req.headers['if-match'];
|
||||
if (value === undefined) return undefined;
|
||||
if (Array.isArray(value) || !/^"[1-9]\d*"$/.test(value)) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_revision', 'Data Service document revision is invalid');
|
||||
}
|
||||
const revision = Number(value.slice(1, -1));
|
||||
if (!Number.isSafeInteger(revision) || revision < 1) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_revision', 'Data Service document revision is invalid');
|
||||
}
|
||||
return revision;
|
||||
return parseIfMatchValue(req, parserFailure, PARSER_MESSAGES.invalidRevision);
|
||||
}
|
||||
|
||||
function parseData(value: unknown): Record<string, unknown> {
|
||||
if (!isRecord(value)) {
|
||||
throw new PreviewDataRouteError(422, 'invalid_request', 'Preview data request is invalid');
|
||||
}
|
||||
return value;
|
||||
return parseDataValue(value, parserFailure, PARSER_MESSAGES.invalidRequest);
|
||||
}
|
||||
|
||||
function requireJsonContentType(req: IncomingMessage): void {
|
||||
|
||||
@@ -1347,7 +1347,14 @@ export async function runFinalAsarExtensionProof(): Promise<PiReleaseExtensionPr
|
||||
throw new Error('Final packaged Pi parent did not load the materialized subagent tool');
|
||||
}
|
||||
if (childToolNames.includes('subagent') || childToolNames.some((name) => (
|
||||
['ask_user', 'agent_browser', 'game_asset_browser', 'game_asset_review', 'task_state', 'changed_file', 'runtime_context']
|
||||
[
|
||||
'ask_user', 'agent_browser', 'game_asset_browser', 'game_asset_review',
|
||||
'task_state', 'changed_file', 'runtime_context',
|
||||
'data_service_configure', 'data_service_inspect', 'data_service_list_projects',
|
||||
'data_service_get_document', 'data_service_list_documents', 'data_service_put_document',
|
||||
'data_service_delete_document', 'data_service_remove_collection', 'data_service_reset',
|
||||
'data_service_remove_project',
|
||||
]
|
||||
.includes(name)
|
||||
))) {
|
||||
throw new Error('Final packaged Pi child exposed parent-only product tools');
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
type ChildProcessWithoutNullStreams,
|
||||
} from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
import { DATA_SERVICE_PI_TOOL_NAMES } from '../../../shared/data-service';
|
||||
import { logger } from '../../utils/logger';
|
||||
import type { CodingRuntimeDisposeReason } from '../contracts';
|
||||
import { PiProcessError, type PiProcessErrorCode } from './process-errors';
|
||||
@@ -128,6 +129,7 @@ export function buildPiRpcArgs(
|
||||
'read', 'bash', 'edit', 'write', 'grep', 'find', 'ls', 'ask_user', 'subagent',
|
||||
'agent_browser', 'game_asset_browser', 'game_asset_review',
|
||||
'task_state', 'changed_file', 'runtime_context',
|
||||
...DATA_SERVICE_PI_TOOL_NAMES,
|
||||
],
|
||||
): string[] {
|
||||
return [
|
||||
|
||||
@@ -156,8 +156,7 @@ function normalizeOrigin(value: string): string {
|
||||
|| hostname === '[::1]';
|
||||
if (
|
||||
!isLoopback
|
||||
|| parsed.protocol !== 'http:'
|
||||
|| !parsed.port
|
||||
|| (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
||||
|| parsed.username
|
||||
|| parsed.password
|
||||
|| parsed.pathname !== '/'
|
||||
@@ -402,6 +401,7 @@ export class PreviewDataSessionManager {
|
||||
handleAgentBrowserLifecycle(event: AgentBrowserLifecycleEvent): void {
|
||||
const session = this.session;
|
||||
if (!session || !samePath(session.snapshot.projectPath, event.projectPath)) return;
|
||||
if (event.generation !== session.snapshot.browserGeneration) return;
|
||||
switch (event.type) {
|
||||
case 'closed':
|
||||
this.invalidate('preview_closed');
|
||||
|
||||
Reference in New Issue
Block a user