@omercnet/paseo-omp 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/SUPPORT.md +40 -0
  5. package/TESTING.md +147 -0
  6. package/client/hub-icon.tsx +12 -0
  7. package/client/hub-popover.tsx +132 -0
  8. package/client/hub-status.ts +29 -0
  9. package/client/memory-panel.tsx +71 -0
  10. package/client/memory-popover.tsx +70 -0
  11. package/client/omp-config-surface.tsx +1274 -0
  12. package/client/omp-doc-links.ts +117 -0
  13. package/client/omp-plugin-manager.tsx +833 -0
  14. package/client/provider-diagnostics-state.ts +250 -0
  15. package/client/provider-icon.tsx +27 -0
  16. package/client/provider-image.tsx +66 -0
  17. package/client/quota-popover.tsx +150 -0
  18. package/client/quota-state.ts +131 -0
  19. package/client/sessions-popover.tsx +73 -0
  20. package/docs/alpha-release-checklist.md +70 -0
  21. package/docs/configuration.md +122 -0
  22. package/docs/core-provider-issue-audit.md +108 -0
  23. package/docs/installation.md +73 -0
  24. package/index.client.tsx +272 -0
  25. package/index.server.ts +51 -0
  26. package/package.json +84 -0
  27. package/paseo-plugin.json +5 -0
  28. package/server/hub.ts +145 -0
  29. package/server/memory.ts +86 -0
  30. package/server/mutation-queue.ts +12 -0
  31. package/server/omp-config.ts +126 -0
  32. package/server/omp-plugins.ts +627 -0
  33. package/server/omp-settings.ts +291 -0
  34. package/server/paths.ts +64 -0
  35. package/server/provider/catalog.ts +173 -0
  36. package/server/provider/config-normalization.ts +148 -0
  37. package/server/provider/connection.ts +992 -0
  38. package/server/provider/host-tools.ts +706 -0
  39. package/server/provider/image.ts +143 -0
  40. package/server/provider/mcp-transport.ts +394 -0
  41. package/server/provider/omp-rpc.ts +2739 -0
  42. package/server/provider/omp.svg +5 -0
  43. package/server/provider/provider-options.ts +27 -0
  44. package/server/provider/registration.ts +151 -0
  45. package/server/provider/security.ts +317 -0
  46. package/server/provider/session-descriptors.ts +431 -0
  47. package/server/provider/session.ts +4451 -0
  48. package/server/provider/settings.ts +78 -0
  49. package/server/provider/subsessions.ts +847 -0
  50. package/server/provider/timeline-projector.ts +1764 -0
  51. package/server/provider-diagnostics.ts +1057 -0
  52. package/server/quota.ts +54 -0
  53. package/server/sessions.ts +58 -0
  54. package/shared/hub.ts +43 -0
  55. package/shared/memory.ts +23 -0
  56. package/shared/omp-config.ts +81 -0
  57. package/shared/omp-plugins.ts +223 -0
  58. package/shared/omp-settings.ts +207 -0
  59. package/shared/provider-diagnostics.ts +117 -0
  60. package/shared/provider-image.ts +160 -0
  61. package/shared/quota.ts +22 -0
  62. package/shared/sessions.ts +23 -0
  63. package/tsconfig.json +16 -0
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
2
+ <title>OMP</title>
3
+ <path d="M7 6.5A5.5 5.5 0 0 1 17.5 9v6A2.5 2.5 0 0 1 15 17.5H9A2.5 2.5 0 0 1 6.5 15V9"/>
4
+ <path d="M9 10h.01M15 10h.01M9.5 14h5M12 3v3"/>
5
+ </svg>
@@ -0,0 +1,27 @@
1
+ import { ZodError, type z } from "zod";
2
+ import { OmpPublicError } from "./security";
3
+ import { OmpProviderOptionsSchema } from "./settings";
4
+
5
+ export type ParsedOmpProviderOptions = z.infer<typeof OmpProviderOptionsSchema>;
6
+
7
+ function formatIssuePath(path: readonly PropertyKey[]): string {
8
+ if (path.length === 0) return "providerOptions";
9
+ return `providerOptions.${path.map(String).join(".")}`;
10
+ }
11
+
12
+ function invalidProviderOptions(error: ZodError): OmpPublicError {
13
+ const details = error.issues
14
+ .map((issue) => `${formatIssuePath(issue.path)}: ${issue.message}`)
15
+ .join("; ");
16
+ return new OmpPublicError(`Invalid OMP provider options: ${details}`);
17
+ }
18
+
19
+ /** Parse and normalize the public providerOptions shape. */
20
+ export function parseOmpProviderOptions(input: unknown): ParsedOmpProviderOptions {
21
+ try {
22
+ return OmpProviderOptionsSchema.parse(input ?? {});
23
+ } catch (error) {
24
+ if (error instanceof ZodError) throw invalidProviderOptions(error);
25
+ throw error;
26
+ }
27
+ }
@@ -0,0 +1,151 @@
1
+ import { createHash } from "node:crypto";
2
+ import { homedir } from "node:os";
3
+ import type { ProviderRegistration } from "@getpaseo/plugin/server/provider";
4
+ import { z } from "zod";
5
+ import { probeOmpAvailability } from "../provider-diagnostics";
6
+ import { createOmpConnection, OmpNativeSessionReservations } from "./connection";
7
+ import type { OmpMcpConnector } from "./host-tools";
8
+ import { OmpRpcRuntime, type OmpRuntime } from "./omp-rpc";
9
+ import { parseOmpProviderOptions } from "./provider-options";
10
+ import { boundedJsonBytes } from "./security";
11
+ import { OmpProviderOptionsSchema } from "./settings";
12
+ import type { OmpTimelineScheduler } from "./timeline-projector";
13
+
14
+ type ProviderCatalogOptionsCompat = {
15
+ scope: "global" | "workspace";
16
+ cwd?: string;
17
+ force?: boolean;
18
+ providerOptions?: Readonly<Record<string, unknown>>;
19
+ settings?: Readonly<Record<string, unknown>>;
20
+ };
21
+ type ProviderAvailabilityCompat = {
22
+ status: "missing" | "unrunnable" | "incompatible" | "available";
23
+ diagnostic?: string;
24
+ };
25
+ type ProviderRegistrationCompat = Omit<
26
+ ProviderRegistration,
27
+ "getCatalogCacheKey" | "checkAvailability"
28
+ > & {
29
+ providerOptionsSchema?: typeof OmpProviderOptionsSchema;
30
+ getCatalogCacheKey?(
31
+ options: ProviderCatalogOptionsCompat,
32
+ context?: { timeoutMs?: number },
33
+ ): Promise<string | undefined>;
34
+ checkAvailability?(
35
+ options: ProviderCatalogOptionsCompat,
36
+ context?: { timeoutMs?: number },
37
+ ): Promise<ProviderAvailabilityCompat>;
38
+ };
39
+
40
+ const CAPABILITIES = [
41
+ "prompt.message",
42
+ "prompt.command",
43
+ "prompt.image",
44
+ "prompt.steer",
45
+ "session.configure",
46
+ "session.list",
47
+ "session.persistence",
48
+ "session.subsession",
49
+ "session.revert.conversation",
50
+ "permission",
51
+ ] as const;
52
+ const ConnectRequestSchema = z.object({
53
+ versions: z.array(z.number().int().positive().max(16)).min(1).max(8),
54
+ capabilities: z.array(z.string().min(1).max(64)).max(32),
55
+ });
56
+
57
+ export interface OmpProviderOptions {
58
+ runtime?: OmpRuntime;
59
+ timelineScheduler?: OmpTimelineScheduler;
60
+ replayTimeoutMs?: number;
61
+ environment?: NodeJS.ProcessEnv;
62
+ mcpInitializationTimeoutMs?: number;
63
+ mcpConnector?: OmpMcpConnector;
64
+ availabilityProbe?: (
65
+ options: ProviderCatalogOptionsCompat,
66
+ timeoutMs: number | undefined,
67
+ ) => Promise<ProviderAvailabilityCompat>;
68
+ }
69
+
70
+ function stableJson(value: unknown): string {
71
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
72
+ if (value !== null && typeof value === "object") {
73
+ return `{${Object.entries(value)
74
+ .filter(([, child]) => child !== undefined)
75
+ .sort(([left], [right]) => left.localeCompare(right))
76
+ .map(([key, child]) => `${JSON.stringify(key)}:${stableJson(child)}`)
77
+ .join(",")}}`;
78
+ }
79
+ return JSON.stringify(value) ?? "null";
80
+ }
81
+
82
+ export function createOmpProvider(options: OmpProviderOptions = {}): ProviderRegistrationCompat {
83
+ const runtime = options.runtime ?? new OmpRpcRuntime({ environment: options.environment });
84
+ const nativeReservations = new OmpNativeSessionReservations();
85
+ return {
86
+ id: "omp-plugin",
87
+ label: "OMP Plugin",
88
+ description: "Direct plugin provider for OMP's rpc-ui protocol",
89
+ icon: "server/provider/omp.svg",
90
+ providerOptionsSchema: OmpProviderOptionsSchema,
91
+ async getCatalogCacheKey(catalogOptions) {
92
+ const providerOptions = parseOmpProviderOptions(catalogOptions.providerOptions);
93
+ const identity = {
94
+ scope: catalogOptions.scope,
95
+ ...(catalogOptions.scope === "workspace" ? { cwd: catalogOptions.cwd } : {}),
96
+ providerOptions,
97
+ settings: catalogOptions.settings ?? {},
98
+ defaultCommand: (options.environment ?? process.env).OMP_COMMAND ?? "omp",
99
+ };
100
+ if (
101
+ boundedJsonBytes(identity, 2 * 1024 * 1024, 4_096, 256 * 1024, 16_384) ===
102
+ Number.POSITIVE_INFINITY
103
+ ) {
104
+ throw new Error("OMP catalog identity is too large");
105
+ }
106
+ return createHash("sha256").update(stableJson(identity)).digest("base64url");
107
+ },
108
+ async checkAvailability(catalogOptions, context) {
109
+ if (options.availabilityProbe) {
110
+ return await options.availabilityProbe(catalogOptions, context?.timeoutMs);
111
+ }
112
+ const providerOptions = parseOmpProviderOptions(catalogOptions.providerOptions);
113
+ const environment = { ...(options.environment ?? process.env), ...providerOptions.env };
114
+ const configuredCommand: readonly [string, ...string[]] = providerOptions.command?.[0]
115
+ ? [providerOptions.command[0], ...providerOptions.command.slice(1)]
116
+ : [environment.OMP_COMMAND || "omp"];
117
+ return await probeOmpAvailability({
118
+ command: configuredCommand,
119
+ cwd:
120
+ catalogOptions.scope === "workspace" && catalogOptions.cwd
121
+ ? catalogOptions.cwd
122
+ : homedir(),
123
+ environment,
124
+ timeoutMs: context?.timeoutMs,
125
+ });
126
+ },
127
+ async connect(request) {
128
+ if (boundedJsonBytes(request, 8 * 1024, 32, 256, 64) === Number.POSITIVE_INFINITY) {
129
+ throw new Error("OMP provider received an oversized connection request");
130
+ }
131
+ const parsed = ConnectRequestSchema.safeParse(request);
132
+ if (!parsed.success || !parsed.data.versions.includes(1)) {
133
+ throw new Error("OMP provider requires a valid provider protocol version 1 request");
134
+ }
135
+ const requestedCapabilities = new Set(parsed.data.capabilities);
136
+ const capabilities = CAPABILITIES.filter((capability) =>
137
+ requestedCapabilities.has(capability),
138
+ );
139
+ return createOmpConnection(
140
+ runtime,
141
+ capabilities,
142
+ options.timelineScheduler,
143
+ options.environment,
144
+ nativeReservations,
145
+ options.mcpConnector,
146
+ options.mcpInitializationTimeoutMs,
147
+ options.replayTimeoutMs,
148
+ );
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,317 @@
1
+ import type { ProviderMcpServerConfig } from "@getpaseo/plugin/server/provider";
2
+ import type { OmpOutputRedaction } from "./settings";
3
+
4
+ export type JsonValue =
5
+ | null
6
+ | boolean
7
+ | number
8
+ | string
9
+ | JsonValue[]
10
+ | { [key: string]: JsonValue };
11
+
12
+ const MAX_PUBLIC_STRING_BYTES = 1024 * 1024;
13
+ const MAX_PUBLIC_COLLECTION_ITEMS = 128;
14
+ const MAX_PUBLIC_DEPTH = 16;
15
+ const MAX_PUBLIC_NODES = 2_048;
16
+ const MAX_EXACT_REDACTION_VALUES = 256;
17
+ const MAX_EXACT_REDACTION_VALUE_BYTES = 256 * 1024;
18
+ const MAX_PUBLIC_JSON_BYTES = 256 * 1024;
19
+ const OMITTED = "<omitted>";
20
+ const REDACTED = "<redacted>";
21
+ const CREDENTIAL_ENV_NAME =
22
+ /(?:^|_)(?:API_KEY|ACCESS_KEY|ACCESS_TOKEN|AUTH|AUTHORIZATION|COOKIE|CREDENTIAL|CREDENTIALS|OAUTH|PASSWORD|PRIVATE_KEY|REFRESH_TOKEN|SECRET|SESSION_TOKEN|TOKEN)(?:$|_)/iu;
23
+
24
+ export function utf8Bytes(value: string): number {
25
+ return Buffer.byteLength(value, "utf8");
26
+ }
27
+ export function configuredOutputRedactionValues(
28
+ mode: OmpOutputRedaction,
29
+ env: Readonly<Record<string, string>> | undefined,
30
+ mcpServers: Readonly<Record<string, ProviderMcpServerConfig>> = {},
31
+ ): readonly string[] {
32
+ if (mode === "none") return [];
33
+ const values: string[] = [];
34
+ for (const [name, value] of Object.entries(env ?? {})) {
35
+ if (CREDENTIAL_ENV_NAME.test(name) && utf8Bytes(value) >= 4) values.push(value);
36
+ }
37
+ for (const server of Object.values(mcpServers)) {
38
+ const configuredValues = server.type === "stdio" ? server.env : server.headers;
39
+ for (const value of Object.values(configuredValues ?? {})) {
40
+ if (utf8Bytes(value) >= 4) values.push(value);
41
+ }
42
+ }
43
+ return values;
44
+ }
45
+
46
+ function jsonStringBytes(value: string): number {
47
+ return utf8Bytes(JSON.stringify(value));
48
+ }
49
+
50
+ function jsonStringCharacterBytes(character: string): number {
51
+ const codeUnit = character.charCodeAt(0);
52
+ if (codeUnit === 0x22 || codeUnit === 0x5c) return 2;
53
+ if (codeUnit <= 0x1f) {
54
+ return codeUnit === 0x08 ||
55
+ codeUnit === 0x09 ||
56
+ codeUnit === 0x0a ||
57
+ codeUnit === 0x0c ||
58
+ codeUnit === 0x0d
59
+ ? 2
60
+ : 6;
61
+ }
62
+ if (character.length === 1 && codeUnit >= 0xd800 && codeUnit <= 0xdfff) return 6;
63
+ return utf8Bytes(character);
64
+ }
65
+
66
+ function truncateJsonString(value: string, maxBytes: number): string {
67
+ if (jsonStringBytes(value) <= maxBytes) return value;
68
+ const suffix = "<truncated>";
69
+ const suffixBytes = jsonStringBytes(suffix);
70
+ if (suffixBytes > maxBytes) return "";
71
+ const budget = maxBytes - suffixBytes;
72
+ let output = "";
73
+ let bytes = 0;
74
+ for (const character of value) {
75
+ const characterBytes = jsonStringCharacterBytes(character);
76
+ if (bytes + characterBytes > budget) break;
77
+ output += character;
78
+ bytes += characterBytes;
79
+ }
80
+ return `${output}${suffix}`;
81
+ }
82
+
83
+ export function truncateUtf8(value: string, maxBytes: number): string {
84
+ if (utf8Bytes(value) <= maxBytes) return value;
85
+ const suffix = "<truncated>";
86
+ const budget = Math.max(0, maxBytes - utf8Bytes(suffix));
87
+ let output = "";
88
+ let bytes = 0;
89
+ for (const character of value) {
90
+ const characterBytes = utf8Bytes(character);
91
+ if (bytes + characterBytes > budget) break;
92
+ output += character;
93
+ bytes += characterBytes;
94
+ }
95
+ return `${output}${suffix}`;
96
+ }
97
+
98
+ export interface BoundedJsonMetrics {
99
+ bytes: number;
100
+ nodes: number;
101
+ }
102
+
103
+ /** Returns undefined as soon as a depth, item, node, string, or cumulative limit is crossed. */
104
+ export function boundedJsonMetrics(
105
+ value: unknown,
106
+ maxBytes: number,
107
+ maxItems = MAX_PUBLIC_COLLECTION_ITEMS,
108
+ maxStringBytes = maxBytes,
109
+ maxNodes = MAX_PUBLIC_NODES,
110
+ ): BoundedJsonMetrics | undefined {
111
+ const stack: Array<{ value: unknown; depth: number }> = [{ value, depth: 0 }];
112
+ let nodes = 0;
113
+ let bytes = 0;
114
+ while (stack.length > 0) {
115
+ const current = stack.pop();
116
+ if (!current) break;
117
+ nodes += 1;
118
+ if (nodes > maxNodes || current.depth > MAX_PUBLIC_DEPTH) return;
119
+ const item = current.value;
120
+ if (item === null || typeof item === "boolean" || typeof item === "number") continue;
121
+ if (typeof item === "string") {
122
+ const itemBytes = utf8Bytes(item);
123
+ if (itemBytes > maxStringBytes) return;
124
+ bytes += itemBytes;
125
+ if (bytes > maxBytes) return;
126
+ continue;
127
+ }
128
+ if (typeof item !== "object") return;
129
+ if (Array.isArray(item)) {
130
+ if (item.length > maxItems) return;
131
+ for (let index = item.length - 1; index >= 0; index -= 1) {
132
+ stack.push({ value: item[index], depth: current.depth + 1 });
133
+ }
134
+ continue;
135
+ }
136
+ let itemCount = 0;
137
+ for (const key in item) {
138
+ if (!Object.hasOwn(item, key)) continue;
139
+ const child = (item as Record<string, unknown>)[key];
140
+ if (child === undefined) continue;
141
+ itemCount += 1;
142
+ if (itemCount > maxItems) return;
143
+ bytes += utf8Bytes(key);
144
+ if (bytes > maxBytes) return;
145
+ stack.push({ value: child, depth: current.depth + 1 });
146
+ }
147
+ }
148
+ return { bytes, nodes };
149
+ }
150
+
151
+ /** Returns Infinity as soon as a depth, item, node, string, or cumulative byte limit is crossed. */
152
+ export function boundedJsonBytes(
153
+ value: unknown,
154
+ maxBytes: number,
155
+ maxItems = MAX_PUBLIC_COLLECTION_ITEMS,
156
+ maxStringBytes = maxBytes,
157
+ maxNodes = MAX_PUBLIC_NODES,
158
+ ): number {
159
+ return (
160
+ boundedJsonMetrics(value, maxBytes, maxItems, maxStringBytes, maxNodes)?.bytes ??
161
+ Number.POSITIVE_INFINITY
162
+ );
163
+ }
164
+
165
+ export class BoundedStringSet {
166
+ private readonly values = new Map<string, true>();
167
+
168
+ constructor(private readonly limit: number) {
169
+ if (!Number.isSafeInteger(limit) || limit < 1)
170
+ throw new Error("Bounded set limit must be positive");
171
+ }
172
+
173
+ has(value: string): boolean {
174
+ return this.values.has(value);
175
+ }
176
+
177
+ add(value: string): void {
178
+ if (this.values.delete(value)) {
179
+ this.values.set(value, true);
180
+ return;
181
+ }
182
+ this.values.set(value, true);
183
+ if (this.values.size <= this.limit) return;
184
+ const oldest = this.values.keys().next().value;
185
+ if (oldest !== undefined) this.values.delete(oldest);
186
+ }
187
+ }
188
+
189
+ export class OmpPublicError extends Error {
190
+ override readonly name = "OmpPublicError";
191
+ }
192
+
193
+ export function isOmpPublicError(error: unknown): error is OmpPublicError {
194
+ return (
195
+ error instanceof OmpPublicError || (error instanceof Error && error.name === "OmpPublicError")
196
+ );
197
+ }
198
+
199
+ export class OmpCleanupFailure extends Error {
200
+ override readonly name = "OmpCleanupFailure";
201
+
202
+ constructor(
203
+ message: string,
204
+ readonly cleanup: Promise<void>,
205
+ readonly nativeSessionId?: string,
206
+ ) {
207
+ void cleanup.catch(() => undefined);
208
+ super(message);
209
+ }
210
+ }
211
+
212
+ export function isOmpCleanupFailure(error: unknown): error is OmpCleanupFailure {
213
+ return (
214
+ error instanceof OmpCleanupFailure ||
215
+ (error instanceof Error &&
216
+ error.name === "OmpCleanupFailure" &&
217
+ "cleanup" in error &&
218
+ error.cleanup instanceof Promise)
219
+ );
220
+ }
221
+
222
+ export class OmpPublicDataSerializer {
223
+ private readonly exactValues: readonly string[];
224
+
225
+ constructor(values: Iterable<string> = []) {
226
+ const unique = new Set<string>();
227
+ let bytes = 0;
228
+ for (const value of values) {
229
+ if (utf8Bytes(value) < 4 || unique.has(value)) continue;
230
+ unique.add(value);
231
+ bytes += utf8Bytes(value);
232
+ if (unique.size > MAX_EXACT_REDACTION_VALUES || bytes > MAX_EXACT_REDACTION_VALUE_BYTES) {
233
+ throw new Error("OMP configured output-redaction budget exceeded");
234
+ }
235
+ }
236
+ this.exactValues = Object.freeze([...unique].sort((left, right) => right.length - left.length));
237
+ }
238
+
239
+ text(input: string, maxBytes = MAX_PUBLIC_STRING_BYTES): string {
240
+ let output = input;
241
+ for (const value of this.exactValues) output = output.split(value).join(REDACTED);
242
+ return truncateUtf8(output, maxBytes);
243
+ }
244
+
245
+ json(
246
+ input: unknown,
247
+ maxStringBytes = MAX_PUBLIC_STRING_BYTES,
248
+ maxOutputBytes = MAX_PUBLIC_JSON_BYTES,
249
+ ): JsonValue {
250
+ const seen = new WeakSet<object>();
251
+ let nodes = 0;
252
+ let remaining = maxOutputBytes;
253
+ const consume = (bytes: number) => {
254
+ if (bytes > remaining) return false;
255
+ remaining -= bytes;
256
+ return true;
257
+ };
258
+ const boundedString = (value: string): string => {
259
+ const sanitized = this.text(value, Math.min(maxStringBytes, remaining));
260
+ const output = truncateJsonString(sanitized, remaining);
261
+ consume(jsonStringBytes(output));
262
+ return output;
263
+ };
264
+ const visit = (value: unknown, depth: number): JsonValue => {
265
+ nodes += 1;
266
+ if (nodes > MAX_PUBLIC_NODES || depth > MAX_PUBLIC_DEPTH || remaining < 16) return OMITTED;
267
+ if (value === null) {
268
+ consume(4);
269
+ return null;
270
+ }
271
+ if (typeof value === "boolean") {
272
+ consume(value ? 4 : 5);
273
+ return value;
274
+ }
275
+ if (typeof value === "number") {
276
+ const safe = Number.isFinite(value) ? value : null;
277
+ consume(utf8Bytes(JSON.stringify(safe)));
278
+ return safe;
279
+ }
280
+ if (typeof value === "string") return boundedString(value);
281
+ if (typeof value !== "object" || seen.has(value)) return OMITTED;
282
+ seen.add(value);
283
+ if (Array.isArray(value)) {
284
+ if (!consume(2)) return OMITTED;
285
+ const output: JsonValue[] = [];
286
+ const length = Math.min(value.length, MAX_PUBLIC_COLLECTION_ITEMS);
287
+ for (let index = 0; index < length && remaining >= 16; index += 1) {
288
+ if (index > 0) consume(1);
289
+ output.push(visit(value[index], depth + 1));
290
+ }
291
+ return output;
292
+ }
293
+ if (!consume(2)) return OMITTED;
294
+ const output = Object.create(null) as { [key: string]: JsonValue };
295
+ let itemCount = 0;
296
+ for (const childKey in value) {
297
+ if (!Object.hasOwn(value, childKey) || remaining < 32) continue;
298
+ itemCount += 1;
299
+ if (itemCount > MAX_PUBLIC_COLLECTION_ITEMS) break;
300
+ const safeKey = this.text(childKey, 256);
301
+ if (
302
+ safeKey === "__proto__" ||
303
+ safeKey === "constructor" ||
304
+ safeKey === "prototype" ||
305
+ Object.hasOwn(output, safeKey)
306
+ ) {
307
+ continue;
308
+ }
309
+ const keyBytes = utf8Bytes(JSON.stringify(safeKey)) + 1 + (itemCount > 1 ? 1 : 0);
310
+ if (!consume(keyBytes)) break;
311
+ output[safeKey] = visit((value as Record<string, unknown>)[childKey], depth + 1);
312
+ }
313
+ return output;
314
+ };
315
+ return visit(input, 0);
316
+ }
317
+ }