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 +12 -0
- package/dist/{chunk-UJGDKNDM.js → chunk-BAP7PHBF.js} +147 -7
- package/dist/deploy.js +1 -1
- package/dist/index.js +111 -25
- package/package.json +5 -5
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
|
-
/**
|
|
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
|
|
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
|
|
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\
|
|
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
|
-
|
|
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
package/dist/index.js
CHANGED
|
@@ -136,7 +136,7 @@ import {
|
|
|
136
136
|
warningLines,
|
|
137
137
|
withFunctionRetries,
|
|
138
138
|
writeProjectLink
|
|
139
|
-
} from "./chunk-
|
|
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
|
|
1707
|
+
const listed2 = await usersCall(
|
|
1708
1708
|
() => client.backend.listUsers(projectId, { page, perPage: PAGE_SIZE })
|
|
1709
1709
|
);
|
|
1710
|
-
total =
|
|
1711
|
-
users.push(...
|
|
1712
|
-
if (
|
|
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 [
|
|
2369
|
-
if (
|
|
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 =
|
|
2381
|
-
const name = cellText(
|
|
2382
|
-
const type = cellText(
|
|
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) =>
|
|
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
|
-
|
|
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
|
|
3368
|
-
const policies = wanted === void 0 ? all : all.filter((policy) => policy.table
|
|
3369
|
-
const
|
|
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(
|
|
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
|
|
4757
|
+
const listed2 = await client.domains.list(projectId);
|
|
4674
4758
|
const verified = [];
|
|
4675
|
-
for (const domain of
|
|
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
|
|
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(
|
|
11303
|
+
if (isOldSearchRefusal(listed2.error))
|
|
11220
11304
|
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
11221
|
-
const page = unwrap(
|
|
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.
|
|
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/
|
|
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/
|
|
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",
|