@smalk/nextjs-ads 0.1.1 → 1.2.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/CHANGELOG.md +30 -0
- package/README.md +57 -1
- package/dist/app.cjs +57 -22
- package/dist/app.cjs.map +1 -1
- package/dist/app.js +2 -2
- package/dist/chunk-4QWOGJYF.js +219 -0
- package/dist/chunk-4QWOGJYF.js.map +1 -0
- package/dist/{chunk-YA6M2IA4.js → chunk-INI3DN37.js} +5 -5
- package/dist/chunk-INI3DN37.js.map +1 -0
- package/dist/index.cjs +232 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +89 -5
- package/dist/index.d.ts +89 -5
- package/dist/index.js +25 -3
- package/dist/middleware.cjs +60 -25
- package/dist/middleware.cjs.map +1 -1
- package/dist/middleware.js +2 -2
- package/dist/pages.cjs +57 -22
- package/dist/pages.cjs.map +1 -1
- package/dist/pages.d.cts +1 -0
- package/dist/pages.d.ts +1 -0
- package/dist/pages.js +2 -2
- package/package.json +10 -6
- package/dist/chunk-HX4ZGDYQ.js +0 -31
- package/dist/chunk-HX4ZGDYQ.js.map +0 -1
- package/dist/chunk-YA6M2IA4.js.map +0 -1
package/dist/middleware.cjs
CHANGED
|
@@ -33,36 +33,71 @@ __export(middleware_exports, {
|
|
|
33
33
|
withSmalkAds: () => withSmalkAds
|
|
34
34
|
});
|
|
35
35
|
module.exports = __toCommonJS(middleware_exports);
|
|
36
|
+
var import_server3 = require("next/server");
|
|
37
|
+
|
|
38
|
+
// src/config.ts
|
|
39
|
+
var DEFAULT_API_BASE = "https://api.smalk.ai/api/v1";
|
|
40
|
+
var PLUGIN_VERSION = "1.1.0";
|
|
41
|
+
function getSmalkConfig() {
|
|
42
|
+
const apiBase = process.env.SMALK_API_BASE || process.env.SMALK_API_BASE_URL || DEFAULT_API_BASE;
|
|
43
|
+
const apiKey = process.env.SMALK_API_KEY ?? "";
|
|
44
|
+
const projectKey = process.env.SMALK_PROJECT_KEY ?? "";
|
|
45
|
+
const cronSecret = process.env.SMALK_CRON_SECRET ?? "";
|
|
46
|
+
const publicHost = process.env.SMALK_PUBLIC_HOST || void 0;
|
|
47
|
+
return {
|
|
48
|
+
apiBase,
|
|
49
|
+
apiKey,
|
|
50
|
+
projectKey,
|
|
51
|
+
cronSecret,
|
|
52
|
+
pluginVersion: PLUGIN_VERSION,
|
|
53
|
+
publicHost
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/freshness/route.ts
|
|
58
|
+
var import_cache = require("next/cache");
|
|
36
59
|
var import_server = require("next/server");
|
|
37
60
|
|
|
61
|
+
// src/freshness/activeUrlsStore.ts
|
|
62
|
+
var FilesystemActiveUrlsStore = class {
|
|
63
|
+
constructor(filePath) {
|
|
64
|
+
this.filePath = filePath;
|
|
65
|
+
}
|
|
66
|
+
filePath;
|
|
67
|
+
async read() {
|
|
68
|
+
const fs = await import("fs/promises");
|
|
69
|
+
const path = await import("path");
|
|
70
|
+
const os = await import("os");
|
|
71
|
+
const f = this.filePath || path.join(os.tmpdir(), "smalk-active-ad-paths.json");
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(await fs.readFile(f, "utf8"));
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async write(payload) {
|
|
79
|
+
const fs = await import("fs/promises");
|
|
80
|
+
const path = await import("path");
|
|
81
|
+
const os = await import("os");
|
|
82
|
+
const f = this.filePath || path.join(os.tmpdir(), "smalk-active-ad-paths.json");
|
|
83
|
+
await fs.mkdir(path.dirname(f), { recursive: true });
|
|
84
|
+
await fs.writeFile(f, JSON.stringify(payload));
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
var currentStore = new FilesystemActiveUrlsStore();
|
|
88
|
+
|
|
89
|
+
// src/freshness/middleware.ts
|
|
90
|
+
var import_server2 = require("next/server");
|
|
91
|
+
|
|
38
92
|
// src/index.ts
|
|
39
93
|
var DEFAULT_TIMEOUT_MS = 100;
|
|
40
94
|
var DEFAULT_REVALIDATE_S = 1200;
|
|
41
|
-
var API_PATH = "/
|
|
42
|
-
function loadConfig() {
|
|
43
|
-
const projectKey = process.env.SMALK_PROJECT_KEY;
|
|
44
|
-
const apiKey = process.env.SMALK_API_KEY;
|
|
45
|
-
const apiBaseUrl = process.env.SMALK_API_BASE_URL || "https://api.smalk.ai";
|
|
46
|
-
const isProd = process.env.NODE_ENV === "production";
|
|
47
|
-
if (!projectKey) {
|
|
48
|
-
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
49
|
-
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
50
|
-
}
|
|
51
|
-
if (!apiKey) {
|
|
52
|
-
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
53
|
-
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
projectKey: projectKey ?? "",
|
|
57
|
-
apiKey: apiKey ?? "",
|
|
58
|
-
apiBaseUrl
|
|
59
|
-
};
|
|
60
|
-
}
|
|
95
|
+
var API_PATH = "/transform/ads/content/";
|
|
61
96
|
|
|
62
97
|
// src/client.ts
|
|
63
98
|
var lastHashByUrl = /* @__PURE__ */ new Map();
|
|
64
99
|
async function fetchAd(input, opts = {}) {
|
|
65
|
-
const cfg =
|
|
100
|
+
const cfg = getSmalkConfig();
|
|
66
101
|
if (!cfg.projectKey || !cfg.apiKey) return null;
|
|
67
102
|
const controller = new AbortController();
|
|
68
103
|
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
@@ -77,7 +112,7 @@ async function fetchAd(input, opts = {}) {
|
|
|
77
112
|
if (opts.preview !== void 0 && opts.preview !== false) {
|
|
78
113
|
body.preview = opts.preview;
|
|
79
114
|
}
|
|
80
|
-
const res = await fetch(`${cfg.
|
|
115
|
+
const res = await fetch(`${cfg.apiBase}${API_PATH}`, {
|
|
81
116
|
method: "POST",
|
|
82
117
|
headers: {
|
|
83
118
|
"Authorization": `Api-Key ${cfg.apiKey}`,
|
|
@@ -144,7 +179,7 @@ async function sha256Hex(input) {
|
|
|
144
179
|
var SMALK_ADS_PATTERN = /<(\w+)[^>]*\bsmalk-ads\b[^>]*>[\s\S]*?<\/\1>/i;
|
|
145
180
|
function withSmalkAds(opts = {}) {
|
|
146
181
|
return async function smalkAdsMiddleware(req) {
|
|
147
|
-
const res =
|
|
182
|
+
const res = import_server3.NextResponse.next();
|
|
148
183
|
const contentType = res.headers.get("content-type") ?? "";
|
|
149
184
|
if (!contentType.includes("text/html")) return res;
|
|
150
185
|
const body = await res.text();
|
|
@@ -158,10 +193,10 @@ function withSmalkAds(opts = {}) {
|
|
|
158
193
|
const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });
|
|
159
194
|
const headers = collectHeaders(res.headers);
|
|
160
195
|
if (!ad) {
|
|
161
|
-
return new
|
|
196
|
+
return new import_server3.NextResponse(body, { headers, status: res.status });
|
|
162
197
|
}
|
|
163
198
|
const replaced = body.replace(match[0], ad.html);
|
|
164
|
-
return new
|
|
199
|
+
return new import_server3.NextResponse(replaced, { headers, status: res.status });
|
|
165
200
|
};
|
|
166
201
|
}
|
|
167
202
|
function collectHeaders(h) {
|
package/dist/middleware.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/middleware.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport { fetchAd } from './client';\n\nconst SMALK_ADS_PATTERN = /<(\\w+)[^>]*\\bsmalk-ads\\b[^>]*>[\\s\\S]*?<\\/\\1>/i;\n\nexport interface WithSmalkAdsOptions {\n revalidate?: number;\n}\n\nexport function withSmalkAds(opts: WithSmalkAdsOptions = {}) {\n return async function smalkAdsMiddleware(req: NextRequest): Promise<Response> {\n const res = NextResponse.next();\n const contentType = res.headers.get('content-type') ?? '';\n if (!contentType.includes('text/html')) return res;\n\n const body = await res.text();\n if (!body || body.indexOf('smalk-ads') === -1) return res;\n\n const match = body.match(SMALK_ADS_PATTERN);\n if (!match) return res;\n\n const userAgent = req.headers.get('user-agent') ?? '';\n const referer = req.headers.get('referer') ?? '';\n const clientIp = pickClientIp(req.headers);\n const pageUrl = req.url ?? `https://${req.headers.get('host') ?? ''}${req.nextUrl?.pathname ?? '/'}`;\n\n const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });\n const headers = collectHeaders(res.headers);\n if (!ad) {\n return new NextResponse(body, { headers, status: res.status }) as unknown as Response;\n }\n const replaced = body.replace(match[0], ad.html);\n return new NextResponse(replaced, { headers, status: res.status }) as unknown as Response;\n };\n}\n\nfunction collectHeaders(h: { entries?: () => Iterable<[string, string]> } & { get?: (k: string) => string | null }): Record<string, string> {\n const out: Record<string, string> = {};\n if (typeof h.entries === 'function') {\n for (const [k, v] of h.entries()) out[k] = v;\n }\n return out;\n}\n\nfunction pickClientIp(h: { get: (k: string) => string | null }): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = h.get(key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n","import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/middleware.ts","../src/config.ts","../src/freshness/route.ts","../src/freshness/activeUrlsStore.ts","../src/freshness/middleware.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\nimport { fetchAd } from './client';\n\nconst SMALK_ADS_PATTERN = /<(\\w+)[^>]*\\bsmalk-ads\\b[^>]*>[\\s\\S]*?<\\/\\1>/i;\n\nexport interface WithSmalkAdsOptions {\n revalidate?: number;\n}\n\nexport function withSmalkAds(opts: WithSmalkAdsOptions = {}) {\n return async function smalkAdsMiddleware(req: NextRequest): Promise<Response> {\n const res = NextResponse.next();\n const contentType = res.headers.get('content-type') ?? '';\n if (!contentType.includes('text/html')) return res;\n\n const body = await res.text();\n if (!body || body.indexOf('smalk-ads') === -1) return res;\n\n const match = body.match(SMALK_ADS_PATTERN);\n if (!match) return res;\n\n const userAgent = req.headers.get('user-agent') ?? '';\n const referer = req.headers.get('referer') ?? '';\n const clientIp = pickClientIp(req.headers);\n const pageUrl = req.url ?? `https://${req.headers.get('host') ?? ''}${req.nextUrl?.pathname ?? '/'}`;\n\n const ad = await fetchAd({ pageUrl, userAgent, referer, clientIp }, { revalidate: opts.revalidate });\n const headers = collectHeaders(res.headers);\n if (!ad) {\n return new NextResponse(body, { headers, status: res.status }) as unknown as Response;\n }\n const replaced = body.replace(match[0], ad.html);\n return new NextResponse(replaced, { headers, status: res.status }) as unknown as Response;\n };\n}\n\nfunction collectHeaders(h: { entries?: () => Iterable<[string, string]> } & { get?: (k: string) => string | null }): Record<string, string> {\n const out: Record<string, string> = {};\n if (typeof h.entries === 'function') {\n for (const [k, v] of h.entries()) out[k] = v;\n }\n return out;\n}\n\nfunction pickClientIp(h: { get: (k: string) => string | null }): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = h.get(key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkConfig {\n /** Full API base including version path, e.g. https://api.smalk.ai/api/v1 */\n apiBase: string;\n apiKey: string;\n projectKey: string;\n cronSecret: string;\n pluginVersion: string;\n publicHost?: string;\n}\n\n/**\n * Backwards-compatible config shape for legacy callers (pre-P2). Adds an\n * `apiBaseUrl` alias for `apiBase`. New code should use `getSmalkConfig()`\n * and read `apiBase` directly.\n */\nexport interface LegacySmalkConfig extends SmalkConfig {\n /**\n * @deprecated Use `apiBase` instead.\n *\n * Pre-P2.5 shape: host-only (e.g. `https://api.smalk.ai`), and external\n * publisher code appended `/api/v1/...`. To preserve that shape after the\n * P2.5 Next config unify (which moved `/api/v1` into the canonical\n * `apiBase`), this field strips a trailing `/api/vN` segment from\n * `apiBase` before exposing it. New code should read `apiBase` directly.\n */\n apiBaseUrl: string;\n}\n\nconst DEFAULT_API_BASE = \"https://api.smalk.ai/api/v1\";\nconst PLUGIN_VERSION = \"1.1.0\";\n\nexport function getSmalkConfig(): SmalkConfig {\n const apiBase = process.env.SMALK_API_BASE || process.env.SMALK_API_BASE_URL || DEFAULT_API_BASE;\n const apiKey = process.env.SMALK_API_KEY ?? \"\";\n const projectKey = process.env.SMALK_PROJECT_KEY ?? \"\";\n const cronSecret = process.env.SMALK_CRON_SECRET ?? \"\";\n const publicHost = process.env.SMALK_PUBLIC_HOST || undefined;\n\n return {\n apiBase,\n apiKey,\n projectKey,\n cronSecret,\n pluginVersion: PLUGIN_VERSION,\n publicHost,\n };\n}\n\n/**\n * Strip a trailing `/api/vN` (or `/api/vN/`) segment from an API base URL.\n * Preserves backward compatibility with publishers building URLs like\n * `${apiBaseUrl}/api/v1/...` — without stripping, those would double-prefix\n * to `/api/v1/api/v1/...` and 404.\n */\nfunction stripApiPathSuffix(apiBase: string): string {\n return apiBase.replace(/\\/api\\/v\\d+\\/?$/, \"\");\n}\n\n/**\n * Legacy loader retained for external consumers (publisher apps) that import\n * `loadConfig` from `@smalk/nextjs-ads`. Returns the canonical `SmalkConfig`\n * plus an `apiBaseUrl` alias whose shape matches the pre-P2.5 contract\n * (host-only, no `/api/v1` suffix). New code should prefer `getSmalkConfig()`\n * and read `apiBase` directly.\n */\nexport function loadConfig(): LegacySmalkConfig {\n const cfg = getSmalkConfig();\n return { ...cfg, apiBaseUrl: stripApiPathSuffix(cfg.apiBase) };\n}\n","import { revalidatePath } from \"next/cache\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { getSmalkConfig } from \"../config\";\nimport { cacheActiveUrls } from \"./activeUrlsCache\";\n\nexport async function smalkFreshnessRevalidateHandler(req: NextRequest) {\n const cfg = getSmalkConfig();\n const secret = req.headers.get(\"x-smalk-cron-secret\");\n if (!secret || secret !== cfg.cronSecret) {\n return NextResponse.json({ error: \"unauthorized\" }, { status: 401 });\n }\n\n const endpoint = `${cfg.apiBase}/projects/${cfg.projectKey}/ads/inventory/active-ad-urls/`;\n const resp = await fetch(endpoint, {\n headers: {\n \"X-API-Key\": cfg.apiKey,\n \"X-Smalk-CMS\": \"nextjs\",\n \"X-Smalk-Plugin\": cfg.pluginVersion,\n },\n cache: \"no-store\",\n });\n if (!resp.ok) {\n return NextResponse.json({ error: `api ${resp.status}` }, { status: 502 });\n }\n const payload = (await resp.json()) as { urls: { url: string }[] };\n const paths = payload.urls\n .map((u) => safePath(u.url, cfg.publicHost))\n .filter((p): p is string => Boolean(p));\n\n await cacheActiveUrls(paths);\n\n let revalidated = 0;\n for (const p of paths) {\n try {\n revalidatePath(p);\n revalidated++;\n } catch (e) {\n console.warn(`[smalk] revalidatePath failed for ${p}`, e);\n }\n }\n\n return NextResponse.json({\n ok: true,\n total: paths.length,\n revalidated,\n ran_at: new Date().toISOString(),\n });\n}\n\nfunction safePath(url: string, publicHost?: string): string | null {\n try {\n const u = new URL(url);\n if (publicHost && u.host !== publicHost) return null;\n return u.pathname;\n } catch {\n return null;\n }\n}\n","export interface ActiveUrlsCachedPayload {\n paths: string[];\n updated_at: number;\n}\n\nexport interface ActiveUrlsStore {\n read(): Promise<ActiveUrlsCachedPayload | null>;\n write(payload: ActiveUrlsCachedPayload): Promise<void>;\n}\n\n// In-memory store (per-process, fast, lost on restart — good for tests).\nexport class InMemoryActiveUrlsStore implements ActiveUrlsStore {\n private value: ActiveUrlsCachedPayload | null = null;\n async read() { return this.value; }\n async write(payload: ActiveUrlsCachedPayload) { this.value = payload; }\n}\n\n// Filesystem fallback — uses os.tmpdir() (current behavior, kept for Node dev).\nexport class FilesystemActiveUrlsStore implements ActiveUrlsStore {\n constructor(private filePath?: string) {}\n async read(): Promise<ActiveUrlsCachedPayload | null> {\n const fs = await import(\"node:fs/promises\");\n const path = await import(\"node:path\");\n const os = await import(\"node:os\");\n const f = this.filePath || path.join(os.tmpdir(), \"smalk-active-ad-paths.json\");\n try {\n return JSON.parse(await fs.readFile(f, \"utf8\"));\n } catch {\n return null;\n }\n }\n async write(payload: ActiveUrlsCachedPayload) {\n const fs = await import(\"node:fs/promises\");\n const path = await import(\"node:path\");\n const os = await import(\"node:os\");\n const f = this.filePath || path.join(os.tmpdir(), \"smalk-active-ad-paths.json\");\n await fs.mkdir(path.dirname(f), { recursive: true });\n await fs.writeFile(f, JSON.stringify(payload));\n }\n}\n\n// Vercel KV adapter — requires @vercel/kv installed by the publisher.\n// We don't bundle the dep; lazy-import via a non-literal specifier so\n// bundlers (vite/vitest/tsup/webpack) don't try to resolve it at build time.\nconst VERCEL_KV_MODULE = \"@vercel/kv\";\nasync function loadVercelKv(): Promise<{ kv: { get(k: string): Promise<unknown>; set(k: string, v: string): Promise<unknown> } }> {\n // Indirect specifier — opaque to bundler static analysis.\n return (await import(/* @vite-ignore */ /* webpackIgnore: true */ VERCEL_KV_MODULE)) as {\n kv: { get(k: string): Promise<unknown>; set(k: string, v: string): Promise<unknown> };\n };\n}\n\nexport class VercelKVActiveUrlsStore implements ActiveUrlsStore {\n private readonly key: string;\n constructor(key = \"smalk:active-ad-paths\") { this.key = key; }\n async read(): Promise<ActiveUrlsCachedPayload | null> {\n const { kv } = await loadVercelKv();\n const raw = await kv.get(this.key);\n if (!raw) return null;\n try { return typeof raw === \"string\" ? JSON.parse(raw) : (raw as ActiveUrlsCachedPayload); }\n catch { return null; }\n }\n async write(payload: ActiveUrlsCachedPayload) {\n const { kv } = await loadVercelKv();\n await kv.set(this.key, JSON.stringify(payload));\n }\n}\n\n// Cloudflare KV adapter — publisher passes the KVNamespace binding.\nexport class CloudflareKVActiveUrlsStore implements ActiveUrlsStore {\n constructor(private namespace: { get(key: string): Promise<string | null>; put(key: string, value: string): Promise<void> }, private key = \"smalk:active-ad-paths\") {}\n async read(): Promise<ActiveUrlsCachedPayload | null> {\n const raw = await this.namespace.get(this.key);\n return raw ? JSON.parse(raw) : null;\n }\n async write(payload: ActiveUrlsCachedPayload) {\n await this.namespace.put(this.key, JSON.stringify(payload));\n }\n}\n\n// Module-level default — process-wide singleton, swap with setActiveUrlsStore().\nlet currentStore: ActiveUrlsStore = new FilesystemActiveUrlsStore();\n\nexport function getActiveUrlsStore(): ActiveUrlsStore {\n return currentStore;\n}\n\nexport function setActiveUrlsStore(store: ActiveUrlsStore): void {\n currentStore = store;\n}\n","import { NextRequest, NextResponse } from \"next/server\";\n\nimport { readActiveUrls } from \"./activeUrlsCache\";\n\nlet pathsCache: Set<string> | null = null;\nlet pathsLoadedAt = 0;\nconst RELOAD_INTERVAL_MS = 60_000;\n\nasync function getPaths(): Promise<Set<string>> {\n const now = Date.now();\n if (!pathsCache || now - pathsLoadedAt > RELOAD_INTERVAL_MS) {\n const data = await readActiveUrls();\n pathsCache = new Set(data?.paths ?? []);\n pathsLoadedAt = now;\n }\n return pathsCache;\n}\n\nexport async function smalkFreshnessMiddleware(req: NextRequest): Promise<NextResponse | undefined> {\n const paths = await getPaths();\n if (!paths.has(req.nextUrl.pathname)) return undefined;\n const res = NextResponse.next();\n // Day-bucket stable Last-Modified so real users with If-Modified-Since\n // still get 304s within the day; AI crawlers (which hit infrequently)\n // get a fresh signal on the next UTC day rollover.\n const dayBucket = Math.floor(Date.now() / 86_400_000) * 86_400_000;\n res.headers.set(\"Last-Modified\", new Date(dayBucket).toUTCString());\n res.headers.set(\"X-Smalk-Freshness\", \"bumped\");\n return res;\n}\n","export interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/transform/ads/content/';\n\nexport { getSmalkConfig, loadConfig } from './config';\nexport type { SmalkConfig, LegacySmalkConfig } from './config';\n\n/** @deprecated Use `SmalkConfig` from `./config`. Kept as alias for legacy imports. */\nexport type { LegacySmalkConfig as SmalkAdsConfig } from './config';\n\nexport { smalkFreshnessRevalidateHandler } from \"./freshness/route\";\nexport { smalkFreshnessMiddleware } from \"./freshness/middleware\";\nexport { cacheActiveUrls, readActiveUrls } from \"./freshness/activeUrlsCache\";\nexport {\n InMemoryActiveUrlsStore,\n FilesystemActiveUrlsStore,\n VercelKVActiveUrlsStore,\n CloudflareKVActiveUrlsStore,\n getActiveUrlsStore,\n setActiveUrlsStore,\n} from \"./freshness/activeUrlsStore\";\nexport type { ActiveUrlsStore, ActiveUrlsCachedPayload } from \"./freshness/activeUrlsStore\";\n","import { getSmalkConfig } from './config';\nimport { AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = getSmalkConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBase}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":"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","names":["import_server","import_server"]}
|
package/dist/middleware.js
CHANGED
package/dist/pages.cjs
CHANGED
|
@@ -36,34 +36,69 @@ __export(pages_exports, {
|
|
|
36
36
|
module.exports = __toCommonJS(pages_exports);
|
|
37
37
|
var React = __toESM(require("react"), 1);
|
|
38
38
|
|
|
39
|
-
// src/
|
|
40
|
-
var
|
|
41
|
-
var
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
48
|
-
if (!projectKey) {
|
|
49
|
-
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
50
|
-
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
51
|
-
}
|
|
52
|
-
if (!apiKey) {
|
|
53
|
-
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
54
|
-
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
55
|
-
}
|
|
39
|
+
// src/config.ts
|
|
40
|
+
var DEFAULT_API_BASE = "https://api.smalk.ai/api/v1";
|
|
41
|
+
var PLUGIN_VERSION = "1.1.0";
|
|
42
|
+
function getSmalkConfig() {
|
|
43
|
+
const apiBase = process.env.SMALK_API_BASE || process.env.SMALK_API_BASE_URL || DEFAULT_API_BASE;
|
|
44
|
+
const apiKey = process.env.SMALK_API_KEY ?? "";
|
|
45
|
+
const projectKey = process.env.SMALK_PROJECT_KEY ?? "";
|
|
46
|
+
const cronSecret = process.env.SMALK_CRON_SECRET ?? "";
|
|
47
|
+
const publicHost = process.env.SMALK_PUBLIC_HOST || void 0;
|
|
56
48
|
return {
|
|
57
|
-
|
|
58
|
-
apiKey
|
|
59
|
-
|
|
49
|
+
apiBase,
|
|
50
|
+
apiKey,
|
|
51
|
+
projectKey,
|
|
52
|
+
cronSecret,
|
|
53
|
+
pluginVersion: PLUGIN_VERSION,
|
|
54
|
+
publicHost
|
|
60
55
|
};
|
|
61
56
|
}
|
|
62
57
|
|
|
58
|
+
// src/freshness/route.ts
|
|
59
|
+
var import_cache = require("next/cache");
|
|
60
|
+
var import_server = require("next/server");
|
|
61
|
+
|
|
62
|
+
// src/freshness/activeUrlsStore.ts
|
|
63
|
+
var FilesystemActiveUrlsStore = class {
|
|
64
|
+
constructor(filePath) {
|
|
65
|
+
this.filePath = filePath;
|
|
66
|
+
}
|
|
67
|
+
filePath;
|
|
68
|
+
async read() {
|
|
69
|
+
const fs = await import("fs/promises");
|
|
70
|
+
const path = await import("path");
|
|
71
|
+
const os = await import("os");
|
|
72
|
+
const f = this.filePath || path.join(os.tmpdir(), "smalk-active-ad-paths.json");
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(await fs.readFile(f, "utf8"));
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async write(payload) {
|
|
80
|
+
const fs = await import("fs/promises");
|
|
81
|
+
const path = await import("path");
|
|
82
|
+
const os = await import("os");
|
|
83
|
+
const f = this.filePath || path.join(os.tmpdir(), "smalk-active-ad-paths.json");
|
|
84
|
+
await fs.mkdir(path.dirname(f), { recursive: true });
|
|
85
|
+
await fs.writeFile(f, JSON.stringify(payload));
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
var currentStore = new FilesystemActiveUrlsStore();
|
|
89
|
+
|
|
90
|
+
// src/freshness/middleware.ts
|
|
91
|
+
var import_server2 = require("next/server");
|
|
92
|
+
|
|
93
|
+
// src/index.ts
|
|
94
|
+
var DEFAULT_TIMEOUT_MS = 100;
|
|
95
|
+
var DEFAULT_REVALIDATE_S = 1200;
|
|
96
|
+
var API_PATH = "/transform/ads/content/";
|
|
97
|
+
|
|
63
98
|
// src/client.ts
|
|
64
99
|
var lastHashByUrl = /* @__PURE__ */ new Map();
|
|
65
100
|
async function fetchAd(input, opts = {}) {
|
|
66
|
-
const cfg =
|
|
101
|
+
const cfg = getSmalkConfig();
|
|
67
102
|
if (!cfg.projectKey || !cfg.apiKey) return null;
|
|
68
103
|
const controller = new AbortController();
|
|
69
104
|
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
@@ -78,7 +113,7 @@ async function fetchAd(input, opts = {}) {
|
|
|
78
113
|
if (opts.preview !== void 0 && opts.preview !== false) {
|
|
79
114
|
body.preview = opts.preview;
|
|
80
115
|
}
|
|
81
|
-
const res = await fetch(`${cfg.
|
|
116
|
+
const res = await fetch(`${cfg.apiBase}${API_PATH}`, {
|
|
82
117
|
method: "POST",
|
|
83
118
|
headers: {
|
|
84
119
|
"Authorization": `Api-Key ${cfg.apiKey}`,
|
package/dist/pages.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/pages.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import * as React from 'react';\nimport type { IncomingMessage } from 'node:http';\nimport { fetchAd } from './client';\nimport type { AdResult } from './index';\n\nexport interface MinimalReq {\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\n\nexport interface GetSmalkAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function getSmalkAd(\n req: IncomingMessage | MinimalReq,\n opts: GetSmalkAdOptions = {},\n): Promise<AdResult | null> {\n const headers = (req as MinimalReq).headers ?? {};\n const url = (req as MinimalReq).url ?? '/';\n const host = headerValue(headers, 'host') ?? '';\n const pageUrl = host ? `https://${host}${url}` : url;\n const userAgent = headerValue(headers, 'user-agent') ?? '';\n const referer = headerValue(headers, 'referer') ?? '';\n const clientIp = pickClientIp(headers);\n\n // Auto-forward `?preview=` from the page URL (so visiting /blog/x?preview=1 works without\n // a callsite change). Explicit opts.preview takes precedence.\n let preview = opts.preview;\n if (preview === undefined) {\n const m = url.match(/[?&]preview=([^&]+)/);\n if (m) {\n let v: string;\n try {\n v = decodeURIComponent(m[1]);\n } catch {\n v = m[1];\n }\n preview = v === '1' || v === 'true' ? true : v;\n }\n }\n\n return fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: opts.revalidate, preview },\n );\n}\n\nexport interface AdHtmlProps {\n ad: AdResult | null;\n className?: string;\n}\n\nexport function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement {\n if (!ad) {\n return React.createElement('span', {\n 'data-smalk': 'comment',\n dangerouslySetInnerHTML: { __html: '<!-- smalk: no ad -->' },\n });\n }\n return React.createElement('div', {\n className: className ?? 'smalk-ads',\n 'data-smalk-booking': ad.bookingId ?? undefined,\n dangerouslySetInnerHTML: { __html: ad.html },\n });\n}\n\nfunction headerValue(h: Record<string, string | string[] | undefined>, key: string): string | null {\n const v = h[key] ?? h[key.toLowerCase()];\n if (Array.isArray(v)) return v[0] ?? null;\n return v ?? null;\n}\n\nfunction pickClientIp(h: Record<string, string | string[] | undefined>): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = headerValue(h, key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n","import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/pages.ts","../src/config.ts","../src/freshness/route.ts","../src/freshness/activeUrlsStore.ts","../src/freshness/middleware.ts","../src/index.ts","../src/client.ts"],"sourcesContent":["import * as React from 'react';\nimport type { IncomingMessage } from 'node:http';\nimport { fetchAd } from './client';\nimport type { AdResult } from './index';\n\nexport interface MinimalReq {\n url?: string;\n headers: Record<string, string | string[] | undefined>;\n}\n\nexport interface GetSmalkAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string. Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function getSmalkAd(\n req: IncomingMessage | MinimalReq,\n opts: GetSmalkAdOptions = {},\n): Promise<AdResult | null> {\n const headers = (req as MinimalReq).headers ?? {};\n const url = (req as MinimalReq).url ?? '/';\n const host = headerValue(headers, 'host') ?? '';\n const pageUrl = host ? `https://${host}${url}` : url;\n const userAgent = headerValue(headers, 'user-agent') ?? '';\n const referer = headerValue(headers, 'referer') ?? '';\n const clientIp = pickClientIp(headers);\n\n // Auto-forward `?preview=` from the page URL (so visiting /blog/x?preview=1 works without\n // a callsite change). Explicit opts.preview takes precedence.\n let preview = opts.preview;\n if (preview === undefined) {\n const m = url.match(/[?&]preview=([^&]+)/);\n if (m) {\n let v: string;\n try {\n v = decodeURIComponent(m[1]);\n } catch {\n v = m[1];\n }\n preview = v === '1' || v === 'true' ? true : v;\n }\n }\n\n return fetchAd(\n { pageUrl, userAgent, referer, clientIp },\n { revalidate: opts.revalidate, preview },\n );\n}\n\nexport interface AdHtmlProps {\n ad: AdResult | null;\n className?: string;\n}\n\nexport function AdHtml({ ad, className }: AdHtmlProps): React.ReactElement {\n if (!ad) {\n return React.createElement('span', {\n 'data-smalk': 'comment',\n dangerouslySetInnerHTML: { __html: '<!-- smalk: no ad -->' },\n });\n }\n return React.createElement('div', {\n className: className ?? 'smalk-ads',\n 'data-smalk-booking': ad.bookingId ?? undefined,\n dangerouslySetInnerHTML: { __html: ad.html },\n });\n}\n\nfunction headerValue(h: Record<string, string | string[] | undefined>, key: string): string | null {\n const v = h[key] ?? h[key.toLowerCase()];\n if (Array.isArray(v)) return v[0] ?? null;\n return v ?? null;\n}\n\nfunction pickClientIp(h: Record<string, string | string[] | undefined>): string {\n for (const key of ['cf-connecting-ip', 'x-real-ip', 'x-forwarded-for']) {\n const v = headerValue(h, key);\n if (v) return v.split(',')[0].trim();\n }\n return '';\n}\n","export interface SmalkConfig {\n /** Full API base including version path, e.g. https://api.smalk.ai/api/v1 */\n apiBase: string;\n apiKey: string;\n projectKey: string;\n cronSecret: string;\n pluginVersion: string;\n publicHost?: string;\n}\n\n/**\n * Backwards-compatible config shape for legacy callers (pre-P2). Adds an\n * `apiBaseUrl` alias for `apiBase`. New code should use `getSmalkConfig()`\n * and read `apiBase` directly.\n */\nexport interface LegacySmalkConfig extends SmalkConfig {\n /**\n * @deprecated Use `apiBase` instead.\n *\n * Pre-P2.5 shape: host-only (e.g. `https://api.smalk.ai`), and external\n * publisher code appended `/api/v1/...`. To preserve that shape after the\n * P2.5 Next config unify (which moved `/api/v1` into the canonical\n * `apiBase`), this field strips a trailing `/api/vN` segment from\n * `apiBase` before exposing it. New code should read `apiBase` directly.\n */\n apiBaseUrl: string;\n}\n\nconst DEFAULT_API_BASE = \"https://api.smalk.ai/api/v1\";\nconst PLUGIN_VERSION = \"1.1.0\";\n\nexport function getSmalkConfig(): SmalkConfig {\n const apiBase = process.env.SMALK_API_BASE || process.env.SMALK_API_BASE_URL || DEFAULT_API_BASE;\n const apiKey = process.env.SMALK_API_KEY ?? \"\";\n const projectKey = process.env.SMALK_PROJECT_KEY ?? \"\";\n const cronSecret = process.env.SMALK_CRON_SECRET ?? \"\";\n const publicHost = process.env.SMALK_PUBLIC_HOST || undefined;\n\n return {\n apiBase,\n apiKey,\n projectKey,\n cronSecret,\n pluginVersion: PLUGIN_VERSION,\n publicHost,\n };\n}\n\n/**\n * Strip a trailing `/api/vN` (or `/api/vN/`) segment from an API base URL.\n * Preserves backward compatibility with publishers building URLs like\n * `${apiBaseUrl}/api/v1/...` — without stripping, those would double-prefix\n * to `/api/v1/api/v1/...` and 404.\n */\nfunction stripApiPathSuffix(apiBase: string): string {\n return apiBase.replace(/\\/api\\/v\\d+\\/?$/, \"\");\n}\n\n/**\n * Legacy loader retained for external consumers (publisher apps) that import\n * `loadConfig` from `@smalk/nextjs-ads`. Returns the canonical `SmalkConfig`\n * plus an `apiBaseUrl` alias whose shape matches the pre-P2.5 contract\n * (host-only, no `/api/v1` suffix). New code should prefer `getSmalkConfig()`\n * and read `apiBase` directly.\n */\nexport function loadConfig(): LegacySmalkConfig {\n const cfg = getSmalkConfig();\n return { ...cfg, apiBaseUrl: stripApiPathSuffix(cfg.apiBase) };\n}\n","import { revalidatePath } from \"next/cache\";\nimport { NextRequest, NextResponse } from \"next/server\";\n\nimport { getSmalkConfig } from \"../config\";\nimport { cacheActiveUrls } from \"./activeUrlsCache\";\n\nexport async function smalkFreshnessRevalidateHandler(req: NextRequest) {\n const cfg = getSmalkConfig();\n const secret = req.headers.get(\"x-smalk-cron-secret\");\n if (!secret || secret !== cfg.cronSecret) {\n return NextResponse.json({ error: \"unauthorized\" }, { status: 401 });\n }\n\n const endpoint = `${cfg.apiBase}/projects/${cfg.projectKey}/ads/inventory/active-ad-urls/`;\n const resp = await fetch(endpoint, {\n headers: {\n \"X-API-Key\": cfg.apiKey,\n \"X-Smalk-CMS\": \"nextjs\",\n \"X-Smalk-Plugin\": cfg.pluginVersion,\n },\n cache: \"no-store\",\n });\n if (!resp.ok) {\n return NextResponse.json({ error: `api ${resp.status}` }, { status: 502 });\n }\n const payload = (await resp.json()) as { urls: { url: string }[] };\n const paths = payload.urls\n .map((u) => safePath(u.url, cfg.publicHost))\n .filter((p): p is string => Boolean(p));\n\n await cacheActiveUrls(paths);\n\n let revalidated = 0;\n for (const p of paths) {\n try {\n revalidatePath(p);\n revalidated++;\n } catch (e) {\n console.warn(`[smalk] revalidatePath failed for ${p}`, e);\n }\n }\n\n return NextResponse.json({\n ok: true,\n total: paths.length,\n revalidated,\n ran_at: new Date().toISOString(),\n });\n}\n\nfunction safePath(url: string, publicHost?: string): string | null {\n try {\n const u = new URL(url);\n if (publicHost && u.host !== publicHost) return null;\n return u.pathname;\n } catch {\n return null;\n }\n}\n","export interface ActiveUrlsCachedPayload {\n paths: string[];\n updated_at: number;\n}\n\nexport interface ActiveUrlsStore {\n read(): Promise<ActiveUrlsCachedPayload | null>;\n write(payload: ActiveUrlsCachedPayload): Promise<void>;\n}\n\n// In-memory store (per-process, fast, lost on restart — good for tests).\nexport class InMemoryActiveUrlsStore implements ActiveUrlsStore {\n private value: ActiveUrlsCachedPayload | null = null;\n async read() { return this.value; }\n async write(payload: ActiveUrlsCachedPayload) { this.value = payload; }\n}\n\n// Filesystem fallback — uses os.tmpdir() (current behavior, kept for Node dev).\nexport class FilesystemActiveUrlsStore implements ActiveUrlsStore {\n constructor(private filePath?: string) {}\n async read(): Promise<ActiveUrlsCachedPayload | null> {\n const fs = await import(\"node:fs/promises\");\n const path = await import(\"node:path\");\n const os = await import(\"node:os\");\n const f = this.filePath || path.join(os.tmpdir(), \"smalk-active-ad-paths.json\");\n try {\n return JSON.parse(await fs.readFile(f, \"utf8\"));\n } catch {\n return null;\n }\n }\n async write(payload: ActiveUrlsCachedPayload) {\n const fs = await import(\"node:fs/promises\");\n const path = await import(\"node:path\");\n const os = await import(\"node:os\");\n const f = this.filePath || path.join(os.tmpdir(), \"smalk-active-ad-paths.json\");\n await fs.mkdir(path.dirname(f), { recursive: true });\n await fs.writeFile(f, JSON.stringify(payload));\n }\n}\n\n// Vercel KV adapter — requires @vercel/kv installed by the publisher.\n// We don't bundle the dep; lazy-import via a non-literal specifier so\n// bundlers (vite/vitest/tsup/webpack) don't try to resolve it at build time.\nconst VERCEL_KV_MODULE = \"@vercel/kv\";\nasync function loadVercelKv(): Promise<{ kv: { get(k: string): Promise<unknown>; set(k: string, v: string): Promise<unknown> } }> {\n // Indirect specifier — opaque to bundler static analysis.\n return (await import(/* @vite-ignore */ /* webpackIgnore: true */ VERCEL_KV_MODULE)) as {\n kv: { get(k: string): Promise<unknown>; set(k: string, v: string): Promise<unknown> };\n };\n}\n\nexport class VercelKVActiveUrlsStore implements ActiveUrlsStore {\n private readonly key: string;\n constructor(key = \"smalk:active-ad-paths\") { this.key = key; }\n async read(): Promise<ActiveUrlsCachedPayload | null> {\n const { kv } = await loadVercelKv();\n const raw = await kv.get(this.key);\n if (!raw) return null;\n try { return typeof raw === \"string\" ? JSON.parse(raw) : (raw as ActiveUrlsCachedPayload); }\n catch { return null; }\n }\n async write(payload: ActiveUrlsCachedPayload) {\n const { kv } = await loadVercelKv();\n await kv.set(this.key, JSON.stringify(payload));\n }\n}\n\n// Cloudflare KV adapter — publisher passes the KVNamespace binding.\nexport class CloudflareKVActiveUrlsStore implements ActiveUrlsStore {\n constructor(private namespace: { get(key: string): Promise<string | null>; put(key: string, value: string): Promise<void> }, private key = \"smalk:active-ad-paths\") {}\n async read(): Promise<ActiveUrlsCachedPayload | null> {\n const raw = await this.namespace.get(this.key);\n return raw ? JSON.parse(raw) : null;\n }\n async write(payload: ActiveUrlsCachedPayload) {\n await this.namespace.put(this.key, JSON.stringify(payload));\n }\n}\n\n// Module-level default — process-wide singleton, swap with setActiveUrlsStore().\nlet currentStore: ActiveUrlsStore = new FilesystemActiveUrlsStore();\n\nexport function getActiveUrlsStore(): ActiveUrlsStore {\n return currentStore;\n}\n\nexport function setActiveUrlsStore(store: ActiveUrlsStore): void {\n currentStore = store;\n}\n","import { NextRequest, NextResponse } from \"next/server\";\n\nimport { readActiveUrls } from \"./activeUrlsCache\";\n\nlet pathsCache: Set<string> | null = null;\nlet pathsLoadedAt = 0;\nconst RELOAD_INTERVAL_MS = 60_000;\n\nasync function getPaths(): Promise<Set<string>> {\n const now = Date.now();\n if (!pathsCache || now - pathsLoadedAt > RELOAD_INTERVAL_MS) {\n const data = await readActiveUrls();\n pathsCache = new Set(data?.paths ?? []);\n pathsLoadedAt = now;\n }\n return pathsCache;\n}\n\nexport async function smalkFreshnessMiddleware(req: NextRequest): Promise<NextResponse | undefined> {\n const paths = await getPaths();\n if (!paths.has(req.nextUrl.pathname)) return undefined;\n const res = NextResponse.next();\n // Day-bucket stable Last-Modified so real users with If-Modified-Since\n // still get 304s within the day; AI crawlers (which hit infrequently)\n // get a fresh signal on the next UTC day rollover.\n const dayBucket = Math.floor(Date.now() / 86_400_000) * 86_400_000;\n res.headers.set(\"Last-Modified\", new Date(dayBucket).toUTCString());\n res.headers.set(\"X-Smalk-Freshness\", \"bumped\");\n return res;\n}\n","export interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/transform/ads/content/';\n\nexport { getSmalkConfig, loadConfig } from './config';\nexport type { SmalkConfig, LegacySmalkConfig } from './config';\n\n/** @deprecated Use `SmalkConfig` from `./config`. Kept as alias for legacy imports. */\nexport type { LegacySmalkConfig as SmalkAdsConfig } from './config';\n\nexport { smalkFreshnessRevalidateHandler } from \"./freshness/route\";\nexport { smalkFreshnessMiddleware } from \"./freshness/middleware\";\nexport { cacheActiveUrls, readActiveUrls } from \"./freshness/activeUrlsCache\";\nexport {\n InMemoryActiveUrlsStore,\n FilesystemActiveUrlsStore,\n VercelKVActiveUrlsStore,\n CloudflareKVActiveUrlsStore,\n getActiveUrlsStore,\n setActiveUrlsStore,\n} from \"./freshness/activeUrlsStore\";\nexport type { ActiveUrlsStore, ActiveUrlsCachedPayload } from \"./freshness/activeUrlsStore\";\n","import { getSmalkConfig } from './config';\nimport { AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = getSmalkConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBase}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":"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","names":["import_server"]}
|
package/dist/pages.d.cts
CHANGED
package/dist/pages.d.ts
CHANGED
package/dist/pages.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smalk/nextjs-ads",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Smalk AI Ads — server-side ad injection for Next.js (App Router, Pages Router, middleware)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
34
|
"README.md",
|
|
35
|
-
"LICENSE"
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"CHANGELOG.md"
|
|
36
37
|
],
|
|
37
38
|
"scripts": {
|
|
38
39
|
"build": "tsup",
|
|
@@ -52,14 +53,14 @@
|
|
|
52
53
|
"@testing-library/react": "^16.0.0",
|
|
53
54
|
"@types/node": "^20.0.0",
|
|
54
55
|
"@types/react": "^18.0.0",
|
|
55
|
-
"happy-dom": "^
|
|
56
|
-
"next": "^
|
|
56
|
+
"happy-dom": "^20.9.0",
|
|
57
|
+
"next": "^16.2.6",
|
|
57
58
|
"react": "^18.0.0",
|
|
58
59
|
"react-dom": "^18.0.0",
|
|
59
60
|
"server-only": "^0.0.1",
|
|
60
61
|
"tsup": "^8.0.0",
|
|
61
62
|
"typescript": "^5.4.0",
|
|
62
|
-
"vitest": "^2.
|
|
63
|
+
"vitest": "^3.2.4"
|
|
63
64
|
},
|
|
64
65
|
"engines": {
|
|
65
66
|
"node": ">=18.0.0"
|
|
@@ -71,5 +72,8 @@
|
|
|
71
72
|
"ssr",
|
|
72
73
|
"ads",
|
|
73
74
|
"publisher"
|
|
74
|
-
]
|
|
75
|
+
],
|
|
76
|
+
"overrides": {
|
|
77
|
+
"postcss": "^8.5.10"
|
|
78
|
+
}
|
|
75
79
|
}
|
package/dist/chunk-HX4ZGDYQ.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
var DEFAULT_TIMEOUT_MS = 100;
|
|
3
|
-
var DEFAULT_REVALIDATE_S = 1200;
|
|
4
|
-
var API_PATH = "/api/v1/transform/ads/content/";
|
|
5
|
-
function loadConfig() {
|
|
6
|
-
const projectKey = process.env.SMALK_PROJECT_KEY;
|
|
7
|
-
const apiKey = process.env.SMALK_API_KEY;
|
|
8
|
-
const apiBaseUrl = process.env.SMALK_API_BASE_URL || "https://api.smalk.ai";
|
|
9
|
-
const isProd = process.env.NODE_ENV === "production";
|
|
10
|
-
if (!projectKey) {
|
|
11
|
-
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production");
|
|
12
|
-
console.warn("[smalk] SMALK_PROJECT_KEY not set \u2014 ad fetch will be skipped");
|
|
13
|
-
}
|
|
14
|
-
if (!apiKey) {
|
|
15
|
-
if (isProd) throw new Error("@smalk/nextjs-ads: SMALK_API_KEY env var is required in production");
|
|
16
|
-
console.warn("[smalk] SMALK_API_KEY not set \u2014 ad fetch will be skipped");
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
projectKey: projectKey ?? "",
|
|
20
|
-
apiKey: apiKey ?? "",
|
|
21
|
-
apiBaseUrl
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export {
|
|
26
|
-
DEFAULT_TIMEOUT_MS,
|
|
27
|
-
DEFAULT_REVALIDATE_S,
|
|
28
|
-
API_PATH,
|
|
29
|
-
loadConfig
|
|
30
|
-
};
|
|
31
|
-
//# sourceMappingURL=chunk-HX4ZGDYQ.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["export interface SmalkAdsConfig {\n projectKey: string;\n apiKey: string;\n apiBaseUrl: string;\n}\n\nexport interface AdInput {\n pageUrl: string;\n userAgent: string;\n referer: string;\n clientIp: string;\n}\n\nexport interface AdResult {\n html: string;\n bookingId: string | null;\n}\n\nexport interface ApiResponse {\n html?: string;\n booking_id?: string;\n metadata?: Record<string, unknown>;\n}\n\nexport const DEFAULT_TIMEOUT_MS = 100;\nexport const DEFAULT_REVALIDATE_S = 1200;\nexport const API_PATH = '/api/v1/transform/ads/content/';\n\nexport function loadConfig(): SmalkAdsConfig {\n const projectKey = process.env.SMALK_PROJECT_KEY;\n const apiKey = process.env.SMALK_API_KEY;\n const apiBaseUrl = process.env.SMALK_API_BASE_URL || 'https://api.smalk.ai';\n const isProd = process.env.NODE_ENV === 'production';\n\n if (!projectKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_PROJECT_KEY env var is required in production');\n console.warn('[smalk] SMALK_PROJECT_KEY not set — ad fetch will be skipped');\n }\n if (!apiKey) {\n if (isProd) throw new Error('@smalk/nextjs-ads: SMALK_API_KEY env var is required in production');\n console.warn('[smalk] SMALK_API_KEY not set — ad fetch will be skipped');\n }\n\n return {\n projectKey: projectKey ?? '',\n apiKey: apiKey ?? '',\n apiBaseUrl,\n };\n}\n"],"mappings":";AAwBO,IAAM,qBAAqB;AAC3B,IAAM,uBAAuB;AAC7B,IAAM,WAAW;AAEjB,SAAS,aAA6B;AAC3C,QAAM,aAAa,QAAQ,IAAI;AAC/B,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,aAAa,QAAQ,IAAI,sBAAsB;AACrD,QAAM,SAAS,QAAQ,IAAI,aAAa;AAExC,MAAI,CAAC,YAAY;AACf,QAAI,OAAQ,OAAM,IAAI,MAAM,wEAAwE;AACpG,YAAQ,KAAK,mEAA8D;AAAA,EAC7E;AACA,MAAI,CAAC,QAAQ;AACX,QAAI,OAAQ,OAAM,IAAI,MAAM,oEAAoE;AAChG,YAAQ,KAAK,+DAA0D;AAAA,EACzE;AAEA,SAAO;AAAA,IACL,YAAY,cAAc;AAAA,IAC1B,QAAQ,UAAU;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client.ts"],"sourcesContent":["import { loadConfig, AdInput, AdResult, ApiResponse, API_PATH, DEFAULT_TIMEOUT_MS, DEFAULT_REVALIDATE_S } from './index';\n\nconst lastHashByUrl = new Map<string, string>();\n\nexport function _resetHashCacheForTests(): void {\n lastHashByUrl.clear();\n}\n\nexport interface FetchAdOptions {\n revalidate?: number;\n /** Preview mode: `true` (PARAGRAPH placeholder) or a format string (e.g. `'FAQ'`). Bypasses booking lookup. */\n preview?: boolean | string;\n}\n\nexport async function fetchAd(input: AdInput, opts: FetchAdOptions = {}): Promise<AdResult | null> {\n const cfg = loadConfig();\n if (!cfg.projectKey || !cfg.apiKey) return null;\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);\n\n try {\n const body: Record<string, unknown> = {\n project_key: cfg.projectKey,\n page_url: input.pageUrl,\n user_agent: input.userAgent,\n referer: input.referer,\n client_ip: input.clientIp,\n };\n if (opts.preview !== undefined && opts.preview !== false) {\n body.preview = opts.preview;\n }\n const res = await fetch(`${cfg.apiBaseUrl}${API_PATH}`, {\n method: 'POST',\n headers: {\n 'Authorization': `Api-Key ${cfg.apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n signal: controller.signal,\n // `next` is a Next.js fetch extension; type provided by next types when present.\n next: {\n revalidate: opts.revalidate ?? DEFAULT_REVALIDATE_S,\n tags: ['smalk-ad', `smalk-ad:${input.pageUrl}`],\n },\n } as RequestInit);\n\n if (!res.ok) {\n logErrorOnce(`smalk-ads fetch failed: HTTP ${res.status}`);\n return null;\n }\n\n const data = (await res.json()) as ApiResponse;\n if (!data.html) return null;\n\n const hash = await sha256Hex(data.html);\n const prev = lastHashByUrl.get(input.pageUrl);\n if (prev && prev !== hash) {\n await maybeRevalidatePath(input.pageUrl);\n }\n lastHashByUrl.set(input.pageUrl, hash);\n\n return { html: data.html, bookingId: data.booking_id ?? null };\n } catch (err) {\n if (err instanceof Error && err.name !== 'AbortError') {\n logErrorOnce(`smalk-ads fetch error: ${err.message}`);\n }\n return null;\n } finally {\n clearTimeout(timer);\n }\n}\n\nasync function maybeRevalidatePath(pageUrl: string): Promise<void> {\n try {\n const mod = await import('next/cache');\n if (typeof mod.revalidatePath === 'function') {\n const pathname = new URL(pageUrl).pathname;\n mod.revalidatePath(pathname);\n }\n } catch {\n // next/cache not available (Pages Router runtime, edge, or test env without next).\n }\n}\n\nconst seenErrors = new Set<string>();\nfunction logErrorOnce(msg: string): void {\n if (seenErrors.has(msg)) return;\n seenErrors.add(msg);\n console.error(`[smalk-nextjs-ads] ${msg}`);\n}\n\nasync function sha256Hex(input: string): Promise<string> {\n const data = new TextEncoder().encode(input);\n const buf = await globalThis.crypto.subtle.digest('SHA-256', data);\n const bytes = new Uint8Array(buf);\n let hex = '';\n for (let i = 0; i < bytes.length; i++) {\n hex += bytes[i].toString(16).padStart(2, '0');\n }\n return hex;\n}\n"],"mappings":";;;;;;;;AAEA,IAAM,gBAAgB,oBAAI,IAAoB;AAY9C,eAAsB,QAAQ,OAAgB,OAAuB,CAAC,GAA6B;AACjG,QAAM,MAAM,WAAW;AACvB,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,OAAQ,QAAO;AAE3C,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,kBAAkB;AAErE,MAAI;AACF,UAAM,OAAgC;AAAA,MACpC,aAAa,IAAI;AAAA,MACjB,UAAU,MAAM;AAAA,MAChB,YAAY,MAAM;AAAA,MAClB,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,IACnB;AACA,QAAI,KAAK,YAAY,UAAa,KAAK,YAAY,OAAO;AACxD,WAAK,UAAU,KAAK;AAAA,IACtB;AACA,UAAM,MAAM,MAAM,MAAM,GAAG,IAAI,UAAU,GAAG,QAAQ,IAAI;AAAA,MACtD,QAAQ;AAAA,MACR,SAAS;AAAA,QACP,iBAAiB,WAAW,IAAI,MAAM;AAAA,QACtC,gBAAgB;AAAA,MAClB;AAAA,MACA,MAAM,KAAK,UAAU,IAAI;AAAA,MACzB,QAAQ,WAAW;AAAA;AAAA,MAEnB,MAAM;AAAA,QACJ,YAAY,KAAK,cAAc;AAAA,QAC/B,MAAM,CAAC,YAAY,YAAY,MAAM,OAAO,EAAE;AAAA,MAChD;AAAA,IACF,CAAgB;AAEhB,QAAI,CAAC,IAAI,IAAI;AACX,mBAAa,gCAAgC,IAAI,MAAM,EAAE;AACzD,aAAO;AAAA,IACT;AAEA,UAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,QAAI,CAAC,KAAK,KAAM,QAAO;AAEvB,UAAM,OAAO,MAAM,UAAU,KAAK,IAAI;AACtC,UAAM,OAAO,cAAc,IAAI,MAAM,OAAO;AAC5C,QAAI,QAAQ,SAAS,MAAM;AACzB,YAAM,oBAAoB,MAAM,OAAO;AAAA,IACzC;AACA,kBAAc,IAAI,MAAM,SAAS,IAAI;AAErC,WAAO,EAAE,MAAM,KAAK,MAAM,WAAW,KAAK,cAAc,KAAK;AAAA,EAC/D,SAAS,KAAK;AACZ,QAAI,eAAe,SAAS,IAAI,SAAS,cAAc;AACrD,mBAAa,0BAA0B,IAAI,OAAO,EAAE;AAAA,IACtD;AACA,WAAO;AAAA,EACT,UAAE;AACA,iBAAa,KAAK;AAAA,EACpB;AACF;AAEA,eAAe,oBAAoB,SAAgC;AACjE,MAAI;AACF,UAAM,MAAM,MAAM,OAAO,YAAY;AACrC,QAAI,OAAO,IAAI,mBAAmB,YAAY;AAC5C,YAAM,WAAW,IAAI,IAAI,OAAO,EAAE;AAClC,UAAI,eAAe,QAAQ;AAAA,IAC7B;AAAA,EACF,QAAQ;AAAA,EAER;AACF;AAEA,IAAM,aAAa,oBAAI,IAAY;AACnC,SAAS,aAAa,KAAmB;AACvC,MAAI,WAAW,IAAI,GAAG,EAAG;AACzB,aAAW,IAAI,GAAG;AAClB,UAAQ,MAAM,sBAAsB,GAAG,EAAE;AAC3C;AAEA,eAAe,UAAU,OAAgC;AACvD,QAAM,OAAO,IAAI,YAAY,EAAE,OAAO,KAAK;AAC3C,QAAM,MAAM,MAAM,WAAW,OAAO,OAAO,OAAO,WAAW,IAAI;AACjE,QAAM,QAAQ,IAAI,WAAW,GAAG;AAChC,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,WAAO,MAAM,CAAC,EAAE,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG;AAAA,EAC9C;AACA,SAAO;AACT;","names":[]}
|