@zveltio/cli 3.0.0-beta.5 → 3.0.0-beta.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1206 -255
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -861,7 +861,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
861
861
  this._exitCallback = (err) => {
862
862
  if (err.code !== "commander.executeSubCommandAsync") {
863
863
  throw err;
864
- } else {}
864
+ }
865
865
  };
866
866
  }
867
867
  return this;
@@ -1888,7 +1888,7 @@ var require_commander = __commonJS((exports) => {
1888
1888
 
1889
1889
  // ../sdk/dist/build/index.js
1890
1890
  import { createRequire } from "module";
1891
- import { existsSync as existsSync5, readFileSync } from "fs";
1891
+ import { existsSync as existsSync6, readFileSync } from "fs";
1892
1892
  import { dirname, join as join5 } from "path";
1893
1893
  function escapeRegExp(s) {
1894
1894
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -1913,7 +1913,7 @@ function resolveSearchPaths(resolveFromDir) {
1913
1913
  for (let i = 0;i < 6; i++) {
1914
1914
  cur = dirname(cur);
1915
1915
  paths.push(cur);
1916
- if (existsSync5(join5(cur, "node_modules")))
1916
+ if (existsSync6(join5(cur, "node_modules")))
1917
1917
  break;
1918
1918
  }
1919
1919
  return [...new Set(paths)];
@@ -1927,7 +1927,7 @@ function resolvePackageRoot(pkg, searchPaths) {
1927
1927
  }
1928
1928
  for (;; ) {
1929
1929
  const pkgJsonPath = join5(candidate, "package.json");
1930
- if (existsSync5(pkgJsonPath)) {
1930
+ if (existsSync6(pkgJsonPath)) {
1931
1931
  const manifest = JSON.parse(readFileSync(pkgJsonPath, "utf8"));
1932
1932
  if (manifest.name === pkg)
1933
1933
  return candidate;
@@ -1951,7 +1951,7 @@ function resolvePackageEsmEntry(pkg, resolveFromDir) {
1951
1951
  throw new Error(`Cannot determine ESM entry for package "${pkg}"`);
1952
1952
  }
1953
1953
  const abs = join5(root, rel.replace(/^\.\//, ""));
1954
- if (!existsSync5(abs)) {
1954
+ if (!existsSync6(abs)) {
1955
1955
  throw new Error(`ESM entry missing for "${pkg}": ${abs}`);
1956
1956
  }
1957
1957
  if (abs.endsWith(".d.ts")) {
@@ -1997,7 +1997,7 @@ var init_build = __esm(() => {
1997
1997
  });
1998
1998
 
1999
1999
  // src/lib/extension-bundle.ts
2000
- import { existsSync as existsSync6, renameSync } from "fs";
2000
+ import { existsSync as existsSync7, renameSync } from "fs";
2001
2001
  import { basename, dirname as dirname2, join as join6 } from "path";
2002
2002
  async function bundleExtensionEngine(opts) {
2003
2003
  const outdir = dirname2(opts.outfile);
@@ -2018,7 +2018,7 @@ async function bundleExtensionEngine(opts) {
2018
2018
  `);
2019
2019
  throw new Error(messages || "Bun.build failed");
2020
2020
  }
2021
- if (!existsSync6(builtPath)) {
2021
+ if (!existsSync7(builtPath)) {
2022
2022
  throw new Error(`Bun.build succeeded but ${builtPath} was not produced`);
2023
2023
  }
2024
2024
  if (builtPath !== opts.outfile) {
@@ -2031,6 +2031,18 @@ var init_extension_bundle = __esm(() => {
2031
2031
  EXTENSION_BUNDLE_CORE_DEPS2 = EXTENSION_BUNDLE_CORE_DEPS;
2032
2032
  });
2033
2033
 
2034
+ // src/lib/pack-isolation.ts
2035
+ function resolvePackIsolation(opts) {
2036
+ let resolved = opts.current;
2037
+ if (opts.firstParty && resolved === "worker" && !opts.keepIsolation) {
2038
+ resolved = undefined;
2039
+ }
2040
+ if (!resolved && opts.communityInject && !opts.firstParty) {
2041
+ resolved = "worker";
2042
+ }
2043
+ return resolved;
2044
+ }
2045
+
2034
2046
  // src/lib/publisher-tier.ts
2035
2047
  function tierAllowsInline(tier) {
2036
2048
  return tier === "first-party" || tier === "verified";
@@ -2069,14 +2081,108 @@ function normalizeTier(v) {
2069
2081
  // src/commands/extension-pack.ts
2070
2082
  var exports_extension_pack = {};
2071
2083
  __export(exports_extension_pack, {
2084
+ recordBundledVersions: () => recordBundledVersions,
2085
+ hashEngineSources: () => hashEngineSources,
2072
2086
  extensionPackCommand: () => extensionPackCommand
2073
2087
  });
2074
2088
  import { createHash } from "crypto";
2075
- import { existsSync as existsSync7, readFileSync as readFileSync2, writeFileSync } from "fs";
2076
- import { join as join7, resolve } from "path";
2089
+ import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync2, writeFileSync } from "fs";
2090
+ import { homedir } from "os";
2091
+ import { join as join7, relative, resolve } from "path";
2092
+ function scrubBuildPaths(outfile, dir) {
2093
+ const original = readFileSync2(outfile, "utf8");
2094
+ let count = 0;
2095
+ let text = original.replace(/(__dirname|__filename)\s*=\s*"(\/[^"]*)"/g, (_match, name, abs) => {
2096
+ count += 1;
2097
+ const at = abs.lastIndexOf("node_modules/");
2098
+ const tail = at === -1 ? abs.slice(abs.lastIndexOf("/") + 1) : abs.slice(at);
2099
+ return `${name} = "/zveltio-extension/${tail}"`;
2100
+ });
2101
+ text = text.replace(/\.{1,2}\/[^\s"'`]*(?:node_modules|packages)\/[^\s"'`]*/g, (match) => {
2102
+ const store = match.indexOf(".bun/");
2103
+ const at = store === -1 ? Math.min(...[match.lastIndexOf("node_modules/"), match.indexOf("packages/")].filter((i) => i >= 0)) : store;
2104
+ count += 1;
2105
+ return `/zveltio-extension/${match.slice(at)}`;
2106
+ });
2107
+ text = text.replace(/(?<=^\/\/\s*)node_modules\//gm, "/zveltio-extension/node_modules/");
2108
+ const prefix = relative(process.cwd(), dir).replace(/\\/g, "/");
2109
+ if (prefix && !prefix.startsWith("..")) {
2110
+ const re = new RegExp(`(?<=^//\\s*)${prefix.replace(/[.*+?^${}()|[\\]]/g, "\\$&")}/`, "gm");
2111
+ const before = text;
2112
+ text = text.replace(re, "");
2113
+ if (text !== before)
2114
+ count += 1;
2115
+ }
2116
+ const home = homedir();
2117
+ if (home && home.startsWith("/") && home.length > 1 && text.includes(home)) {
2118
+ count += text.split(home).length - 1;
2119
+ text = text.split(home).join("/zveltio-extension");
2120
+ }
2121
+ if (count > 0)
2122
+ writeFileSync(outfile, text, "utf8");
2123
+ return count;
2124
+ }
2125
+ function recordBundledVersions(outfile, dir) {
2126
+ let text = readFileSync2(outfile, "utf8");
2127
+ const recorded = [];
2128
+ for (const dep of CORE_DEPS) {
2129
+ if (!text.includes(`node_modules/${dep}/`) || text.includes(`.bun/${dep}@`))
2130
+ continue;
2131
+ for (let at = resolve(dir);; at = resolve(at, "..")) {
2132
+ const pkg = join7(at, "node_modules", dep, "package.json");
2133
+ if (existsSync8(pkg)) {
2134
+ const { version } = JSON.parse(readFileSync2(pkg, "utf8"));
2135
+ if (version)
2136
+ recorded.push(`${dep}@${version}`);
2137
+ break;
2138
+ }
2139
+ if (at === resolve(at, ".."))
2140
+ break;
2141
+ }
2142
+ }
2143
+ if (recorded.length === 0)
2144
+ return recorded;
2145
+ text = text.replace(/\n*$/, `
2146
+ `);
2147
+ text += recorded.map((r) => `// @zveltio-bundled ${r}
2148
+ `).join("");
2149
+ writeFileSync(outfile, text, "utf8");
2150
+ return recorded;
2151
+ }
2152
+ function hashEngineSources(dir) {
2153
+ const engineDir = join7(dir, "engine");
2154
+ const files = [];
2155
+ const walk = (d) => {
2156
+ for (const entry of readdirSync(d, { withFileTypes: true })) {
2157
+ const full = join7(d, entry.name);
2158
+ if (entry.isDirectory()) {
2159
+ if (entry.name === "migrations" || entry.name === "node_modules")
2160
+ continue;
2161
+ walk(full);
2162
+ continue;
2163
+ }
2164
+ if (!/\.(ts|tsx|js|mjs|json)$/.test(entry.name))
2165
+ continue;
2166
+ if (/\.(test|spec)\.[a-z]+$/.test(entry.name))
2167
+ continue;
2168
+ if (entry.name === "index.js")
2169
+ continue;
2170
+ files.push(full);
2171
+ }
2172
+ };
2173
+ walk(engineDir);
2174
+ const h = createHash("sha256");
2175
+ for (const f of files.sort()) {
2176
+ h.update(f.slice(engineDir.length).replace(/\\/g, "/"));
2177
+ h.update("\x00");
2178
+ h.update(readFileSync2(f));
2179
+ h.update("\x00");
2180
+ }
2181
+ return h.digest("hex");
2182
+ }
2077
2183
  function readManifest(dir) {
2078
2184
  const path = join7(dir, "manifest.json");
2079
- if (!existsSync7(path)) {
2185
+ if (!existsSync8(path)) {
2080
2186
  throw new Error(`No manifest.json at ${path}.`);
2081
2187
  }
2082
2188
  return JSON.parse(readFileSync2(path, "utf8"));
@@ -2099,7 +2205,7 @@ async function extensionPackCommand(opts) {
2099
2205
  const dir = resolve(opts.dir ?? process.cwd());
2100
2206
  const entry = join7(dir, "engine", "index.ts");
2101
2207
  const outfile = join7(dir, "engine", "index.js");
2102
- if (!existsSync7(entry)) {
2208
+ if (!existsSync8(entry)) {
2103
2209
  throw new Error(`No engine/index.ts at ${entry}. ` + `Run from the extension root or pass --dir. Extensions without an engine entry don't need pack.`);
2104
2210
  }
2105
2211
  const manifest = readManifest(dir);
@@ -2125,6 +2231,14 @@ async function extensionPackCommand(opts) {
2125
2231
  } catch (err) {
2126
2232
  throw new Error(`Bun bundle failed: ${err.message}`);
2127
2233
  }
2234
+ const scrubbed = scrubBuildPaths(outfile, dir);
2235
+ if (scrubbed > 0) {
2236
+ console.log(` ${c5.green("\u2713")} ${c5.dim(`${scrubbed} build path(s) neutralised`)}`);
2237
+ }
2238
+ const recorded = recordBundledVersions(outfile, dir);
2239
+ if (recorded.length > 0) {
2240
+ console.log(` ${c5.green("\u2713")} ${c5.dim(`recorded bundled ${recorded.join(", ")}`)}`);
2241
+ }
2128
2242
  const bundleBytes = readFileSync2(outfile);
2129
2243
  const engineSha256 = createHash("sha256").update(bundleBytes).digest("hex");
2130
2244
  const bundleText = bundleBytes.toString("utf8").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/[^\n]*/g, "$1");
@@ -2136,21 +2250,29 @@ async function extensionPackCommand(opts) {
2136
2250
  }
2137
2251
  const sizeKb = Math.round(bundleBytes.byteLength / 1024);
2138
2252
  console.log(` ${c5.green("\u2713")} ${c5.bold(`engine/index.js`)} ${c5.dim(`(${sizeKb} KB, sha256=${engineSha256.slice(0, 12)}\u2026)`)}`);
2139
- let resolvedIsolation = manifest.engine?.isolation;
2140
- if (!resolvedIsolation) {
2253
+ let communityInject = false;
2254
+ if (!manifest.engine?.isolation) {
2141
2255
  const resolved = await resolvePublisherTier({
2142
2256
  firstParty: opts.firstParty,
2143
2257
  token: opts.token,
2144
2258
  registryUrl: opts.registryUrl
2145
2259
  });
2146
2260
  if (!resolved.allowsInline) {
2147
- resolvedIsolation = "worker";
2261
+ communityInject = true;
2148
2262
  console.log(` ${c5.yellow("\u26A0")} ${resolved.tier} publisher \u2014 auto-set ${c5.bold('engine.isolation: "worker"')} ` + `(community extensions can't run inline).`);
2149
2263
  if (resolved.source === "default") {
2150
2264
  console.log(c5.dim(" Tier defaulted to community (no --first-party flag, no registry token). " + "If you are verified/first-party, pass --first-party or set ZVELTIO_REGISTRY_TOKEN."));
2151
2265
  }
2152
2266
  }
2267
+ } else if (opts.firstParty && manifest.engine.isolation === "worker" && !opts.keepIsolation) {
2268
+ console.log(` ${c5.yellow("\u26A0")} first-party pack \u2014 clearing sticky ${c5.bold('engine.isolation: "worker"')} ` + `(inline is the first-party default). Pass ${c5.bold("--keep-isolation")} to retain worker.`);
2153
2269
  }
2270
+ const resolvedIsolation = resolvePackIsolation({
2271
+ current: manifest.engine?.isolation,
2272
+ firstParty: opts.firstParty,
2273
+ keepIsolation: opts.keepIsolation,
2274
+ communityInject
2275
+ });
2154
2276
  if (!opts.noManifestUpdate) {
2155
2277
  manifest.engine = {
2156
2278
  entry: "engine/index.js",
@@ -2162,6 +2284,7 @@ async function extensionPackCommand(opts) {
2162
2284
  };
2163
2285
  manifest.integrity = {
2164
2286
  engineSha256,
2287
+ sourceSha256: hashEngineSources(dir),
2165
2288
  ...manifest.integrity?.archiveSha256 ? { archiveSha256: manifest.integrity.archiveSha256 } : {}
2166
2289
  };
2167
2290
  writeManifest(dir, manifest);
@@ -3104,10 +3227,13 @@ function findDockerfile(cwd) {
3104
3227
  return null;
3105
3228
  }
3106
3229
  async function generateDockerfile(cwd, projectName) {
3107
- let entryPoint = "packages/engine/src/index.ts";
3108
- if (!existsSync4(join4(cwd, entryPoint))) {
3109
- entryPoint = existsSync4(join4(cwd, "src/index.ts")) ? "src/index.ts" : "index.ts";
3110
- }
3230
+ const candidates = [
3231
+ "packages/engine/src/binary-entry.ts",
3232
+ "packages/engine/src/index.ts",
3233
+ "src/binary-entry.ts",
3234
+ "src/index.ts"
3235
+ ];
3236
+ const entryPoint = candidates.find((f) => existsSync4(join4(cwd, f))) ?? "index.ts";
3111
3237
  await Bun.write(join4(cwd, "Dockerfile"), `# Auto-generated by zveltio deploy
3112
3238
  # Edit this file to customise your production image.
3113
3239
  FROM oven/bun:1-alpine AS base
@@ -3140,6 +3266,40 @@ CMD ["./dist/${projectName}"]
3140
3266
  `);
3141
3267
  }
3142
3268
 
3269
+ // src/lib/engine-modules.ts
3270
+ import { existsSync as existsSync5 } from "fs";
3271
+ import { fileURLToPath } from "url";
3272
+ var CANDIDATES = [
3273
+ "../../../engine/src/db/",
3274
+ "../../engine/src/db/"
3275
+ ];
3276
+
3277
+ class EngineModulesUnavailable extends Error {
3278
+ constructor() {
3279
+ super(`this CLI cannot reach the engine\u2019s migration runner.
3280
+
3281
+ ` + ` The published @zveltio/cli package does not ship the engine, so the
3282
+ ` + ` direct-to-database path only works from a repository checkout.
3283
+
3284
+ ` + ` Use one of these instead:
3285
+ ` + ` zveltio migrate # the release binary, with DATABASE_URL set
3286
+ ` + " zveltio migrate --url http://host # against a running engine");
3287
+ this.name = "EngineModulesUnavailable";
3288
+ }
3289
+ }
3290
+ async function loadEngineMigrationModules() {
3291
+ for (const base of CANDIDATES) {
3292
+ const dbUrl = new URL(`${base}index.js`, import.meta.url);
3293
+ const path = fileURLToPath(dbUrl);
3294
+ if (!existsSync5(path) && !existsSync5(path.replace(/\.js$/, ".ts")))
3295
+ continue;
3296
+ const db = await import(dbUrl.href);
3297
+ const migrations = await import(new URL(`${base}migrations/index.js`, import.meta.url).href);
3298
+ return { db, migrations };
3299
+ }
3300
+ throw new EngineModulesUnavailable;
3301
+ }
3302
+
3143
3303
  // src/commands/migrate.ts
3144
3304
  var migrateCommand = new Command("migrate").description("Run pending database migrations").option("--url <url>", "Engine URL", "http://localhost:3000").option("--database-url <url>", "Direct database URL (skip engine)").option("--dry-run", "Show pending migrations without applying").action(async (opts) => {
3145
3305
  console.log(`
@@ -3155,10 +3315,9 @@ var migrateCommand = new Command("migrate").description("Run pending database mi
3155
3315
  async function runMigrationsDirectly(opts, databaseUrl) {
3156
3316
  try {
3157
3317
  process.env.DATABASE_URL = databaseUrl;
3158
- const dbPath = new URL("../../../engine/src/db/index.js", import.meta.url).href;
3159
- const migrationsPath = new URL("../../../engine/src/db/migrations/index.js", import.meta.url).href;
3160
- const { initDatabase } = await import(dbPath);
3161
- const { runMigrations, getAppliedMigrations, getLastAppliedMigration } = await import(migrationsPath);
3318
+ const { db: dbModule, migrations } = await loadEngineMigrationModules();
3319
+ const { initDatabase } = dbModule;
3320
+ const { runMigrations, getAppliedMigrations, getLastAppliedMigration } = migrations;
3162
3321
  const db = await initDatabase();
3163
3322
  if (opts.dryRun) {
3164
3323
  const applied = await getAppliedMigrations(db);
@@ -3229,37 +3388,41 @@ async function runMigrationsViaAPI(opts) {
3229
3388
  }
3230
3389
 
3231
3390
  // src/commands/extension.ts
3232
- import { existsSync as existsSync8 } from "fs";
3391
+ import { existsSync as existsSync9 } from "fs";
3233
3392
  import { mkdir, writeFile } from "fs/promises";
3234
3393
  import { join as join8 } from "path";
3235
3394
  async function extensionCommand(action, name, opts) {
3236
3395
  switch (action) {
3237
3396
  case "create":
3238
- await createExtension(name, opts.category || "custom");
3397
+ await createExtension(name, opts.category || "custom", opts.codePage === true);
3239
3398
  break;
3240
3399
  case "build":
3241
3400
  await buildExtension(opts);
3242
3401
  break;
3243
- case "dev":
3244
- await devExtension();
3245
- break;
3246
3402
  }
3247
3403
  }
3248
- async function createExtension(name, category) {
3404
+ async function createExtension(name, category, codePage) {
3249
3405
  const safeName = name.toLowerCase().replace(/[^a-z0-9-]/g, "-");
3406
+ const extName = `${category}/${safeName}`;
3407
+ const table = `zv_${safeName.replace(/-/g, "_")}_items`;
3250
3408
  const targetDir = join8(process.cwd(), "extensions", category, safeName);
3251
- if (existsSync8(targetDir)) {
3409
+ if (existsSync9(targetDir)) {
3252
3410
  console.error(`Extension already exists: ${targetDir}`);
3253
3411
  process.exit(1);
3254
3412
  }
3255
3413
  console.log(`
3256
- Creating extension: ${category}/${safeName}
3414
+ Creating extension: ${extName}
3257
3415
  `);
3258
- await mkdir(join8(targetDir, "engine"), { recursive: true });
3259
- await mkdir(join8(targetDir, "studio", "src", "pages"), { recursive: true });
3260
3416
  await mkdir(join8(targetDir, "engine", "migrations"), { recursive: true });
3417
+ await mkdir(join8(targetDir, "studio", "src", "components"), { recursive: true });
3418
+ if (codePage) {
3419
+ await mkdir(join8(targetDir, "studio", "pages"), { recursive: true });
3420
+ } else {
3421
+ await mkdir(join8(targetDir, "studio", "schemas"), { recursive: true });
3422
+ await mkdir(join8(targetDir, "studio", "messages"), { recursive: true });
3423
+ }
3261
3424
  await writeFile(join8(targetDir, "manifest.json"), JSON.stringify({
3262
- name: safeName,
3425
+ name: extName,
3263
3426
  package: `@zveltio/ext-${safeName}`,
3264
3427
  category,
3265
3428
  displayName: name,
@@ -3268,13 +3431,29 @@ Creating extension: ${category}/${safeName}
3268
3431
  zveltioMinVersion: "1.0.0",
3269
3432
  zveltioMaxVersion: "4.0.0",
3270
3433
  permissions: ["database"],
3271
- contributes: { engine: true, studio: true, fieldTypes: [] }
3434
+ studio: {
3435
+ pages: [
3436
+ {
3437
+ path: `/admin/${safeName}`,
3438
+ label: name,
3439
+ icon: "Puzzle",
3440
+ ...codePage ? {} : { schema: `schemas/${safeName}.json` }
3441
+ }
3442
+ ],
3443
+ navGroup: "developer"
3444
+ },
3445
+ contributes: {
3446
+ engine: true,
3447
+ studio: true,
3448
+ fieldTypes: [],
3449
+ slots: []
3450
+ }
3272
3451
  }, null, 2));
3273
3452
  await writeFile(join8(targetDir, "engine", "index.ts"), `import type { ZveltioExtension } from '@zveltio/sdk/extension';
3274
3453
  import { join } from 'path';
3275
3454
 
3276
3455
  const extension: ZveltioExtension = {
3277
- name: '${category}/${safeName}',
3456
+ name: '${extName}',
3278
3457
  category: '${category}',
3279
3458
 
3280
3459
  getMigrations() {
@@ -3284,88 +3463,120 @@ const extension: ZveltioExtension = {
3284
3463
  },
3285
3464
 
3286
3465
  async register(app, ctx) {
3287
- // Register your API routes here
3288
- app.get('/api/${safeName}/ping', (c) => c.json({ pong: true }));
3466
+ // The route the generated page reads. ctx.db resolves the caller's tenant
3467
+ // transaction, so this returns only that tenant's rows.
3468
+ app.get('/items', async (c) => {
3469
+ const data = await ctx.db
3470
+ .selectFrom('${table}')
3471
+ .selectAll()
3472
+ .orderBy('created_at', 'desc')
3473
+ .limit(100)
3474
+ .execute();
3475
+ return c.json({ data });
3476
+ });
3289
3477
  },
3290
3478
  };
3291
3479
 
3292
3480
  export default extension;
3293
3481
  `);
3294
- await writeFile(join8(targetDir, "engine", "migrations", "001_init.sql"), `-- ${name} extension initial schema
3295
- -- Add your tables here
3482
+ await writeFile(join8(targetDir, "engine", "migrations", "001_init.sql"), `-- ${name} \u2014 initial schema.
3483
+ --
3484
+ -- Tenant isolation is not optional and not something to add later: a table
3485
+ -- without it is readable by every tenant on the instance. The three parts below
3486
+ -- are the whole pattern.
3487
+
3488
+ CREATE TABLE IF NOT EXISTS ${table} (
3489
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3490
+ name TEXT NOT NULL,
3491
+ -- 1. The column, defaulting to the tenant of the transaction doing the write,
3492
+ -- so application code never has to name it (and cannot forge it).
3493
+ tenant_id UUID NOT NULL DEFAULT COALESCE(
3494
+ NULLIF(current_setting('zveltio.current_tenant', true), '')::uuid,
3495
+ '00000000-0000-0000-0000-000000000001'::uuid
3496
+ ),
3497
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3498
+ );
3499
+
3500
+ CREATE INDEX IF NOT EXISTS idx_${table}_tenant ON ${table}(tenant_id);
3296
3501
 
3297
- -- Example:
3298
- -- CREATE TABLE IF NOT EXISTS zv_${safeName.replace(/-/g, "_")} (
3299
- -- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3300
- -- name TEXT NOT NULL,
3301
- -- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3302
- -- );
3502
+ -- 2. FORCE matters. Without it Postgres lets the table owner bypass the policy,
3503
+ -- and the engine connects as owner on a stock install \u2014 RLS becomes advisory.
3504
+ ALTER TABLE ${table} ENABLE ROW LEVEL SECURITY;
3505
+ ALTER TABLE ${table} FORCE ROW LEVEL SECURITY;
3506
+
3507
+ -- 3. The host's own predicate, not a hand-rolled copy: reads and writes agree
3508
+ -- with the column DEFAULT above, and the engine reconciles every policy
3509
+ -- named \`tenant_isolation_*\` onto it at boot.
3510
+ DROP POLICY IF EXISTS tenant_isolation_${table} ON ${table};
3511
+ CREATE POLICY tenant_isolation_${table} ON ${table}
3512
+ USING (tenant_id = ANY (zveltio_visible_tenants()))
3513
+ WITH CHECK (zveltio_tenant_write_ok(tenant_id));
3303
3514
  `);
3304
- await writeFile(join8(targetDir, "studio", "src", "index.ts"), `import MainPage from './pages/MainPage.svelte';
3515
+ if (codePage) {
3516
+ await writeFile(join8(targetDir, "studio", "pages", "+page.svelte"), `<script lang="ts">
3517
+ import { api } from '$lib/api.js';
3305
3518
 
3306
- export default function register() {
3307
- const zveltio = (window as any).__zveltio;
3308
- if (!zveltio) {
3309
- console.error('Zveltio Studio API not available');
3310
- return;
3311
- }
3519
+ // A code page owns its fetching, loading and error states, and its strings
3520
+ // have to reach i18n by hand. A schema gets all three from the host \u2014 worth
3521
+ // re-checking that this page really needs to be code.
3522
+ let items = $state<Array<{ id: string; name: string }>>([]);
3312
3523
 
3313
- zveltio.registerRoute({
3314
- path: '${safeName}',
3315
- component: MainPage,
3316
- label: '${name}',
3317
- icon: 'Puzzle',
3318
- category: '${category}',
3524
+ $effect(() => {
3525
+ api
3526
+ .get<{ data: Array<{ id: string; name: string }> }>('/ext/${extName}/items')
3527
+ .then((r) => {
3528
+ items = r.data;
3529
+ });
3319
3530
  });
3320
- }
3321
- `);
3322
- await writeFile(join8(targetDir, "studio", "src", "pages", "MainPage.svelte"), `<script lang="ts">
3323
- const engineUrl = (window as any).__ZVELTIO_ENGINE_URL__;
3324
3531
  </script>
3325
3532
 
3326
3533
  <div class="space-y-6">
3327
3534
  <h1 class="text-2xl font-bold">${name}</h1>
3328
- <p class="text-base-content/60">Welcome to the ${name} extension.</p>
3535
+ {#each items as item (item.id)}
3536
+ <div>{item.name}</div>
3537
+ {/each}
3329
3538
  </div>
3330
3539
  `);
3331
- await writeFile(join8(targetDir, "studio", "vite.config.ts"), `import { svelte } from '@sveltejs/vite-plugin-svelte';
3332
- import { defineConfig } from 'vite';
3333
-
3334
- export default defineConfig({
3335
- plugins: [svelte()],
3336
- build: {
3337
- lib: {
3338
- entry: 'src/index.ts',
3339
- formats: ['iife'],
3340
- fileName: () => 'bundle.js',
3341
- },
3342
- rollupOptions: {
3343
- external: ['svelte', 'svelte/internal', 'svelte/store'],
3344
- output: {
3345
- globals: {
3346
- 'svelte': 'window.__SvelteRuntime?.svelte',
3347
- 'svelte/internal': 'window.__SvelteRuntime?.internal',
3348
- 'svelte/store': 'window.__SvelteRuntime?.store',
3349
- },
3350
- },
3351
- },
3352
- },
3353
- });
3540
+ } else {
3541
+ await writeFile(join8(targetDir, "studio", "schemas", `${safeName}.json`), `${JSON.stringify({
3542
+ sduiSchema: 1,
3543
+ title: `${safeName}.title`,
3544
+ subtitle: `${safeName}.subtitle`,
3545
+ resources: [
3546
+ {
3547
+ id: "items",
3548
+ dataSource: `/ext/${extName}/items`,
3549
+ dataPath: "data",
3550
+ search: { fields: ["name"], placeholder: "common.search" },
3551
+ columns: [
3552
+ { key: "name", label: "common.col.name" },
3553
+ { key: "created_at", label: "common.col.created", type: "date" }
3554
+ ]
3555
+ }
3556
+ ]
3557
+ }, null, 2)}
3558
+ `);
3559
+ await writeFile(join8(targetDir, "studio", "messages", "en.json"), `${JSON.stringify({
3560
+ [`${safeName}.title`]: name,
3561
+ [`${safeName}.subtitle`]: `Manage ${name.toLowerCase()} records`
3562
+ }, null, 2)}
3563
+ `);
3564
+ }
3565
+ await writeFile(join8(targetDir, "studio", "src", "contribute.ts.example"), `/**
3566
+ * Rename to contribute.ts to register dashboard/settings slot widgets.
3567
+ * Synced by packages/studio/scripts/sync-extensions.ts \u2014 no studio/dist/ build.
3568
+ *
3569
+ * import { registerContributionSlot } from '$lib/extension-api.svelte.js';
3570
+ * import MyWidget from './components/MyWidget.svelte';
3571
+ *
3572
+ * export function activate(): void {
3573
+ * registerContributionSlot('${extName}', 'dashboard.widgets', {
3574
+ * component: MyWidget,
3575
+ * priority: 100,
3576
+ * });
3577
+ * }
3578
+ */
3354
3579
  `);
3355
- await writeFile(join8(targetDir, "studio", "package.json"), JSON.stringify({
3356
- name: `@zveltio/ext-${safeName}-studio`,
3357
- version: "1.0.0",
3358
- type: "module",
3359
- scripts: {
3360
- build: "vite build",
3361
- dev: "vite build --watch"
3362
- },
3363
- dependencies: { svelte: "^5.0.0" },
3364
- devDependencies: {
3365
- "@sveltejs/vite-plugin-svelte": "^4.0.0",
3366
- vite: "^6.0.0"
3367
- }
3368
- }, null, 2));
3369
3580
  await writeFile(join8(targetDir, ".gitattributes"), `* text=auto eol=lf
3370
3581
 
3371
3582
  # Packed engine bundle: byte-identical across OSes. Manifest
@@ -3392,57 +3603,60 @@ jobs:
3392
3603
  - name: Install dependencies
3393
3604
  run: bun install
3394
3605
 
3606
+ # Validate FIRST, on the COMMITTED bundle. It checks the bundle against
3607
+ # manifest integrity.engineSha256 and against the source it was built
3608
+ # from, so a bundle nobody repacked fails here. Running pack before this
3609
+ # would rewrite both hashes and the check could never fail.
3610
+ - name: Validate committed bundle, manifest + structure
3611
+ run: bunx @zveltio/cli extension validate
3612
+
3395
3613
  - name: Pack extension (engine/index.ts \u2192 engine/index.js + manifest hash)
3396
3614
  run: bunx @zveltio/cli extension pack
3397
-
3398
- - name: Verify committed bundle matches manifest engineSha256
3399
- # If pack changed engine/index.js, the committed bytes drift
3400
- # from manifest.integrity.engineSha256. The publishing pipeline
3401
- # would reject this \u2014 fail PR before merging.
3402
- run: |
3403
- actual=$(sha256sum engine/index.js | awk '{print $1}')
3404
- declared=$(node -e "console.log(require('./manifest.json').integrity?.engineSha256 ?? '')")
3405
- if [ "$actual" != "$declared" ]; then
3406
- echo "::error::Bundle hash $actual \u2260 manifest engineSha256 $declared"
3407
- echo "::error::Run \\\`bunx @zveltio/cli extension pack\\\` locally and commit the result."
3408
- exit 1
3409
- fi
3410
-
3411
- - name: Validate manifest + structure
3412
- run: bunx @zveltio/cli extension validate
3413
3615
  `);
3616
+ const pageLines = codePage ? ` pages/
3617
+ +page.svelte <- code page (synced into Studio at release)` : ` schemas/
3618
+ ${safeName}.json <- THE PAGE (rendered by the host; no build step)
3619
+ messages/
3620
+ en.json <- keys this schema owns; other locales filled from en`;
3414
3621
  console.log(`Extension scaffolded at extensions/${category}/${safeName}/
3415
3622
 
3416
- Structure:
3417
3623
  engine/
3418
- index.ts <- API routes
3419
- migrations/ <- SQL migrations
3624
+ index.ts <- API routes (mounted at /ext/${extName}/*)
3625
+ migrations/ <- SQL, with tenant isolation already wired
3420
3626
  studio/
3627
+ ${pageLines}
3421
3628
  src/
3422
- index.ts <- Studio registration
3423
- pages/ <- Svelte UI components
3424
- vite.config.ts
3629
+ components/ <- shared Svelte (optional)
3630
+ contribute.ts.example <- rename to contribute.ts for a dashboard widget
3425
3631
  manifest.json
3426
- .gitattributes <- pins engine/index.js as binary (no EOL conversion)
3427
- .github/workflows/
3428
- ci.yml <- pack + hash verify + validate on every PR
3632
+ .gitattributes
3633
+ .github/workflows/ci.yml
3429
3634
 
3430
3635
  Next steps:
3431
- 1. Add your business logic in engine/index.ts
3432
- 2. Create your UI in studio/src/pages/
3433
- 3. Run \`bunx @zveltio/cli extension pack\` to build the bundle
3434
- 4. Enable the extension: ZVELTIO_EXTENSIONS=${category}/${safeName}
3636
+ 1. Put your logic in engine/index.ts \u2014 that is where the truth lives
3637
+ 2. ${codePage ? "Write the page in studio/pages/+page.svelte" : `Shape the page in studio/schemas/${safeName}.json`}
3638
+ 3. \`bunx @zveltio/cli extension pack\` \u2014 produces engine/index.js + integrity
3639
+ (validate needs this: it rejects a manifest with no engine block)
3640
+ 4. \`bunx @zveltio/cli extension validate\`${codePage ? " \u2014 manifest, migrations and bundle" : ` \u2014 checks the schema against the
3641
+ routes you actually serve, and the message keys against what you ship`}
3642
+ 5. Local preview: \`cd packages/studio && bun scripts/sync-extensions.ts\`,
3643
+ then \`bun run dev\` \u2014 see developer-guide.md \xA72
3435
3644
 
3436
- Isolation (MARKETPLACE-POLICY \xA72):
3437
- Community publishers MUST run in worker isolation. \`extension pack\`
3438
- auto-sets "engine": { ..., "isolation": "worker" } for you unless it can
3439
- confirm you're a verified/first-party publisher (via --first-party or a
3440
- registry token). Worker mode = Bun.Worker with crash isolation and no
3441
- direct DB credentials.
3645
+ Where UI goes:
3646
+ a page -> studio/schemas/*.json (almost always)
3647
+ a widget on the dashboard -> studio/src/contribute.ts (occasionally)
3648
+ canvas / chat / map / IDE -> studio/pages/+page.svelte (rarely)
3442
3649
 
3443
- First-party / vendor builds that want inline isolation: pack with
3444
- --first-party. See https://github.com/zveltio-devs/zveltio/blob/master/docs/EXTENSION-DEVELOPER-GUIDE.md#135-isolation-tiers-be-honest-about-what-you-ship
3650
+ Do NOT add studio/vite.config.ts, studio/package.json, or studio/dist/ \u2014 removed in alpha.94/beta.15.
3445
3651
  `);
3652
+ if (codePage) {
3653
+ console.warn(`\x1B[33mScaffolded a CODE page. Across the 56 shipped extensions, 61 pages are
3654
+ schemas and seven are code \u2014 and each of those seven sits beside a schema, for
3655
+ the part a schema could not express. On this road you own fetching,
3656
+ loading and error states, i18n by hand, and a Studio release to ship a change.
3657
+ Worth it for a canvas, a chat, a map or an inbox \u2014 for a list, a form or
3658
+ settings, delete studio/pages/ and add \`schema\` back to manifest.studio.pages[0].\x1B[0m`);
3659
+ }
3446
3660
  }
3447
3661
  async function buildExtension(opts) {
3448
3662
  console.warn("\x1B[33m`zveltio extension build` is deprecated \u2014 use `zveltio extension pack`.\x1B[0m");
@@ -3450,26 +3664,10 @@ async function buildExtension(opts) {
3450
3664
  const { extensionPackCommand: extensionPackCommand2 } = await Promise.resolve().then(() => (init_extension_pack(), exports_extension_pack));
3451
3665
  await extensionPackCommand2({ dir: opts.dir });
3452
3666
  }
3453
- async function devExtension() {
3454
- console.log(`
3455
- Starting extension dev mode...
3456
- `);
3457
- const studioProc = Bun.spawn(["bun", "run", "dev"], {
3458
- cwd: "studio",
3459
- stdout: "inherit",
3460
- stderr: "inherit"
3461
- });
3462
- console.log(" Studio: watching for changes...");
3463
- process.on("SIGINT", () => {
3464
- studioProc.kill();
3465
- process.exit(0);
3466
- });
3467
- await studioProc.exited;
3468
- }
3469
3667
 
3470
3668
  // src/commands/extension-types.ts
3471
- import { existsSync as existsSync9, readdirSync, readFileSync as readFileSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3472
- import { join as join9, relative } from "path";
3669
+ import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync3, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
3670
+ import { join as join9, relative as relative2 } from "path";
3473
3671
 
3474
3672
  // ../sdk/dist/schema-codegen.js
3475
3673
  var NUMERIC_TYPES = new Set([
@@ -3539,6 +3737,10 @@ function mapPgTypeToTs(pgType) {
3539
3737
  tsBase = "unknown";
3540
3738
  return isArray ? `${tsBase}[]` : tsBase;
3541
3739
  }
3740
+ function upSection(sql) {
3741
+ const i = sql.search(/^--\s*DOWN\s*$/im);
3742
+ return i < 0 ? sql : sql.slice(0, i);
3743
+ }
3542
3744
  function stripComments(sql) {
3543
3745
  let out = "";
3544
3746
  let i = 0;
@@ -3716,13 +3918,12 @@ function parseSchema(sqlChunks) {
3716
3918
  }
3717
3919
  return tables.get(name);
3718
3920
  };
3719
- const allSql = sqlChunks.map(stripComments).join(`
3921
+ const allSql = sqlChunks.map(upSection).map(stripComments).join(`
3720
3922
  `);
3721
3923
  const createRe = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s*\(/gi;
3722
- let m;
3723
- while ((m = createRe.exec(allSql)) !== null) {
3924
+ for (const m of allSql.matchAll(createRe)) {
3724
3925
  const tableName = m[1] ?? m[2];
3725
- const openIdx = createRe.lastIndex - 1;
3926
+ const openIdx = m.index + m[0].length - 1;
3726
3927
  const closeIdx = findMatchingParen(allSql, openIdx);
3727
3928
  if (closeIdx < 0)
3728
3929
  continue;
@@ -3731,10 +3932,9 @@ function parseSchema(sqlChunks) {
3731
3932
  const tableCols = ensureTable(tableName);
3732
3933
  for (const c6 of columns)
3733
3934
  tableCols.set(c6.name, c6);
3734
- createRe.lastIndex = closeIdx + 1;
3735
3935
  }
3736
3936
  const alterRe = /ALTER\s+TABLE\s+(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+ADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+([^;]+?)(?=;|$)/gi;
3737
- while ((m = alterRe.exec(allSql)) !== null) {
3937
+ for (const m of allSql.matchAll(alterRe)) {
3738
3938
  const tableName = m[1] ?? m[2];
3739
3939
  const colName = m[3] ?? m[4];
3740
3940
  const tail = m[5];
@@ -3744,6 +3944,10 @@ function parseSchema(sqlChunks) {
3744
3944
  tableCols.set(col.name, col);
3745
3945
  }
3746
3946
  }
3947
+ const dropRe = /ALTER\s+TABLE\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][a-zA-Z0-9_]*))/gi;
3948
+ for (const m of allSql.matchAll(dropRe)) {
3949
+ tables.get(m[1] ?? m[2])?.delete(m[3] ?? m[4]);
3950
+ }
3747
3951
  return {
3748
3952
  tables: order.map((name) => ({ name, columns: [...tables.get(name).values()] }))
3749
3953
  };
@@ -3793,18 +3997,18 @@ ${c6.bold("Extension types")}
3793
3997
  `);
3794
3998
  console.log(` Extension dir: ${c6.dim(dir)}`);
3795
3999
  const manifestPath = join9(dir, "manifest.json");
3796
- if (!existsSync9(manifestPath)) {
4000
+ if (!existsSync10(manifestPath)) {
3797
4001
  console.error(c6.red(`No manifest.json found in ${dir}`));
3798
4002
  console.error(c6.dim(" Run this command from your extension's root folder, or pass --dir."));
3799
4003
  process.exit(1);
3800
4004
  }
3801
4005
  const migrationsDir = join9(dir, "engine", "migrations");
3802
- if (!existsSync9(migrationsDir)) {
4006
+ if (!existsSync10(migrationsDir)) {
3803
4007
  console.error(c6.yellow(`No engine/migrations directory found at ${migrationsDir}`));
3804
4008
  console.error(c6.dim(" Nothing to generate. Add SQL migrations first."));
3805
4009
  process.exit(0);
3806
4010
  }
3807
- const sqlFiles = readdirSync(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
4011
+ const sqlFiles = readdirSync2(migrationsDir).filter((f) => f.endsWith(".sql")).sort();
3808
4012
  if (sqlFiles.length === 0) {
3809
4013
  console.error(c6.yellow(`No .sql files in ${migrationsDir}`));
3810
4014
  process.exit(0);
@@ -3832,13 +4036,14 @@ ${c6.bold("Extension types")}
3832
4036
  mkdirSync2(join9(outputPath, ".."), { recursive: true });
3833
4037
  writeFileSync2(outputPath, tsBody, "utf8");
3834
4038
  console.log(`
3835
- ${c6.green("Written:")} ${relative(process.cwd(), outputPath) || outputPath}`);
4039
+ ${c6.green("Written:")} ${relative2(process.cwd(), outputPath) || outputPath}`);
3836
4040
  console.log(c6.dim(" Add `.zveltio/` to your .gitignore. Regenerate with this command after every migration."));
3837
4041
  console.log("");
3838
4042
  }
3839
4043
 
3840
4044
  // src/commands/extension-validate.ts
3841
- import { existsSync as existsSync10, readdirSync as readdirSync2, readFileSync as readFileSync4, statSync } from "fs";
4045
+ import { createHash as createHash2 } from "crypto";
4046
+ import { existsSync as existsSync11, readdirSync as readdirSync3, readFileSync as readFileSync4, statSync } from "fs";
3842
4047
  import { join as join10 } from "path";
3843
4048
 
3844
4049
  // ../sdk/dist/validate/migration-parse.js
@@ -3855,6 +4060,221 @@ function parseMigrationSql(raw) {
3855
4060
  return { up, down: downBody.length > 0 ? downBody : null };
3856
4061
  }
3857
4062
 
4063
+ // ../sdk/dist/validate/shared-message-keys.js
4064
+ var SHARED_MESSAGE_KEYS = new Set([
4065
+ "common.accessRestricted",
4066
+ "common.actions",
4067
+ "common.add",
4068
+ "common.added",
4069
+ "common.aiStudio",
4070
+ "common.approve",
4071
+ "common.assign",
4072
+ "common.bulkActions",
4073
+ "common.cancel",
4074
+ "common.cancelEdit",
4075
+ "common.clear",
4076
+ "common.clearSelection",
4077
+ "common.close",
4078
+ "common.col.account",
4079
+ "common.col.active",
4080
+ "common.col.address",
4081
+ "common.col.amount",
4082
+ "common.col.balance",
4083
+ "common.col.bank",
4084
+ "common.col.bcc",
4085
+ "common.col.category",
4086
+ "common.col.cc",
4087
+ "common.col.channel",
4088
+ "common.col.client",
4089
+ "common.col.code",
4090
+ "common.col.collection",
4091
+ "common.col.company",
4092
+ "common.col.created",
4093
+ "common.col.credit",
4094
+ "common.col.currency",
4095
+ "common.col.date",
4096
+ "common.col.days",
4097
+ "common.col.debit",
4098
+ "common.col.department",
4099
+ "common.col.description",
4100
+ "common.col.docNumber",
4101
+ "common.col.dueDate",
4102
+ "common.col.duration",
4103
+ "common.col.email",
4104
+ "common.col.employee",
4105
+ "common.col.enabled",
4106
+ "common.col.end",
4107
+ "common.col.event",
4108
+ "common.col.field",
4109
+ "common.col.file",
4110
+ "common.col.from",
4111
+ "common.col.hireDate",
4112
+ "common.col.host",
4113
+ "common.col.iban",
4114
+ "common.col.index",
4115
+ "common.col.key",
4116
+ "common.col.label",
4117
+ "common.col.leaveType",
4118
+ "common.col.locale",
4119
+ "common.col.message",
4120
+ "common.col.method",
4121
+ "common.col.model",
4122
+ "common.col.name",
4123
+ "common.col.notes",
4124
+ "common.col.number",
4125
+ "common.col.paid",
4126
+ "common.col.parent",
4127
+ "common.col.password",
4128
+ "common.col.path",
4129
+ "common.col.phone",
4130
+ "common.col.port",
4131
+ "common.col.position",
4132
+ "common.col.price",
4133
+ "common.col.priority",
4134
+ "common.col.product",
4135
+ "common.col.project",
4136
+ "common.col.provider",
4137
+ "common.col.quantity",
4138
+ "common.col.reason",
4139
+ "common.col.recipient",
4140
+ "common.col.records",
4141
+ "common.col.required",
4142
+ "common.col.resource",
4143
+ "common.col.role",
4144
+ "common.col.sku",
4145
+ "common.col.slug",
4146
+ "common.col.start",
4147
+ "common.col.status",
4148
+ "common.col.subject",
4149
+ "common.col.taxId",
4150
+ "common.col.template",
4151
+ "common.col.ticket",
4152
+ "common.col.time",
4153
+ "common.col.title",
4154
+ "common.col.to",
4155
+ "common.col.total",
4156
+ "common.col.type",
4157
+ "common.col.unique",
4158
+ "common.col.unit",
4159
+ "common.col.user",
4160
+ "common.col.username",
4161
+ "common.col.validUntil",
4162
+ "common.col.value",
4163
+ "common.col.vat",
4164
+ "common.col.vendor",
4165
+ "common.col.version",
4166
+ "common.col.warehouse",
4167
+ "common.col.year",
4168
+ "common.collectionRequired",
4169
+ "common.confirm",
4170
+ "common.copied",
4171
+ "common.copy",
4172
+ "common.copyShort",
4173
+ "common.create",
4174
+ "common.delete",
4175
+ "common.deleteCount",
4176
+ "common.deleteFailed",
4177
+ "common.discard",
4178
+ "common.dismiss",
4179
+ "common.dismissNotification",
4180
+ "common.done",
4181
+ "common.download",
4182
+ "common.edit",
4183
+ "common.emptyDash",
4184
+ "common.failed",
4185
+ "common.fields",
4186
+ "common.filter.admin",
4187
+ "common.filter.all",
4188
+ "common.filter.allStatuses",
4189
+ "common.filter.auth",
4190
+ "common.filter.data",
4191
+ "common.format",
4192
+ "common.generate",
4193
+ "common.loadFailed",
4194
+ "common.loading",
4195
+ "common.logout",
4196
+ "common.manage",
4197
+ "common.mergeFailed",
4198
+ "common.nameRequired",
4199
+ "common.new",
4200
+ "common.next",
4201
+ "common.no",
4202
+ "common.noResults",
4203
+ "common.notFound",
4204
+ "common.op.delete",
4205
+ "common.op.insert",
4206
+ "common.op.update",
4207
+ "common.open",
4208
+ "common.optional",
4209
+ "common.optionalParen",
4210
+ "common.other",
4211
+ "common.pageOf",
4212
+ "common.post",
4213
+ "common.prev",
4214
+ "common.publish",
4215
+ "common.rangeOf",
4216
+ "common.refresh",
4217
+ "common.reject",
4218
+ "common.remove",
4219
+ "common.rename",
4220
+ "common.reorder",
4221
+ "common.required",
4222
+ "common.resetFailed",
4223
+ "common.retry",
4224
+ "common.revoke",
4225
+ "common.save",
4226
+ "common.saveFailed",
4227
+ "common.saved",
4228
+ "common.saving",
4229
+ "common.search",
4230
+ "common.selectCollection",
4231
+ "common.selectPlaceholder",
4232
+ "common.selected",
4233
+ "common.send",
4234
+ "common.somethingWrong",
4235
+ "common.status.approved",
4236
+ "common.status.cancelled",
4237
+ "common.status.draft",
4238
+ "common.status.pending",
4239
+ "common.status.reimbursed",
4240
+ "common.status.rejected",
4241
+ "common.status.submitted",
4242
+ "common.submit",
4243
+ "common.sync",
4244
+ "common.testConnection",
4245
+ "common.typeToSearch",
4246
+ "common.undo",
4247
+ "common.unexpectedError",
4248
+ "common.unknownError",
4249
+ "common.upload",
4250
+ "common.uploadFailed",
4251
+ "common.uploading",
4252
+ "common.verifyingPermissions",
4253
+ "common.view",
4254
+ "common.viewAll",
4255
+ "common.yes",
4256
+ "ext.approved",
4257
+ "ext.confirm.deleteForm",
4258
+ "ext.confirm.deleteNamed",
4259
+ "ext.confirm.removeSearchIndex",
4260
+ "ext.confirmDelete",
4261
+ "ext.copied",
4262
+ "ext.copyFailed",
4263
+ "ext.created",
4264
+ "ext.deleted",
4265
+ "ext.errorPrefix",
4266
+ "ext.linkCopied",
4267
+ "ext.loadFailed",
4268
+ "ext.rejected",
4269
+ "ext.reveal.copy",
4270
+ "ext.reveal.note",
4271
+ "ext.reveal.title",
4272
+ "ext.saveFailed",
4273
+ "ext.saved",
4274
+ "ext.sent",
4275
+ "ext.submitted"
4276
+ ]);
4277
+
3858
4278
  // ../sdk/dist/validate/index.js
3859
4279
  var SEMVER_RE = /^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/;
3860
4280
  var KNOWN_CATEGORIES = new Set([
@@ -3890,6 +4310,9 @@ var SAFE_VERSION_RANGE = /^[\d.*^~>=<| -]+$/;
3890
4310
  function err(code, message, file) {
3891
4311
  return file ? { code, message, file } : { code, message };
3892
4312
  }
4313
+ function warn(code, message, file) {
4314
+ return { ...err(code, message, file), severity: "warning" };
4315
+ }
3893
4316
  function validateManifest(input) {
3894
4317
  const out = [];
3895
4318
  const m = input.manifest;
@@ -3917,7 +4340,7 @@ function validateManifest(input) {
3917
4340
  out.push(err("MANIFEST_BAD_MAX_VERSION", `zveltioMaxVersion "${obj.zveltioMaxVersion}" is not valid semver`));
3918
4341
  }
3919
4342
  if (typeof obj.category === "string" && !KNOWN_CATEGORIES.has(obj.category)) {
3920
- out.push(err("MANIFEST_UNKNOWN_CATEGORY", `category "${obj.category}" isn't in the known set \u2014 typo? Known: ${[...KNOWN_CATEGORIES].sort().join(", ")}`));
4343
+ out.push(warn("MANIFEST_UNKNOWN_CATEGORY", `category "${obj.category}" isn't in the known set \u2014 typo? Known: ${[...KNOWN_CATEGORIES].sort().join(", ")}`));
3921
4344
  }
3922
4345
  return out;
3923
4346
  }
@@ -3949,12 +4372,111 @@ function hasDestructiveDdl(upSql) {
3949
4372
  return true;
3950
4373
  if (/\bDROP\s+COLUMN\b/i.test(s))
3951
4374
  return true;
3952
- if (/\bALTER\s+COLUMN\b[\s\S]*?\bDROP\s+(NOT\s+NULL|DEFAULT)\b/i.test(s))
3953
- return true;
3954
4375
  return false;
3955
4376
  }
4377
+ function userRefUuidColumns(upSql) {
4378
+ const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
4379
+ const found = new Set;
4380
+ for (const m of s.matchAll(/(\b[a-z_][a-z0-9_]*_by)\s+UUID\b/gi)) {
4381
+ found.add(m[1].toLowerCase());
4382
+ }
4383
+ for (const m of s.matchAll(/ALTER\s+COLUMN\s+(\b[a-z_][a-z0-9_]*_by)\s+TYPE\s+UUID\b/gi)) {
4384
+ found.add(m[1].toLowerCase());
4385
+ }
4386
+ return [...found];
4387
+ }
4388
+ function userRefFixedColumns(upSql) {
4389
+ const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
4390
+ return [
4391
+ ...new Set([...s.matchAll(/ALTER\s+COLUMN\s+(\b[a-z_][a-z0-9_]*_by)\s+TYPE\s+TEXT\b/gi)].map((m) => m[1].toLowerCase()))
4392
+ ];
4393
+ }
4394
+ function createTableColumnDefs(sql) {
4395
+ const out = [];
4396
+ const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[^(]*\(/gi;
4397
+ let m;
4398
+ while ((m = re.exec(sql)) !== null) {
4399
+ let depth = 1;
4400
+ let item = "";
4401
+ let i = m.index + m[0].length;
4402
+ for (;i < sql.length && depth > 0; i++) {
4403
+ const ch = sql[i];
4404
+ if (ch === "(")
4405
+ depth++;
4406
+ else if (ch === ")") {
4407
+ depth--;
4408
+ if (depth === 0)
4409
+ break;
4410
+ }
4411
+ if (depth === 1 && ch === ",") {
4412
+ out.push(item);
4413
+ item = "";
4414
+ continue;
4415
+ }
4416
+ item += ch;
4417
+ }
4418
+ out.push(item);
4419
+ re.lastIndex = i;
4420
+ }
4421
+ return out;
4422
+ }
4423
+ function columnLevelUniqueColumns(sql) {
4424
+ const out = [];
4425
+ for (const def of createTableColumnDefs(sql)) {
4426
+ const t = def.trim();
4427
+ const bare = t.replace(/'(?:[^']|'')*'/g, "''");
4428
+ if (!/\bUNIQUE\b(?!\s*\()/i.test(bare))
4429
+ continue;
4430
+ const name = /^([a-z_][a-z0-9_]*)\s+\S/i.exec(t)?.[1];
4431
+ if (!name)
4432
+ continue;
4433
+ if (/^(?:primary|unique|constraint|foreign|check|exclude|like)$/i.test(name))
4434
+ continue;
4435
+ out.push(name);
4436
+ }
4437
+ return out;
4438
+ }
4439
+ function keyColumnSets(upSql) {
4440
+ const s = upSql.replace(/--[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
4441
+ const sets = [];
4442
+ const add = (cols) => sets.push(cols.split(",").map((c7) => c7.trim().replace(/["`]/g, "").toLowerCase()).filter(Boolean));
4443
+ for (const m of s.matchAll(/\bUNIQUE\s*\(([^)]+)\)/gi))
4444
+ add(m[1]);
4445
+ for (const m of s.matchAll(/\bPRIMARY\s+KEY\s*\(([^)]+)\)/gi))
4446
+ add(m[1]);
4447
+ for (const m of s.matchAll(/CREATE\s+UNIQUE\s+INDEX[^(]*\(([^)]+)\)/gi))
4448
+ add(m[1]);
4449
+ for (const col of columnLevelUniqueColumns(s))
4450
+ add(col);
4451
+ for (const m of s.matchAll(/\bADD\s+COLUMN\s+(?:IF\s+NOT\s+EXISTS\s+)?([a-z_][a-z0-9_]*)[^,;\n]*?\bUNIQUE\b(?!\s*\()/gi))
4452
+ add(m[1]);
4453
+ return sets;
4454
+ }
4455
+ function narrowKey(parts) {
4456
+ if (parts.length === 0)
4457
+ return null;
4458
+ if (parts.includes("tenant_id"))
4459
+ return null;
4460
+ if (parts.length === 1 && parts[0] === "id")
4461
+ return null;
4462
+ if (parts.some((p) => /token|hash|secret/.test(p)))
4463
+ return null;
4464
+ if (parts.some((p) => /_id$/.test(p)))
4465
+ return null;
4466
+ return [...parts].sort().join(", ");
4467
+ }
4468
+ function widenedKey(parts) {
4469
+ if (!parts.includes("tenant_id"))
4470
+ return null;
4471
+ const rest = parts.filter((p) => p !== "tenant_id").sort();
4472
+ return rest.length > 0 ? rest.join(", ") : null;
4473
+ }
3956
4474
  function validateMigrations(input) {
3957
4475
  const out = [];
4476
+ const narrowKeys = [];
4477
+ const widened = new Set;
4478
+ const uuidRefs = [];
4479
+ const fixedRefs = new Set;
3958
4480
  for (const file of input.files) {
3959
4481
  if (file.sql.trim() === "") {
3960
4482
  out.push(err("MIGRATION_EMPTY", "migration file is empty", file.filename));
@@ -3974,6 +4496,25 @@ function validateMigrations(input) {
3974
4496
  if (input.requireDownForDestructive && hasDestructiveDdl(parsed.up) && !parsed.down) {
3975
4497
  out.push(err("MIGRATION_DESTRUCTIVE_NO_DOWN", 'migration contains destructive DDL (DROP TABLE/COLUMN or ALTER ... DROP) but has no "-- DOWN" section. Add one so uninstall --purgeData can roll back.', file.filename));
3976
4498
  }
4499
+ narrowKeys.push(...keyColumnSets(parsed.up).map(narrowKey).filter((k) => k !== null).map((cols) => ({ cols, filename: file.filename })));
4500
+ for (const parts of keyColumnSets(parsed.up)) {
4501
+ const wide = widenedKey(parts);
4502
+ if (wide)
4503
+ widened.add(wide);
4504
+ }
4505
+ uuidRefs.push(...userRefUuidColumns(parsed.up).map((column) => ({ column, filename: file.filename })));
4506
+ for (const column of userRefFixedColumns(parsed.up))
4507
+ fixedRefs.add(column);
4508
+ }
4509
+ for (const { column, filename } of uuidRefs) {
4510
+ if (fixedRefs.has(column))
4511
+ continue;
4512
+ out.push(err("SCHEMA_USER_REF_UUID", `"${column}" is declared UUID and its name says it records a user. ` + '`"user".id` is a 32-character nanoid, so Postgres rejects every write with ' + '22P02 "invalid input syntax for type uuid" \u2014 the route then fails for ' + "everybody, always, with an error naming the type rather than the cause. " + "Declare it TEXT.", filename));
4513
+ }
4514
+ for (const { cols, filename } of narrowKeys) {
4515
+ if (widened.has(cols))
4516
+ continue;
4517
+ out.push(err("SCHEMA_UNIQUE_WITHOUT_TENANT", `UNIQUE (${cols}) leaves out tenant_id. Several companies share one ` + "instance, so the second one to use a value is refused it \u2014 its own invoice " + "number, its own product code, its own fiscal year \u2014 and row-level security " + "hides the conflicting row, so it is shown a database error about something " + `it cannot see and cannot resolve. Use UNIQUE (tenant_id, ${cols}) unless ` + "the value must be unique across the whole instance, as a token or a hash is.", filename));
3977
4518
  }
3978
4519
  return out;
3979
4520
  }
@@ -3997,6 +4538,215 @@ function validateBundleSize(input) {
3997
4538
  }
3998
4539
  return [];
3999
4540
  }
4541
+ var SDUI_SHARED_READ_ALLOWLIST = new Set([
4542
+ "/api/collections",
4543
+ "/api/views"
4544
+ ]);
4545
+ function normalizeRoute(p) {
4546
+ return p.split("?")[0].replace(/\{[^}]+\}/g, "*").replace(/:[^/]+/g, "*").replace(/\/+$/, "") || "/";
4547
+ }
4548
+ var SDUI_I18N_SLOTS = [
4549
+ "title",
4550
+ "subtitle",
4551
+ "label",
4552
+ "placeholder",
4553
+ "note",
4554
+ "confirm",
4555
+ "description",
4556
+ "emptyText",
4557
+ "emptyTitle",
4558
+ "hint"
4559
+ ];
4560
+ var I18N_UNIVERSAL = new Set([
4561
+ "API",
4562
+ "JSON",
4563
+ "NDJSON",
4564
+ "CSV",
4565
+ "SQL",
4566
+ "URL",
4567
+ "UUID",
4568
+ "ID",
4569
+ "HTTP",
4570
+ "HTTPS",
4571
+ "GET",
4572
+ "POST",
4573
+ "PUT",
4574
+ "PATCH",
4575
+ "DELETE",
4576
+ "Zveltio",
4577
+ "OK",
4578
+ "UTC",
4579
+ "AI",
4580
+ "SDK",
4581
+ "CLI",
4582
+ "RLS",
4583
+ "DB",
4584
+ "PDF",
4585
+ "XML",
4586
+ "YAML"
4587
+ ]);
4588
+ var I18N_PRODUCT_NAMES = new Set([
4589
+ "HubSpot",
4590
+ "Notion",
4591
+ "Airtable",
4592
+ "Okta",
4593
+ "Entra ID",
4594
+ "Slack",
4595
+ "Stripe",
4596
+ "Salesforce",
4597
+ "ANAF"
4598
+ ]);
4599
+ function isProductNameList(s) {
4600
+ const parts = s.split(/[/,\u00B7|]/).map((p) => p.trim()).filter(Boolean);
4601
+ return parts.length > 0 && parts.every((p) => I18N_PRODUCT_NAMES.has(p));
4602
+ }
4603
+ function looksLikeMessageKey(s) {
4604
+ return /^[a-z][a-z0-9]*(\.[a-z0-9_]+)+$/i.test(s);
4605
+ }
4606
+ function looksTranslatable(s) {
4607
+ const t = s.trim();
4608
+ if (t.length < 3)
4609
+ return false;
4610
+ if (I18N_UNIVERSAL.has(t))
4611
+ return false;
4612
+ if (isProductNameList(t))
4613
+ return false;
4614
+ if (!/[a-z]{2}/.test(t))
4615
+ return false;
4616
+ if (/^[a-z0-9_.\-/]+$/.test(t))
4617
+ return false;
4618
+ if (/^[a-z0-9_]+=/.test(t))
4619
+ return false;
4620
+ if (t.startsWith("http"))
4621
+ return false;
4622
+ return /^[A-Z]/.test(t) || t.split(/\s+/).length > 1;
4623
+ }
4624
+ function collectI18nStrings(node, acc) {
4625
+ if (Array.isArray(node)) {
4626
+ for (const x of node)
4627
+ collectI18nStrings(x, acc);
4628
+ return;
4629
+ }
4630
+ if (node && typeof node === "object") {
4631
+ for (const [k, v] of Object.entries(node)) {
4632
+ if (SDUI_I18N_SLOTS.includes(k) && typeof v === "string" && v)
4633
+ acc.push({ slot: k, value: v });
4634
+ else
4635
+ collectI18nStrings(v, acc);
4636
+ }
4637
+ }
4638
+ }
4639
+ function collectEndpoints(node, acc) {
4640
+ if (Array.isArray(node)) {
4641
+ for (const x of node)
4642
+ collectEndpoints(x, acc);
4643
+ return;
4644
+ }
4645
+ if (node && typeof node === "object") {
4646
+ for (const [k, v] of Object.entries(node)) {
4647
+ if ((k === "dataSource" || k === "endpoint" || k === "saveEndpoint" || k === "catalogDataSource" || k === "loadEndpoint") && typeof v === "string")
4648
+ acc.push(v);
4649
+ else
4650
+ collectEndpoints(v, acc);
4651
+ }
4652
+ }
4653
+ }
4654
+ function validateSduiSchema(input) {
4655
+ const out = [];
4656
+ const { schema, extName, file } = input;
4657
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
4658
+ out.push(err("SDUI_NOT_OBJECT", "schema must be a JSON object", file));
4659
+ return out;
4660
+ }
4661
+ const s = schema;
4662
+ if (typeof s.title !== "string" || !s.title)
4663
+ out.push(err("SDUI_NO_TITLE", 'schema missing "title"', file));
4664
+ if (s.kind === "settings") {
4665
+ if (typeof s.dataSource !== "string" || typeof s.saveEndpoint !== "string")
4666
+ out.push(err("SDUI_SETTINGS_SHAPE", "settings schema needs dataSource + saveEndpoint", file));
4667
+ } else if (!Array.isArray(s.resources) || s.resources.length === 0) {
4668
+ out.push(err("SDUI_NO_RESOURCES", 'page schema needs a non-empty "resources" array', file));
4669
+ }
4670
+ const mount = `/ext/${extName}`;
4671
+ const depMounts = (input.dependencies ?? []).map((d) => `/ext/${d}`);
4672
+ const provided = input.providedRoutes ? new Set(input.providedRoutes.map(normalizeRoute)) : undefined;
4673
+ const inMount = (bare, m) => bare === m || bare.startsWith(`${m}/`);
4674
+ const endpoints = [];
4675
+ collectEndpoints(schema, endpoints);
4676
+ for (const raw of endpoints) {
4677
+ const bare = raw.split("?")[0];
4678
+ if (SDUI_SHARED_READ_ALLOWLIST.has(bare))
4679
+ continue;
4680
+ if (depMounts.some((m) => inMount(bare, m)))
4681
+ continue;
4682
+ if (!inMount(bare, mount)) {
4683
+ out.push(err("SDUI_ENDPOINT_FOREIGN", `endpoint "${raw}" is outside this extension's namespace "${mount}/" (or a declared dependency, or the shared-read allowlist). Schemas may only call their own routes.`, file));
4684
+ continue;
4685
+ }
4686
+ if (provided) {
4687
+ const rel = normalizeRoute(bare.slice(mount.length) || "/");
4688
+ if (!isProvidedRoute(provided, rel)) {
4689
+ out.push(err("SDUI_ENDPOINT_UNKNOWN", `endpoint "${raw}" resolves to "${rel}", which the engine does not serve. Provided routes: ${[...provided].sort().join(", ") || "(none found)"}.`, file));
4690
+ }
4691
+ }
4692
+ }
4693
+ if (input.messageKeys) {
4694
+ const known = new Set(input.messageKeys);
4695
+ const strings = [];
4696
+ collectI18nStrings(schema, strings);
4697
+ for (const { slot, value } of strings) {
4698
+ if (known.has(value))
4699
+ continue;
4700
+ if (looksLikeMessageKey(value)) {
4701
+ out.push(err("SDUI_I18N_KEY_MISSING", `${slot} "${value}" is not a known message key. The host renders an unknown key as literal text, so this reaches the user as-is. Add it to studio/messages/{locale}.json, or use a shared common.* key.`, file));
4702
+ } else if (looksTranslatable(value)) {
4703
+ out.push(warn("SDUI_I18N_HARDCODED", `${slot} "${value}" is hardcoded text, not a message key \u2014 it stays English in all nine locales. Move it to studio/messages/{locale}.json.`, file));
4704
+ }
4705
+ }
4706
+ }
4707
+ return out;
4708
+ }
4709
+ function joinRoutePath(prefix, child) {
4710
+ if (!child || child === "/")
4711
+ return prefix || "/";
4712
+ const p = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
4713
+ return child.startsWith("/") ? `${p}${child}` : `${p}/${child}`;
4714
+ }
4715
+ function isProvidedRoute(provided, rel) {
4716
+ if (provided.has(rel))
4717
+ return true;
4718
+ for (const p of provided) {
4719
+ if (!p.endsWith("/*"))
4720
+ continue;
4721
+ const base = p.slice(0, -2);
4722
+ if (!base || base === "/")
4723
+ continue;
4724
+ if (rel === base || rel.startsWith(`${base}/`))
4725
+ return true;
4726
+ }
4727
+ return false;
4728
+ }
4729
+ function extractEngineRoutes(engineSourceTexts) {
4730
+ const routes = new Set;
4731
+ const methodRe = /\.(?:get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/g;
4732
+ const mountRe = /\.route\(\s*['"`]([^'"`]+)['"`]/g;
4733
+ for (const text of engineSourceTexts) {
4734
+ let m;
4735
+ while ((m = methodRe.exec(text)) !== null) {
4736
+ if (m[1].startsWith("/"))
4737
+ routes.add(m[1]);
4738
+ }
4739
+ while ((m = mountRe.exec(text)) !== null) {
4740
+ const prefix = m[1];
4741
+ if (!prefix.startsWith("/"))
4742
+ continue;
4743
+ routes.add(prefix);
4744
+ if (prefix !== "/")
4745
+ routes.add(joinRoutePath(prefix, "/*"));
4746
+ }
4747
+ }
4748
+ return [...routes];
4749
+ }
4000
4750
  function validateExtension(input) {
4001
4751
  const errors = [
4002
4752
  ...validateManifest(input.manifest),
@@ -4006,7 +4756,7 @@ function validateExtension(input) {
4006
4756
  ...input.bundleSize ? validateBundleSize(input.bundleSize) : []
4007
4757
  ];
4008
4758
  return {
4009
- ok: errors.length === 0,
4759
+ ok: errors.every((e) => e.severity === "warning"),
4010
4760
  errors,
4011
4761
  stats: {
4012
4762
  tables: input.stats.tables,
@@ -4017,6 +4767,7 @@ function validateExtension(input) {
4017
4767
  }
4018
4768
 
4019
4769
  // src/commands/extension-validate.ts
4770
+ init_extension_pack();
4020
4771
  var c7 = {
4021
4772
  bold: (s) => `\x1B[1m${s}\x1B[0m`,
4022
4773
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -4034,14 +4785,16 @@ var PEER_DEPS_ALLOWLIST = new Set([
4034
4785
  "@aws-sdk/s3-request-presigner",
4035
4786
  "nanoid",
4036
4787
  "qrcode",
4788
+ "pdf-lib",
4789
+ "@pdf-lib/fontkit",
4037
4790
  "pdfkit",
4038
4791
  "graphql"
4039
4792
  ]);
4040
4793
  function recursiveSize(dir) {
4041
4794
  let total = 0;
4042
- if (!existsSync10(dir))
4795
+ if (!existsSync11(dir))
4043
4796
  return 0;
4044
- for (const name of readdirSync2(dir)) {
4797
+ for (const name of readdirSync3(dir)) {
4045
4798
  const full = join10(dir, name);
4046
4799
  if (name === "node_modules" || name === ".zveltio" || name === "dist")
4047
4800
  continue;
@@ -4058,14 +4811,119 @@ function recursiveSize(dir) {
4058
4811
  }
4059
4812
  return total;
4060
4813
  }
4814
+ function readEngineSources(dir) {
4815
+ const bundled = join10(dir, "engine", "index.js");
4816
+ if (existsSync11(bundled)) {
4817
+ try {
4818
+ return [readFileSync4(bundled, "utf8")];
4819
+ } catch {}
4820
+ }
4821
+ const out = [];
4822
+ const walk = (d) => {
4823
+ if (!existsSync11(d))
4824
+ return;
4825
+ for (const name of readdirSync3(d)) {
4826
+ if (name === "migrations" || name === "node_modules")
4827
+ continue;
4828
+ const full = join10(d, name);
4829
+ let st;
4830
+ try {
4831
+ st = statSync(full);
4832
+ } catch {
4833
+ continue;
4834
+ }
4835
+ if (st.isDirectory())
4836
+ walk(full);
4837
+ else if (/\.(ts|js)$/.test(name)) {
4838
+ try {
4839
+ out.push(readFileSync4(full, "utf8"));
4840
+ } catch {}
4841
+ }
4842
+ }
4843
+ };
4844
+ walk(join10(dir, "engine"));
4845
+ return out;
4846
+ }
4847
+ function readOwnMessageKeys(dir) {
4848
+ const path = join10(dir, "studio", "messages", "en.json");
4849
+ if (!existsSync11(path))
4850
+ return;
4851
+ try {
4852
+ const parsed = JSON.parse(readFileSync4(path, "utf8"));
4853
+ return new Set(Object.keys(parsed));
4854
+ } catch {
4855
+ return;
4856
+ }
4857
+ }
4858
+ function readDependencyMessageKeys(dir, extName, deps) {
4859
+ const out = new Set;
4860
+ const depth = extName.split("/").length;
4861
+ const root = join10(dir, ...Array(depth).fill(".."));
4862
+ for (const dep of deps) {
4863
+ const path = join10(root, dep, "studio", "messages", "en.json");
4864
+ if (!existsSync11(path))
4865
+ continue;
4866
+ try {
4867
+ for (const k of Object.keys(JSON.parse(readFileSync4(path, "utf8"))))
4868
+ out.add(k);
4869
+ } catch {}
4870
+ }
4871
+ return out;
4872
+ }
4061
4873
  function inferExpectedName(dir) {
4062
4874
  const norm = dir.replace(/\\/g, "/");
4063
- const idx = norm.indexOf("zveltio-extensions/");
4875
+ const marker = "zveltio-extensions/";
4876
+ const idx = norm.lastIndexOf(marker);
4064
4877
  if (idx >= 0) {
4065
- return norm.slice(idx + "zveltio-extensions/".length).replace(/\/$/, "");
4878
+ return norm.slice(idx + marker.length).replace(/\/$/, "");
4066
4879
  }
4067
4880
  return norm.split("/").filter(Boolean).pop() ?? "";
4068
4881
  }
4882
+ function checkBundleIntegrity(dir, manifest) {
4883
+ const bundle = join10(dir, "engine", "index.js");
4884
+ const integrity = manifest?.integrity;
4885
+ if (!integrity)
4886
+ return [];
4887
+ if (!existsSync11(bundle)) {
4888
+ return integrity.engineSha256 && existsSync11(join10(dir, "engine", "index.ts")) ? [
4889
+ {
4890
+ code: "BUNDLE_MISSING",
4891
+ message: "manifest records integrity.engineSha256 but engine/index.js does not exist. " + "Run `zveltio extension pack`.",
4892
+ file: "engine/index.js"
4893
+ }
4894
+ ] : [];
4895
+ }
4896
+ const out = [];
4897
+ if (integrity.engineSha256) {
4898
+ const actual = createHash2("sha256").update(readFileSync4(bundle)).digest("hex");
4899
+ if (actual !== integrity.engineSha256) {
4900
+ out.push({
4901
+ code: "BUNDLE_HASH_MISMATCH",
4902
+ message: `engine/index.js (sha256 ${actual.slice(0, 12)}\u2026) does not match ` + `integrity.engineSha256 (${integrity.engineSha256.slice(0, 12)}\u2026). ` + "Re-pack \u2014 the engine refuses a bundle whose hash disagrees with its manifest.",
4903
+ file: "engine/index.js"
4904
+ });
4905
+ }
4906
+ }
4907
+ if (!existsSync11(join10(dir, "engine", "index.ts")))
4908
+ return out;
4909
+ if (!integrity.sourceSha256) {
4910
+ out.push({
4911
+ code: "BUNDLE_SOURCE_UNRECORDED",
4912
+ severity: "warning",
4913
+ message: "manifest has no integrity.sourceSha256, so nothing can tell whether the bundle " + "was built from the committed source. Re-pack to record it.",
4914
+ file: "manifest.json"
4915
+ });
4916
+ return out;
4917
+ }
4918
+ if (hashEngineSources(dir) !== integrity.sourceSha256) {
4919
+ out.push({
4920
+ code: "BUNDLE_OLDER_THAN_SOURCE",
4921
+ message: "engine/index.js was NOT built from the current engine/ sources, so the changes in " + "them do not run anywhere. Run `zveltio extension pack`.",
4922
+ file: "engine/index.js"
4923
+ });
4924
+ }
4925
+ return out;
4926
+ }
4069
4927
  async function extensionValidateCommand(opts = {}) {
4070
4928
  const dir = opts.dir ?? process.cwd();
4071
4929
  console.log(`
@@ -4074,7 +4932,7 @@ ${c7.bold("Extension validate")}
4074
4932
  console.log(` Extension dir: ${c7.dim(dir)}`);
4075
4933
  const manifestPath = join10(dir, "manifest.json");
4076
4934
  let manifest = null;
4077
- if (existsSync10(manifestPath)) {
4935
+ if (existsSync11(manifestPath)) {
4078
4936
  try {
4079
4937
  manifest = JSON.parse(readFileSync4(manifestPath, "utf8"));
4080
4938
  } catch (e) {
@@ -4086,8 +4944,8 @@ ${c7.bold("Extension validate")}
4086
4944
  const migrationsDir = join10(dir, "engine", "migrations");
4087
4945
  let sqlFiles = [];
4088
4946
  let parsedTableCount = 0;
4089
- if (existsSync10(migrationsDir)) {
4090
- sqlFiles = readdirSync2(migrationsDir).filter((f) => f.endsWith(".sql")).sort().map((filename) => ({
4947
+ if (existsSync11(migrationsDir)) {
4948
+ sqlFiles = readdirSync3(migrationsDir).filter((f) => f.endsWith(".sql")).sort().map((filename) => ({
4091
4949
  filename,
4092
4950
  sql: readFileSync4(join10(migrationsDir, filename), "utf8")
4093
4951
  }));
@@ -4099,7 +4957,7 @@ ${c7.bold("Extension validate")}
4099
4957
  }
4100
4958
  const paths = {};
4101
4959
  for (const rel of ["manifest.json", "engine/index.ts", "engine/index.js"]) {
4102
- paths[rel] = existsSync10(join10(dir, rel));
4960
+ paths[rel] = existsSync11(join10(dir, rel));
4103
4961
  }
4104
4962
  const required = ["manifest.json"];
4105
4963
  if (!isStudioOnly && !(paths["engine/index.ts"] || paths["engine/index.js"])) {
@@ -4126,6 +4984,60 @@ ${c7.bold("Extension validate")}
4126
4984
  },
4127
4985
  stats: { tables: parsedTableCount, migrations: sqlFiles.length }
4128
4986
  });
4987
+ const sduiErrors = [];
4988
+ {
4989
+ const pages = manifest && typeof manifest === "object" ? manifest?.studio?.pages ?? [] : [];
4990
+ const schemaPages = (Array.isArray(pages) ? pages : []).filter((p) => typeof p?.schema === "string");
4991
+ if (schemaPages.length > 0) {
4992
+ const manifestRecord = manifest && typeof manifest === "object" ? manifest : undefined;
4993
+ const extName = typeof manifestRecord?.name === "string" ? manifestRecord.name : inferExpectedName(dir);
4994
+ const provided = extractEngineRoutes(readEngineSources(dir));
4995
+ const ownMessageKeys = readOwnMessageKeys(dir);
4996
+ const dependencies = Array.isArray(manifest?.dependencies) ? manifest.dependencies.map((d) => typeof d === "string" ? d : d?.name).filter((x) => typeof x === "string") : [];
4997
+ for (const p of schemaPages) {
4998
+ const rel = join10("studio", p.schema).replace(/\\/g, "/");
4999
+ const abs = join10(dir, "studio", p.schema);
5000
+ let parsed = null;
5001
+ if (!existsSync11(abs)) {
5002
+ sduiErrors.push({
5003
+ code: "SDUI_SCHEMA_MISSING",
5004
+ message: "schema file not found",
5005
+ file: rel
5006
+ });
5007
+ continue;
5008
+ }
5009
+ try {
5010
+ parsed = JSON.parse(readFileSync4(abs, "utf8"));
5011
+ } catch (e) {
5012
+ sduiErrors.push({
5013
+ code: "SDUI_SCHEMA_BAD_JSON",
5014
+ message: `not valid JSON: ${e.message}`,
5015
+ file: rel
5016
+ });
5017
+ continue;
5018
+ }
5019
+ sduiErrors.push(...validateSduiSchema({
5020
+ schema: parsed,
5021
+ extName,
5022
+ providedRoutes: provided,
5023
+ dependencies,
5024
+ messageKeys: ownMessageKeys && new Set([
5025
+ ...ownMessageKeys,
5026
+ ...SHARED_MESSAGE_KEYS,
5027
+ ...readDependencyMessageKeys(dir, extName, dependencies)
5028
+ ]),
5029
+ file: rel
5030
+ }));
5031
+ }
5032
+ result.errors.push(...sduiErrors);
5033
+ const sduiHard = sduiErrors.filter((e) => e.severity !== "warning");
5034
+ if (sduiHard.length > 0)
5035
+ result.ok = false;
5036
+ const sduiWarn = sduiErrors.length - sduiHard.length;
5037
+ const sduiSummary = sduiHard.length > 0 ? c7.red(`${sduiHard.length} error(s)`) : sduiWarn > 0 ? c7.yellow(`${schemaPages.length} OK, ${sduiWarn} warning(s)`) : c7.green(`${schemaPages.length} OK`);
5038
+ console.log(` SDUI schemas: ${sduiSummary}`);
5039
+ }
5040
+ }
4129
5041
  const m = manifest;
4130
5042
  const studioOnly = isStudioOnly;
4131
5043
  const v2Status = (() => {
@@ -4170,8 +5082,8 @@ ${c7.bold("Extension validate")}
4170
5082
  }
4171
5083
  console.log("");
4172
5084
  if (isolation !== "worker" && !inlineOk) {
4173
- console.error(c7.red(`Validation failed: ${resolved.tier} publishers must declare ` + `engine.isolation: "worker" (MARKETPLACE-POLICY \xA72).`));
4174
- console.error(c7.dim(' Add `"isolation": "worker"` to the engine block in manifest.json, ' + "then re-pack. If you are a verified/first-party publisher, pass " + "--first-party or set ZVELTIO_REGISTRY_TOKEN so the tier can be confirmed. " + "See docs/MARKETPLACE-POLICY.md \xA72."));
5085
+ console.error(c7.red(`Validation failed: ${resolved.tier} publishers must declare ` + `engine.isolation: "worker" (marketplace-policy.md \xA72).`));
5086
+ console.error(c7.dim(' Add `"isolation": "worker"` to the engine block in manifest.json, ' + "then re-pack. If you are a verified/first-party publisher, pass " + "--first-party or set ZVELTIO_REGISTRY_TOKEN so the tier can be confirmed. " + "See docs/extensions/marketplace-policy.md \xA72."));
4175
5087
  if (opts.silentExit)
4176
5088
  throw new Error("Validation failed: community inline isolation");
4177
5089
  process.exit(1);
@@ -4179,7 +5091,7 @@ ${c7.bold("Extension validate")}
4179
5091
  }
4180
5092
  if (v2Status === "v1-legacy") {
4181
5093
  console.error(c7.red("Validation failed: manifest is v1 (no engine.bundled block)."));
4182
- console.error(c7.dim(" v1 manifests are unsupported since 1.0.0-beta.1. Run `zveltio extension pack` to " + "generate the v2 engine + integrity blocks. See docs/EXTENSION-DEVELOPER-GUIDE.md \xA74."));
5094
+ console.error(c7.dim(" v1 manifests are unsupported since 1.0.0-beta.1. Run `zveltio extension pack` to " + "generate the v2 engine + integrity blocks. See docs/extensions/developer-guide.md \xA74."));
4183
5095
  if (opts.silentExit)
4184
5096
  throw new Error("Validation failed: v1 manifest unsupported");
4185
5097
  process.exit(1);
@@ -4191,28 +5103,43 @@ ${c7.bold("Extension validate")}
4191
5103
  throw new Error("Validation failed: incomplete v2 manifest");
4192
5104
  process.exit(1);
4193
5105
  }
5106
+ const integrityErrors = checkBundleIntegrity(dir, manifest);
5107
+ if (integrityErrors.length > 0) {
5108
+ result.errors.push(...integrityErrors);
5109
+ if (integrityErrors.some((e) => e.severity !== "warning"))
5110
+ result.ok = false;
5111
+ }
5112
+ const warnings = result.errors.filter((e) => e.severity === "warning");
5113
+ const hardErrors = result.errors.filter((e) => e.severity !== "warning");
5114
+ if (warnings.length > 0) {
5115
+ console.log(c7.yellow(`${warnings.length} warning(s):`));
5116
+ for (const e of warnings)
5117
+ printError(e, "warning");
5118
+ console.log("");
5119
+ }
4194
5120
  if (result.ok) {
4195
5121
  console.log(c7.green("Validation passed."));
4196
5122
  console.log(c7.dim(" Run `zveltio extension types` to refresh generated types, then `zveltio extension publish` when ready."));
4197
5123
  console.log("");
4198
5124
  return;
4199
5125
  }
4200
- console.log(c7.red(`Validation failed with ${result.errors.length} error(s):`));
5126
+ console.log(c7.red(`Validation failed with ${hardErrors.length} error(s):`));
4201
5127
  console.log("");
4202
- for (const e of result.errors) {
4203
- printError(e);
5128
+ for (const e of hardErrors) {
5129
+ printError(e, "error");
4204
5130
  }
4205
5131
  console.log("");
4206
5132
  if (!opts.silentExit)
4207
5133
  process.exit(1);
4208
5134
  }
4209
- function printError(e) {
5135
+ function printError(e, severity = "error") {
4210
5136
  const loc = e.file ? c7.dim(` (${e.file})`) : "";
4211
- console.log(` ${c7.red(e.code)} ${e.message}${loc}`);
5137
+ const code = severity === "warning" ? c7.yellow(e.code) : c7.red(e.code);
5138
+ console.log(` ${code} ${e.message}${loc}`);
4212
5139
  }
4213
5140
 
4214
5141
  // src/commands/extension-publish.ts
4215
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
5142
+ import { existsSync as existsSync13, mkdirSync as mkdirSync4, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync4 } from "fs";
4216
5143
  import { join as join12, resolve as resolve2 } from "path";
4217
5144
  import { tmpdir } from "os";
4218
5145
 
@@ -4281,16 +5208,16 @@ init_extension_pack();
4281
5208
 
4282
5209
  // src/commands/keys.ts
4283
5210
  import {
4284
- existsSync as existsSync11,
5211
+ existsSync as existsSync12,
4285
5212
  mkdirSync as mkdirSync3,
4286
- readdirSync as readdirSync3,
5213
+ readdirSync as readdirSync4,
4287
5214
  readFileSync as readFileSync5,
4288
5215
  writeFileSync as writeFileSync3,
4289
5216
  chmodSync,
4290
5217
  statSync as statSync2
4291
5218
  } from "fs";
4292
5219
  import { join as join11 } from "path";
4293
- import { homedir, platform } from "os";
5220
+ import { homedir as homedir2, platform } from "os";
4294
5221
  var c8 = {
4295
5222
  bold: (s) => `\x1B[1m${s}\x1B[0m`,
4296
5223
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -4299,11 +5226,11 @@ var c8 = {
4299
5226
  dim: (s) => `\x1B[2m${s}\x1B[0m`
4300
5227
  };
4301
5228
  function keysDir() {
4302
- return join11(homedir(), ".zveltio", "keys");
5229
+ return join11(homedir2(), ".zveltio", "keys");
4303
5230
  }
4304
5231
  function ensureKeysDir() {
4305
5232
  const dir = keysDir();
4306
- if (!existsSync11(dir)) {
5233
+ if (!existsSync12(dir)) {
4307
5234
  mkdirSync3(dir, { recursive: true });
4308
5235
  if (platform() !== "win32") {
4309
5236
  try {
@@ -4327,7 +5254,7 @@ function writeKeyFile(kp) {
4327
5254
  }
4328
5255
  function readKeyFile(keyId) {
4329
5256
  const path = keyPath(keyId);
4330
- if (!existsSync11(path)) {
5257
+ if (!existsSync12(path)) {
4331
5258
  throw new Error(`Keypair "${keyId}" not found at ${path}. Run \`zveltio keys generate --id ${keyId}\` first.`);
4332
5259
  }
4333
5260
  const raw = readFileSync5(path, "utf8");
@@ -4341,10 +5268,10 @@ function resolveKeyId(explicit) {
4341
5268
  if (explicit)
4342
5269
  return explicit;
4343
5270
  const dir = keysDir();
4344
- if (!existsSync11(dir)) {
5271
+ if (!existsSync12(dir)) {
4345
5272
  throw new Error("No keypairs found. Run `zveltio keys generate` first.");
4346
5273
  }
4347
- const ids = readdirSync3(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
5274
+ const ids = readdirSync4(dir).filter((f) => f.endsWith(".json")).map((f) => f.slice(0, -5));
4348
5275
  if (ids.length === 0)
4349
5276
  throw new Error("No keypairs found. Run `zveltio keys generate` first.");
4350
5277
  if (ids.length > 1) {
@@ -4355,7 +5282,7 @@ function resolveKeyId(explicit) {
4355
5282
  async function keysGenerateCommand(opts = {}) {
4356
5283
  ensureKeysDir();
4357
5284
  const kp = await generateKeypair(opts.id);
4358
- if (existsSync11(keyPath(kp.keyId)) && !opts.force) {
5285
+ if (existsSync12(keyPath(kp.keyId)) && !opts.force) {
4359
5286
  console.error(c8.red(`Key "${kp.keyId}" already exists. Pass --force to overwrite.`));
4360
5287
  process.exit(1);
4361
5288
  }
@@ -4379,11 +5306,11 @@ ${c8.bold("Public key (share with registry admin):")}
4379
5306
  }
4380
5307
  async function keysListCommand() {
4381
5308
  const dir = keysDir();
4382
- if (!existsSync11(dir)) {
5309
+ if (!existsSync12(dir)) {
4383
5310
  console.log(`${c8.yellow("No keypairs yet.")} Run ${c8.dim("zveltio keys generate")} to create one.`);
4384
5311
  return;
4385
5312
  }
4386
- const files = readdirSync3(dir).filter((f) => f.endsWith(".json")).sort();
5313
+ const files = readdirSync4(dir).filter((f) => f.endsWith(".json")).sort();
4387
5314
  if (files.length === 0) {
4388
5315
  console.log(`${c8.yellow("No keypairs yet.")} Run ${c8.dim("zveltio keys generate")} to create one.`);
4389
5316
  return;
@@ -4420,7 +5347,7 @@ var c9 = {
4420
5347
  };
4421
5348
  function readManifest2(dir) {
4422
5349
  const path = join12(dir, "manifest.json");
4423
- if (!existsSync12(path)) {
5350
+ if (!existsSync13(path)) {
4424
5351
  throw new Error(`No manifest.json at ${path}. Run from the extension's root or pass --dir.`);
4425
5352
  }
4426
5353
  const raw = readFileSync6(path, "utf8");
@@ -4460,7 +5387,7 @@ async function createArchive(dir, outFile) {
4460
5387
  throw new Error(`tar failed (exit ${code})`);
4461
5388
  }
4462
5389
  async function runStudioBuild(dir) {
4463
- if (!existsSync12(join12(dir, "studio")))
5390
+ if (!existsSync13(join12(dir, "studio")))
4464
5391
  return;
4465
5392
  const proc = Bun.spawn(["bun", "run", "build"], {
4466
5393
  cwd: join12(dir, "studio"),
@@ -4472,7 +5399,7 @@ async function runStudioBuild(dir) {
4472
5399
  throw new Error(`Studio build failed (exit ${code})`);
4473
5400
  }
4474
5401
  async function runEnginePack(dir, tierOpts) {
4475
- if (!existsSync12(join12(dir, "engine", "index.ts")))
5402
+ if (!existsSync13(join12(dir, "engine", "index.ts")))
4476
5403
  return;
4477
5404
  await extensionPackCommand({ dir, ...tierOpts });
4478
5405
  }
@@ -4569,7 +5496,7 @@ ${c9.green("Dry-run complete")} \u2014 skipped archive/sign/publish.
4569
5496
  console.log(`
4570
5497
  ${c9.bold("Step 4/6: archive")}`);
4571
5498
  const tmpDir = opts.output ? resolve2(opts.output) : join12(tmpdir(), `zveltio-publish-${Date.now()}`);
4572
- if (!existsSync12(tmpDir))
5499
+ if (!existsSync13(tmpDir))
4573
5500
  mkdirSync4(tmpDir, { recursive: true });
4574
5501
  const zvextPath = join12(tmpDir, `${artifactBaseName(manifest)}.zvext`);
4575
5502
  try {
@@ -4671,10 +5598,10 @@ ${c9.bold("Step 6/6: upload")}`);
4671
5598
  }
4672
5599
 
4673
5600
  // src/commands/extension-dev.ts
4674
- import { existsSync as existsSync13, statSync as statSync4 } from "fs";
5601
+ import { existsSync as existsSync14, statSync as statSync4 } from "fs";
4675
5602
  import { watch as fsWatch } from "fs";
4676
- import { join as join13, resolve as resolve3, relative as relative3 } from "path";
4677
- import { readdirSync as readdirSync4 } from "fs";
5603
+ import { join as join13, resolve as resolve3, relative as relative4 } from "path";
5604
+ import { readdirSync as readdirSync5 } from "fs";
4678
5605
  var c10 = {
4679
5606
  bold: (s) => `\x1B[1m${s}\x1B[0m`,
4680
5607
  green: (s) => `\x1B[32m${s}\x1B[0m`,
@@ -4687,7 +5614,7 @@ function readManifestName(dir, override) {
4687
5614
  if (override)
4688
5615
  return override;
4689
5616
  const manifestPath = join13(dir, "manifest.json");
4690
- if (!existsSync13(manifestPath)) {
5617
+ if (!existsSync14(manifestPath)) {
4691
5618
  throw new Error(`No manifest.json at ${manifestPath}. Pass --name or run from the extension's root.`);
4692
5619
  }
4693
5620
  const Bun_ = globalThis.Bun;
@@ -4703,7 +5630,7 @@ function readManifestName(dir, override) {
4703
5630
  return m.name;
4704
5631
  }
4705
5632
  function collectEngineFiles(engineDir) {
4706
- if (!existsSync13(engineDir))
5633
+ if (!existsSync14(engineDir))
4707
5634
  return [];
4708
5635
  const out = [];
4709
5636
  const stack = [engineDir];
@@ -4711,7 +5638,7 @@ function collectEngineFiles(engineDir) {
4711
5638
  const dir = stack.pop();
4712
5639
  let entries;
4713
5640
  try {
4714
- entries = readdirSync4(dir);
5641
+ entries = readdirSync5(dir);
4715
5642
  } catch {
4716
5643
  continue;
4717
5644
  }
@@ -4778,17 +5705,20 @@ ${c10.bold("Extension dev")}
4778
5705
  process.exit(1);
4779
5706
  }
4780
5707
  let studioProc = null;
4781
- if (!opts.noStudio && existsSync13(join13(dir, "studio"))) {
5708
+ const studioPkg = join13(dir, "studio", "package.json");
5709
+ if (!opts.noStudio && existsSync14(studioPkg)) {
4782
5710
  studioProc = Bun.spawn(["bun", "run", "dev"], {
4783
5711
  cwd: join13(dir, "studio"),
4784
5712
  stdout: "inherit",
4785
5713
  stderr: "inherit"
4786
5714
  });
4787
- console.log(` Studio: ${c10.green("watching (vite dev)")}`);
5715
+ console.log(` Studio: ${c10.green("watching (legacy studio/package.json vite dev)")}`);
5716
+ } else if (!opts.noStudio && existsSync14(join13(dir, "studio"))) {
5717
+ console.log(` Studio: ${c10.dim("tier-3 pages \u2014 run packages/studio dev after sync-extensions (see dev guide \xA72)")}`);
4788
5718
  } else {
4789
5719
  console.log(` Studio: ${c10.dim(opts.noStudio ? "skipped (--no-studio)" : "no studio/ folder \u2014 skipped")}`);
4790
5720
  }
4791
- if (!existsSync13(engineDir)) {
5721
+ if (!existsSync14(engineDir)) {
4792
5722
  console.log(` Engine src: ${c10.yellow("no engine/ folder \u2014 engine watch disabled")}`);
4793
5723
  } else {
4794
5724
  const files = collectEngineFiles(engineDir);
@@ -4797,7 +5727,7 @@ ${c10.bold("Extension dev")}
4797
5727
  let lastTrigger = "";
4798
5728
  const debounceMs = 250;
4799
5729
  const onChange = (filePath) => {
4800
- lastTrigger = relative3(dir, filePath);
5730
+ lastTrigger = relative4(dir, filePath);
4801
5731
  if (timer)
4802
5732
  clearTimeout(timer);
4803
5733
  timer = setTimeout(async () => {
@@ -4899,7 +5829,7 @@ ${c11.bold("Generate Types")}
4899
5829
  }
4900
5830
 
4901
5831
  // src/commands/install.ts
4902
- import { existsSync as existsSync14 } from "fs";
5832
+ import { existsSync as existsSync15 } from "fs";
4903
5833
  import { readFile, mkdir as mkdir2, cp } from "fs/promises";
4904
5834
  import { join as join14 } from "path";
4905
5835
  async function installCommand(name, opts) {
@@ -4914,7 +5844,7 @@ async function installCommand(name, opts) {
4914
5844
  await installFromCatalog(name, engineUrl);
4915
5845
  }
4916
5846
  async function installFromPath(name, sourcePath, engineUrl, force) {
4917
- if (!existsSync14(join14(sourcePath, "manifest.json"))) {
5847
+ if (!existsSync15(join14(sourcePath, "manifest.json"))) {
4918
5848
  console.error("\u274C No manifest.json found at", sourcePath);
4919
5849
  process.exit(1);
4920
5850
  }
@@ -4945,7 +5875,7 @@ async function installFromPath(name, sourcePath, engineUrl, force) {
4945
5875
  console.log(` Version: ${manifest.version || "?"}`);
4946
5876
  console.log(` Category: ${category}`);
4947
5877
  const enginePkgPath = join14(process.cwd(), "packages/engine/package.json");
4948
- if (existsSync14(enginePkgPath)) {
5878
+ if (existsSync15(enginePkgPath)) {
4949
5879
  const enginePkg = JSON.parse(await readFile(enginePkgPath, "utf-8"));
4950
5880
  const engineVersion = enginePkg.version || "unknown";
4951
5881
  if (manifest.zveltioMinVersion) {
@@ -4957,7 +5887,7 @@ async function installFromPath(name, sourcePath, engineUrl, force) {
4957
5887
  }
4958
5888
  const extName = manifest.name;
4959
5889
  const targetDir = join14(process.cwd(), "extensions", category, extName);
4960
- if (existsSync14(targetDir) && !force) {
5890
+ if (existsSync15(targetDir) && !force) {
4961
5891
  console.error(`\u274C Extension already exists at ${targetDir}`);
4962
5892
  console.error(` Use --force to overwrite.`);
4963
5893
  process.exit(1);
@@ -5123,6 +6053,7 @@ async function promptHidden(question) {
5123
6053
  }
5124
6054
  async function createGodCommand(opts) {
5125
6055
  const engineUrl = opts.url || process.env.ENGINE_URL || "http://localhost:3000";
6056
+ const recoveryToken = opts.recoveryToken || process.env.RECOVERY_TOKEN || "";
5126
6057
  console.log(`
5127
6058
  \u26A0\uFE0F SYSTEM RECOVERY OVERRIDE ACCOUNT`);
5128
6059
  console.log("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501");
@@ -5200,28 +6131,29 @@ Creating God (super-admin) user at ${engineUrl}
5200
6131
  const err2 = await res.json().catch(() => ({ message: res.statusText }));
5201
6132
  throw new Error(err2.message || `HTTP ${res.status}`);
5202
6133
  }
5203
- const { user } = await res.json();
5204
- console.log("\uD83D\uDD11 Granting god permissions...");
5205
- const loginRes = await fetch(`${engineUrl}/api/auth/sign-in/email`, {
5206
- method: "POST",
5207
- headers: { "Content-Type": "application/json" },
5208
- body: JSON.stringify({ email: email.trim(), password })
5209
- });
5210
- if (!loginRes.ok) {
5211
- console.warn("\u26A0\uFE0F Could not auto-grant admin role \u2014 grant manually via permissions API");
6134
+ await res.json();
6135
+ if (!recoveryToken) {
5212
6136
  console.log(`
5213
- \u2705 User created: ${email}
6137
+ \u2705 User created: ${email.trim()}`);
6138
+ console.warn(`
6139
+ \u26A0\uFE0F NOT granted the god role \u2014 no recovery token available.`);
6140
+ console.warn(" `god` is handed out by one endpoint only, and it requires a token:");
6141
+ console.warn(" RECOVERY_TOKEN=<32+ chars> zveltio create-god --email \u2026");
6142
+ console.warn(" zveltio create-god --recovery-token <token> --email \u2026");
6143
+ console.warn(" The engine must be started with the same RECOVERY_TOKEN set.");
6144
+ console.warn(" With direct database access, the release binary does it without a");
6145
+ console.warn(` token: DATABASE_URL=\u2026 zveltio create-god --email \u2026 --password \u2026
5214
6146
  `);
5215
- return;
6147
+ process.exit(1);
5216
6148
  }
5217
- const sessionCookie = loginRes.headers.get("set-cookie") || "";
5218
- const permRes = await fetch(`${engineUrl}/api/permissions/assign-role`, {
6149
+ console.log("\uD83D\uDD11 Granting god permissions...");
6150
+ const permRes = await fetch(`${engineUrl}/api/permissions/bootstrap`, {
5219
6151
  method: "POST",
5220
6152
  headers: {
5221
6153
  "Content-Type": "application/json",
5222
- Cookie: sessionCookie
6154
+ Authorization: `Bearer ${recoveryToken}`
5223
6155
  },
5224
- body: JSON.stringify({ userId: user.id, role: "god" })
6156
+ body: JSON.stringify({ email: email.trim() })
5225
6157
  });
5226
6158
  if (permRes.ok) {
5227
6159
  console.log(`
@@ -5229,15 +6161,20 @@ Creating God (super-admin) user at ${engineUrl}
5229
6161
  console.log(` Email: ${email.trim()}`);
5230
6162
  console.log(` Name: ${name.trim()}`);
5231
6163
  console.log(` Role: god
6164
+ `);
6165
+ console.log(" The recovery token is now spent. Rotate RECOVERY_TOKEN before the");
6166
+ console.log(` next recovery, and note that any previous god was stood down.
5232
6167
  `);
5233
6168
  console.log(`\uD83D\uDC49 Log in at: ${engineUrl}/admin
5234
6169
  `);
5235
6170
  } else {
6171
+ const body = await permRes.json().catch(() => ({}));
5236
6172
  console.log(`
5237
6173
  \u2705 User created: ${email.trim()}`);
5238
- console.warn("\u26A0\uFE0F Could not auto-assign god role.");
5239
- console.warn(` Use the Permissions page in Studio to assign the admin role manually.
6174
+ console.error(`
6175
+ \u274C The role was NOT granted: ${body.error ?? `engine returned ${permRes.status}`}
5240
6176
  `);
6177
+ process.exit(1);
5241
6178
  }
5242
6179
  } catch (err2) {
5243
6180
  rl2.close();
@@ -5319,20 +6256,27 @@ var rollbackCommand = new Command("rollback").description("Rollback database mig
5319
6256
  process.exit(1);
5320
6257
  }
5321
6258
  process.env.DATABASE_URL = databaseUrl;
5322
- const dbPath = new URL("../../../engine/src/db/index.js", import.meta.url).href;
5323
- const migrationsPath = new URL("../../../engine/src/db/migrations/index.js", import.meta.url).href;
5324
- const { initDatabase } = await import(dbPath);
5325
- const { getLastAppliedMigration, rollbackMigration } = await import(migrationsPath);
6259
+ let engine;
6260
+ try {
6261
+ engine = await loadEngineMigrationModules();
6262
+ } catch (err2) {
6263
+ console.error(`
6264
+ \u274C ${err2.message}
6265
+ `);
6266
+ process.exit(1);
6267
+ }
6268
+ const { initDatabase } = engine.db;
6269
+ const { getLastAppliedMigration, rollbackMigration } = engine.migrations;
5326
6270
  let targetVersion;
5327
6271
  if (opts.to !== undefined) {
5328
6272
  targetVersion = parseInt(opts.to);
5329
- if (isNaN(targetVersion)) {
6273
+ if (Number.isNaN(targetVersion)) {
5330
6274
  console.error("\u274C --to must be a valid integer");
5331
6275
  process.exit(1);
5332
6276
  }
5333
6277
  } else {
5334
6278
  const steps = parseInt(opts.steps);
5335
- if (isNaN(steps) || steps < 1) {
6279
+ if (Number.isNaN(steps) || steps < 1) {
5336
6280
  console.error("\u274C --steps must be a positive integer");
5337
6281
  process.exit(1);
5338
6282
  }
@@ -5430,7 +6374,7 @@ var versionCommand = new Command("version").description("Show version informatio
5430
6374
  });
5431
6375
 
5432
6376
  // src/commands/update.ts
5433
- import { existsSync as existsSync15 } from "fs";
6377
+ import { existsSync as existsSync16 } from "fs";
5434
6378
  import { join as join15 } from "path";
5435
6379
  var c12 = {
5436
6380
  bold: (s) => `\x1B[1m${s}\x1B[0m`,
@@ -5448,7 +6392,7 @@ ${c12.bold("Zveltio Update")}
5448
6392
  const installDir = opts.dir || process.env.ZVELTIO_DIR || join15(process.cwd(), "zveltio");
5449
6393
  const metaPath = join15(installDir, ".zveltio-install.json");
5450
6394
  let meta = null;
5451
- if (existsSync15(metaPath)) {
6395
+ if (existsSync16(metaPath)) {
5452
6396
  try {
5453
6397
  meta = JSON.parse(await Bun.file(metaPath).text());
5454
6398
  } catch {}
@@ -5465,8 +6409,8 @@ ${c12.bold("Zveltio Update")}
5465
6409
  throw new Error(`Registry returned ${res.status}`);
5466
6410
  }
5467
6411
  versionsData = await res.json();
5468
- if (!versionsData?.latest) {
5469
- throw new Error('Invalid versions response (missing "latest" field)');
6412
+ if (!versionsData?.latest && !versionsData?.latest_beta) {
6413
+ throw new Error('Invalid versions response (no "latest" or "latest_beta")');
5470
6414
  }
5471
6415
  } catch (err2) {
5472
6416
  if (opts.check) {
@@ -5496,6 +6440,13 @@ ${c12.bold("Zveltio Update")}
5496
6440
  process.exit(1);
5497
6441
  }
5498
6442
  const targetVersion = opts.version || (opts.channel === "beta" ? versionsData.latest_beta ?? versionsData.latest : versionsData.latest);
6443
+ if (!targetVersion) {
6444
+ console.error(c12.red(`
6445
+ No stable release has been published yet.`));
6446
+ console.error(c12.dim(` Latest beta: v${versionsData.latest_beta} \u2014 run ${c12.cyan("zveltio update --channel beta")}
6447
+ `));
6448
+ process.exit(1);
6449
+ }
5499
6450
  console.log(` Target version: ${c12.cyan(`v${targetVersion}`)}`);
5500
6451
  if (opts.check) {
5501
6452
  const hasUpdate = compareVersions(targetVersion, currentVersion) > 0;
@@ -5557,7 +6508,7 @@ async function getVersionFromPackageJson() {
5557
6508
  join15(process.cwd(), "package.json")
5558
6509
  ];
5559
6510
  for (const pkgPath of candidates) {
5560
- if (!existsSync15(pkgPath))
6511
+ if (!existsSync16(pkgPath))
5561
6512
  continue;
5562
6513
  try {
5563
6514
  const pkg = JSON.parse(await Bun.file(pkgPath).text());
@@ -5581,14 +6532,14 @@ async function getCurrentVersionFromAPI(port) {
5581
6532
  }
5582
6533
  async function runUpdateScript(targetVersion, installDir, meta, force) {
5583
6534
  const envPath = join15(installDir, ".env");
5584
- if (existsSync15(envPath)) {
6535
+ if (existsSync16(envPath)) {
5585
6536
  const backupPath = `${envPath}.backup.${Date.now()}`;
5586
6537
  const envContent = await Bun.file(envPath).text();
5587
6538
  await Bun.write(backupPath, envContent);
5588
6539
  console.log(` Backed up .env \u2192 ${backupPath}`);
5589
6540
  }
5590
6541
  const installScript = join15(installDir, "install.sh");
5591
- if (existsSync15(installScript)) {
6542
+ if (existsSync16(installScript)) {
5592
6543
  const args = [
5593
6544
  "bash",
5594
6545
  installScript,
@@ -5607,7 +6558,7 @@ async function runUpdateScript(targetVersion, installDir, meta, force) {
5607
6558
  if (code !== 0) {
5608
6559
  console.error(`
5609
6560
  Update failed. Your previous version is still running.`);
5610
- if (existsSync15(envPath)) {
6561
+ if (existsSync16(envPath)) {
5611
6562
  console.log(` Restore .env: copy ${envPath}.backup.* to ${envPath}`);
5612
6563
  }
5613
6564
  process.exit(1);
@@ -5772,14 +6723,14 @@ ${c13.bold("Zveltio Status")}
5772
6723
  console.log("");
5773
6724
  }
5774
6725
  async function getLocalVersion() {
5775
- const { existsSync: existsSync16 } = await import("fs");
6726
+ const { existsSync: existsSync17 } = await import("fs");
5776
6727
  const { join: join16 } = await import("path");
5777
6728
  const candidates = [
5778
6729
  join16(process.cwd(), "packages/engine/package.json"),
5779
6730
  join16(process.cwd(), "package.json")
5780
6731
  ];
5781
6732
  for (const p of candidates) {
5782
- if (!existsSync16(p))
6733
+ if (!existsSync17(p))
5783
6734
  continue;
5784
6735
  try {
5785
6736
  const pkg = JSON.parse(await Bun.file(p).text());
@@ -5792,7 +6743,7 @@ async function getLocalVersion() {
5792
6743
  // package.json
5793
6744
  var package_default = {
5794
6745
  name: "@zveltio/cli",
5795
- version: "3.0.0-beta.5",
6746
+ version: "3.0.0-beta.68",
5796
6747
  type: "module",
5797
6748
  bin: {
5798
6749
  zveltio: "./dist/index.js"
@@ -5807,8 +6758,8 @@ var package_default = {
5807
6758
  dev: "bun run --watch src/index.ts"
5808
6759
  },
5809
6760
  dependencies: {
5810
- "@zveltio/sdk": "3.0.0-beta.5",
5811
- commander: "^12.0.0"
6761
+ "@zveltio/sdk": "3.0.0-beta.68",
6762
+ commander: "^12.1.0"
5812
6763
  },
5813
6764
  devDependencies: {
5814
6765
  "bun-types": "latest",
@@ -5832,7 +6783,7 @@ program2.addCommand(rollbackCommand);
5832
6783
  program2.command("status").description("Show system status (DB, cache, uptime, version)").option("--url <url>", "Engine URL", "http://localhost:3000").option("--json", "Output as JSON").action(statusCommand);
5833
6784
  program2.addCommand(versionCommand);
5834
6785
  program2.addCommand(updateCommand);
5835
- program2.command("create-god").description("Create the first super-admin (god) user interactively").option("--url <url>", "Engine URL", "http://localhost:3000").option("--email <email>", "Admin email (skip prompt)").option("--name <name>", "Admin name (skip prompt)").action(createGodCommand);
6786
+ program2.command("create-god").description("Create the first super-admin (god) user interactively").option("--url <url>", "Engine URL", "http://localhost:3000").option("--email <email>", "Admin email (skip prompt)").option("--name <name>", "Admin name (skip prompt)").option("--recovery-token <token>", "RECOVERY_TOKEN the engine was started with \u2014 required to grant the god role (env: RECOVERY_TOKEN)").action(createGodCommand);
5836
6787
  var generate = program2.command("generate").description("Code generation utilities");
5837
6788
  generate.command("types [collection]").description("Generate TypeScript types for collections (writes to ./types/zveltio.d.ts)").option("-o, --output <path>", "Output file path", "./types/zveltio.d.ts").option("--url <url>", "Engine URL", "http://localhost:3000").action(generateTypesCommand);
5838
6789
  program2.command("generate-types [collection]").description("Generate TypeScript types for your collections").option("-o, --output <path>", "Output file path", "./types/zveltio.d.ts").option("--url <url>", "Engine URL", "http://localhost:3000").action(generateTypesCommand);
@@ -5860,10 +6811,10 @@ extensions.command("disable <name>").description("Disable an active extension").
5860
6811
  console.log(`${name} disabled.`);
5861
6812
  });
5862
6813
  var ext = program2.command("extension").description('Manage Zveltio extensions (use "extensions" for new commands)');
5863
- ext.command("create <name>").description("Scaffold a new extension").option("--category <category>", "Extension category", "custom").action((name, opts) => extensionCommand("create", name, opts));
6814
+ ext.command("create <name>").description("Scaffold a new extension (admin page as an SDUI schema)").option("--category <category>", "Extension category", "custom").option("--code-page", "Scaffold a Svelte page instead of a schema \u2014 for UI a schema cannot express (canvas, chat, map, inbox)").action((name, opts) => extensionCommand("create", name, opts));
5864
6815
  ext.command("build").description("[deprecated] Alias for `extension pack` \u2014 produces the v2 engine bundle").option("--dir <dir>", "Extension root directory (defaults to cwd)").action((opts) => extensionCommand("build", "", opts));
5865
6816
  ext.command("dev").description("Watch engine/ + studio/, hot-reload the running engine on changes (S4-03)").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--url <url>", "Engine URL (env: ZVELTIO_ENGINE_URL)", "http://localhost:3000").option("--name <name>", "Extension name (default: read from manifest.json)").option("--no-studio", "Skip the Studio dev process (engine watch only)").action((opts) => extensionDevCommand(opts));
5866
- ext.command("pack").description("Bundle engine/index.ts \u2192 engine/index.js (target=bun, ESM) and write engine + integrity blocks into manifest.json. Required before publish on v2 channels.").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--sourcemap", "Emit engine/index.js.map alongside the bundle").option("--no-manifest-update", "Build the bundle but do not patch manifest.json").option("--first-party", "Vendor / monorepo build \u2014 keep inline isolation, skip worker auto-inject").option("--token <token>", "Registry token for the publisher-tier lookup (env: ZVELTIO_REGISTRY_TOKEN)").option("--registry-url <url>", "Registry base URL (env: ZVELTIO_REGISTRY_URL)").action(async (opts) => {
6817
+ ext.command("pack").description("Bundle engine/index.ts \u2192 engine/index.js (target=bun, ESM) and write engine + integrity blocks into manifest.json. Required before publish on v2 channels.").option("--dir <dir>", "Extension root directory (defaults to cwd)").option("--sourcemap", "Emit engine/index.js.map alongside the bundle").option("--no-manifest-update", "Build the bundle but do not patch manifest.json").option("--first-party", "Vendor / monorepo build \u2014 keep inline isolation, skip worker auto-inject").option("--keep-isolation", 'With --first-party, retain engine.isolation: "worker" if already set (default: clear sticky worker)').option("--token <token>", "Registry token for the publisher-tier lookup (env: ZVELTIO_REGISTRY_TOKEN)").option("--registry-url <url>", "Registry base URL (env: ZVELTIO_REGISTRY_URL)").action(async (opts) => {
5867
6818
  const { extensionPackCommand: extensionPackCommand2 } = await Promise.resolve().then(() => (init_extension_pack(), exports_extension_pack));
5868
6819
  return extensionPackCommand2(opts);
5869
6820
  });