@palbase/web 5.0.1 → 7.0.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 +15 -15
- package/dist/{analytics-facade-Cp3d4QI-.d.ts → analytics-facade-CdTK9Y0s.d.ts} +42 -1
- package/dist/{analytics-facade-CaluA4bW.d.cts → analytics-facade-TB-D4ww8.d.cts} +42 -1
- package/dist/{chunk-QKFPPZLF.js → chunk-7SNSLY5M.js} +20 -3
- package/dist/chunk-7SNSLY5M.js.map +1 -0
- package/dist/{chunk-BNAGRUIQ.js → chunk-ACCJV6FV.js} +32 -2
- package/dist/chunk-ACCJV6FV.js.map +1 -0
- package/dist/{chunk-OWLTZG2V.js → chunk-ZC6Q3GPX.js} +2 -2
- package/dist/index.cjs +18 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/internal.cjs +49 -2
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +3 -3
- package/dist/internal.d.ts +3 -3
- package/dist/internal.js +2 -2
- package/dist/next/client.cjs +49 -2
- package/dist/next/client.cjs.map +1 -1
- package/dist/next/client.js +2 -2
- package/dist/next/index.cjs +49 -2
- package/dist/next/index.cjs.map +1 -1
- package/dist/next/index.d.cts +2 -2
- package/dist/next/index.d.ts +2 -2
- package/dist/next/index.js +3 -3
- package/dist/next/{middleware.cjs → proxy.cjs} +11 -11
- package/dist/next/proxy.cjs.map +1 -0
- package/dist/next/{middleware.d.cts → proxy.d.cts} +32 -31
- package/dist/next/{middleware.d.ts → proxy.d.ts} +32 -31
- package/dist/next/{middleware.js → proxy.js} +8 -8
- package/dist/next/proxy.js.map +1 -0
- package/dist/{pb-DUK5qy81.d.ts → pb-B6ZcZy8v.d.ts} +1 -1
- package/dist/{pb-DvV6ftmf.d.cts → pb-CiSj4lM3.d.cts} +1 -1
- package/dist/react/index.cjs +1 -1
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +2 -2
- package/package.json +8 -8
- package/dist/chunk-BNAGRUIQ.js.map +0 -1
- package/dist/chunk-QKFPPZLF.js.map +0 -1
- package/dist/next/middleware.cjs.map +0 -1
- package/dist/next/middleware.js.map +0 -1
- /package/dist/{chunk-OWLTZG2V.js.map → chunk-ZC6Q3GPX.js.map} +0 -0
package/dist/next/index.d.cts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
|
-
import { P as PB } from '../pb-
|
|
3
|
-
import '../analytics-facade-
|
|
2
|
+
import { P as PB } from '../pb-CiSj4lM3.cjs';
|
|
3
|
+
import '../analytics-facade-TB-D4ww8.cjs';
|
|
4
4
|
import '../storage-BPaeSG8K.cjs';
|
|
5
5
|
import './pooled-flags-4GtDtsiu.js';
|
|
6
6
|
|
package/dist/next/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
|
-
import { P as PB } from '../pb-
|
|
3
|
-
import '../analytics-facade-
|
|
2
|
+
import { P as PB } from '../pb-B6ZcZy8v.js';
|
|
3
|
+
import '../analytics-facade-CdTK9Y0s.js';
|
|
4
4
|
import '../storage-BPaeSG8K.js';
|
|
5
5
|
import './pooled-flags-4GtDtsiu.js';
|
|
6
6
|
|
package/dist/next/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
importNextServer
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-ZC6Q3GPX.js";
|
|
4
4
|
import {
|
|
5
5
|
SESSION_COOKIE_ATTRS,
|
|
6
6
|
clearedSessionCookieNames,
|
|
@@ -14,10 +14,10 @@ import {
|
|
|
14
14
|
buildRuntime,
|
|
15
15
|
createBoundClient,
|
|
16
16
|
getRuntime
|
|
17
|
-
} from "../chunk-
|
|
17
|
+
} from "../chunk-7SNSLY5M.js";
|
|
18
18
|
import {
|
|
19
19
|
BackendError
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-ACCJV6FV.js";
|
|
21
21
|
import {
|
|
22
22
|
environmentRefFromApiKey
|
|
23
23
|
} from "../chunk-CFDU23TB.js";
|
|
@@ -27,12 +27,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
27
27
|
));
|
|
28
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
29
|
|
|
30
|
-
// src/next/
|
|
31
|
-
var
|
|
32
|
-
__export(
|
|
33
|
-
|
|
30
|
+
// src/next/proxy.ts
|
|
31
|
+
var proxy_exports = {};
|
|
32
|
+
__export(proxy_exports, {
|
|
33
|
+
palbeProxy: () => palbeProxy
|
|
34
34
|
});
|
|
35
|
-
module.exports = __toCommonJS(
|
|
35
|
+
module.exports = __toCommonJS(proxy_exports);
|
|
36
36
|
|
|
37
37
|
// src/api-key.ts
|
|
38
38
|
var PUBLISHABLE_API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20}$/;
|
|
@@ -170,7 +170,7 @@ async function importNextServer(caller) {
|
|
|
170
170
|
}
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
// src/next/
|
|
173
|
+
// src/next/proxy.ts
|
|
174
174
|
var DEFAULT_REFRESH_MARGIN_MS = 6e4;
|
|
175
175
|
var inflightRefreshes = /* @__PURE__ */ new Map();
|
|
176
176
|
function refreshSingleFlight(config, refreshToken) {
|
|
@@ -212,15 +212,15 @@ async function refresh(config, refreshToken) {
|
|
|
212
212
|
return { kind: "transient" };
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
|
-
async function
|
|
215
|
+
async function palbeProxy(request, config, opts) {
|
|
216
216
|
const ref = environmentRefFromPublishableApiKey(config.apiKey);
|
|
217
217
|
if (ref === "") {
|
|
218
218
|
throw new BackendError("validation", {
|
|
219
219
|
code: "validation_error",
|
|
220
|
-
message: "
|
|
220
|
+
message: "palbeProxy() needs the publishable API key from palbe.gen.ts (pb_<environmentRef>_c\u2026). Re-run `palbase web link` and copy the generated values."
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
|
-
const { NextResponse } = await importNextServer("
|
|
223
|
+
const { NextResponse } = await importNextServer("palbeProxy");
|
|
224
224
|
const passThrough = () => opts?.response ?? NextResponse.next({ request });
|
|
225
225
|
const session = decodeSessionCookies((name) => request.cookies.get(name)?.value, ref);
|
|
226
226
|
if (!session) return passThrough();
|
|
@@ -258,6 +258,6 @@ async function palbeMiddleware(request, config, opts) {
|
|
|
258
258
|
}
|
|
259
259
|
// Annotate the CommonJS export names for ESM import in node:
|
|
260
260
|
0 && (module.exports = {
|
|
261
|
-
|
|
261
|
+
palbeProxy
|
|
262
262
|
});
|
|
263
|
-
//# sourceMappingURL=
|
|
263
|
+
//# sourceMappingURL=proxy.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/next/proxy.ts","../../src/api-key.ts","../../src/errors.ts","../../src/next/cookie-codec.ts","../../src/next/shared.ts"],"sourcesContent":["/**\n * '@palbase/web/next/proxy' — proactive cookie-session refresh in the Next.js\n * proxy (the file convention that replaced `middleware` in Next 16; this\n * package's peer range is `next >= 16`).\n *\n * REQUIRED — not optional — for session-bearing RSC apps. Server Components\n * cannot write cookies, so pbServer's reactive refresh cannot PERSIST a\n * rotated session there: every RSC render would re-refresh from the SAME\n * cookie refresh token, and once two of those land more than 30s apart\n * (outside palauth's rotation grace) the reuse detector REVOKES THE WHOLE\n * TOKEN FAMILY — the user is force-logged-out everywhere. The proxy refreshes\n * the cookie BEFORE the RSC tree renders, so pbServer hydrates a still-valid\n * session and never burns the token itself.\n *\n * BUNDLE RULE — why this is a SEPARATE entry taking an EXPLICIT config\n * instead of reading the process-global one like pbServer does:\n *\n * Next compiles the proxy into its OWN bundle, so the generated palbe.gen.ts\n * imported by app/layout.tsx does not reach it. Importing palbe.gen.ts from\n * proxy.ts to fix that would drag `__configure` → buildRuntime →\n * messaging/wasm/loader.ts into that graph: measured on a real app, 2.4 MB\n * where this entry ships 168 KB. (Under the pre-16 `middleware` convention it\n * was worse than size — that bundle was Edge, and webpack failed the build\n * outright with \"Dynamic Code Evaluation ... not allowed in Edge Runtime\".\n * `proxy` runs on nodejs, which is not configurable, so the hard error is\n * gone and the weight is not.)\n *\n * So the proxy never touches the runtime: it takes the two published identity\n * values it actually needs and speaks the one auth wire directly. Nothing on\n * this module's import graph may reach ../runtime.js —\n * __tests__/next-proxy-bundle.test.ts locks that against the real build\n * output.\n *\n * 'next/server' is imported only lazily (the type-only NextRequest/\n * NextResponse imports erase at build time).\n *\n * ```ts\n * // proxy.ts\n * import { palbeProxy } from '@palbase/web/next/proxy';\n * import type { NextRequest } from 'next/server';\n *\n * export function proxy(request: NextRequest) {\n * return palbeProxy(request, {\n * url: 'https://abc12345m.palbase.studio',\n * apiKey: 'pb_abc12345m_c...',\n * });\n * }\n *\n * // `config` MUST be declared here, in this file: Next reads it off THIS\n * // file's AST (export const + literal initializer). A re-exported or\n * // imported config is invisible and silently degrades to a catch-all.\n * export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };\n * ```\n */\nimport type { NextRequest, NextResponse } from 'next/server';\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport { BackendError } from '../errors.js';\nimport {\n clearedSessionCookieNames,\n decodeSessionCookies,\n encodeSessionCookiesDecoded,\n SESSION_COOKIE_ATTRS,\n type StoredSession,\n} from './cookie-codec.js';\nimport { importNextServer } from './shared.js';\n\nconst DEFAULT_REFRESH_MARGIN_MS = 60_000;\n\n/**\n * The published identity palbeProxy needs — the same two values\n * `palbase web link` bakes into palbe.gen.ts. Both are publishable (the\n * anon key already ships in every browser bundle); a service_role key is\n * REJECTED, it has no business in a request-path bundle.\n *\n * A `PalbeConfig` is structurally assignable, so apps that already hold one\n * can pass it straight through.\n */\nexport interface PalbeProxyConfig {\n /** Environment URL, e.g. `https://abc12345m.palbase.studio`. */\n url: string;\n /** Publishable (anon) API key: `pb_<environmentRef>_c<random>`. */\n apiKey: string;\n}\n\nexport interface PalbeProxyOptions {\n /** Refresh when the access token expires within this window. Default 60s. */\n refreshMarginMs?: number;\n /**\n * Pre-built response to decorate (e.g. from your own chained proxy logic) instead of\n * building `NextResponse.next({ request })`. Pass-through cases return it\n * UNMODIFIED. Caveat: a pre-built response snapshotted the request headers\n * at ITS creation, so a rotated cookie reaches the browser (Set-Cookie)\n * but not the downstream RSC render of THIS request — prefer omitting this\n * and building your response after palbeProxy when possible.\n */\n response?: NextResponse;\n}\n\ntype RefreshOutcome =\n | { kind: 'rotated'; session: StoredSession }\n | { kind: 'terminal' } // 400/401/403 — refresh token dead, sign out\n | { kind: 'transient' }; // network / 5xx / malformed — keep the session\n\n/**\n * Per-process single-flight: concurrent proxy invocations holding the\n * SAME refresh token (parallel tabs / prefetches) share one refresh call —\n * a per-token thundering herd would race palauth rotation. Multi-pod\n * concurrency is covered by palauth's 30s rotation grace (concurrent\n * refreshes of one token all succeed); per-process dedup is all that's\n * needed here (Studio precedent — don't over-engineer).\n */\nconst inflightRefreshes = new Map<string, Promise<RefreshOutcome>>();\n\nfunction refreshSingleFlight(\n config: PalbeProxyConfig,\n refreshToken: string,\n): Promise<RefreshOutcome> {\n const existing = inflightRefreshes.get(refreshToken);\n if (existing) return existing;\n const pending = refresh(config, refreshToken).finally(() => {\n inflightRefreshes.delete(refreshToken);\n });\n inflightRefreshes.set(refreshToken, pending);\n return pending;\n}\n\n/** Direct palauth refresh call (no runtime: the proxy needs only this one wire). */\nasync function refresh(config: PalbeProxyConfig, refreshToken: string): Promise<RefreshOutcome> {\n try {\n const res = await fetch(`${config.url}/auth/token/refresh`, {\n method: 'POST',\n headers: { apikey: config.apiKey, 'content-type': 'application/json' },\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n if (res.status === 400 || res.status === 401 || res.status === 403) {\n return { kind: 'terminal' };\n }\n if (!res.ok) return { kind: 'transient' };\n const raw: unknown = await res.json();\n if (typeof raw === 'object' && raw !== null) {\n const obj = raw as Record<string, unknown>;\n if (\n typeof obj.access_token === 'string' &&\n typeof obj.refresh_token === 'string' &&\n typeof obj.expires_in === 'number'\n ) {\n return {\n kind: 'rotated',\n session: {\n accessToken: obj.access_token,\n refreshToken: obj.refresh_token,\n expiresAt: Date.now() + obj.expires_in * 1000,\n },\n };\n }\n }\n return { kind: 'transient' }; // malformed 200 — never destroy a session over it\n } catch {\n return { kind: 'transient' }; // network failure\n }\n}\n\n/**\n * Keep the session cookie fresh for the request. No session / still-fresh\n * session → untouched pass-through (no cache-control changes). Stale or\n * expired → single-flight refresh, then the rotated (or cleared, on a dead\n * token) cookies are written onto BOTH the request (for the downstream RSC\n * render via `NextResponse.next({ request })`) and the response (for the\n * browser), with `Cache-Control: private, no-store` on touched responses.\n *\n * Cookie writes go through Next's cookie APIs with the DECODED codec values:\n * Next percent-encodes on serialization, so the wire bytes come out exactly\n * as the shared codec's (see encodeSessionCookiesDecoded). NextRequest's\n * cookie store is NOT a CookieStoreLike (its set is the 2-arg/RequestCookie\n * form), hence the direct codec calls here instead of the pbServer adapter.\n */\nexport async function palbeProxy(\n request: NextRequest,\n config: PalbeProxyConfig,\n opts?: PalbeProxyOptions,\n): Promise<NextResponse> {\n // Fail LOUDLY on a bad key. A silently empty ref would name the cookie\n // `palbe-session-` — a name the browser SDK never writes — so every\n // request would look session-less and the proxy would degrade to a\n // no-op, which is exactly the token-family revocation this exists to stop.\n const ref = environmentRefFromPublishableApiKey(config.apiKey);\n if (ref === '') {\n throw new BackendError('validation', {\n code: 'validation_error',\n message:\n 'palbeProxy() needs the publishable API key from palbe.gen.ts (pb_<environmentRef>_c…). Re-run `palbase web link` and copy the generated values.',\n });\n }\n const { NextResponse } = await importNextServer('palbeProxy');\n const passThrough = (): NextResponse => opts?.response ?? NextResponse.next({ request });\n\n const session = decodeSessionCookies((name) => request.cookies.get(name)?.value, ref);\n if (!session) return passThrough();\n if (session.expiresAt - Date.now() > (opts?.refreshMarginMs ?? DEFAULT_REFRESH_MARGIN_MS)) {\n return passThrough();\n }\n\n const outcome = await refreshSingleFlight(config, session.refreshToken);\n if (outcome.kind === 'transient') return passThrough(); // keep the session over blips\n\n // Session-cookie names in the incoming jar — the browser's state mirror.\n const staleNames = clearedSessionCookieNames(ref, (name) => request.cookies.has(name));\n\n if (outcome.kind === 'terminal') {\n for (const name of staleNames) request.cookies.delete(name);\n const response = opts?.response ?? NextResponse.next({ request });\n for (const name of staleNames) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n }\n\n const write = encodeSessionCookiesDecoded(ref, outcome.session);\n for (const name of staleNames) request.cookies.delete(name);\n for (const { name, value } of write.set) request.cookies.set(name, value);\n for (const name of write.clear) request.cookies.delete(name);\n // Build AFTER the request-cookie mutation: NextResponse.next({ request })\n // snapshots the request headers for the downstream RSC render.\n const response = opts?.response ?? NextResponse.next({ request });\n const written = new Set(write.set.map((c) => c.name));\n for (const { name, value } of write.set) {\n response.cookies.set(name, value, SESSION_COOKIE_ATTRS);\n }\n // Browser-side cleanup: stale names not part of the new run (a shrunken /\n // grown chunk layout) + the codec's one-past-the-end overflow guard.\n for (const name of new Set([...write.clear, ...staleNames])) {\n if (!written.has(name)) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n}\n","/**\n * `pb_<environmentRef>_<scope><random>` → environmentRef ('' if malformed).\n * Utility for callers that only have a raw key. App-bound runtime storage and\n * palbe/next cookie naming use the explicit `PalbeConfig.environmentRef` emitted\n * by `palbe-gen`.\n *\n * The key is shape-validated (prefix `pb`, a 4-24 character lowercase ASCII\n * alphanumeric environmentRef, and a `<scope><20-base62-random>` secret segment\n * where scope ∈ {c,s}) before a ref is returned. A structurally-invalid key returns\n * '' so session storage stays\n * UNSCOPED rather than getting bound to a partially-parsed ref — a loose parse\n * (e.g. `pb_victim_cXX`) must not scope persisted sessions to `victim` and\n * hydrate a session belonging to a different, real key. The backend remains the\n * authority that rejects a bad key on the first request; this keeps the client's\n * local session-storage scoping honest. (Audit finding apikey-ref-mismatch.)\n */\nconst API_KEY_RE = /^pb_([a-z0-9]{4,24})_[cs][A-Za-z0-9]{20}$/;\nconst PUBLISHABLE_API_KEY_RE = /^pb_([a-z0-9]{4,24})_c[A-Za-z0-9]{20}$/;\n\nexport function environmentRefFromApiKey(apiKey: string): string {\n const m = API_KEY_RE.exec(apiKey);\n return m ? (m[1] ?? '') : '';\n}\n\n/** Internal app-runtime parser: browser config accepts publishable keys only. */\nexport function environmentRefFromPublishableApiKey(apiKey: string): string {\n const match = PUBLISHABLE_API_KEY_RE.exec(apiKey);\n return match ? (match[1] ?? '') : '';\n}\n","import { PalbaseError, type PalbaseResponse } from '@palbase/core';\n\nexport interface FieldError {\n field: string;\n message: string;\n}\n\nexport type BackendErrorKind =\n | 'notConfigured'\n | 'validation'\n | 'unauthorized'\n | 'rateLimited'\n | 'server'\n | 'network'\n | 'decode';\n\ninterface BackendErrorParams {\n code: string;\n message: string;\n status?: number;\n requestId?: string;\n fields?: FieldError[];\n retryAfter?: number;\n data?: unknown;\n}\n\nexport class BackendError extends Error {\n readonly kind: BackendErrorKind;\n readonly code: string;\n readonly status: number;\n readonly requestId?: string;\n readonly fields?: FieldError[];\n readonly retryAfter?: number;\n readonly data?: unknown;\n\n constructor(kind: BackendErrorKind, params: BackendErrorParams) {\n super(params.message);\n this.name = 'BackendError';\n this.kind = kind;\n this.code = params.code;\n this.status = params.status ?? 0;\n this.requestId = params.requestId;\n this.fields = params.fields;\n this.retryAfter = params.retryAfter;\n this.data = params.data;\n }\n\n static notConfigured(): BackendError {\n return new BackendError('notConfigured', {\n code: 'not_configured',\n message:\n \"Palbe is not configured. Run 'palbase web link' in your project and make sure palbe.gen.ts is imported once at app startup.\",\n });\n }\n}\n\nfunction isFieldErrorArray(value: unknown): value is FieldError[] {\n return (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every(\n (v) =>\n typeof v === 'object' &&\n v !== null &&\n typeof (v as Record<string, unknown>).field === 'string' &&\n typeof (v as Record<string, unknown>).message === 'string',\n )\n );\n}\n\nfunction pickField(value: unknown, key: string): unknown {\n if (typeof value === 'object' && value !== null) {\n return (value as Record<string, unknown>)[key];\n }\n return undefined;\n}\n\nfunction pickNumber(value: unknown, key: string): number | undefined {\n const n = pickField(value, key);\n return typeof n === 'number' ? n : undefined;\n}\n\nfunction pickString(value: unknown, key: string): string | undefined {\n const s = pickField(value, key);\n return typeof s === 'string' ? s : undefined;\n}\n\nexport function fromPalbaseError(err: PalbaseError): BackendError {\n // HttpClient stores the WHOLE wire envelope as err.details:\n // { error, error_description, status, request_id, retry_after?, details?, data? }\n const base: BackendErrorParams = {\n code: err.code,\n message: err.message,\n status: err.status,\n requestId: pickString(err.details, 'request_id'),\n data: pickField(err.details, 'data'),\n };\n if (err.code === 'network_error') return new BackendError('network', base);\n if (err.status === 401) return new BackendError('unauthorized', base);\n if (err.status === 429)\n return new BackendError('rateLimited', {\n ...base,\n retryAfter: pickNumber(err.details, 'retry_after'),\n });\n // Field-error array lives at the envelope's nested `details` key.\n const nested = pickField(err.details, 'details');\n if (err.status === 400 && isFieldErrorArray(nested))\n return new BackendError('validation', { ...base, fields: nested });\n // Fallthrough also covers status-0 non-network errors (e.g. auth client's\n // synthetic 'no_refresh_token' with status 0) — those intentionally map to 'server'.\n return new BackendError('server', base);\n}\n\n/** Decode a raw wire body (used by paths that bypass HttpClient, e.g. upload). */\nexport function fromEnvelope(status: number, body: unknown): BackendError {\n const code = pickString(body, 'error') ?? 'http_error';\n const message = pickString(body, 'error_description') ?? `HTTP ${status}`;\n const requestId = pickString(body, 'request_id');\n const details = pickField(body, 'details');\n const params: BackendErrorParams = {\n code,\n message,\n status,\n requestId,\n data: pickField(body, 'data'),\n };\n if (status === 401) return new BackendError('unauthorized', params);\n if (status === 429)\n return new BackendError('rateLimited', {\n ...params,\n // Real 429 wire body has TOP-LEVEL retry_after; nested details is a fallback.\n retryAfter: pickNumber(body, 'retry_after') ?? pickNumber(details, 'retry_after'),\n });\n if (status === 400 && isFieldErrorArray(details))\n return new BackendError('validation', { ...params, fields: details });\n return new BackendError('server', params);\n}\n\n/**\n * Type guard for BackendError that survives module-identity splits.\n * This package ships dual ESM + CJS builds; if both end up loaded (or the\n * package is installed twice), two distinct BackendError classes coexist and\n * `instanceof` fails for errors thrown by \"the other\" copy. Falls back to a\n * structural check on `name` + `kind`.\n */\nexport function isBackendError(e: unknown): e is BackendError {\n return (\n e instanceof BackendError ||\n (typeof e === 'object' &&\n e !== null &&\n (e as Record<string, unknown>).name === 'BackendError' &&\n typeof (e as Record<string, unknown>).kind === 'string')\n );\n}\n\n/**\n * Structural fallback after instanceof for PalbaseError — same dual ESM+CJS\n * identity-split rationale as isBackendError above: two loaded PalbaseError\n * classes break `instanceof` across copies; the name check bridges that.\n * Returns the error as PalbaseError, or null when it isn't one.\n */\nexport function asPalbaseError(e: unknown): PalbaseError | null {\n if (e instanceof PalbaseError) return e;\n if (e instanceof Error && e.name === 'PalbaseError') return e as PalbaseError;\n return null;\n}\n\n/** Convert an internal {data,error} envelope into data-or-throw. */\nexport function unwrap<T>(res: PalbaseResponse<T>): T {\n if (res.error) throw fromPalbaseError(res.error);\n return res.data as T;\n}\n","/**\n * Session-cookie codec shared by EVERY palbe/next surface (browser storage\n * adapter, pbServer cookie adapter, middleware, auth callback). Pure\n * functions — no 'next' imports, no palbe runtime imports — so both sides\n * provably read and write the SAME bytes.\n *\n * Format (P3 design contract):\n * - name: `palbe-session-<environmentRef>`; overflow chunk names append `.0`,\n * `.1`, … (cookie names otherwise avoid dots — tooling friendliness).\n * - value: `encodeURIComponent(JSON.stringify({ a, r, e }))` with a=access\n * token, r=refresh token, e=expiry (ms epoch).\n * - one cookie when the encoded value fits MAX_COOKIE_VALUE chars, else\n * chunks ONLY (the base name must not coexist with chunks — callers clear\n * stale names via clearedSessionCookieNames before writing, then honor the\n * encode result's `clear` overflow guard after writing).\n */\n\nexport { environmentRefFromApiKey } from '../api-key.js';\n\nexport interface StoredSession {\n accessToken: string;\n refreshToken: string;\n /** ms epoch */\n expiresAt: number;\n}\n\n/** Browsers cap a cookie at ~4096 bytes incl. name + attributes; 3500 leaves headroom. */\nconst MAX_COOKIE_VALUE = 3500;\n\n/**\n * The P3 cookie contract attributes, shared by every server-side writer\n * (pbServer adapter, middleware, callback); the browser writer mirrors them\n * in document.cookie string form (next/client.ts). NOT HttpOnly by design —\n * the browser SDK must read/write the session (Supabase-paradigm tradeoff);\n * Max-Age = 30d = the refresh-token TTL.\n */\nexport const SESSION_COOKIE_ATTRS = {\n path: '/',\n sameSite: 'lax',\n secure: true,\n maxAge: 2_592_000,\n} as const;\n\nexport function sessionCookieName(environmentRef: string): string {\n return `palbe-session-${environmentRef}`;\n}\n\nexport interface SessionCookieWrite {\n /** Cookies to SET, in order. */\n set: Array<{ name: string; value: string }>;\n /**\n * Names to DELETE alongside the set: the one-past-the-end OVERFLOW GUARD\n * (base write → `.0`; chunks `.0..n` → `.(n+1)`). Decode stops at the\n * first missing chunk, so deleting exactly one-past-the-end guarantees a\n * stale orphan chunk (left behind a gap, where presence-probing cleanup\n * cannot see it) can never concatenate into the freshly written run.\n * Writers MUST honor both lists.\n */\n clear: string[];\n}\n\nexport function encodeSessionCookies(\n environmentRef: string,\n session: StoredSession,\n): SessionCookieWrite {\n const name = sessionCookieName(environmentRef);\n const value = encodeURIComponent(\n JSON.stringify({ a: session.accessToken, r: session.refreshToken, e: session.expiresAt }),\n );\n if (value.length <= MAX_COOKIE_VALUE) {\n return { set: [{ name, value }], clear: [`${name}.0`] };\n }\n const chunks: Array<{ name: string; value: string }> = [];\n // Chunk WITHOUT splitting a %XX escape (every '%' in the encoded value\n // starts one — encodeURIComponent never emits a bare '%'). Next's cookie\n // layer percent-DECODES values on read and silently DROPS a cookie whose\n // value is an invalid escape sequence, so a split escape would lose that\n // chunk server-side; independent decodability also lets\n // encodeSessionCookiesDecoded mirror this run byte-exactly.\n for (let start = 0; start < value.length; ) {\n let end = Math.min(start + MAX_COOKIE_VALUE, value.length);\n if (end < value.length) {\n if (value[end - 1] === '%') end -= 1;\n else if (value[end - 2] === '%') end -= 2;\n }\n chunks.push({ name: `${name}.${chunks.length}`, value: value.slice(start, end) });\n start = end;\n }\n return { set: chunks, clear: [`${name}.${chunks.length}`] };\n}\n\n/**\n * encodeSessionCookies for percent-ENCODING cookie stores. Next's request\n * and response cookie APIs apply encodeURIComponent to values when\n * serializing (and decodeURIComponent on read) — handing them the codec's\n * already-encoded values would DOUBLE-encode the wire bytes and break the\n * browser-side reader. This variant returns the same cookie run with\n * DECODED values: `encodeURIComponent(value)` reproduces the\n * encodeSessionCookies bytes exactly (chunk boundaries never split an\n * escape), so an encoding store writes byte-identical cookies to a raw\n * writer like document.cookie.\n */\nexport function encodeSessionCookiesDecoded(\n environmentRef: string,\n session: StoredSession,\n): SessionCookieWrite {\n const { set, clear } = encodeSessionCookies(environmentRef, session);\n return {\n set: set.map(({ name, value }) => ({ name, value: decodeURIComponent(value) })),\n clear,\n };\n}\n\n/**\n * Read the session back from a cookie getter: the base name wins; otherwise\n * `.0`, `.1`, … are concatenated until the first missing chunk. Tolerant:\n * malformed URI escapes / JSON / shape → null (treated as \"no session\").\n *\n * Accepts BOTH value forms. Next's cookie stores percent-DECODE values once\n * on read, so server-side callers (pbServer adapter, middleware) hand us the\n * already-decoded JSON; raw readers (document.cookie) hand us the encoded\n * form. The raw (already-decoded) parse runs FIRST because it is\n * unambiguous — an encoded value can never raw-parse as JSON\n * (encodeURIComponent escapes `{` itself, so it starts with `%7B`) — whereas\n * decode-first would silently corrupt a literal `%XX` run inside a token\n * (decodeURIComponent('%41') → 'A', no throw) or throw on an invalid escape.\n */\nexport function decodeSessionCookies(\n get: (name: string) => string | undefined,\n environmentRef: string,\n): StoredSession | null {\n const base = sessionCookieName(environmentRef);\n let encoded = get(base);\n if (encoded === undefined) {\n let joined = '';\n for (let i = 0; ; i++) {\n const part = get(`${base}.${i}`);\n if (part === undefined) break;\n joined += part;\n }\n if (joined === '') return null;\n encoded = joined;\n }\n const raw = parseStoredSession(encoded);\n if (raw !== null) return raw;\n try {\n return parseStoredSession(decodeURIComponent(encoded));\n } catch {\n return null;\n }\n}\n\n/** JSON-parse + shape-check one candidate value; malformed → null. */\nfunction parseStoredSession(json: string): StoredSession | null {\n try {\n const parsed: unknown = JSON.parse(json);\n if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {\n const obj = parsed as Record<string, unknown>;\n if (typeof obj.a === 'string' && typeof obj.r === 'string' && typeof obj.e === 'number') {\n return { accessToken: obj.a, refreshToken: obj.r, expiresAt: obj.e };\n }\n }\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Names a writer must delete before/while writing: the base name (when\n * present) plus every existing `.N` chunk, probed in order until the first\n * missing one (writes are always a contiguous `.0..n` run, so a gap means\n * the end of palbe-owned chunks).\n */\nexport function clearedSessionCookieNames(\n environmentRef: string,\n present: (name: string) => boolean,\n): string[] {\n const base = sessionCookieName(environmentRef);\n const names: string[] = [];\n if (present(base)) names.push(base);\n for (let i = 0; ; i++) {\n const chunk = `${base}.${i}`;\n if (!present(chunk)) break;\n names.push(chunk);\n }\n return names;\n}\n\n/**\n * Parse the canonical chunk index out of `palbe-session-<ref>.N`; non-chunk\n * names (the bare base, a foreign ref, junk suffixes like `.x` / `.01` /\n * `.-1`) → null. The index is a non-negative integer with NO leading zeros\n * (the encoder emits `.0`, `.1`, … verbatim), so `String(n) === suffix`\n * round-trips it exactly.\n */\nfunction chunkIndex(base: string, name: string): number | null {\n if (!name.startsWith(`${base}.`)) return null;\n const suffix = name.slice(base.length + 1);\n if (!/^\\d+$/.test(suffix)) return null;\n const idx = Number.parseInt(suffix, 10);\n return String(idx) === suffix ? idx : null;\n}\n\n/**\n * getAll-powered orphan sweep — the deletion list a writer with the FULL\n * cookie jar should clear, catching crash-orphans that presence-probing\n * (clearedSessionCookieNames, which stops at the first gap) CANNOT see.\n *\n * Given every cookie name in the jar, returns the `palbe-session-<ref>`-family\n * names that are NOT part of a single valid session:\n * - A valid session is EITHER the bare base alone, OR the contiguous chunk\n * run `.0, .1, …, n` (starting at `.0`, no gaps).\n * - Any `.N` beyond the first gap from `.0` is an orphan (incl. ALL chunks\n * when `.0` is absent — no run exists).\n * - The base coexisting with ANY chunk is inconsistent (the contract forbids\n * it; decode lets the base win and silently strands the chunks): the base\n * is kept, the chunks are swept (a fresh write replaces the base in place).\n *\n * Only canonical `.N` names count; foreign refs and malformed suffixes are\n * ignored. Output order: base (if inconsistent) first, then chunks ascending.\n */\nexport function orphanSessionCookieNames(environmentRef: string, allNames: string[]): string[] {\n const base = sessionCookieName(environmentRef);\n const hasBase = allNames.includes(base);\n const indices = new Set<number>();\n for (const name of allNames) {\n const idx = chunkIndex(base, name);\n if (idx !== null) indices.add(idx);\n }\n if (indices.size === 0) return []; // unchunked (base alone) or no palbe cookies\n\n // Length of the contiguous run from .0 (0 when .0 is missing).\n let runLen = 0;\n while (indices.has(runLen)) runLen++;\n\n const orphans: string[] = [];\n // Base coexisting with chunks: base wins on decode → chunks are stale.\n // Sweep the base too only when there are NO chunks behind the run to keep\n // (the contract forbids the mix; clearing chunks alone resolves it, but if\n // the run itself is invalid—no .0—the base is the real session and stays).\n // Decision: when base coexists with ANY chunk, the chunks are orphans; the\n // base is the live session and is kept — UNLESS the chunk set forms a valid\n // run AND there is also a gap-orphan, in which case both base+gap-orphan are\n // inconsistent leftovers. Concretely: keep it simple and SAFE — the base is\n // only swept when it sits alongside a partial/gapped chunk set (a crash mid\n // base→chunk migration), never when chunks form a clean run the base shadows.\n const everyChunkContiguous = indices.size === runLen && runLen > 0;\n if (hasBase && !everyChunkContiguous) orphans.push(base);\n\n for (const idx of [...indices].sort((a, b) => a - b)) {\n if (idx >= runLen) orphans.push(`${base}.${idx}`);\n }\n // Base + clean contiguous run: base wins on decode, chunks are stranded.\n if (hasBase && everyChunkContiguous) {\n for (let i = 0; i < runLen; i++) orphans.push(`${base}.${i}`);\n }\n return orphans;\n}\n","/**\n * The lazy 'next/server' import, shared by every palbe/next surface that\n * builds a NextResponse (palbeMiddleware, handleAuthCallback).\n *\n * MODULE-GRAPH RULE: this module must stay reachable from the middleware\n * entry, which Next compiles into its own Edge bundle — so it may import\n * NOTHING that leads to ../runtime.js. Keep it at errors.js and below.\n * (Global-config lookup, which does reach the runtime, lives in\n * ./global-config.ts.)\n */\nimport { BackendError } from '../errors.js';\n\nlet nextServerModule: Promise<typeof import('next/server')> | undefined;\n\n/**\n * Lazy 'next/server' import (MODULE-GRAPH RULE: never top-level — importing\n * palbe/next must work without next installed) with a guided failure.\n * The module promise is memoized: bundler dynamic-import machinery can take\n * macrotask-scale hops PER CALL, so without the cache two near-simultaneous\n * middleware invocations could straddle a refresh — every call after the\n * first now resolves in pure microtasks.\n */\nexport async function importNextServer(caller: string): Promise<typeof import('next/server')> {\n nextServerModule ??= import('next/server');\n try {\n return await nextServerModule;\n } catch (cause) {\n nextServerModule = undefined; // never poison later calls with a cached rejection\n const detail = cause instanceof Error && cause.message ? ` (${cause.message})` : '';\n throw new BackendError('validation', {\n code: 'next_required',\n message: `${caller}() requires Next.js — 'next/server' could not be resolved${detail}.`,\n });\n }\n}\n"],"mappings":"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","names":["response"]}
|
|
@@ -1,47 +1,48 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* '@palbase/web/next/
|
|
5
|
-
* Next
|
|
4
|
+
* '@palbase/web/next/proxy' — proactive cookie-session refresh in the Next.js
|
|
5
|
+
* proxy (the file convention that replaced `middleware` in Next 16; this
|
|
6
|
+
* package's peer range is `next >= 16`).
|
|
6
7
|
*
|
|
7
8
|
* REQUIRED — not optional — for session-bearing RSC apps. Server Components
|
|
8
9
|
* cannot write cookies, so pbServer's reactive refresh cannot PERSIST a
|
|
9
10
|
* rotated session there: every RSC render would re-refresh from the SAME
|
|
10
11
|
* cookie refresh token, and once two of those land more than 30s apart
|
|
11
12
|
* (outside palauth's rotation grace) the reuse detector REVOKES THE WHOLE
|
|
12
|
-
* TOKEN FAMILY — the user is force-logged-out everywhere.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* TOKEN FAMILY — the user is force-logged-out everywhere. The proxy refreshes
|
|
14
|
+
* the cookie BEFORE the RSC tree renders, so pbServer hydrates a still-valid
|
|
15
|
+
* session and never burns the token itself.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
+
* BUNDLE RULE — why this is a SEPARATE entry taking an EXPLICIT config
|
|
17
18
|
* instead of reading the process-global one like pbServer does:
|
|
18
19
|
*
|
|
19
|
-
* Next compiles
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
20
|
+
* Next compiles the proxy into its OWN bundle, so the generated palbe.gen.ts
|
|
21
|
+
* imported by app/layout.tsx does not reach it. Importing palbe.gen.ts from
|
|
22
|
+
* proxy.ts to fix that would drag `__configure` → buildRuntime →
|
|
23
|
+
* messaging/wasm/loader.ts into that graph: measured on a real app, 2.4 MB
|
|
24
|
+
* where this entry ships 168 KB. (Under the pre-16 `middleware` convention it
|
|
25
|
+
* was worse than size — that bundle was Edge, and webpack failed the build
|
|
26
|
+
* outright with "Dynamic Code Evaluation ... not allowed in Edge Runtime".
|
|
27
|
+
* `proxy` runs on nodejs, which is not configurable, so the hard error is
|
|
28
|
+
* gone and the weight is not.)
|
|
28
29
|
*
|
|
29
|
-
* So the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* __tests__/next-
|
|
33
|
-
*
|
|
30
|
+
* So the proxy never touches the runtime: it takes the two published identity
|
|
31
|
+
* values it actually needs and speaks the one auth wire directly. Nothing on
|
|
32
|
+
* this module's import graph may reach ../runtime.js —
|
|
33
|
+
* __tests__/next-proxy-bundle.test.ts locks that against the real build
|
|
34
|
+
* output.
|
|
34
35
|
*
|
|
35
36
|
* 'next/server' is imported only lazily (the type-only NextRequest/
|
|
36
37
|
* NextResponse imports erase at build time).
|
|
37
38
|
*
|
|
38
39
|
* ```ts
|
|
39
|
-
* //
|
|
40
|
-
* import {
|
|
40
|
+
* // proxy.ts
|
|
41
|
+
* import { palbeProxy } from '@palbase/web/next/proxy';
|
|
41
42
|
* import type { NextRequest } from 'next/server';
|
|
42
43
|
*
|
|
43
|
-
* export function
|
|
44
|
-
* return
|
|
44
|
+
* export function proxy(request: NextRequest) {
|
|
45
|
+
* return palbeProxy(request, {
|
|
45
46
|
* url: 'https://abc12345m.palbase.studio',
|
|
46
47
|
* apiKey: 'pb_abc12345m_c...',
|
|
47
48
|
* });
|
|
@@ -55,7 +56,7 @@ import { NextResponse, NextRequest } from 'next/server';
|
|
|
55
56
|
*/
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
* The published identity
|
|
59
|
+
* The published identity palbeProxy needs — the same two values
|
|
59
60
|
* `palbase web link` bakes into palbe.gen.ts. Both are publishable (the
|
|
60
61
|
* anon key already ships in every browser bundle); a service_role key is
|
|
61
62
|
* REJECTED, it has no business in a request-path bundle.
|
|
@@ -63,22 +64,22 @@ import { NextResponse, NextRequest } from 'next/server';
|
|
|
63
64
|
* A `PalbeConfig` is structurally assignable, so apps that already hold one
|
|
64
65
|
* can pass it straight through.
|
|
65
66
|
*/
|
|
66
|
-
interface
|
|
67
|
+
interface PalbeProxyConfig {
|
|
67
68
|
/** Environment URL, e.g. `https://abc12345m.palbase.studio`. */
|
|
68
69
|
url: string;
|
|
69
70
|
/** Publishable (anon) API key: `pb_<environmentRef>_c<random>`. */
|
|
70
71
|
apiKey: string;
|
|
71
72
|
}
|
|
72
|
-
interface
|
|
73
|
+
interface PalbeProxyOptions {
|
|
73
74
|
/** Refresh when the access token expires within this window. Default 60s. */
|
|
74
75
|
refreshMarginMs?: number;
|
|
75
76
|
/**
|
|
76
|
-
* Pre-built response to decorate (e.g. from chained
|
|
77
|
+
* Pre-built response to decorate (e.g. from your own chained proxy logic) instead of
|
|
77
78
|
* building `NextResponse.next({ request })`. Pass-through cases return it
|
|
78
79
|
* UNMODIFIED. Caveat: a pre-built response snapshotted the request headers
|
|
79
80
|
* at ITS creation, so a rotated cookie reaches the browser (Set-Cookie)
|
|
80
81
|
* but not the downstream RSC render of THIS request — prefer omitting this
|
|
81
|
-
* and building your response after
|
|
82
|
+
* and building your response after palbeProxy when possible.
|
|
82
83
|
*/
|
|
83
84
|
response?: NextResponse;
|
|
84
85
|
}
|
|
@@ -96,6 +97,6 @@ interface PalbeMiddlewareOptions {
|
|
|
96
97
|
* cookie store is NOT a CookieStoreLike (its set is the 2-arg/RequestCookie
|
|
97
98
|
* form), hence the direct codec calls here instead of the pbServer adapter.
|
|
98
99
|
*/
|
|
99
|
-
declare function
|
|
100
|
+
declare function palbeProxy(request: NextRequest, config: PalbeProxyConfig, opts?: PalbeProxyOptions): Promise<NextResponse>;
|
|
100
101
|
|
|
101
|
-
export { type
|
|
102
|
+
export { type PalbeProxyConfig, type PalbeProxyOptions, palbeProxy };
|
|
@@ -1,47 +1,48 @@
|
|
|
1
1
|
import { NextResponse, NextRequest } from 'next/server';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* '@palbase/web/next/
|
|
5
|
-
* Next
|
|
4
|
+
* '@palbase/web/next/proxy' — proactive cookie-session refresh in the Next.js
|
|
5
|
+
* proxy (the file convention that replaced `middleware` in Next 16; this
|
|
6
|
+
* package's peer range is `next >= 16`).
|
|
6
7
|
*
|
|
7
8
|
* REQUIRED — not optional — for session-bearing RSC apps. Server Components
|
|
8
9
|
* cannot write cookies, so pbServer's reactive refresh cannot PERSIST a
|
|
9
10
|
* rotated session there: every RSC render would re-refresh from the SAME
|
|
10
11
|
* cookie refresh token, and once two of those land more than 30s apart
|
|
11
12
|
* (outside palauth's rotation grace) the reuse detector REVOKES THE WHOLE
|
|
12
|
-
* TOKEN FAMILY — the user is force-logged-out everywhere.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
13
|
+
* TOKEN FAMILY — the user is force-logged-out everywhere. The proxy refreshes
|
|
14
|
+
* the cookie BEFORE the RSC tree renders, so pbServer hydrates a still-valid
|
|
15
|
+
* session and never burns the token itself.
|
|
15
16
|
*
|
|
16
|
-
*
|
|
17
|
+
* BUNDLE RULE — why this is a SEPARATE entry taking an EXPLICIT config
|
|
17
18
|
* instead of reading the process-global one like pbServer does:
|
|
18
19
|
*
|
|
19
|
-
* Next compiles
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
20
|
+
* Next compiles the proxy into its OWN bundle, so the generated palbe.gen.ts
|
|
21
|
+
* imported by app/layout.tsx does not reach it. Importing palbe.gen.ts from
|
|
22
|
+
* proxy.ts to fix that would drag `__configure` → buildRuntime →
|
|
23
|
+
* messaging/wasm/loader.ts into that graph: measured on a real app, 2.4 MB
|
|
24
|
+
* where this entry ships 168 KB. (Under the pre-16 `middleware` convention it
|
|
25
|
+
* was worse than size — that bundle was Edge, and webpack failed the build
|
|
26
|
+
* outright with "Dynamic Code Evaluation ... not allowed in Edge Runtime".
|
|
27
|
+
* `proxy` runs on nodejs, which is not configurable, so the hard error is
|
|
28
|
+
* gone and the weight is not.)
|
|
28
29
|
*
|
|
29
|
-
* So the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* __tests__/next-
|
|
33
|
-
*
|
|
30
|
+
* So the proxy never touches the runtime: it takes the two published identity
|
|
31
|
+
* values it actually needs and speaks the one auth wire directly. Nothing on
|
|
32
|
+
* this module's import graph may reach ../runtime.js —
|
|
33
|
+
* __tests__/next-proxy-bundle.test.ts locks that against the real build
|
|
34
|
+
* output.
|
|
34
35
|
*
|
|
35
36
|
* 'next/server' is imported only lazily (the type-only NextRequest/
|
|
36
37
|
* NextResponse imports erase at build time).
|
|
37
38
|
*
|
|
38
39
|
* ```ts
|
|
39
|
-
* //
|
|
40
|
-
* import {
|
|
40
|
+
* // proxy.ts
|
|
41
|
+
* import { palbeProxy } from '@palbase/web/next/proxy';
|
|
41
42
|
* import type { NextRequest } from 'next/server';
|
|
42
43
|
*
|
|
43
|
-
* export function
|
|
44
|
-
* return
|
|
44
|
+
* export function proxy(request: NextRequest) {
|
|
45
|
+
* return palbeProxy(request, {
|
|
45
46
|
* url: 'https://abc12345m.palbase.studio',
|
|
46
47
|
* apiKey: 'pb_abc12345m_c...',
|
|
47
48
|
* });
|
|
@@ -55,7 +56,7 @@ import { NextResponse, NextRequest } from 'next/server';
|
|
|
55
56
|
*/
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
|
-
* The published identity
|
|
59
|
+
* The published identity palbeProxy needs — the same two values
|
|
59
60
|
* `palbase web link` bakes into palbe.gen.ts. Both are publishable (the
|
|
60
61
|
* anon key already ships in every browser bundle); a service_role key is
|
|
61
62
|
* REJECTED, it has no business in a request-path bundle.
|
|
@@ -63,22 +64,22 @@ import { NextResponse, NextRequest } from 'next/server';
|
|
|
63
64
|
* A `PalbeConfig` is structurally assignable, so apps that already hold one
|
|
64
65
|
* can pass it straight through.
|
|
65
66
|
*/
|
|
66
|
-
interface
|
|
67
|
+
interface PalbeProxyConfig {
|
|
67
68
|
/** Environment URL, e.g. `https://abc12345m.palbase.studio`. */
|
|
68
69
|
url: string;
|
|
69
70
|
/** Publishable (anon) API key: `pb_<environmentRef>_c<random>`. */
|
|
70
71
|
apiKey: string;
|
|
71
72
|
}
|
|
72
|
-
interface
|
|
73
|
+
interface PalbeProxyOptions {
|
|
73
74
|
/** Refresh when the access token expires within this window. Default 60s. */
|
|
74
75
|
refreshMarginMs?: number;
|
|
75
76
|
/**
|
|
76
|
-
* Pre-built response to decorate (e.g. from chained
|
|
77
|
+
* Pre-built response to decorate (e.g. from your own chained proxy logic) instead of
|
|
77
78
|
* building `NextResponse.next({ request })`. Pass-through cases return it
|
|
78
79
|
* UNMODIFIED. Caveat: a pre-built response snapshotted the request headers
|
|
79
80
|
* at ITS creation, so a rotated cookie reaches the browser (Set-Cookie)
|
|
80
81
|
* but not the downstream RSC render of THIS request — prefer omitting this
|
|
81
|
-
* and building your response after
|
|
82
|
+
* and building your response after palbeProxy when possible.
|
|
82
83
|
*/
|
|
83
84
|
response?: NextResponse;
|
|
84
85
|
}
|
|
@@ -96,6 +97,6 @@ interface PalbeMiddlewareOptions {
|
|
|
96
97
|
* cookie store is NOT a CookieStoreLike (its set is the 2-arg/RequestCookie
|
|
97
98
|
* form), hence the direct codec calls here instead of the pbServer adapter.
|
|
98
99
|
*/
|
|
99
|
-
declare function
|
|
100
|
+
declare function palbeProxy(request: NextRequest, config: PalbeProxyConfig, opts?: PalbeProxyOptions): Promise<NextResponse>;
|
|
100
101
|
|
|
101
|
-
export { type
|
|
102
|
+
export { type PalbeProxyConfig, type PalbeProxyOptions, palbeProxy };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
importNextServer
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-ZC6Q3GPX.js";
|
|
4
4
|
import {
|
|
5
5
|
SESSION_COOKIE_ATTRS,
|
|
6
6
|
clearedSessionCookieNames,
|
|
@@ -9,13 +9,13 @@ import {
|
|
|
9
9
|
} from "../chunk-6VGKMNXO.js";
|
|
10
10
|
import {
|
|
11
11
|
BackendError
|
|
12
|
-
} from "../chunk-
|
|
12
|
+
} from "../chunk-ACCJV6FV.js";
|
|
13
13
|
import {
|
|
14
14
|
environmentRefFromPublishableApiKey
|
|
15
15
|
} from "../chunk-CFDU23TB.js";
|
|
16
16
|
import "../chunk-PZ5AY32C.js";
|
|
17
17
|
|
|
18
|
-
// src/next/
|
|
18
|
+
// src/next/proxy.ts
|
|
19
19
|
var DEFAULT_REFRESH_MARGIN_MS = 6e4;
|
|
20
20
|
var inflightRefreshes = /* @__PURE__ */ new Map();
|
|
21
21
|
function refreshSingleFlight(config, refreshToken) {
|
|
@@ -57,15 +57,15 @@ async function refresh(config, refreshToken) {
|
|
|
57
57
|
return { kind: "transient" };
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
async function
|
|
60
|
+
async function palbeProxy(request, config, opts) {
|
|
61
61
|
const ref = environmentRefFromPublishableApiKey(config.apiKey);
|
|
62
62
|
if (ref === "") {
|
|
63
63
|
throw new BackendError("validation", {
|
|
64
64
|
code: "validation_error",
|
|
65
|
-
message: "
|
|
65
|
+
message: "palbeProxy() needs the publishable API key from palbe.gen.ts (pb_<environmentRef>_c\u2026). Re-run `palbase web link` and copy the generated values."
|
|
66
66
|
});
|
|
67
67
|
}
|
|
68
|
-
const { NextResponse } = await importNextServer("
|
|
68
|
+
const { NextResponse } = await importNextServer("palbeProxy");
|
|
69
69
|
const passThrough = () => opts?.response ?? NextResponse.next({ request });
|
|
70
70
|
const session = decodeSessionCookies((name) => request.cookies.get(name)?.value, ref);
|
|
71
71
|
if (!session) return passThrough();
|
|
@@ -102,6 +102,6 @@ async function palbeMiddleware(request, config, opts) {
|
|
|
102
102
|
return response;
|
|
103
103
|
}
|
|
104
104
|
export {
|
|
105
|
-
|
|
105
|
+
palbeProxy
|
|
106
106
|
};
|
|
107
|
-
//# sourceMappingURL=
|
|
107
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/next/proxy.ts"],"sourcesContent":["/**\n * '@palbase/web/next/proxy' — proactive cookie-session refresh in the Next.js\n * proxy (the file convention that replaced `middleware` in Next 16; this\n * package's peer range is `next >= 16`).\n *\n * REQUIRED — not optional — for session-bearing RSC apps. Server Components\n * cannot write cookies, so pbServer's reactive refresh cannot PERSIST a\n * rotated session there: every RSC render would re-refresh from the SAME\n * cookie refresh token, and once two of those land more than 30s apart\n * (outside palauth's rotation grace) the reuse detector REVOKES THE WHOLE\n * TOKEN FAMILY — the user is force-logged-out everywhere. The proxy refreshes\n * the cookie BEFORE the RSC tree renders, so pbServer hydrates a still-valid\n * session and never burns the token itself.\n *\n * BUNDLE RULE — why this is a SEPARATE entry taking an EXPLICIT config\n * instead of reading the process-global one like pbServer does:\n *\n * Next compiles the proxy into its OWN bundle, so the generated palbe.gen.ts\n * imported by app/layout.tsx does not reach it. Importing palbe.gen.ts from\n * proxy.ts to fix that would drag `__configure` → buildRuntime →\n * messaging/wasm/loader.ts into that graph: measured on a real app, 2.4 MB\n * where this entry ships 168 KB. (Under the pre-16 `middleware` convention it\n * was worse than size — that bundle was Edge, and webpack failed the build\n * outright with \"Dynamic Code Evaluation ... not allowed in Edge Runtime\".\n * `proxy` runs on nodejs, which is not configurable, so the hard error is\n * gone and the weight is not.)\n *\n * So the proxy never touches the runtime: it takes the two published identity\n * values it actually needs and speaks the one auth wire directly. Nothing on\n * this module's import graph may reach ../runtime.js —\n * __tests__/next-proxy-bundle.test.ts locks that against the real build\n * output.\n *\n * 'next/server' is imported only lazily (the type-only NextRequest/\n * NextResponse imports erase at build time).\n *\n * ```ts\n * // proxy.ts\n * import { palbeProxy } from '@palbase/web/next/proxy';\n * import type { NextRequest } from 'next/server';\n *\n * export function proxy(request: NextRequest) {\n * return palbeProxy(request, {\n * url: 'https://abc12345m.palbase.studio',\n * apiKey: 'pb_abc12345m_c...',\n * });\n * }\n *\n * // `config` MUST be declared here, in this file: Next reads it off THIS\n * // file's AST (export const + literal initializer). A re-exported or\n * // imported config is invisible and silently degrades to a catch-all.\n * export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] };\n * ```\n */\nimport type { NextRequest, NextResponse } from 'next/server';\nimport { environmentRefFromPublishableApiKey } from '../api-key.js';\nimport { BackendError } from '../errors.js';\nimport {\n clearedSessionCookieNames,\n decodeSessionCookies,\n encodeSessionCookiesDecoded,\n SESSION_COOKIE_ATTRS,\n type StoredSession,\n} from './cookie-codec.js';\nimport { importNextServer } from './shared.js';\n\nconst DEFAULT_REFRESH_MARGIN_MS = 60_000;\n\n/**\n * The published identity palbeProxy needs — the same two values\n * `palbase web link` bakes into palbe.gen.ts. Both are publishable (the\n * anon key already ships in every browser bundle); a service_role key is\n * REJECTED, it has no business in a request-path bundle.\n *\n * A `PalbeConfig` is structurally assignable, so apps that already hold one\n * can pass it straight through.\n */\nexport interface PalbeProxyConfig {\n /** Environment URL, e.g. `https://abc12345m.palbase.studio`. */\n url: string;\n /** Publishable (anon) API key: `pb_<environmentRef>_c<random>`. */\n apiKey: string;\n}\n\nexport interface PalbeProxyOptions {\n /** Refresh when the access token expires within this window. Default 60s. */\n refreshMarginMs?: number;\n /**\n * Pre-built response to decorate (e.g. from your own chained proxy logic) instead of\n * building `NextResponse.next({ request })`. Pass-through cases return it\n * UNMODIFIED. Caveat: a pre-built response snapshotted the request headers\n * at ITS creation, so a rotated cookie reaches the browser (Set-Cookie)\n * but not the downstream RSC render of THIS request — prefer omitting this\n * and building your response after palbeProxy when possible.\n */\n response?: NextResponse;\n}\n\ntype RefreshOutcome =\n | { kind: 'rotated'; session: StoredSession }\n | { kind: 'terminal' } // 400/401/403 — refresh token dead, sign out\n | { kind: 'transient' }; // network / 5xx / malformed — keep the session\n\n/**\n * Per-process single-flight: concurrent proxy invocations holding the\n * SAME refresh token (parallel tabs / prefetches) share one refresh call —\n * a per-token thundering herd would race palauth rotation. Multi-pod\n * concurrency is covered by palauth's 30s rotation grace (concurrent\n * refreshes of one token all succeed); per-process dedup is all that's\n * needed here (Studio precedent — don't over-engineer).\n */\nconst inflightRefreshes = new Map<string, Promise<RefreshOutcome>>();\n\nfunction refreshSingleFlight(\n config: PalbeProxyConfig,\n refreshToken: string,\n): Promise<RefreshOutcome> {\n const existing = inflightRefreshes.get(refreshToken);\n if (existing) return existing;\n const pending = refresh(config, refreshToken).finally(() => {\n inflightRefreshes.delete(refreshToken);\n });\n inflightRefreshes.set(refreshToken, pending);\n return pending;\n}\n\n/** Direct palauth refresh call (no runtime: the proxy needs only this one wire). */\nasync function refresh(config: PalbeProxyConfig, refreshToken: string): Promise<RefreshOutcome> {\n try {\n const res = await fetch(`${config.url}/auth/token/refresh`, {\n method: 'POST',\n headers: { apikey: config.apiKey, 'content-type': 'application/json' },\n body: JSON.stringify({ refresh_token: refreshToken }),\n });\n if (res.status === 400 || res.status === 401 || res.status === 403) {\n return { kind: 'terminal' };\n }\n if (!res.ok) return { kind: 'transient' };\n const raw: unknown = await res.json();\n if (typeof raw === 'object' && raw !== null) {\n const obj = raw as Record<string, unknown>;\n if (\n typeof obj.access_token === 'string' &&\n typeof obj.refresh_token === 'string' &&\n typeof obj.expires_in === 'number'\n ) {\n return {\n kind: 'rotated',\n session: {\n accessToken: obj.access_token,\n refreshToken: obj.refresh_token,\n expiresAt: Date.now() + obj.expires_in * 1000,\n },\n };\n }\n }\n return { kind: 'transient' }; // malformed 200 — never destroy a session over it\n } catch {\n return { kind: 'transient' }; // network failure\n }\n}\n\n/**\n * Keep the session cookie fresh for the request. No session / still-fresh\n * session → untouched pass-through (no cache-control changes). Stale or\n * expired → single-flight refresh, then the rotated (or cleared, on a dead\n * token) cookies are written onto BOTH the request (for the downstream RSC\n * render via `NextResponse.next({ request })`) and the response (for the\n * browser), with `Cache-Control: private, no-store` on touched responses.\n *\n * Cookie writes go through Next's cookie APIs with the DECODED codec values:\n * Next percent-encodes on serialization, so the wire bytes come out exactly\n * as the shared codec's (see encodeSessionCookiesDecoded). NextRequest's\n * cookie store is NOT a CookieStoreLike (its set is the 2-arg/RequestCookie\n * form), hence the direct codec calls here instead of the pbServer adapter.\n */\nexport async function palbeProxy(\n request: NextRequest,\n config: PalbeProxyConfig,\n opts?: PalbeProxyOptions,\n): Promise<NextResponse> {\n // Fail LOUDLY on a bad key. A silently empty ref would name the cookie\n // `palbe-session-` — a name the browser SDK never writes — so every\n // request would look session-less and the proxy would degrade to a\n // no-op, which is exactly the token-family revocation this exists to stop.\n const ref = environmentRefFromPublishableApiKey(config.apiKey);\n if (ref === '') {\n throw new BackendError('validation', {\n code: 'validation_error',\n message:\n 'palbeProxy() needs the publishable API key from palbe.gen.ts (pb_<environmentRef>_c…). Re-run `palbase web link` and copy the generated values.',\n });\n }\n const { NextResponse } = await importNextServer('palbeProxy');\n const passThrough = (): NextResponse => opts?.response ?? NextResponse.next({ request });\n\n const session = decodeSessionCookies((name) => request.cookies.get(name)?.value, ref);\n if (!session) return passThrough();\n if (session.expiresAt - Date.now() > (opts?.refreshMarginMs ?? DEFAULT_REFRESH_MARGIN_MS)) {\n return passThrough();\n }\n\n const outcome = await refreshSingleFlight(config, session.refreshToken);\n if (outcome.kind === 'transient') return passThrough(); // keep the session over blips\n\n // Session-cookie names in the incoming jar — the browser's state mirror.\n const staleNames = clearedSessionCookieNames(ref, (name) => request.cookies.has(name));\n\n if (outcome.kind === 'terminal') {\n for (const name of staleNames) request.cookies.delete(name);\n const response = opts?.response ?? NextResponse.next({ request });\n for (const name of staleNames) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n }\n\n const write = encodeSessionCookiesDecoded(ref, outcome.session);\n for (const name of staleNames) request.cookies.delete(name);\n for (const { name, value } of write.set) request.cookies.set(name, value);\n for (const name of write.clear) request.cookies.delete(name);\n // Build AFTER the request-cookie mutation: NextResponse.next({ request })\n // snapshots the request headers for the downstream RSC render.\n const response = opts?.response ?? NextResponse.next({ request });\n const written = new Set(write.set.map((c) => c.name));\n for (const { name, value } of write.set) {\n response.cookies.set(name, value, SESSION_COOKIE_ATTRS);\n }\n // Browser-side cleanup: stale names not part of the new run (a shrunken /\n // grown chunk layout) + the codec's one-past-the-end overflow guard.\n for (const name of new Set([...write.clear, ...staleNames])) {\n if (!written.has(name)) {\n response.cookies.set(name, '', { ...SESSION_COOKIE_ATTRS, maxAge: 0 });\n }\n }\n response.headers.set('cache-control', 'private, no-store');\n return response;\n}\n"],"mappings":"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","names":["response"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging,
|
|
1
|
+
import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging, X as PalbePerf, d as PalbeRuntime } from './analytics-facade-CdTK9Y0s.js';
|
|
2
2
|
|
|
3
3
|
interface CallOptions {
|
|
4
4
|
headers?: Record<string, string>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging,
|
|
1
|
+
import { x as PalbeAuth, z as PalbeFlags, E as PalbeRealtime, w as PalbeAnalytics, y as PalbeCalls, B as PalbeMessaging, X as PalbePerf, d as PalbeRuntime } from './analytics-facade-TB-D4ww8.cjs';
|
|
2
2
|
|
|
3
3
|
interface CallOptions {
|
|
4
4
|
headers?: Record<string, string>;
|
package/dist/react/index.cjs
CHANGED