naro-cli 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -897,6 +897,18 @@ An `--update-check` you give replaces the default: without
897
897
 
898
898
  ## Release notes
899
899
 
900
+ ### 0.29.0
901
+
902
+ - `naro usage` shows Hobby custom domains as `1 per project` (an apex and its
903
+ `www.` form count as one) instead of `No limit`.
904
+ - `naro policy rules` marks an action allowed by a policy written as
905
+ `CREATE POLICY` as `custom`, and one covered by a policy it cannot read as
906
+ `invalid`, and lists those policies under the table. `--json` is unchanged.
907
+ - The hints under `naro policy rules --env <name>` point at that environment.
908
+ - naro-mcp 0.28.0 goes out with it: `get_team_usage` explains
909
+ `domainsPerProject` and `perProject`, and `list_policy_rules` says that
910
+ `none` means no rule `naro policy set` wrote, not nobody.
911
+
900
912
  ### 0.28.0
901
913
 
902
914
  - Next.js 15 and below are refused up front with `NEXT_VERSION_UNSUPPORTED`
@@ -306,6 +306,9 @@ function sanitizeRoutes(value) {
306
306
  return JSON.stringify(routes).length > MAX_ROUTE_LIST_BYTES ? void 0 : routes;
307
307
  }
308
308
 
309
+ // ../../packages/core/src/static-assets.ts
310
+ var IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
311
+
309
312
  // ../../packages/core/src/artifact-manifest.ts
310
313
  var MAX_FILE_BYTES = 25 * 1024 * 1024;
311
314
  var MAX_ARTIFACT_BYTES = 64 * 1024 * 1024;
@@ -2511,12 +2514,20 @@ var DEPLOYMENT_ERROR_CODES = [
2511
2514
  "DATABASE_FULL",
2512
2515
  /** Usage limits spec 2026-10-18 §6: a hobby team created its 100 deployments this UTC month. */
2513
2516
  "USAGE_LIMIT_REACHED",
2517
+ /** Hobby: another limit (requests, D1, Storage, Functions, Realtime) is reached, so new builds wait (spec 2026-11-07-hobby-any-limit-blocks-build). */
2518
+ "BUILDS_PAUSED",
2514
2519
  /** A hobby team has its 3 projects; no project can be added. */
2515
2520
  "PROJECT_LIMIT_REACHED",
2516
2521
  /** A hobby team has its 1 member; no member or invite can be added. */
2517
2522
  "MEMBER_LIMIT_REACHED",
2518
- /** Custom domains are not part of the Hobby plan. */
2523
+ /**
2524
+ * Custom domains were not part of the Hobby plan. No longer answered since
2525
+ * 202611070030 (one domain per hobby project); kept so an older client
2526
+ * still names it.
2527
+ */
2519
2528
  "PLAN_DOMAINS_NOT_INCLUDED",
2529
+ /** A hobby project has its 1 custom domain (with its www form); no other can be added. */
2530
+ "PLAN_DOMAIN_LIMIT_REACHED",
2520
2531
  /** Named environments (spec 2026-10-18-named-environments §15): the name is not lowercase letters, digits and single hyphens, or is over 20 characters. */
2521
2532
  "ENVIRONMENT_NAME_INVALID",
2522
2533
  /** `production` and `preview` are reserved names. */
@@ -17567,7 +17578,7 @@ var REQUESTS = {
17567
17578
  subject100: (team) => `${team} has reached its monthly request limit`,
17568
17579
  usage: (team, used, limit, month) => `${team} has used ${used} of the ${limit} requests the Hobby plan includes for ${month}.`,
17569
17580
  warning: "When it reaches the limit, requests past it may be blocked until the month ends (UTC).",
17570
- blocked: "Sites on this team answer 503 until the month ends (UTC).",
17581
+ blocked: "Sites on this team are blocked (HTTP 403 or 503) until the month ends (UTC).",
17571
17582
  running: "Sites keep running for now.",
17572
17583
  button: "View usage",
17573
17584
  footer: (team) => `You're receiving this because you're an owner or billing member of ${team} on Naro.`
@@ -17577,7 +17588,7 @@ var REQUESTS = {
17577
17588
  subject100: (team) => `${team} \uD300\uC774 \uC774\uBC88 \uB2EC \uC694\uCCAD \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4`,
17578
17589
  usage: (team, used, limit, month) => `${team} \uD300\uC774 ${month} Hobby \uD50C\uB79C\uC5D0 \uD3EC\uD568\uB41C \uC694\uCCAD ${limit}\uAC74 \uC911 ${used}\uAC74\uC744 \uC37C\uC2B5\uB2C8\uB2E4.`,
17579
17590
  warning: "\uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD558\uBA74 \uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uADF8 \uB4A4 \uC694\uCCAD\uC774 \uB9C9\uD790 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
17580
- blocked: "\uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uC774 \uD300\uC758 \uC0AC\uC774\uD2B8\uB294 503\uC73C\uB85C \uC751\uB2F5\uD569\uB2C8\uB2E4.",
17591
+ blocked: "\uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uC774 \uD300\uC758 \uC0AC\uC774\uD2B8\uB294 \uC694\uCCAD\uC744 \uB9C9\uC2B5\uB2C8\uB2E4(HTTP 403 \uB610\uB294 503).",
17581
17592
  running: "\uC9C0\uAE08\uC740 \uC0AC\uC774\uD2B8\uAC00 \uACC4\uC18D \uB3D9\uC791\uD569\uB2C8\uB2E4.",
17582
17593
  button: "\uC0AC\uC6A9\uB7C9 \uBCF4\uAE30",
17583
17594
  footer: (team) => `Naro\uC5D0\uC11C ${team} \uD300\uC758 owner \uB610\uB294 billing \uC5ED\uD560\uC774\uB77C \uC774 \uBA54\uC77C\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`
@@ -17587,7 +17598,7 @@ var REQUESTS = {
17587
17598
  subject100: (team) => `${team} \u30C1\u30FC\u30E0\u304C\u4ECA\u6708\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F`,
17588
17599
  usage: (team, used, limit, month) => `${team} \u30C1\u30FC\u30E0\u306F ${month} \u306E Hobby \u30D7\u30E9\u30F3\u306B\u542B\u307E\u308C\u308B ${limit} \u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u3046\u3061 ${used} \u3092\u4F7F\u7528\u3057\u307E\u3057\u305F\u3002`,
17589
17600
  warning: "\u4E0A\u9650\u306B\u9054\u3059\u308B\u3068\u3001\u6708\u672B(UTC)\u307E\u3067\u4EE5\u964D\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30D6\u30ED\u30C3\u30AF\u3055\u308C\u308B\u5834\u5408\u304C\u3042\u308A\u307E\u3059\u3002",
17590
- blocked: "\u6708\u672B(UTC)\u307E\u3067\u3001\u3053\u306E\u30C1\u30FC\u30E0\u306E\u30B5\u30A4\u30C8\u306F503\u3092\u8FD4\u3057\u307E\u3059\u3002",
17601
+ blocked: "\u6708\u672B(UTC)\u307E\u3067\u3001\u3053\u306E\u30C1\u30FC\u30E0\u306E\u30B5\u30A4\u30C8\u306F\u30EA\u30AF\u30A8\u30B9\u30C8\u3092\u62D2\u5426\u3057\u307E\u3059(HTTP 403 \u307E\u305F\u306F 503)\u3002",
17591
17602
  running: "\u73FE\u5728\u3001\u30B5\u30A4\u30C8\u306F\u5F15\u304D\u7D9A\u304D\u52D5\u4F5C\u3057\u3066\u3044\u307E\u3059\u3002",
17592
17603
  button: "\u4F7F\u7528\u91CF\u3092\u898B\u308B",
17593
17604
  footer: (team) => `Naro \u3067 ${team} \u30C1\u30FC\u30E0\u306E owner \u307E\u305F\u306F billing \u30ED\u30FC\u30EB\u306E\u305F\u3081\u3001\u3053\u306E\u30E1\u30FC\u30EB\u3092\u304A\u9001\u308A\u3057\u3066\u3044\u307E\u3059\u3002`
@@ -17739,6 +17750,63 @@ async function wrapNextConfig(appRoot, file2) {
17739
17750
  );
17740
17751
  return { wrapper: plan.wrapper, project: project2 };
17741
17752
  }
17753
+ function assetPrefixWrapperSource(projectSpecifier, typescript) {
17754
+ return `${typescript ? "// @ts-nocheck\n" : ""}// Written by Naro for this build only. The project's own next.config is
17755
+ // ${projectSpecifier}, unchanged; this wrapper serves _next/static from
17756
+ // Naro's CDN unless the project sets its own assetPrefix.
17757
+ import projectConfig from ${JSON.stringify(projectSpecifier)};
17758
+
17759
+ export default async function naroNextConfig(phase, context) {
17760
+ const config =
17761
+ typeof projectConfig === "function"
17762
+ ? await projectConfig(phase, context)
17763
+ : await projectConfig;
17764
+ const rest = { ...(config ?? {}) };
17765
+ return {
17766
+ ...rest,
17767
+ assetPrefix: rest.assetPrefix || process.env.NARO_STATIC_ASSET_PREFIX,
17768
+ };
17769
+ }
17770
+ `;
17771
+ }
17772
+ var ASSET_PREFIX_ONLY_CONFIG = `// Written by Naro for this build only: the project has no next.config.
17773
+ export default { assetPrefix: process.env.NARO_STATIC_ASSET_PREFIX };
17774
+ `;
17775
+ var CONFIG_LOOKUP = [
17776
+ "next.config.js",
17777
+ "next.config.mjs",
17778
+ "next.config.ts",
17779
+ "next.config.mts",
17780
+ "next.config.cjs"
17781
+ ];
17782
+ async function wrapNextConfigForAssetPrefix(appRoot, prefix) {
17783
+ if (!prefix) return "skipped";
17784
+ let file2;
17785
+ for (const name of CONFIG_LOOKUP)
17786
+ if (await exists(join2(appRoot, name))) {
17787
+ file2 = name;
17788
+ break;
17789
+ }
17790
+ if (!file2) {
17791
+ await writeFile2(join2(appRoot, "next.config.mjs"), ASSET_PREFIX_ONLY_CONFIG);
17792
+ return "created";
17793
+ }
17794
+ const plan = WRAPPABLE[file2];
17795
+ if (!plan) return "skipped";
17796
+ const project2 = `${PROJECT_CONFIG_BASENAME}.${file2.slice("next.config.".length)}`;
17797
+ if (await exists(join2(appRoot, project2))) return "skipped";
17798
+ await rename(join2(appRoot, file2), join2(appRoot, project2));
17799
+ try {
17800
+ await writeFile2(
17801
+ join2(appRoot, plan.wrapper),
17802
+ assetPrefixWrapperSource(plan.specifier, plan.typescript)
17803
+ );
17804
+ } catch (error51) {
17805
+ await rename(join2(appRoot, project2), join2(appRoot, file2));
17806
+ throw error51;
17807
+ }
17808
+ return "wrapped";
17809
+ }
17742
17810
 
17743
17811
  // ../../packages/next-adapter/src/index.ts
17744
17812
  function nextAdapterPath(base2 = import.meta.url) {
@@ -19201,6 +19269,54 @@ async function readStaticArtifact(appRoot, outputDirectory, identity, options, o
19201
19269
  };
19202
19270
  }
19203
19271
 
19272
+ // ../../packages/build/src/static-cdn.ts
19273
+ var STATIC_ASSET_PREFIX_ENV = "NARO_STATIC_ASSET_PREFIX";
19274
+ var STATIC_ASSETS_ON_CDN_NOTE = "Static assets (_next/static) are served from static.naro.sh.";
19275
+ var STATIC_ASSETS_ON_WORKER_NOTE = "Static assets stay on the Worker: the build did not use Naro's asset prefix (the project sets its own assetPrefix or a basePath, or it is not a Next.js build).";
19276
+ function buildEnvWithStaticPrefix(env, prefix) {
19277
+ const result = { ...env };
19278
+ delete result[STATIC_ASSET_PREFIX_ENV];
19279
+ if (prefix) result[STATIC_ASSET_PREFIX_ENV] = prefix;
19280
+ return result;
19281
+ }
19282
+ function contains(file2, needle) {
19283
+ return Buffer.from(
19284
+ file2.data.buffer,
19285
+ file2.data.byteOffset,
19286
+ file2.data.byteLength
19287
+ ).includes(needle);
19288
+ }
19289
+ function withStaticAssetPrefix(artifact, prefix) {
19290
+ if (!prefix) return { artifact, note: null };
19291
+ const needle = Buffer.from(prefix, "utf8");
19292
+ const files = [artifact.entrypoint, ...artifact.modules, ...artifact.assets];
19293
+ if (!files.some((file2) => contains(file2, needle)))
19294
+ return { artifact, note: STATIC_ASSETS_ON_WORKER_NOTE };
19295
+ return {
19296
+ artifact: { ...artifact, staticAssetPrefix: prefix },
19297
+ note: STATIC_ASSETS_ON_CDN_NOTE
19298
+ };
19299
+ }
19300
+ var IMMUTABLE_HEADERS_FILE = "_headers";
19301
+ var IMMUTABLE_HEADERS = `/_next/static/*
19302
+ Cache-Control: ${IMMUTABLE_CACHE_CONTROL}
19303
+ `;
19304
+ function withImmutableHeaders(artifact) {
19305
+ if (artifact.assets.some((asset) => asset.name === IMMUTABLE_HEADERS_FILE))
19306
+ return artifact;
19307
+ return {
19308
+ ...artifact,
19309
+ assets: [
19310
+ ...artifact.assets,
19311
+ {
19312
+ name: IMMUTABLE_HEADERS_FILE,
19313
+ data: new TextEncoder().encode(IMMUTABLE_HEADERS),
19314
+ contentType: "text/plain"
19315
+ }
19316
+ ]
19317
+ };
19318
+ }
19319
+
19204
19320
  // ../../packages/build/src/strategy/fingerprint.ts
19205
19321
  import { createHash as createHash3 } from "crypto";
19206
19322
  import { readFile as readFile9 } from "fs/promises";
@@ -20352,6 +20468,17 @@ function createVinextCloudflareStrategy(options) {
20352
20468
  );
20353
20469
  if (!added.ok) return failed("prepare", describeFailure(added));
20354
20470
  }
20471
+ try {
20472
+ await wrapNextConfigForAssetPrefix(
20473
+ fork.appRoot,
20474
+ context.env[STATIC_ASSET_PREFIX_ENV]
20475
+ );
20476
+ } catch (error51) {
20477
+ await warn(
20478
+ observer,
20479
+ `Could not point static assets at static.naro.sh: ${describeError(error51)}`
20480
+ );
20481
+ }
20355
20482
  const projectBin = adopt ? null : await findProjectBin(fork.appRoot, fork.checkout, "vinext");
20356
20483
  const build = await runStep(
20357
20484
  context,
@@ -20920,7 +21047,7 @@ var LocalBuildExecutor = class {
20920
21047
  const context = {
20921
21048
  identity,
20922
21049
  branch: input.branch,
20923
- env: input.env ?? {},
21050
+ env: buildEnvWithStaticPrefix(input.env, input.staticAssetPrefix),
20924
21051
  // The toolchain add has to use the store the install linked
20925
21052
  // node_modules from (withSharedCache).
20926
21053
  packageManager: cachePorts ? withSharedCache(packageManager, pmCacheDir) : packageManager,
@@ -20976,7 +21103,10 @@ var LocalBuildExecutor = class {
20976
21103
  args: ["run", resolved.buildScript],
20977
21104
  cwd: appRoot,
20978
21105
  env: {
20979
- ...input.env,
21106
+ // Static presets never get the asset prefix (spec
21107
+ // 2026-11-07-static-asset-cdn §3): a Vite base or CRA
21108
+ // PUBLIC_URL also moves the router and public/ files.
21109
+ ...buildEnvWithStaticPrefix(input.env, void 0),
20980
21110
  NPM_CONFIG_USERCONFIG: packageManagerConfigPath
20981
21111
  },
20982
21112
  timeoutMs: 15 * 6e4,
@@ -21010,6 +21140,14 @@ var LocalBuildExecutor = class {
21010
21140
  appRoot,
21011
21141
  observer
21012
21142
  );
21143
+ const cdn = withStaticAssetPrefix(artifact, input.staticAssetPrefix);
21144
+ if (cdn.note)
21145
+ await observer.onLog({
21146
+ level: "INFO",
21147
+ stage: "DEPLOYING",
21148
+ message: cdn.note
21149
+ });
21150
+ artifact = withImmutableHeaders(cdn.artifact);
21013
21151
  return {
21014
21152
  gitCommitSha,
21015
21153
  artifact,
@@ -34236,11 +34374,13 @@ function summarizeTeamUsage(report, currentMonth) {
34236
34374
  };
34237
34375
  const meters = USAGE_METRICS.map((metric) => {
34238
34376
  const limit = report.limits?.[metric] ?? null;
34377
+ const perProject = metric === "domains" ? report.limits?.domainsPerProject ?? null : null;
34239
34378
  return {
34240
34379
  metric,
34241
34380
  used: used[metric],
34242
34381
  limit,
34243
- percent: limit ? Math.floor(used[metric] * 100 / limit) : null
34382
+ percent: limit ? Math.floor(used[metric] * 100 / limit) : null,
34383
+ perProject
34244
34384
  };
34245
34385
  });
34246
34386
  return {
package/dist/deploy.js CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  slugify,
15
15
  userAdapterUnreadable,
16
16
  workingTreeNotices
17
- } from "./chunk-UJGDKNDM.js";
17
+ } from "./chunk-BAP7PHBF.js";
18
18
  import "./chunk-K3NQKI34.js";
19
19
  export {
20
20
  assertFunctionsNode,
package/dist/index.js CHANGED
@@ -136,7 +136,7 @@ import {
136
136
  warningLines,
137
137
  withFunctionRetries,
138
138
  writeProjectLink
139
- } from "./chunk-UJGDKNDM.js";
139
+ } from "./chunk-BAP7PHBF.js";
140
140
  import "./chunk-K3NQKI34.js";
141
141
 
142
142
  // src/args.ts
@@ -1704,12 +1704,12 @@ async function allUsers(client, projectId) {
1704
1704
  const users = [];
1705
1705
  let total = 0;
1706
1706
  for (let page = 1; ; page++) {
1707
- const listed = await usersCall(
1707
+ const listed2 = await usersCall(
1708
1708
  () => client.backend.listUsers(projectId, { page, perPage: PAGE_SIZE })
1709
1709
  );
1710
- total = listed.total;
1711
- users.push(...listed.users);
1712
- if (listed.users.length < PAGE_SIZE || users.length >= total)
1710
+ total = listed2.total;
1711
+ users.push(...listed2.users);
1712
+ if (listed2.users.length < PAGE_SIZE || users.length >= total)
1713
1713
  return { users, total };
1714
1714
  }
1715
1715
  }
@@ -2365,8 +2365,8 @@ function chunk(items3, size) {
2365
2365
  }
2366
2366
  async function readSchema(client, projectId) {
2367
2367
  const schema = await client.db.query(projectId, { sql: SCHEMA_SQL });
2368
- const [listed, virtual, internal] = schema.statements;
2369
- if (listed === void 0 || virtual === void 0 || internal === void 0)
2368
+ const [listed2, virtual, internal] = schema.statements;
2369
+ if (listed2 === void 0 || virtual === void 0 || internal === void 0)
2370
2370
  throw new NaroError(
2371
2371
  "DATABASE_QUERY_FAILED",
2372
2372
  `The schema could not be read: the database answered with ${String(schema.statements.length)} result sets instead of 3.`
@@ -2377,9 +2377,9 @@ async function readSchema(client, projectId) {
2377
2377
  const virtualNames = new Set(
2378
2378
  virtual.rows.map((row) => cellText(virtual, row, "name").toLowerCase())
2379
2379
  );
2380
- const objects2 = listed.rows.map((row) => {
2381
- const name = cellText(listed, row, "name");
2382
- const type = cellText(listed, row, "type");
2380
+ const objects2 = listed2.rows.map((row) => {
2381
+ const name = cellText(listed2, row, "name");
2382
+ const type = cellText(listed2, row, "type");
2383
2383
  if (type === "view" || type === "trigger") return { type, name };
2384
2384
  return {
2385
2385
  type: virtualNames.has(name.toLowerCase()) ? "virtual" : "table",
@@ -3247,6 +3247,72 @@ function cellOf(value) {
3247
3247
  if (typeof value === "string") return value;
3248
3248
  return isInvalid(value) ? "invalid" : "condition";
3249
3249
  }
3250
+ var SET_POLICY_NAMES = new Set(
3251
+ POLICY_ACTIONS.map((action) => `naro_${action}`)
3252
+ );
3253
+ function customPolicies(named) {
3254
+ const byTable = /* @__PURE__ */ new Map();
3255
+ for (const policy of named) {
3256
+ if (SET_POLICY_NAMES.has(policy.name)) continue;
3257
+ const invalid = policy.invalid !== null;
3258
+ if (!invalid && policy.as !== "permissive") continue;
3259
+ const key = asciiLower(policy.table);
3260
+ const list = byTable.get(key) ?? [];
3261
+ list.push({ name: policy.name, command: policy.command, invalid });
3262
+ byTable.set(key, list);
3263
+ }
3264
+ return byTable;
3265
+ }
3266
+ function covers(policy, action) {
3267
+ return policy.command === "all" || policy.command === action;
3268
+ }
3269
+ var CUSTOM_NOTE = "custom: a policy written as CREATE POLICY allows it. naro policy list shows it.";
3270
+ var INVALID_NOTE = "invalid: a policy that cannot be read covers it, so the Data API refuses it (500 POLICY_INVALID). naro policy list shows why.";
3271
+ var NAMES_ONLY_NOTE = "`none` may not mean denied on those tables: naro policy list shows their policies.";
3272
+ function listed(entries) {
3273
+ return entries.map((entry) => `${printable(entry.name)} (${entry.command})`).toSorted().join(", ");
3274
+ }
3275
+ function customLines(policies, custom) {
3276
+ const lines = [];
3277
+ let allows = false;
3278
+ let unreadable2 = false;
3279
+ for (const policy of policies) {
3280
+ const table3 = printable(policy.table);
3281
+ if (custom) {
3282
+ const list = custom.get(asciiLower(policy.table)) ?? [];
3283
+ const valid = list.filter((entry) => !entry.invalid);
3284
+ const invalid = list.filter((entry) => entry.invalid);
3285
+ if (valid.length > 0) {
3286
+ allows = true;
3287
+ lines.push(`${table3} custom: ${listed(valid)}`);
3288
+ }
3289
+ if (invalid.length > 0) {
3290
+ unreadable2 = true;
3291
+ lines.push(`${table3} invalid: ${listed(invalid)}`);
3292
+ }
3293
+ continue;
3294
+ }
3295
+ const names = (policy.policies ?? []).filter(
3296
+ (name) => !SET_POLICY_NAMES.has(name)
3297
+ );
3298
+ if (names.length === 0) continue;
3299
+ lines.push(
3300
+ `${table3} also has policies written as SQL: ${names.map(printable).join(", ")}`
3301
+ );
3302
+ }
3303
+ if (lines.length === 0) return lines;
3304
+ if (!custom) return [...lines, NAMES_ONLY_NOTE];
3305
+ return [
3306
+ ...lines,
3307
+ ...allows ? [CUSTOM_NOTE] : [],
3308
+ ...unreadable2 ? [INVALID_NOTE] : []
3309
+ ];
3310
+ }
3311
+ var POLICY_TABLE_HINTS = /* @__PURE__ */ new Set([
3312
+ CUSTOM_NOTE,
3313
+ INVALID_NOTE,
3314
+ NAMES_ONLY_NOTE
3315
+ ]);
3250
3316
  function conditionText(action, value) {
3251
3317
  if (typeof value === "string") return value;
3252
3318
  if (isInvalid(value))
@@ -3262,11 +3328,19 @@ function conditionText(action, value) {
3262
3328
  const text = parts.join("; ");
3263
3329
  return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
3264
3330
  }
3265
- function policyTable(policies) {
3331
+ function policyTable(policies, custom) {
3266
3332
  const header = ["Table", ...POLICY_ACTIONS, "Owner column"];
3333
+ const shown = (policy, action) => {
3334
+ const value = policy[action];
3335
+ const sql2 = (custom?.get(asciiLower(policy.table)) ?? []).filter(
3336
+ (entry) => covers(entry, action)
3337
+ );
3338
+ if (sql2.some((entry) => entry.invalid)) return "invalid";
3339
+ return value === "none" && sql2.length > 0 ? "custom" : cellOf(value);
3340
+ };
3267
3341
  const rows = policies.map((policy) => [
3268
3342
  printable(policy.table),
3269
- ...POLICY_ACTIONS.map((action) => cellOf(policy[action])),
3343
+ ...POLICY_ACTIONS.map((action) => shown(policy, action)),
3270
3344
  policy.ownerColumn === null ? "-" : printable(policy.ownerColumn)
3271
3345
  ]);
3272
3346
  const widths = header.map(
@@ -3289,19 +3363,22 @@ function policyTable(policies) {
3289
3363
  }
3290
3364
  )
3291
3365
  );
3292
- if (lines.length === 0) return table3;
3366
+ const sql = customLines(policies, custom);
3367
+ const below = sql.length > 0 ? ["", ...sql] : [];
3368
+ if (lines.length === 0) return [...table3, ...below];
3293
3369
  const width = Math.max(...lines.map((line) => line.label.length));
3294
3370
  return [
3295
3371
  ...table3,
3296
3372
  "",
3297
- ...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
3373
+ ...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`),
3374
+ ...below
3298
3375
  ];
3299
3376
  }
3300
3377
  var UNSHOWN = "has characters that cannot be shown here";
3301
3378
  function hintsFor(lines, environment) {
3302
3379
  if (environment === null) return lines;
3303
3380
  return lines.map(
3304
- (line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") ? forEnvironment(line, environment) : line
3381
+ (line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") || POLICY_TABLE_HINTS.has(line) ? forEnvironment(line, environment) : line
3305
3382
  );
3306
3383
  }
3307
3384
  function statementLine(policy) {
@@ -3364,9 +3441,13 @@ async function runPolicyRules(context) {
3364
3441
  const client = await dataClient(context);
3365
3442
  const projectId = await context.resolveProjectId();
3366
3443
  const all = await client.backend.policies(projectId);
3367
- const wanted = table3?.toLowerCase();
3368
- const policies = wanted === void 0 ? all : all.filter((policy) => policy.table.toLowerCase() === wanted);
3369
- const lines = policies.length > 0 ? [...policyTable(policies), "", DEFAULT_DENY] : [
3444
+ const wanted = table3 === void 0 ? void 0 : asciiLower(table3);
3445
+ const policies = wanted === void 0 ? all : all.filter((policy) => asciiLower(policy.table) === wanted);
3446
+ const custom = context.flags.json || policies.length === 0 ? void 0 : await client.backend.rlsPolicies(projectId, table3 === void 0 ? {} : { table: table3 }).then(customPolicies, (error) => {
3447
+ if (!isNaroError(error)) throw error;
3448
+ return null;
3449
+ });
3450
+ const lines = policies.length > 0 ? [...policyTable(policies, custom), "", DEFAULT_DENY] : [
3370
3451
  table3 === void 0 ? "No tables yet." : `No table named ${printable(table3)}.`
3371
3452
  ];
3372
3453
  context.output.result(
@@ -3501,7 +3582,10 @@ async function runPolicySet(context) {
3501
3582
  hintsFor(
3502
3583
  [
3503
3584
  `\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
3504
- ...policyTable([policy]),
3585
+ ...policyTable(
3586
+ [policy],
3587
+ named === void 0 ? void 0 : customPolicies(named)
3588
+ ),
3505
3589
  ...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
3506
3590
  ],
3507
3591
  dataEnvironment(context.flags)
@@ -4670,9 +4754,9 @@ async function verifyAll(client, projectId, hostname, auto = false) {
4670
4754
  return [
4671
4755
  await client.domains.verifyByHostname(projectId, hostname, { auto })
4672
4756
  ];
4673
- const listed = await client.domains.list(projectId);
4757
+ const listed2 = await client.domains.list(projectId);
4674
4758
  const verified = [];
4675
- for (const domain of listed)
4759
+ for (const domain of listed2)
4676
4760
  verified.push(await client.domains.verify(projectId, domain.id, { auto }));
4677
4761
  return verified;
4678
4762
  }
@@ -11211,14 +11295,14 @@ async function listAll(storage, bucket, prefix, recursive, search) {
11211
11295
  truncated = true;
11212
11296
  break;
11213
11297
  }
11214
- const listed = await storage.from(bucket).list(folder, {
11298
+ const listed2 = await storage.from(bucket).list(folder, {
11215
11299
  limit: PAGE,
11216
11300
  offset,
11217
11301
  ...search === "" ? {} : { search }
11218
11302
  });
11219
- if (isOldSearchRefusal(listed.error))
11303
+ if (isOldSearchRefusal(listed2.error))
11220
11304
  throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
11221
- const page = unwrap(listed);
11305
+ const page = unwrap(listed2);
11222
11306
  for (const entry of page) {
11223
11307
  const path = folder === "" ? entry.name : `${folder}/${entry.name}`;
11224
11308
  if (isFolder(entry)) {
@@ -14067,6 +14151,8 @@ function meterRow(meter, limitsKnown) {
14067
14151
  const label3 = METRIC_LABELS[meter.metric];
14068
14152
  const used = formatCount(meter.used);
14069
14153
  if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
14154
+ if (typeof meter.perProject === "number")
14155
+ return [label3, used, `${formatCount(meter.perProject)} per project`, "\u2014"];
14070
14156
  if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
14071
14157
  if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
14072
14158
  return [label3, used, formatCount(meter.limit), usedPercent(meter)];
@@ -14408,7 +14494,7 @@ Usage:
14408
14494
  naro backend users delete <id|username|email>
14409
14495
  Delete a user and end its sessions (asks first; --yes to skip)
14410
14496
  naro policy list [--table <t>] Row-level security policies as CREATE POLICY (no policy = deny)
14411
- naro policy rules [--table <t>] Each table's policies as rules (public, authenticated, owner, none) and conditions
14497
+ naro policy rules [--table <t>] Each table's policies as rules (public, authenticated, owner, none, custom, invalid) and conditions
14412
14498
  naro policy create "CREATE POLICY <name> ON <table> \u2026" | --file <policies.sql>
14413
14499
  Postgres's CREATE POLICY: AS PERMISSIVE|RESTRICTIVE,
14414
14500
  FOR ALL|SELECT|INSERT|UPDATE|DELETE, TO anon|authenticated|public,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "naro-cli",
3
- "version": "0.28.0",
3
+ "version": "0.29.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "naro": "./dist/index.js"
@@ -37,13 +37,13 @@
37
37
  "typescript": "7.0.2",
38
38
  "vitest": "3.2.7",
39
39
  "naro-js": "0.9.0",
40
- "@next-deploy/build": "0.1.0",
41
- "@next-deploy/config": "0.1.0",
40
+ "@next-deploy/core": "0.1.0",
42
41
  "@next-deploy/data-core": "0.1.0",
43
42
  "@next-deploy/functions-runtime": "0.1.0",
44
- "@next-deploy/functions-bundler": "0.1.0",
45
43
  "@next-deploy/sdk": "0.1.0",
46
- "@next-deploy/core": "0.1.0"
44
+ "@next-deploy/config": "0.1.0",
45
+ "@next-deploy/functions-bundler": "0.1.0",
46
+ "@next-deploy/build": "0.1.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsup && pnpm build:types",