@pasko70/pibo 2.5.2 → 3.0.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 (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +464 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -0,0 +1,198 @@
1
+ import { readdirSync, readFileSync, readlinkSync } from "node:fs";
2
+ import { connect } from "node:net";
3
+ const RESERVED_PREVIEW_PORTS = new Set([
4
+ 2375,
5
+ 2376,
6
+ 3306,
7
+ 4788,
8
+ 4789,
9
+ 4808,
10
+ 4809,
11
+ 5432,
12
+ 6379,
13
+ 9200,
14
+ 9222,
15
+ 9223,
16
+ 27017,
17
+ ]);
18
+ const targetIdentityCache = new Map();
19
+ export function validatePreviewPort(port) {
20
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) {
21
+ throw new Error("Preview port must be an integer between 1024 and 65535");
22
+ }
23
+ if (RESERVED_PREVIEW_PORTS.has(port)) {
24
+ throw new Error(`Port ${port} is reserved and cannot be exposed as a Pibo preview`);
25
+ }
26
+ return port;
27
+ }
28
+ export async function probePreviewTarget(port, options = {}) {
29
+ validatePreviewPort(port);
30
+ for (const host of ["127.0.0.1", "::1"]) {
31
+ const startedAt = performance.now();
32
+ const reachable = await new Promise((resolve) => {
33
+ const socket = connect({ host, port });
34
+ let settled = false;
35
+ const finish = (value) => {
36
+ if (settled)
37
+ return;
38
+ settled = true;
39
+ socket.destroy();
40
+ resolve(value);
41
+ };
42
+ socket.setTimeout(options.timeoutMs ?? 750);
43
+ socket.once("connect", () => finish(true));
44
+ socket.once("timeout", () => finish(false));
45
+ socket.once("error", () => finish(false));
46
+ });
47
+ if (reachable)
48
+ return { host, latencyMs: Math.max(0, Math.round(performance.now() - startedAt)) };
49
+ }
50
+ return undefined;
51
+ }
52
+ export function previewProcessStartTicks(pid) {
53
+ try {
54
+ const stat = readFileSync(`/proc/${pid}/stat`, "utf8").trim();
55
+ const closingParen = stat.lastIndexOf(")");
56
+ if (closingParen < 0)
57
+ return undefined;
58
+ return stat.slice(closingParen + 1).trim().split(/\s+/)[19];
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
64
+ export function isPiboYieldedProcess(pid) {
65
+ if (process.platform !== "linux")
66
+ return false;
67
+ try {
68
+ return /(?:^|\/)pibo-yielded-[^/\n]+\.service(?:$|\/)/m.test(readFileSync(`/proc/${pid}/cgroup`, "utf8"));
69
+ }
70
+ catch {
71
+ return false;
72
+ }
73
+ }
74
+ export function isPiboControlProcess(pid) {
75
+ if (pid === process.pid)
76
+ return true;
77
+ if (process.platform !== "linux")
78
+ return false;
79
+ try {
80
+ const cgroup = readFileSync(`/proc/${pid}/cgroup`, "utf8");
81
+ if (/(?:^|\/)pibo-(?:web|gateway|dev|fallback)[^/\n]*\.service(?:$|\/)/m.test(cgroup))
82
+ return true;
83
+ }
84
+ catch {
85
+ // Fall through to command-line inspection.
86
+ }
87
+ try {
88
+ const argv = readFileSync(`/proc/${pid}/cmdline`)
89
+ .toString("utf8")
90
+ .split("\0")
91
+ .filter(Boolean);
92
+ const command = argv.join(" ");
93
+ return /(?:^|\s)(?:pibo|[^\s/]*\/pibo)(?:\s|$)/.test(command) &&
94
+ /(?:^|\s)gateway(?::web)?(?:\s|$)/.test(command) ||
95
+ /(?:dist|src)\/bin\/pibo\.(?:js|ts).*\bgateway(?::web)?\b/.test(command);
96
+ }
97
+ catch {
98
+ return false;
99
+ }
100
+ }
101
+ function loopbackSocketInodes(host, port) {
102
+ if (process.platform !== "linux")
103
+ return new Set();
104
+ const portHex = port.toString(16).toUpperCase().padStart(4, "0");
105
+ const sources = host === "127.0.0.1"
106
+ ? [["/proc/net/tcp", "0100007F"]]
107
+ : [["/proc/net/tcp6", "00000000000000000000000001000000"]];
108
+ const inodes = new Set();
109
+ for (const [path, address] of sources) {
110
+ let lines;
111
+ try {
112
+ lines = readFileSync(path, "utf8").split("\n").slice(1);
113
+ }
114
+ catch {
115
+ continue;
116
+ }
117
+ for (const line of lines) {
118
+ const fields = line.trim().split(/\s+/);
119
+ if (fields.length < 10 || fields[3] !== "0A")
120
+ continue;
121
+ const [localAddress, localPort] = fields[1].split(":");
122
+ if (localAddress === address && localPort === portHex && fields[9])
123
+ inodes.add(fields[9]);
124
+ }
125
+ }
126
+ return inodes;
127
+ }
128
+ function processOwnsSocket(pid, inodes) {
129
+ if (inodes.size === 0)
130
+ return false;
131
+ let descriptors;
132
+ try {
133
+ descriptors = readdirSync(`/proc/${pid}/fd`);
134
+ }
135
+ catch {
136
+ return false;
137
+ }
138
+ for (const descriptor of descriptors) {
139
+ try {
140
+ const target = readlinkSync(`/proc/${pid}/fd/${descriptor}`);
141
+ const match = target.match(/^socket:\[(\d+)]$/);
142
+ if (match && inodes.has(match[1]))
143
+ return true;
144
+ }
145
+ catch {
146
+ // File descriptors can disappear while being inspected.
147
+ }
148
+ }
149
+ return false;
150
+ }
151
+ export function findPreviewTargetProcess(host, port) {
152
+ const inodes = loopbackSocketInodes(host, port);
153
+ if (inodes.size === 0)
154
+ return undefined;
155
+ let entries;
156
+ try {
157
+ entries = readdirSync("/proc");
158
+ }
159
+ catch {
160
+ return undefined;
161
+ }
162
+ const owners = [];
163
+ for (const entry of entries) {
164
+ if (!/^\d+$/.test(entry))
165
+ continue;
166
+ const pid = Number(entry);
167
+ if (!processOwnsSocket(pid, inodes))
168
+ continue;
169
+ const startTicks = previewProcessStartTicks(pid);
170
+ if (startTicks)
171
+ owners.push({ pid, startTicks });
172
+ }
173
+ return owners.length === 1 ? owners[0] : undefined;
174
+ }
175
+ export function isPreviewTargetProcessCurrent(exposure, options = {}) {
176
+ if (process.platform !== "linux")
177
+ return true;
178
+ if (!exposure.targetProcessId || !exposure.targetProcessStartTicks)
179
+ return false;
180
+ const cacheMs = options.cacheMs ?? 1_000;
181
+ const cacheKey = [
182
+ exposure.id,
183
+ exposure.targetHost,
184
+ exposure.targetPort,
185
+ exposure.targetProcessId,
186
+ exposure.targetProcessStartTicks,
187
+ ].join(":");
188
+ const cached = targetIdentityCache.get(cacheKey);
189
+ const now = Date.now();
190
+ if (cached && now - cached.checkedAt < cacheMs)
191
+ return cached.current;
192
+ const current = previewProcessStartTicks(exposure.targetProcessId) === exposure.targetProcessStartTicks &&
193
+ processOwnsSocket(exposure.targetProcessId, loopbackSocketInodes(exposure.targetHost, exposure.targetPort));
194
+ if (targetIdentityCache.size >= 1_024)
195
+ targetIdentityCache.clear();
196
+ targetIdentityCache.set(cacheKey, { checkedAt: now, current });
197
+ return current;
198
+ }
@@ -0,0 +1,11 @@
1
+ import { definePiboPlugin } from "../plugins/registry.js";
2
+ import { createPreviewWebApp } from "./web-app.js";
3
+ export function createPiboPreviewPlugin(options = {}) {
4
+ return definePiboPlugin({
5
+ id: "pibo.session-live-previews",
6
+ name: "Pibo Session Live Previews",
7
+ register(api) {
8
+ api.registerWebApp(createPreviewWebApp(options));
9
+ },
10
+ });
11
+ }
@@ -0,0 +1,38 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { basename } from "node:path";
4
+ const command = process.env.PIBO_PREVIEW_START_COMMAND;
5
+ if (!command)
6
+ throw new Error("Preview supervisor requires a start command");
7
+ const environment = { ...process.env };
8
+ delete environment.PIBO_PREVIEW_START_COMMAND;
9
+ const shell = previewShell();
10
+ const child = spawn(shell.command, shell.args(command), {
11
+ env: environment,
12
+ stdio: "ignore",
13
+ windowsHide: true,
14
+ });
15
+ child.once("error", (error) => {
16
+ console.error(`Preview supervisor could not start its command: ${error.message}`);
17
+ process.exitCode = 1;
18
+ });
19
+ child.once("exit", (code, signal) => {
20
+ process.exitCode = code ?? (signal ? 1 : 0);
21
+ });
22
+ function previewShell() {
23
+ if (process.platform === "win32") {
24
+ return {
25
+ command: process.env.ComSpec ?? "cmd.exe",
26
+ args: (value) => ["/d", "/s", "/c", value],
27
+ };
28
+ }
29
+ const shell = process.env.SHELL && existsSync(process.env.SHELL)
30
+ ? process.env.SHELL
31
+ : existsSync("/bin/bash")
32
+ ? "/bin/bash"
33
+ : "/bin/sh";
34
+ return {
35
+ command: shell,
36
+ args: (value) => basename(shell).includes("bash") ? ["-lc", value] : ["-c", value],
37
+ };
38
+ }
@@ -0,0 +1,387 @@
1
+ import { request as httpRequest } from "node:http";
2
+ import { isIP } from "node:net";
3
+ export const PREVIEW_SESSION_COOKIE = "__Host-pibo_preview_session";
4
+ export const PREVIEW_INSECURE_SESSION_COOKIE = "pibo_preview_session";
5
+ export const DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS = 128;
6
+ export const DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS_PER_PREVIEW = 32;
7
+ const HOP_BY_HOP_HEADERS = new Set([
8
+ "connection",
9
+ "keep-alive",
10
+ "proxy-authenticate",
11
+ "proxy-authorization",
12
+ "te",
13
+ "trailer",
14
+ "transfer-encoding",
15
+ "upgrade",
16
+ ]);
17
+ export class PreviewProxyLimiter {
18
+ maxTotal;
19
+ maxPerPreview;
20
+ total = 0;
21
+ perPreview = new Map();
22
+ constructor(maxTotal = DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS, maxPerPreview = DEFAULT_MAX_PREVIEW_PROXY_CONNECTIONS_PER_PREVIEW) {
23
+ this.maxTotal = maxTotal;
24
+ this.maxPerPreview = maxPerPreview;
25
+ if (!Number.isInteger(maxTotal) || maxTotal < 1 || maxTotal > 10_000) {
26
+ throw new Error("Preview proxy total connection limit must be between 1 and 10000");
27
+ }
28
+ if (!Number.isInteger(maxPerPreview) || maxPerPreview < 1 || maxPerPreview > maxTotal) {
29
+ throw new Error("Preview proxy per-preview connection limit must be positive and no greater than the total limit");
30
+ }
31
+ }
32
+ tryAcquire(previewId) {
33
+ const current = this.perPreview.get(previewId) ?? 0;
34
+ if (this.total >= this.maxTotal || current >= this.maxPerPreview)
35
+ return undefined;
36
+ this.total += 1;
37
+ this.perPreview.set(previewId, current + 1);
38
+ let released = false;
39
+ return () => {
40
+ if (released)
41
+ return;
42
+ released = true;
43
+ this.total -= 1;
44
+ const next = (this.perPreview.get(previewId) ?? 1) - 1;
45
+ if (next > 0)
46
+ this.perPreview.set(previewId, next);
47
+ else
48
+ this.perPreview.delete(previewId);
49
+ };
50
+ }
51
+ snapshot() {
52
+ return { total: this.total, previews: this.perPreview.size };
53
+ }
54
+ }
55
+ export function previewSessionCookieName(protocol) {
56
+ return protocol === "https:" ? PREVIEW_SESSION_COOKIE : PREVIEW_INSECURE_SESSION_COOKIE;
57
+ }
58
+ function firstHeader(value) {
59
+ return Array.isArray(value) ? value[0] : value;
60
+ }
61
+ export function cookieValue(header, name) {
62
+ for (const item of header?.split(";") ?? []) {
63
+ const separator = item.indexOf("=");
64
+ if (separator < 0)
65
+ continue;
66
+ if (item.slice(0, separator).trim() !== name)
67
+ continue;
68
+ try {
69
+ return decodeURIComponent(item.slice(separator + 1).trim());
70
+ }
71
+ catch {
72
+ return undefined;
73
+ }
74
+ }
75
+ return undefined;
76
+ }
77
+ function isPiboCredentialCookie(name) {
78
+ const normalized = name.toLowerCase();
79
+ return normalized.startsWith("pibo_") ||
80
+ normalized.startsWith("__host-pibo_") ||
81
+ normalized.startsWith("__secure-pibo_") ||
82
+ normalized.includes("better-auth");
83
+ }
84
+ function withoutPiboCredentialCookies(header) {
85
+ const cookies = (header?.split(";") ?? [])
86
+ .map((item) => item.trim())
87
+ .filter((item) => {
88
+ const separator = item.indexOf("=");
89
+ if (!item || separator < 0)
90
+ return false;
91
+ return !isPiboCredentialCookie(item.slice(0, separator).trim());
92
+ });
93
+ return cookies.length ? cookies.join("; ") : undefined;
94
+ }
95
+ function isCredentialOrProxyMetadataHeader(name) {
96
+ return name === "authorization" ||
97
+ name === "forwarded" ||
98
+ name === "proxy-authorization" ||
99
+ name === "remote-user" ||
100
+ name === "x-real-ip" ||
101
+ name.startsWith("x-pibo-") ||
102
+ name.startsWith("x-forwarded-") ||
103
+ name.startsWith("x-auth-") ||
104
+ name.startsWith("x-original-") ||
105
+ name.startsWith("x-goog-authenticated-user-") ||
106
+ name.startsWith("cf-access-");
107
+ }
108
+ function upstreamOrigin(exposure) {
109
+ const host = exposure.targetHost === "::1" ? `[::1]` : exposure.targetHost;
110
+ return `http://${host}:${exposure.targetPort}`;
111
+ }
112
+ function rewriteOriginHeader(value, exposure) {
113
+ if (!value)
114
+ return undefined;
115
+ if (value === "null")
116
+ return value;
117
+ try {
118
+ const parsed = new URL(value);
119
+ const upstream = new URL(upstreamOrigin(exposure));
120
+ parsed.protocol = upstream.protocol;
121
+ parsed.hostname = upstream.hostname;
122
+ parsed.port = upstream.port;
123
+ return parsed.toString();
124
+ }
125
+ catch {
126
+ return undefined;
127
+ }
128
+ }
129
+ function requestHeaders(incoming, exposure, requestURL, websocket) {
130
+ const headers = {};
131
+ for (const [name, value] of Object.entries(incoming)) {
132
+ const normalized = name.toLowerCase();
133
+ if (HOP_BY_HOP_HEADERS.has(normalized) || normalized === "host" || isCredentialOrProxyMetadataHeader(normalized))
134
+ continue;
135
+ if (normalized === "cookie") {
136
+ const cookie = withoutPiboCredentialCookies(firstHeader(value));
137
+ if (cookie)
138
+ headers.cookie = cookie;
139
+ continue;
140
+ }
141
+ if (normalized === "origin") {
142
+ const origin = rewriteOriginHeader(firstHeader(value), exposure);
143
+ if (origin)
144
+ headers.origin = origin;
145
+ continue;
146
+ }
147
+ if (normalized === "referer") {
148
+ const referer = rewriteOriginHeader(firstHeader(value), exposure);
149
+ if (referer)
150
+ headers.referer = referer;
151
+ continue;
152
+ }
153
+ headers[normalized] = value;
154
+ }
155
+ const targetHost = exposure.targetHost === "::1" ? `[::1]` : exposure.targetHost;
156
+ headers.host = `${targetHost}:${exposure.targetPort}`;
157
+ headers["x-forwarded-host"] = requestURL.host;
158
+ headers["x-forwarded-proto"] = requestURL.protocol.slice(0, -1);
159
+ if (websocket) {
160
+ headers.connection = "Upgrade";
161
+ headers.upgrade = incoming.upgrade ?? "websocket";
162
+ }
163
+ return headers;
164
+ }
165
+ function isForbiddenLocalRedirectHost(hostname) {
166
+ const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, "");
167
+ if (normalized === "localhost" || normalized.endsWith(".localhost"))
168
+ return true;
169
+ const family = isIP(normalized);
170
+ if (family === 4) {
171
+ const [a, b] = normalized.split(".").map(Number);
172
+ return a === 0 || a === 10 || a === 127 || a >= 224 ||
173
+ a === 169 && b === 254 ||
174
+ a === 172 && b >= 16 && b <= 31 ||
175
+ a === 192 && b === 168 ||
176
+ a === 100 && b >= 64 && b <= 127;
177
+ }
178
+ if (family === 6) {
179
+ return normalized === "::" || normalized === "::1" ||
180
+ normalized.startsWith("fc") || normalized.startsWith("fd") ||
181
+ normalized.startsWith("fe8") || normalized.startsWith("fe9") ||
182
+ normalized.startsWith("fea") || normalized.startsWith("feb") ||
183
+ normalized.startsWith("::ffff:");
184
+ }
185
+ return false;
186
+ }
187
+ export function sanitizePreviewRedirectLocation(value, exposure, previewOrigin) {
188
+ if (/[\u0000-\u001f\u007f]/.test(value))
189
+ return undefined;
190
+ try {
191
+ const location = new URL(value, previewOrigin);
192
+ const upstream = new URL(upstreamOrigin(exposure));
193
+ if (location.origin === upstream.origin) {
194
+ const preview = new URL(previewOrigin);
195
+ location.protocol = preview.protocol;
196
+ location.hostname = preview.hostname;
197
+ location.port = preview.port;
198
+ }
199
+ if (location.protocol !== "http:" && location.protocol !== "https:")
200
+ return undefined;
201
+ if (location.origin !== previewOrigin && isForbiddenLocalRedirectHost(location.hostname))
202
+ return undefined;
203
+ return location.toString();
204
+ }
205
+ catch {
206
+ return undefined;
207
+ }
208
+ }
209
+ export function sanitizePreviewSetCookie(value) {
210
+ if (/[\u0000-\u001f\u007f]/.test(value))
211
+ return undefined;
212
+ const [nameValue, ...attributes] = value.split(";");
213
+ const separator = nameValue?.indexOf("=") ?? -1;
214
+ if (!nameValue || separator <= 0 || isPiboCredentialCookie(nameValue.slice(0, separator).trim()))
215
+ return undefined;
216
+ const rewritten = [nameValue, ...attributes.filter((attribute) => !/^\s*domain\s*=/i.test(attribute))];
217
+ return rewritten.filter(Boolean).join(";");
218
+ }
219
+ function rewriteFrameAncestors(value, piboOrigin) {
220
+ if (!piboOrigin)
221
+ return value;
222
+ const directives = (value ?? "")
223
+ .split(";")
224
+ .map((item) => item.trim())
225
+ .filter((item) => item && !item.toLowerCase().startsWith("frame-ancestors"));
226
+ directives.push(`frame-ancestors ${piboOrigin}`);
227
+ return directives.join("; ");
228
+ }
229
+ function responseHeaders(incoming, exposure, previewOrigin, piboOrigin) {
230
+ const headers = {};
231
+ for (const [name, value] of Object.entries(incoming)) {
232
+ const normalized = name.toLowerCase();
233
+ if (HOP_BY_HOP_HEADERS.has(normalized) || normalized === "x-frame-options" || normalized.startsWith("x-pibo-"))
234
+ continue;
235
+ if (normalized === "set-cookie") {
236
+ const values = (Array.isArray(value) ? value : value ? [value] : [])
237
+ .map(sanitizePreviewSetCookie)
238
+ .filter((item) => Boolean(item));
239
+ if (values.length)
240
+ headers[normalized] = values;
241
+ continue;
242
+ }
243
+ if (normalized === "location") {
244
+ const location = sanitizePreviewRedirectLocation(firstHeader(value) ?? "", exposure, previewOrigin);
245
+ if (location)
246
+ headers.location = location;
247
+ continue;
248
+ }
249
+ if (normalized === "content-security-policy") {
250
+ headers[normalized] = rewriteFrameAncestors(firstHeader(value), piboOrigin);
251
+ continue;
252
+ }
253
+ if (normalized === "access-control-allow-origin" && typeof value === "string") {
254
+ headers[normalized] = value === upstreamOrigin(exposure) ? previewOrigin : value;
255
+ continue;
256
+ }
257
+ headers[normalized] = value;
258
+ }
259
+ if (!headers["content-security-policy"] && piboOrigin) {
260
+ headers["content-security-policy"] = `frame-ancestors ${piboOrigin}`;
261
+ }
262
+ headers["referrer-policy"] = "no-referrer";
263
+ return headers;
264
+ }
265
+ function targetOptions(exposure, path, headers, method) {
266
+ return {
267
+ host: exposure.targetHost,
268
+ port: exposure.targetPort,
269
+ method: method ?? "GET",
270
+ path,
271
+ headers,
272
+ };
273
+ }
274
+ function upstreamRequestPath(requestURL) {
275
+ return `${requestURL.pathname}${requestURL.search}`;
276
+ }
277
+ export function proxyPreviewHttp(input) {
278
+ return new Promise((resolve) => {
279
+ let settled = false;
280
+ let upstreamRequest;
281
+ const finish = () => {
282
+ if (settled)
283
+ return;
284
+ settled = true;
285
+ resolve();
286
+ };
287
+ input.response.once("finish", finish);
288
+ const headers = requestHeaders(input.request.headers, input.exposure, input.requestURL, false);
289
+ const upstream = httpRequest(targetOptions(input.exposure, upstreamRequestPath(input.requestURL), headers, input.request.method), (upstreamResponse) => {
290
+ upstream.setTimeout(0);
291
+ input.response.writeHead(upstreamResponse.statusCode ?? 502, upstreamResponse.statusMessage, responseHeaders(upstreamResponse.headers, input.exposure, input.requestURL.origin, input.piboOrigin));
292
+ upstreamResponse.pipe(input.response);
293
+ upstreamResponse.once("error", () => {
294
+ if (!input.response.writableEnded)
295
+ input.response.destroy();
296
+ });
297
+ });
298
+ upstreamRequest = upstream;
299
+ upstream.setTimeout(5_000, () => upstream.destroy(new Error("Preview upstream connection timed out")));
300
+ upstream.once("error", (error) => {
301
+ if (!input.response.headersSent) {
302
+ input.response.writeHead(502, { "content-type": "application/json", "cache-control": "no-store" });
303
+ input.response.end(JSON.stringify({ error: "Preview upstream unavailable" }));
304
+ }
305
+ else if (!input.response.writableEnded)
306
+ input.response.destroy(error);
307
+ });
308
+ input.request.once("aborted", () => upstream.destroy());
309
+ input.response.once("close", () => {
310
+ upstreamRequest?.destroy();
311
+ finish();
312
+ });
313
+ input.request.pipe(upstream);
314
+ });
315
+ }
316
+ function writeUpgradeResponse(socket, response, exposure, previewOrigin) {
317
+ const lines = ["HTTP/1.1 101 Switching Protocols", "Connection: Upgrade", "Upgrade: websocket"];
318
+ const headers = responseHeaders(response.headers, exposure, previewOrigin);
319
+ for (const [name, rawValue] of Object.entries(headers)) {
320
+ if (name === "connection" || name === "upgrade" || name === "content-length")
321
+ continue;
322
+ for (const value of Array.isArray(rawValue) ? rawValue : rawValue === undefined ? [] : [rawValue]) {
323
+ lines.push(`${name}: ${value}`);
324
+ }
325
+ }
326
+ socket.write(`${lines.join("\r\n")}\r\n\r\n`);
327
+ }
328
+ export function proxyPreviewWebSocket(input) {
329
+ return new Promise((resolve) => {
330
+ let settled = false;
331
+ let upstreamSocket;
332
+ const finish = () => {
333
+ if (settled)
334
+ return;
335
+ settled = true;
336
+ resolve();
337
+ };
338
+ const headers = requestHeaders(input.request.headers, input.exposure, input.requestURL, true);
339
+ const upstreamRequest = httpRequest(targetOptions(input.exposure, upstreamRequestPath(input.requestURL), headers, "GET"));
340
+ let upgraded = false;
341
+ const closeFromClient = () => {
342
+ upstreamRequest.destroy();
343
+ upstreamSocket?.destroy();
344
+ finish();
345
+ };
346
+ input.socket.once("end", closeFromClient);
347
+ input.socket.once("close", closeFromClient);
348
+ input.socket.once("error", closeFromClient);
349
+ upstreamRequest.setTimeout(5_000, () => upstreamRequest.destroy(new Error("Preview WebSocket connection timed out")));
350
+ upstreamRequest.once("upgrade", (response, socket, upstreamHead) => {
351
+ upgraded = true;
352
+ upstreamSocket = socket;
353
+ upstreamRequest.setTimeout(0);
354
+ writeUpgradeResponse(input.socket, response, input.exposure, input.requestURL.origin);
355
+ if (input.head.length)
356
+ socket.write(input.head);
357
+ if (upstreamHead.length)
358
+ input.socket.write(upstreamHead);
359
+ socket.pipe(input.socket);
360
+ input.socket.pipe(socket);
361
+ const closeBoth = () => {
362
+ socket.destroy();
363
+ input.socket.destroy();
364
+ finish();
365
+ };
366
+ socket.once("error", closeBoth);
367
+ input.socket.once("error", closeBoth);
368
+ socket.once("close", () => {
369
+ input.socket.destroy();
370
+ finish();
371
+ });
372
+ });
373
+ upstreamRequest.once("response", (response) => {
374
+ if (upgraded)
375
+ return;
376
+ response.resume();
377
+ input.socket.end(`HTTP/1.1 ${response.statusCode ?? 502} ${response.statusMessage ?? "Bad Gateway"}\r\nConnection: close\r\n\r\n`);
378
+ finish();
379
+ });
380
+ upstreamRequest.once("error", () => {
381
+ if (!input.socket.destroyed)
382
+ input.socket.end("HTTP/1.1 502 Bad Gateway\r\nConnection: close\r\n\r\n");
383
+ finish();
384
+ });
385
+ upstreamRequest.end();
386
+ });
387
+ }