@farm.js/preview-gateway 0.1.0-beta.99 → 0.1.0

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.
package/README.md CHANGED
@@ -2,10 +2,10 @@
2
2
 
3
3
  Farm.js preview gateway for Vercel-hosted instant preview URLs
4
4
 
5
- Farm.js is currently in beta.
5
+ See [Stability and Support](https://farmjs.dev/docs/stability) for what this package guarantees.
6
6
 
7
7
  ```bash
8
- npm install @farm.js/preview-gateway@beta
8
+ npm install @farm.js/preview-gateway
9
9
  ```
10
10
 
11
11
  See the [Farm.js repository](https://github.com/farming-labs/farm.js) for documentation, examples, and support.
@@ -13,3 +13,16 @@ See the [Farm.js repository](https://github.com/farming-labs/farm.js) for docume
13
13
  The gateway limits public request bodies and local preview response bodies to 5 MiB by default. Use `maxBodyBytes` and `maxResponseBodyBytes` when creating the handler to set lower or higher limits. Oversized local responses are rejected at upload and the waiting public request receives a `502` response instead of being left pending.
14
14
 
15
15
  Public disconnects are propagated through the request queue. The polling agent aborts the matching localhost fetch instead of leaving abandoned app work running until its timeout.
16
+
17
+ Managed deployments can configure `auth` with a GitHub OAuth client id and a signing secret. The gateway then exposes the CLI login exchange, issues account-bound tunnel grants scoped to one preview name and absolute expiry, and requires a grant before creating a session:
18
+
19
+ ```ts
20
+ const handler = createPreviewGatewayHandler({
21
+ auth: {
22
+ githubClientId: process.env.FARM_PREVIEW_GITHUB_CLIENT_ID,
23
+ signingSecret: process.env.FARM_PREVIEW_AUTH_SECRET,
24
+ },
25
+ });
26
+ ```
27
+
28
+ Keep the signing secret server-only and at least 32 bytes long. GitHub access tokens are used only to resolve the account identity during exchange and are not included in Farm credentials.
@@ -1,5 +1,69 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
2
 
3
+ //#region src/auth.d.ts
4
+ interface PreviewManagedAuthOptions {
5
+ /** Secret used to sign account tokens and one-preview tunnel grants. */
6
+ signingSecret: string;
7
+ /** Public client id for the Farming Labs GitHub OAuth app. */
8
+ githubClientId: string;
9
+ accountTokenTtlMs?: number;
10
+ defaultSessionTtlMs?: number;
11
+ maxSessionTtlMs?: number;
12
+ githubApiUrl?: string;
13
+ fetch?: typeof fetch;
14
+ }
15
+ interface PreviewAccountIdentity {
16
+ provider: "github";
17
+ subject: string;
18
+ login: string;
19
+ name?: string;
20
+ avatarUrl?: string;
21
+ }
22
+ interface PreviewAccountClaims extends PreviewAccountIdentity {
23
+ kind: "account";
24
+ issuedAt: number;
25
+ expiresAt: number;
26
+ }
27
+ interface PreviewTunnelGrantClaims {
28
+ kind: "tunnel";
29
+ subject: string;
30
+ login: string;
31
+ name: string;
32
+ issuedAt: number;
33
+ expiresAt: number;
34
+ }
35
+ interface PreviewTunnelGrant {
36
+ token: string;
37
+ name: string;
38
+ expiresAt: number;
39
+ }
40
+ declare function getPreviewAuthPublicConfig(options: PreviewManagedAuthOptions): {
41
+ enabled: true;
42
+ provider: "github";
43
+ clientId: string;
44
+ scope: string;
45
+ defaultSessionTtlMs: number;
46
+ maxSessionTtlMs: number;
47
+ };
48
+ declare function exchangeGitHubPreviewToken(providerToken: string, options: PreviewManagedAuthOptions): Promise<{
49
+ token: string;
50
+ expiresAt: number;
51
+ user: PreviewAccountIdentity;
52
+ }>;
53
+ declare function verifyPreviewAccountToken(token: string, options: Pick<PreviewManagedAuthOptions, "signingSecret">): PreviewAccountClaims;
54
+ declare function issuePreviewTunnelGrant(account: PreviewAccountClaims, input: {
55
+ name: string;
56
+ expiresInMs?: number;
57
+ }, options: PreviewManagedAuthOptions): PreviewTunnelGrant;
58
+ declare function verifyPreviewTunnelGrant(token: string, options: Pick<PreviewManagedAuthOptions, "signingSecret"> & {
59
+ name?: string;
60
+ }): PreviewTunnelGrantClaims;
61
+ declare function readPreviewBearerToken(request: Request): string | undefined;
62
+ declare class PreviewAuthError extends Error {
63
+ status: number;
64
+ constructor(status: number, message: string);
65
+ }
66
+ //#endregion
3
67
  //#region src/index.d.ts
4
68
  interface PreviewGatewayOptions {
5
69
  domain?: string;
@@ -12,6 +76,8 @@ interface PreviewGatewayOptions {
12
76
  pollIntervalMs?: number;
13
77
  maxBodyBytes?: number;
14
78
  maxResponseBodyBytes?: number;
79
+ /** Managed account login and one-preview grant configuration. */
80
+ auth?: PreviewManagedAuthOptions;
15
81
  }
16
82
  interface PreviewGatewaySession {
17
83
  id: string;
@@ -52,8 +118,18 @@ interface PreviewGatewayStore {
52
118
  deleteResponse(sessionId: string, requestId: string): Promise<void>;
53
119
  deleteSession(session: PreviewGatewaySession): Promise<void>;
54
120
  }
55
- declare function createPreviewGatewayHandler(options?: PreviewGatewayOptions): (request: Request) => Promise<Response>;
56
- declare function createNodePreviewGatewayHandler(options?: PreviewGatewayOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
121
+ /**
122
+ * A gateway request handler, plus the name-ownership probe another preview
123
+ * transport needs before it claims the same public hostname.
124
+ */
125
+ type PreviewGatewayHandler = ((request: Request) => Promise<Response>) & {
126
+ isPreviewNameClaimed(name: string): Promise<boolean>;
127
+ };
128
+ type NodePreviewGatewayHandler = ((req: IncomingMessage, res: ServerResponse) => Promise<void>) & {
129
+ isPreviewNameClaimed(name: string): Promise<boolean>;
130
+ };
131
+ declare function createPreviewGatewayHandler(options?: PreviewGatewayOptions): PreviewGatewayHandler;
132
+ declare function createNodePreviewGatewayHandler(options?: PreviewGatewayOptions): NodePreviewGatewayHandler;
57
133
  declare function createPreviewGatewayStoreFromEnv(): PreviewGatewayStore;
58
134
  declare class MemoryPreviewGatewayStore implements PreviewGatewayStore {
59
135
  private sessions;
@@ -63,7 +139,7 @@ declare class MemoryPreviewGatewayStore implements PreviewGatewayStore {
63
139
  createSession(session: PreviewGatewaySession): Promise<void>;
64
140
  getSessionByName(name: string): Promise<PreviewGatewaySession | undefined>;
65
141
  getSessionById(id: string): Promise<PreviewGatewaySession | undefined>;
66
- touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void>;
142
+ touchSession(session: PreviewGatewaySession, _ttlMs: number): Promise<void>;
67
143
  enqueueRequest(sessionId: string, request: PreviewGatewayRequest): Promise<void>;
68
144
  takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]>;
69
145
  saveResponse(sessionId: string, requestId: string, response: PreviewGatewayResponse): Promise<void>;
@@ -96,5 +172,5 @@ declare class RedisRestPreviewGatewayStore implements PreviewGatewayStore {
96
172
  private responseKey;
97
173
  }
98
174
  //#endregion
99
- export { MemoryPreviewGatewayStore, PreviewGatewayOptions, PreviewGatewayRequest, PreviewGatewayResponse, PreviewGatewaySession, PreviewGatewayStore, RedisRestPreviewGatewayStore, createNodePreviewGatewayHandler, createPreviewGatewayHandler, createPreviewGatewayStoreFromEnv };
100
- //# sourceMappingURL=index-BGlJkAXq.d.ts.map
175
+ export { MemoryPreviewGatewayStore, NodePreviewGatewayHandler, type PreviewAccountClaims, type PreviewAccountIdentity, PreviewAuthError, PreviewGatewayHandler, PreviewGatewayOptions, PreviewGatewayRequest, PreviewGatewayResponse, PreviewGatewaySession, PreviewGatewayStore, type PreviewManagedAuthOptions, type PreviewTunnelGrant, type PreviewTunnelGrantClaims, RedisRestPreviewGatewayStore, createNodePreviewGatewayHandler, createPreviewGatewayHandler, createPreviewGatewayStoreFromEnv, exchangeGitHubPreviewToken, getPreviewAuthPublicConfig, issuePreviewTunnelGrant, readPreviewBearerToken, verifyPreviewAccountToken, verifyPreviewTunnelGrant };
176
+ //# sourceMappingURL=index-C4ZBBfyt.d.ts.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,69 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
2
 
3
+ //#region src/auth.d.ts
4
+ interface PreviewManagedAuthOptions {
5
+ /** Secret used to sign account tokens and one-preview tunnel grants. */
6
+ signingSecret: string;
7
+ /** Public client id for the Farming Labs GitHub OAuth app. */
8
+ githubClientId: string;
9
+ accountTokenTtlMs?: number;
10
+ defaultSessionTtlMs?: number;
11
+ maxSessionTtlMs?: number;
12
+ githubApiUrl?: string;
13
+ fetch?: typeof fetch;
14
+ }
15
+ interface PreviewAccountIdentity {
16
+ provider: "github";
17
+ subject: string;
18
+ login: string;
19
+ name?: string;
20
+ avatarUrl?: string;
21
+ }
22
+ interface PreviewAccountClaims extends PreviewAccountIdentity {
23
+ kind: "account";
24
+ issuedAt: number;
25
+ expiresAt: number;
26
+ }
27
+ interface PreviewTunnelGrantClaims {
28
+ kind: "tunnel";
29
+ subject: string;
30
+ login: string;
31
+ name: string;
32
+ issuedAt: number;
33
+ expiresAt: number;
34
+ }
35
+ interface PreviewTunnelGrant {
36
+ token: string;
37
+ name: string;
38
+ expiresAt: number;
39
+ }
40
+ declare function getPreviewAuthPublicConfig(options: PreviewManagedAuthOptions): {
41
+ enabled: true;
42
+ provider: "github";
43
+ clientId: string;
44
+ scope: string;
45
+ defaultSessionTtlMs: number;
46
+ maxSessionTtlMs: number;
47
+ };
48
+ declare function exchangeGitHubPreviewToken(providerToken: string, options: PreviewManagedAuthOptions): Promise<{
49
+ token: string;
50
+ expiresAt: number;
51
+ user: PreviewAccountIdentity;
52
+ }>;
53
+ declare function verifyPreviewAccountToken(token: string, options: Pick<PreviewManagedAuthOptions, "signingSecret">): PreviewAccountClaims;
54
+ declare function issuePreviewTunnelGrant(account: PreviewAccountClaims, input: {
55
+ name: string;
56
+ expiresInMs?: number;
57
+ }, options: PreviewManagedAuthOptions): PreviewTunnelGrant;
58
+ declare function verifyPreviewTunnelGrant(token: string, options: Pick<PreviewManagedAuthOptions, "signingSecret"> & {
59
+ name?: string;
60
+ }): PreviewTunnelGrantClaims;
61
+ declare function readPreviewBearerToken(request: Request): string | undefined;
62
+ declare class PreviewAuthError extends Error {
63
+ status: number;
64
+ constructor(status: number, message: string);
65
+ }
66
+ //#endregion
3
67
  //#region src/index.d.ts
4
68
  interface PreviewGatewayOptions {
5
69
  domain?: string;
@@ -12,6 +76,8 @@ interface PreviewGatewayOptions {
12
76
  pollIntervalMs?: number;
13
77
  maxBodyBytes?: number;
14
78
  maxResponseBodyBytes?: number;
79
+ /** Managed account login and one-preview grant configuration. */
80
+ auth?: PreviewManagedAuthOptions;
15
81
  }
16
82
  interface PreviewGatewaySession {
17
83
  id: string;
@@ -52,8 +118,18 @@ interface PreviewGatewayStore {
52
118
  deleteResponse(sessionId: string, requestId: string): Promise<void>;
53
119
  deleteSession(session: PreviewGatewaySession): Promise<void>;
54
120
  }
55
- declare function createPreviewGatewayHandler(options?: PreviewGatewayOptions): (request: Request) => Promise<Response>;
56
- declare function createNodePreviewGatewayHandler(options?: PreviewGatewayOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
121
+ /**
122
+ * A gateway request handler, plus the name-ownership probe another preview
123
+ * transport needs before it claims the same public hostname.
124
+ */
125
+ type PreviewGatewayHandler = ((request: Request) => Promise<Response>) & {
126
+ isPreviewNameClaimed(name: string): Promise<boolean>;
127
+ };
128
+ type NodePreviewGatewayHandler = ((req: IncomingMessage, res: ServerResponse) => Promise<void>) & {
129
+ isPreviewNameClaimed(name: string): Promise<boolean>;
130
+ };
131
+ declare function createPreviewGatewayHandler(options?: PreviewGatewayOptions): PreviewGatewayHandler;
132
+ declare function createNodePreviewGatewayHandler(options?: PreviewGatewayOptions): NodePreviewGatewayHandler;
57
133
  declare function createPreviewGatewayStoreFromEnv(): PreviewGatewayStore;
58
134
  declare class MemoryPreviewGatewayStore implements PreviewGatewayStore {
59
135
  private sessions;
@@ -63,7 +139,7 @@ declare class MemoryPreviewGatewayStore implements PreviewGatewayStore {
63
139
  createSession(session: PreviewGatewaySession): Promise<void>;
64
140
  getSessionByName(name: string): Promise<PreviewGatewaySession | undefined>;
65
141
  getSessionById(id: string): Promise<PreviewGatewaySession | undefined>;
66
- touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void>;
142
+ touchSession(session: PreviewGatewaySession, _ttlMs: number): Promise<void>;
67
143
  enqueueRequest(sessionId: string, request: PreviewGatewayRequest): Promise<void>;
68
144
  takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]>;
69
145
  saveResponse(sessionId: string, requestId: string, response: PreviewGatewayResponse): Promise<void>;
@@ -96,5 +172,5 @@ declare class RedisRestPreviewGatewayStore implements PreviewGatewayStore {
96
172
  private responseKey;
97
173
  }
98
174
  //#endregion
99
- export { MemoryPreviewGatewayStore, PreviewGatewayOptions, PreviewGatewayRequest, PreviewGatewayResponse, PreviewGatewaySession, PreviewGatewayStore, RedisRestPreviewGatewayStore, createNodePreviewGatewayHandler, createPreviewGatewayHandler, createPreviewGatewayStoreFromEnv };
100
- //# sourceMappingURL=index-BGlJkAXq.d.ts.map
175
+ export { MemoryPreviewGatewayStore, NodePreviewGatewayHandler, type PreviewAccountClaims, type PreviewAccountIdentity, PreviewAuthError, PreviewGatewayHandler, PreviewGatewayOptions, PreviewGatewayRequest, PreviewGatewayResponse, PreviewGatewaySession, PreviewGatewayStore, type PreviewManagedAuthOptions, type PreviewTunnelGrant, type PreviewTunnelGrantClaims, RedisRestPreviewGatewayStore, createNodePreviewGatewayHandler, createPreviewGatewayHandler, createPreviewGatewayStoreFromEnv, exchangeGitHubPreviewToken, getPreviewAuthPublicConfig, issuePreviewTunnelGrant, readPreviewBearerToken, verifyPreviewAccountToken, verifyPreviewTunnelGrant };
176
+ //# sourceMappingURL=index-C4ZBBfyt.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1,150 @@
1
1
  import { setTimeout } from "node:timers/promises";
2
+ import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
3
+ //#region src/auth.ts
4
+ const TOKEN_PREFIX = "farm_preview_v1";
5
+ const DEFAULT_ACCOUNT_TOKEN_TTL_MS = 6048e5;
6
+ const DEFAULT_SESSION_TTL_MS$1 = 36e5;
7
+ const DEFAULT_MAX_SESSION_TTL_MS = 864e5;
8
+ const MIN_SESSION_TTL_MS = 6e4;
9
+ const MAX_CLOCK_SKEW_MS = 3e5;
10
+ function getPreviewAuthPublicConfig(options) {
11
+ validatePreviewAuthOptions(options);
12
+ const durations = resolvePreviewDurations(options);
13
+ return {
14
+ enabled: true,
15
+ provider: "github",
16
+ clientId: options.githubClientId,
17
+ scope: "read:user",
18
+ defaultSessionTtlMs: durations.defaultSessionTtlMs,
19
+ maxSessionTtlMs: durations.maxSessionTtlMs
20
+ };
21
+ }
22
+ async function exchangeGitHubPreviewToken(providerToken, options) {
23
+ validatePreviewAuthOptions(options);
24
+ if (!providerToken || providerToken.length > 4096) throw new PreviewAuthError(400, "A GitHub access token is required.");
25
+ const response = await (options.fetch || fetch)(`${options.githubApiUrl || "https://api.github.com"}/user`, {
26
+ headers: {
27
+ accept: "application/vnd.github+json",
28
+ authorization: `Bearer ${providerToken}`,
29
+ "user-agent": "farm-preview-gateway",
30
+ "x-github-api-version": "2026-03-10"
31
+ },
32
+ signal: AbortSignal.timeout(1e4)
33
+ });
34
+ if (!response.ok) {
35
+ await response.body?.cancel();
36
+ throw new PreviewAuthError(401, "GitHub could not verify this Farm Preview login.");
37
+ }
38
+ const profile = await response.json();
39
+ if (!Number.isSafeInteger(profile.id) || !profile.login) throw new PreviewAuthError(502, "GitHub returned an invalid user profile.");
40
+ const now = Date.now();
41
+ const expiresAt = now + (options.accountTokenTtlMs ?? DEFAULT_ACCOUNT_TOKEN_TTL_MS);
42
+ const user = {
43
+ provider: "github",
44
+ subject: String(profile.id),
45
+ login: profile.login,
46
+ ...profile.name ? { name: profile.name } : {},
47
+ ...profile.avatar_url ? { avatarUrl: profile.avatar_url } : {}
48
+ };
49
+ return {
50
+ token: signPreviewClaims({
51
+ kind: "account",
52
+ ...user,
53
+ issuedAt: now,
54
+ expiresAt
55
+ }, options.signingSecret),
56
+ expiresAt,
57
+ user
58
+ };
59
+ }
60
+ function verifyPreviewAccountToken(token, options) {
61
+ const claims = verifyPreviewClaims(token, options.signingSecret);
62
+ if (claims.kind !== "account" || claims.provider !== "github" || typeof claims.subject !== "string" || typeof claims.login !== "string") throw new PreviewAuthError(401, "The Farm Preview account token is invalid.");
63
+ return claims;
64
+ }
65
+ function issuePreviewTunnelGrant(account, input, options) {
66
+ validatePreviewAuthOptions(options);
67
+ const durations = resolvePreviewDurations(options);
68
+ if (input.expiresInMs !== void 0 && (!Number.isSafeInteger(input.expiresInMs) || input.expiresInMs <= 0)) throw new PreviewAuthError(400, "Preview expiry must be a positive integer.");
69
+ const requestedTtlMs = input.expiresInMs ?? durations.defaultSessionTtlMs;
70
+ const ttlMs = Math.max(MIN_SESSION_TTL_MS, Math.min(durations.maxSessionTtlMs, requestedTtlMs));
71
+ const issuedAt = Date.now();
72
+ const expiresAt = issuedAt + ttlMs;
73
+ return {
74
+ token: signPreviewClaims({
75
+ kind: "tunnel",
76
+ subject: account.subject,
77
+ login: account.login,
78
+ name: input.name,
79
+ issuedAt,
80
+ expiresAt,
81
+ nonce: randomUUID()
82
+ }, options.signingSecret),
83
+ name: input.name,
84
+ expiresAt
85
+ };
86
+ }
87
+ function verifyPreviewTunnelGrant(token, options) {
88
+ const claims = verifyPreviewClaims(token, options.signingSecret);
89
+ if (claims.kind !== "tunnel" || typeof claims.subject !== "string" || typeof claims.login !== "string" || typeof claims.name !== "string" || options.name !== void 0 && claims.name !== options.name) throw new PreviewAuthError(401, "The Farm Preview tunnel grant is invalid.");
90
+ return claims;
91
+ }
92
+ function readPreviewBearerToken(request) {
93
+ const authorization = request.headers.get("authorization");
94
+ if (!authorization || !/^Bearer /i.test(authorization)) return void 0;
95
+ return authorization.slice(7).trim() || void 0;
96
+ }
97
+ var PreviewAuthError = class extends Error {
98
+ constructor(status, message) {
99
+ super(message);
100
+ this.status = status;
101
+ }
102
+ };
103
+ function resolvePreviewDurations(options) {
104
+ const maxSessionTtlMs = Math.max(MIN_SESSION_TTL_MS, options.maxSessionTtlMs ?? DEFAULT_MAX_SESSION_TTL_MS);
105
+ return {
106
+ maxSessionTtlMs,
107
+ defaultSessionTtlMs: Math.min(maxSessionTtlMs, Math.max(MIN_SESSION_TTL_MS, options.defaultSessionTtlMs ?? DEFAULT_SESSION_TTL_MS$1))
108
+ };
109
+ }
110
+ function validatePreviewAuthOptions(options) {
111
+ if (Buffer.byteLength(options.signingSecret) < 32) throw new Error("Farm Preview auth signingSecret must contain at least 32 bytes.");
112
+ if (!options.githubClientId) throw new Error("Farm Preview auth requires a GitHub OAuth client id.");
113
+ for (const [name, value] of [
114
+ ["accountTokenTtlMs", options.accountTokenTtlMs],
115
+ ["defaultSessionTtlMs", options.defaultSessionTtlMs],
116
+ ["maxSessionTtlMs", options.maxSessionTtlMs]
117
+ ]) if (value !== void 0 && (!Number.isSafeInteger(value) || value <= 0)) throw new Error(`Farm Preview auth ${name} must be a positive integer.`);
118
+ }
119
+ function signPreviewClaims(claims, secret) {
120
+ const payload = Buffer.from(JSON.stringify(claims)).toString("base64url");
121
+ const signature = createHmac("sha256", secret).update(`${TOKEN_PREFIX}.${payload}`).digest("base64url");
122
+ return `${TOKEN_PREFIX}.${payload}.${signature}`;
123
+ }
124
+ function verifyPreviewClaims(token, secret) {
125
+ if (!token || token.length > 16384) throw new PreviewAuthError(401, "The Farm Preview credential is invalid.");
126
+ const [prefix, payload, presentedSignature, ...rest] = token.split(".");
127
+ if (prefix !== TOKEN_PREFIX || !payload || !presentedSignature || rest.length) throw new PreviewAuthError(401, "The Farm Preview credential is invalid.");
128
+ const expectedSignature = createHmac("sha256", secret).update(`${TOKEN_PREFIX}.${payload}`).digest();
129
+ let presented;
130
+ try {
131
+ presented = Buffer.from(presentedSignature, "base64url");
132
+ } catch {
133
+ throw new PreviewAuthError(401, "The Farm Preview credential is invalid.");
134
+ }
135
+ if (presented.length !== expectedSignature.length || !timingSafeEqual(presented, expectedSignature)) throw new PreviewAuthError(401, "The Farm Preview credential is invalid.");
136
+ let claims;
137
+ try {
138
+ claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
139
+ } catch {
140
+ throw new PreviewAuthError(401, "The Farm Preview credential is invalid.");
141
+ }
142
+ if (typeof claims !== "object" || claims === null || Array.isArray(claims) || !("issuedAt" in claims) || !("expiresAt" in claims) || !Number.isSafeInteger(claims.issuedAt) || !Number.isSafeInteger(claims.expiresAt) || Number(claims.issuedAt) > Date.now() + MAX_CLOCK_SKEW_MS || Number(claims.expiresAt) <= Date.now()) throw new PreviewAuthError(401, "The Farm Preview credential has expired or is invalid.");
143
+ return claims;
144
+ }
145
+ //#endregion
2
146
  //#region src/index.ts
3
- const DEFAULT_DOMAIN = "preview.farming-labs.dev";
147
+ const DEFAULT_DOMAIN = "preview.farmjs.dev";
4
148
  const DEFAULT_SESSION_TTL_MS = 18e5;
5
149
  const DEFAULT_CLIENT_HEARTBEAT_TIMEOUT_MS = 2e4;
6
150
  const DEFAULT_REQUEST_TIMEOUT_MS = 25e3;
@@ -31,6 +175,7 @@ function getHopByHopHeaderNames(headers) {
31
175
  return names;
32
176
  }
33
177
  function createPreviewGatewayHandler(options = {}) {
178
+ if (options.auth) getPreviewAuthPublicConfig(options.auth);
34
179
  const store = options.store || createPreviewGatewayStoreFromEnv();
35
180
  const config = {
36
181
  domain: normalizeDomain(options.domain || process.env.FARM_PREVIEW_DOMAIN || DEFAULT_DOMAIN),
@@ -41,9 +186,10 @@ function createPreviewGatewayHandler(options = {}) {
41
186
  pollTimeoutMs: options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
42
187
  pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
43
188
  maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,
44
- maxResponseBodyBytes: options.maxResponseBodyBytes ?? DEFAULT_MAX_RESPONSE_BODY_BYTES
189
+ maxResponseBodyBytes: options.maxResponseBodyBytes ?? DEFAULT_MAX_RESPONSE_BODY_BYTES,
190
+ auth: options.auth
45
191
  };
46
- return async function handlePreviewGatewayRequest(request) {
192
+ return Object.assign(async function handlePreviewGatewayRequest(request) {
47
193
  try {
48
194
  const url = new URL(request.url);
49
195
  if (request.method === "GET" && url.pathname === "/api/health") return json({
@@ -51,6 +197,19 @@ function createPreviewGatewayHandler(options = {}) {
51
197
  domain: config.domain,
52
198
  store: store.constructor.name || "PreviewGatewayStore"
53
199
  });
200
+ if (request.method === "GET" && url.pathname === "/api/auth/config") return json(config.auth ? getPreviewAuthPublicConfig(config.auth) : {
201
+ enabled: false,
202
+ defaultSessionTtlMs: config.sessionTtlMs,
203
+ maxSessionTtlMs: config.sessionTtlMs
204
+ });
205
+ if (request.method === "POST" && url.pathname === "/api/auth/exchange") {
206
+ if (!config.auth) return text("Managed preview authentication is not configured.", 404);
207
+ return await exchangePreviewAccount(request, config.auth);
208
+ }
209
+ if (request.method === "POST" && url.pathname === "/api/tunnel/grants") {
210
+ if (!config.auth) return text("Managed preview authentication is not configured.", 404);
211
+ return await createTunnelGrant(request, config.auth);
212
+ }
54
213
  if (request.method === "POST" && url.pathname === "/api/sessions") return await createSession(request, store, config);
55
214
  const sessionRoute = matchSessionRoute(url.pathname);
56
215
  if (sessionRoute) return await handleSessionRoute(request, url, store, config, sessionRoute);
@@ -63,14 +222,36 @@ function createPreviewGatewayHandler(options = {}) {
63
222
  health: "/api/health"
64
223
  }, 200);
65
224
  } catch (error) {
66
- if (error instanceof GatewayHttpError) return text(error.message, error.status);
225
+ if (error instanceof GatewayHttpError || error instanceof PreviewAuthError) return text(error.message, error.status);
67
226
  return text(error instanceof Error ? error.message : "Unexpected preview gateway error.", 500);
68
227
  }
69
- };
228
+ }, { async isPreviewNameClaimed(name) {
229
+ const claimed = sanitizePreviewName(name);
230
+ if (!claimed) return false;
231
+ const session = await store.getSessionByName(claimed);
232
+ return Boolean(session && isPreviewClientOnline(session, config));
233
+ } });
234
+ }
235
+ async function exchangePreviewAccount(request, auth) {
236
+ const input = await request.json().catch(() => ({}));
237
+ if (input.provider !== "github") throw new PreviewAuthError(400, "Farm Preview currently supports GitHub sign-in.");
238
+ return json(await exchangeGitHubPreviewToken(input.accessToken || "", auth));
239
+ }
240
+ async function createTunnelGrant(request, auth) {
241
+ const accountToken = readPreviewBearerToken(request);
242
+ if (!accountToken) throw new PreviewAuthError(401, "Sign in with Farm Preview before creating a tunnel grant.");
243
+ const account = verifyPreviewAccountToken(accountToken, auth);
244
+ const input = await request.json().catch(() => ({}));
245
+ const name = sanitizePreviewName(input.name);
246
+ if (!name) throw new PreviewAuthError(400, "A valid preview name is required.");
247
+ return json(issuePreviewTunnelGrant(account, {
248
+ name,
249
+ ...input.expiresInMs !== void 0 ? { expiresInMs: input.expiresInMs } : {}
250
+ }, auth));
70
251
  }
71
252
  function createNodePreviewGatewayHandler(options = {}) {
72
253
  const handler = createPreviewGatewayHandler(options);
73
- return async function nodePreviewGatewayHandler(req, res) {
254
+ return Object.assign(async function nodePreviewGatewayHandler(req, res) {
74
255
  const controller = new AbortController();
75
256
  const abort = () => controller.abort();
76
257
  const abortOnClose = () => {
@@ -96,7 +277,7 @@ function createNodePreviewGatewayHandler(options = {}) {
96
277
  req.off("aborted", abort);
97
278
  res.off("close", abortOnClose);
98
279
  }
99
- };
280
+ }, { isPreviewNameClaimed: handler.isPreviewNameClaimed });
100
281
  }
101
282
  function createPreviewGatewayStoreFromEnv() {
102
283
  const redisUrl = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;
@@ -132,9 +313,8 @@ var MemoryPreviewGatewayStore = class {
132
313
  }
133
314
  return session;
134
315
  }
135
- async touchSession(session, ttlMs) {
316
+ async touchSession(session, _ttlMs) {
136
317
  if (!this.sessions.has(session.id)) return;
137
- session.expiresAt = Date.now() + ttlMs;
138
318
  this.sessions.set(session.id, session);
139
319
  const nameOwner = this.names.get(session.name);
140
320
  if (!nameOwner || nameOwner === session.id) this.names.set(session.name, session.id);
@@ -180,14 +360,12 @@ var RedisRestPreviewGatewayStore = class {
180
360
  return value ? JSON.parse(value) : void 0;
181
361
  }
182
362
  async touchSession(session, ttlMs) {
183
- const nextSession = {
184
- ...session,
185
- expiresAt: Date.now() + ttlMs
186
- };
187
- if (await this.command("SET", this.sessionKey(session.id), JSON.stringify(nextSession), "PX", ttlMs, "XX") === null) return;
363
+ const nextSession = { ...session };
364
+ const remainingTtlMs = Math.max(1, Math.min(ttlMs, session.expiresAt - Date.now()));
365
+ if (await this.command("SET", this.sessionKey(session.id), JSON.stringify(nextSession), "PX", remainingTtlMs, "XX") === null) return;
188
366
  const nameOwner = await this.command("GET", this.nameKey(session.name));
189
- if (!nameOwner || nameOwner === session.id) await this.command("SET", this.nameKey(session.name), session.id, "PX", ttlMs);
190
- await this.command("EXPIRE", this.queueKey(session.id), Math.ceil(ttlMs / 1e3));
367
+ if (!nameOwner || nameOwner === session.id) await this.command("SET", this.nameKey(session.name), session.id, "PX", remainingTtlMs);
368
+ await this.command("EXPIRE", this.queueKey(session.id), Math.ceil(remainingTtlMs / 1e3));
191
369
  }
192
370
  async enqueueRequest(sessionId, request, ttlMs) {
193
371
  await this.command("RPUSH", this.queueKey(sessionId), JSON.stringify(request));
@@ -247,10 +425,26 @@ async function createSession(request, store, config) {
247
425
  const input = await request.json().catch(() => ({}));
248
426
  const requestedName = sanitizePreviewName(input.name);
249
427
  let name = requestedName || randomPreviewName();
428
+ let expiresAt = Date.now() + config.sessionTtlMs;
429
+ if (config.auth) {
430
+ const grantToken = readPreviewBearerToken(request);
431
+ if (!grantToken) throw new GatewayHttpError(401, "Sign in with Farm Preview before creating a session.");
432
+ try {
433
+ const grant = verifyPreviewTunnelGrant(grantToken, {
434
+ signingSecret: config.auth.signingSecret,
435
+ ...requestedName ? { name: requestedName } : {}
436
+ });
437
+ name = grant.name;
438
+ expiresAt = grant.expiresAt;
439
+ } catch (error) {
440
+ if (error instanceof PreviewAuthError) throw new GatewayHttpError(error.status, error.message);
441
+ throw error;
442
+ }
443
+ } else if (Number.isFinite(input.expiresInMs)) expiresAt = Date.now() + clamp(Number(input.expiresInMs), 6e4, config.sessionTtlMs);
250
444
  for (let attempt = 0;; attempt++) {
251
445
  const existing = await store.getSessionByName(name);
252
446
  if (!existing || !isPreviewClientOnline(existing, config)) break;
253
- if (requestedName) return text(`A Farm preview named "${name}" is already active. Choose another name or stop the running preview.`, 409);
447
+ if (requestedName || config.auth) return text(`A Farm preview named "${name}" is already active. Choose another name or stop the running preview.`, 409);
254
448
  if (attempt >= 4) return text("Could not allocate a unique preview name. Try again.", 503);
255
449
  name = randomPreviewName();
256
450
  }
@@ -263,10 +457,10 @@ async function createSession(request, store, config) {
263
457
  token: randomId("token"),
264
458
  localUrl: input.localUrl,
265
459
  createdAt: Date.now(),
266
- expiresAt: Date.now() + config.sessionTtlMs,
460
+ expiresAt,
267
461
  lastHeartbeatAt: Date.now()
268
462
  };
269
- await store.createSession(session, config.sessionTtlMs);
463
+ await store.createSession(session, Math.max(1, expiresAt - Date.now()));
270
464
  return json({
271
465
  id: session.id,
272
466
  name: session.name,
@@ -276,17 +470,17 @@ async function createSession(request, store, config) {
276
470
  });
277
471
  }
278
472
  async function handleSessionRoute(request, url, store, config, route) {
279
- const session = await requireSession(store, route.sessionId, url.searchParams.get("token"));
473
+ const session = await requireSession(store, route.sessionId, readPreviewBearerToken(request) || url.searchParams.get("token"));
280
474
  if (request.method === "GET" && route.action === "requests") return await pollSessionRequests(url, store, config, session);
281
475
  if (request.method === "POST" && route.action === "responses" && route.requestId) {
282
476
  let response;
283
477
  try {
284
478
  response = await parsePreviewResponse(request, config.maxResponseBodyBytes);
285
479
  } catch (error) {
286
- if (error instanceof GatewayHttpError && error.status === 413) await store.saveResponse(session.id, route.requestId, createOversizedPreviewResponse(config.maxResponseBodyBytes), config.sessionTtlMs);
480
+ if (error instanceof GatewayHttpError && error.status === 413) await store.saveResponse(session.id, route.requestId, createOversizedPreviewResponse(config.maxResponseBodyBytes), remainingSessionTtl(session, config));
287
481
  throw error;
288
482
  }
289
- await store.saveResponse(session.id, route.requestId, response, config.sessionTtlMs);
483
+ await store.saveResponse(session.id, route.requestId, response, remainingSessionTtl(session, config));
290
484
  await markSessionOnline(store, config, session);
291
485
  return json({ ok: true });
292
486
  }
@@ -294,7 +488,7 @@ async function handleSessionRoute(request, url, store, config, route) {
294
488
  await markSessionOnline(store, config, session);
295
489
  return json({
296
490
  ok: true,
297
- expiresAt: Date.now() + config.sessionTtlMs
491
+ expiresAt: session.expiresAt
298
492
  });
299
493
  }
300
494
  if (request.method === "DELETE" && !route.action) {
@@ -349,14 +543,14 @@ async function pollSessionRequests(url, store, config, session) {
349
543
  }
350
544
  async function proxyPublicRequest(request, url, store, config, route) {
351
545
  const session = await store.getSessionByName(route.name);
352
- if (!session) return text(`No active Farm preview is running for "${route.name}".`, 404);
546
+ if (!session) return inactivePreviewResponse(request, route.name);
353
547
  if (!isPreviewClientOnline(session, config)) {
354
548
  await store.deleteSession(session);
355
- return text(`No active Farm preview is running for "${route.name}".`, 404);
549
+ return inactivePreviewResponse(request, route.name);
356
550
  }
357
551
  const previewRequest = await serializePreviewRequest(request, url, route.path, config.maxBodyBytes);
358
- await store.enqueueRequest(session.id, previewRequest, config.sessionTtlMs);
359
- await store.touchSession(session, config.sessionTtlMs);
552
+ await store.enqueueRequest(session.id, previewRequest, remainingSessionTtl(session, config));
553
+ await store.touchSession(session, remainingSessionTtl(session, config));
360
554
  const response = await waitForPreviewResponse(store, config, session, previewRequest.id, request.signal);
361
555
  if (response === "cancelled") {
362
556
  await store.enqueueRequest(session.id, {
@@ -365,10 +559,10 @@ async function proxyPublicRequest(request, url, store, config, route) {
365
559
  encoding: void 0,
366
560
  cancelled: true,
367
561
  createdAt: Date.now()
368
- }, config.sessionTtlMs);
562
+ }, remainingSessionTtl(session, config));
369
563
  return new Response(null, { status: 499 });
370
564
  }
371
- if (response === "stale") return text(`No active Farm preview is running for "${route.name}".`, 404);
565
+ if (response === "stale") return inactivePreviewResponse(request, route.name);
372
566
  if (!response) return text("The local Farm preview did not respond before the gateway timed out.", 504);
373
567
  const headers = new Headers();
374
568
  const responseHopByHopHeaders = getHopByHopHeaderNames(response.headers || {});
@@ -412,11 +606,14 @@ async function waitForPreviewResponse(store, config, session, requestId, signal)
412
606
  }
413
607
  async function markSessionOnline(store, config, session) {
414
608
  session.lastHeartbeatAt = Date.now();
415
- await store.touchSession(session, config.sessionTtlMs);
609
+ await store.touchSession(session, remainingSessionTtl(session, config));
610
+ }
611
+ function remainingSessionTtl(session, config) {
612
+ return Math.max(1, Math.min(config.sessionTtlMs, session.expiresAt - Date.now()));
416
613
  }
417
614
  function isPreviewClientOnline(session, config) {
418
615
  const lastHeartbeatAt = session.lastHeartbeatAt || session.createdAt;
419
- return Date.now() - lastHeartbeatAt <= config.clientHeartbeatTimeoutMs;
616
+ return session.expiresAt > Date.now() && Date.now() - lastHeartbeatAt <= config.clientHeartbeatTimeoutMs;
420
617
  }
421
618
  async function serializePreviewRequest(request, url, routePath, maxBodyBytes) {
422
619
  const method = request.method.toUpperCase();
@@ -546,6 +743,23 @@ function text(value, status = 200) {
546
743
  }
547
744
  });
548
745
  }
746
+ function inactivePreviewResponse(request, name) {
747
+ if (!request.headers.get("accept")?.includes("text/html")) return text(`No active Farm preview is running for "${name}".`, 404);
748
+ const safeName = name.replace(/[&<>"']/g, (character) => {
749
+ if (character === "&") return "&amp;";
750
+ if (character === "<") return "&lt;";
751
+ if (character === ">") return "&gt;";
752
+ if (character === "\"") return "&quot;";
753
+ return "&#39;";
754
+ });
755
+ return new Response(`<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Farm preview expired</title><style>body{font:16px/1.5 system-ui,sans-serif;display:grid;min-height:100vh;margin:0;place-items:center;background:#f7f7f5;color:#171714}main{max-width:34rem;padding:2rem;text-align:center}h1{font-size:1.75rem}code{background:#e9e9e4;border-radius:.35rem;padding:.15rem .35rem}</style><main><h1>This Farm preview has expired</h1><p>The temporary preview <code>${safeName}</code> is no longer connected.</p><p>Ask its owner to run <code>farm preview</code> again.</p></main></html>`, {
756
+ status: 410,
757
+ headers: {
758
+ "content-type": "text/html; charset=utf-8",
759
+ "cache-control": "no-store"
760
+ }
761
+ });
762
+ }
549
763
  function sanitizePreviewName(value) {
550
764
  if (!value) return void 0;
551
765
  return value.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48);
@@ -576,6 +790,6 @@ var GatewayHttpError = class extends Error {
576
790
  }
577
791
  };
578
792
  //#endregion
579
- export { MemoryPreviewGatewayStore, RedisRestPreviewGatewayStore, createNodePreviewGatewayHandler, createPreviewGatewayHandler, createPreviewGatewayStoreFromEnv };
793
+ export { MemoryPreviewGatewayStore, PreviewAuthError, RedisRestPreviewGatewayStore, createNodePreviewGatewayHandler, createPreviewGatewayHandler, createPreviewGatewayStoreFromEnv, exchangeGitHubPreviewToken, getPreviewAuthPublicConfig, issuePreviewTunnelGrant, readPreviewBearerToken, verifyPreviewAccountToken, verifyPreviewTunnelGrant };
580
794
 
581
795
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["delay"],"sources":["../src/index.ts"],"sourcesContent":["import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from \"node:http\";\nimport { setTimeout as delay } from \"node:timers/promises\";\n\nexport interface PreviewGatewayOptions {\n domain?: string;\n baseUrl?: string;\n store?: PreviewGatewayStore;\n sessionTtlMs?: number;\n clientHeartbeatTimeoutMs?: number;\n requestTimeoutMs?: number;\n pollTimeoutMs?: number;\n pollIntervalMs?: number;\n maxBodyBytes?: number;\n maxResponseBodyBytes?: number;\n}\n\nexport interface PreviewGatewaySession {\n id: string;\n name: string;\n hostname: string;\n publicUrl: string;\n token: string;\n localUrl?: string;\n createdAt: number;\n expiresAt: number;\n lastHeartbeatAt?: number;\n}\n\nexport interface PreviewGatewayRequest {\n id: string;\n method: string;\n path: string;\n headers: Record<string, string>;\n body?: string | null;\n encoding?: \"base64\";\n createdAt: number;\n cancelled?: true;\n}\n\nexport interface PreviewGatewayResponse {\n status: number;\n // A header may carry multiple values (notably Set-Cookie: a login response\n // commonly sets a session cookie and a CSRF cookie). Those must survive as\n // an array through the store and back onto the public response.\n headers?: Record<string, string | string[]>;\n body?: string | null;\n encoding?: \"base64\";\n}\n\nexport interface PreviewGatewayStore {\n createSession(session: PreviewGatewaySession, ttlMs: number): Promise<void>;\n getSessionByName(name: string): Promise<PreviewGatewaySession | undefined>;\n getSessionById(id: string): Promise<PreviewGatewaySession | undefined>;\n touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void>;\n enqueueRequest(sessionId: string, request: PreviewGatewayRequest, ttlMs: number): Promise<void>;\n takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]>;\n saveResponse(\n sessionId: string,\n requestId: string,\n response: PreviewGatewayResponse,\n ttlMs: number,\n ): Promise<void>;\n getResponse(sessionId: string, requestId: string): Promise<PreviewGatewayResponse | undefined>;\n deleteResponse(sessionId: string, requestId: string): Promise<void>;\n deleteSession(session: PreviewGatewaySession): Promise<void>;\n}\n\ninterface PreviewGatewayRuntimeConfig {\n domain: string;\n baseUrl?: string;\n sessionTtlMs: number;\n clientHeartbeatTimeoutMs: number;\n requestTimeoutMs: number;\n pollTimeoutMs: number;\n pollIntervalMs: number;\n maxBodyBytes: number;\n maxResponseBodyBytes: number;\n}\n\nconst DEFAULT_DOMAIN = \"preview.farming-labs.dev\";\nconst DEFAULT_SESSION_TTL_MS = 1000 * 60 * 30;\nconst DEFAULT_CLIENT_HEARTBEAT_TIMEOUT_MS = 1000 * 20;\nconst DEFAULT_REQUEST_TIMEOUT_MS = 25000;\nconst DEFAULT_POLL_TIMEOUT_MS = 15000;\nconst DEFAULT_POLL_INTERVAL_MS = 100;\nconst DEFAULT_MAX_BODY_BYTES = 1024 * 1024 * 5;\nconst DEFAULT_MAX_RESPONSE_BODY_BYTES = 1024 * 1024 * 5;\nconst DEFAULT_POLL_REQUEST_LIMIT = 50;\nconst HOP_BY_HOP_HEADERS = new Set([\n \"connection\",\n \"content-length\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\nfunction getHopByHopHeaderNames(headers: Headers | Record<string, string | string[]>): Set<string> {\n const names = new Set(HOP_BY_HOP_HEADERS);\n const connection =\n headers instanceof Headers\n ? headers.get(\"connection\")\n : Object.entries(headers).find(([name]) => name.toLowerCase() === \"connection\")?.[1];\n const values = Array.isArray(connection) ? connection : connection ? [connection] : [];\n for (const value of values) {\n for (const name of value.split(\",\")) {\n const normalized = name.trim().toLowerCase();\n if (normalized) names.add(normalized);\n }\n }\n return names;\n}\n\nexport function createPreviewGatewayHandler(options: PreviewGatewayOptions = {}) {\n const store = options.store || createPreviewGatewayStoreFromEnv();\n const config = {\n domain: normalizeDomain(options.domain || process.env.FARM_PREVIEW_DOMAIN || DEFAULT_DOMAIN),\n baseUrl: options.baseUrl || process.env.FARM_PREVIEW_GATEWAY_URL,\n sessionTtlMs: options.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,\n clientHeartbeatTimeoutMs:\n options.clientHeartbeatTimeoutMs ?? DEFAULT_CLIENT_HEARTBEAT_TIMEOUT_MS,\n requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n pollTimeoutMs: options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,\n pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,\n maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,\n maxResponseBodyBytes: options.maxResponseBodyBytes ?? DEFAULT_MAX_RESPONSE_BODY_BYTES,\n };\n\n return async function handlePreviewGatewayRequest(request: Request): Promise<Response> {\n try {\n const url = new URL(request.url);\n\n if (request.method === \"GET\" && url.pathname === \"/api/health\") {\n return json({\n ok: true,\n domain: config.domain,\n store: store.constructor.name || \"PreviewGatewayStore\",\n });\n }\n\n if (request.method === \"POST\" && url.pathname === \"/api/sessions\") {\n return await createSession(request, store, config);\n }\n\n const sessionRoute = matchSessionRoute(url.pathname);\n if (sessionRoute) {\n return await handleSessionRoute(request, url, store, config, sessionRoute);\n }\n\n const publicRoute = resolvePublicPreviewRoute(request, url, config.domain);\n if (publicRoute) {\n return await proxyPublicRequest(request, url, store, config, publicRoute);\n }\n\n return json(\n {\n ok: true,\n message: \"Farm Preview Gateway\",\n createSession: \"/api/sessions\",\n health: \"/api/health\",\n },\n 200,\n );\n } catch (error) {\n if (error instanceof GatewayHttpError) {\n return text(error.message, error.status);\n }\n return text(\n error instanceof Error ? error.message : \"Unexpected preview gateway error.\",\n 500,\n );\n }\n };\n}\n\nexport function createNodePreviewGatewayHandler(options: PreviewGatewayOptions = {}) {\n const handler = createPreviewGatewayHandler(options);\n\n return async function nodePreviewGatewayHandler(req: IncomingMessage, res: ServerResponse) {\n const controller = new AbortController();\n const abort = () => controller.abort();\n const abortOnClose = () => {\n if (!res.writableEnded) abort();\n };\n req.once(\"aborted\", abort);\n res.once(\"close\", abortOnClose);\n try {\n const response = await handler(nodeRequestToWebRequest(req, controller.signal));\n if (controller.signal.aborted || res.destroyed) {\n await response.body?.cancel();\n return;\n }\n res.statusCode = response.status;\n // Headers.forEach folds repeated headers into one comma-joined value, which\n // corrupts multiple Set-Cookie. Emit those separately as an array so each\n // cookie becomes its own header line.\n const setCookies = response.headers.getSetCookie?.() ?? [];\n response.headers.forEach((value, key) => {\n if (key.toLowerCase() === \"set-cookie\") return;\n res.setHeader(key, value);\n });\n if (setCookies.length > 0) {\n res.setHeader(\"set-cookie\", setCookies);\n }\n res.end(Buffer.from(await response.arrayBuffer()));\n } finally {\n req.off(\"aborted\", abort);\n res.off(\"close\", abortOnClose);\n }\n };\n}\n\nexport function createPreviewGatewayStoreFromEnv(): PreviewGatewayStore {\n const redisUrl = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;\n const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN;\n\n if (redisUrl && redisToken) {\n return new RedisRestPreviewGatewayStore({\n url: redisUrl,\n token: redisToken,\n });\n }\n\n return new MemoryPreviewGatewayStore();\n}\n\nexport class MemoryPreviewGatewayStore implements PreviewGatewayStore {\n private sessions = new Map<string, PreviewGatewaySession>();\n private names = new Map<string, string>();\n private queues = new Map<string, PreviewGatewayRequest[]>();\n private responses = new Map<string, PreviewGatewayResponse>();\n\n async createSession(session: PreviewGatewaySession): Promise<void> {\n this.sessions.set(session.id, session);\n this.names.set(session.name, session.id);\n this.queues.set(session.id, []);\n }\n\n async getSessionByName(name: string): Promise<PreviewGatewaySession | undefined> {\n const id = this.names.get(name);\n return id ? this.getSessionById(id) : undefined;\n }\n\n async getSessionById(id: string): Promise<PreviewGatewaySession | undefined> {\n const session = this.sessions.get(id);\n if (!session) return undefined;\n if (session.expiresAt <= Date.now()) {\n await this.deleteSession(session);\n return undefined;\n }\n return session;\n }\n\n async touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void> {\n // A concurrent delete (the preview was stopped) must win: a poll handler\n // that outlives the DELETE must never resurrect a session that is no\n // longer stored, or its name would look \"online\" and block a restart.\n if (!this.sessions.has(session.id)) return;\n session.expiresAt = Date.now() + ttlMs;\n this.sessions.set(session.id, session);\n const nameOwner = this.names.get(session.name);\n if (!nameOwner || nameOwner === session.id) {\n this.names.set(session.name, session.id);\n }\n }\n\n async enqueueRequest(sessionId: string, request: PreviewGatewayRequest): Promise<void> {\n const queue = this.queues.get(sessionId) || [];\n queue.push(request);\n this.queues.set(sessionId, queue);\n }\n\n async takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]> {\n const queue = this.queues.get(sessionId) || [];\n return queue.splice(0, limit);\n }\n\n async saveResponse(\n sessionId: string,\n requestId: string,\n response: PreviewGatewayResponse,\n ): Promise<void> {\n this.responses.set(responseKey(sessionId, requestId), response);\n }\n\n async getResponse(\n sessionId: string,\n requestId: string,\n ): Promise<PreviewGatewayResponse | undefined> {\n return this.responses.get(responseKey(sessionId, requestId));\n }\n\n async deleteResponse(sessionId: string, requestId: string): Promise<void> {\n this.responses.delete(responseKey(sessionId, requestId));\n }\n\n async deleteSession(session: PreviewGatewaySession): Promise<void> {\n this.sessions.delete(session.id);\n // The name may have been taken over by a newer session; only remove the\n // mapping if it still points at the session being deleted.\n if (this.names.get(session.name) === session.id) {\n this.names.delete(session.name);\n }\n this.queues.delete(session.id);\n }\n}\n\nexport class RedisRestPreviewGatewayStore implements PreviewGatewayStore {\n constructor(private options: { url: string; token: string; prefix?: string }) {}\n\n async createSession(session: PreviewGatewaySession, ttlMs: number): Promise<void> {\n await this.command(\"SET\", this.sessionKey(session.id), JSON.stringify(session), \"PX\", ttlMs);\n await this.command(\"SET\", this.nameKey(session.name), session.id, \"PX\", ttlMs);\n await this.command(\"DEL\", this.queueKey(session.id));\n }\n\n async getSessionByName(name: string): Promise<PreviewGatewaySession | undefined> {\n const id = await this.command<string | null>(\"GET\", this.nameKey(name));\n return id ? this.getSessionById(id) : undefined;\n }\n\n async getSessionById(id: string): Promise<PreviewGatewaySession | undefined> {\n const value = await this.command<string | null>(\"GET\", this.sessionKey(id));\n return value ? JSON.parse(value) : undefined;\n }\n\n async touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void> {\n const nextSession = {\n ...session,\n expiresAt: Date.now() + ttlMs,\n };\n // XX only refreshes a session key that still exists. A poll handler that\n // outlives a DELETE must not recreate the session (nor its name mapping),\n // which would otherwise block reclaiming the name on restart.\n const result = await this.command<string | null>(\n \"SET\",\n this.sessionKey(session.id),\n JSON.stringify(nextSession),\n \"PX\",\n ttlMs,\n \"XX\",\n );\n if (result === null) return;\n const nameOwner = await this.command<string | null>(\"GET\", this.nameKey(session.name));\n if (!nameOwner || nameOwner === session.id) {\n await this.command(\"SET\", this.nameKey(session.name), session.id, \"PX\", ttlMs);\n }\n await this.command(\"EXPIRE\", this.queueKey(session.id), Math.ceil(ttlMs / 1000));\n }\n\n async enqueueRequest(\n sessionId: string,\n request: PreviewGatewayRequest,\n ttlMs: number,\n ): Promise<void> {\n await this.command(\"RPUSH\", this.queueKey(sessionId), JSON.stringify(request));\n await this.command(\"EXPIRE\", this.queueKey(sessionId), Math.ceil(ttlMs / 1000));\n }\n\n async takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]> {\n const values = await this.command<string[] | string | null>(\n \"LPOP\",\n this.queueKey(sessionId),\n limit,\n );\n if (!values) return [];\n const list = Array.isArray(values) ? values : [values];\n return list.map((value) => JSON.parse(value));\n }\n\n async saveResponse(\n sessionId: string,\n requestId: string,\n response: PreviewGatewayResponse,\n ttlMs: number,\n ): Promise<void> {\n await this.command(\n \"SET\",\n this.responseKey(sessionId, requestId),\n JSON.stringify(response),\n \"PX\",\n ttlMs,\n );\n }\n\n async getResponse(\n sessionId: string,\n requestId: string,\n ): Promise<PreviewGatewayResponse | undefined> {\n const value = await this.command<string | null>(\"GET\", this.responseKey(sessionId, requestId));\n return value ? JSON.parse(value) : undefined;\n }\n\n async deleteResponse(sessionId: string, requestId: string): Promise<void> {\n await this.command(\"DEL\", this.responseKey(sessionId, requestId));\n }\n\n async deleteSession(session: PreviewGatewaySession): Promise<void> {\n const keys = [this.sessionKey(session.id), this.queueKey(session.id)];\n // The name may have been taken over by a newer session; only remove the\n // mapping if it still points at the session being deleted.\n const nameOwner = await this.command<string | null>(\"GET\", this.nameKey(session.name));\n if (nameOwner === session.id) {\n keys.push(this.nameKey(session.name));\n }\n await this.command(\"DEL\", ...keys);\n }\n\n private async command<T = unknown>(...args: Array<string | number>) {\n const response = await fetch(this.options.url, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${this.options.token}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify(args),\n });\n\n if (!response.ok) {\n throw new Error(\n `Preview gateway Redis command failed (${response.status}): ${await response.text()}`,\n );\n }\n\n const data = (await response.json()) as { result?: T; error?: string };\n if (data.error) {\n throw new Error(`Preview gateway Redis command failed: ${data.error}`);\n }\n return data.result as T;\n }\n\n private key(value: string) {\n return `${this.options.prefix || \"farm-preview\"}:${value}`;\n }\n\n private sessionKey(id: string) {\n return this.key(`session:${id}`);\n }\n\n private nameKey(name: string) {\n return this.key(`name:${name}`);\n }\n\n private queueKey(id: string) {\n return this.key(`queue:${id}`);\n }\n\n private responseKey(sessionId: string, requestId: string) {\n return this.key(`response:${sessionId}:${requestId}`);\n }\n}\n\nasync function createSession(\n request: Request,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n) {\n const input = (await request.json().catch(() => ({}))) as {\n name?: string;\n localUrl?: string;\n };\n const requestedName = sanitizePreviewName(input.name);\n let name = requestedName || randomPreviewName();\n\n // A name that is still owned by a live session cannot be claimed by a new\n // one — otherwise any caller could repoint an active preview's public URL\n // to their own session. Stale sessions (missed heartbeats) may be taken\n // over, matching how public routing treats them.\n for (let attempt = 0; ; attempt++) {\n const existing = await store.getSessionByName(name);\n if (!existing || !isPreviewClientOnline(existing, config)) {\n break;\n }\n if (requestedName) {\n return text(\n `A Farm preview named \"${name}\" is already active. Choose another name or stop the running preview.`,\n 409,\n );\n }\n if (attempt >= 4) {\n return text(\"Could not allocate a unique preview name. Try again.\", 503);\n }\n name = randomPreviewName();\n }\n\n const hostname = `${name}.${config.domain}`;\n const session: PreviewGatewaySession = {\n id: randomId(\"sess\"),\n name,\n hostname,\n publicUrl: createPublicUrl(request, config, name),\n token: randomId(\"token\"),\n localUrl: input.localUrl,\n createdAt: Date.now(),\n expiresAt: Date.now() + config.sessionTtlMs,\n lastHeartbeatAt: Date.now(),\n };\n\n await store.createSession(session, config.sessionTtlMs);\n\n return json({\n id: session.id,\n name: session.name,\n token: session.token,\n publicUrl: session.publicUrl,\n expiresAt: session.expiresAt,\n });\n}\n\nasync function handleSessionRoute(\n request: Request,\n url: URL,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n route: { sessionId: string; action?: string; requestId?: string },\n) {\n const session = await requireSession(store, route.sessionId, url.searchParams.get(\"token\"));\n\n if (request.method === \"GET\" && route.action === \"requests\") {\n return await pollSessionRequests(url, store, config, session);\n }\n\n if (request.method === \"POST\" && route.action === \"responses\" && route.requestId) {\n let response: PreviewGatewayResponse;\n try {\n response = await parsePreviewResponse(request, config.maxResponseBodyBytes);\n } catch (error) {\n if (error instanceof GatewayHttpError && error.status === 413) {\n await store.saveResponse(\n session.id,\n route.requestId,\n createOversizedPreviewResponse(config.maxResponseBodyBytes),\n config.sessionTtlMs,\n );\n }\n throw error;\n }\n await store.saveResponse(session.id, route.requestId, response, config.sessionTtlMs);\n await markSessionOnline(store, config, session);\n return json({ ok: true });\n }\n\n if (request.method === \"POST\" && route.action === \"heartbeat\") {\n await markSessionOnline(store, config, session);\n return json({ ok: true, expiresAt: Date.now() + config.sessionTtlMs });\n }\n\n if (request.method === \"DELETE\" && !route.action) {\n await store.deleteSession(session);\n return json({ ok: true });\n }\n\n return text(\"Preview gateway route not found.\", 404);\n}\n\nasync function parsePreviewResponse(request: Request, maxBodyBytes: number) {\n const maxEncodedBytes = Math.ceil(maxBodyBytes / 3) * 4 + 64 * 1024;\n const contentLength = Number(request.headers.get(\"content-length\") || 0);\n if (Number.isFinite(contentLength) && contentLength > maxEncodedBytes) {\n await request.body?.cancel();\n throw new GatewayHttpError(413, \"Preview response body is too large.\");\n }\n\n const bytes = await readBodyWithLimit(\n request,\n maxEncodedBytes,\n \"Preview response body is too large.\",\n );\n let response: PreviewGatewayResponse;\n try {\n response = JSON.parse(bytes.toString(\"utf8\")) as PreviewGatewayResponse;\n } catch {\n throw new GatewayHttpError(400, \"Preview response body must be valid JSON.\");\n }\n\n if (getPreviewResponseBodySize(response) > maxBodyBytes) {\n throw new GatewayHttpError(413, \"Preview response body is too large.\");\n }\n return response;\n}\n\nfunction getPreviewResponseBodySize(response: PreviewGatewayResponse) {\n if (!response.body) return 0;\n return Buffer.byteLength(response.body, response.encoding === \"base64\" ? \"base64\" : \"utf8\");\n}\n\nfunction createOversizedPreviewResponse(maxBodyBytes: number): PreviewGatewayResponse {\n return {\n status: 502,\n headers: { \"content-type\": \"text/plain; charset=utf-8\" },\n body: Buffer.from(\n `The local preview response exceeded the ${maxBodyBytes} byte limit.`,\n ).toString(\"base64\"),\n encoding: \"base64\",\n };\n}\n\nasync function pollSessionRequests(\n url: URL,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n session: PreviewGatewaySession,\n) {\n const waitMs = clamp(Number(url.searchParams.get(\"wait\") || config.pollTimeoutMs), 1000, 25000);\n const deadline = Date.now() + waitMs;\n\n await markSessionOnline(store, config, session);\n\n while (Date.now() < deadline) {\n const requests = await store.takeRequests(session.id, DEFAULT_POLL_REQUEST_LIMIT);\n if (requests.length) {\n await markSessionOnline(store, config, session);\n return json({ requests });\n }\n await delay(config.pollIntervalMs);\n }\n\n await markSessionOnline(store, config, session);\n return new Response(null, { status: 204 });\n}\n\nasync function proxyPublicRequest(\n request: Request,\n url: URL,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n route: { name: string; path: string },\n) {\n const session = await store.getSessionByName(route.name);\n if (!session) {\n return text(`No active Farm preview is running for \"${route.name}\".`, 404);\n }\n if (!isPreviewClientOnline(session, config)) {\n await store.deleteSession(session);\n return text(`No active Farm preview is running for \"${route.name}\".`, 404);\n }\n\n const previewRequest = await serializePreviewRequest(\n request,\n url,\n route.path,\n config.maxBodyBytes,\n );\n await store.enqueueRequest(session.id, previewRequest, config.sessionTtlMs);\n await store.touchSession(session, config.sessionTtlMs);\n\n const response = await waitForPreviewResponse(\n store,\n config,\n session,\n previewRequest.id,\n request.signal,\n );\n if (response === \"cancelled\") {\n await store.enqueueRequest(\n session.id,\n {\n ...previewRequest,\n body: undefined,\n encoding: undefined,\n cancelled: true,\n createdAt: Date.now(),\n },\n config.sessionTtlMs,\n );\n return new Response(null, { status: 499 });\n }\n if (response === \"stale\") {\n return text(`No active Farm preview is running for \"${route.name}\".`, 404);\n }\n if (!response) {\n return text(\"The local Farm preview did not respond before the gateway timed out.\", 504);\n }\n\n const headers = new Headers();\n const responseHopByHopHeaders = getHopByHopHeaderNames(response.headers || {});\n for (const [key, value] of Object.entries(response.headers || {})) {\n const normalized = key.toLowerCase();\n if (responseHopByHopHeaders.has(normalized) || normalized === \"content-encoding\") {\n continue;\n }\n // Append multi-valued headers (Set-Cookie) so every value reaches the\n // visitor; Headers.set would keep only the last.\n for (const single of Array.isArray(value) ? value : [value]) {\n headers.append(key, single);\n }\n }\n headers.set(\"cache-control\", \"no-store\");\n headers.set(\"x-farm-preview\", session.name);\n\n return new Response(\n response.body\n ? Buffer.from(response.body, response.encoding === \"base64\" ? \"base64\" : \"utf8\")\n : null,\n {\n status: response.status || 200,\n headers,\n },\n );\n}\n\nasync function waitForPreviewResponse(\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n session: PreviewGatewaySession,\n requestId: string,\n signal: AbortSignal,\n) {\n const deadline = Date.now() + config.requestTimeoutMs;\n let nextLivenessCheckAt = 0;\n\n while (Date.now() < deadline) {\n if (signal.aborted) return \"cancelled\";\n if (Date.now() >= nextLivenessCheckAt) {\n const latestSession = await store.getSessionById(session.id);\n if (!latestSession || !isPreviewClientOnline(latestSession, config)) {\n if (latestSession) await store.deleteSession(latestSession);\n return \"stale\";\n }\n nextLivenessCheckAt = Date.now() + 1000;\n }\n\n const response = await store.getResponse(session.id, requestId);\n if (response) {\n await store.deleteResponse(session.id, requestId);\n return response;\n }\n try {\n await delay(config.pollIntervalMs, undefined, { signal });\n } catch {\n if (signal.aborted) return \"cancelled\";\n throw new Error(\"Preview response polling was interrupted.\");\n }\n }\n\n return undefined;\n}\n\nasync function markSessionOnline(\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n session: PreviewGatewaySession,\n) {\n session.lastHeartbeatAt = Date.now();\n await store.touchSession(session, config.sessionTtlMs);\n}\n\nfunction isPreviewClientOnline(\n session: PreviewGatewaySession,\n config: PreviewGatewayRuntimeConfig,\n) {\n const lastHeartbeatAt = session.lastHeartbeatAt || session.createdAt;\n return Date.now() - lastHeartbeatAt <= config.clientHeartbeatTimeoutMs;\n}\n\nasync function serializePreviewRequest(\n request: Request,\n url: URL,\n routePath: string,\n maxBodyBytes: number,\n): Promise<PreviewGatewayRequest> {\n const method = request.method.toUpperCase();\n const headers: Record<string, string> = {};\n const requestHopByHopHeaders = getHopByHopHeaderNames(request.headers);\n request.headers.forEach((value, key) => {\n const normalized = key.toLowerCase();\n if (\n !requestHopByHopHeaders.has(normalized) &&\n normalized !== \"forwarded\" &&\n !normalized.startsWith(\"x-forwarded-\")\n ) {\n headers[key] = value;\n }\n });\n\n headers[\"x-forwarded-host\"] = request.headers.get(\"host\") || url.host;\n headers[\"x-forwarded-proto\"] = url.protocol.replace(\":\", \"\");\n\n let body: string | undefined;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const contentLength = Number(request.headers.get(\"content-length\") || 0);\n if (contentLength > maxBodyBytes) {\n throw new GatewayHttpError(413, \"Preview request body is too large.\");\n }\n\n const buffer = await readBodyWithLimit(\n request,\n maxBodyBytes,\n \"Preview request body is too large.\",\n );\n body = buffer.toString(\"base64\");\n }\n\n return {\n id: randomId(\"req\"),\n method,\n path: `${routePath}${url.search}`,\n headers,\n body,\n encoding: body ? \"base64\" : undefined,\n createdAt: Date.now(),\n };\n}\n\nasync function readBodyWithLimit(request: Request, maxBytes: number, message: string) {\n if (!request.body) return Buffer.alloc(0);\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > maxBytes) {\n await reader.cancel();\n throw new GatewayHttpError(413, message);\n }\n chunks.push(value);\n }\n } catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error;\n }\n return Buffer.concat(chunks, size);\n}\n\nasync function requireSession(store: PreviewGatewayStore, sessionId: string, token: string | null) {\n const session = await store.getSessionById(sessionId);\n if (!session || !token || token !== session.token) {\n throw new GatewayHttpError(401, \"Invalid preview gateway session.\");\n }\n return session;\n}\n\nfunction resolvePublicPreviewRoute(request: Request, url: URL, domain: string) {\n const pathMatch = url.pathname.match(/^\\/__preview\\/([^/]+)(\\/.*)?$/);\n if (pathMatch) {\n return {\n name: pathMatch[1],\n path: pathMatch[2] || \"/\",\n };\n }\n\n const host = (request.headers.get(\"host\") || url.host).split(\":\")[0];\n if (!host || host === domain || !host.endsWith(`.${domain}`)) {\n return undefined;\n }\n\n return {\n name: host.slice(0, -(domain.length + 1)),\n path: url.pathname || \"/\",\n };\n}\n\nfunction matchSessionRoute(pathname: string) {\n const match = pathname.match(\n /^\\/api\\/sessions\\/([^/]+)(?:\\/(requests|heartbeat|responses)(?:\\/([^/]+))?)?$/,\n );\n if (!match) return undefined;\n return {\n sessionId: match[1],\n action: match[2],\n requestId: match[3],\n };\n}\n\nfunction nodeRequestToWebRequest(req: IncomingMessage, signal?: AbortSignal) {\n const host = headerValue(req.headers, \"host\") || \"localhost\";\n // This adapter is the public trust boundary. A visitor-controlled forwarded\n // protocol must not influence the authority serialized to the local app.\n const protocol = isLocalHost(host.split(\":\")[0]) ? \"http\" : \"https\";\n const url = `${protocol}://${host}${req.url || \"/\"}`;\n const headers = nodeHeadersToWebHeaders(req.headers);\n const method = req.method || \"GET\";\n const init: RequestInit & { duplex?: \"half\" } = {\n method,\n headers,\n signal,\n };\n\n if (method !== \"GET\" && method !== \"HEAD\") {\n init.body = req as unknown as BodyInit;\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nfunction nodeHeadersToWebHeaders(headers: IncomingHttpHeaders) {\n const next = new Headers();\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const item of value) next.append(key, item);\n } else {\n next.set(key, value);\n }\n }\n return next;\n}\n\nfunction headerValue(headers: IncomingHttpHeaders, key: string) {\n const value = headers[key];\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction createPublicUrl(request: Request, config: PreviewGatewayRuntimeConfig, name: string) {\n if (config.baseUrl) {\n const base = new URL(config.baseUrl);\n if (isLocalHost(base.hostname)) {\n return `${config.baseUrl.replace(/\\/+$/, \"\")}/__preview/${name}`;\n }\n }\n\n const host = request.headers.get(\"host\") || new URL(request.url).host;\n if (isLocalHost(host.split(\":\")[0])) {\n return `${new URL(request.url).origin}/__preview/${name}`;\n }\n\n return `https://${name}.${config.domain}`;\n}\n\nfunction json(value: unknown, status = 200) {\n return new Response(JSON.stringify(value), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n\nfunction text(value: string, status = 200) {\n return new Response(value, {\n status,\n headers: {\n \"content-type\": \"text/plain; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n\nfunction sanitizePreviewName(value: string | undefined) {\n if (!value) return undefined;\n return value\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 48);\n}\n\nfunction randomPreviewName() {\n return `farm-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction randomId(prefix: string) {\n return `${prefix}_${crypto.randomUUID().replace(/-/g, \"\")}`;\n}\n\nfunction normalizeDomain(value: string) {\n return value\n .replace(/^https?:\\/\\//, \"\")\n .replace(/^\\.*/, \"\")\n .replace(/\\/*$/, \"\");\n}\n\nfunction responseKey(sessionId: string, requestId: string) {\n return `${sessionId}:${requestId}`;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n if (!Number.isFinite(value)) return min;\n return Math.max(min, Math.min(max, value));\n}\n\nfunction isLocalHost(hostname: string) {\n return hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"::1\";\n}\n\nclass GatewayHttpError extends Error {\n constructor(\n public status: number,\n message: string,\n ) {\n super(message);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["DEFAULT_SESSION_TTL_MS","delay"],"sources":["../src/auth.ts","../src/index.ts"],"sourcesContent":["import { createHmac, randomUUID, timingSafeEqual } from \"node:crypto\";\n\nexport interface PreviewManagedAuthOptions {\n /** Secret used to sign account tokens and one-preview tunnel grants. */\n signingSecret: string;\n /** Public client id for the Farming Labs GitHub OAuth app. */\n githubClientId: string;\n accountTokenTtlMs?: number;\n defaultSessionTtlMs?: number;\n maxSessionTtlMs?: number;\n githubApiUrl?: string;\n fetch?: typeof fetch;\n}\n\nexport interface PreviewAccountIdentity {\n provider: \"github\";\n subject: string;\n login: string;\n name?: string;\n avatarUrl?: string;\n}\n\nexport interface PreviewAccountClaims extends PreviewAccountIdentity {\n kind: \"account\";\n issuedAt: number;\n expiresAt: number;\n}\n\nexport interface PreviewTunnelGrantClaims {\n kind: \"tunnel\";\n subject: string;\n login: string;\n name: string;\n issuedAt: number;\n expiresAt: number;\n}\n\nexport interface PreviewTunnelGrant {\n token: string;\n name: string;\n expiresAt: number;\n}\n\nconst TOKEN_PREFIX = \"farm_preview_v1\";\nconst DEFAULT_ACCOUNT_TOKEN_TTL_MS = 1000 * 60 * 60 * 24 * 7;\nconst DEFAULT_SESSION_TTL_MS = 1000 * 60 * 60;\nconst DEFAULT_MAX_SESSION_TTL_MS = 1000 * 60 * 60 * 24;\nconst MIN_SESSION_TTL_MS = 1000 * 60;\nconst MAX_CLOCK_SKEW_MS = 1000 * 60 * 5;\n\nexport function getPreviewAuthPublicConfig(options: PreviewManagedAuthOptions) {\n validatePreviewAuthOptions(options);\n const durations = resolvePreviewDurations(options);\n return {\n enabled: true as const,\n provider: \"github\" as const,\n clientId: options.githubClientId,\n scope: \"read:user\",\n defaultSessionTtlMs: durations.defaultSessionTtlMs,\n maxSessionTtlMs: durations.maxSessionTtlMs,\n };\n}\n\nexport async function exchangeGitHubPreviewToken(\n providerToken: string,\n options: PreviewManagedAuthOptions,\n): Promise<{ token: string; expiresAt: number; user: PreviewAccountIdentity }> {\n validatePreviewAuthOptions(options);\n if (!providerToken || providerToken.length > 4096) {\n throw new PreviewAuthError(400, \"A GitHub access token is required.\");\n }\n\n const request = options.fetch || fetch;\n const response = await request(`${options.githubApiUrl || \"https://api.github.com\"}/user`, {\n headers: {\n accept: \"application/vnd.github+json\",\n authorization: `Bearer ${providerToken}`,\n \"user-agent\": \"farm-preview-gateway\",\n \"x-github-api-version\": \"2026-03-10\",\n },\n signal: AbortSignal.timeout(10_000),\n });\n if (!response.ok) {\n await response.body?.cancel();\n throw new PreviewAuthError(401, \"GitHub could not verify this Farm Preview login.\");\n }\n\n const profile = (await response.json()) as {\n id?: number;\n login?: string;\n name?: string | null;\n avatar_url?: string;\n };\n if (!Number.isSafeInteger(profile.id) || !profile.login) {\n throw new PreviewAuthError(502, \"GitHub returned an invalid user profile.\");\n }\n\n const now = Date.now();\n const expiresAt = now + (options.accountTokenTtlMs ?? DEFAULT_ACCOUNT_TOKEN_TTL_MS);\n const user: PreviewAccountIdentity = {\n provider: \"github\",\n subject: String(profile.id),\n login: profile.login,\n ...(profile.name ? { name: profile.name } : {}),\n ...(profile.avatar_url ? { avatarUrl: profile.avatar_url } : {}),\n };\n const claims: PreviewAccountClaims = {\n kind: \"account\",\n ...user,\n issuedAt: now,\n expiresAt,\n };\n\n return {\n token: signPreviewClaims(claims, options.signingSecret),\n expiresAt,\n user,\n };\n}\n\nexport function verifyPreviewAccountToken(\n token: string,\n options: Pick<PreviewManagedAuthOptions, \"signingSecret\">,\n): PreviewAccountClaims {\n const claims = verifyPreviewClaims(token, options.signingSecret);\n if (\n claims.kind !== \"account\" ||\n claims.provider !== \"github\" ||\n typeof claims.subject !== \"string\" ||\n typeof claims.login !== \"string\"\n ) {\n throw new PreviewAuthError(401, \"The Farm Preview account token is invalid.\");\n }\n return claims as unknown as PreviewAccountClaims;\n}\n\nexport function issuePreviewTunnelGrant(\n account: PreviewAccountClaims,\n input: { name: string; expiresInMs?: number },\n options: PreviewManagedAuthOptions,\n): PreviewTunnelGrant {\n validatePreviewAuthOptions(options);\n const durations = resolvePreviewDurations(options);\n if (\n input.expiresInMs !== undefined &&\n (!Number.isSafeInteger(input.expiresInMs) || input.expiresInMs <= 0)\n ) {\n throw new PreviewAuthError(400, \"Preview expiry must be a positive integer.\");\n }\n const requestedTtlMs = input.expiresInMs ?? durations.defaultSessionTtlMs;\n const ttlMs = Math.max(MIN_SESSION_TTL_MS, Math.min(durations.maxSessionTtlMs, requestedTtlMs));\n const issuedAt = Date.now();\n const expiresAt = issuedAt + ttlMs;\n const claims: PreviewTunnelGrantClaims & { nonce: string } = {\n kind: \"tunnel\",\n subject: account.subject,\n login: account.login,\n name: input.name,\n issuedAt,\n expiresAt,\n nonce: randomUUID(),\n };\n return {\n token: signPreviewClaims(claims, options.signingSecret),\n name: input.name,\n expiresAt,\n };\n}\n\nexport function verifyPreviewTunnelGrant(\n token: string,\n options: Pick<PreviewManagedAuthOptions, \"signingSecret\"> & { name?: string },\n): PreviewTunnelGrantClaims {\n const claims = verifyPreviewClaims(token, options.signingSecret);\n if (\n claims.kind !== \"tunnel\" ||\n typeof claims.subject !== \"string\" ||\n typeof claims.login !== \"string\" ||\n typeof claims.name !== \"string\" ||\n (options.name !== undefined && claims.name !== options.name)\n ) {\n throw new PreviewAuthError(401, \"The Farm Preview tunnel grant is invalid.\");\n }\n return claims as unknown as PreviewTunnelGrantClaims;\n}\n\nexport function readPreviewBearerToken(request: Request): string | undefined {\n const authorization = request.headers.get(\"authorization\");\n if (!authorization || !/^Bearer /i.test(authorization)) return undefined;\n const token = authorization.slice(7).trim();\n return token || undefined;\n}\n\nexport class PreviewAuthError extends Error {\n constructor(\n public status: number,\n message: string,\n ) {\n super(message);\n }\n}\n\nfunction resolvePreviewDurations(options: PreviewManagedAuthOptions) {\n const maxSessionTtlMs = Math.max(\n MIN_SESSION_TTL_MS,\n options.maxSessionTtlMs ?? DEFAULT_MAX_SESSION_TTL_MS,\n );\n return {\n maxSessionTtlMs,\n defaultSessionTtlMs: Math.min(\n maxSessionTtlMs,\n Math.max(MIN_SESSION_TTL_MS, options.defaultSessionTtlMs ?? DEFAULT_SESSION_TTL_MS),\n ),\n };\n}\n\nfunction validatePreviewAuthOptions(options: PreviewManagedAuthOptions) {\n if (Buffer.byteLength(options.signingSecret) < 32) {\n throw new Error(\"Farm Preview auth signingSecret must contain at least 32 bytes.\");\n }\n if (!options.githubClientId) {\n throw new Error(\"Farm Preview auth requires a GitHub OAuth client id.\");\n }\n for (const [name, value] of [\n [\"accountTokenTtlMs\", options.accountTokenTtlMs],\n [\"defaultSessionTtlMs\", options.defaultSessionTtlMs],\n [\"maxSessionTtlMs\", options.maxSessionTtlMs],\n ] as const) {\n if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {\n throw new Error(`Farm Preview auth ${name} must be a positive integer.`);\n }\n }\n}\n\nfunction signPreviewClaims(claims: object, secret: string) {\n const payload = Buffer.from(JSON.stringify(claims)).toString(\"base64url\");\n const signature = createHmac(\"sha256\", secret)\n .update(`${TOKEN_PREFIX}.${payload}`)\n .digest(\"base64url\");\n return `${TOKEN_PREFIX}.${payload}.${signature}`;\n}\n\nfunction verifyPreviewClaims(token: string, secret: string): Record<string, unknown> {\n if (!token || token.length > 16_384) {\n throw new PreviewAuthError(401, \"The Farm Preview credential is invalid.\");\n }\n const [prefix, payload, presentedSignature, ...rest] = token.split(\".\");\n if (prefix !== TOKEN_PREFIX || !payload || !presentedSignature || rest.length) {\n throw new PreviewAuthError(401, \"The Farm Preview credential is invalid.\");\n }\n\n const expectedSignature = createHmac(\"sha256\", secret)\n .update(`${TOKEN_PREFIX}.${payload}`)\n .digest();\n let presented: Buffer;\n try {\n presented = Buffer.from(presentedSignature, \"base64url\");\n } catch {\n throw new PreviewAuthError(401, \"The Farm Preview credential is invalid.\");\n }\n if (\n presented.length !== expectedSignature.length ||\n !timingSafeEqual(presented, expectedSignature)\n ) {\n throw new PreviewAuthError(401, \"The Farm Preview credential is invalid.\");\n }\n\n let claims: unknown;\n try {\n claims = JSON.parse(Buffer.from(payload, \"base64url\").toString(\"utf8\"));\n } catch {\n throw new PreviewAuthError(401, \"The Farm Preview credential is invalid.\");\n }\n if (\n typeof claims !== \"object\" ||\n claims === null ||\n Array.isArray(claims) ||\n !(\"issuedAt\" in claims) ||\n !(\"expiresAt\" in claims) ||\n !Number.isSafeInteger(claims.issuedAt) ||\n !Number.isSafeInteger(claims.expiresAt) ||\n Number(claims.issuedAt) > Date.now() + MAX_CLOCK_SKEW_MS ||\n Number(claims.expiresAt) <= Date.now()\n ) {\n throw new PreviewAuthError(401, \"The Farm Preview credential has expired or is invalid.\");\n }\n return claims as Record<string, unknown>;\n}\n","import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from \"node:http\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport {\n exchangeGitHubPreviewToken,\n getPreviewAuthPublicConfig,\n issuePreviewTunnelGrant,\n PreviewAuthError,\n readPreviewBearerToken,\n verifyPreviewAccountToken,\n verifyPreviewTunnelGrant,\n type PreviewManagedAuthOptions,\n} from \"./auth.js\";\n\nexport {\n exchangeGitHubPreviewToken,\n getPreviewAuthPublicConfig,\n issuePreviewTunnelGrant,\n PreviewAuthError,\n readPreviewBearerToken,\n verifyPreviewAccountToken,\n verifyPreviewTunnelGrant,\n type PreviewAccountClaims,\n type PreviewAccountIdentity,\n type PreviewManagedAuthOptions,\n type PreviewTunnelGrant,\n type PreviewTunnelGrantClaims,\n} from \"./auth.js\";\n\nexport interface PreviewGatewayOptions {\n domain?: string;\n baseUrl?: string;\n store?: PreviewGatewayStore;\n sessionTtlMs?: number;\n clientHeartbeatTimeoutMs?: number;\n requestTimeoutMs?: number;\n pollTimeoutMs?: number;\n pollIntervalMs?: number;\n maxBodyBytes?: number;\n maxResponseBodyBytes?: number;\n /** Managed account login and one-preview grant configuration. */\n auth?: PreviewManagedAuthOptions;\n}\n\nexport interface PreviewGatewaySession {\n id: string;\n name: string;\n hostname: string;\n publicUrl: string;\n token: string;\n localUrl?: string;\n createdAt: number;\n expiresAt: number;\n lastHeartbeatAt?: number;\n}\n\nexport interface PreviewGatewayRequest {\n id: string;\n method: string;\n path: string;\n headers: Record<string, string>;\n body?: string | null;\n encoding?: \"base64\";\n createdAt: number;\n cancelled?: true;\n}\n\nexport interface PreviewGatewayResponse {\n status: number;\n // A header may carry multiple values (notably Set-Cookie: a login response\n // commonly sets a session cookie and a CSRF cookie). Those must survive as\n // an array through the store and back onto the public response.\n headers?: Record<string, string | string[]>;\n body?: string | null;\n encoding?: \"base64\";\n}\n\nexport interface PreviewGatewayStore {\n createSession(session: PreviewGatewaySession, ttlMs: number): Promise<void>;\n getSessionByName(name: string): Promise<PreviewGatewaySession | undefined>;\n getSessionById(id: string): Promise<PreviewGatewaySession | undefined>;\n touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void>;\n enqueueRequest(sessionId: string, request: PreviewGatewayRequest, ttlMs: number): Promise<void>;\n takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]>;\n saveResponse(\n sessionId: string,\n requestId: string,\n response: PreviewGatewayResponse,\n ttlMs: number,\n ): Promise<void>;\n getResponse(sessionId: string, requestId: string): Promise<PreviewGatewayResponse | undefined>;\n deleteResponse(sessionId: string, requestId: string): Promise<void>;\n deleteSession(session: PreviewGatewaySession): Promise<void>;\n}\n\ninterface PreviewGatewayRuntimeConfig {\n domain: string;\n baseUrl?: string;\n sessionTtlMs: number;\n clientHeartbeatTimeoutMs: number;\n requestTimeoutMs: number;\n pollTimeoutMs: number;\n pollIntervalMs: number;\n maxBodyBytes: number;\n maxResponseBodyBytes: number;\n auth?: PreviewManagedAuthOptions;\n}\n\nconst DEFAULT_DOMAIN = \"preview.farmjs.dev\";\nconst DEFAULT_SESSION_TTL_MS = 1000 * 60 * 30;\nconst DEFAULT_CLIENT_HEARTBEAT_TIMEOUT_MS = 1000 * 20;\nconst DEFAULT_REQUEST_TIMEOUT_MS = 25000;\nconst DEFAULT_POLL_TIMEOUT_MS = 15000;\nconst DEFAULT_POLL_INTERVAL_MS = 100;\nconst DEFAULT_MAX_BODY_BYTES = 1024 * 1024 * 5;\nconst DEFAULT_MAX_RESPONSE_BODY_BYTES = 1024 * 1024 * 5;\nconst DEFAULT_POLL_REQUEST_LIMIT = 50;\nconst HOP_BY_HOP_HEADERS = new Set([\n \"connection\",\n \"content-length\",\n \"keep-alive\",\n \"proxy-authenticate\",\n \"proxy-authorization\",\n \"te\",\n \"trailer\",\n \"transfer-encoding\",\n \"upgrade\",\n]);\n\nfunction getHopByHopHeaderNames(headers: Headers | Record<string, string | string[]>): Set<string> {\n const names = new Set(HOP_BY_HOP_HEADERS);\n const connection =\n headers instanceof Headers\n ? headers.get(\"connection\")\n : Object.entries(headers).find(([name]) => name.toLowerCase() === \"connection\")?.[1];\n const values = Array.isArray(connection) ? connection : connection ? [connection] : [];\n for (const value of values) {\n for (const name of value.split(\",\")) {\n const normalized = name.trim().toLowerCase();\n if (normalized) names.add(normalized);\n }\n }\n return names;\n}\n\n/**\n * A gateway request handler, plus the name-ownership probe another preview\n * transport needs before it claims the same public hostname.\n */\nexport type PreviewGatewayHandler = ((request: Request) => Promise<Response>) & {\n isPreviewNameClaimed(name: string): Promise<boolean>;\n};\n\nexport type NodePreviewGatewayHandler = ((\n req: IncomingMessage,\n res: ServerResponse,\n) => Promise<void>) & {\n isPreviewNameClaimed(name: string): Promise<boolean>;\n};\n\nexport function createPreviewGatewayHandler(\n options: PreviewGatewayOptions = {},\n): PreviewGatewayHandler {\n if (options.auth) getPreviewAuthPublicConfig(options.auth);\n const store = options.store || createPreviewGatewayStoreFromEnv();\n const config = {\n domain: normalizeDomain(options.domain || process.env.FARM_PREVIEW_DOMAIN || DEFAULT_DOMAIN),\n baseUrl: options.baseUrl || process.env.FARM_PREVIEW_GATEWAY_URL,\n sessionTtlMs: options.sessionTtlMs ?? DEFAULT_SESSION_TTL_MS,\n clientHeartbeatTimeoutMs:\n options.clientHeartbeatTimeoutMs ?? DEFAULT_CLIENT_HEARTBEAT_TIMEOUT_MS,\n requestTimeoutMs: options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,\n pollTimeoutMs: options.pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,\n pollIntervalMs: options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,\n maxBodyBytes: options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES,\n maxResponseBodyBytes: options.maxResponseBodyBytes ?? DEFAULT_MAX_RESPONSE_BODY_BYTES,\n auth: options.auth,\n };\n\n const handlePreviewGatewayRequest = async function handlePreviewGatewayRequest(\n request: Request,\n ): Promise<Response> {\n try {\n const url = new URL(request.url);\n\n if (request.method === \"GET\" && url.pathname === \"/api/health\") {\n return json({\n ok: true,\n domain: config.domain,\n store: store.constructor.name || \"PreviewGatewayStore\",\n });\n }\n\n if (request.method === \"GET\" && url.pathname === \"/api/auth/config\") {\n return json(\n config.auth\n ? getPreviewAuthPublicConfig(config.auth)\n : {\n enabled: false,\n defaultSessionTtlMs: config.sessionTtlMs,\n maxSessionTtlMs: config.sessionTtlMs,\n },\n );\n }\n\n if (request.method === \"POST\" && url.pathname === \"/api/auth/exchange\") {\n if (!config.auth) return text(\"Managed preview authentication is not configured.\", 404);\n return await exchangePreviewAccount(request, config.auth);\n }\n\n if (request.method === \"POST\" && url.pathname === \"/api/tunnel/grants\") {\n if (!config.auth) return text(\"Managed preview authentication is not configured.\", 404);\n return await createTunnelGrant(request, config.auth);\n }\n\n if (request.method === \"POST\" && url.pathname === \"/api/sessions\") {\n return await createSession(request, store, config);\n }\n\n const sessionRoute = matchSessionRoute(url.pathname);\n if (sessionRoute) {\n return await handleSessionRoute(request, url, store, config, sessionRoute);\n }\n\n const publicRoute = resolvePublicPreviewRoute(request, url, config.domain);\n if (publicRoute) {\n return await proxyPublicRequest(request, url, store, config, publicRoute);\n }\n\n return json(\n {\n ok: true,\n message: \"Farm Preview Gateway\",\n createSession: \"/api/sessions\",\n health: \"/api/health\",\n },\n 200,\n );\n } catch (error) {\n if (error instanceof GatewayHttpError || error instanceof PreviewAuthError) {\n return text(error.message, error.status);\n }\n return text(\n error instanceof Error ? error.message : \"Unexpected preview gateway error.\",\n 500,\n );\n }\n };\n\n // The persistent relay serves the same public hostnames from a separate name\n // namespace, and its route wins over this handler, so it has to be able to\n // ask whether a live polling session already owns a name before accepting a\n // claim for it.\n return Object.assign(handlePreviewGatewayRequest, {\n async isPreviewNameClaimed(name: string) {\n const claimed = sanitizePreviewName(name);\n if (!claimed) return false;\n const session = await store.getSessionByName(claimed);\n return Boolean(session && isPreviewClientOnline(session, config));\n },\n });\n}\n\nasync function exchangePreviewAccount(request: Request, auth: PreviewManagedAuthOptions) {\n const input = (await request.json().catch(() => ({}))) as {\n provider?: string;\n accessToken?: string;\n };\n if (input.provider !== \"github\") {\n throw new PreviewAuthError(400, \"Farm Preview currently supports GitHub sign-in.\");\n }\n return json(await exchangeGitHubPreviewToken(input.accessToken || \"\", auth));\n}\n\nasync function createTunnelGrant(request: Request, auth: PreviewManagedAuthOptions) {\n const accountToken = readPreviewBearerToken(request);\n if (!accountToken) {\n throw new PreviewAuthError(401, \"Sign in with Farm Preview before creating a tunnel grant.\");\n }\n const account = verifyPreviewAccountToken(accountToken, auth);\n const input = (await request.json().catch(() => ({}))) as {\n name?: string;\n expiresInMs?: number;\n };\n const name = sanitizePreviewName(input.name);\n if (!name) throw new PreviewAuthError(400, \"A valid preview name is required.\");\n\n return json(\n issuePreviewTunnelGrant(\n account,\n {\n name,\n ...(input.expiresInMs !== undefined ? { expiresInMs: input.expiresInMs } : {}),\n },\n auth,\n ),\n );\n}\n\nexport function createNodePreviewGatewayHandler(\n options: PreviewGatewayOptions = {},\n): NodePreviewGatewayHandler {\n const handler = createPreviewGatewayHandler(options);\n\n const nodePreviewGatewayHandler = async function nodePreviewGatewayHandler(\n req: IncomingMessage,\n res: ServerResponse,\n ) {\n const controller = new AbortController();\n const abort = () => controller.abort();\n const abortOnClose = () => {\n if (!res.writableEnded) abort();\n };\n req.once(\"aborted\", abort);\n res.once(\"close\", abortOnClose);\n try {\n const response = await handler(nodeRequestToWebRequest(req, controller.signal));\n if (controller.signal.aborted || res.destroyed) {\n await response.body?.cancel();\n return;\n }\n res.statusCode = response.status;\n // Headers.forEach folds repeated headers into one comma-joined value, which\n // corrupts multiple Set-Cookie. Emit those separately as an array so each\n // cookie becomes its own header line.\n const setCookies = response.headers.getSetCookie?.() ?? [];\n response.headers.forEach((value, key) => {\n if (key.toLowerCase() === \"set-cookie\") return;\n res.setHeader(key, value);\n });\n if (setCookies.length > 0) {\n res.setHeader(\"set-cookie\", setCookies);\n }\n res.end(Buffer.from(await response.arrayBuffer()));\n } finally {\n req.off(\"aborted\", abort);\n res.off(\"close\", abortOnClose);\n }\n };\n\n return Object.assign(nodePreviewGatewayHandler, {\n isPreviewNameClaimed: handler.isPreviewNameClaimed,\n });\n}\n\nexport function createPreviewGatewayStoreFromEnv(): PreviewGatewayStore {\n const redisUrl = process.env.UPSTASH_REDIS_REST_URL || process.env.KV_REST_API_URL;\n const redisToken = process.env.UPSTASH_REDIS_REST_TOKEN || process.env.KV_REST_API_TOKEN;\n\n if (redisUrl && redisToken) {\n return new RedisRestPreviewGatewayStore({\n url: redisUrl,\n token: redisToken,\n });\n }\n\n return new MemoryPreviewGatewayStore();\n}\n\nexport class MemoryPreviewGatewayStore implements PreviewGatewayStore {\n private sessions = new Map<string, PreviewGatewaySession>();\n private names = new Map<string, string>();\n private queues = new Map<string, PreviewGatewayRequest[]>();\n private responses = new Map<string, PreviewGatewayResponse>();\n\n async createSession(session: PreviewGatewaySession): Promise<void> {\n this.sessions.set(session.id, session);\n this.names.set(session.name, session.id);\n this.queues.set(session.id, []);\n }\n\n async getSessionByName(name: string): Promise<PreviewGatewaySession | undefined> {\n const id = this.names.get(name);\n return id ? this.getSessionById(id) : undefined;\n }\n\n async getSessionById(id: string): Promise<PreviewGatewaySession | undefined> {\n const session = this.sessions.get(id);\n if (!session) return undefined;\n if (session.expiresAt <= Date.now()) {\n await this.deleteSession(session);\n return undefined;\n }\n return session;\n }\n\n async touchSession(session: PreviewGatewaySession, _ttlMs: number): Promise<void> {\n // A concurrent delete (the preview was stopped) must win: a poll handler\n // that outlives the DELETE must never resurrect a session that is no\n // longer stored, or its name would look \"online\" and block a restart.\n if (!this.sessions.has(session.id)) return;\n this.sessions.set(session.id, session);\n const nameOwner = this.names.get(session.name);\n if (!nameOwner || nameOwner === session.id) {\n this.names.set(session.name, session.id);\n }\n }\n\n async enqueueRequest(sessionId: string, request: PreviewGatewayRequest): Promise<void> {\n const queue = this.queues.get(sessionId) || [];\n queue.push(request);\n this.queues.set(sessionId, queue);\n }\n\n async takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]> {\n const queue = this.queues.get(sessionId) || [];\n return queue.splice(0, limit);\n }\n\n async saveResponse(\n sessionId: string,\n requestId: string,\n response: PreviewGatewayResponse,\n ): Promise<void> {\n this.responses.set(responseKey(sessionId, requestId), response);\n }\n\n async getResponse(\n sessionId: string,\n requestId: string,\n ): Promise<PreviewGatewayResponse | undefined> {\n return this.responses.get(responseKey(sessionId, requestId));\n }\n\n async deleteResponse(sessionId: string, requestId: string): Promise<void> {\n this.responses.delete(responseKey(sessionId, requestId));\n }\n\n async deleteSession(session: PreviewGatewaySession): Promise<void> {\n this.sessions.delete(session.id);\n // The name may have been taken over by a newer session; only remove the\n // mapping if it still points at the session being deleted.\n if (this.names.get(session.name) === session.id) {\n this.names.delete(session.name);\n }\n this.queues.delete(session.id);\n }\n}\n\nexport class RedisRestPreviewGatewayStore implements PreviewGatewayStore {\n constructor(private options: { url: string; token: string; prefix?: string }) {}\n\n async createSession(session: PreviewGatewaySession, ttlMs: number): Promise<void> {\n await this.command(\"SET\", this.sessionKey(session.id), JSON.stringify(session), \"PX\", ttlMs);\n await this.command(\"SET\", this.nameKey(session.name), session.id, \"PX\", ttlMs);\n await this.command(\"DEL\", this.queueKey(session.id));\n }\n\n async getSessionByName(name: string): Promise<PreviewGatewaySession | undefined> {\n const id = await this.command<string | null>(\"GET\", this.nameKey(name));\n return id ? this.getSessionById(id) : undefined;\n }\n\n async getSessionById(id: string): Promise<PreviewGatewaySession | undefined> {\n const value = await this.command<string | null>(\"GET\", this.sessionKey(id));\n return value ? JSON.parse(value) : undefined;\n }\n\n async touchSession(session: PreviewGatewaySession, ttlMs: number): Promise<void> {\n const nextSession = { ...session };\n const remainingTtlMs = Math.max(1, Math.min(ttlMs, session.expiresAt - Date.now()));\n // XX only refreshes a session key that still exists. A poll handler that\n // outlives a DELETE must not recreate the session (nor its name mapping),\n // which would otherwise block reclaiming the name on restart.\n const result = await this.command<string | null>(\n \"SET\",\n this.sessionKey(session.id),\n JSON.stringify(nextSession),\n \"PX\",\n remainingTtlMs,\n \"XX\",\n );\n if (result === null) return;\n const nameOwner = await this.command<string | null>(\"GET\", this.nameKey(session.name));\n if (!nameOwner || nameOwner === session.id) {\n await this.command(\"SET\", this.nameKey(session.name), session.id, \"PX\", remainingTtlMs);\n }\n await this.command(\"EXPIRE\", this.queueKey(session.id), Math.ceil(remainingTtlMs / 1000));\n }\n\n async enqueueRequest(\n sessionId: string,\n request: PreviewGatewayRequest,\n ttlMs: number,\n ): Promise<void> {\n await this.command(\"RPUSH\", this.queueKey(sessionId), JSON.stringify(request));\n await this.command(\"EXPIRE\", this.queueKey(sessionId), Math.ceil(ttlMs / 1000));\n }\n\n async takeRequests(sessionId: string, limit: number): Promise<PreviewGatewayRequest[]> {\n const values = await this.command<string[] | string | null>(\n \"LPOP\",\n this.queueKey(sessionId),\n limit,\n );\n if (!values) return [];\n const list = Array.isArray(values) ? values : [values];\n return list.map((value) => JSON.parse(value));\n }\n\n async saveResponse(\n sessionId: string,\n requestId: string,\n response: PreviewGatewayResponse,\n ttlMs: number,\n ): Promise<void> {\n await this.command(\n \"SET\",\n this.responseKey(sessionId, requestId),\n JSON.stringify(response),\n \"PX\",\n ttlMs,\n );\n }\n\n async getResponse(\n sessionId: string,\n requestId: string,\n ): Promise<PreviewGatewayResponse | undefined> {\n const value = await this.command<string | null>(\"GET\", this.responseKey(sessionId, requestId));\n return value ? JSON.parse(value) : undefined;\n }\n\n async deleteResponse(sessionId: string, requestId: string): Promise<void> {\n await this.command(\"DEL\", this.responseKey(sessionId, requestId));\n }\n\n async deleteSession(session: PreviewGatewaySession): Promise<void> {\n const keys = [this.sessionKey(session.id), this.queueKey(session.id)];\n // The name may have been taken over by a newer session; only remove the\n // mapping if it still points at the session being deleted.\n const nameOwner = await this.command<string | null>(\"GET\", this.nameKey(session.name));\n if (nameOwner === session.id) {\n keys.push(this.nameKey(session.name));\n }\n await this.command(\"DEL\", ...keys);\n }\n\n private async command<T = unknown>(...args: Array<string | number>) {\n const response = await fetch(this.options.url, {\n method: \"POST\",\n headers: {\n authorization: `Bearer ${this.options.token}`,\n \"content-type\": \"application/json\",\n },\n body: JSON.stringify(args),\n });\n\n if (!response.ok) {\n throw new Error(\n `Preview gateway Redis command failed (${response.status}): ${await response.text()}`,\n );\n }\n\n const data = (await response.json()) as { result?: T; error?: string };\n if (data.error) {\n throw new Error(`Preview gateway Redis command failed: ${data.error}`);\n }\n return data.result as T;\n }\n\n private key(value: string) {\n return `${this.options.prefix || \"farm-preview\"}:${value}`;\n }\n\n private sessionKey(id: string) {\n return this.key(`session:${id}`);\n }\n\n private nameKey(name: string) {\n return this.key(`name:${name}`);\n }\n\n private queueKey(id: string) {\n return this.key(`queue:${id}`);\n }\n\n private responseKey(sessionId: string, requestId: string) {\n return this.key(`response:${sessionId}:${requestId}`);\n }\n}\n\nasync function createSession(\n request: Request,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n) {\n const input = (await request.json().catch(() => ({}))) as {\n name?: string;\n localUrl?: string;\n expiresInMs?: number;\n };\n const requestedName = sanitizePreviewName(input.name);\n let name = requestedName || randomPreviewName();\n let expiresAt = Date.now() + config.sessionTtlMs;\n\n if (config.auth) {\n const grantToken = readPreviewBearerToken(request);\n if (!grantToken) {\n throw new GatewayHttpError(401, \"Sign in with Farm Preview before creating a session.\");\n }\n try {\n const grant = verifyPreviewTunnelGrant(grantToken, {\n signingSecret: config.auth.signingSecret,\n ...(requestedName ? { name: requestedName } : {}),\n });\n name = grant.name;\n expiresAt = grant.expiresAt;\n } catch (error) {\n if (error instanceof PreviewAuthError)\n throw new GatewayHttpError(error.status, error.message);\n throw error;\n }\n } else if (Number.isFinite(input.expiresInMs)) {\n expiresAt = Date.now() + clamp(Number(input.expiresInMs), 60_000, config.sessionTtlMs);\n }\n\n // A name that is still owned by a live session cannot be claimed by a new\n // one — otherwise any caller could repoint an active preview's public URL\n // to their own session. Stale sessions (missed heartbeats) may be taken\n // over, matching how public routing treats them.\n for (let attempt = 0; ; attempt++) {\n const existing = await store.getSessionByName(name);\n if (!existing || !isPreviewClientOnline(existing, config)) {\n break;\n }\n if (requestedName || config.auth) {\n return text(\n `A Farm preview named \"${name}\" is already active. Choose another name or stop the running preview.`,\n 409,\n );\n }\n if (attempt >= 4) {\n return text(\"Could not allocate a unique preview name. Try again.\", 503);\n }\n name = randomPreviewName();\n }\n\n const hostname = `${name}.${config.domain}`;\n const session: PreviewGatewaySession = {\n id: randomId(\"sess\"),\n name,\n hostname,\n publicUrl: createPublicUrl(request, config, name),\n token: randomId(\"token\"),\n localUrl: input.localUrl,\n createdAt: Date.now(),\n expiresAt,\n lastHeartbeatAt: Date.now(),\n };\n\n await store.createSession(session, Math.max(1, expiresAt - Date.now()));\n\n return json({\n id: session.id,\n name: session.name,\n token: session.token,\n publicUrl: session.publicUrl,\n expiresAt: session.expiresAt,\n });\n}\n\nasync function handleSessionRoute(\n request: Request,\n url: URL,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n route: { sessionId: string; action?: string; requestId?: string },\n) {\n const session = await requireSession(\n store,\n route.sessionId,\n readPreviewBearerToken(request) || url.searchParams.get(\"token\"),\n );\n\n if (request.method === \"GET\" && route.action === \"requests\") {\n return await pollSessionRequests(url, store, config, session);\n }\n\n if (request.method === \"POST\" && route.action === \"responses\" && route.requestId) {\n let response: PreviewGatewayResponse;\n try {\n response = await parsePreviewResponse(request, config.maxResponseBodyBytes);\n } catch (error) {\n if (error instanceof GatewayHttpError && error.status === 413) {\n await store.saveResponse(\n session.id,\n route.requestId,\n createOversizedPreviewResponse(config.maxResponseBodyBytes),\n remainingSessionTtl(session, config),\n );\n }\n throw error;\n }\n await store.saveResponse(\n session.id,\n route.requestId,\n response,\n remainingSessionTtl(session, config),\n );\n await markSessionOnline(store, config, session);\n return json({ ok: true });\n }\n\n if (request.method === \"POST\" && route.action === \"heartbeat\") {\n await markSessionOnline(store, config, session);\n return json({ ok: true, expiresAt: session.expiresAt });\n }\n\n if (request.method === \"DELETE\" && !route.action) {\n await store.deleteSession(session);\n return json({ ok: true });\n }\n\n return text(\"Preview gateway route not found.\", 404);\n}\n\nasync function parsePreviewResponse(request: Request, maxBodyBytes: number) {\n const maxEncodedBytes = Math.ceil(maxBodyBytes / 3) * 4 + 64 * 1024;\n const contentLength = Number(request.headers.get(\"content-length\") || 0);\n if (Number.isFinite(contentLength) && contentLength > maxEncodedBytes) {\n await request.body?.cancel();\n throw new GatewayHttpError(413, \"Preview response body is too large.\");\n }\n\n const bytes = await readBodyWithLimit(\n request,\n maxEncodedBytes,\n \"Preview response body is too large.\",\n );\n let response: PreviewGatewayResponse;\n try {\n response = JSON.parse(bytes.toString(\"utf8\")) as PreviewGatewayResponse;\n } catch {\n throw new GatewayHttpError(400, \"Preview response body must be valid JSON.\");\n }\n\n if (getPreviewResponseBodySize(response) > maxBodyBytes) {\n throw new GatewayHttpError(413, \"Preview response body is too large.\");\n }\n return response;\n}\n\nfunction getPreviewResponseBodySize(response: PreviewGatewayResponse) {\n if (!response.body) return 0;\n return Buffer.byteLength(response.body, response.encoding === \"base64\" ? \"base64\" : \"utf8\");\n}\n\nfunction createOversizedPreviewResponse(maxBodyBytes: number): PreviewGatewayResponse {\n return {\n status: 502,\n headers: { \"content-type\": \"text/plain; charset=utf-8\" },\n body: Buffer.from(\n `The local preview response exceeded the ${maxBodyBytes} byte limit.`,\n ).toString(\"base64\"),\n encoding: \"base64\",\n };\n}\n\nasync function pollSessionRequests(\n url: URL,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n session: PreviewGatewaySession,\n) {\n const waitMs = clamp(Number(url.searchParams.get(\"wait\") || config.pollTimeoutMs), 1000, 25000);\n const deadline = Date.now() + waitMs;\n\n await markSessionOnline(store, config, session);\n\n while (Date.now() < deadline) {\n const requests = await store.takeRequests(session.id, DEFAULT_POLL_REQUEST_LIMIT);\n if (requests.length) {\n await markSessionOnline(store, config, session);\n return json({ requests });\n }\n await delay(config.pollIntervalMs);\n }\n\n await markSessionOnline(store, config, session);\n return new Response(null, { status: 204 });\n}\n\nasync function proxyPublicRequest(\n request: Request,\n url: URL,\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n route: { name: string; path: string },\n) {\n const session = await store.getSessionByName(route.name);\n if (!session) {\n return inactivePreviewResponse(request, route.name);\n }\n if (!isPreviewClientOnline(session, config)) {\n await store.deleteSession(session);\n return inactivePreviewResponse(request, route.name);\n }\n\n const previewRequest = await serializePreviewRequest(\n request,\n url,\n route.path,\n config.maxBodyBytes,\n );\n await store.enqueueRequest(session.id, previewRequest, remainingSessionTtl(session, config));\n await store.touchSession(session, remainingSessionTtl(session, config));\n\n const response = await waitForPreviewResponse(\n store,\n config,\n session,\n previewRequest.id,\n request.signal,\n );\n if (response === \"cancelled\") {\n await store.enqueueRequest(\n session.id,\n {\n ...previewRequest,\n body: undefined,\n encoding: undefined,\n cancelled: true,\n createdAt: Date.now(),\n },\n remainingSessionTtl(session, config),\n );\n return new Response(null, { status: 499 });\n }\n if (response === \"stale\") {\n return inactivePreviewResponse(request, route.name);\n }\n if (!response) {\n return text(\"The local Farm preview did not respond before the gateway timed out.\", 504);\n }\n\n const headers = new Headers();\n const responseHopByHopHeaders = getHopByHopHeaderNames(response.headers || {});\n for (const [key, value] of Object.entries(response.headers || {})) {\n const normalized = key.toLowerCase();\n if (responseHopByHopHeaders.has(normalized) || normalized === \"content-encoding\") {\n continue;\n }\n // Append multi-valued headers (Set-Cookie) so every value reaches the\n // visitor; Headers.set would keep only the last.\n for (const single of Array.isArray(value) ? value : [value]) {\n headers.append(key, single);\n }\n }\n headers.set(\"cache-control\", \"no-store\");\n headers.set(\"x-farm-preview\", session.name);\n\n return new Response(\n response.body\n ? Buffer.from(response.body, response.encoding === \"base64\" ? \"base64\" : \"utf8\")\n : null,\n {\n status: response.status || 200,\n headers,\n },\n );\n}\n\nasync function waitForPreviewResponse(\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n session: PreviewGatewaySession,\n requestId: string,\n signal: AbortSignal,\n) {\n const deadline = Date.now() + config.requestTimeoutMs;\n let nextLivenessCheckAt = 0;\n\n while (Date.now() < deadline) {\n if (signal.aborted) return \"cancelled\";\n if (Date.now() >= nextLivenessCheckAt) {\n const latestSession = await store.getSessionById(session.id);\n if (!latestSession || !isPreviewClientOnline(latestSession, config)) {\n if (latestSession) await store.deleteSession(latestSession);\n return \"stale\";\n }\n nextLivenessCheckAt = Date.now() + 1000;\n }\n\n const response = await store.getResponse(session.id, requestId);\n if (response) {\n await store.deleteResponse(session.id, requestId);\n return response;\n }\n try {\n await delay(config.pollIntervalMs, undefined, { signal });\n } catch {\n if (signal.aborted) return \"cancelled\";\n throw new Error(\"Preview response polling was interrupted.\");\n }\n }\n\n return undefined;\n}\n\nasync function markSessionOnline(\n store: PreviewGatewayStore,\n config: PreviewGatewayRuntimeConfig,\n session: PreviewGatewaySession,\n) {\n session.lastHeartbeatAt = Date.now();\n await store.touchSession(session, remainingSessionTtl(session, config));\n}\n\nfunction remainingSessionTtl(session: PreviewGatewaySession, config: PreviewGatewayRuntimeConfig) {\n return Math.max(1, Math.min(config.sessionTtlMs, session.expiresAt - Date.now()));\n}\n\nfunction isPreviewClientOnline(\n session: PreviewGatewaySession,\n config: PreviewGatewayRuntimeConfig,\n) {\n const lastHeartbeatAt = session.lastHeartbeatAt || session.createdAt;\n return (\n session.expiresAt > Date.now() &&\n Date.now() - lastHeartbeatAt <= config.clientHeartbeatTimeoutMs\n );\n}\n\nasync function serializePreviewRequest(\n request: Request,\n url: URL,\n routePath: string,\n maxBodyBytes: number,\n): Promise<PreviewGatewayRequest> {\n const method = request.method.toUpperCase();\n const headers: Record<string, string> = {};\n const requestHopByHopHeaders = getHopByHopHeaderNames(request.headers);\n request.headers.forEach((value, key) => {\n const normalized = key.toLowerCase();\n if (\n !requestHopByHopHeaders.has(normalized) &&\n normalized !== \"forwarded\" &&\n !normalized.startsWith(\"x-forwarded-\")\n ) {\n headers[key] = value;\n }\n });\n\n headers[\"x-forwarded-host\"] = request.headers.get(\"host\") || url.host;\n headers[\"x-forwarded-proto\"] = url.protocol.replace(\":\", \"\");\n\n let body: string | undefined;\n if (method !== \"GET\" && method !== \"HEAD\") {\n const contentLength = Number(request.headers.get(\"content-length\") || 0);\n if (contentLength > maxBodyBytes) {\n throw new GatewayHttpError(413, \"Preview request body is too large.\");\n }\n\n const buffer = await readBodyWithLimit(\n request,\n maxBodyBytes,\n \"Preview request body is too large.\",\n );\n body = buffer.toString(\"base64\");\n }\n\n return {\n id: randomId(\"req\"),\n method,\n path: `${routePath}${url.search}`,\n headers,\n body,\n encoding: body ? \"base64\" : undefined,\n createdAt: Date.now(),\n };\n}\n\nasync function readBodyWithLimit(request: Request, maxBytes: number, message: string) {\n if (!request.body) return Buffer.alloc(0);\n const reader = request.body.getReader();\n const chunks: Uint8Array[] = [];\n let size = 0;\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n size += value.byteLength;\n if (size > maxBytes) {\n await reader.cancel();\n throw new GatewayHttpError(413, message);\n }\n chunks.push(value);\n }\n } catch (error) {\n await reader.cancel().catch(() => undefined);\n throw error;\n }\n return Buffer.concat(chunks, size);\n}\n\nasync function requireSession(store: PreviewGatewayStore, sessionId: string, token: string | null) {\n const session = await store.getSessionById(sessionId);\n if (!session || !token || token !== session.token) {\n throw new GatewayHttpError(401, \"Invalid preview gateway session.\");\n }\n return session;\n}\n\nfunction resolvePublicPreviewRoute(request: Request, url: URL, domain: string) {\n const pathMatch = url.pathname.match(/^\\/__preview\\/([^/]+)(\\/.*)?$/);\n if (pathMatch) {\n return {\n name: pathMatch[1],\n path: pathMatch[2] || \"/\",\n };\n }\n\n const host = (request.headers.get(\"host\") || url.host).split(\":\")[0];\n if (!host || host === domain || !host.endsWith(`.${domain}`)) {\n return undefined;\n }\n\n return {\n name: host.slice(0, -(domain.length + 1)),\n path: url.pathname || \"/\",\n };\n}\n\nfunction matchSessionRoute(pathname: string) {\n const match = pathname.match(\n /^\\/api\\/sessions\\/([^/]+)(?:\\/(requests|heartbeat|responses)(?:\\/([^/]+))?)?$/,\n );\n if (!match) return undefined;\n return {\n sessionId: match[1],\n action: match[2],\n requestId: match[3],\n };\n}\n\nfunction nodeRequestToWebRequest(req: IncomingMessage, signal?: AbortSignal) {\n const host = headerValue(req.headers, \"host\") || \"localhost\";\n // This adapter is the public trust boundary. A visitor-controlled forwarded\n // protocol must not influence the authority serialized to the local app.\n const protocol = isLocalHost(host.split(\":\")[0]) ? \"http\" : \"https\";\n const url = `${protocol}://${host}${req.url || \"/\"}`;\n const headers = nodeHeadersToWebHeaders(req.headers);\n const method = req.method || \"GET\";\n const init: RequestInit & { duplex?: \"half\" } = {\n method,\n headers,\n signal,\n };\n\n if (method !== \"GET\" && method !== \"HEAD\") {\n init.body = req as unknown as BodyInit;\n init.duplex = \"half\";\n }\n\n return new Request(url, init);\n}\n\nfunction nodeHeadersToWebHeaders(headers: IncomingHttpHeaders) {\n const next = new Headers();\n for (const [key, value] of Object.entries(headers)) {\n if (value === undefined) continue;\n if (Array.isArray(value)) {\n for (const item of value) next.append(key, item);\n } else {\n next.set(key, value);\n }\n }\n return next;\n}\n\nfunction headerValue(headers: IncomingHttpHeaders, key: string) {\n const value = headers[key];\n return Array.isArray(value) ? value[0] : value;\n}\n\nfunction createPublicUrl(request: Request, config: PreviewGatewayRuntimeConfig, name: string) {\n if (config.baseUrl) {\n const base = new URL(config.baseUrl);\n if (isLocalHost(base.hostname)) {\n return `${config.baseUrl.replace(/\\/+$/, \"\")}/__preview/${name}`;\n }\n }\n\n const host = request.headers.get(\"host\") || new URL(request.url).host;\n if (isLocalHost(host.split(\":\")[0])) {\n return `${new URL(request.url).origin}/__preview/${name}`;\n }\n\n return `https://${name}.${config.domain}`;\n}\n\nfunction json(value: unknown, status = 200) {\n return new Response(JSON.stringify(value), {\n status,\n headers: {\n \"content-type\": \"application/json\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n\nfunction text(value: string, status = 200) {\n return new Response(value, {\n status,\n headers: {\n \"content-type\": \"text/plain; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n });\n}\n\nfunction inactivePreviewResponse(request: Request, name: string) {\n if (!request.headers.get(\"accept\")?.includes(\"text/html\")) {\n return text(`No active Farm preview is running for \"${name}\".`, 404);\n }\n const safeName = name.replace(/[&<>\"']/g, (character) => {\n if (character === \"&\") return \"&amp;\";\n if (character === \"<\") return \"&lt;\";\n if (character === \">\") return \"&gt;\";\n if (character === '\"') return \"&quot;\";\n return \"&#39;\";\n });\n return new Response(\n `<!doctype html><html lang=\"en\"><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Farm preview expired</title><style>body{font:16px/1.5 system-ui,sans-serif;display:grid;min-height:100vh;margin:0;place-items:center;background:#f7f7f5;color:#171714}main{max-width:34rem;padding:2rem;text-align:center}h1{font-size:1.75rem}code{background:#e9e9e4;border-radius:.35rem;padding:.15rem .35rem}</style><main><h1>This Farm preview has expired</h1><p>The temporary preview <code>${safeName}</code> is no longer connected.</p><p>Ask its owner to run <code>farm preview</code> again.</p></main></html>`,\n {\n status: 410,\n headers: {\n \"content-type\": \"text/html; charset=utf-8\",\n \"cache-control\": \"no-store\",\n },\n },\n );\n}\n\nfunction sanitizePreviewName(value: string | undefined) {\n if (!value) return undefined;\n return value\n .toLowerCase()\n .replace(/[^a-z0-9-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n .slice(0, 48);\n}\n\nfunction randomPreviewName() {\n return `farm-${Math.random().toString(36).slice(2, 8)}`;\n}\n\nfunction randomId(prefix: string) {\n return `${prefix}_${crypto.randomUUID().replace(/-/g, \"\")}`;\n}\n\nfunction normalizeDomain(value: string) {\n return value\n .replace(/^https?:\\/\\//, \"\")\n .replace(/^\\.*/, \"\")\n .replace(/\\/*$/, \"\");\n}\n\nfunction responseKey(sessionId: string, requestId: string) {\n return `${sessionId}:${requestId}`;\n}\n\nfunction clamp(value: number, min: number, max: number) {\n if (!Number.isFinite(value)) return min;\n return Math.max(min, Math.min(max, value));\n}\n\nfunction isLocalHost(hostname: string) {\n return hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"::1\";\n}\n\nclass GatewayHttpError extends Error {\n constructor(\n public status: number,\n message: string,\n ) {\n super(message);\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/preview-gateway",
3
- "version": "0.1.0-beta.99",
3
+ "version": "0.1.0",
4
4
  "description": "Farm.js preview gateway for Vercel-hosted instant preview URLs",
5
5
  "license": "MIT",
6
6
  "repository": {