naro-cli 0.28.0 → 0.30.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 +56 -1
- package/dist/{chunk-UJGDKNDM.js → chunk-ITTYPT3A.js} +398 -101
- package/dist/deploy.js +1 -1
- package/dist/functions/deploy.d.ts +7 -0
- package/dist/functions/retry.d.ts +16 -1
- package/dist/index.js +316 -53
- package/dist/{stack-PDV3NPWC.js → stack-DFLT7AVX.js} +41 -22
- package/package.json +8 -9
package/dist/deploy.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { bootCheck, bundleFunction } from "@next-deploy/functions-bundler";
|
|
2
2
|
import { type FunctionUploadTarget, type NaroClient } from "@next-deploy/sdk";
|
|
3
|
+
/**
|
|
4
|
+
* A function's own failures while its version is made: said, and the run
|
|
5
|
+
* goes on with the next. FUNCTION_BUSY is not one — the lease is the
|
|
6
|
+
* project's worker's (design 2026-11-08 G5), so the next function would
|
|
7
|
+
* meet it too; it is retried, then stops the run.
|
|
8
|
+
*/
|
|
3
9
|
export declare const PER_FUNCTION_CODES: ReadonlySet<string>;
|
|
4
10
|
export interface DeploySteps {
|
|
5
11
|
bundle: typeof bundleFunction;
|
|
@@ -67,4 +73,5 @@ export declare function assertFunctionsNode(version?: string): void;
|
|
|
67
73
|
*/
|
|
68
74
|
export declare function uploadTimeoutMs(upload: FunctionUploadTarget, now?: number): number;
|
|
69
75
|
export declare const deadlineReached: (name: string, deadlineMs: number) => string;
|
|
76
|
+
export declare const batchDeadlineReached: (count: number, deadlineMs: number) => string;
|
|
70
77
|
export declare function deployFunctions(options: DeployFunctionsOptions): Promise<DeployFunctionsResult>;
|
|
@@ -6,7 +6,22 @@ export declare const FUNCTION_RATE_LIMIT_MAX_SECONDS = 300;
|
|
|
6
6
|
export declare function defaultSleep(ms: number): Promise<void>;
|
|
7
7
|
/** `details.retryAfter` rounded up, at most 300; 60 when it is missing. */
|
|
8
8
|
export declare function retryAfterSeconds(error: NaroError): number;
|
|
9
|
-
|
|
9
|
+
/** What a retry says, for one function or for a batch of them. */
|
|
10
|
+
export interface RetryWords {
|
|
11
|
+
rateLimited(seconds: number, attempt: number, of: number): string;
|
|
12
|
+
busy(attempt: number, of: number): string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Several functions put live in one call (design 2026-11-08 G4): the lease
|
|
16
|
+
* the server waits on is the project's worker, not one function's.
|
|
17
|
+
*/
|
|
18
|
+
export declare const batchRetryWords: (count: number) => RetryWords;
|
|
19
|
+
/**
|
|
20
|
+
* `op` again after FUNCTIONS_RATE_LIMITED and FUNCTION_BUSY, judged by the
|
|
21
|
+
* code alone (the words are the server's to change). `name` is the
|
|
22
|
+
* function's, or the words of a batch.
|
|
23
|
+
*/
|
|
24
|
+
export declare function withFunctionRetries<T>(op: () => Promise<T>, name: string | RetryWords, io: {
|
|
10
25
|
sleep: (ms: number) => Promise<void>;
|
|
11
26
|
log: (line: string) => void;
|
|
12
27
|
/** Tries after FUNCTIONS_RATE_LIMITED: 3 by default; naro-mcp passes 0. */
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
appliedMigrations,
|
|
29
29
|
applyMigrationFiles,
|
|
30
30
|
assertFunctionsNode,
|
|
31
|
+
assertProjectLinkWritable,
|
|
31
32
|
buildResetPlan,
|
|
32
33
|
cannotAsk,
|
|
33
34
|
cellText,
|
|
@@ -66,6 +67,7 @@ import {
|
|
|
66
67
|
isPrintable,
|
|
67
68
|
isReservedEnvTarget,
|
|
68
69
|
isStalledBackend,
|
|
70
|
+
isSymbolicLink,
|
|
69
71
|
isTerminalStatus,
|
|
70
72
|
listMigrationFiles,
|
|
71
73
|
loadStoredSession,
|
|
@@ -136,7 +138,7 @@ import {
|
|
|
136
138
|
warningLines,
|
|
137
139
|
withFunctionRetries,
|
|
138
140
|
writeProjectLink
|
|
139
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-ITTYPT3A.js";
|
|
140
142
|
import "./chunk-K3NQKI34.js";
|
|
141
143
|
|
|
142
144
|
// src/args.ts
|
|
@@ -1704,12 +1706,12 @@ async function allUsers(client, projectId) {
|
|
|
1704
1706
|
const users = [];
|
|
1705
1707
|
let total = 0;
|
|
1706
1708
|
for (let page = 1; ; page++) {
|
|
1707
|
-
const
|
|
1709
|
+
const listed2 = await usersCall(
|
|
1708
1710
|
() => client.backend.listUsers(projectId, { page, perPage: PAGE_SIZE })
|
|
1709
1711
|
);
|
|
1710
|
-
total =
|
|
1711
|
-
users.push(...
|
|
1712
|
-
if (
|
|
1712
|
+
total = listed2.total;
|
|
1713
|
+
users.push(...listed2.users);
|
|
1714
|
+
if (listed2.users.length < PAGE_SIZE || users.length >= total)
|
|
1713
1715
|
return { users, total };
|
|
1714
1716
|
}
|
|
1715
1717
|
}
|
|
@@ -2365,8 +2367,8 @@ function chunk(items3, size) {
|
|
|
2365
2367
|
}
|
|
2366
2368
|
async function readSchema(client, projectId) {
|
|
2367
2369
|
const schema = await client.db.query(projectId, { sql: SCHEMA_SQL });
|
|
2368
|
-
const [
|
|
2369
|
-
if (
|
|
2370
|
+
const [listed2, virtual, internal] = schema.statements;
|
|
2371
|
+
if (listed2 === void 0 || virtual === void 0 || internal === void 0)
|
|
2370
2372
|
throw new NaroError(
|
|
2371
2373
|
"DATABASE_QUERY_FAILED",
|
|
2372
2374
|
`The schema could not be read: the database answered with ${String(schema.statements.length)} result sets instead of 3.`
|
|
@@ -2377,9 +2379,9 @@ async function readSchema(client, projectId) {
|
|
|
2377
2379
|
const virtualNames = new Set(
|
|
2378
2380
|
virtual.rows.map((row) => cellText(virtual, row, "name").toLowerCase())
|
|
2379
2381
|
);
|
|
2380
|
-
const objects2 =
|
|
2381
|
-
const name = cellText(
|
|
2382
|
-
const type = cellText(
|
|
2382
|
+
const objects2 = listed2.rows.map((row) => {
|
|
2383
|
+
const name = cellText(listed2, row, "name");
|
|
2384
|
+
const type = cellText(listed2, row, "type");
|
|
2383
2385
|
if (type === "view" || type === "trigger") return { type, name };
|
|
2384
2386
|
return {
|
|
2385
2387
|
type: virtualNames.has(name.toLowerCase()) ? "virtual" : "table",
|
|
@@ -3247,6 +3249,72 @@ function cellOf(value) {
|
|
|
3247
3249
|
if (typeof value === "string") return value;
|
|
3248
3250
|
return isInvalid(value) ? "invalid" : "condition";
|
|
3249
3251
|
}
|
|
3252
|
+
var SET_POLICY_NAMES = new Set(
|
|
3253
|
+
POLICY_ACTIONS.map((action) => `naro_${action}`)
|
|
3254
|
+
);
|
|
3255
|
+
function customPolicies(named) {
|
|
3256
|
+
const byTable = /* @__PURE__ */ new Map();
|
|
3257
|
+
for (const policy of named) {
|
|
3258
|
+
if (SET_POLICY_NAMES.has(policy.name)) continue;
|
|
3259
|
+
const invalid = policy.invalid !== null;
|
|
3260
|
+
if (!invalid && policy.as !== "permissive") continue;
|
|
3261
|
+
const key = asciiLower(policy.table);
|
|
3262
|
+
const list = byTable.get(key) ?? [];
|
|
3263
|
+
list.push({ name: policy.name, command: policy.command, invalid });
|
|
3264
|
+
byTable.set(key, list);
|
|
3265
|
+
}
|
|
3266
|
+
return byTable;
|
|
3267
|
+
}
|
|
3268
|
+
function covers(policy, action) {
|
|
3269
|
+
return policy.command === "all" || policy.command === action;
|
|
3270
|
+
}
|
|
3271
|
+
var CUSTOM_NOTE = "custom: a policy written as CREATE POLICY allows it. naro policy list shows it.";
|
|
3272
|
+
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.";
|
|
3273
|
+
var NAMES_ONLY_NOTE = "`none` may not mean denied on those tables: naro policy list shows their policies.";
|
|
3274
|
+
function listed(entries) {
|
|
3275
|
+
return entries.map((entry) => `${printable(entry.name)} (${entry.command})`).toSorted().join(", ");
|
|
3276
|
+
}
|
|
3277
|
+
function customLines(policies, custom) {
|
|
3278
|
+
const lines = [];
|
|
3279
|
+
let allows = false;
|
|
3280
|
+
let unreadable2 = false;
|
|
3281
|
+
for (const policy of policies) {
|
|
3282
|
+
const table3 = printable(policy.table);
|
|
3283
|
+
if (custom) {
|
|
3284
|
+
const list = custom.get(asciiLower(policy.table)) ?? [];
|
|
3285
|
+
const valid = list.filter((entry) => !entry.invalid);
|
|
3286
|
+
const invalid = list.filter((entry) => entry.invalid);
|
|
3287
|
+
if (valid.length > 0) {
|
|
3288
|
+
allows = true;
|
|
3289
|
+
lines.push(`${table3} custom: ${listed(valid)}`);
|
|
3290
|
+
}
|
|
3291
|
+
if (invalid.length > 0) {
|
|
3292
|
+
unreadable2 = true;
|
|
3293
|
+
lines.push(`${table3} invalid: ${listed(invalid)}`);
|
|
3294
|
+
}
|
|
3295
|
+
continue;
|
|
3296
|
+
}
|
|
3297
|
+
const names = (policy.policies ?? []).filter(
|
|
3298
|
+
(name) => !SET_POLICY_NAMES.has(name)
|
|
3299
|
+
);
|
|
3300
|
+
if (names.length === 0) continue;
|
|
3301
|
+
lines.push(
|
|
3302
|
+
`${table3} also has policies written as SQL: ${names.map(printable).join(", ")}`
|
|
3303
|
+
);
|
|
3304
|
+
}
|
|
3305
|
+
if (lines.length === 0) return lines;
|
|
3306
|
+
if (!custom) return [...lines, NAMES_ONLY_NOTE];
|
|
3307
|
+
return [
|
|
3308
|
+
...lines,
|
|
3309
|
+
...allows ? [CUSTOM_NOTE] : [],
|
|
3310
|
+
...unreadable2 ? [INVALID_NOTE] : []
|
|
3311
|
+
];
|
|
3312
|
+
}
|
|
3313
|
+
var POLICY_TABLE_HINTS = /* @__PURE__ */ new Set([
|
|
3314
|
+
CUSTOM_NOTE,
|
|
3315
|
+
INVALID_NOTE,
|
|
3316
|
+
NAMES_ONLY_NOTE
|
|
3317
|
+
]);
|
|
3250
3318
|
function conditionText(action, value) {
|
|
3251
3319
|
if (typeof value === "string") return value;
|
|
3252
3320
|
if (isInvalid(value))
|
|
@@ -3262,11 +3330,19 @@ function conditionText(action, value) {
|
|
|
3262
3330
|
const text = parts.join("; ");
|
|
3263
3331
|
return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
|
|
3264
3332
|
}
|
|
3265
|
-
function policyTable(policies) {
|
|
3333
|
+
function policyTable(policies, custom) {
|
|
3266
3334
|
const header = ["Table", ...POLICY_ACTIONS, "Owner column"];
|
|
3335
|
+
const shown = (policy, action) => {
|
|
3336
|
+
const value = policy[action];
|
|
3337
|
+
const sql2 = (custom?.get(asciiLower(policy.table)) ?? []).filter(
|
|
3338
|
+
(entry) => covers(entry, action)
|
|
3339
|
+
);
|
|
3340
|
+
if (sql2.some((entry) => entry.invalid)) return "invalid";
|
|
3341
|
+
return value === "none" && sql2.length > 0 ? "custom" : cellOf(value);
|
|
3342
|
+
};
|
|
3267
3343
|
const rows = policies.map((policy) => [
|
|
3268
3344
|
printable(policy.table),
|
|
3269
|
-
...POLICY_ACTIONS.map((action) =>
|
|
3345
|
+
...POLICY_ACTIONS.map((action) => shown(policy, action)),
|
|
3270
3346
|
policy.ownerColumn === null ? "-" : printable(policy.ownerColumn)
|
|
3271
3347
|
]);
|
|
3272
3348
|
const widths = header.map(
|
|
@@ -3289,19 +3365,22 @@ function policyTable(policies) {
|
|
|
3289
3365
|
}
|
|
3290
3366
|
)
|
|
3291
3367
|
);
|
|
3292
|
-
|
|
3368
|
+
const sql = customLines(policies, custom);
|
|
3369
|
+
const below = sql.length > 0 ? ["", ...sql] : [];
|
|
3370
|
+
if (lines.length === 0) return [...table3, ...below];
|
|
3293
3371
|
const width = Math.max(...lines.map((line) => line.label.length));
|
|
3294
3372
|
return [
|
|
3295
3373
|
...table3,
|
|
3296
3374
|
"",
|
|
3297
|
-
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
|
|
3375
|
+
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`),
|
|
3376
|
+
...below
|
|
3298
3377
|
];
|
|
3299
3378
|
}
|
|
3300
3379
|
var UNSHOWN = "has characters that cannot be shown here";
|
|
3301
3380
|
function hintsFor(lines, environment) {
|
|
3302
3381
|
if (environment === null) return lines;
|
|
3303
3382
|
return lines.map(
|
|
3304
|
-
(line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") ? forEnvironment(line, environment) : line
|
|
3383
|
+
(line) => line.includes(UNSHOWN) || line.startsWith("Add one with ") || POLICY_TABLE_HINTS.has(line) ? forEnvironment(line, environment) : line
|
|
3305
3384
|
);
|
|
3306
3385
|
}
|
|
3307
3386
|
function statementLine(policy) {
|
|
@@ -3364,9 +3443,13 @@ async function runPolicyRules(context) {
|
|
|
3364
3443
|
const client = await dataClient(context);
|
|
3365
3444
|
const projectId = await context.resolveProjectId();
|
|
3366
3445
|
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
|
|
3446
|
+
const wanted = table3 === void 0 ? void 0 : asciiLower(table3);
|
|
3447
|
+
const policies = wanted === void 0 ? all : all.filter((policy) => asciiLower(policy.table) === wanted);
|
|
3448
|
+
const custom = context.flags.json || policies.length === 0 ? void 0 : await client.backend.rlsPolicies(projectId, table3 === void 0 ? {} : { table: table3 }).then(customPolicies, (error) => {
|
|
3449
|
+
if (!isNaroError(error)) throw error;
|
|
3450
|
+
return null;
|
|
3451
|
+
});
|
|
3452
|
+
const lines = policies.length > 0 ? [...policyTable(policies, custom), "", DEFAULT_DENY] : [
|
|
3370
3453
|
table3 === void 0 ? "No tables yet." : `No table named ${printable(table3)}.`
|
|
3371
3454
|
];
|
|
3372
3455
|
context.output.result(
|
|
@@ -3501,7 +3584,10 @@ async function runPolicySet(context) {
|
|
|
3501
3584
|
hintsFor(
|
|
3502
3585
|
[
|
|
3503
3586
|
`\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
|
|
3504
|
-
...policyTable(
|
|
3587
|
+
...policyTable(
|
|
3588
|
+
[policy],
|
|
3589
|
+
named === void 0 ? void 0 : customPolicies(named)
|
|
3590
|
+
),
|
|
3505
3591
|
...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
|
|
3506
3592
|
],
|
|
3507
3593
|
dataEnvironment(context.flags)
|
|
@@ -4217,6 +4303,7 @@ async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDep
|
|
|
4217
4303
|
const target = deployTarget(flags);
|
|
4218
4304
|
const flaggedAdapter = flaggedUserAdapter(flags);
|
|
4219
4305
|
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
4306
|
+
assertProjectLinkWritable(context.cwd, "deploying");
|
|
4220
4307
|
if (target.production && !flags.yes && !flags.json && isInteractive()) {
|
|
4221
4308
|
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
4222
4309
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
@@ -4670,9 +4757,9 @@ async function verifyAll(client, projectId, hostname, auto = false) {
|
|
|
4670
4757
|
return [
|
|
4671
4758
|
await client.domains.verifyByHostname(projectId, hostname, { auto })
|
|
4672
4759
|
];
|
|
4673
|
-
const
|
|
4760
|
+
const listed2 = await client.domains.list(projectId);
|
|
4674
4761
|
const verified = [];
|
|
4675
|
-
for (const domain of
|
|
4762
|
+
for (const domain of listed2)
|
|
4676
4763
|
verified.push(await client.domains.verify(projectId, domain.id, { auto }));
|
|
4677
4764
|
return verified;
|
|
4678
4765
|
}
|
|
@@ -5070,6 +5157,13 @@ function passwordRecoveryRequired() {
|
|
|
5070
5157
|
message: "updateUser({ password }) works in the password recovery flow: open the link resetPasswordForEmail sent, then call it on that page."
|
|
5071
5158
|
};
|
|
5072
5159
|
}
|
|
5160
|
+
function recoveryForAnotherUser(email) {
|
|
5161
|
+
return {
|
|
5162
|
+
code: "RECOVERY_FOR_ANOTHER_USER",
|
|
5163
|
+
message: "This password reset link is for another account than the one signed in on this browser, so it was not used. To reset that account's password, sign out first. If you did not ask for it, ignore it.",
|
|
5164
|
+
details: { email }
|
|
5165
|
+
};
|
|
5166
|
+
}
|
|
5073
5167
|
function invalidProvider() {
|
|
5074
5168
|
return {
|
|
5075
5169
|
code: "VALIDATION_ERROR",
|
|
@@ -5095,6 +5189,12 @@ function websocketUnavailable() {
|
|
|
5095
5189
|
message: "This runtime has no WebSocket. Pass one to createClient({ realtime: { WebSocket } })."
|
|
5096
5190
|
};
|
|
5097
5191
|
}
|
|
5192
|
+
function websocketHeadersUnavailable() {
|
|
5193
|
+
return {
|
|
5194
|
+
code: "WEBSOCKET_HEADERS_UNAVAILABLE",
|
|
5195
|
+
message: "The service key goes in the apikey header of the realtime connection, which this runtime's WebSocket cannot send. Pass one that takes { headers } \u2014 ws (npm:ws) \u2014 as createClient({ realtime: { WebSocket } })."
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5098
5198
|
function realtimeTimedOut(ms) {
|
|
5099
5199
|
const seconds = Math.max(1, Math.round(ms / 1e3));
|
|
5100
5200
|
return {
|
|
@@ -5768,8 +5868,11 @@ function isBrowser(scope = globalThis) {
|
|
|
5768
5868
|
const g = scope;
|
|
5769
5869
|
return g.window !== void 0 && g.document !== void 0;
|
|
5770
5870
|
}
|
|
5771
|
-
function isWorkerd(
|
|
5772
|
-
return
|
|
5871
|
+
function isWorkerd(scope = globalThis) {
|
|
5872
|
+
return scope.navigator?.userAgent === "Cloudflare-Workers";
|
|
5873
|
+
}
|
|
5874
|
+
function isDeno(scope = globalThis) {
|
|
5875
|
+
return typeof scope.Deno?.version?.deno === "string";
|
|
5773
5876
|
}
|
|
5774
5877
|
function hasServerSignal(g) {
|
|
5775
5878
|
return isWorkerd(g) || // Vercel's edge runtime and Next.js's edge (middleware, edge routes).
|
|
@@ -6145,12 +6248,8 @@ var AuthClient = class {
|
|
|
6145
6248
|
await this.#flow.verifiers.take(fragment.naro_flow);
|
|
6146
6249
|
return { data: none, error: flowError(type, fragment.naro_error) };
|
|
6147
6250
|
}
|
|
6148
|
-
if (type === "recovery" && fragment.naro_token)
|
|
6149
|
-
|
|
6150
|
-
await this.#http.accessJwt?.clear();
|
|
6151
|
-
await this.#flow.recovery.keep(fragment.naro_token);
|
|
6152
|
-
return { data: none, error: null };
|
|
6153
|
-
}
|
|
6251
|
+
if (type === "recovery" && fragment.naro_token)
|
|
6252
|
+
return this.#recoveryAnswer(none, fragment.naro_token);
|
|
6154
6253
|
if (fragment.naro_code) {
|
|
6155
6254
|
const traded = await this.exchangeCodeForSession(
|
|
6156
6255
|
fragment.naro_code,
|
|
@@ -6160,6 +6259,45 @@ var AuthClient = class {
|
|
|
6160
6259
|
}
|
|
6161
6260
|
return { data: none, error: null };
|
|
6162
6261
|
}
|
|
6262
|
+
/**
|
|
6263
|
+
* A reset link's token, read on the page it opened (security audit
|
|
6264
|
+
* 2026-11-14 F6). The fragment is whoever sent the link's to write — one
|
|
6265
|
+
* who holds a reset token of their own account can send the landing URL
|
|
6266
|
+
* itself — so the worker is asked whose token it is: the answer names the
|
|
6267
|
+
* account (masked, `data.email`, for the page to show) and whether it is
|
|
6268
|
+
* the one signed in here.
|
|
6269
|
+
*
|
|
6270
|
+
* Signed in as another account, the link is not used: the session stays,
|
|
6271
|
+
* the token is not kept, and the answer is RECOVERY_FOR_ANOTHER_USER — a
|
|
6272
|
+
* password typed here cannot land on the sender's account. Otherwise,
|
|
6273
|
+
* like supabase-js's recovery session, the link takes the place of
|
|
6274
|
+
* whoever was signed in on this browser (the reset ends every session of
|
|
6275
|
+
* that account anyway, PR #68 review 6): `updateUser` then acts on the
|
|
6276
|
+
* link's account alone. A token the worker does not know is INVALID_TOKEN
|
|
6277
|
+
* here, and nothing is kept.
|
|
6278
|
+
*/
|
|
6279
|
+
async #recoveryAnswer(none, token) {
|
|
6280
|
+
const result = await call(this.#http, {
|
|
6281
|
+
method: "POST",
|
|
6282
|
+
path: "/auth/recovery-owner",
|
|
6283
|
+
body: { token },
|
|
6284
|
+
withToken: true,
|
|
6285
|
+
// The page's queries wait on this; they must not wait forever.
|
|
6286
|
+
timeoutMs: EXCHANGE_TIMEOUT_MS
|
|
6287
|
+
});
|
|
6288
|
+
if (result.error !== null) return { data: none, error: result.error };
|
|
6289
|
+
const { body } = result.answer;
|
|
6290
|
+
const owner = isRecord(body) && isRecord(body.data) ? body.data : null;
|
|
6291
|
+
if (owner === null)
|
|
6292
|
+
return { data: none, error: invalidResponse(result.answer.status) };
|
|
6293
|
+
const email = typeof owner.email === "string" ? owner.email : null;
|
|
6294
|
+
if (owner.self === false)
|
|
6295
|
+
return { data: none, error: recoveryForAnotherUser(email) };
|
|
6296
|
+
await this.#http.tokens.clear();
|
|
6297
|
+
await this.#http.accessJwt?.clear();
|
|
6298
|
+
await this.#flow.recovery.keep(token);
|
|
6299
|
+
return { data: { ...none, email }, error: null };
|
|
6300
|
+
}
|
|
6163
6301
|
/**
|
|
6164
6302
|
* Emails a password reset link, when the address has an account. The
|
|
6165
6303
|
* answer is the same either way — it does not say whether it had one.
|
|
@@ -6183,11 +6321,12 @@ var AuthClient = class {
|
|
|
6183
6321
|
* "(P1-2 리뷰 반영)".)
|
|
6184
6322
|
*
|
|
6185
6323
|
* Like supabase-js, it acts on the recovery link's account, which took the
|
|
6186
|
-
* place of any signed-in session when the link was read
|
|
6187
|
-
*
|
|
6188
|
-
*
|
|
6189
|
-
*
|
|
6190
|
-
*
|
|
6324
|
+
* place of any signed-in session when the link was read (a link for
|
|
6325
|
+
* another account than the signed-in one is not read, `#recoveryAnswer`)
|
|
6326
|
+
* — so the token is used only while no session is signed in on this
|
|
6327
|
+
* browser, for 15 minutes after the link was opened, and never after a
|
|
6328
|
+
* sign-in, sign-up or sign-out forgot it (PR #68 review 6). Otherwise
|
|
6329
|
+
* PASSWORD_RECOVERY_REQUIRED, with no request.
|
|
6191
6330
|
*/
|
|
6192
6331
|
async updateUser(attributes) {
|
|
6193
6332
|
await this.#pendingUrl();
|
|
@@ -7882,8 +8021,76 @@ function channelNameProblem(name) {
|
|
|
7882
8021
|
function socketUrl(baseUrl, apiKey) {
|
|
7883
8022
|
const url = new URL("/_naro/v1/realtime/websocket", baseUrl);
|
|
7884
8023
|
url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
|
|
7885
|
-
|
|
7886
|
-
}
|
|
8024
|
+
const key = apiKey === null ? "" : `apikey=${encodeURIComponent(apiKey)}&`;
|
|
8025
|
+
return `${url.href}?${key}vsn=${REALTIME_PROTOCOL_VERSION}`;
|
|
8026
|
+
}
|
|
8027
|
+
var UpgradeSocket = class {
|
|
8028
|
+
onopen = null;
|
|
8029
|
+
onmessage = null;
|
|
8030
|
+
onclose = null;
|
|
8031
|
+
onerror = null;
|
|
8032
|
+
#ws = null;
|
|
8033
|
+
/** `close()` was called, or the open failed: no event goes out after. */
|
|
8034
|
+
#closed = null;
|
|
8035
|
+
constructor(fetchLike, url, headers) {
|
|
8036
|
+
let pending;
|
|
8037
|
+
try {
|
|
8038
|
+
pending = fetchLike(url, {
|
|
8039
|
+
headers: { Upgrade: "websocket", ...headers }
|
|
8040
|
+
});
|
|
8041
|
+
} catch {
|
|
8042
|
+
pending = Promise.reject(new Error("fetch threw"));
|
|
8043
|
+
}
|
|
8044
|
+
pending.then(
|
|
8045
|
+
(response) => this.#arrived(response),
|
|
8046
|
+
() => this.#failed()
|
|
8047
|
+
);
|
|
8048
|
+
}
|
|
8049
|
+
#arrived(response) {
|
|
8050
|
+
const ws = response.webSocket ?? null;
|
|
8051
|
+
if (ws === null) {
|
|
8052
|
+
void response.body?.cancel?.().catch(() => void 0);
|
|
8053
|
+
this.#failed();
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
if (this.#closed !== null) {
|
|
8057
|
+
try {
|
|
8058
|
+
ws.accept();
|
|
8059
|
+
ws.close(this.#closed.code, this.#closed.reason);
|
|
8060
|
+
} catch {
|
|
8061
|
+
}
|
|
8062
|
+
return;
|
|
8063
|
+
}
|
|
8064
|
+
this.#ws = ws;
|
|
8065
|
+
ws.addEventListener("message", (event) => {
|
|
8066
|
+
this.onmessage?.({ data: event.data });
|
|
8067
|
+
});
|
|
8068
|
+
ws.addEventListener("close", (event) => {
|
|
8069
|
+
this.onclose?.({ code: event.code, reason: event.reason ?? "" });
|
|
8070
|
+
});
|
|
8071
|
+
ws.addEventListener("error", () => {
|
|
8072
|
+
this.onerror?.();
|
|
8073
|
+
});
|
|
8074
|
+
ws.accept();
|
|
8075
|
+
this.onopen?.();
|
|
8076
|
+
}
|
|
8077
|
+
/** No connection came of it: a close before the open. */
|
|
8078
|
+
#failed() {
|
|
8079
|
+
if (this.#closed !== null) return;
|
|
8080
|
+
this.#closed = { code: void 0, reason: void 0 };
|
|
8081
|
+
this.onclose?.({ code: 1006, reason: "" });
|
|
8082
|
+
}
|
|
8083
|
+
send(data) {
|
|
8084
|
+
if (this.#ws === null)
|
|
8085
|
+
throw new Error("The realtime connection is not open.");
|
|
8086
|
+
this.#ws.send(data);
|
|
8087
|
+
}
|
|
8088
|
+
close(code, reason) {
|
|
8089
|
+
if (this.#closed !== null) return;
|
|
8090
|
+
this.#closed = { code, reason };
|
|
8091
|
+
this.#ws?.close(code, reason);
|
|
8092
|
+
}
|
|
8093
|
+
};
|
|
7887
8094
|
var HEARTBEAT_MS = 25e3;
|
|
7888
8095
|
var TIMEOUT_MS = 1e4;
|
|
7889
8096
|
var PACED_BURST = CONNECTION_FRAME_BURST - 20;
|
|
@@ -8297,19 +8504,19 @@ var RealtimeSocket = class {
|
|
|
8297
8504
|
}, this.#timeoutMs);
|
|
8298
8505
|
}
|
|
8299
8506
|
#open() {
|
|
8300
|
-
const
|
|
8507
|
+
const passed = this.#config.options.WebSocket;
|
|
8508
|
+
const runtimes = globalThis.WebSocket;
|
|
8509
|
+
const Impl = passed ?? runtimes;
|
|
8301
8510
|
if (typeof Impl !== "function")
|
|
8302
8511
|
return this.#stop(websocketUnavailable(), true);
|
|
8303
8512
|
if (this.#config.service && !isKnownServer())
|
|
8304
8513
|
return this.#stop(serviceKeyOffServer(), true);
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
8311
|
-
return this.#stop(socketRefused(error), true);
|
|
8312
|
-
}
|
|
8514
|
+
const made = this.#config.service ? this.#serviceSocket(
|
|
8515
|
+
Impl,
|
|
8516
|
+
passed === void 0 || passed === runtimes
|
|
8517
|
+
) : this.#anonSocket(Impl);
|
|
8518
|
+
if ("error" in made) return this.#stop(made.error, true);
|
|
8519
|
+
const ws = made.ws;
|
|
8313
8520
|
this.#generation += 1;
|
|
8314
8521
|
this.#ws = ws;
|
|
8315
8522
|
this.#state = "connecting";
|
|
@@ -8324,6 +8531,48 @@ var RealtimeSocket = class {
|
|
|
8324
8531
|
this.#timeoutMs
|
|
8325
8532
|
);
|
|
8326
8533
|
}
|
|
8534
|
+
/**
|
|
8535
|
+
* The anon key's connection: the one place a key goes into a URL (design
|
|
8536
|
+
* §10 "3-2d") — a browser's WebSocket has no headers to put it in.
|
|
8537
|
+
*/
|
|
8538
|
+
#anonSocket(Impl) {
|
|
8539
|
+
try {
|
|
8540
|
+
return {
|
|
8541
|
+
ws: new Impl(socketUrl(this.#config.baseUrl, this.#config.apiKey))
|
|
8542
|
+
};
|
|
8543
|
+
} catch (error) {
|
|
8544
|
+
return { error: socketRefused(error) };
|
|
8545
|
+
}
|
|
8546
|
+
}
|
|
8547
|
+
/**
|
|
8548
|
+
* The service key's connection (security audit 2026-11-14 F7): the key in
|
|
8549
|
+
* the `apikey` header and never in the URL, where edge and worker logs
|
|
8550
|
+
* would keep it. Node 22+, Bun and `ws` take `{ headers }` on the
|
|
8551
|
+
* constructor; workerd's constructor takes none, so there — unless the
|
|
8552
|
+
* app passed a WebSocket of its own, other than the runtime's — the
|
|
8553
|
+
* connection is a `fetch` upgrade (`UpgradeSocket`); Deno's takes none
|
|
8554
|
+
* either, and has no upgrade fetch, so the service key is refused there
|
|
8555
|
+
* until a WebSocket that takes headers (`ws`) is passed. `runtimes` says
|
|
8556
|
+
* the class is the runtime's own.
|
|
8557
|
+
*/
|
|
8558
|
+
#serviceSocket(Impl, runtimes) {
|
|
8559
|
+
const headers = { apikey: this.#config.apiKey };
|
|
8560
|
+
const url = socketUrl(this.#config.baseUrl, null);
|
|
8561
|
+
if (runtimes && isWorkerd())
|
|
8562
|
+
return {
|
|
8563
|
+
ws: new UpgradeSocket(
|
|
8564
|
+
this.#config.fetch ?? globalThis.fetch,
|
|
8565
|
+
`http${url.slice(2)}`,
|
|
8566
|
+
headers
|
|
8567
|
+
)
|
|
8568
|
+
};
|
|
8569
|
+
if (runtimes && isDeno()) return { error: websocketHeadersUnavailable() };
|
|
8570
|
+
try {
|
|
8571
|
+
return { ws: new Impl(url, { headers }) };
|
|
8572
|
+
} catch (error) {
|
|
8573
|
+
return { error: socketRefused(error) };
|
|
8574
|
+
}
|
|
8575
|
+
}
|
|
8327
8576
|
#opened(ws) {
|
|
8328
8577
|
if (this.#ws !== ws || this.#state !== "connecting") return;
|
|
8329
8578
|
clearTimeout(this.#openTimer);
|
|
@@ -9507,6 +9756,7 @@ var RealtimeClient = class {
|
|
|
9507
9756
|
apiKey: http.apiKey,
|
|
9508
9757
|
service,
|
|
9509
9758
|
options,
|
|
9759
|
+
fetch: http.fetch,
|
|
9510
9760
|
events: {
|
|
9511
9761
|
// Not awaited, and not live yet: onLive says when the hub took it.
|
|
9512
9762
|
opened: () => {
|
|
@@ -11211,14 +11461,14 @@ async function listAll(storage, bucket, prefix, recursive, search) {
|
|
|
11211
11461
|
truncated = true;
|
|
11212
11462
|
break;
|
|
11213
11463
|
}
|
|
11214
|
-
const
|
|
11464
|
+
const listed2 = await storage.from(bucket).list(folder, {
|
|
11215
11465
|
limit: PAGE,
|
|
11216
11466
|
offset,
|
|
11217
11467
|
...search === "" ? {} : { search }
|
|
11218
11468
|
});
|
|
11219
|
-
if (isOldSearchRefusal(
|
|
11469
|
+
if (isOldSearchRefusal(listed2.error))
|
|
11220
11470
|
throw new NaroError("BACKEND_OUTDATED", STORAGE_OUTDATED_MESSAGE);
|
|
11221
|
-
const page = unwrap(
|
|
11471
|
+
const page = unwrap(listed2);
|
|
11222
11472
|
for (const entry of page) {
|
|
11223
11473
|
const path = folder === "" ? entry.name : `${folder}/${entry.name}`;
|
|
11224
11474
|
if (isFolder(entry)) {
|
|
@@ -12604,6 +12854,12 @@ function parsePullSurface(value) {
|
|
|
12604
12854
|
async function runEnvPull(context) {
|
|
12605
12855
|
const file = context.positionals[0] ?? ".env.local";
|
|
12606
12856
|
const surface = parsePullSurface(context.flags.env);
|
|
12857
|
+
const path = resolve5(context.cwd, file);
|
|
12858
|
+
if (isSymbolicLink(path))
|
|
12859
|
+
throw new NaroError(
|
|
12860
|
+
"VALIDATION_FAILED",
|
|
12861
|
+
`${file} is a symbolic link. Remove it, or pass another file name.`
|
|
12862
|
+
);
|
|
12607
12863
|
const client = await context.client();
|
|
12608
12864
|
const projectId = await context.resolveProjectId();
|
|
12609
12865
|
if (!isReservedEnvTarget(surface))
|
|
@@ -12613,7 +12869,6 @@ async function runEnvPull(context) {
|
|
|
12613
12869
|
const count2 = Object.keys(vars).length;
|
|
12614
12870
|
const text = `# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
12615
12871
|
${serializeDotenv(vars)}`;
|
|
12616
|
-
const path = resolve5(context.cwd, file);
|
|
12617
12872
|
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
12618
12873
|
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
12619
12874
|
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
@@ -13014,7 +13269,7 @@ function functionName(context, usage) {
|
|
|
13014
13269
|
}
|
|
13015
13270
|
function statusWord(fn) {
|
|
13016
13271
|
if (fn.status !== "error") return fn.status;
|
|
13017
|
-
return `error
|
|
13272
|
+
return `error: ${printable(fn.error ?? "unknown error")}`;
|
|
13018
13273
|
}
|
|
13019
13274
|
var cpuHours = (ms) => (ms / 36e5).toLocaleString("en-US", { maximumFractionDigits: 1 });
|
|
13020
13275
|
function functionLines(list) {
|
|
@@ -13035,8 +13290,13 @@ function functionLines(list) {
|
|
|
13035
13290
|
return `${fn.name.padEnd(width)} ${version2.padEnd(versionWidth)} ${verify} ${statusWord(fn)} ${fn.url}${stale}`;
|
|
13036
13291
|
});
|
|
13037
13292
|
lines.push(
|
|
13038
|
-
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per
|
|
13293
|
+
`Up to ${list.limits.functions.toLocaleString("en-US")} functions per project.`
|
|
13039
13294
|
);
|
|
13295
|
+
const today = list.today;
|
|
13296
|
+
if (today)
|
|
13297
|
+
lines.push(
|
|
13298
|
+
`Functions today (UTC, the project's): ${today.invocations.toLocaleString("en-US")} calls, ${today.cpuMs.toLocaleString("en-US")} ms of CPU.`
|
|
13299
|
+
);
|
|
13040
13300
|
const usage = list.usage;
|
|
13041
13301
|
if (usage !== null) {
|
|
13042
13302
|
const percent2 = usage.cpuMsLimit > 0 ? Math.floor(usage.cpuMs / usage.cpuMsLimit * 100) : 0;
|
|
@@ -14067,6 +14327,8 @@ function meterRow(meter, limitsKnown) {
|
|
|
14067
14327
|
const label3 = METRIC_LABELS[meter.metric];
|
|
14068
14328
|
const used = formatCount(meter.used);
|
|
14069
14329
|
if (!limitsKnown) return [label3, used, "\u2014", "\u2014"];
|
|
14330
|
+
if (typeof meter.perProject === "number")
|
|
14331
|
+
return [label3, used, `${formatCount(meter.perProject)} per project`, "\u2014"];
|
|
14070
14332
|
if (meter.limit === null) return [label3, used, "No limit", "\u2014"];
|
|
14071
14333
|
if (meter.limit === 0) return [label3, used, "Not included", "\u2014"];
|
|
14072
14334
|
return [label3, used, formatCount(meter.limit), usedPercent(meter)];
|
|
@@ -14408,7 +14670,7 @@ Usage:
|
|
|
14408
14670
|
naro backend users delete <id|username|email>
|
|
14409
14671
|
Delete a user and end its sessions (asks first; --yes to skip)
|
|
14410
14672
|
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
|
|
14673
|
+
naro policy rules [--table <t>] Each table's policies as rules (public, authenticated, owner, none, custom, invalid) and conditions
|
|
14412
14674
|
naro policy create "CREATE POLICY <name> ON <table> \u2026" | --file <policies.sql>
|
|
14413
14675
|
Postgres's CREATE POLICY: AS PERMISSIVE|RESTRICTIVE,
|
|
14414
14676
|
FOR ALL|SELECT|INSERT|UPDATE|DELETE, TO anon|authenticated|public,
|
|
@@ -14469,7 +14731,8 @@ Usage:
|
|
|
14469
14731
|
naro functions new <name> Write naro/functions/<name>/index.ts, a handler to start from
|
|
14470
14732
|
naro functions deploy [name\u2026] [--no-verify-jwt] [--import-map <file>] [--skip-check]
|
|
14471
14733
|
Bundle on this machine (Node.js 22 or later), check that it starts, upload;
|
|
14472
|
-
every function in the folder when no name is given
|
|
14734
|
+
every function in the folder when no name is given;
|
|
14735
|
+
several go live together in one upload
|
|
14473
14736
|
naro functions versions <name> Its stored versions, newest first
|
|
14474
14737
|
naro functions rollback <name> [<version>]
|
|
14475
14738
|
Put an earlier version live again (the one before the live one by default)
|