gencow 0.1.156 → 0.1.157

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.
@@ -13,6 +13,7 @@ export function renderDeployHelp() {
13
13
  log(` ${BOLD}Options:${RESET}`);
14
14
  log(` ${DIM}--static [dir]${RESET} Deploy backend first, then static files`);
15
15
  log(` ${DIM}--prod${RESET} Deploy to production (Pro+ only)`);
16
+ log(` ${DIM}--region <slug>${RESET} First production app placement region`);
16
17
  log(` ${DIM}--rollback${RESET} Rollback to previous deployment`);
17
18
  log(` ${DIM}--force, -f${RESET} Skip optional dependency scan`);
18
19
  log(` ${DIM}--app, -a${RESET} Target specific app\n`);
@@ -21,7 +22,7 @@ export function renderDeployHelp() {
21
22
  log(` gencow deploy status`);
22
23
  log(` gencow deploy logs -n 100`);
23
24
  log(` gencow deploy --static dist/`);
24
- log(` gencow deploy --prod`);
25
+ log(` gencow deploy --prod --region kr`);
25
26
  log(` gencow deploy --rollback\n`);
26
27
  }
27
28
 
@@ -15,6 +15,7 @@ export function parseDeployArgs(deployArgs) {
15
15
  envTarget: "dev",
16
16
  forceDeploy: false,
17
17
  isRollback: false,
18
+ region: null,
18
19
  staticDeploy: false,
19
20
  staticDirArg: null,
20
21
  unknownArg: null,
@@ -26,7 +27,21 @@ export function parseDeployArgs(deployArgs) {
26
27
  else if (arg === "--force" || arg === "-f") parsed.forceDeploy = true;
27
28
  else if (arg === "--rollback") parsed.isRollback = true;
28
29
  else if (arg === "--app" || arg === "-a") parsed.appId = deployArgs[++index];
29
- else if (arg === "--static") {
30
+ else if (arg === "--region") {
31
+ const value = deployArgs[index + 1];
32
+ if (!value || value.startsWith("-")) {
33
+ parsed.unknownArg = arg;
34
+ break;
35
+ }
36
+ parsed.region = value;
37
+ index += 1;
38
+ } else if (arg.startsWith("--region=")) {
39
+ parsed.region = arg.slice("--region=".length) || null;
40
+ if (!parsed.region) {
41
+ parsed.unknownArg = arg;
42
+ break;
43
+ }
44
+ } else if (arg === "--static") {
30
45
  parsed.staticDeploy = true;
31
46
  const maybeDir = deployArgs[index + 1];
32
47
  if (maybeDir && !maybeDir.startsWith("-")) {
@@ -87,6 +102,7 @@ function showUnknownDeployArgument(arg, { errorImpl = error, infoImpl = info, lo
87
102
  infoImpl("Usage: gencow deploy [options]");
88
103
  infoImpl(" gencow deploy Dev backend deploy");
89
104
  infoImpl(" gencow deploy --prod Production backend deploy (Pro+)");
105
+ infoImpl(" gencow deploy --prod --region kr First production app placement");
90
106
  infoImpl(" gencow deploy --rollback Rollback to previous version");
91
107
  infoImpl(" gencow deploy logs View server logs");
92
108
  infoImpl(" gencow deploy status Check app status");
@@ -134,6 +150,7 @@ async function ensureProductionDeployTarget({
134
150
  writeFileSyncImpl = writeFileSync,
135
151
  exitImpl = (code) => process.exit(code),
136
152
  prodAppId = null,
153
+ region = null,
137
154
  }) {
138
155
  if (!appId || prodAppId) {
139
156
  return prodAppId ?? appId;
@@ -154,6 +171,7 @@ async function ensureProductionDeployTarget({
154
171
  const createProdRes = await platformFetchImpl(creds, `/platform/apps/${appId}/create-prod`, {
155
172
  method: "POST",
156
173
  headers: { "Content-Type": "application/json" },
174
+ body: JSON.stringify(region ? { region } : {}),
157
175
  });
158
176
 
159
177
  if (!createProdRes.ok) {
@@ -187,9 +205,17 @@ async function ensureProductionDeployTarget({
187
205
 
188
206
  if (createProdData.alreadyExists) {
189
207
  infoImpl(`Prod app verified: ${nextProdAppId}`);
208
+ if (region && createProdData.region && createProdData.region !== region) {
209
+ infoImpl(
210
+ `Region unchanged: existing prod app is in ${createProdData.region}; --region only applies to first prod creation.`,
211
+ );
212
+ }
190
213
  } else {
191
214
  successImpl(`Prod app created: ${nextProdAppId}`);
192
215
  infoImpl(`URL: ${createProdData.url}`);
216
+ if (createProdData.region) {
217
+ infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
218
+ }
193
219
  await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
194
220
  }
195
221
 
@@ -391,10 +417,14 @@ export function createDeployCommand({
391
417
  writeFileSyncImpl,
392
418
  exitImpl,
393
419
  prodAppId,
420
+ region: parsed.region,
394
421
  });
395
422
  if (!prodTarget) return;
396
423
  appId = prodTarget;
397
424
  infoImpl(`Deploy target: ${CYAN}${appId}${RESET} (production)`);
425
+ if (prodAppId && parsed.region) {
426
+ infoImpl("Region option only applies when the production app is first created; no app move was requested.");
427
+ }
398
428
  }
399
429
 
400
430
  if (parsed.staticDeploy) {
@@ -298,7 +298,7 @@ export function buildAiPrompt(apiObj, namespaces) {
298
298
  md += `- Existing generated components can use ai.chat(), ai.vision.extractText(), ai.image.generate(), and ai.generateObject() through the Gencow facade.\n`;
299
299
  md += `- Do not import @ai-sdk/openai/createOpenAI directly, call provider APIs directly, or create wrapper files such as openai-direct.ts.\n`;
300
300
  md += `- Do not log image base64, prompts, provider tokens, or raw private document snippets.\n`;
301
- md += `- Recommended models: gpt-5.4-mini for most apps, gpt-5.5 for highest quality, gpt-5.4-nano for high-volume extraction.\n`;
301
+ md += `- Recommended chat models: gpt-5.4-mini for most apps, gpt-5.5 for Pro/Scale highest quality, gpt-5.4-nano for high-volume extraction.\n`;
302
302
  md += `- Use ai.generateObject() with a Zod schema for structured output. Do not use ai.chat() + JSON.parse().\n`;
303
303
  md += `- Install AI with gencow add AI, RAG with gencow add RAG, Memory with gencow add Memory, and durable agents with gencow add Agent.\n`;
304
304
  md += `\`\`\`\n\n`;
@@ -307,7 +307,7 @@ export function buildAiPrompt(apiObj, namespaces) {
307
307
 
308
308
  export function buildAiUsageSection() {
309
309
  let md = `---\n\n## AI Usage\n\n`;
310
- md += `Gencow supports GPT-5.5, GPT-5.4, GPT-5 mini/nano, GPT-4.1, GPT-4o, and GPT Image models through the platform \`model_pricing\` catalog.\n`;
310
+ md += `Gencow supports a focused active chat catalog: gpt-5.5 on Pro/Scale, gpt-5.4-mini by default, gpt-5.4-nano for high volume, and temporary legacy gpt-4o-mini compatibility. Image and embedding models are also controlled by the platform \`model_pricing\` catalog.\n`;
311
311
  md += `Local development uses \`OPENAI_API_KEY\`; cloud deployments automatically use the Gencow AI proxy and service credits.\n\n`;
312
312
  md += `\`\`\`typescript\n`;
313
313
  md += `import { generateImage, generateText } from "ai";\n`;
@@ -341,10 +341,10 @@ export function buildAiUsageSection() {
341
341
  md += `\`\`\`\n\n`;
342
342
  md += `| Purpose | Recommended model |\n`;
343
343
  md += `|---|---|\n`;
344
- md += `| Highest quality / complex reasoning | \`gpt-5.5\` |\n`;
344
+ md += `| Highest quality / complex reasoning | \`gpt-5.5\` (Pro/Scale only) |\n`;
345
345
  md += `| Most production chat, coding, and agent tasks | \`gpt-5.4-mini\` |\n`;
346
346
  md += `| High-volume classification and extraction | \`gpt-5.4-nano\` |\n`;
347
- md += `| Compatibility low-cost path | \`gpt-4o-mini\` |\n`;
347
+ md += `| Temporary legacy compatibility path | \`gpt-4o-mini\` |\n`;
348
348
  md += `| Image-to-text / OCR-style extraction | \`gpt-5.4-mini\` or \`gpt-4o-mini\` |\n`;
349
349
  md += `| Image generation | \`gpt-image-2\` or \`gpt-image-1-mini\` |\n\n`;
350
350
  md += `Do not wire \`@ai-sdk/openai\` or provider keys directly. Use \`createGencowAI()\` or the \`ai.*\` facade from \`./ai\`.\n\n`;
@@ -1,8 +1,9 @@
1
- import { existsSync, readFileSync } from "fs";
1
+ import { existsSync, readFileSync, writeFileSync } from "fs";
2
2
  import { resolve } from "path";
3
3
 
4
4
  import { readProjectDisplayName } from "./cli-project-runtime.mjs";
5
- import { BOLD, CYAN, DIM, RESET, error, info, log } from "./output.mjs";
5
+ import { BOLD, CYAN, DIM, RESET, error, info, log, success } from "./output.mjs";
6
+ import { platformFetch } from "./platform-client.mjs";
6
7
  import { detectStaticDeployBackend } from "./static-deploy-command.mjs";
7
8
 
8
9
  export function parseStaticArgs(staticArgs) {
@@ -10,6 +11,7 @@ export function parseStaticArgs(staticArgs) {
10
11
  appId: null,
11
12
  forceDeploy: false,
12
13
  isProd: false,
14
+ region: null,
13
15
  staticDir: null,
14
16
  };
15
17
 
@@ -17,6 +19,8 @@ export function parseStaticArgs(staticArgs) {
17
19
  const arg = staticArgs[index];
18
20
  if (arg === "--force" || arg === "-f") parsed.forceDeploy = true;
19
21
  else if (arg === "--prod") parsed.isProd = true;
22
+ else if (arg === "--region") parsed.region = staticArgs[++index] || null;
23
+ else if (arg.startsWith("--region=")) parsed.region = arg.slice("--region=".length) || null;
20
24
  else if (arg === "--app" || arg === "-a") parsed.appId = staticArgs[++index];
21
25
  else if (!arg.startsWith("-")) parsed.staticDir = arg;
22
26
  }
@@ -62,6 +66,57 @@ export function readStaticProjectContext(options = {}) {
62
66
  };
63
67
  }
64
68
 
69
+ async function ensureStaticProductionTarget({
70
+ appId,
71
+ creds,
72
+ gencowJsonPath,
73
+ platformFetchImpl = platformFetch,
74
+ readFileSyncImpl = readFileSync,
75
+ region = null,
76
+ setTimeoutImpl = setTimeout,
77
+ successImpl = success,
78
+ errorImpl = error,
79
+ infoImpl = info,
80
+ writeFileSyncImpl = writeFileSync,
81
+ }) {
82
+ if (!appId) {
83
+ errorImpl("App not found. Run from the project root with gencow.json.");
84
+ return null;
85
+ }
86
+
87
+ infoImpl("Creating prod app...");
88
+ const createProdRes = await platformFetchImpl(creds, `/platform/apps/${appId}/create-prod`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify(region ? { region } : {}),
92
+ });
93
+
94
+ if (!createProdRes.ok) {
95
+ const errData = await createProdRes.json().catch(() => ({}));
96
+ errorImpl(`Prod app creation failed: ${errData.error || createProdRes.statusText}`);
97
+ return null;
98
+ }
99
+
100
+ const createProdData = await createProdRes.json();
101
+ const nextProdAppId = createProdData.prodApp;
102
+ const gencowJson = existsSync(gencowJsonPath) ? JSON.parse(readFileSyncImpl(gencowJsonPath, "utf8")) : {};
103
+ gencowJson.prodApp = nextProdAppId;
104
+ writeFileSyncImpl(gencowJsonPath, JSON.stringify(gencowJson, null, 2));
105
+
106
+ if (createProdData.alreadyExists) {
107
+ infoImpl(`Prod app verified: ${nextProdAppId}`);
108
+ } else {
109
+ successImpl(`Prod app created: ${nextProdAppId}`);
110
+ if (createProdData.url) infoImpl(`URL: ${createProdData.url}`);
111
+ if (createProdData.region) {
112
+ infoImpl(`Region: ${createProdData.region} (${createProdData.regionSource || "selected"})`);
113
+ }
114
+ await new Promise((resolveDelay) => setTimeoutImpl(resolveDelay, 3000));
115
+ }
116
+
117
+ return nextProdAppId;
118
+ }
119
+
65
120
  export function createStaticCommand(options = {}) {
66
121
  const {
67
122
  BOLDValue = BOLD,
@@ -72,9 +127,14 @@ export function createStaticCommand(options = {}) {
72
127
  errorImpl = error,
73
128
  infoImpl = info,
74
129
  logImpl = log,
130
+ platformFetchImpl = platformFetch,
131
+ readFileSyncImpl = readFileSync,
75
132
  readStaticProjectContextImpl = readStaticProjectContext,
76
133
  requireCredsImpl,
77
134
  runStaticDeployRuntimeImpl,
135
+ setTimeoutImpl = setTimeout,
136
+ successImpl = success,
137
+ writeFileSyncImpl = writeFileSync,
78
138
  } = options;
79
139
 
80
140
  return async function runStaticCommand(...staticArgs) {
@@ -87,6 +147,7 @@ export function createStaticCommand(options = {}) {
87
147
  );
88
148
  logImpl(` ${BOLDValue}Options:${RESETValue}`);
89
149
  logImpl(` ${DIMValue}--prod${RESETValue} Deploy to production app`);
150
+ logImpl(` ${DIMValue}--region <slug>${RESETValue} First production app placement region`);
90
151
  logImpl(` ${DIMValue}--force, -f${RESETValue} Skip optional dependency scan`);
91
152
  logImpl(` ${DIMValue}--app, -a${RESETValue} Target specific app\n`);
92
153
  logImpl(` ${BOLDValue}Examples:${RESETValue}`);
@@ -104,13 +165,26 @@ export function createStaticCommand(options = {}) {
104
165
  cwd: cwdImpl(),
105
166
  explicitAppId: parsed.appId,
106
167
  });
107
- const deployTarget = parsed.isProd ? prodAppId || appId : appId;
168
+ let deployTarget = parsed.isProd ? prodAppId || appId : appId;
108
169
  const envTarget = parsed.isProd ? "prod" : "dev";
109
170
 
110
171
  if (parsed.isProd && !prodAppId) {
111
- errorImpl("No production app found. Run 'gencow deploy' first to create one.");
112
- infoImpl(`${DIMValue}For dev static deploy, omit --prod: gencow static [dir]${RESETValue}\n`);
113
- return;
172
+ deployTarget = await ensureStaticProductionTarget({
173
+ appId,
174
+ creds,
175
+ gencowJsonPath,
176
+ platformFetchImpl,
177
+ readFileSyncImpl,
178
+ region: parsed.region,
179
+ setTimeoutImpl,
180
+ successImpl,
181
+ errorImpl,
182
+ infoImpl,
183
+ writeFileSyncImpl,
184
+ });
185
+ if (!deployTarget) return;
186
+ } else if (parsed.isProd && parsed.region) {
187
+ infoImpl("Region option only applies when the production app is first created; no app move was requested.");
114
188
  }
115
189
 
116
190
  return runStaticDeployRuntimeImpl({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gencow",
3
- "version": "0.1.156",
3
+ "version": "0.1.157",
4
4
  "description": "Gencow — AI Backend Engine",
5
5
  "type": "module",
6
6
  "bin": {
package/server/index.js CHANGED
@@ -63151,7 +63151,7 @@ var require_html_writer = __commonJS({
63151
63151
  }
63152
63152
  function generateAttributeString(attributes) {
63153
63153
  return _3.map(attributes, function(value, key) {
63154
- return " " + key + '="' + escapeHtmlAttribute(value) + '"';
63154
+ return " " + key + '="' + escapeHtmlAttribute2(value) + '"';
63155
63155
  }).join("");
63156
63156
  }
63157
63157
  function text2(value) {
@@ -63175,7 +63175,7 @@ var require_html_writer = __commonJS({
63175
63175
  function escapeHtmlText(value) {
63176
63176
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
63177
63177
  }
63178
- function escapeHtmlAttribute(value) {
63178
+ function escapeHtmlAttribute2(value) {
63179
63179
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
63180
63180
  }
63181
63181
  }
@@ -93267,7 +93267,9 @@ async function ensurePlatformSchemaUpgrade(params) {
93267
93267
  await params.rawSql(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS status_reason TEXT`);
93268
93268
  await params.rawSql(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS status_message TEXT`);
93269
93269
  await params.rawSql(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS status_updated_at TIMESTAMP`);
93270
- await params.rawSql(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS memory_oom_count INTEGER NOT NULL DEFAULT 0`);
93270
+ await params.rawSql(
93271
+ `ALTER TABLE apps ADD COLUMN IF NOT EXISTS memory_oom_count INTEGER NOT NULL DEFAULT 0`
93272
+ );
93271
93273
  await params.rawSql(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS memory_oom_window_started_at TIMESTAMP`);
93272
93274
  await params.rawSql(`ALTER TABLE apps ADD COLUMN IF NOT EXISTS memory_oom_last_at TIMESTAMP`);
93273
93275
  await params.rawSql(`ALTER TABLE deployments ADD COLUMN IF NOT EXISTS env TEXT DEFAULT 'dev'`);
@@ -93555,20 +93557,20 @@ async function ensurePlatformSchemaUpgrade(params) {
93555
93557
  await params.rawSql(`INSERT INTO model_pricing (model, type, provider, display_name, unit_label, input_credit_per_token, output_credit_per_token, active, sort_order)
93556
93558
  VALUES
93557
93559
  ('gpt-5.5', 'chat', 'openai', 'GPT-5.5', '1K tok', 50, 300, true, 0),
93558
- ('gpt-5.4', 'chat', 'openai', 'GPT-5.4', '1K tok', 25, 150, true, 10),
93560
+ ('gpt-5.4', 'chat', 'openai', 'GPT-5.4', '1K tok', 25, 150, false, 10),
93559
93561
  ('gpt-5.4-mini', 'chat', 'openai', 'GPT-5.4 mini', '1K tok', 7.5, 45, true, 20),
93560
93562
  ('gpt-5.4-nano', 'chat', 'openai', 'GPT-5.4 nano', '1K tok', 2, 12.5, true, 30),
93561
- ('gpt-5.3-chat-latest', 'chat', 'openai', 'GPT-5.3 Chat', '1K tok', 17.5, 140, true, 40),
93562
- ('gpt-5.2', 'chat', 'openai', 'GPT-5.2', '1K tok', 17.5, 140, true, 50),
93563
- ('gpt-5.2-chat-latest', 'chat', 'openai', 'GPT-5.2 Chat', '1K tok', 17.5, 140, true, 60),
93564
- ('gpt-5.1', 'chat', 'openai', 'GPT-5.1', '1K tok', 12.5, 100, true, 70),
93565
- ('gpt-5', 'chat', 'openai', 'GPT-5', '1K tok', 12.5, 100, true, 80),
93566
- ('gpt-5-mini', 'chat', 'openai', 'GPT-5 mini', '1K tok', 2.5, 20, true, 90),
93567
- ('gpt-5-nano', 'chat', 'openai', 'GPT-5 nano', '1K tok', 0.5, 4, true, 100),
93568
- ('gpt-4.1', 'chat', 'openai', 'GPT-4.1', '1K tok', 20, 80, true, 110),
93569
- ('gpt-4.1-mini', 'chat', 'openai', 'GPT-4.1 mini', '1K tok', 4, 16, true, 120),
93570
- ('gpt-4.1-nano', 'chat', 'openai', 'GPT-4.1 nano', '1K tok', 1, 4, true, 130),
93571
- ('gpt-4o', 'chat', 'openai', 'GPT-4o', '1K tok', 25, 100, true, 140),
93563
+ ('gpt-5.3-chat-latest', 'chat', 'openai', 'GPT-5.3 Chat', '1K tok', 17.5, 140, false, 40),
93564
+ ('gpt-5.2', 'chat', 'openai', 'GPT-5.2', '1K tok', 17.5, 140, false, 50),
93565
+ ('gpt-5.2-chat-latest', 'chat', 'openai', 'GPT-5.2 Chat', '1K tok', 17.5, 140, false, 60),
93566
+ ('gpt-5.1', 'chat', 'openai', 'GPT-5.1', '1K tok', 12.5, 100, false, 70),
93567
+ ('gpt-5', 'chat', 'openai', 'GPT-5', '1K tok', 12.5, 100, false, 80),
93568
+ ('gpt-5-mini', 'chat', 'openai', 'GPT-5 mini', '1K tok', 2.5, 20, false, 90),
93569
+ ('gpt-5-nano', 'chat', 'openai', 'GPT-5 nano', '1K tok', 0.5, 4, false, 100),
93570
+ ('gpt-4.1', 'chat', 'openai', 'GPT-4.1', '1K tok', 20, 80, false, 110),
93571
+ ('gpt-4.1-mini', 'chat', 'openai', 'GPT-4.1 mini', '1K tok', 4, 16, false, 120),
93572
+ ('gpt-4.1-nano', 'chat', 'openai', 'GPT-4.1 nano', '1K tok', 1, 4, false, 130),
93573
+ ('gpt-4o', 'chat', 'openai', 'GPT-4o', '1K tok', 25, 100, false, 140),
93572
93574
  ('gpt-4o-mini', 'chat', 'openai', 'GPT-4o mini', '1K tok', 1.5, 6, true, 150),
93573
93575
  ('gpt-image-2', 'image', 'openai', 'GPT Image 2', '1K tok', 50, 300, true, 180),
93574
93576
  ('gpt-image-1.5', 'image', 'openai', 'GPT Image 1.5', '1K tok', 50, 320, true, 190),
@@ -94043,6 +94045,19 @@ var compressibleStaticExtensions = /* @__PURE__ */ new Set([
94043
94045
  var encodedFileCache = /* @__PURE__ */ new Map();
94044
94046
  var encodedFileCacheTtlMs = 10 * 60 * 1e3;
94045
94047
  var encodedFileCacheMaxEntries = 240;
94048
+ function injectHtmlHeadTag(html2, tag, skipMarker) {
94049
+ if (!tag || html2.includes(skipMarker)) return html2;
94050
+ const headClose = /<\/head\s*>/i;
94051
+ if (headClose.test(html2)) return html2.replace(headClose, ` ${tag}
94052
+ </head>`);
94053
+ const htmlOpen = /<html(\s[^>]*)?>/i;
94054
+ if (htmlOpen.test(html2)) return html2.replace(htmlOpen, (match2) => `${match2}
94055
+ <head>
94056
+ ${tag}
94057
+ </head>`);
94058
+ return `${tag}
94059
+ ${html2}`;
94060
+ }
94046
94061
  function acceptsEncoding(value, encoding) {
94047
94062
  const tokens = value.toLowerCase().split(",").map((part) => part.trim());
94048
94063
  return tokens.some((token) => {
@@ -94085,6 +94100,16 @@ async function encodedStaticFile(filePath, encoding, originalSize) {
94085
94100
  encodedFileCache.set(cacheKey, { body: output, createdAt: Date.now() });
94086
94101
  return output;
94087
94102
  }
94103
+ function encodeStaticBuffer(input, encoding) {
94104
+ if (!encoding || input.byteLength <= 0) return { body: input, encoding: null };
94105
+ const output = encoding === "br" ? brotliCompressSync(input) : gzipSync(input);
94106
+ if (output.byteLength >= input.byteLength) return { body: input, encoding: null };
94107
+ return { body: output, encoding };
94108
+ }
94109
+ function bufferToBlob(buffer) {
94110
+ const arrayBuffer = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
94111
+ return new Blob([arrayBuffer]);
94112
+ }
94088
94113
  async function serveStaticFile(params) {
94089
94114
  const staticDir = join2(params.dataDir, "static");
94090
94115
  const resolution = await resolveStaticFileRequest({
@@ -94100,15 +94125,34 @@ async function serveStaticFile(params) {
94100
94125
  const fileSize = file.size;
94101
94126
  const requestMethod = String(params.c?.req?.method ?? "").toUpperCase();
94102
94127
  const encoding = preferredStaticEncoding(requestHeader(params.c, "Accept-Encoding"), resolution.filePath);
94103
- const encodedBody = encoding && Number.isSafeInteger(fileSize) && fileSize > 0 ? await encodedStaticFile(resolution.filePath, encoding, fileSize) : null;
94104
- const responseBody = encodedBody ? new Uint8Array(encodedBody) : file;
94105
- const responseBytes = encodedBody?.byteLength ?? fileSize;
94128
+ const isHtml = [".html", ".htm"].includes(extname2(resolution.filePath).toLowerCase());
94129
+ const shouldInjectHtml = Boolean(params.htmlHeadInjection && isHtml);
94130
+ let responseBody;
94131
+ let responseBytes = fileSize;
94132
+ let responseEncoding = null;
94133
+ if (shouldInjectHtml) {
94134
+ const originalHtml = await file.text();
94135
+ const html2 = injectHtmlHeadTag(
94136
+ originalHtml,
94137
+ params.htmlHeadInjection ?? "",
94138
+ params.htmlHeadInjectionSkipMarker ?? params.htmlHeadInjection ?? ""
94139
+ );
94140
+ const encoded = encodeStaticBuffer(Buffer.from(html2), encoding);
94141
+ responseBody = bufferToBlob(encoded.body);
94142
+ responseBytes = encoded.body.byteLength;
94143
+ responseEncoding = encoded.encoding;
94144
+ } else {
94145
+ const encodedBody = encoding && Number.isSafeInteger(fileSize) && fileSize > 0 ? await encodedStaticFile(resolution.filePath, encoding, fileSize) : null;
94146
+ responseBody = encodedBody ? bufferToBlob(encodedBody) : file;
94147
+ responseBytes = encodedBody?.byteLength ?? fileSize;
94148
+ responseEncoding = encodedBody ? encoding : null;
94149
+ }
94106
94150
  if (requestMethod !== "HEAD" && Number.isSafeInteger(responseBytes) && responseBytes > 0) {
94107
94151
  params.recordBytes?.(responseBytes);
94108
94152
  }
94109
94153
  const headers = new Headers({ "Cache-Control": resolution.cacheControl });
94110
- if (encodedBody && encoding) {
94111
- headers.set("Content-Encoding", encoding);
94154
+ if (responseEncoding) {
94155
+ headers.set("Content-Encoding", responseEncoding);
94112
94156
  headers.set("Vary", "Accept-Encoding");
94113
94157
  }
94114
94158
  if (file.type) headers.set("Content-Type", file.type);
@@ -94478,6 +94522,15 @@ function createServerNodeProxyLoopbackRequest(params) {
94478
94522
 
94479
94523
  // ../server/src/server-platform-app-proxy.ts
94480
94524
  var INTERNAL_DOMAIN_VERIFY_PATH = "/internal/domain/verify";
94525
+ var HOSTED_ANALYTICS_SCRIPT_PATH = "/_gencow/analytics.js";
94526
+ function escapeHtmlAttribute(value) {
94527
+ return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
94528
+ }
94529
+ function buildHostedAnalyticsScriptTag(appRow) {
94530
+ const deploy = appRow.currentDeployId === null || appRow.currentDeployId === void 0 ? "" : String(appRow.currentDeployId);
94531
+ const deployAttr = deploy ? ` data-deploy="${escapeHtmlAttribute(deploy)}"` : "";
94532
+ return `<script defer src="${HOSTED_ANALYTICS_SCRIPT_PATH}" data-site="app:${escapeHtmlAttribute(String(appRow.id))}"${deployAttr}></script>`;
94533
+ }
94481
94534
  function buildRuntimeUnavailableBody(result2) {
94482
94535
  return {
94483
94536
  error: result2.error,
@@ -94590,6 +94643,7 @@ async function maybeServeHostedAnalytics(params) {
94590
94643
  app: params.appRow,
94591
94644
  host: params.host,
94592
94645
  rawSql: params.rawSql,
94646
+ analyticsRawSql: params.analyticsRawSql ?? params.rawSql,
94593
94647
  logger: params.logger
94594
94648
  });
94595
94649
  }
@@ -94736,6 +94790,7 @@ function registerPlatformAppProxy(params) {
94736
94790
  c,
94737
94791
  functionsPath: params.functionsPath,
94738
94792
  rawSql: params.rawSql,
94793
+ analyticsRawSql: params.analyticsRawSql ?? params.rawSql,
94739
94794
  logger: logger2
94740
94795
  });
94741
94796
  if (analyticsRes) {
@@ -94785,7 +94840,9 @@ function registerPlatformAppProxy(params) {
94785
94840
  dataDir: appRow.dataDir,
94786
94841
  pathname,
94787
94842
  appName,
94788
- recordBytes: (bytes) => params.addProxyMetric(appName, bytes)
94843
+ recordBytes: (bytes) => params.addProxyMetric(appName, bytes),
94844
+ htmlHeadInjection: buildHostedAnalyticsScriptTag(appRow),
94845
+ htmlHeadInjectionSkipMarker: HOSTED_ANALYTICS_SCRIPT_PATH
94789
94846
  });
94790
94847
  }
94791
94848
  logger2.warn(
@@ -94877,7 +94934,9 @@ function registerPlatformAppProxy(params) {
94877
94934
  dataDir: appRow.dataDir,
94878
94935
  pathname,
94879
94936
  appName,
94880
- recordBytes: (bytes) => params.addProxyMetric(appName, bytes)
94937
+ recordBytes: (bytes) => params.addProxyMetric(appName, bytes),
94938
+ htmlHeadInjection: buildHostedAnalyticsScriptTag(appRow),
94939
+ htmlHeadInjectionSkipMarker: HOSTED_ANALYTICS_SCRIPT_PATH
94881
94940
  });
94882
94941
  }
94883
94942
  if (hasBackend) {
@@ -98207,6 +98266,7 @@ async function bootstrapPlatformRuntime(params) {
98207
98266
  },
98208
98267
  generateInternalToken,
98209
98268
  rawSql: params.rawSql,
98269
+ analyticsRawSql: params.analyticsRawSql ?? params.rawSql,
98210
98270
  functionsPath: params.functionsPath,
98211
98271
  addProxyMetric,
98212
98272
  createProxyRequestReplayPlan,
@@ -103654,6 +103714,7 @@ function createRequestContextBuilder(params) {
103654
103714
  db: createRlsDbImpl(params.db, refreshRls),
103655
103715
  unsafeDb: params.db,
103656
103716
  rawSql: params.rawSql,
103717
+ analyticsRawSql: params.analyticsRawSql ?? params.rawSql,
103657
103718
  auth: refreshAuth,
103658
103719
  storage: params.storage,
103659
103720
  realtime: noopRealtime,
@@ -103721,6 +103782,7 @@ function createRequestContextBuilder(params) {
103721
103782
  );
103722
103783
  partialCtx.unsafeDb = params.db;
103723
103784
  partialCtx.rawSql = params.rawSql;
103785
+ partialCtx.analyticsRawSql = params.analyticsRawSql ?? params.rawSql;
103724
103786
  return partialCtx;
103725
103787
  };
103726
103788
  }
@@ -110081,6 +110143,7 @@ async function bootstrapServerMainRuntime(params) {
110081
110143
  const buildCtx = createRequestContextBuilder({
110082
110144
  db: params.db,
110083
110145
  rawSql: params.rawSql,
110146
+ analyticsRawSql: params.analyticsRawSql ?? params.rawSql,
110084
110147
  storage: params.storage,
110085
110148
  scheduler,
110086
110149
  isBaas: params.isBaas,
@@ -110405,6 +110468,76 @@ async function bootstrapServerMainRuntime(params) {
110405
110468
  return server;
110406
110469
  }
110407
110470
 
110471
+ // ../server/src/server-analytics-db.ts
110472
+ function isDisabledAnalyticsEnv(env2) {
110473
+ const enabled = env2.GENCOW_ANALYTICS_ENABLED?.trim().toLowerCase();
110474
+ const backend = env2.GENCOW_ANALYTICS_BACKEND?.trim().toLowerCase();
110475
+ return ["0", "false", "off", "disabled", "no"].includes(enabled ?? "") || backend === "disabled";
110476
+ }
110477
+ function redactPostgresDsn(value) {
110478
+ return value.replace(/postgres(?:ql)?:\/\/[^\s"']+/gi, "postgres://[redacted]");
110479
+ }
110480
+ async function connectPostgresAnalytics(url) {
110481
+ const mod6 = await import("postgres");
110482
+ return mod6.default(url, {
110483
+ max: Number(process.env.GENCOW_ANALYTICS_DB_POOL_MAX || 3),
110484
+ idle_timeout: Number(process.env.GENCOW_ANALYTICS_DB_IDLE_TIMEOUT_SEC || 20),
110485
+ connect_timeout: Number(process.env.GENCOW_ANALYTICS_DB_CONNECT_TIMEOUT_SEC || 5)
110486
+ });
110487
+ }
110488
+ function createUnavailableRawSql(reason) {
110489
+ return (async () => {
110490
+ throw new Error(`analytics database unavailable: ${redactPostgresDsn(reason).slice(0, 160)}`);
110491
+ });
110492
+ }
110493
+ async function createAnalyticsSqlRuntime(params) {
110494
+ const env2 = params.env ?? process.env;
110495
+ const logger2 = params.logger ?? console;
110496
+ const analyticsDatabaseUrl = env2.ANALYTICS_DATABASE_URL?.trim();
110497
+ if (!analyticsDatabaseUrl || isDisabledAnalyticsEnv(env2)) {
110498
+ return {
110499
+ rawSql: params.fallbackRawSql,
110500
+ source: "platform-fallback",
110501
+ close: async () => {
110502
+ }
110503
+ };
110504
+ }
110505
+ const connect = params.connect ?? connectPostgresAnalytics;
110506
+ const ensureSchema = params.ensureSchema ?? ensurePlatformAnalyticsSchema;
110507
+ let client = null;
110508
+ try {
110509
+ client = await connect(analyticsDatabaseUrl);
110510
+ const rawSql = (async (query2, sqlParams) => {
110511
+ const result2 = sqlParams && sqlParams.length > 0 ? await client.unsafe(query2, sqlParams) : await client.unsafe(query2);
110512
+ return Array.from(result2);
110513
+ });
110514
+ await ensureSchema({ rawSql });
110515
+ logger2.log("[analytics] using dedicated Postgres analytics store (ANALYTICS_DATABASE_URL configured)");
110516
+ return {
110517
+ rawSql,
110518
+ source: "dedicated-postgres",
110519
+ close: async () => {
110520
+ await client?.end?.();
110521
+ }
110522
+ };
110523
+ } catch (error) {
110524
+ const message = error instanceof Error ? error.message : String(error);
110525
+ try {
110526
+ await client?.end?.();
110527
+ } catch {
110528
+ }
110529
+ logger2.warn(
110530
+ `[analytics] dedicated analytics DB unavailable; collector/provider will fail open: ${redactPostgresDsn(message).slice(0, 160)}`
110531
+ );
110532
+ return {
110533
+ rawSql: createUnavailableRawSql(message),
110534
+ source: "unavailable",
110535
+ close: async () => {
110536
+ }
110537
+ };
110538
+ }
110539
+ }
110540
+
110408
110541
  // ../server/src/server-websocket-runtime.ts
110409
110542
  function createServerWebsocketRuntime() {
110410
110543
  const { upgradeWebSocket: upgradeWebSocket3, websocket: baseWebsocket } = createBunWebSocket();
@@ -113066,6 +113199,8 @@ function createSettingsRoutes(rawSql, options) {
113066
113199
  var SYSTEM_ENV_KEYS = [
113067
113200
  "DATABASE_URL",
113068
113201
  // DB 비밀번호 포함 → 유출 시 다른 테넌트 접근 가능
113202
+ "ANALYTICS_DATABASE_URL",
113203
+ // Analytics 전용 DB 비밀번호 포함 → 서버 내부 store만 사용
113069
113204
  "GENCOW_WAKE_SIGNAL_DATABASE_URL",
113070
113205
  // LISTEN 전용 DB URL → DB 비밀번호 포함
113071
113206
  "GENCOW_REALTIME_OUTBOX_POLL_INTERVAL_MS",
@@ -113697,6 +113832,10 @@ async function main() {
113697
113832
  if (loadedCount > 0) {
113698
113833
  console.log(`[settings] Loaded ${loadedCount} env var(s) from DB \u2713`);
113699
113834
  }
113835
+ const analyticsSqlRuntime = await createAnalyticsSqlRuntime({
113836
+ fallbackRawSql: systemRawSql,
113837
+ logger: console
113838
+ });
113700
113839
  const platformEmailEnv = {
113701
113840
  RESEND_API_KEY: process.env.RESEND_API_KEY,
113702
113841
  PLATFORM_EMAIL_FROM: process.env.PLATFORM_EMAIL_FROM,
@@ -113737,6 +113876,7 @@ async function main() {
113737
113876
  app,
113738
113877
  db: platformSystemDb,
113739
113878
  rawSql: systemRawSql,
113879
+ analyticsRawSql: analyticsSqlRuntime.rawSql,
113740
113880
  functionsPath,
113741
113881
  platformDomain: PLATFORM_DOMAIN,
113742
113882
  getBunServer: () => bunServer,
@@ -113760,7 +113900,11 @@ async function main() {
113760
113900
  db,
113761
113901
  platformSystemDb,
113762
113902
  rawSql,
113763
- close,
113903
+ analyticsRawSql: analyticsSqlRuntime.rawSql,
113904
+ close: async () => {
113905
+ await analyticsSqlRuntime.close();
113906
+ await close();
113907
+ },
113764
113908
  driver,
113765
113909
  storage,
113766
113910
  storageDir,