naro-cli 0.23.0 → 0.24.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 +25 -0
- package/dist/{chunk-ALK4VTL2.js → chunk-XJIWWZ2T.js} +95 -28
- package/dist/deploy.js +1 -1
- package/dist/env-services.d.ts +44 -0
- package/dist/index.js +52 -5
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -23,6 +23,13 @@ asks before opening a dotenv file to fill what is missing. Decline and it
|
|
|
23
23
|
prints the `naro env add` commands instead. With `--yes`, `--json` or outside
|
|
24
24
|
a TTY it never asks — it just lists what is missing and deploys.
|
|
25
25
|
|
|
26
|
+
When those names give away a service you are already running, the deploy says
|
|
27
|
+
so instead of leaving you with a list of names. An app that talks to an
|
|
28
|
+
existing Supabase project prints `Supabase — in that project's dashboard:` and
|
|
29
|
+
the screen each missing value is on, so moving the app here is a copy from one
|
|
30
|
+
tab to another. naro never calls Supabase: the values arrive like any other
|
|
31
|
+
env var, from `.env.local` once you agree or from `naro env add`.
|
|
32
|
+
|
|
26
33
|
Values you register are encrypted at rest, and `naro env pull` /
|
|
27
34
|
`naro env list --reveal` can read them back — your account credentials are
|
|
28
35
|
effectively your env credentials.
|
|
@@ -877,6 +884,24 @@ An `--update-check` you give replaces the default: without
|
|
|
877
884
|
|
|
878
885
|
## Release notes
|
|
879
886
|
|
|
887
|
+
### 0.24.0
|
|
888
|
+
|
|
889
|
+
- `naro deploy` names the service behind the env vars it is missing. Names that
|
|
890
|
+
are `SUPABASE_*` under any framework prefix (`NEXT_PUBLIC_`, `VITE_`,
|
|
891
|
+
`EXPO_PUBLIC_`, …) print `Supabase — in that project's dashboard:` beneath
|
|
892
|
+
the missing-keys line, then only the screens a missing value is actually on —
|
|
893
|
+
`the project URL — the Connect dialog at the top of the page` for a name
|
|
894
|
+
ending `_URL`, `the keys — Settings > API Keys` for one ending `_KEY`. A
|
|
895
|
+
`SUPABASE_*` name neither screen holds (`SUPABASE_ACCESS_TOKEN` is made on
|
|
896
|
+
the account, not in a project; `SUPABASE_DB_PASSWORD`, `SUPABASE_PROJECT_REF`
|
|
897
|
+
and `SUPABASE_JWT_SECRET` are elsewhere) gets no hint at all rather than a
|
|
898
|
+
screen it is not on. When other keys are missing too the line names only the
|
|
899
|
+
ones it placed. Nothing is sent to Supabase and no token is stored; the
|
|
900
|
+
values still come from `.env.local` with your consent or from
|
|
901
|
+
`naro env add`.
|
|
902
|
+
- `naro realtime [status]` ends with the project's month of Realtime, across its environments: `This month (UTC, all environments): <n> messages · est. $<x.xx>`, or `This month (UTC): not metered yet` before the meter's first run (and from a control plane older than this). When an operator caps the project's monthly messages it adds `⚠ This project has used <p>% of its monthly realtime message limit (<limit>).` from 80%, and `⚠ This project has used its realtime messages for this month. Realtime resumes on the 1st at 00:00 UTC.` once new connections are refused. `--json` carries the same as `realtime.usage`.
|
|
903
|
+
- naro-mcp 0.23.0 goes out with it (no tool changes).
|
|
904
|
+
|
|
880
905
|
### 0.23.0
|
|
881
906
|
|
|
882
907
|
0.22.0 went out on 2026-10-31 without these.
|
|
@@ -320,6 +320,33 @@ var PHASES = [
|
|
|
320
320
|
];
|
|
321
321
|
var ACTIVE_STATUSES = new Set(PHASES);
|
|
322
322
|
|
|
323
|
+
// ../../packages/core/src/dotenv.ts
|
|
324
|
+
var LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/dgm;
|
|
325
|
+
var QUOTES = /* @__PURE__ */ new Set(["'", '"', "`"]);
|
|
326
|
+
function parseDotenvDetailed(text3) {
|
|
327
|
+
const vars = {};
|
|
328
|
+
const unclosedQuotes = [];
|
|
329
|
+
const lines = text3.replace(/\r\n?/g, "\n");
|
|
330
|
+
let line = 1;
|
|
331
|
+
let counted = 0;
|
|
332
|
+
for (const match of lines.matchAll(LINE)) {
|
|
333
|
+
const key = match[1];
|
|
334
|
+
const raw = match[2] ?? "";
|
|
335
|
+
let value = raw.trim();
|
|
336
|
+
const maybeQuote = value[0];
|
|
337
|
+
if (maybeQuote !== void 0 && QUOTES.has(maybeQuote) && !value.includes(maybeQuote, 1)) {
|
|
338
|
+
const at = (match.indices?.[2]?.[0] ?? match.index) + (raw.length - raw.trimStart().length);
|
|
339
|
+
for (; counted < at; counted += 1) if (lines[counted] === "\n") line += 1;
|
|
340
|
+
unclosedQuotes.push(line);
|
|
341
|
+
}
|
|
342
|
+
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
|
|
343
|
+
if (maybeQuote === '"')
|
|
344
|
+
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
345
|
+
vars[key] = value;
|
|
346
|
+
}
|
|
347
|
+
return { vars, unclosedQuotes };
|
|
348
|
+
}
|
|
349
|
+
|
|
323
350
|
// ../../packages/core/src/deployment-stall.ts
|
|
324
351
|
var STALL_WINDOW_MS = 10 * 6e4;
|
|
325
352
|
|
|
@@ -15223,13 +15250,22 @@ var RESERVED_SUBDOMAINS = [
|
|
|
15223
15250
|
"www"
|
|
15224
15251
|
];
|
|
15225
15252
|
var BACKEND_WORKER_NAME_PREFIX = "naro-backend-";
|
|
15253
|
+
var AUTH_WORKER_NAME_PREFIX = "naro-auth-";
|
|
15254
|
+
var DATA_WORKER_NAME_PREFIX = "naro-data-";
|
|
15255
|
+
var NARO_WORKER_NAME_PREFIXES = [
|
|
15256
|
+
BACKEND_WORKER_NAME_PREFIX,
|
|
15257
|
+
AUTH_WORKER_NAME_PREFIX,
|
|
15258
|
+
DATA_WORKER_NAME_PREFIX
|
|
15259
|
+
];
|
|
15226
15260
|
var SUBDOMAIN_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
|
|
15227
15261
|
var SUBDOMAIN_MAX_LENGTH = 54;
|
|
15228
15262
|
function subdomainProblem(value) {
|
|
15229
15263
|
if (value.length < 1 || value.length > SUBDOMAIN_MAX_LENGTH) return "invalid";
|
|
15230
15264
|
if (!SUBDOMAIN_PATTERN.test(value)) return "invalid";
|
|
15231
15265
|
if (value.endsWith("-preview")) return "reserved";
|
|
15232
|
-
if (
|
|
15266
|
+
if (NARO_WORKER_NAME_PREFIXES.some(
|
|
15267
|
+
(prefix) => value === prefix.slice(0, -1) || value.startsWith(prefix)
|
|
15268
|
+
))
|
|
15233
15269
|
return "reserved";
|
|
15234
15270
|
if (value === FUNCTION_WORKER_NAME_PREFIX.slice(0, -1) || value.startsWith(FUNCTION_WORKER_NAME_PREFIX))
|
|
15235
15271
|
return "reserved";
|
|
@@ -21316,6 +21352,23 @@ var BackendApi = class {
|
|
|
21316
21352
|
);
|
|
21317
21353
|
return { backend, key };
|
|
21318
21354
|
}
|
|
21355
|
+
/**
|
|
21356
|
+
* Replaces the access JWT's signing key (design 2026-11-01). Signing moves
|
|
21357
|
+
* to the new key at once; tokens the old keys signed keep working until
|
|
21358
|
+
* `previousKeysExpireAt` (their own 15 minutes), so nobody is signed out.
|
|
21359
|
+
* The API keys and the sessions are untouched. A worker uploaded before
|
|
21360
|
+
* this answers `BACKEND_OUTDATED` — `enable` updates it.
|
|
21361
|
+
*/
|
|
21362
|
+
async rotateSigningKey(projectId) {
|
|
21363
|
+
return this.client.request(
|
|
21364
|
+
this.path(projectId, "/signing-key/rotate"),
|
|
21365
|
+
{
|
|
21366
|
+
method: "POST",
|
|
21367
|
+
body: {},
|
|
21368
|
+
context: "Rotate signing key"
|
|
21369
|
+
}
|
|
21370
|
+
);
|
|
21371
|
+
}
|
|
21319
21372
|
/**
|
|
21320
21373
|
* The sign-in settings (spec P2-6 §12): email, Google and GitHub, the
|
|
21321
21374
|
* allowed redirect origins, whether sign-in waits for a confirmed email,
|
|
@@ -22162,31 +22215,7 @@ function summarizeStatement(statement2) {
|
|
|
22162
22215
|
// ../../packages/sdk/src/dotenv.ts
|
|
22163
22216
|
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
22164
22217
|
import { join as join20 } from "path";
|
|
22165
|
-
var
|
|
22166
|
-
var QUOTES = /* @__PURE__ */ new Set(["'", '"', "`"]);
|
|
22167
|
-
function parseDotenvDetailed(text3) {
|
|
22168
|
-
const vars = {};
|
|
22169
|
-
const unclosedQuotes = [];
|
|
22170
|
-
const lines = text3.replace(/\r\n?/g, "\n");
|
|
22171
|
-
let line = 1;
|
|
22172
|
-
let counted = 0;
|
|
22173
|
-
for (const match of lines.matchAll(LINE)) {
|
|
22174
|
-
const key = match[1];
|
|
22175
|
-
const raw = match[2] ?? "";
|
|
22176
|
-
let value = raw.trim();
|
|
22177
|
-
const maybeQuote = value[0];
|
|
22178
|
-
if (maybeQuote !== void 0 && QUOTES.has(maybeQuote) && !value.includes(maybeQuote, 1)) {
|
|
22179
|
-
const at = (match.indices?.[2]?.[0] ?? match.index) + (raw.length - raw.trimStart().length);
|
|
22180
|
-
for (; counted < at; counted += 1) if (lines[counted] === "\n") line += 1;
|
|
22181
|
-
unclosedQuotes.push(line);
|
|
22182
|
-
}
|
|
22183
|
-
value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
|
|
22184
|
-
if (maybeQuote === '"')
|
|
22185
|
-
value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
|
|
22186
|
-
vars[key] = value;
|
|
22187
|
-
}
|
|
22188
|
-
return { vars, unclosedQuotes };
|
|
22189
|
-
}
|
|
22218
|
+
var QUOTES2 = /* @__PURE__ */ new Set(["'", '"', "`"]);
|
|
22190
22219
|
function parseDotenv(text3) {
|
|
22191
22220
|
return parseDotenvDetailed(text3).vars;
|
|
22192
22221
|
}
|
|
@@ -22199,7 +22228,7 @@ function readProjectEnv(dir) {
|
|
|
22199
22228
|
}
|
|
22200
22229
|
var PLAIN = /^[^\s"'#$`\\]*$/;
|
|
22201
22230
|
function bareReadsBack(value) {
|
|
22202
|
-
return !/[#\r\n\u2028\u2029]/.test(value) && value.trim() === value && !
|
|
22231
|
+
return !/[#\r\n\u2028\u2029]/.test(value) && value.trim() === value && !QUOTES2.has(value[0] ?? "");
|
|
22203
22232
|
}
|
|
22204
22233
|
function dotenvValue(value) {
|
|
22205
22234
|
if (PLAIN.test(value)) return value;
|
|
@@ -32031,6 +32060,43 @@ async function promptForMissingEnv(ctx, keys) {
|
|
|
32031
32060
|
return collected;
|
|
32032
32061
|
}
|
|
32033
32062
|
|
|
32063
|
+
// src/env-services.ts
|
|
32064
|
+
var PUBLIC_PREFIXES = [
|
|
32065
|
+
"NEXT_PUBLIC_",
|
|
32066
|
+
"NUXT_PUBLIC_",
|
|
32067
|
+
"EXPO_PUBLIC_",
|
|
32068
|
+
"REACT_APP_",
|
|
32069
|
+
"PUBLIC_",
|
|
32070
|
+
"GATSBY_",
|
|
32071
|
+
"VITE_"
|
|
32072
|
+
];
|
|
32073
|
+
function bareEnvKey(key) {
|
|
32074
|
+
let longest = "";
|
|
32075
|
+
for (const prefix of PUBLIC_PREFIXES)
|
|
32076
|
+
if (key.startsWith(prefix) && prefix.length > longest.length)
|
|
32077
|
+
longest = prefix;
|
|
32078
|
+
return key.slice(longest.length);
|
|
32079
|
+
}
|
|
32080
|
+
function supabaseKeys(keys) {
|
|
32081
|
+
return keys.filter((key) => bareEnvKey(key).startsWith("SUPABASE_"));
|
|
32082
|
+
}
|
|
32083
|
+
function envServiceHints(keys) {
|
|
32084
|
+
const own2 = supabaseKeys(keys);
|
|
32085
|
+
const isUrl = (key) => bareEnvKey(key).endsWith("_URL");
|
|
32086
|
+
const isKey = (key) => bareEnvKey(key).endsWith("_KEY");
|
|
32087
|
+
const placed = own2.filter((key) => isUrl(key) || isKey(key));
|
|
32088
|
+
if (placed.length === 0) return [];
|
|
32089
|
+
const lines = [
|
|
32090
|
+
placed.length === keys.length ? " Supabase \u2014 in that project's dashboard:" : ` Supabase (${placed.join(", ")}) \u2014 in that project's dashboard:`
|
|
32091
|
+
];
|
|
32092
|
+
if (placed.some(isUrl))
|
|
32093
|
+
lines.push(
|
|
32094
|
+
" the project URL \u2014 the Connect dialog at the top of the page"
|
|
32095
|
+
);
|
|
32096
|
+
if (placed.some(isKey)) lines.push(" the keys \u2014 Settings > API Keys");
|
|
32097
|
+
return lines;
|
|
32098
|
+
}
|
|
32099
|
+
|
|
32034
32100
|
// src/env-surface.ts
|
|
32035
32101
|
function pullEnv(rows, surface) {
|
|
32036
32102
|
const chosen = /* @__PURE__ */ new Map();
|
|
@@ -34500,6 +34566,7 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
34500
34566
|
log2(
|
|
34501
34567
|
`\u25B8 ${String(missing.length)} env var(s) needed, not set on this project: ${missing.join(", ")}`
|
|
34502
34568
|
);
|
|
34569
|
+
for (const line of envServiceHints(missing)) log2(line);
|
|
34503
34570
|
const canCollect = environment === void 0 || options.projectId;
|
|
34504
34571
|
if (options.onMissingEnv && canCollect)
|
|
34505
34572
|
collected = await options.onMissingEnv(missing);
|
|
@@ -34640,6 +34707,7 @@ function deploymentUrl(detail, ctx, production) {
|
|
|
34640
34707
|
}
|
|
34641
34708
|
|
|
34642
34709
|
export {
|
|
34710
|
+
parseDotenvDetailed,
|
|
34643
34711
|
isFunctionName,
|
|
34644
34712
|
parseGitHubRemote,
|
|
34645
34713
|
schemaMigrationTitle,
|
|
@@ -34678,7 +34746,6 @@ export {
|
|
|
34678
34746
|
cellText,
|
|
34679
34747
|
renderRows,
|
|
34680
34748
|
summarizeStatement,
|
|
34681
|
-
parseDotenvDetailed,
|
|
34682
34749
|
serializeDotenv,
|
|
34683
34750
|
systemTablePredicate,
|
|
34684
34751
|
MAX_SINGLE_UPLOAD_BYTES,
|
package/dist/deploy.js
CHANGED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which outside service the env keys a deploy is missing belong to, and where
|
|
3
|
+
* that service keeps their values.
|
|
4
|
+
*
|
|
5
|
+
* The deploy already works out the names it needs by scanning the source
|
|
6
|
+
* (`scanEnvReferences`), but a list of bare names only helps someone who
|
|
7
|
+
* already knows where each value comes from. The first thing anyone moving an
|
|
8
|
+
* existing app here hits is exactly that gap: the app talks to a Supabase
|
|
9
|
+
* project that is already running, and the deploy asks for
|
|
10
|
+
* `NEXT_PUBLIC_SUPABASE_URL` without saying it is asking for something they
|
|
11
|
+
* have in another browser tab.
|
|
12
|
+
*
|
|
13
|
+
* This only names the service and the screen its values are on. Nothing here
|
|
14
|
+
* reaches Supabase's API, stores a token or creates anything there — the
|
|
15
|
+
* values still arrive the way every other env var does, from `.env.local`
|
|
16
|
+
* with the user's consent or from `naro env add`.
|
|
17
|
+
*/
|
|
18
|
+
/** The key without that prefix. The longest match wins, so order is free. */
|
|
19
|
+
export declare function bareEnvKey(key: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* The given keys that name a Supabase project, in the order given.
|
|
22
|
+
*
|
|
23
|
+
* Everything Supabase hands a project is `SUPABASE_*`: the project URL, the
|
|
24
|
+
* publishable and secret keys, the legacy `anon` and `service_role` keys, the
|
|
25
|
+
* JWT secret, the account's access token, the database password. Matching the
|
|
26
|
+
* prefix rather than a fixed list keeps a name we have not seen — Supabase
|
|
27
|
+
* renamed its keys once already — from dropping out. A name that merely
|
|
28
|
+
* contains the word (`MY_SUPABASE_URL`) is not one of theirs.
|
|
29
|
+
*
|
|
30
|
+
* Belonging to Supabase is not the same as being on one of the screens
|
|
31
|
+
* `envServiceHints` knows; that function decides which of these it can place.
|
|
32
|
+
*/
|
|
33
|
+
export declare function supabaseKeys(keys: readonly string[]): string[];
|
|
34
|
+
/**
|
|
35
|
+
* Lines naming the service the missing keys belong to, to print under the
|
|
36
|
+
* "env var(s) needed" line. Empty when nothing is recognised, which is the
|
|
37
|
+
* common case — this adds to that line, it never replaces it.
|
|
38
|
+
*
|
|
39
|
+
* Adding a second service means another `supabaseKeys`-shaped filter and
|
|
40
|
+
* another branch here. It is deliberately not a table of services yet: the
|
|
41
|
+
* useful half of each entry is the screen its values are on, and that is
|
|
42
|
+
* prose we can only write for a dashboard we have actually checked.
|
|
43
|
+
*/
|
|
44
|
+
export declare function envServiceHints(keys: readonly string[]): string[];
|
package/dist/index.js
CHANGED
|
@@ -128,7 +128,7 @@ import {
|
|
|
128
128
|
warningLines,
|
|
129
129
|
withFunctionRetries,
|
|
130
130
|
writeProjectLink
|
|
131
|
-
} from "./chunk-
|
|
131
|
+
} from "./chunk-XJIWWZ2T.js";
|
|
132
132
|
import "./chunk-K3NQKI34.js";
|
|
133
133
|
|
|
134
134
|
// src/args.ts
|
|
@@ -1244,6 +1244,23 @@ async function runBackendKeysRotate(context) {
|
|
|
1244
1244
|
]
|
|
1245
1245
|
);
|
|
1246
1246
|
}
|
|
1247
|
+
async function runBackendSigningKeyRotate(context) {
|
|
1248
|
+
const client = await dataClient(context);
|
|
1249
|
+
const environment = dataEnvironment(context.flags);
|
|
1250
|
+
const projectId = await context.resolveProjectId();
|
|
1251
|
+
await confirmOrAbort(
|
|
1252
|
+
context,
|
|
1253
|
+
"Replace the key that signs access tokens? New tokens are signed with the new key at once; tokens already issued keep working until they expire (up to 15 minutes). Nobody is signed out."
|
|
1254
|
+
);
|
|
1255
|
+
const rotated = await client.backend.rotateSigningKey(projectId);
|
|
1256
|
+
context.output.result(
|
|
1257
|
+
{ projectId, ...rotated },
|
|
1258
|
+
[
|
|
1259
|
+
`\u2705 Replaced the signing key (kid ${rotated.kid}).`,
|
|
1260
|
+
`Tokens signed with the old key work until ${rotated.previousKeysExpireAt}.`
|
|
1261
|
+
].map((line) => forEnvironment(line, environment))
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1247
1264
|
|
|
1248
1265
|
// src/commands/backend-auth.ts
|
|
1249
1266
|
var processStdin = {
|
|
@@ -6234,6 +6251,21 @@ function tableName(table3) {
|
|
|
6234
6251
|
function linesFor(lines, environment) {
|
|
6235
6252
|
return lines.map((line) => forEnvironment(line, environment));
|
|
6236
6253
|
}
|
|
6254
|
+
function usageLines2(usage) {
|
|
6255
|
+
if (usage === null) return ["This month (UTC): not metered yet"];
|
|
6256
|
+
const lines = [
|
|
6257
|
+
`This month (UTC, all environments): ${number(usage.messages)} messages \xB7 est. $${usage.estCostUsd.toFixed(2)}`
|
|
6258
|
+
];
|
|
6259
|
+
if (usage.blocked)
|
|
6260
|
+
lines.push(
|
|
6261
|
+
"\u26A0 This project has used its realtime messages for this month. Realtime resumes on the 1st at 00:00 UTC."
|
|
6262
|
+
);
|
|
6263
|
+
else if (usage.messageLimit !== null && usage.messages >= Math.ceil(usage.messageLimit * 0.8))
|
|
6264
|
+
lines.push(
|
|
6265
|
+
`\u26A0 This project has used ${String(Math.floor(usage.messages * 100 / usage.messageLimit))}% of its monthly realtime message limit (${number(usage.messageLimit)}).`
|
|
6266
|
+
);
|
|
6267
|
+
return lines;
|
|
6268
|
+
}
|
|
6237
6269
|
function realtimeLines(info) {
|
|
6238
6270
|
if (!info.enabled)
|
|
6239
6271
|
return [
|
|
@@ -6244,7 +6276,9 @@ function realtimeLines(info) {
|
|
|
6244
6276
|
return [
|
|
6245
6277
|
"Realtime: on (public channels: off)",
|
|
6246
6278
|
info.tables.length === 0 ? "Published tables: none \u2014 naro realtime tables add <table>" : `Published tables: ${info.tables.map(tableName).join(", ")}`,
|
|
6247
|
-
live2 === null ? "Connections now: not known \u2014 the backend is off or not ready, or the hub did not answer" : `Connections now: ${number(live2.connections)} \xB7 channels ${number(live2.channels)} \xB7 presences ${number(live2.presences)}
|
|
6279
|
+
live2 === null ? "Connections now: not known \u2014 the backend is off or not ready, or the hub did not answer" : `Connections now: ${number(live2.connections)} \xB7 channels ${number(live2.channels)} \xB7 presences ${number(live2.presences)}`,
|
|
6280
|
+
// A control plane older than 3-2e sends no `usage`.
|
|
6281
|
+
...usageLines2(info.usage ?? null)
|
|
6248
6282
|
];
|
|
6249
6283
|
}
|
|
6250
6284
|
async function enableRealtime(client, projectId, environment) {
|
|
@@ -7422,6 +7456,9 @@ var RETRY_AFTER_CODES = /* @__PURE__ */ new Set([
|
|
|
7422
7456
|
"STORAGE_OPS_QUOTA_EXCEEDED",
|
|
7423
7457
|
// Realtime's REST broadcast over the project's message budget (x-retry-after: 1).
|
|
7424
7458
|
"TOO_MANY_MESSAGES",
|
|
7459
|
+
// The dispatcher's REST 429 of a project over its month of Realtime (3-2e):
|
|
7460
|
+
// `x-retry-after` is the seconds to the 1st at 00:00 UTC.
|
|
7461
|
+
"REALTIME_QUOTA_EXCEEDED",
|
|
7425
7462
|
// Naro Functions (design F20): a team over its monthly function CPU, and
|
|
7426
7463
|
// the control plane's Cloudflare limit. `invoke` reads x-retry-after on
|
|
7427
7464
|
// every relay error anyway (invoke/client.ts).
|
|
@@ -9343,6 +9380,7 @@ var TableBuilder = class {
|
|
|
9343
9380
|
var BACKOFF_STEPS_MS = [1e3, 2e3, 5e3, 1e4];
|
|
9344
9381
|
var RATE_LIMITED_MIN_MS = 5e3;
|
|
9345
9382
|
var IP_LIMIT_WAIT_MS = 6e4;
|
|
9383
|
+
var QUOTA_WAIT_MS = 9e5;
|
|
9346
9384
|
var RESTART_JITTER_MS = 1e4;
|
|
9347
9385
|
var UNOPENED_SLOWDOWN_AFTER = 6;
|
|
9348
9386
|
var UNOPENED_DELAY_MS = 6e4;
|
|
@@ -9359,7 +9397,7 @@ function closeAction(code, reason) {
|
|
|
9359
9397
|
if (code === 4401) return { kind: "session" };
|
|
9360
9398
|
if (code === 4503) return { kind: "restart" };
|
|
9361
9399
|
if (code === 4029)
|
|
9362
|
-
return reason === "IP_RATE_LIMITED" ? { kind: "wait", ms: IP_LIMIT_WAIT_MS } : { kind: "retry", minMs: RATE_LIMITED_MIN_MS };
|
|
9400
|
+
return reason === "IP_RATE_LIMITED" ? { kind: "wait", ms: IP_LIMIT_WAIT_MS } : reason === "REALTIME_QUOTA_EXCEEDED" ? { kind: "wait", ms: QUOTA_WAIT_MS } : { kind: "retry", minMs: RATE_LIMITED_MIN_MS };
|
|
9363
9401
|
return { kind: "retry", minMs: 0 };
|
|
9364
9402
|
}
|
|
9365
9403
|
function backoffMs(attempt22, random) {
|
|
@@ -13131,7 +13169,7 @@ function projectName(project) {
|
|
|
13131
13169
|
return `Moved to another team ${printable(project.projectId)}`;
|
|
13132
13170
|
return printable(project.name ?? project.projectId);
|
|
13133
13171
|
}
|
|
13134
|
-
function
|
|
13172
|
+
function usageLines3(report, summary) {
|
|
13135
13173
|
const lines = [
|
|
13136
13174
|
`${printable(report.team.slug)} \xB7 ${planName(report.team.plan)} plan \xB7 ${printable(report.month)} (UTC)`
|
|
13137
13175
|
];
|
|
@@ -13178,7 +13216,7 @@ async function runUsage(context) {
|
|
|
13178
13216
|
month !== void 0 ? { month } : {}
|
|
13179
13217
|
);
|
|
13180
13218
|
const summary = summarizeTeamUsage(report, usageMonthOf(/* @__PURE__ */ new Date()));
|
|
13181
|
-
context.output.result({ ...report, summary },
|
|
13219
|
+
context.output.result({ ...report, summary }, usageLines3(report, summary));
|
|
13182
13220
|
}
|
|
13183
13221
|
|
|
13184
13222
|
// src/router.ts
|
|
@@ -13303,6 +13341,8 @@ Usage:
|
|
|
13303
13341
|
naro backend keys [--reveal] API keys; --reveal prints the service key (a secret; needs database write)
|
|
13304
13342
|
naro backend keys rotate --anon|--service
|
|
13305
13343
|
Replace a key; the old one stops working at once (asks first)
|
|
13344
|
+
naro backend signing-key rotate Replace the key that signs access tokens (if it may have leaked);
|
|
13345
|
+
tokens already issued keep working up to 15 minutes (asks first)
|
|
13306
13346
|
naro backend auth Sign-in settings: email, Google, GitHub, redirects, callback URLs,
|
|
13307
13347
|
sign-ups and the minimum password length
|
|
13308
13348
|
naro backend auth signups <on|off>
|
|
@@ -14032,6 +14072,13 @@ function routeCommand(parsed) {
|
|
|
14032
14072
|
positionals: positionals.slice(2)
|
|
14033
14073
|
};
|
|
14034
14074
|
return null;
|
|
14075
|
+
case "signing-key":
|
|
14076
|
+
if (positionals[1] === "rotate")
|
|
14077
|
+
return {
|
|
14078
|
+
handler: runBackendSigningKeyRotate,
|
|
14079
|
+
positionals: positionals.slice(2)
|
|
14080
|
+
};
|
|
14081
|
+
return null;
|
|
14035
14082
|
case "auth":
|
|
14036
14083
|
return routeBackendAuth(positionals.slice(1));
|
|
14037
14084
|
case "users":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "naro-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"naro": "./dist/index.js"
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"tsx": "4.23.12",
|
|
38
38
|
"typescript": "7.0.2",
|
|
39
39
|
"vitest": "3.2.7",
|
|
40
|
-
"naro-js": "0.7.0",
|
|
41
|
-
"@next-deploy/config": "0.1.0",
|
|
42
40
|
"@next-deploy/core": "0.1.0",
|
|
43
41
|
"@next-deploy/build": "0.1.0",
|
|
44
42
|
"@next-deploy/data-core": "0.1.0",
|
|
43
|
+
"naro-js": "0.7.1",
|
|
44
|
+
"@next-deploy/functions-bundler": "0.1.0",
|
|
45
45
|
"@next-deploy/functions-runtime": "0.1.0",
|
|
46
46
|
"@next-deploy/sdk": "0.1.0",
|
|
47
|
-
"@next-deploy/
|
|
47
|
+
"@next-deploy/config": "0.1.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsup && pnpm build:types",
|