naro-cli 0.9.1 → 0.11.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 +248 -52
- package/dist/artifact-types.d.ts +5 -0
- package/dist/{chunk-Z757IOF3.js → chunk-L7JOGWP3.js} +2455 -133
- package/dist/deploy.js +1 -1
- package/dist/git-info.d.ts +14 -1
- package/dist/index.js +2342 -1833
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
BACKEND_STEPS,
|
|
3
4
|
BOOKMARK_FORMAT_MESSAGE,
|
|
4
5
|
NaroClient,
|
|
5
6
|
NaroError,
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
buildResetPlan,
|
|
10
11
|
cellText,
|
|
11
12
|
clearSession,
|
|
13
|
+
commentText,
|
|
12
14
|
compareMigrations,
|
|
13
15
|
controlPlaneUrl,
|
|
14
16
|
countLintedTables,
|
|
@@ -33,11 +35,14 @@ import {
|
|
|
33
35
|
openBrowser,
|
|
34
36
|
parseGitHubRemote,
|
|
35
37
|
parsePolicyNode,
|
|
38
|
+
parsePolicyStatement,
|
|
36
39
|
parseRedirectEntry,
|
|
37
40
|
pendingMigrationNames,
|
|
38
41
|
policiesForTypes,
|
|
39
42
|
projectPath,
|
|
40
43
|
promptForMissingEnv,
|
|
44
|
+
quoteIdent,
|
|
45
|
+
quotePolicyName,
|
|
41
46
|
readProjectLink,
|
|
42
47
|
refreshStoredSession,
|
|
43
48
|
removeProjectLink,
|
|
@@ -48,11 +53,12 @@ import {
|
|
|
48
53
|
schemaMigrationTitle,
|
|
49
54
|
serializeDotenv,
|
|
50
55
|
slugify,
|
|
56
|
+
splitPolicyStatements,
|
|
51
57
|
summarizeStatement,
|
|
52
58
|
systemTablePredicate,
|
|
53
59
|
toNaroError,
|
|
54
60
|
writeProjectLink
|
|
55
|
-
} from "./chunk-
|
|
61
|
+
} from "./chunk-L7JOGWP3.js";
|
|
56
62
|
import "./chunk-K3NQKI34.js";
|
|
57
63
|
|
|
58
64
|
// src/args.ts
|
|
@@ -82,8 +88,10 @@ var BOOLEAN_FLAGS = {
|
|
|
82
88
|
"--unique": "unique",
|
|
83
89
|
"--no-write": "noWrite",
|
|
84
90
|
"--signed-in": "signedIn",
|
|
91
|
+
"--allow-unindexed": "allowUnindexed",
|
|
85
92
|
"--api-key-stdin": "apiKeyStdin",
|
|
86
|
-
"--client-secret-stdin": "clientSecretStdin"
|
|
93
|
+
"--client-secret-stdin": "clientSecretStdin",
|
|
94
|
+
"--password-stdin": "passwordStdin"
|
|
87
95
|
};
|
|
88
96
|
var REPEATED_FLAGS = {
|
|
89
97
|
"--column": "columns"
|
|
@@ -120,7 +128,11 @@ var STRING_FLAGS = {
|
|
|
120
128
|
"--from-file": "fromFile",
|
|
121
129
|
"--provider": "provider",
|
|
122
130
|
"--from": "from",
|
|
123
|
-
"--client-id": "clientId"
|
|
131
|
+
"--client-id": "clientId",
|
|
132
|
+
"--username": "username",
|
|
133
|
+
"--email": "email",
|
|
134
|
+
"--password": "password",
|
|
135
|
+
"--min-length": "minLength"
|
|
124
136
|
};
|
|
125
137
|
var SECRET_FLAGS = {
|
|
126
138
|
"--api-key": "--api-key-stdin",
|
|
@@ -571,6 +583,9 @@ function printable(text) {
|
|
|
571
583
|
}
|
|
572
584
|
return out;
|
|
573
585
|
}
|
|
586
|
+
function isPrintable(text) {
|
|
587
|
+
return printable(text) === text;
|
|
588
|
+
}
|
|
574
589
|
function warningLine(warning) {
|
|
575
590
|
if (warning === "out-of-order")
|
|
576
591
|
return " \u26A0 older than an applied migration \u2014 db push will still apply it";
|
|
@@ -985,12 +1000,24 @@ var AUTH_USAGE = {
|
|
|
985
1000
|
oauth: "Usage: naro backend auth oauth set google|github --client-id <id> --client-secret-stdin",
|
|
986
1001
|
oauthRemove: "Usage: naro backend auth oauth remove google|github",
|
|
987
1002
|
redirect: "Usage: naro backend auth redirects add|remove <url>",
|
|
988
|
-
verification: "Usage: naro backend auth require-verification on|off"
|
|
1003
|
+
verification: "Usage: naro backend auth require-verification on|off",
|
|
1004
|
+
signups: "Usage: naro backend auth signups on|off",
|
|
1005
|
+
password: "Usage: naro backend auth password --min-length <n>"
|
|
989
1006
|
};
|
|
1007
|
+
var MIN_LENGTH_FLOOR = 6;
|
|
1008
|
+
var MIN_LENGTH_CEILING = 128;
|
|
990
1009
|
function sentence2(text) {
|
|
991
1010
|
const line = printable(text).trim();
|
|
992
1011
|
return /[.!?…]$/.test(line) ? line : `${line}.`;
|
|
993
1012
|
}
|
|
1013
|
+
function stoppedMessage(backend) {
|
|
1014
|
+
const step = backend.step ?? "worker";
|
|
1015
|
+
const reason = backend.error?.trim() ? `: ${sentence2(backend.error)}` : ".";
|
|
1016
|
+
if (BACKEND_STEPS.indexOf(step) > BACKEND_STEPS.indexOf("worker"))
|
|
1017
|
+
return `Saved and live on the Naro worker, but updating the backend stopped after the upload, at the ${step} step${reason} Run \`naro backend enable\` to finish.`;
|
|
1018
|
+
return `Saved, but not live yet: updating the Naro worker stopped at the ${step} step${reason} The worker keeps its previous settings until \`naro backend enable\` puts the saved ones on it.`;
|
|
1019
|
+
}
|
|
1020
|
+
var NOT_APPLIED_WARNING = "\u26A0 These are the saved settings. The Naro worker's last update did not get past its upload, so it may still run older ones \u2014 run `naro backend enable` to put these on it.";
|
|
994
1021
|
function authLines(auth) {
|
|
995
1022
|
const lines = [
|
|
996
1023
|
auth.email ? `Email: ${auth.email.provider} \u2014 ${printable(auth.email.from)} (API key ${printable(auth.email.apiKeyPrefix)}\u2026)` : "Email: not set up"
|
|
@@ -1003,7 +1030,9 @@ function authLines(auth) {
|
|
|
1003
1030
|
}
|
|
1004
1031
|
lines.push(
|
|
1005
1032
|
`Redirects: ${[`${productionOrigins(auth).join(", ")} (production hosts, always)`, ...auth.redirectOrigins].join(", ")}`,
|
|
1006
|
-
`Email verification: ${auth.requireEmailVerification ? "required to sign in" : "not required"}
|
|
1033
|
+
`Email verification: ${auth.requireEmailVerification ? "required to sign in" : "not required"}`,
|
|
1034
|
+
`Sign-ups: ${auth.signupsEnabled ? "open" : "closed (only `naro backend users create` makes accounts)"}`,
|
|
1035
|
+
`Minimum password length: ${String(auth.passwordMinLength)}`
|
|
1007
1036
|
);
|
|
1008
1037
|
return lines;
|
|
1009
1038
|
}
|
|
@@ -1026,9 +1055,9 @@ function adviceLines2(auth) {
|
|
|
1026
1055
|
"Redirect URIs to register with each provider \u2014 the one for the origin your app passes to createClient({ url }):"
|
|
1027
1056
|
);
|
|
1028
1057
|
for (const provider of ["google", "github"]) {
|
|
1029
|
-
const
|
|
1058
|
+
const label2 = ` ${LABELS[provider]}: `;
|
|
1030
1059
|
callbacksOf(auth, provider).forEach(
|
|
1031
|
-
(url, index) => lines.push(`${index === 0 ?
|
|
1060
|
+
(url, index) => lines.push(`${index === 0 ? label2 : " ".repeat(label2.length)}${url}`)
|
|
1032
1061
|
);
|
|
1033
1062
|
}
|
|
1034
1063
|
return lines;
|
|
@@ -1037,6 +1066,7 @@ async function runBackendAuthStatus(context) {
|
|
|
1037
1066
|
const client = await context.client();
|
|
1038
1067
|
const projectId = await context.resolveProjectId();
|
|
1039
1068
|
const auth = await client.backend.auth(projectId);
|
|
1069
|
+
if (auth.applied === false) context.output.warn(NOT_APPLIED_WARNING);
|
|
1040
1070
|
context.output.result({ projectId, auth }, [
|
|
1041
1071
|
...authLines(auth),
|
|
1042
1072
|
...adviceLines2(auth)
|
|
@@ -1066,17 +1096,22 @@ async function change(context, send, done, after) {
|
|
|
1066
1096
|
"The change did not answer within 2 minutes. `naro backend auth` shows what is saved, and `naro backend enable` finishes putting it on the Naro worker.",
|
|
1067
1097
|
{ cause: error }
|
|
1068
1098
|
);
|
|
1099
|
+
const saved = isNaroError(error) ? error.details?.auth : void 0;
|
|
1100
|
+
if (isNaroError(error) && error.code === "BACKEND_PROVISION_FAILED" && typeof saved === "object" && saved !== null) {
|
|
1101
|
+
for (const line of authLines(saved))
|
|
1102
|
+
context.output.log(line);
|
|
1103
|
+
throw new NaroError(error.code, error.message, {
|
|
1104
|
+
details: { projectId, ...error.details }
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1069
1107
|
throw error;
|
|
1070
1108
|
}
|
|
1071
1109
|
const { auth, backend } = answer;
|
|
1072
1110
|
if (backend.status === "error") {
|
|
1073
1111
|
for (const line of authLines(auth)) context.output.log(line);
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
`Saved, but updating the Naro worker stopped at ${step}${backend.error ? `: ${sentence2(backend.error)}` : "."} Run \`naro backend enable\` to finish.`,
|
|
1078
|
-
{ details: { projectId, auth, backend } }
|
|
1079
|
-
);
|
|
1112
|
+
throw new NaroError("BACKEND_PROVISION_FAILED", stoppedMessage(backend), {
|
|
1113
|
+
details: { projectId, auth, backend }
|
|
1114
|
+
});
|
|
1080
1115
|
}
|
|
1081
1116
|
if (backend.status !== "ready")
|
|
1082
1117
|
context.output.warn(
|
|
@@ -1272,6 +1307,199 @@ async function runBackendAuthRequireVerification(context) {
|
|
|
1272
1307
|
]
|
|
1273
1308
|
);
|
|
1274
1309
|
}
|
|
1310
|
+
async function runBackendAuthSignups(context) {
|
|
1311
|
+
const [value] = context.positionals;
|
|
1312
|
+
if (value !== "on" && value !== "off")
|
|
1313
|
+
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.signups);
|
|
1314
|
+
const open = value === "on";
|
|
1315
|
+
const client = await context.client();
|
|
1316
|
+
await change(
|
|
1317
|
+
context,
|
|
1318
|
+
(projectId) => client.backend.setAuth(projectId, { signupsEnabled: open }),
|
|
1319
|
+
[
|
|
1320
|
+
open ? "\u2705 Sign-ups are open: anyone can create an account, by email or with Google or GitHub." : "\u2705 Sign-ups are closed: sign-up answers 403 SIGNUPS_DISABLED, and a first Google or GitHub sign-in is refused. Existing users still sign in; create accounts with `naro backend users create`."
|
|
1321
|
+
]
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
async function runBackendAuthPassword(context) {
|
|
1325
|
+
const given = context.flags.minLength;
|
|
1326
|
+
if (given === void 0 || context.positionals.length > 0)
|
|
1327
|
+
throw new NaroError("VALIDATION_FAILED", AUTH_USAGE.password);
|
|
1328
|
+
const length = /^\d{1,3}$/.test(given) ? Number(given) : Number.NaN;
|
|
1329
|
+
if (!Number.isInteger(length) || length < MIN_LENGTH_FLOOR || length > MIN_LENGTH_CEILING)
|
|
1330
|
+
throw new NaroError(
|
|
1331
|
+
"VALIDATION_FAILED",
|
|
1332
|
+
`--min-length must be a whole number from ${String(MIN_LENGTH_FLOOR)} to ${String(MIN_LENGTH_CEILING)}. ${AUTH_USAGE.password}`
|
|
1333
|
+
);
|
|
1334
|
+
const client = await context.client();
|
|
1335
|
+
await change(
|
|
1336
|
+
context,
|
|
1337
|
+
(projectId) => client.backend.setAuth(projectId, { passwordMinLength: length }),
|
|
1338
|
+
[
|
|
1339
|
+
`\u2705 New passwords take at least ${String(length)} characters: sign-up, password reset and \`naro backend users create\`. Passwords set before still sign in.`
|
|
1340
|
+
]
|
|
1341
|
+
);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// src/commands/backend-users.ts
|
|
1345
|
+
var USERS_USAGE = {
|
|
1346
|
+
create: "Usage: naro backend users create (--username <name> | --email <address>) (--password-stdin | --password <password>)",
|
|
1347
|
+
delete: "Usage: naro backend users delete <id|username|email> [--yes]"
|
|
1348
|
+
};
|
|
1349
|
+
var PASSWORD_FLAG_WARNING = "\u26A0 --password puts the password in your shell history. Pipe it in with --password-stdin instead.";
|
|
1350
|
+
var OUTDATED_MESSAGE = "This project's backend is older than user management. Run `naro backend enable` once to update this project's backend. Then run this command again.";
|
|
1351
|
+
var PAGE_SIZE = 1e3;
|
|
1352
|
+
var processStdin3 = {
|
|
1353
|
+
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1354
|
+
readStdin
|
|
1355
|
+
};
|
|
1356
|
+
async function usersCall(call) {
|
|
1357
|
+
try {
|
|
1358
|
+
return await call();
|
|
1359
|
+
} catch (error) {
|
|
1360
|
+
if (isNaroError(error) && error.code === "BACKEND_OUTDATED")
|
|
1361
|
+
throw new NaroError("BACKEND_OUTDATED", OUTDATED_MESSAGE, {
|
|
1362
|
+
...error.status !== void 0 ? { status: error.status } : {},
|
|
1363
|
+
cause: error
|
|
1364
|
+
});
|
|
1365
|
+
throw error;
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async function allUsers(client, projectId) {
|
|
1369
|
+
const users = [];
|
|
1370
|
+
let total = 0;
|
|
1371
|
+
for (let page = 1; ; page++) {
|
|
1372
|
+
const listed = await usersCall(
|
|
1373
|
+
() => client.backend.listUsers(projectId, { page, perPage: PAGE_SIZE })
|
|
1374
|
+
);
|
|
1375
|
+
total = listed.total;
|
|
1376
|
+
users.push(...listed.users);
|
|
1377
|
+
if (listed.users.length < PAGE_SIZE || users.length >= total)
|
|
1378
|
+
return { users, total };
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
function orDash(value) {
|
|
1382
|
+
return value === null ? "-" : printable(value);
|
|
1383
|
+
}
|
|
1384
|
+
function label(user) {
|
|
1385
|
+
return printable(user.username ?? user.email ?? user.id);
|
|
1386
|
+
}
|
|
1387
|
+
function userTable(users) {
|
|
1388
|
+
const rows = users.map((user) => [
|
|
1389
|
+
printable(user.id),
|
|
1390
|
+
orDash(user.username),
|
|
1391
|
+
orDash(user.email),
|
|
1392
|
+
user.emailVerified ? "yes" : "no",
|
|
1393
|
+
printable(user.createdAt)
|
|
1394
|
+
]);
|
|
1395
|
+
const header = ["ID", "USERNAME", "EMAIL", "VERIFIED", "CREATED"];
|
|
1396
|
+
const widths = header.map(
|
|
1397
|
+
(title, column) => Math.max(title.length, ...rows.map((row) => row[column]?.length ?? 0))
|
|
1398
|
+
);
|
|
1399
|
+
return [header, ...rows].map(
|
|
1400
|
+
(row) => row.map(
|
|
1401
|
+
(cell, column) => column === row.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)
|
|
1402
|
+
).join(" ")
|
|
1403
|
+
);
|
|
1404
|
+
}
|
|
1405
|
+
async function runBackendUsersList(context) {
|
|
1406
|
+
const client = await context.client();
|
|
1407
|
+
const projectId = await context.resolveProjectId();
|
|
1408
|
+
const { users, total } = await allUsers(client, projectId);
|
|
1409
|
+
context.output.result(
|
|
1410
|
+
{ projectId, users, total },
|
|
1411
|
+
users.length === 0 ? [
|
|
1412
|
+
"No users yet. Create one with `naro backend users create --username <name> --password-stdin`."
|
|
1413
|
+
] : [
|
|
1414
|
+
`${String(total)} ${total === 1 ? "user" : "users"}, newest first:`,
|
|
1415
|
+
...userTable(users)
|
|
1416
|
+
]
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
async function passwordOf(context, input) {
|
|
1420
|
+
const { password, passwordStdin } = context.flags;
|
|
1421
|
+
if (password !== void 0 && passwordStdin)
|
|
1422
|
+
throw new NaroError(
|
|
1423
|
+
"VALIDATION_FAILED",
|
|
1424
|
+
`Pass the password one way: --password-stdin or --password. ${USERS_USAGE.create}`
|
|
1425
|
+
);
|
|
1426
|
+
if (password !== void 0) {
|
|
1427
|
+
context.output.warn(PASSWORD_FLAG_WARNING);
|
|
1428
|
+
if (password === "")
|
|
1429
|
+
throw new NaroError("VALIDATION_FAILED", "The password is empty.");
|
|
1430
|
+
return password;
|
|
1431
|
+
}
|
|
1432
|
+
if (!passwordStdin)
|
|
1433
|
+
throw new NaroError(
|
|
1434
|
+
"VALIDATION_FAILED",
|
|
1435
|
+
`Pass the password on stdin with --password-stdin. ${USERS_USAGE.create}`
|
|
1436
|
+
);
|
|
1437
|
+
const value = input.stdinIsTty() ? await context.prompter.askHidden("? Password: ") : await input.readStdin();
|
|
1438
|
+
if (value === "")
|
|
1439
|
+
throw new NaroError("VALIDATION_FAILED", "No password came in on stdin.");
|
|
1440
|
+
return value;
|
|
1441
|
+
}
|
|
1442
|
+
function userLines(user) {
|
|
1443
|
+
return [
|
|
1444
|
+
`ID: ${printable(user.id)}`,
|
|
1445
|
+
`Username: ${orDash(user.username)}`,
|
|
1446
|
+
`Email: ${orDash(user.email)}`,
|
|
1447
|
+
`Verified: ${user.emailVerified ? "yes" : "no"}`,
|
|
1448
|
+
`Created: ${printable(user.createdAt)}`
|
|
1449
|
+
];
|
|
1450
|
+
}
|
|
1451
|
+
async function runBackendUsersCreate(context, input = processStdin3) {
|
|
1452
|
+
const { username, email } = context.flags;
|
|
1453
|
+
if (username === void 0 && email === void 0 || context.positionals.length > 0)
|
|
1454
|
+
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.create);
|
|
1455
|
+
const password = await passwordOf(context, input);
|
|
1456
|
+
const client = await context.client();
|
|
1457
|
+
const projectId = await context.resolveProjectId();
|
|
1458
|
+
const body = { password };
|
|
1459
|
+
if (username !== void 0) body.username = username;
|
|
1460
|
+
if (email !== void 0) body.email = email;
|
|
1461
|
+
const user = await usersCall(
|
|
1462
|
+
() => client.backend.createUser(projectId, body)
|
|
1463
|
+
);
|
|
1464
|
+
const signIn = user.username === null ? "naro.auth.signIn({ email, password })" : `naro.auth.signIn({ username: ${JSON.stringify(printable(user.username))}, password })`;
|
|
1465
|
+
context.output.result({ projectId, user }, [
|
|
1466
|
+
`\u2705 Created user ${label(user)}. It signs in at once with ${signIn}.`,
|
|
1467
|
+
...userLines(user)
|
|
1468
|
+
]);
|
|
1469
|
+
}
|
|
1470
|
+
function find(users, given) {
|
|
1471
|
+
const byId = users.find((user) => user.id === given);
|
|
1472
|
+
if (byId) return byId;
|
|
1473
|
+
const wanted = given.toLowerCase();
|
|
1474
|
+
return users.find(
|
|
1475
|
+
(user) => given.includes("@") ? user.email?.toLowerCase() === wanted : user.username?.toLowerCase() === wanted
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
async function runBackendUsersDelete(context) {
|
|
1479
|
+
const [given] = context.positionals;
|
|
1480
|
+
if (given === void 0 || context.positionals.length > 1)
|
|
1481
|
+
throw new NaroError("VALIDATION_FAILED", USERS_USAGE.delete);
|
|
1482
|
+
const client = await context.client();
|
|
1483
|
+
const projectId = await context.resolveProjectId();
|
|
1484
|
+
const { users } = await allUsers(client, projectId);
|
|
1485
|
+
const user = find(users, given);
|
|
1486
|
+
if (!user)
|
|
1487
|
+
throw new NaroError(
|
|
1488
|
+
"USER_NOT_FOUND",
|
|
1489
|
+
`No user has the id, username or email ${printable(given)}. \`naro backend users\` lists them.`
|
|
1490
|
+
);
|
|
1491
|
+
const named = user.id === label(user) ? label(user) : `${label(user)} (${printable(user.id)})`;
|
|
1492
|
+
await confirmOrAbort(
|
|
1493
|
+
context,
|
|
1494
|
+
`Delete the user ${named}? Their sessions end at once and the account cannot be brought back.`
|
|
1495
|
+
);
|
|
1496
|
+
const deleted = await usersCall(
|
|
1497
|
+
() => client.backend.deleteUser(projectId, user.id)
|
|
1498
|
+
);
|
|
1499
|
+
context.output.result({ projectId, user: deleted }, [
|
|
1500
|
+
`\u2705 Deleted user ${named}. Their sessions have ended.`
|
|
1501
|
+
]);
|
|
1502
|
+
}
|
|
1275
1503
|
|
|
1276
1504
|
// src/commands/cancel.ts
|
|
1277
1505
|
async function runCancel(context) {
|
|
@@ -1300,7 +1528,7 @@ async function runCancel(context) {
|
|
|
1300
1528
|
// src/commands/db.ts
|
|
1301
1529
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1302
1530
|
import { resolve } from "path";
|
|
1303
|
-
var
|
|
1531
|
+
var processStdin4 = {
|
|
1304
1532
|
stdinIsTty: () => process.stdin.isTTY === true,
|
|
1305
1533
|
readStdin
|
|
1306
1534
|
};
|
|
@@ -1418,7 +1646,7 @@ async function readSql(context, deps) {
|
|
|
1418
1646
|
throw new NaroError("VALIDATION_FAILED", USAGE);
|
|
1419
1647
|
return sql;
|
|
1420
1648
|
}
|
|
1421
|
-
async function runDbQuery(context, deps =
|
|
1649
|
+
async function runDbQuery(context, deps = processStdin4) {
|
|
1422
1650
|
const sql = await readSql(context, deps);
|
|
1423
1651
|
const client = await context.client();
|
|
1424
1652
|
const projectId = await context.resolveProjectId();
|
|
@@ -1682,7 +1910,7 @@ async function runDbRestore(context) {
|
|
|
1682
1910
|
target = resolved.bookmark;
|
|
1683
1911
|
at = resolved.timestamp ?? given.timestamp;
|
|
1684
1912
|
}
|
|
1685
|
-
const
|
|
1913
|
+
const label2 = at === null ? `bookmark ${target}` : `${at} (bookmark ${target})`;
|
|
1686
1914
|
if (context.flags.dryRun) {
|
|
1687
1915
|
context.output.result(
|
|
1688
1916
|
{
|
|
@@ -1690,13 +1918,13 @@ async function runDbRestore(context) {
|
|
|
1690
1918
|
bookmark: target,
|
|
1691
1919
|
...at === null ? {} : { timestamp: at }
|
|
1692
1920
|
},
|
|
1693
|
-
[`Would restore to ${
|
|
1921
|
+
[`Would restore to ${label2}. Nothing was changed.`]
|
|
1694
1922
|
);
|
|
1695
1923
|
return;
|
|
1696
1924
|
}
|
|
1697
1925
|
await confirmOrAbort(
|
|
1698
1926
|
context,
|
|
1699
|
-
`Restore the project database to ${
|
|
1927
|
+
`Restore the project database to ${label2}? Everything written after that point is rolled back \u2014 migration history included \u2014 and queries running right now are cancelled.`
|
|
1700
1928
|
);
|
|
1701
1929
|
const before = await bookmarkNow(context, client, projectId);
|
|
1702
1930
|
let restored;
|
|
@@ -1999,1987 +2227,2207 @@ async function runDbReset(context) {
|
|
|
1999
2227
|
// src/commands/db-schema.ts
|
|
2000
2228
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
2001
2229
|
import { join as join3 } from "path";
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2230
|
+
|
|
2231
|
+
// src/commands/policy.ts
|
|
2232
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
2233
|
+
import { resolve as resolve3 } from "path";
|
|
2234
|
+
|
|
2235
|
+
// ../../packages/naro-sdk/src/query/filters.ts
|
|
2236
|
+
var MAX_FILTER_DEPTH = 4;
|
|
2237
|
+
var OPERATORS = {
|
|
2238
|
+
eq: "eq",
|
|
2239
|
+
neq: "neq",
|
|
2240
|
+
gt: "gt",
|
|
2241
|
+
gte: "gte",
|
|
2242
|
+
lt: "lt",
|
|
2243
|
+
lte: "lte",
|
|
2244
|
+
in: "in",
|
|
2245
|
+
is: "is",
|
|
2246
|
+
like: "like",
|
|
2247
|
+
ilike: "ilike",
|
|
2248
|
+
contains: "contains",
|
|
2249
|
+
containedBy: "containedBy",
|
|
2250
|
+
overlaps: "overlaps",
|
|
2251
|
+
cs: "contains",
|
|
2252
|
+
cd: "containedBy",
|
|
2253
|
+
ov: "overlaps"
|
|
2013
2254
|
};
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
"set-default": "SET DEFAULT",
|
|
2019
|
-
restrict: "RESTRICT",
|
|
2020
|
-
"no-action": "NO ACTION"
|
|
2255
|
+
function operatorOf(name) {
|
|
2256
|
+
return typeof name === "string" && Object.hasOwn(OPERATORS, name) ? OPERATORS[name] : void 0;
|
|
2257
|
+
}
|
|
2258
|
+
var Refusal = class extends Error {
|
|
2021
2259
|
};
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
function segments(spec) {
|
|
2025
|
-
const parts = [];
|
|
2026
|
-
let current = "";
|
|
2027
|
-
let quoted = false;
|
|
2028
|
-
for (const char of spec) {
|
|
2029
|
-
if (char === "'") quoted = !quoted;
|
|
2030
|
-
if (char === ":" && !quoted) {
|
|
2031
|
-
parts.push(current);
|
|
2032
|
-
current = "";
|
|
2033
|
-
continue;
|
|
2034
|
-
}
|
|
2035
|
-
current += char;
|
|
2036
|
-
}
|
|
2037
|
-
if (quoted) return null;
|
|
2038
|
-
parts.push(current);
|
|
2039
|
-
return parts;
|
|
2260
|
+
function refuse(reason) {
|
|
2261
|
+
throw new Refusal(reason);
|
|
2040
2262
|
}
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
if (lower === "false") return false;
|
|
2048
|
-
const upper = text.toUpperCase();
|
|
2049
|
-
if (KEYWORDS.has(upper)) return { keyword: upper };
|
|
2050
|
-
if (NUMBER.test(text)) return Number(text);
|
|
2051
|
-
return text;
|
|
2263
|
+
var LIST_HINT = {
|
|
2264
|
+
in: ["(", ")", "needs a list in parentheses, like in.(a,b),"],
|
|
2265
|
+
array: ["{", "}", "needs a list in braces, like cs.{a,b},"]
|
|
2266
|
+
};
|
|
2267
|
+
function isSpace(char) {
|
|
2268
|
+
return char !== "" && char.trim() === "";
|
|
2052
2269
|
}
|
|
2053
|
-
function
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
);
|
|
2061
|
-
const [name, type, ...modifiers] = parts;
|
|
2062
|
-
if (!name || !type) throw new NaroError("VALIDATION_FAILED", SPEC_USAGE);
|
|
2063
|
-
const column = {
|
|
2064
|
-
name,
|
|
2065
|
-
type
|
|
2066
|
-
};
|
|
2067
|
-
let onDelete;
|
|
2068
|
-
for (const modifier of modifiers) {
|
|
2069
|
-
const eq = modifier.indexOf("=");
|
|
2070
|
-
const key = (eq < 0 ? modifier : modifier.slice(0, eq)).toLowerCase();
|
|
2071
|
-
const value = eq < 0 ? void 0 : modifier.slice(eq + 1);
|
|
2072
|
-
if (value === void 0 && key === "pk") column.primaryKey = true;
|
|
2073
|
-
else if (value === void 0 && key === "autoincrement")
|
|
2074
|
-
column.autoIncrement = true;
|
|
2075
|
-
else if (value === void 0 && key === "notnull") column.notNull = true;
|
|
2076
|
-
else if (value === void 0 && key === "unique") column.unique = true;
|
|
2077
|
-
else if (value !== void 0 && key === "default")
|
|
2078
|
-
column.default = defaultValue(value);
|
|
2079
|
-
else if (value !== void 0 && key === "references") {
|
|
2080
|
-
const dot = value.lastIndexOf(".");
|
|
2081
|
-
const table = dot < 0 ? "" : value.slice(0, dot);
|
|
2082
|
-
const target = dot < 0 ? "" : value.slice(dot + 1);
|
|
2083
|
-
if (!table || !target)
|
|
2084
|
-
throw new NaroError(
|
|
2085
|
-
"VALIDATION_FAILED",
|
|
2086
|
-
`${shown}: references needs <table>.<column>.`
|
|
2087
|
-
);
|
|
2088
|
-
column.references = { table, column: target };
|
|
2089
|
-
} else if (value !== void 0 && key === "ondelete") {
|
|
2090
|
-
onDelete = ON_DELETE[value.toLowerCase()];
|
|
2091
|
-
if (!onDelete)
|
|
2092
|
-
throw new NaroError(
|
|
2093
|
-
"VALIDATION_FAILED",
|
|
2094
|
-
`${shown}: ondelete takes cascade, set-null, set-default, restrict or no-action.`
|
|
2095
|
-
);
|
|
2096
|
-
} else
|
|
2097
|
-
throw new NaroError(
|
|
2098
|
-
"VALIDATION_FAILED",
|
|
2099
|
-
`${shown}: unknown modifier ${printable(modifier)}. Use pk, autoincrement, notnull, unique, default=<value>, references=<table>.<column> or ondelete=<action>.`
|
|
2100
|
-
);
|
|
2270
|
+
function looseWord(text) {
|
|
2271
|
+
return text.replace(/\s+/g, "").toLowerCase().replace(/\(\)$/, "");
|
|
2272
|
+
}
|
|
2273
|
+
var Parser = class {
|
|
2274
|
+
constructor(text, tokens = {}) {
|
|
2275
|
+
this.text = text;
|
|
2276
|
+
this.tokens = tokens;
|
|
2101
2277
|
}
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2278
|
+
index = 0;
|
|
2279
|
+
/**
|
|
2280
|
+
* A token at the next value, followed only by spaces and then one of
|
|
2281
|
+
* `stops` or the end — `auth.uid()x` is text, not the token.
|
|
2282
|
+
*/
|
|
2283
|
+
token(stops) {
|
|
2284
|
+
this.skipSpaces();
|
|
2285
|
+
const at = this.index;
|
|
2286
|
+
for (const [word, value] of Object.entries(this.tokens)) {
|
|
2287
|
+
if (!this.text.startsWith(word, at)) continue;
|
|
2288
|
+
let end = at + word.length;
|
|
2289
|
+
while (isSpace(this.text[end] ?? "")) end++;
|
|
2290
|
+
const next = this.text[end] ?? "";
|
|
2291
|
+
if (next !== "" && !stops.includes(next)) continue;
|
|
2292
|
+
this.index = end;
|
|
2293
|
+
return { value, word, at };
|
|
2294
|
+
}
|
|
2295
|
+
return void 0;
|
|
2109
2296
|
}
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
function policyLine(policy, dryRun = false) {
|
|
2113
|
-
const rules = [policy.select, policy.insert, policy.update, policy.delete];
|
|
2114
|
-
const owner = policy.ownerColumn === null ? "" : ` (owner column ${printable(policy.ownerColumn)})`;
|
|
2115
|
-
const uniform = rules.every((rule) => rule === rules[0]);
|
|
2116
|
-
const summary = uniform ? `${String(rules[0])} for select, insert, update and delete` : `select ${policy.select}, insert ${policy.insert}, update ${policy.update}, delete ${policy.delete}`;
|
|
2117
|
-
const head = dryRun ? "Would set the Data API policy" : "\u2713 Data API policy";
|
|
2118
|
-
return `${head} on ${printable(policy.table)}: ${summary}${owner}.`;
|
|
2119
|
-
}
|
|
2120
|
-
function removedLine(policy, dryRun) {
|
|
2121
|
-
const owner = policy.ownerColumn === null ? "" : `, owner column ${printable(policy.ownerColumn)}`;
|
|
2122
|
-
const rules = `select ${policy.select}, insert ${policy.insert}, update ${policy.update}, delete ${policy.delete}${owner}`;
|
|
2123
|
-
return `${dryRun ? "Would remove" : "\u2713 Removed"} the Data API policy on ${printable(policy.table)} (${rules}).`;
|
|
2124
|
-
}
|
|
2125
|
-
async function runSchemaChange(context, operations, question) {
|
|
2126
|
-
const dryRun = context.flags.dryRun === true;
|
|
2127
|
-
if (question !== void 0 && !dryRun)
|
|
2128
|
-
await confirmOrAbort(context, question);
|
|
2129
|
-
const sent = dryRun ? operations.map((operation) => {
|
|
2130
|
-
if (operation.op !== "dropTable" && operation.op !== "dropColumn")
|
|
2131
|
-
return operation;
|
|
2132
|
-
const { confirm: _confirm, ...rest } = operation;
|
|
2133
|
-
return rest;
|
|
2134
|
-
}) : operations;
|
|
2135
|
-
const client = await context.client();
|
|
2136
|
-
const projectId = await context.resolveProjectId();
|
|
2137
|
-
const dir = migrationsDir(context);
|
|
2138
|
-
const local = listMigrationFiles(dir);
|
|
2139
|
-
const ledger = await appliedMigrations(client, projectId);
|
|
2140
|
-
const pending = pendingMigrationNames(local, ledger);
|
|
2141
|
-
if (pending.length > 0 && !dryRun)
|
|
2142
|
-
throw migrationsPendingError(pending.map(printable));
|
|
2143
|
-
const name = nextMigrationFileName(
|
|
2144
|
-
[...local, ...ledger.map((row) => row.name)],
|
|
2145
|
-
schemaMigrationTitle(operations[0] ?? {})
|
|
2146
|
-
);
|
|
2147
|
-
const result = await client.db.schema(projectId, sent, {
|
|
2148
|
-
...dryRun ? { dryRun: true } : {},
|
|
2149
|
-
name
|
|
2150
|
-
});
|
|
2151
|
-
const sqlLines = result.sql.trimEnd().split("\n").map(printable);
|
|
2152
|
-
const removed = result.removedPolicies ?? [];
|
|
2153
|
-
if (result.dryRun) {
|
|
2154
|
-
for (const warning of result.warnings)
|
|
2155
|
-
context.output.warn(`\u26A0 ${printable(warning)}`);
|
|
2156
|
-
context.output.result({ projectId, ...result, file: null }, [
|
|
2157
|
-
...sqlLines,
|
|
2158
|
-
`Dry run \u2014 nothing was applied. It would be recorded as ${printable(result.name)}.`,
|
|
2159
|
-
...result.policies.map((policy) => policyLine(policy, true)),
|
|
2160
|
-
...removed.map((policy) => removedLine(policy, true))
|
|
2161
|
-
]);
|
|
2162
|
-
return;
|
|
2163
|
-
}
|
|
2164
|
-
const allWarnings = result.name === name ? result.warnings : [...result.warnings, migrationNameMismatch(name, result.name)];
|
|
2165
|
-
let file = null;
|
|
2166
|
-
let fileWarning;
|
|
2167
|
-
const relDir = relativeToCwd(context, dir);
|
|
2168
|
-
const rel = join3(relDir === "." ? "" : relDir, name);
|
|
2169
|
-
if (context.flags.noWrite) {
|
|
2170
|
-
fileWarning = `Not written to ${relDir} (--no-write): naro db reset will not re-apply ${result.name}, and naro migration list shows it as remote-only.`;
|
|
2171
|
-
} else {
|
|
2172
|
-
try {
|
|
2173
|
-
mkdirSync3(dir, { recursive: true });
|
|
2174
|
-
writeFileSync3(join3(dir, name), result.sql, { flag: "wx" });
|
|
2175
|
-
file = rel;
|
|
2176
|
-
} catch (error) {
|
|
2177
|
-
const reason = (error instanceof Error ? error.message : String(error)).replace(/[.\s]+$/, "");
|
|
2178
|
-
fileWarning = `Applied, but ${rel} could not be written: ${reason}. Save the SQL above as ${rel} so naro db push and naro db reset see it.`;
|
|
2179
|
-
}
|
|
2297
|
+
at() {
|
|
2298
|
+
return this.text[this.index] ?? "";
|
|
2180
2299
|
}
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
...sqlLines,
|
|
2194
|
-
file === null ? `\u2705 Applied ${printable(result.name)}.` : `\u2705 Applied ${printable(result.name)} and wrote ${printable(file)}.`,
|
|
2195
|
-
...result.policies.map((policy) => policyLine(policy)),
|
|
2196
|
-
...removed.map((policy) => removedLine(policy, false))
|
|
2197
|
-
]
|
|
2198
|
-
);
|
|
2199
|
-
}
|
|
2200
|
-
function columnsFlag(context) {
|
|
2201
|
-
return context.flags.columns ?? [];
|
|
2202
|
-
}
|
|
2203
|
-
async function runDbTableUsage() {
|
|
2204
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.table);
|
|
2205
|
-
}
|
|
2206
|
-
async function runDbIndexUsage() {
|
|
2207
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.index);
|
|
2208
|
-
}
|
|
2209
|
-
async function runDbTableCreate(context) {
|
|
2210
|
-
const [table] = context.positionals;
|
|
2211
|
-
const specs = columnsFlag(context);
|
|
2212
|
-
if (!table || specs.length === 0)
|
|
2213
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.create);
|
|
2214
|
-
const columns = specs.map(parseColumnSpec);
|
|
2215
|
-
const owner = context.flags.ownerColumn;
|
|
2216
|
-
await runSchemaChange(context, [
|
|
2217
|
-
{
|
|
2218
|
-
op: "createTable",
|
|
2219
|
-
table,
|
|
2220
|
-
columns,
|
|
2221
|
-
...context.flags.ifNotExists ? { ifNotExists: true } : {},
|
|
2222
|
-
...owner === void 0 ? {} : { owner: { column: owner } }
|
|
2223
|
-
}
|
|
2224
|
-
]);
|
|
2225
|
-
}
|
|
2226
|
-
async function runDbTableAddColumn(context) {
|
|
2227
|
-
const [table] = context.positionals;
|
|
2228
|
-
const specs = columnsFlag(context);
|
|
2229
|
-
const [spec] = specs;
|
|
2230
|
-
if (!table || spec === void 0 || specs.length !== 1)
|
|
2231
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
|
|
2232
|
-
await runSchemaChange(context, [
|
|
2233
|
-
{ op: "addColumn", table, column: parseColumnSpec(spec) }
|
|
2234
|
-
]);
|
|
2235
|
-
}
|
|
2236
|
-
async function runDbTableRenameColumn(context) {
|
|
2237
|
-
const [table, column, to] = context.positionals;
|
|
2238
|
-
if (!table || !column || !to)
|
|
2239
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
|
|
2240
|
-
await runSchemaChange(context, [{ op: "renameColumn", table, column, to }]);
|
|
2241
|
-
}
|
|
2242
|
-
async function runDbTableRename(context) {
|
|
2243
|
-
const [table, to] = context.positionals;
|
|
2244
|
-
if (!table || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
|
|
2245
|
-
await runSchemaChange(context, [{ op: "renameTable", table, to }]);
|
|
2246
|
-
}
|
|
2247
|
-
async function runDbTableDropColumn(context) {
|
|
2248
|
-
const [table, column] = context.positionals;
|
|
2249
|
-
if (!table || !column)
|
|
2250
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
|
|
2251
|
-
const target = `${table}.${column}`;
|
|
2252
|
-
await runSchemaChange(
|
|
2253
|
-
context,
|
|
2254
|
-
[{ op: "dropColumn", table, column, confirm: target }],
|
|
2255
|
-
`Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
|
|
2256
|
-
);
|
|
2257
|
-
}
|
|
2258
|
-
async function runDbTableDrop(context) {
|
|
2259
|
-
const [table] = context.positionals;
|
|
2260
|
-
if (!table) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
|
|
2261
|
-
await runSchemaChange(
|
|
2262
|
-
context,
|
|
2263
|
-
[{ op: "dropTable", table, confirm: table }],
|
|
2264
|
-
`Drop table ${printable(table)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
|
|
2265
|
-
);
|
|
2266
|
-
}
|
|
2267
|
-
async function runDbIndexCreate(context) {
|
|
2268
|
-
const [table, list] = context.positionals;
|
|
2269
|
-
const columns = listFlag(list);
|
|
2270
|
-
if (!table || columns.length === 0)
|
|
2271
|
-
throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
|
|
2272
|
-
const name = context.flags.name;
|
|
2273
|
-
await runSchemaChange(context, [
|
|
2274
|
-
{
|
|
2275
|
-
op: "createIndex",
|
|
2276
|
-
table,
|
|
2277
|
-
columns,
|
|
2278
|
-
...context.flags.unique ? { unique: true } : {},
|
|
2279
|
-
...name === void 0 ? {} : { name }
|
|
2280
|
-
}
|
|
2281
|
-
]);
|
|
2282
|
-
}
|
|
2283
|
-
async function runDbIndexDrop(context) {
|
|
2284
|
-
const [name] = context.positionals;
|
|
2285
|
-
if (!name) throw new NaroError("VALIDATION_FAILED", USAGE2.indexDrop);
|
|
2286
|
-
await runSchemaChange(context, [{ op: "dropIndex", name }]);
|
|
2287
|
-
}
|
|
2288
|
-
|
|
2289
|
-
// src/commands/git.ts
|
|
2290
|
-
var defaults2 = {
|
|
2291
|
-
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
2292
|
-
openBrowser,
|
|
2293
|
-
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
|
|
2294
|
-
};
|
|
2295
|
-
function connected(context, project) {
|
|
2296
|
-
const branch = project.defaultBranch;
|
|
2297
|
-
context.output.result(
|
|
2298
|
-
{
|
|
2299
|
-
projectId: project.id,
|
|
2300
|
-
repositoryFullName: project.repositoryFullName,
|
|
2301
|
-
autoDeploy: true
|
|
2302
|
-
},
|
|
2303
|
-
[
|
|
2304
|
-
`\u2705 Connected to ${project.repositoryFullName ?? "the repository"}.`,
|
|
2305
|
-
` Pushes to ${branch} now deploy automatically.`,
|
|
2306
|
-
"",
|
|
2307
|
-
" To turn this off: naro git disconnect"
|
|
2308
|
-
]
|
|
2309
|
-
);
|
|
2310
|
-
}
|
|
2311
|
-
async function runGitConnect(context, overrides = {}) {
|
|
2312
|
-
const deps = { ...defaults2, ...overrides };
|
|
2313
|
-
const { output } = context;
|
|
2314
|
-
const explicit = context.positionals[0];
|
|
2315
|
-
let fullName;
|
|
2316
|
-
if (explicit) {
|
|
2317
|
-
const parsed = parseGitHubRemote(explicit) ?? parseGitHubRemote(`https://github.com/${explicit}`);
|
|
2318
|
-
if (!parsed)
|
|
2319
|
-
throw new NaroError(
|
|
2320
|
-
"VALIDATION_FAILED",
|
|
2321
|
-
`"${explicit}" is not an owner/repo pair.`
|
|
2322
|
-
);
|
|
2323
|
-
fullName = parsed.fullName;
|
|
2324
|
-
} else {
|
|
2325
|
-
const remote = await deps.detectRemote(context.cwd);
|
|
2326
|
-
const parsed = remote ? parseGitHubRemote(remote) : null;
|
|
2327
|
-
if (!parsed) {
|
|
2328
|
-
throw new NaroError(
|
|
2329
|
-
"NO_GIT_REMOTE",
|
|
2330
|
-
remote ? `Could not read a GitHub owner/repo from "${remote}". Pass it directly: naro git connect <owner/repo>` : "This directory has no GitHub origin remote. Pass one directly: naro git connect <owner/repo>"
|
|
2331
|
-
);
|
|
2332
|
-
}
|
|
2333
|
-
fullName = parsed.fullName;
|
|
2300
|
+
/**
|
|
2301
|
+
* An unquoted value that is one of the tokens misspelled — `auth.uid`,
|
|
2302
|
+
* `AUTH.UID` — is refused: read as text it would compare with the words
|
|
2303
|
+
* themselves (사용자 리뷰 반영). Quoted, it is the text.
|
|
2304
|
+
*/
|
|
2305
|
+
notNearToken(value, at) {
|
|
2306
|
+
const loose = looseWord(value);
|
|
2307
|
+
for (const word of Object.keys(this.tokens))
|
|
2308
|
+
if (loose !== "" && loose === looseWord(word))
|
|
2309
|
+
refuse(
|
|
2310
|
+
`has ${JSON.stringify(value)} ${this.where(at)}: did you mean ${word}? Quote it to mean the text`
|
|
2311
|
+
);
|
|
2334
2312
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2313
|
+
/** `scalar`, with an unquoted value held to notNearToken. */
|
|
2314
|
+
bareOrQuoted(stops) {
|
|
2315
|
+
this.skipSpaces();
|
|
2316
|
+
const at = this.index;
|
|
2317
|
+
const quoted = this.at() === '"';
|
|
2318
|
+
const text = this.scalar(stops);
|
|
2319
|
+
if (!quoted) this.notNearToken(text, at);
|
|
2320
|
+
return { text, quoted };
|
|
2341
2321
|
}
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
"REPOSITORY_NOT_ACCESSIBLE",
|
|
2345
|
-
`Naro cannot reach ${fullName}, and no install URL was offered. Connect it from the dashboard.`
|
|
2346
|
-
);
|
|
2322
|
+
where(index = this.index) {
|
|
2323
|
+
return `at character ${String(index + 1)}`;
|
|
2347
2324
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
output.log(` Opening ${first.installUrl}`);
|
|
2351
|
-
deps.openBrowser(first.installUrl);
|
|
2352
|
-
output.log(" Waiting for the installation to finish...\n");
|
|
2353
|
-
await waitForInstall(context, projectId, fullName, deps);
|
|
2354
|
-
}
|
|
2355
|
-
async function waitForInstall(context, projectId, fullName, deps) {
|
|
2356
|
-
const client = await context.client();
|
|
2357
|
-
const project = await client.projects.get(projectId);
|
|
2358
|
-
const teams = await client.teams.list();
|
|
2359
|
-
const teamSlug = teams.find((team) => team.id === project.teamId)?.slug;
|
|
2360
|
-
if (teamSlug) {
|
|
2361
|
-
const attempts = deps.pollAttempts ?? 60;
|
|
2362
|
-
const interval = deps.pollIntervalMs ?? 3e3;
|
|
2363
|
-
for (let attempt2 = 0; attempt2 < attempts; attempt2 += 1) {
|
|
2364
|
-
await deps.sleep(interval);
|
|
2365
|
-
let reachable = false;
|
|
2366
|
-
try {
|
|
2367
|
-
const repositories = await client.github.listRepositories(teamSlug);
|
|
2368
|
-
reachable = repositories.some(
|
|
2369
|
-
(repository) => repository.fullName.toLowerCase() === fullName.toLowerCase()
|
|
2370
|
-
);
|
|
2371
|
-
} catch {
|
|
2372
|
-
}
|
|
2373
|
-
if (!reachable) continue;
|
|
2374
|
-
const retry = await client.projects.connectGit(projectId, fullName);
|
|
2375
|
-
if (retry.ok) {
|
|
2376
|
-
connected(context, retry.project);
|
|
2377
|
-
return;
|
|
2378
|
-
}
|
|
2379
|
-
}
|
|
2325
|
+
skipSpaces() {
|
|
2326
|
+
while (isSpace(this.at())) this.index++;
|
|
2380
2327
|
}
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
[
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
const projectId = await context.resolveProjectId();
|
|
2392
|
-
const project = await client.projects.disconnectGit(projectId);
|
|
2393
|
-
context.output.result({ projectId: project.id, autoDeploy: false }, [
|
|
2394
|
-
`\u2705 ${project.name} no longer deploys on push.`,
|
|
2395
|
-
" Deploy from here with: naro deploy"
|
|
2396
|
-
]);
|
|
2397
|
-
}
|
|
2398
|
-
async function offerAutoDeploy(context, projectId, overrides) {
|
|
2399
|
-
const deps = { ...defaults2, ...overrides };
|
|
2400
|
-
if (overrides.canPrompt === false) return;
|
|
2401
|
-
try {
|
|
2402
|
-
if (readProjectLink(context.cwd)?.autoDeployDeclined === true) return;
|
|
2403
|
-
const connection = overrides.gitConnection;
|
|
2404
|
-
if (connection.state === "connected" || connection.state === "none") return;
|
|
2405
|
-
const fullName = connection.fullName;
|
|
2406
|
-
if (!fullName) return;
|
|
2407
|
-
let wanted;
|
|
2408
|
-
if (connection.state === "unreachable") {
|
|
2409
|
-
const installed = connection.installedAccounts ?? [];
|
|
2410
|
-
context.output.log(
|
|
2411
|
-
`
|
|
2412
|
-
\u25B8 Automatic deploys need the Naro GitHub App on \`${connection.repoOwner ?? fullName}\`.`
|
|
2413
|
-
);
|
|
2414
|
-
if (overrides.teamSlug && installed.length > 0)
|
|
2415
|
-
context.output.log(
|
|
2416
|
-
` Team ${overrides.teamSlug} has it on: ${installed.join(", ")}.`
|
|
2417
|
-
);
|
|
2418
|
-
wanted = await context.prompter.confirm(
|
|
2419
|
-
"Install it for this team now? (opens your browser)"
|
|
2420
|
-
);
|
|
2421
|
-
} else {
|
|
2422
|
-
wanted = await context.prompter.confirm(
|
|
2423
|
-
"Should your changes deploy automatically from now on?"
|
|
2424
|
-
);
|
|
2425
|
-
}
|
|
2426
|
-
if (!wanted) {
|
|
2427
|
-
const link = readProjectLink(context.cwd);
|
|
2428
|
-
writeProjectLink(context.cwd, {
|
|
2429
|
-
...link ?? { projectId },
|
|
2430
|
-
autoDeployDeclined: true
|
|
2431
|
-
});
|
|
2432
|
-
context.output.log(
|
|
2433
|
-
"\u25B8 Not now. Later, if you change your mind: naro git connect"
|
|
2434
|
-
);
|
|
2435
|
-
return;
|
|
2436
|
-
}
|
|
2437
|
-
const client = await context.client();
|
|
2438
|
-
const result = await client.projects.connectGit(projectId, fullName);
|
|
2439
|
-
if (result.ok) {
|
|
2440
|
-
connected(context, result.project);
|
|
2441
|
-
return;
|
|
2328
|
+
/** `item ("," item)*`, up to a `)` or the end, inside `level` groups. */
|
|
2329
|
+
list(level) {
|
|
2330
|
+
const nodes = [];
|
|
2331
|
+
for (; ; ) {
|
|
2332
|
+
nodes.push(this.item(level));
|
|
2333
|
+
this.skipSpaces();
|
|
2334
|
+
const char = this.at();
|
|
2335
|
+
if (char === ",") this.index++;
|
|
2336
|
+
else if (char === ")" || char === "") return nodes;
|
|
2337
|
+
else refuse(`has unexpected text ${this.where()}`);
|
|
2442
2338
|
}
|
|
2443
|
-
if (!result.installUrl) return;
|
|
2444
|
-
context.output.log(`
|
|
2445
|
-
\u25B8 Naro needs access to ${fullName}.`);
|
|
2446
|
-
context.output.log(` Opening ${result.installUrl}`);
|
|
2447
|
-
deps.openBrowser(result.installUrl);
|
|
2448
|
-
context.output.log(" Waiting for the installation to finish...\n");
|
|
2449
|
-
await waitForInstall(context, projectId, fullName, deps);
|
|
2450
|
-
} catch (error) {
|
|
2451
|
-
context.output.log(
|
|
2452
|
-
`\u25B8 Could not set up automatic deploys (${error instanceof Error ? error.message : String(error)}).`
|
|
2453
|
-
);
|
|
2454
|
-
context.output.log(" Try later with: naro git connect");
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
// src/commands/link.ts
|
|
2459
|
-
var SWITCH_ACCOUNT = "Sign in as a different account\u2026";
|
|
2460
|
-
async function resolveTeam(context, teams, deps = {}) {
|
|
2461
|
-
const explicit = context.flags.team ?? process.env.NARO_TEAM;
|
|
2462
|
-
if (explicit) {
|
|
2463
|
-
const match = teams.find((team) => team.slug === explicit);
|
|
2464
|
-
if (!match)
|
|
2465
|
-
throw new NaroError(
|
|
2466
|
-
"NOT_FOUND",
|
|
2467
|
-
`You are not a member of team "${explicit}".`
|
|
2468
|
-
);
|
|
2469
|
-
return match;
|
|
2470
|
-
}
|
|
2471
|
-
const linked = context.projectLink();
|
|
2472
|
-
if (linked?.teamSlug) {
|
|
2473
|
-
const match = teams.find((team) => team.slug === linked.teamSlug);
|
|
2474
|
-
if (match) return match;
|
|
2475
2339
|
}
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
const
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
const
|
|
2494
|
-
|
|
2495
|
-
);
|
|
2496
|
-
if (ok) return only;
|
|
2497
|
-
return switchAndRetry(context, deps);
|
|
2340
|
+
item(level) {
|
|
2341
|
+
this.skipSpaces();
|
|
2342
|
+
const start = this.index;
|
|
2343
|
+
const char = this.at();
|
|
2344
|
+
if (char === "" || char === "," || char === ")")
|
|
2345
|
+
refuse(`has an empty item ${this.where()}`);
|
|
2346
|
+
const group = /^(not\.)?(and|or)\(/.exec(this.text.slice(start));
|
|
2347
|
+
if (!group) return this.leaf(level);
|
|
2348
|
+
const negated = group[1] !== void 0;
|
|
2349
|
+
const inner = level + (negated ? 2 : 1);
|
|
2350
|
+
if (inner > MAX_FILTER_DEPTH)
|
|
2351
|
+
refuse(`nests deeper than ${String(MAX_FILTER_DEPTH)} groups`);
|
|
2352
|
+
const open = start + group[0].length - 1;
|
|
2353
|
+
this.index = open + 1;
|
|
2354
|
+
const children = this.list(inner);
|
|
2355
|
+
if (this.at() !== ")") refuse(`has an unclosed "(" ${this.where(open)}`);
|
|
2356
|
+
this.index++;
|
|
2357
|
+
const node = group[2] === "or" ? { or: children } : { and: children };
|
|
2358
|
+
return negated ? { not: node } : node;
|
|
2498
2359
|
}
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
)
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
"CANCELED",
|
|
2517
|
-
"Canceled. Switch accounts with `naro logout && naro login`."
|
|
2518
|
-
);
|
|
2519
|
-
await deps.switchAccount();
|
|
2520
|
-
const teams = await deps.listTeams();
|
|
2521
|
-
return resolveTeam(context, teams, deps);
|
|
2522
|
-
}
|
|
2523
|
-
function saveLink(context, project, teamSlug) {
|
|
2524
|
-
writeProjectLink(context.cwd, {
|
|
2525
|
-
projectId: project.id,
|
|
2526
|
-
teamSlug,
|
|
2527
|
-
projectName: project.name
|
|
2528
|
-
});
|
|
2529
|
-
if (ensureGitignoreHasNaro(context.cwd))
|
|
2530
|
-
context.output.log("\u25B8 added .naro to .gitignore");
|
|
2531
|
-
}
|
|
2532
|
-
async function runLink(context) {
|
|
2533
|
-
const client = await context.client();
|
|
2534
|
-
const teams = await client.teams.list();
|
|
2535
|
-
const team = await resolveTeam(context, teams, { intent: "read" });
|
|
2536
|
-
const projects = await client.projects.list(team.slug);
|
|
2537
|
-
let project;
|
|
2538
|
-
if (context.flags.project) {
|
|
2539
|
-
project = projects.find(
|
|
2540
|
-
(candidate) => candidate.id === context.flags.project
|
|
2541
|
-
);
|
|
2542
|
-
if (!project) {
|
|
2543
|
-
project = await client.projects.get(context.flags.project);
|
|
2360
|
+
/** `column.[not.]operator.value`. */
|
|
2361
|
+
leaf(level) {
|
|
2362
|
+
const start = this.index;
|
|
2363
|
+
const column = this.at() === '"' ? this.quoted() : this.word().trim();
|
|
2364
|
+
if (column === "" || this.at() !== ".")
|
|
2365
|
+
refuse(`needs column.operator.value ${this.where(start)}`);
|
|
2366
|
+
this.index++;
|
|
2367
|
+
let nameAt = this.index;
|
|
2368
|
+
let name = this.word();
|
|
2369
|
+
let negated = false;
|
|
2370
|
+
if (name === "not" && this.at() === ".") {
|
|
2371
|
+
if (level + 1 > MAX_FILTER_DEPTH)
|
|
2372
|
+
refuse(`nests deeper than ${String(MAX_FILTER_DEPTH)} groups`);
|
|
2373
|
+
negated = true;
|
|
2374
|
+
this.index++;
|
|
2375
|
+
nameAt = this.index;
|
|
2376
|
+
name = this.word();
|
|
2544
2377
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
(
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
throw new NaroError(
|
|
2551
|
-
"PROJECT_NOT_FOUND",
|
|
2552
|
-
`No project named "${context.flags.name}" in team ${team.slug}. Run \`naro deploy\` to create it.`
|
|
2553
|
-
);
|
|
2554
|
-
} else {
|
|
2555
|
-
const guess = slugify(context.cwd.split("/").at(-1) ?? "app");
|
|
2556
|
-
const exact = projects.find(
|
|
2557
|
-
(candidate) => candidate.name === guess || candidate.slug === guess
|
|
2558
|
-
);
|
|
2559
|
-
if (exact && (context.flags.yes || context.flags.json)) {
|
|
2560
|
-
project = exact;
|
|
2561
|
-
} else if (projects.length === 0) {
|
|
2562
|
-
throw new NaroError(
|
|
2563
|
-
"PROJECT_NOT_FOUND",
|
|
2564
|
-
`Team ${team.slug} has no projects yet. Run \`naro deploy\` to create one.`
|
|
2565
|
-
);
|
|
2566
|
-
} else {
|
|
2567
|
-
const options = projects.map(
|
|
2568
|
-
(candidate) => `${candidate.name} (${candidate.subdomain})`
|
|
2569
|
-
);
|
|
2570
|
-
const index = await context.prompter.select(
|
|
2571
|
-
`Link ${context.cwd} to which project?`,
|
|
2572
|
-
options
|
|
2573
|
-
);
|
|
2574
|
-
project = projects[index];
|
|
2378
|
+
const operator = operatorOf(name);
|
|
2379
|
+
if (this.at() !== ".") {
|
|
2380
|
+
if (name !== "" && operator === void 0)
|
|
2381
|
+
refuse(`has an unknown operator "${name}" ${this.where(nameAt)}`);
|
|
2382
|
+
refuse(`needs column.operator.value ${this.where(start)}`);
|
|
2575
2383
|
}
|
|
2384
|
+
if (operator === void 0)
|
|
2385
|
+
refuse(`has an unknown operator "${name}" ${this.where(nameAt)}`);
|
|
2386
|
+
this.index++;
|
|
2387
|
+
const leaf = { operator, column, value: this.value(operator) };
|
|
2388
|
+
return negated ? { not: leaf } : leaf;
|
|
2576
2389
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
projectId: project.id,
|
|
2584
|
-
projectName: project.name,
|
|
2585
|
-
teamSlug: owningTeam.slug,
|
|
2586
|
-
subdomain: project.subdomain
|
|
2587
|
-
},
|
|
2588
|
-
[`\u2705 Linked to ${owningTeam.slug}/${project.name} (.naro/project.json)`]
|
|
2589
|
-
);
|
|
2590
|
-
}
|
|
2591
|
-
async function runUnlink(context) {
|
|
2592
|
-
const existing = readProjectLink(context.cwd);
|
|
2593
|
-
const removed = removeProjectLink(context.cwd);
|
|
2594
|
-
context.output.result(
|
|
2595
|
-
{ removed, ...existing ? { projectId: existing.projectId } : {} },
|
|
2596
|
-
[removed ? "\u2705 Unlinked (.naro removed)" : "Nothing to unlink here."]
|
|
2597
|
-
);
|
|
2598
|
-
}
|
|
2599
|
-
|
|
2600
|
-
// src/commands/deploy.ts
|
|
2601
|
-
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
2602
|
-
const { flags, output } = context;
|
|
2603
|
-
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
2604
|
-
if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
|
|
2605
|
-
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
2606
|
-
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
2390
|
+
/** Up to the next `.`, `,`, `(`, `)` or the end. */
|
|
2391
|
+
word() {
|
|
2392
|
+
const start = this.index;
|
|
2393
|
+
while (this.index < this.text.length && !".,()".includes(this.at()))
|
|
2394
|
+
this.index++;
|
|
2395
|
+
return this.text.slice(start, this.index);
|
|
2607
2396
|
}
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
`);
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2397
|
+
/** `"…"`, with `\` making the next character literal. */
|
|
2398
|
+
quoted() {
|
|
2399
|
+
const start = this.index;
|
|
2400
|
+
this.index++;
|
|
2401
|
+
let text = "";
|
|
2402
|
+
for (; ; ) {
|
|
2403
|
+
if (this.index >= this.text.length)
|
|
2404
|
+
refuse(`has an unclosed quote ${this.where(start)}`);
|
|
2405
|
+
const char = this.at();
|
|
2406
|
+
this.index++;
|
|
2407
|
+
if (char === '"') return text;
|
|
2408
|
+
if (char === "\\" && this.index < this.text.length) {
|
|
2409
|
+
text += this.at();
|
|
2410
|
+
this.index++;
|
|
2411
|
+
} else text += char;
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
/** A quoted value, or everything up to `stops`, trimmed. */
|
|
2415
|
+
scalar(stops) {
|
|
2416
|
+
this.skipSpaces();
|
|
2417
|
+
if (this.at() === '"') {
|
|
2418
|
+
const text = this.quoted();
|
|
2419
|
+
this.skipSpaces();
|
|
2420
|
+
if (this.at() !== "" && !stops.includes(this.at()))
|
|
2421
|
+
refuse(`has text after a closing quote ${this.where()}`);
|
|
2422
|
+
return text;
|
|
2423
|
+
}
|
|
2424
|
+
const start = this.index;
|
|
2425
|
+
while (this.index < this.text.length && !stops.includes(this.at()))
|
|
2426
|
+
this.index++;
|
|
2427
|
+
return this.text.slice(start, this.index).trim();
|
|
2428
|
+
}
|
|
2429
|
+
/**
|
|
2430
|
+
* `(a,b)` for `in`: text values, except that an unquoted `true` or `false`
|
|
2431
|
+
* is a boolean (사용자 리뷰 반영, as in `eq`). `{a,b}` for the array
|
|
2432
|
+
* operators: text, except that an unquoted exact decimal is a number
|
|
2433
|
+
* (적대적 리뷰 반영, narrowed by 사용자 리뷰 반영) — json_each's values
|
|
2434
|
+
* have no type affinity to turn the text "1" into 1, so as text `cs.{1}`
|
|
2435
|
+
* matched no numeric array and `not.ov.{1}` matched every row. Quoted,
|
|
2436
|
+
* `"1"` and `"true"` stay text.
|
|
2437
|
+
*/
|
|
2438
|
+
listValue(kind) {
|
|
2439
|
+
const [open, close, hint] = LIST_HINT[kind];
|
|
2440
|
+
this.skipSpaces();
|
|
2441
|
+
const openAt = this.index;
|
|
2442
|
+
if (this.at() !== open) refuse(`${hint} ${this.where()}`);
|
|
2443
|
+
this.index++;
|
|
2444
|
+
this.skipSpaces();
|
|
2445
|
+
const values = [];
|
|
2446
|
+
if (this.at() === close) {
|
|
2447
|
+
this.index++;
|
|
2448
|
+
return values;
|
|
2449
|
+
}
|
|
2450
|
+
for (; ; ) {
|
|
2451
|
+
this.skipSpaces();
|
|
2452
|
+
const itemAt = this.index;
|
|
2453
|
+
const token = this.token(`,${close}`);
|
|
2454
|
+
if (token) values.push(token.value);
|
|
2455
|
+
else {
|
|
2456
|
+
const { text: value, quoted } = this.bareOrQuoted(`,${close}`);
|
|
2457
|
+
if (!quoted && value === "") {
|
|
2458
|
+
if (this.index >= this.text.length)
|
|
2459
|
+
refuse(`has an unclosed "${open}" ${this.where(openAt)}`);
|
|
2460
|
+
refuse(`has an empty item ${this.where(itemAt)}`);
|
|
2461
|
+
}
|
|
2462
|
+
values.push(
|
|
2463
|
+
quoted ? value : kind === "array" ? exactNumber(value) : truth(value)
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
if (this.index >= this.text.length)
|
|
2467
|
+
refuse(`has an unclosed "${open}" ${this.where(openAt)}`);
|
|
2468
|
+
const char = this.at();
|
|
2469
|
+
this.index++;
|
|
2470
|
+
if (char === close) return values;
|
|
2471
|
+
}
|
|
2647
2472
|
}
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
}
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
if (match) return match;
|
|
2681
|
-
} else if (link) {
|
|
2682
|
-
let project;
|
|
2683
|
-
try {
|
|
2684
|
-
project = await client.projects.get(link.projectId);
|
|
2685
|
-
} catch (error) {
|
|
2686
|
-
if (isNaroError(error) && error.code !== "NOT_FOUND") throw error;
|
|
2687
|
-
throw new NaroError(
|
|
2688
|
-
"PROJECT_NOT_FOUND",
|
|
2689
|
-
`This directory is linked to a project ${await accountLabel(context)} cannot see.
|
|
2690
|
-
The CLI signs in separately from the dashboard \u2014 check with \`naro whoami\`.
|
|
2691
|
-
Wrong account? naro logout && naro login
|
|
2692
|
-
Project gone? remove .naro, then naro deploy`,
|
|
2693
|
-
{ cause: error }
|
|
2694
|
-
);
|
|
2473
|
+
/** The value after `operator.`. */
|
|
2474
|
+
value(operator) {
|
|
2475
|
+
const start = this.index;
|
|
2476
|
+
switch (operator) {
|
|
2477
|
+
case "in":
|
|
2478
|
+
return this.listValue("in");
|
|
2479
|
+
case "contains":
|
|
2480
|
+
case "containedBy":
|
|
2481
|
+
case "overlaps":
|
|
2482
|
+
return this.listValue("array");
|
|
2483
|
+
case "is":
|
|
2484
|
+
return keyword(this.scalar(",)"), start);
|
|
2485
|
+
case "like":
|
|
2486
|
+
case "ilike": {
|
|
2487
|
+
const token = this.token(",)");
|
|
2488
|
+
if (token)
|
|
2489
|
+
refuse(
|
|
2490
|
+
`cannot use ${token.word} with ${operator} ${this.where(token.at)}`
|
|
2491
|
+
);
|
|
2492
|
+
return likePattern(this.bareOrQuoted(",)").text);
|
|
2493
|
+
}
|
|
2494
|
+
case "eq":
|
|
2495
|
+
case "neq": {
|
|
2496
|
+
const token = this.token(",)");
|
|
2497
|
+
if (token) return token.value;
|
|
2498
|
+
const { text, quoted } = this.bareOrQuoted(",)");
|
|
2499
|
+
return quoted ? text : truth(text);
|
|
2500
|
+
}
|
|
2501
|
+
default: {
|
|
2502
|
+
const token = this.token(",)");
|
|
2503
|
+
return token ? token.value : this.bareOrQuoted(",)").text;
|
|
2504
|
+
}
|
|
2695
2505
|
}
|
|
2696
|
-
const match = teams.find((team) => team.id === project.teamId);
|
|
2697
|
-
if (match) return match;
|
|
2698
2506
|
}
|
|
2699
|
-
|
|
2507
|
+
};
|
|
2508
|
+
function likePattern(pattern) {
|
|
2509
|
+
return pattern.replaceAll("*", "%");
|
|
2700
2510
|
}
|
|
2701
|
-
function
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
clearSession();
|
|
2707
|
-
await runLogin(context);
|
|
2708
|
-
},
|
|
2709
|
-
listTeams: async () => (await context.client()).teams.list()
|
|
2710
|
-
};
|
|
2511
|
+
function truth(text) {
|
|
2512
|
+
const word = text.toLowerCase();
|
|
2513
|
+
if (word === "true") return true;
|
|
2514
|
+
if (word === "false") return false;
|
|
2515
|
+
return text;
|
|
2711
2516
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
return "this account";
|
|
2720
|
-
}
|
|
2517
|
+
var DECIMAL = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
2518
|
+
function exactNumber(text) {
|
|
2519
|
+
if (!DECIMAL.test(text)) return text;
|
|
2520
|
+
const number = Number(text);
|
|
2521
|
+
if (!text.includes(".")) return Number.isSafeInteger(number) ? number : text;
|
|
2522
|
+
const trimmed = text.replace(/\.?0+$/, "");
|
|
2523
|
+
return String(number) === trimmed ? number : text;
|
|
2721
2524
|
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
return sha ? sha.slice(0, 7) : "-";
|
|
2525
|
+
function quoteFilterValue(value) {
|
|
2526
|
+
const text = String(value);
|
|
2527
|
+
return `"${text.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
|
|
2726
2528
|
}
|
|
2727
|
-
function
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2529
|
+
function keyword(text, at) {
|
|
2530
|
+
const word = text.toLowerCase();
|
|
2531
|
+
if (word === "null") return null;
|
|
2532
|
+
if (word === "true") return true;
|
|
2533
|
+
if (word === "false") return false;
|
|
2534
|
+
return refuse(
|
|
2535
|
+
`needs null, true or false after is at character ${String(at + 1)}`
|
|
2536
|
+
);
|
|
2735
2537
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
[
|
|
2744
|
-
"ID".padEnd(38),
|
|
2745
|
-
"STATUS".padEnd(10),
|
|
2746
|
-
"BRANCH".padEnd(16),
|
|
2747
|
-
"COMMIT".padEnd(9),
|
|
2748
|
-
"URL"
|
|
2749
|
-
].join(" "),
|
|
2750
|
-
...deployments.map(formatDeploymentLine)
|
|
2751
|
-
]);
|
|
2538
|
+
function attempt(parse) {
|
|
2539
|
+
try {
|
|
2540
|
+
return { ok: true, node: parse() };
|
|
2541
|
+
} catch (error) {
|
|
2542
|
+
if (error instanceof Refusal) return { ok: false, reason: error.message };
|
|
2543
|
+
throw error;
|
|
2544
|
+
}
|
|
2752
2545
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
)
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
...lastLogs.length > 0 ? [
|
|
2772
|
-
"",
|
|
2773
|
-
"Recent build logs:",
|
|
2774
|
-
...lastLogs.map((log) => ` [${log.stage}] ${log.message}`)
|
|
2775
|
-
] : []
|
|
2776
|
-
]);
|
|
2546
|
+
function parseLogic(text, kind, options = {}) {
|
|
2547
|
+
if (typeof text !== "string")
|
|
2548
|
+
return { ok: false, reason: "must be a filter string or a function" };
|
|
2549
|
+
if (text.trim() === "") return { ok: false, reason: "must not be empty" };
|
|
2550
|
+
return attempt(() => {
|
|
2551
|
+
if (options.group === false) {
|
|
2552
|
+
const alone = items(text, options.tokens, 0);
|
|
2553
|
+
if (alone.length === 1 && alone[0]) return alone[0];
|
|
2554
|
+
}
|
|
2555
|
+
const nodes = items(text, options.tokens, 1);
|
|
2556
|
+
return kind === "or" ? { or: nodes } : { and: nodes };
|
|
2557
|
+
});
|
|
2558
|
+
}
|
|
2559
|
+
function items(text, tokens, level) {
|
|
2560
|
+
const parser = new Parser(text, tokens);
|
|
2561
|
+
const nodes = parser.list(level);
|
|
2562
|
+
if (parser.at() === ")") refuse(`has an unmatched ")" ${parser.where()}`);
|
|
2563
|
+
return nodes;
|
|
2777
2564
|
}
|
|
2778
2565
|
|
|
2779
|
-
// src/commands/
|
|
2780
|
-
var
|
|
2781
|
-
|
|
2782
|
-
|
|
2566
|
+
// src/commands/policy.ts
|
|
2567
|
+
var SET_USAGE = "Usage: naro policy set <table> [--select <rule>] [--insert <rule>] [--update <rule>] [--delete <rule>] [--owner <column>] [--select-when <filter>] [--insert-when <filter>] [--update-when <filter>] [--update-check <filter>] [--delete-when <filter>] [--signed-in] | --from-file <policy.json>";
|
|
2568
|
+
var REMOVE_USAGE = "Usage: naro policy remove <table>";
|
|
2569
|
+
var CREATE_USAGE = 'Usage: naro policy create "CREATE POLICY <name> ON <table> \u2026" | --file <policies.sql>';
|
|
2570
|
+
var DROP_USAGE = "Usage: naro policy drop <name> --table <table>";
|
|
2571
|
+
var TAKES_EFFECT = "It takes effect within about 10 seconds.";
|
|
2572
|
+
var DEFAULT_DENY = "Tables without a policy deny anon and signed-in requests. The service key skips policies.";
|
|
2573
|
+
var RULE_FLAGS = {
|
|
2574
|
+
select: "selectRule",
|
|
2575
|
+
insert: "insertRule",
|
|
2576
|
+
update: "updateRule",
|
|
2577
|
+
delete: "deleteRule"
|
|
2783
2578
|
};
|
|
2784
|
-
var
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2579
|
+
var WHEN_FLAGS = {
|
|
2580
|
+
select: { key: "selectWhen", tree: "using" },
|
|
2581
|
+
insert: { key: "insertWhen", tree: "check" },
|
|
2582
|
+
update: { key: "updateWhen", tree: "using" },
|
|
2583
|
+
delete: { key: "deleteWhen", tree: "using" }
|
|
2584
|
+
};
|
|
2585
|
+
var AUTH_UID = "auth.uid()";
|
|
2586
|
+
var OTHER_FLAGS = [
|
|
2587
|
+
"selectRule",
|
|
2588
|
+
"insertRule",
|
|
2589
|
+
"updateRule",
|
|
2590
|
+
"deleteRule",
|
|
2591
|
+
"ownerColumn",
|
|
2592
|
+
"selectWhen",
|
|
2593
|
+
"insertWhen",
|
|
2594
|
+
"updateWhen",
|
|
2595
|
+
"updateCheck",
|
|
2596
|
+
"deleteWhen",
|
|
2597
|
+
"signedIn"
|
|
2598
|
+
];
|
|
2599
|
+
function isRule(value) {
|
|
2600
|
+
return POLICY_RULES.includes(value);
|
|
2789
2601
|
}
|
|
2790
|
-
function
|
|
2791
|
-
|
|
2602
|
+
function refuse2(message) {
|
|
2603
|
+
throw new NaroError("VALIDATION_FAILED", message);
|
|
2792
2604
|
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
function axisMark(value) {
|
|
2796
|
-
if (AXIS_DONE.has(value)) return "\u2713";
|
|
2797
|
-
if (AXIS_FAILED.has(value)) return "\u2717";
|
|
2798
|
-
return "\xB7";
|
|
2605
|
+
function flagName(key) {
|
|
2606
|
+
return `--${key.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)}`;
|
|
2799
2607
|
}
|
|
2800
|
-
function
|
|
2801
|
-
const
|
|
2802
|
-
["
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
)
|
|
2608
|
+
function conditionTree(flag, text) {
|
|
2609
|
+
const parsed = parseLogic(text, "and", {
|
|
2610
|
+
tokens: { [AUTH_UID]: { auth: "uid" } },
|
|
2611
|
+
group: false
|
|
2612
|
+
});
|
|
2613
|
+
if (!parsed.ok) refuse2(`${flag} ${parsed.reason}.`);
|
|
2614
|
+
const tree = parsed.node;
|
|
2615
|
+
const checked = parsePolicyNode(tree, "");
|
|
2616
|
+
if (!checked.ok) {
|
|
2617
|
+
const path = checked.path.replace(/^\./, "");
|
|
2618
|
+
refuse2(
|
|
2619
|
+
path === "" ? `${flag} ${checked.reason}.` : `${flag}: ${path} ${checked.reason}.`
|
|
2620
|
+
);
|
|
2621
|
+
}
|
|
2622
|
+
return tree;
|
|
2809
2623
|
}
|
|
2810
|
-
function
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2624
|
+
function conditionsFrom(flags) {
|
|
2625
|
+
const conditions = {};
|
|
2626
|
+
for (const action of POLICY_ACTIONS) {
|
|
2627
|
+
const { key, tree } = WHEN_FLAGS[action];
|
|
2628
|
+
const text = flags[key];
|
|
2629
|
+
if (text === void 0) continue;
|
|
2630
|
+
const flag = flagName(key);
|
|
2631
|
+
if (flags[RULE_FLAGS[action]] !== void 0)
|
|
2632
|
+
refuse2(`--${action} and ${flag} cannot be used together.`);
|
|
2633
|
+
const condition = { [tree]: conditionTree(flag, text) };
|
|
2634
|
+
if (action === "update" && flags.updateCheck !== void 0)
|
|
2635
|
+
condition.check = conditionTree("--update-check", flags.updateCheck);
|
|
2636
|
+
if (flags.signedIn) condition.role = "authenticated";
|
|
2637
|
+
conditions[action] = condition;
|
|
2638
|
+
}
|
|
2639
|
+
if (flags.updateCheck !== void 0 && flags.updateWhen === void 0)
|
|
2640
|
+
refuse2("--update-check needs --update-when.");
|
|
2641
|
+
if (flags.signedIn && Object.keys(conditions).length === 0)
|
|
2642
|
+
refuse2(
|
|
2643
|
+
"--signed-in needs --select-when, --insert-when, --update-when or --delete-when."
|
|
2644
|
+
);
|
|
2645
|
+
return conditions;
|
|
2817
2646
|
}
|
|
2818
|
-
function
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2647
|
+
function patchFromFile(context, path, table) {
|
|
2648
|
+
let text;
|
|
2649
|
+
try {
|
|
2650
|
+
text = readFileSync3(resolve3(context.cwd, path), "utf8");
|
|
2651
|
+
} catch {
|
|
2652
|
+
refuse2(`Could not read ${path}.`);
|
|
2653
|
+
}
|
|
2654
|
+
let body;
|
|
2655
|
+
try {
|
|
2656
|
+
body = JSON.parse(text);
|
|
2657
|
+
} catch {
|
|
2658
|
+
refuse2(`${path} is not JSON.`);
|
|
2659
|
+
}
|
|
2660
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
2661
|
+
refuse2(`${path} must hold a JSON object.`);
|
|
2662
|
+
const { table: named, ...fields } = body;
|
|
2663
|
+
const patch = Object.fromEntries(
|
|
2664
|
+
Object.entries(fields).filter(([key]) => key !== "policies")
|
|
2665
|
+
);
|
|
2666
|
+
if (Object.hasOwn(body, "table")) {
|
|
2667
|
+
if (typeof named !== "string")
|
|
2668
|
+
refuse2(`${path}: table must be a string, the table's name.`);
|
|
2669
|
+
if (table !== void 0 && asciiLower(named) !== asciiLower(table))
|
|
2670
|
+
refuse2(
|
|
2671
|
+
`${path} is the policy of ${printable(named)}, not ${printable(table)}. Remove its "table" to set it on ${printable(table)}.`
|
|
2826
2672
|
);
|
|
2827
|
-
if (instruction.note) lines2.push(` note: ${instruction.note}`);
|
|
2828
|
-
}
|
|
2829
|
-
return lines2;
|
|
2830
2673
|
}
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
|
|
2836
|
-
` Value: ${record.value}`
|
|
2674
|
+
const allowed = /* @__PURE__ */ new Set([...POLICY_ACTIONS, "ownerColumn"]);
|
|
2675
|
+
if (Object.keys(patch).some((key) => !allowed.has(key)))
|
|
2676
|
+
refuse2(
|
|
2677
|
+
`${path}: Only table, select, insert, update, delete and ownerColumn can be set.`
|
|
2837
2678
|
);
|
|
2838
|
-
|
|
2839
|
-
return lines;
|
|
2679
|
+
return patch;
|
|
2840
2680
|
}
|
|
2841
|
-
function
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2681
|
+
function asciiLower(text) {
|
|
2682
|
+
return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
2683
|
+
}
|
|
2684
|
+
function policyPatch(flags, context, table) {
|
|
2685
|
+
if (flags.fromFile !== void 0) {
|
|
2686
|
+
if (OTHER_FLAGS.some(
|
|
2687
|
+
(key) => flags[key] !== void 0 && flags[key] !== false
|
|
2688
|
+
))
|
|
2689
|
+
refuse2(
|
|
2690
|
+
"--from-file takes the whole policy: leave out the other policy options."
|
|
2691
|
+
);
|
|
2692
|
+
if (!context) refuse2(SET_USAGE);
|
|
2693
|
+
return patchFromFile(context, flags.fromFile, table);
|
|
2849
2694
|
}
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2695
|
+
const patch = {};
|
|
2696
|
+
for (const action of POLICY_ACTIONS) {
|
|
2697
|
+
const value = flags[RULE_FLAGS[action]];
|
|
2698
|
+
if (value === void 0) continue;
|
|
2699
|
+
if (!isRule(value))
|
|
2700
|
+
refuse2(`--${action} must be one of ${POLICY_RULES.join(", ")}.`);
|
|
2701
|
+
patch[action] = value;
|
|
2853
2702
|
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
lines.push(
|
|
2859
|
-
guidance.apex ? ` Apex domain (${domain.hostname}):` : " Note:"
|
|
2860
|
-
);
|
|
2861
|
-
for (const note of guidance.notes) lines.push(` ${note}`);
|
|
2703
|
+
Object.assign(patch, conditionsFrom(flags));
|
|
2704
|
+
if (flags.ownerColumn !== void 0) {
|
|
2705
|
+
if (flags.ownerColumn.trim() === "") refuse2("--owner must name a column.");
|
|
2706
|
+
patch.ownerColumn = flags.ownerColumn;
|
|
2862
2707
|
}
|
|
2863
|
-
|
|
2708
|
+
if (Object.keys(patch).length === 0)
|
|
2709
|
+
refuse2(
|
|
2710
|
+
`Set at least one of --select, --insert, --update, --delete, --owner, --select-when, --insert-when, --update-when, --delete-when or --from-file. ${SET_USAGE}`
|
|
2711
|
+
);
|
|
2712
|
+
return patch;
|
|
2864
2713
|
}
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
|
|
2873
|
-
`Domains (${String(domains.length)}):`,
|
|
2874
|
-
...domains.flatMap(domainLines),
|
|
2875
|
-
// list reads our stored copy; only verify goes and looks again.
|
|
2876
|
-
...pending.length > 0 ? ["", "Re-check DNS now: naro domains verify"] : []
|
|
2877
|
-
]
|
|
2878
|
-
);
|
|
2714
|
+
var SHORT = {
|
|
2715
|
+
contains: "cs",
|
|
2716
|
+
containedBy: "cd",
|
|
2717
|
+
overlaps: "ov"
|
|
2718
|
+
};
|
|
2719
|
+
function isAuthUid(value) {
|
|
2720
|
+
return typeof value === "object" && value !== null && value.auth === "uid";
|
|
2879
2721
|
}
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
1,
|
|
2883
|
-
"Usage: naro domains add <hostname> [--no-www]"
|
|
2884
|
-
);
|
|
2885
|
-
const client = await context.client();
|
|
2886
|
-
const projectId = await context.resolveProjectId();
|
|
2887
|
-
const withWww = context.flags.noWww !== true;
|
|
2888
|
-
const domain = await client.domains.add(projectId, hostname, {
|
|
2889
|
-
withWww
|
|
2890
|
-
});
|
|
2891
|
-
const companion = domain.companion;
|
|
2892
|
-
const apex = domain.apex ?? domain.guidance?.apex ?? false;
|
|
2893
|
-
const alternative = domain.guidance?.alternative ?? `www.${domain.hostname}`;
|
|
2894
|
-
context.output.result(
|
|
2895
|
-
{ projectId, domain, withWww, ...companion ? { companion } : {} },
|
|
2896
|
-
[
|
|
2897
|
-
`\u2705 Domain added: ${domain.hostname}`,
|
|
2898
|
-
"",
|
|
2899
|
-
...domainLines(domain),
|
|
2900
|
-
...companion ? [
|
|
2901
|
-
"",
|
|
2902
|
-
` Also registered ${companion.hostname} \u2014 an apex domain often cannot hold`,
|
|
2903
|
-
" the CNAME above, and this form always can.",
|
|
2904
|
-
"",
|
|
2905
|
-
...domainLines(companion)
|
|
2906
|
-
] : [],
|
|
2907
|
-
...!companion && apex && !withWww ? [
|
|
2908
|
-
"",
|
|
2909
|
-
` --no-www: ${domain.hostname} was registered on its own. If your registrar`,
|
|
2910
|
-
` refuses the record above, run \`naro domains add ${alternative}\`.`
|
|
2911
|
-
] : [],
|
|
2912
|
-
"",
|
|
2913
|
-
isReady(domain) ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains verify`."
|
|
2914
|
-
]
|
|
2915
|
-
);
|
|
2722
|
+
function nearAuthUid(text) {
|
|
2723
|
+
return text.replace(/\s+/g, "").toLowerCase().replace(/\(\)$/, "") === "auth.uid";
|
|
2916
2724
|
}
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2725
|
+
var DECIMAL2 = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
2726
|
+
function readsAsNumber(value) {
|
|
2727
|
+
const text = String(value);
|
|
2728
|
+
if (!DECIMAL2.test(text) || !Object.is(Number(text), value)) return false;
|
|
2729
|
+
return text.includes(".") || Number.isSafeInteger(value);
|
|
2730
|
+
}
|
|
2731
|
+
function stringText(value, slot) {
|
|
2732
|
+
if (slot === "is") return void 0;
|
|
2733
|
+
if (slot === "like" && value.includes("*")) return void 0;
|
|
2734
|
+
const bare = value !== "" && value.trim() === value && !/[,()"{}\\]/.test(value) && !/^(?:true|false|null)$/i.test(value) && !nearAuthUid(value) && !(slot === "array" && DECIMAL2.test(value));
|
|
2735
|
+
return bare ? value : quoteFilterValue(value);
|
|
2736
|
+
}
|
|
2737
|
+
function valueText(value, slot) {
|
|
2738
|
+
if (isAuthUid(value))
|
|
2739
|
+
return slot === "like" || slot === "is" ? void 0 : AUTH_UID;
|
|
2740
|
+
if (value === null) return slot === "is" ? "null" : void 0;
|
|
2741
|
+
if (typeof value === "boolean")
|
|
2742
|
+
return slot === "is" || slot === "eq" || slot === "in" ? String(value) : void 0;
|
|
2743
|
+
if (typeof value === "number")
|
|
2744
|
+
return slot === "array" && readsAsNumber(value) ? String(value) : void 0;
|
|
2745
|
+
if (typeof value === "string") return stringText(value, slot);
|
|
2746
|
+
return void 0;
|
|
2747
|
+
}
|
|
2748
|
+
function columnText(name) {
|
|
2749
|
+
if (name === "") return void 0;
|
|
2750
|
+
return /[.,()"\s]/.test(name) ? quoteFilterValue(name) : name;
|
|
2751
|
+
}
|
|
2752
|
+
function slotOf(operator) {
|
|
2753
|
+
if (operator === "eq" || operator === "neq") return "eq";
|
|
2754
|
+
if (operator === "like" || operator === "ilike") return "like";
|
|
2755
|
+
if (operator === "is") return "is";
|
|
2756
|
+
return "ordered";
|
|
2927
2757
|
}
|
|
2928
|
-
|
|
2929
|
-
const
|
|
2930
|
-
|
|
2931
|
-
const
|
|
2932
|
-
const
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
"No custom domains. Add one with `naro domains add example.com`."
|
|
2941
|
-
]);
|
|
2942
|
-
return;
|
|
2758
|
+
function leafText(leaf, negated) {
|
|
2759
|
+
const column = columnText(leaf.column);
|
|
2760
|
+
if (column === void 0) return void 0;
|
|
2761
|
+
const prefix = `${column}.${negated ? "not." : ""}`;
|
|
2762
|
+
const short = SHORT[leaf.operator];
|
|
2763
|
+
if (leaf.operator === "in" || short !== void 0) {
|
|
2764
|
+
if (!Array.isArray(leaf.value)) return void 0;
|
|
2765
|
+
const slot = short === void 0 ? "in" : "array";
|
|
2766
|
+
const items2 = leaf.value.map((item) => valueText(item, slot));
|
|
2767
|
+
if (items2.some((item) => item === void 0)) return void 0;
|
|
2768
|
+
const list = items2.join(",");
|
|
2769
|
+
return slot === "in" ? `${prefix}in.(${list})` : `${prefix}${String(short)}.{${list}}`;
|
|
2943
2770
|
}
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
isTerminal(domain) ? `\u26A0 ${domain.hostname}: ${axisSummary(domain)} \u2014 waiting cannot fix this one` : `\u25B8 ${domain.hostname}: ${axisSummary(domain)}`
|
|
2963
|
-
);
|
|
2964
|
-
await deps.sleep(Math.min(delay, remaining));
|
|
2965
|
-
delay = Math.min(delay * 2, MAX_DELAY_MS);
|
|
2966
|
-
domains = await verifyAll(client, projectId, hostname, true);
|
|
2967
|
-
if (domains.every(isReady)) break;
|
|
2968
|
-
}
|
|
2771
|
+
if (Array.isArray(leaf.value)) return void 0;
|
|
2772
|
+
const text = valueText(leaf.value, slotOf(leaf.operator));
|
|
2773
|
+
return text === void 0 ? void 0 : `${prefix}${leaf.operator}.${text}`;
|
|
2774
|
+
}
|
|
2775
|
+
function groupText(kind, children) {
|
|
2776
|
+
const texts = children.map((child) => filterText(child, false));
|
|
2777
|
+
if (texts.length === 0 || texts.some((text) => text === void 0))
|
|
2778
|
+
return void 0;
|
|
2779
|
+
return `${kind}(${texts.join(",")})`;
|
|
2780
|
+
}
|
|
2781
|
+
function filterText(node, top = true) {
|
|
2782
|
+
if ("operator" in node) return leafText(node, false);
|
|
2783
|
+
if ("not" in node) {
|
|
2784
|
+
const inner = node.not;
|
|
2785
|
+
if ("operator" in inner) return leafText(inner, true);
|
|
2786
|
+
if ("or" in inner) return negatedText(groupText("or", inner.or));
|
|
2787
|
+
if ("and" in inner) return negatedText(groupText("and", inner.and));
|
|
2788
|
+
return void 0;
|
|
2969
2789
|
}
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
const notReady = domains.filter((domain) => !isReady(domain));
|
|
2975
|
-
const names = notReady.map((domain) => domain.hostname).join(", ");
|
|
2976
|
-
const details = { projectId, domains, ready: false };
|
|
2977
|
-
if (timedOut) {
|
|
2978
|
-
const cause2 = notReady[0]?.cause;
|
|
2979
|
-
throw new NaroError(
|
|
2980
|
-
"TIMEOUT",
|
|
2981
|
-
`${names} did not go live within 10 minutes.${cause2 ? ` ${cause2.message}` : ""} DNS changes can take longer to propagate \u2014 re-run \`naro domains verify\` later.`,
|
|
2982
|
-
{ details }
|
|
2983
|
-
);
|
|
2984
|
-
}
|
|
2985
|
-
if (abandoned) {
|
|
2986
|
-
const blocked = domains.find(isTerminal);
|
|
2987
|
-
throw new NaroError(
|
|
2988
|
-
blocked?.cause?.code ?? "DOMAIN_VERIFICATION_FAILED",
|
|
2989
|
-
blocked?.cause?.message ?? `${names} cannot be verified (status: ${blocked?.status ?? "unknown"}). Remove it and add it again.`,
|
|
2990
|
-
{ details }
|
|
2991
|
-
);
|
|
2992
|
-
}
|
|
2993
|
-
const cause = notReady[0]?.cause;
|
|
2994
|
-
throw new NaroError(
|
|
2995
|
-
cause?.code ?? "DOMAIN_NOT_READY",
|
|
2996
|
-
`${names} is not live yet.${cause ? ` ${cause.message}` : ""} Add the records above, then run \`naro domains verify --wait\`.`,
|
|
2997
|
-
{ details }
|
|
2998
|
-
);
|
|
2790
|
+
if ("or" in node) return groupText("or", node.or);
|
|
2791
|
+
if (top && node.and.length > 1) {
|
|
2792
|
+
const texts = node.and.map((child) => filterText(child, false));
|
|
2793
|
+
return texts.some((text) => text === void 0) ? void 0 : texts.join(",");
|
|
2999
2794
|
}
|
|
3000
|
-
|
|
3001
|
-
...body,
|
|
3002
|
-
"",
|
|
3003
|
-
domains.length === 1 ? `\u2705 ${domains[0]?.hostname ?? ""} is live.` : "\u2705 Every domain is live."
|
|
3004
|
-
]);
|
|
2795
|
+
return groupText("and", node.and);
|
|
3005
2796
|
}
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
2797
|
+
function negatedText(text) {
|
|
2798
|
+
return text === void 0 ? void 0 : `not.${text}`;
|
|
2799
|
+
}
|
|
2800
|
+
function isInvalid(value) {
|
|
2801
|
+
return typeof value === "object" && "invalid" in value;
|
|
2802
|
+
}
|
|
2803
|
+
function cellOf(value) {
|
|
2804
|
+
if (typeof value === "string") return value;
|
|
2805
|
+
return isInvalid(value) ? "invalid" : "condition";
|
|
2806
|
+
}
|
|
2807
|
+
function conditionText(action, value) {
|
|
2808
|
+
if (typeof value === "string") return value;
|
|
2809
|
+
if (isInvalid(value))
|
|
2810
|
+
return `cannot be read (${value.invalid}); it denies everyone but the service key`;
|
|
2811
|
+
const using = value.using ? filterText(value.using) : "";
|
|
2812
|
+
const check = value.check ? filterText(value.check) : "";
|
|
2813
|
+
if (using === void 0 || check === void 0)
|
|
2814
|
+
return `as JSON ${JSON.stringify(value)}`;
|
|
2815
|
+
const parts = [];
|
|
2816
|
+
if (using !== "") parts.push(`using ${using}`);
|
|
2817
|
+
if (check !== "") parts.push(`check ${check}`);
|
|
2818
|
+
else if (action === "update") parts.push("check = using");
|
|
2819
|
+
const text = parts.join("; ");
|
|
2820
|
+
return value.role === "authenticated" ? `${text} (signed-in users only)` : text;
|
|
2821
|
+
}
|
|
2822
|
+
function policyTable(policies) {
|
|
2823
|
+
const header = ["Table", ...POLICY_ACTIONS, "Owner column"];
|
|
2824
|
+
const rows = policies.map((policy) => [
|
|
2825
|
+
printable(policy.table),
|
|
2826
|
+
...POLICY_ACTIONS.map((action) => cellOf(policy[action])),
|
|
2827
|
+
policy.ownerColumn === null ? "-" : printable(policy.ownerColumn)
|
|
2828
|
+
]);
|
|
2829
|
+
const widths = header.map(
|
|
2830
|
+
(title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
|
|
3010
2831
|
);
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
2832
|
+
const table = [header, ...rows].map(
|
|
2833
|
+
(cells) => cells.map(
|
|
2834
|
+
(cell, index) => index === cells.length - 1 ? cell : cell.padEnd(widths[index] ?? 0)
|
|
2835
|
+
).join(" ").trimEnd()
|
|
3014
2836
|
);
|
|
3015
|
-
const
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
2837
|
+
const lines = policies.flatMap(
|
|
2838
|
+
(policy) => POLICY_ACTIONS.filter((action) => typeof policy[action] !== "string").map(
|
|
2839
|
+
(action) => {
|
|
2840
|
+
const value = policy[action];
|
|
2841
|
+
const text = conditionText(action, value);
|
|
2842
|
+
return {
|
|
2843
|
+
label: `${printable(policy.table)}.${action}`,
|
|
2844
|
+
text: isInvalid(value) || isPrintable(text) ? printable(text) : `${UNSHOWN}: naro policy list --json has the exact text`
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
)
|
|
2848
|
+
);
|
|
2849
|
+
if (lines.length === 0) return table;
|
|
2850
|
+
const width = Math.max(...lines.map((line) => line.label.length));
|
|
2851
|
+
return [
|
|
2852
|
+
...table,
|
|
2853
|
+
"",
|
|
2854
|
+
...lines.map((line) => `${line.label.padEnd(width)} ${line.text}`)
|
|
2855
|
+
];
|
|
3021
2856
|
}
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3028
|
-
function parseTarget(value) {
|
|
3029
|
-
if (value === void 0) return "all";
|
|
3030
|
-
if (!TARGETS.has(value))
|
|
3031
|
-
throw new NaroError(
|
|
3032
|
-
"VALIDATION_FAILED",
|
|
3033
|
-
"Environment must be production, preview or all."
|
|
2857
|
+
var UNSHOWN = "has characters that cannot be shown here";
|
|
2858
|
+
function statementLine(policy) {
|
|
2859
|
+
if (policy.invalid !== null)
|
|
2860
|
+
return printable(
|
|
2861
|
+
`-- ${policyNamed(policy.name, policy.table)} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped or created again.`
|
|
3034
2862
|
);
|
|
3035
|
-
return
|
|
2863
|
+
return shownStatement(policy, policy.table, "naro policy list --json");
|
|
3036
2864
|
}
|
|
3037
|
-
function
|
|
3038
|
-
|
|
3039
|
-
return "\u2022\u2022\u2022\u2022\u2022\u2022 (secret)";
|
|
2865
|
+
function policyNamed(name, table) {
|
|
2866
|
+
return `Policy ${commentText(quotePolicyName(name))} on ${commentText(quoteIdent(table))}`;
|
|
3040
2867
|
}
|
|
3041
|
-
|
|
2868
|
+
function shownStatement(statement, table, exact) {
|
|
2869
|
+
if (isPrintable(statement.sql)) return statement.sql;
|
|
2870
|
+
return printable(
|
|
2871
|
+
`-- ${policyNamed(statement.name, table)} ${UNSHOWN}, so it is not printed as CREATE POLICY: ${exact} has its exact text.`
|
|
2872
|
+
);
|
|
2873
|
+
}
|
|
2874
|
+
function policyStatements(policies) {
|
|
2875
|
+
const lines = [];
|
|
2876
|
+
let table;
|
|
2877
|
+
for (const policy of policies) {
|
|
2878
|
+
if (table !== void 0 && policy.table !== table) lines.push("");
|
|
2879
|
+
table = policy.table;
|
|
2880
|
+
lines.push(statementLine(policy));
|
|
2881
|
+
}
|
|
2882
|
+
return lines;
|
|
2883
|
+
}
|
|
2884
|
+
async function runPolicyList(context) {
|
|
2885
|
+
const table = context.flags.table;
|
|
3042
2886
|
const client = await context.client();
|
|
3043
2887
|
const projectId = await context.resolveProjectId();
|
|
3044
|
-
const
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
{
|
|
3048
|
-
projectId,
|
|
3049
|
-
envVars: rows.map((row) => ({
|
|
3050
|
-
id: row.id,
|
|
3051
|
-
key: row.key,
|
|
3052
|
-
target: row.target,
|
|
3053
|
-
isSecret: row.isSecret,
|
|
3054
|
-
value: row.isSecret && !reveal ? null : row.value
|
|
3055
|
-
}))
|
|
3056
|
-
},
|
|
3057
|
-
[
|
|
3058
|
-
`Environment variables (${String(rows.length)}):`,
|
|
3059
|
-
...rows.map(
|
|
3060
|
-
(row) => ` ${row.key.padEnd(32)} ${row.target.padEnd(12)} ${reveal ? row.value ?? "" : maskedValue(row)}`
|
|
3061
|
-
)
|
|
3062
|
-
]
|
|
2888
|
+
const policies = await client.backend.rlsPolicies(
|
|
2889
|
+
projectId,
|
|
2890
|
+
table === void 0 ? {} : { table }
|
|
3063
2891
|
);
|
|
2892
|
+
let lines;
|
|
2893
|
+
if (policies.length > 0)
|
|
2894
|
+
lines = [...policyStatements(policies), "", DEFAULT_DENY];
|
|
2895
|
+
else if (table !== void 0)
|
|
2896
|
+
lines = [
|
|
2897
|
+
`No policies on ${printable(table)}: anon and signed-in requests to it are denied.`
|
|
2898
|
+
];
|
|
2899
|
+
else
|
|
2900
|
+
lines = [
|
|
2901
|
+
"No policies yet: every table denies anon and signed-in requests.",
|
|
2902
|
+
'Add one with `naro policy create "CREATE POLICY \u2026"` or `naro policy set <table> --select public`.'
|
|
2903
|
+
];
|
|
2904
|
+
context.output.result({ projectId, policies }, lines);
|
|
2905
|
+
}
|
|
2906
|
+
function statementText(context) {
|
|
2907
|
+
const [positional] = context.positionals;
|
|
2908
|
+
const file = context.flags.file;
|
|
2909
|
+
if (positional !== void 0 && file !== void 0)
|
|
2910
|
+
refuse2(`Give the statement or --file, not both. ${CREATE_USAGE}`);
|
|
2911
|
+
let text = positional;
|
|
2912
|
+
if (file !== void 0) {
|
|
2913
|
+
try {
|
|
2914
|
+
text = readFileSync3(resolve3(context.cwd, file), "utf8");
|
|
2915
|
+
} catch {
|
|
2916
|
+
refuse2(`Could not read ${file}.`);
|
|
2917
|
+
}
|
|
2918
|
+
}
|
|
2919
|
+
if (text === void 0 || text.trim() === "") refuse2(CREATE_USAGE);
|
|
2920
|
+
return text;
|
|
3064
2921
|
}
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
2922
|
+
function policyStatementsOf(text) {
|
|
2923
|
+
const split = splitPolicyStatements(text);
|
|
2924
|
+
if (!split.ok)
|
|
2925
|
+
refuse2(`${split.reason} at character ${String(split.position)}.`);
|
|
2926
|
+
const { statements } = split;
|
|
2927
|
+
if (statements.length === 0) refuse2(CREATE_USAGE);
|
|
2928
|
+
statements.forEach((statement, index) => {
|
|
2929
|
+
const parsed = parsePolicyStatement(statement);
|
|
2930
|
+
if (parsed.ok) return;
|
|
2931
|
+
const where = `${parsed.reason} at character ${String(parsed.position)}.`;
|
|
2932
|
+
refuse2(
|
|
2933
|
+
statements.length === 1 ? where : `Statement ${String(index + 1)} of ${String(statements.length)}: ${where} Nothing was sent.`
|
|
3075
2934
|
);
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
2935
|
+
});
|
|
2936
|
+
return statements;
|
|
2937
|
+
}
|
|
2938
|
+
function resultLine(result) {
|
|
2939
|
+
if ("created" in result)
|
|
2940
|
+
return `\u2705 Created policy ${printable(result.created.name)} on ${printable(result.created.table)}.`;
|
|
2941
|
+
if (result.dropped === null)
|
|
2942
|
+
return "Nothing to drop: no such policy (IF EXISTS).";
|
|
2943
|
+
return `\u2705 Dropped policy ${printable(result.dropped.name)} on ${printable(result.dropped.table)}.`;
|
|
2944
|
+
}
|
|
2945
|
+
async function runStatements(client, projectId, statements, allowUnindexed) {
|
|
2946
|
+
const applied = [];
|
|
2947
|
+
for (const [index, statement] of statements.entries()) {
|
|
2948
|
+
try {
|
|
2949
|
+
applied.push(
|
|
2950
|
+
await client.backend.runPolicySql(
|
|
2951
|
+
projectId,
|
|
2952
|
+
statement,
|
|
2953
|
+
allowUnindexed ? { allowUnindexed } : {}
|
|
2954
|
+
)
|
|
2955
|
+
);
|
|
2956
|
+
} catch (error) {
|
|
2957
|
+
if (!isNaroError(error) || statements.length === 1) throw error;
|
|
2958
|
+
let before = "Nothing ran.";
|
|
2959
|
+
if (index === 1)
|
|
2960
|
+
before = "Statement 1 ran and stays; the rest were not sent.";
|
|
2961
|
+
else if (index > 1)
|
|
2962
|
+
before = `Statements 1\u2013${String(index)} ran and stay; the rest were not sent.`;
|
|
2963
|
+
throw new NaroError(
|
|
2964
|
+
error.code,
|
|
2965
|
+
`Statement ${String(index + 1)} of ${String(statements.length)}: ${error.message} ${before}`,
|
|
2966
|
+
{
|
|
2967
|
+
...error.status === void 0 ? {} : { status: error.status },
|
|
2968
|
+
details: { ...error.details, applied },
|
|
2969
|
+
cause: error
|
|
2970
|
+
}
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
3082
2973
|
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
2974
|
+
return applied;
|
|
2975
|
+
}
|
|
2976
|
+
async function runPolicyCreate(context) {
|
|
2977
|
+
const statements = policyStatementsOf(statementText(context));
|
|
3086
2978
|
const client = await context.client();
|
|
3087
2979
|
const projectId = await context.resolveProjectId();
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
2980
|
+
const applied = await runStatements(
|
|
2981
|
+
client,
|
|
2982
|
+
projectId,
|
|
2983
|
+
statements,
|
|
2984
|
+
context.flags.allowUnindexed === true
|
|
2985
|
+
);
|
|
2986
|
+
context.output.result({ projectId, applied }, [
|
|
2987
|
+
...applied.map(resultLine),
|
|
2988
|
+
applied.length === 1 ? TAKES_EFFECT : "They take effect within about 10 seconds."
|
|
3091
2989
|
]);
|
|
3092
2990
|
}
|
|
3093
|
-
async function
|
|
3094
|
-
context.positionalOrThrow(
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
);
|
|
3098
|
-
const [key, targetArg] = context.positionals;
|
|
3099
|
-
const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
|
|
2991
|
+
async function runPolicyDrop(context) {
|
|
2992
|
+
const [name] = context.positionalOrThrow(1, DROP_USAGE);
|
|
2993
|
+
const table = context.flags.table;
|
|
2994
|
+
if (table === void 0 || table === "") refuse2(DROP_USAGE);
|
|
3100
2995
|
await confirmOrAbort(
|
|
3101
2996
|
context,
|
|
3102
|
-
`
|
|
2997
|
+
`Drop policy ${printable(name)} on ${printable(table)}? It stops applying within about 10 seconds.`
|
|
3103
2998
|
);
|
|
3104
2999
|
const client = await context.client();
|
|
3105
3000
|
const projectId = await context.resolveProjectId();
|
|
3106
|
-
const
|
|
3107
|
-
|
|
3108
|
-
|
|
3001
|
+
const dropped = await client.backend.dropRlsPolicy(
|
|
3002
|
+
projectId,
|
|
3003
|
+
table,
|
|
3004
|
+
name
|
|
3005
|
+
);
|
|
3006
|
+
context.output.result({ projectId, dropped }, [
|
|
3007
|
+
`\u2705 Dropped policy ${printable(dropped.name)} on ${printable(dropped.table)}. It stops applying within about 10 seconds.`
|
|
3109
3008
|
]);
|
|
3110
3009
|
}
|
|
3111
|
-
async function
|
|
3112
|
-
const
|
|
3113
|
-
const
|
|
3114
|
-
if (context.flags.env && !["production", "preview"].includes(context.flags.env))
|
|
3115
|
-
throw new NaroError(
|
|
3116
|
-
"VALIDATION_FAILED",
|
|
3117
|
-
"--env must be production or preview."
|
|
3118
|
-
);
|
|
3010
|
+
async function runPolicySet(context) {
|
|
3011
|
+
const [table] = context.positionalOrThrow(1, SET_USAGE);
|
|
3012
|
+
const patch = policyPatch(context.flags, context, table);
|
|
3119
3013
|
const client = await context.client();
|
|
3120
3014
|
const projectId = await context.resolveProjectId();
|
|
3121
|
-
const
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
const vars = Object.fromEntries(
|
|
3126
|
-
selected.map((row) => [row.key, row.value ?? ""])
|
|
3015
|
+
const policy = await client.backend.setPolicy(
|
|
3016
|
+
projectId,
|
|
3017
|
+
table,
|
|
3018
|
+
patch
|
|
3127
3019
|
);
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3020
|
+
let named;
|
|
3021
|
+
try {
|
|
3022
|
+
named = await client.backend.rlsPolicies(projectId, {
|
|
3023
|
+
table: policy.table
|
|
3024
|
+
});
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
if (!isNaroError(error)) throw error;
|
|
3132
3027
|
}
|
|
3133
|
-
writeFileSync4(
|
|
3134
|
-
path,
|
|
3135
|
-
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
3136
|
-
${serializeDotenv(vars)}`,
|
|
3137
|
-
{ mode: 384 }
|
|
3138
|
-
);
|
|
3139
3028
|
context.output.result(
|
|
3140
|
-
{ projectId,
|
|
3141
|
-
[
|
|
3029
|
+
{ projectId, policy, ...named === void 0 ? {} : { policies: named } },
|
|
3030
|
+
[
|
|
3031
|
+
`\u2705 Saved the policy for ${printable(policy.table)}. ${TAKES_EFFECT}`,
|
|
3032
|
+
...policyTable([policy]),
|
|
3033
|
+
...named === void 0 || named.length === 0 ? [] : ["", ...policyStatements(named)]
|
|
3034
|
+
]
|
|
3142
3035
|
);
|
|
3143
3036
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
const { lang } = context.flags;
|
|
3151
|
-
if (lang !== void 0 && lang !== "typescript")
|
|
3152
|
-
throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
|
|
3037
|
+
async function runPolicyRemove(context) {
|
|
3038
|
+
const [table] = context.positionalOrThrow(1, REMOVE_USAGE);
|
|
3039
|
+
await confirmOrAbort(
|
|
3040
|
+
context,
|
|
3041
|
+
`Remove every policy on ${printable(table)}? Anon and signed-in requests to it are denied from then on.`
|
|
3042
|
+
);
|
|
3153
3043
|
const client = await context.client();
|
|
3154
3044
|
const projectId = await context.resolveProjectId();
|
|
3155
|
-
const
|
|
3156
|
-
const
|
|
3157
|
-
|
|
3158
|
-
|
|
3159
|
-
read.policies === void 0 ? {} : { policies: read.policies }
|
|
3160
|
-
);
|
|
3161
|
-
const undescribed = tables.flatMap(
|
|
3162
|
-
(table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
|
|
3163
|
-
);
|
|
3164
|
-
for (const { name, error } of undescribed)
|
|
3165
|
-
context.output.warn(
|
|
3166
|
-
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
3167
|
-
);
|
|
3168
|
-
if (read.policies === void 0)
|
|
3169
|
-
context.output.warn(
|
|
3170
|
-
`\u26A0 The backend's policies could not be read (${printable(read.reason)}), so owner columns stay required in Insert.`
|
|
3171
|
-
);
|
|
3172
|
-
const extra = {
|
|
3173
|
-
...undescribed.length > 0 ? { undescribed } : {},
|
|
3174
|
-
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
3175
|
-
};
|
|
3176
|
-
if (context.flags.output) {
|
|
3177
|
-
const path = resolve3(context.cwd, context.flags.output);
|
|
3178
|
-
mkdirSync4(dirname2(path), { recursive: true });
|
|
3179
|
-
writeFileSync5(path, types);
|
|
3180
|
-
context.output.result(
|
|
3181
|
-
{
|
|
3182
|
-
projectId,
|
|
3183
|
-
output: context.flags.output,
|
|
3184
|
-
tables: tables.length,
|
|
3185
|
-
...extra
|
|
3186
|
-
},
|
|
3187
|
-
[
|
|
3188
|
-
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
3189
|
-
]
|
|
3190
|
-
);
|
|
3191
|
-
return;
|
|
3192
|
-
}
|
|
3193
|
-
context.output.result({ projectId, types, ...extra }, [
|
|
3194
|
-
types.replace(/\n$/, "")
|
|
3045
|
+
const policy = await client.backend.removePolicy(projectId, table);
|
|
3046
|
+
const names = policy.policies ?? [];
|
|
3047
|
+
context.output.result({ projectId, removed: true, policy }, [
|
|
3048
|
+
names.length === 0 ? `\u2705 Removed the policy for ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.` : `\u2705 Removed ${names.length === 1 ? "policy" : "policies"} ${names.map(printable).join(", ")} on ${printable(policy.table)}. Anon and signed-in requests to it are denied within about 10 seconds.`
|
|
3195
3049
|
]);
|
|
3196
3050
|
}
|
|
3197
3051
|
|
|
3198
|
-
// src/commands/
|
|
3199
|
-
var
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3052
|
+
// src/commands/db-schema.ts
|
|
3053
|
+
var USAGE2 = {
|
|
3054
|
+
table: "Usage: naro db table create|add-column|rename-column|rename|drop-column|drop \u2026 (see naro help)",
|
|
3055
|
+
index: "Usage: naro db index create|drop \u2026 (see naro help)",
|
|
3056
|
+
create: "Usage: naro db table create <name> --column <spec> [--column <spec>\u2026] [--owner <column>] [--if-not-exists]",
|
|
3057
|
+
addColumn: "Usage: naro db table add-column <table> --column <spec>",
|
|
3058
|
+
renameColumn: "Usage: naro db table rename-column <table> <column> <new-name>",
|
|
3059
|
+
rename: "Usage: naro db table rename <table> <new-name>",
|
|
3060
|
+
dropColumn: "Usage: naro db table drop-column <table> <column>",
|
|
3061
|
+
drop: "Usage: naro db table drop <table>",
|
|
3062
|
+
indexCreate: "Usage: naro db index create <table> <column[,column\u2026]> [--unique] [--name <name>]",
|
|
3063
|
+
indexDrop: "Usage: naro db index drop <name>"
|
|
3064
|
+
};
|
|
3065
|
+
var SPEC_USAGE = '--column needs name:type, e.g. --column "title:text:notnull".';
|
|
3066
|
+
var ON_DELETE = {
|
|
3067
|
+
cascade: "CASCADE",
|
|
3068
|
+
"set-null": "SET NULL",
|
|
3069
|
+
"set-default": "SET DEFAULT",
|
|
3070
|
+
restrict: "RESTRICT",
|
|
3071
|
+
"no-action": "NO ACTION"
|
|
3072
|
+
};
|
|
3073
|
+
var KEYWORDS = /* @__PURE__ */ new Set(["CURRENT_TIMESTAMP", "CURRENT_DATE", "CURRENT_TIME"]);
|
|
3074
|
+
var NUMBER = /^-?(?:\d+\.?\d*|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
3075
|
+
function segments(spec) {
|
|
3076
|
+
const parts = [];
|
|
3077
|
+
let current = "";
|
|
3078
|
+
let quoted = false;
|
|
3079
|
+
for (const char of spec) {
|
|
3080
|
+
if (char === "'") quoted = !quoted;
|
|
3081
|
+
if (char === ":" && !quoted) {
|
|
3082
|
+
parts.push(current);
|
|
3083
|
+
current = "";
|
|
3084
|
+
continue;
|
|
3085
|
+
}
|
|
3086
|
+
current += char;
|
|
3087
|
+
}
|
|
3088
|
+
if (quoted) return null;
|
|
3089
|
+
parts.push(current);
|
|
3090
|
+
return parts;
|
|
3205
3091
|
}
|
|
3206
|
-
function
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3092
|
+
function defaultValue(text) {
|
|
3093
|
+
if (text.length >= 2 && text.startsWith("'") && text.endsWith("'"))
|
|
3094
|
+
return text.slice(1, -1).replace(/''/g, "'");
|
|
3095
|
+
const lower = text.toLowerCase();
|
|
3096
|
+
if (lower === "null") return null;
|
|
3097
|
+
if (lower === "true") return true;
|
|
3098
|
+
if (lower === "false") return false;
|
|
3099
|
+
const upper = text.toUpperCase();
|
|
3100
|
+
if (KEYWORDS.has(upper)) return { keyword: upper };
|
|
3101
|
+
if (NUMBER.test(text)) return Number(text);
|
|
3102
|
+
return text;
|
|
3210
3103
|
}
|
|
3211
|
-
|
|
3212
|
-
const
|
|
3213
|
-
const
|
|
3214
|
-
|
|
3215
|
-
);
|
|
3216
|
-
const fallback = deployments[0];
|
|
3217
|
-
const chosen = latest ?? fallback;
|
|
3218
|
-
if (!chosen)
|
|
3104
|
+
function parseColumnSpec(spec) {
|
|
3105
|
+
const shown = `--column "${printable(spec)}"`;
|
|
3106
|
+
const parts = segments(spec);
|
|
3107
|
+
if (parts === null)
|
|
3219
3108
|
throw new NaroError(
|
|
3220
|
-
"
|
|
3221
|
-
|
|
3222
|
-
);
|
|
3223
|
-
return chosen.id;
|
|
3224
|
-
}
|
|
3225
|
-
async function buildLogs(context, client, deploymentId) {
|
|
3226
|
-
if (!context.flags.follow) {
|
|
3227
|
-
const detail = await client.deployments.get(deploymentId);
|
|
3228
|
-
context.output.result(
|
|
3229
|
-
{ deploymentId, type: "build", logs: detail.logs },
|
|
3230
|
-
detail.logs.map(formatBuildLog)
|
|
3109
|
+
"VALIDATION_FAILED",
|
|
3110
|
+
`${shown}: a quoted default is not closed.`
|
|
3231
3111
|
);
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
if (
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3112
|
+
const [name, type, ...modifiers] = parts;
|
|
3113
|
+
if (!name || !type) throw new NaroError("VALIDATION_FAILED", SPEC_USAGE);
|
|
3114
|
+
const column = {
|
|
3115
|
+
name,
|
|
3116
|
+
type
|
|
3117
|
+
};
|
|
3118
|
+
let onDelete;
|
|
3119
|
+
for (const modifier of modifiers) {
|
|
3120
|
+
const eq = modifier.indexOf("=");
|
|
3121
|
+
const key = (eq < 0 ? modifier : modifier.slice(0, eq)).toLowerCase();
|
|
3122
|
+
const value = eq < 0 ? void 0 : modifier.slice(eq + 1);
|
|
3123
|
+
if (value === void 0 && key === "pk") column.primaryKey = true;
|
|
3124
|
+
else if (value === void 0 && key === "autoincrement")
|
|
3125
|
+
column.autoIncrement = true;
|
|
3126
|
+
else if (value === void 0 && key === "notnull") column.notNull = true;
|
|
3127
|
+
else if (value === void 0 && key === "unique") column.unique = true;
|
|
3128
|
+
else if (value !== void 0 && key === "default")
|
|
3129
|
+
column.default = defaultValue(value);
|
|
3130
|
+
else if (value !== void 0 && key === "references") {
|
|
3131
|
+
const dot = value.lastIndexOf(".");
|
|
3132
|
+
const table = dot < 0 ? "" : value.slice(0, dot);
|
|
3133
|
+
const target = dot < 0 ? "" : value.slice(dot + 1);
|
|
3134
|
+
if (!table || !target)
|
|
3135
|
+
throw new NaroError(
|
|
3136
|
+
"VALIDATION_FAILED",
|
|
3137
|
+
`${shown}: references needs <table>.<column>.`
|
|
3138
|
+
);
|
|
3139
|
+
column.references = { table, column: target };
|
|
3140
|
+
} else if (value !== void 0 && key === "ondelete") {
|
|
3141
|
+
onDelete = ON_DELETE[value.toLowerCase()];
|
|
3142
|
+
if (!onDelete)
|
|
3143
|
+
throw new NaroError(
|
|
3144
|
+
"VALIDATION_FAILED",
|
|
3145
|
+
`${shown}: ondelete takes cascade, set-null, set-default, restrict or no-action.`
|
|
3146
|
+
);
|
|
3147
|
+
} else
|
|
3148
|
+
throw new NaroError(
|
|
3149
|
+
"VALIDATION_FAILED",
|
|
3150
|
+
`${shown}: unknown modifier ${printable(modifier)}. Use pk, autoincrement, notnull, unique, default=<value>, references=<table>.<column> or ondelete=<action>.`
|
|
3252
3151
|
);
|
|
3253
|
-
return;
|
|
3254
|
-
}
|
|
3255
|
-
await sleep(FOLLOW_INTERVAL_MS);
|
|
3256
3152
|
}
|
|
3153
|
+
if (onDelete !== void 0) {
|
|
3154
|
+
if (!column.references)
|
|
3155
|
+
throw new NaroError(
|
|
3156
|
+
"VALIDATION_FAILED",
|
|
3157
|
+
`${shown}: ondelete needs references=<table>.<column>.`
|
|
3158
|
+
);
|
|
3159
|
+
column.references.onDelete = onDelete;
|
|
3160
|
+
}
|
|
3161
|
+
return column;
|
|
3257
3162
|
}
|
|
3258
|
-
|
|
3259
|
-
const
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3163
|
+
function policyLine(policy, dryRun = false) {
|
|
3164
|
+
const rules = [policy.select, policy.insert, policy.update, policy.delete];
|
|
3165
|
+
const owner = policy.ownerColumn === null ? "" : ` (owner column ${printable(policy.ownerColumn)})`;
|
|
3166
|
+
const uniform = rules.every((rule) => rule === rules[0]);
|
|
3167
|
+
const summary = uniform ? `${String(rules[0])} for select, insert, update and delete` : `select ${policy.select}, insert ${policy.insert}, update ${policy.update}, delete ${policy.delete}`;
|
|
3168
|
+
const head = dryRun ? "Would set the Data API policy" : "\u2713 Data API policy";
|
|
3169
|
+
return `${head} on ${printable(policy.table)}: ${summary}${owner}.`;
|
|
3170
|
+
}
|
|
3171
|
+
function ruleText(value) {
|
|
3172
|
+
if (typeof value === "string") return value;
|
|
3173
|
+
return "invalid" in value ? "invalid" : "condition";
|
|
3174
|
+
}
|
|
3175
|
+
var SUGAR_NAME = /^naro_(?:select|insert|update|delete)$/;
|
|
3176
|
+
function removedLines(policy, dryRun) {
|
|
3177
|
+
const verb = dryRun ? "Would remove" : "\u2713 Removed";
|
|
3178
|
+
const names = policy.policies ?? [];
|
|
3179
|
+
let line;
|
|
3180
|
+
if (names.every((name) => SUGAR_NAME.test(name))) {
|
|
3181
|
+
const owner = policy.ownerColumn === null ? "" : `, owner column ${printable(policy.ownerColumn)}`;
|
|
3182
|
+
const rules = `select ${ruleText(policy.select)}, insert ${ruleText(policy.insert)}, update ${ruleText(policy.update)}, delete ${ruleText(policy.delete)}${owner}`;
|
|
3183
|
+
line = `${verb} the Data API policy on ${printable(policy.table)} (${rules}).`;
|
|
3184
|
+
} else
|
|
3185
|
+
line = `${verb} ${String(names.length)} Data API ${names.length === 1 ? "policy" : "policies"} on ${printable(policy.table)}: ${names.map(printable).join(", ")}.`;
|
|
3186
|
+
const heading = " As SQL, for naro policy create:";
|
|
3187
|
+
const statements = policy.statements;
|
|
3188
|
+
if (statements === void 0) {
|
|
3189
|
+
const sql = policy.sql ?? "";
|
|
3190
|
+
if (sql === "") return [line];
|
|
3191
|
+
return isPrintable(sql) ? [line, heading, sql] : [
|
|
3192
|
+
line,
|
|
3193
|
+
" The SQL has characters that cannot be shown here: --json has the exact text."
|
|
3194
|
+
];
|
|
3195
|
+
}
|
|
3196
|
+
const owners = statements.filter(
|
|
3197
|
+
(statement) => typeof statement.ownerColumn === "string"
|
|
3198
|
+
);
|
|
3199
|
+
const created = statements.filter((statement) => !owners.includes(statement));
|
|
3200
|
+
const lines = [line];
|
|
3201
|
+
if (created.length > 0)
|
|
3202
|
+
lines.push(
|
|
3203
|
+
heading,
|
|
3204
|
+
...created.map(
|
|
3205
|
+
(statement) => shownStatement(statement, policy.table, "--json")
|
|
3206
|
+
)
|
|
3264
3207
|
);
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
{ projectId, type: "runtime", environment, entries },
|
|
3273
|
-
entries.length > 0 ? entries.map(formatRuntimeLog) : ["(no runtime logs yet \u2014 the worker logs on each request)"]
|
|
3208
|
+
if (owners.length > 0) {
|
|
3209
|
+
const one = owners.length === 1;
|
|
3210
|
+
const command = policy.setCommand;
|
|
3211
|
+
lines.push(
|
|
3212
|
+
` As a command, for the owner ${one ? "rule" : "rules"} (${owners.map((owner) => printable(owner.name)).join(", ")}): CREATE POLICY has no owner column, so naro policy create cannot make ${one ? "it" : "them"} again.`,
|
|
3213
|
+
// The server's bytes, or not shown: a name may hold a line break.
|
|
3214
|
+
typeof command !== "string" ? " naro policy set <table> --<action> owner --owner <column> sets one." : isPrintable(command) ? command : " The command has characters that cannot be shown here: --json has the exact text."
|
|
3274
3215
|
);
|
|
3275
|
-
return;
|
|
3276
3216
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3217
|
+
return lines;
|
|
3218
|
+
}
|
|
3219
|
+
function removedBeforeFailure(error) {
|
|
3220
|
+
if (!isNaroError(error)) return [];
|
|
3221
|
+
const removed = error.details?.removedPolicies;
|
|
3222
|
+
if (!Array.isArray(removed)) return [];
|
|
3223
|
+
return removed.filter(
|
|
3224
|
+
(entry) => typeof entry === "object" && entry !== null && typeof entry.table === "string" && entry.ownerColumn !== void 0
|
|
3279
3225
|
);
|
|
3280
|
-
let lastTs = -1;
|
|
3281
|
-
let boundary = /* @__PURE__ */ new Set();
|
|
3282
|
-
for (; ; ) {
|
|
3283
|
-
const entries = await client.logs.runtime(projectId, options);
|
|
3284
|
-
for (const entry of entries) {
|
|
3285
|
-
if (entry.ts < lastTs) continue;
|
|
3286
|
-
const key = JSON.stringify(entry);
|
|
3287
|
-
if (entry.ts === lastTs) {
|
|
3288
|
-
if (boundary.has(key)) continue;
|
|
3289
|
-
} else {
|
|
3290
|
-
lastTs = entry.ts;
|
|
3291
|
-
boundary = /* @__PURE__ */ new Set();
|
|
3292
|
-
}
|
|
3293
|
-
boundary.add(key);
|
|
3294
|
-
context.output.log(formatRuntimeLog(entry));
|
|
3295
|
-
}
|
|
3296
|
-
await sleep(FOLLOW_INTERVAL_MS);
|
|
3297
|
-
}
|
|
3298
3226
|
}
|
|
3299
|
-
async function
|
|
3227
|
+
async function runSchemaChange(context, operations, question) {
|
|
3228
|
+
const dryRun = context.flags.dryRun === true;
|
|
3229
|
+
if (question !== void 0 && !dryRun)
|
|
3230
|
+
await confirmOrAbort(context, question);
|
|
3231
|
+
const sent = dryRun ? operations.map((operation) => {
|
|
3232
|
+
if (operation.op !== "dropTable" && operation.op !== "dropColumn")
|
|
3233
|
+
return operation;
|
|
3234
|
+
const { confirm: _confirm, ...rest } = operation;
|
|
3235
|
+
return rest;
|
|
3236
|
+
}) : operations;
|
|
3300
3237
|
const client = await context.client();
|
|
3301
|
-
const
|
|
3302
|
-
const
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
);
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3238
|
+
const projectId = await context.resolveProjectId();
|
|
3239
|
+
const dir = migrationsDir(context);
|
|
3240
|
+
const local = listMigrationFiles(dir);
|
|
3241
|
+
const ledger = await appliedMigrations(client, projectId);
|
|
3242
|
+
const pending = pendingMigrationNames(local, ledger);
|
|
3243
|
+
if (pending.length > 0 && !dryRun)
|
|
3244
|
+
throw migrationsPendingError(pending.map(printable));
|
|
3245
|
+
const name = nextMigrationFileName(
|
|
3246
|
+
[...local, ...ledger.map((row) => row.name)],
|
|
3247
|
+
schemaMigrationTitle(operations[0] ?? {})
|
|
3248
|
+
);
|
|
3249
|
+
const result = await client.db.schema(projectId, sent, {
|
|
3250
|
+
...dryRun ? { dryRun: true } : {},
|
|
3251
|
+
name
|
|
3252
|
+
}).catch((error) => {
|
|
3253
|
+
if (!context.flags.json)
|
|
3254
|
+
for (const policy of removedBeforeFailure(error))
|
|
3255
|
+
for (const line of removedLines(policy, false))
|
|
3256
|
+
context.output.warn(line);
|
|
3257
|
+
throw error;
|
|
3258
|
+
});
|
|
3259
|
+
const sqlLines = result.sql.trimEnd().split("\n").map(printable);
|
|
3260
|
+
const removed = result.removedPolicies ?? [];
|
|
3261
|
+
if (result.dryRun) {
|
|
3262
|
+
for (const warning of result.warnings)
|
|
3263
|
+
context.output.warn(`\u26A0 ${printable(warning)}`);
|
|
3264
|
+
context.output.result({ projectId, ...result, file: null }, [
|
|
3265
|
+
...sqlLines,
|
|
3266
|
+
`Dry run \u2014 nothing was applied. It would be recorded as ${printable(result.name)}.`,
|
|
3267
|
+
...result.policies.map((policy) => policyLine(policy, true)),
|
|
3268
|
+
...removed.flatMap((policy) => removedLines(policy, true))
|
|
3269
|
+
]);
|
|
3316
3270
|
return;
|
|
3317
3271
|
}
|
|
3318
|
-
const
|
|
3319
|
-
|
|
3320
|
-
|
|
3272
|
+
const allWarnings = result.name === name ? result.warnings : [...result.warnings, migrationNameMismatch(name, result.name)];
|
|
3273
|
+
let file = null;
|
|
3274
|
+
let fileWarning;
|
|
3275
|
+
const relDir = relativeToCwd(context, dir);
|
|
3276
|
+
const rel = join3(relDir === "." ? "" : relDir, name);
|
|
3277
|
+
if (context.flags.noWrite) {
|
|
3278
|
+
fileWarning = `Not written to ${relDir} (--no-write): naro db reset will not re-apply ${result.name}, and naro migration list shows it as remote-only.`;
|
|
3279
|
+
} else {
|
|
3280
|
+
try {
|
|
3281
|
+
mkdirSync3(dir, { recursive: true });
|
|
3282
|
+
writeFileSync3(join3(dir, name), result.sql, { flag: "wx" });
|
|
3283
|
+
file = rel;
|
|
3284
|
+
} catch (error) {
|
|
3285
|
+
const reason = (error instanceof Error ? error.message : String(error)).replace(/[.\s]+$/, "");
|
|
3286
|
+
fileWarning = `Applied, but ${rel} could not be written: ${reason}. Save the SQL above as ${rel} so naro db push and naro db reset see it.`;
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
for (const warning of allWarnings)
|
|
3290
|
+
context.output.warn(`\u26A0 ${printable(warning)}`);
|
|
3291
|
+
if (fileWarning) context.output.warn(`\u26A0 ${printable(fileWarning)}`);
|
|
3292
|
+
context.output.result(
|
|
3293
|
+
{
|
|
3294
|
+
projectId,
|
|
3295
|
+
...result,
|
|
3296
|
+
warnings: allWarnings,
|
|
3297
|
+
file,
|
|
3298
|
+
...fileWarning ? { fileWarning } : {}
|
|
3299
|
+
},
|
|
3300
|
+
[
|
|
3301
|
+
...sqlLines,
|
|
3302
|
+
file === null ? `\u2705 Applied ${printable(result.name)}.` : `\u2705 Applied ${printable(result.name)} and wrote ${printable(file)}.`,
|
|
3303
|
+
...result.policies.map((policy) => policyLine(policy)),
|
|
3304
|
+
...removed.flatMap((policy) => removedLines(policy, false))
|
|
3305
|
+
]
|
|
3321
3306
|
);
|
|
3322
|
-
await buildLogs(context, client, target);
|
|
3323
3307
|
}
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
|
|
3338
|
-
|
|
3339
|
-
|
|
3340
|
-
|
|
3341
|
-
|
|
3342
|
-
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
3308
|
+
function columnsFlag(context) {
|
|
3309
|
+
return context.flags.columns ?? [];
|
|
3310
|
+
}
|
|
3311
|
+
async function runDbTableUsage() {
|
|
3312
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.table);
|
|
3313
|
+
}
|
|
3314
|
+
async function runDbIndexUsage() {
|
|
3315
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.index);
|
|
3316
|
+
}
|
|
3317
|
+
async function runDbTableCreate(context) {
|
|
3318
|
+
const [table] = context.positionals;
|
|
3319
|
+
const specs = columnsFlag(context);
|
|
3320
|
+
if (!table || specs.length === 0)
|
|
3321
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.create);
|
|
3322
|
+
const columns = specs.map(parseColumnSpec);
|
|
3323
|
+
const owner = context.flags.ownerColumn;
|
|
3324
|
+
await runSchemaChange(context, [
|
|
3325
|
+
{
|
|
3326
|
+
op: "createTable",
|
|
3327
|
+
table,
|
|
3328
|
+
columns,
|
|
3329
|
+
...context.flags.ifNotExists ? { ifNotExists: true } : {},
|
|
3330
|
+
...owner === void 0 ? {} : { owner: { column: owner } }
|
|
3331
|
+
}
|
|
3332
|
+
]);
|
|
3333
|
+
}
|
|
3334
|
+
async function runDbTableAddColumn(context) {
|
|
3335
|
+
const [table] = context.positionals;
|
|
3336
|
+
const specs = columnsFlag(context);
|
|
3337
|
+
const [spec] = specs;
|
|
3338
|
+
if (!table || spec === void 0 || specs.length !== 1)
|
|
3339
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.addColumn);
|
|
3340
|
+
await runSchemaChange(context, [
|
|
3341
|
+
{ op: "addColumn", table, column: parseColumnSpec(spec) }
|
|
3342
|
+
]);
|
|
3343
|
+
}
|
|
3344
|
+
async function runDbTableRenameColumn(context) {
|
|
3345
|
+
const [table, column, to] = context.positionals;
|
|
3346
|
+
if (!table || !column || !to)
|
|
3347
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.renameColumn);
|
|
3348
|
+
await runSchemaChange(context, [{ op: "renameColumn", table, column, to }]);
|
|
3349
|
+
}
|
|
3350
|
+
async function runDbTableRename(context) {
|
|
3351
|
+
const [table, to] = context.positionals;
|
|
3352
|
+
if (!table || !to) throw new NaroError("VALIDATION_FAILED", USAGE2.rename);
|
|
3353
|
+
await runSchemaChange(context, [{ op: "renameTable", table, to }]);
|
|
3354
|
+
}
|
|
3355
|
+
async function runDbTableDropColumn(context) {
|
|
3356
|
+
const [table, column] = context.positionals;
|
|
3357
|
+
if (!table || !column)
|
|
3358
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.dropColumn);
|
|
3359
|
+
const target = `${table}.${column}`;
|
|
3360
|
+
await runSchemaChange(
|
|
3361
|
+
context,
|
|
3362
|
+
[{ op: "dropColumn", table, column, confirm: target }],
|
|
3363
|
+
`Drop column ${printable(target)} and its value in every row? naro db restore can bring it back (Time Travel, 30 days).`
|
|
3364
|
+
);
|
|
3365
|
+
}
|
|
3366
|
+
async function runDbTableDrop(context) {
|
|
3367
|
+
const [table] = context.positionals;
|
|
3368
|
+
if (!table) throw new NaroError("VALIDATION_FAILED", USAGE2.drop);
|
|
3369
|
+
await runSchemaChange(
|
|
3370
|
+
context,
|
|
3371
|
+
[{ op: "dropTable", table, confirm: table }],
|
|
3372
|
+
`Drop table ${printable(table)} and every row in it? naro db restore can bring it back (Time Travel, 30 days).`
|
|
3373
|
+
);
|
|
3374
|
+
}
|
|
3375
|
+
async function runDbIndexCreate(context) {
|
|
3376
|
+
const [table, list] = context.positionals;
|
|
3377
|
+
const columns = listFlag(list);
|
|
3378
|
+
if (!table || columns.length === 0)
|
|
3379
|
+
throw new NaroError("VALIDATION_FAILED", USAGE2.indexCreate);
|
|
3380
|
+
const name = context.flags.name;
|
|
3381
|
+
await runSchemaChange(context, [
|
|
3382
|
+
{
|
|
3383
|
+
op: "createIndex",
|
|
3384
|
+
table,
|
|
3385
|
+
columns,
|
|
3386
|
+
...context.flags.unique ? { unique: true } : {},
|
|
3387
|
+
...name === void 0 ? {} : { name }
|
|
3388
|
+
}
|
|
3389
|
+
]);
|
|
3351
3390
|
}
|
|
3352
|
-
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
|
|
3391
|
+
async function runDbIndexDrop(context) {
|
|
3392
|
+
const [name] = context.positionals;
|
|
3393
|
+
if (!name) throw new NaroError("VALIDATION_FAILED", USAGE2.indexDrop);
|
|
3394
|
+
await runSchemaChange(context, [{ op: "dropIndex", name }]);
|
|
3356
3395
|
}
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3396
|
+
|
|
3397
|
+
// src/commands/git.ts
|
|
3398
|
+
var defaults2 = {
|
|
3399
|
+
detectRemote: async (dir) => (await detectGitInfo(dir)).repositoryUrl,
|
|
3400
|
+
openBrowser,
|
|
3401
|
+
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms))
|
|
3360
3402
|
};
|
|
3361
|
-
function
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3403
|
+
function connected(context, project) {
|
|
3404
|
+
const branch = project.defaultBranch;
|
|
3405
|
+
context.output.result(
|
|
3406
|
+
{
|
|
3407
|
+
projectId: project.id,
|
|
3408
|
+
repositoryFullName: project.repositoryFullName,
|
|
3409
|
+
autoDeploy: true
|
|
3410
|
+
},
|
|
3411
|
+
[
|
|
3412
|
+
`\u2705 Connected to ${project.repositoryFullName ?? "the repository"}.`,
|
|
3413
|
+
` Pushes to ${branch} now deploy automatically.`,
|
|
3414
|
+
"",
|
|
3415
|
+
" To turn this off: naro git disconnect"
|
|
3416
|
+
]
|
|
3417
|
+
);
|
|
3366
3418
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3419
|
+
async function runGitConnect(context, overrides = {}) {
|
|
3420
|
+
const deps = { ...defaults2, ...overrides };
|
|
3421
|
+
const { output } = context;
|
|
3422
|
+
const explicit = context.positionals[0];
|
|
3423
|
+
let fullName;
|
|
3424
|
+
if (explicit) {
|
|
3425
|
+
const parsed = parseGitHubRemote(explicit) ?? parseGitHubRemote(`https://github.com/${explicit}`);
|
|
3426
|
+
if (!parsed)
|
|
3427
|
+
throw new NaroError(
|
|
3428
|
+
"VALIDATION_FAILED",
|
|
3429
|
+
`"${explicit}" is not an owner/repo pair.`
|
|
3430
|
+
);
|
|
3431
|
+
fullName = parsed.fullName;
|
|
3432
|
+
} else {
|
|
3433
|
+
const remote = await deps.detectRemote(context.cwd);
|
|
3434
|
+
const parsed = remote ? parseGitHubRemote(remote) : null;
|
|
3435
|
+
if (!parsed) {
|
|
3436
|
+
throw new NaroError(
|
|
3437
|
+
"NO_GIT_REMOTE",
|
|
3438
|
+
remote ? `Could not read a GitHub owner/repo from "${remote}". Pass it directly: naro git connect <owner/repo>` : "This directory has no GitHub origin remote. Pass one directly: naro git connect <owner/repo>"
|
|
3439
|
+
);
|
|
3388
3440
|
}
|
|
3389
|
-
|
|
3390
|
-
}
|
|
3391
|
-
at() {
|
|
3392
|
-
return this.text[this.index] ?? "";
|
|
3393
|
-
}
|
|
3394
|
-
/**
|
|
3395
|
-
* An unquoted value that is one of the tokens misspelled — `auth.uid`,
|
|
3396
|
-
* `AUTH.UID` — is refused: read as text it would compare with the words
|
|
3397
|
-
* themselves (사용자 리뷰 반영). Quoted, it is the text.
|
|
3398
|
-
*/
|
|
3399
|
-
notNearToken(value, at) {
|
|
3400
|
-
const loose = looseWord(value);
|
|
3401
|
-
for (const word of Object.keys(this.tokens))
|
|
3402
|
-
if (loose !== "" && loose === looseWord(word))
|
|
3403
|
-
refuse(
|
|
3404
|
-
`has ${JSON.stringify(value)} ${this.where(at)}: did you mean ${word}? Quote it to mean the text`
|
|
3405
|
-
);
|
|
3406
|
-
}
|
|
3407
|
-
/** `scalar`, with an unquoted value held to notNearToken. */
|
|
3408
|
-
bareOrQuoted(stops) {
|
|
3409
|
-
this.skipSpaces();
|
|
3410
|
-
const at = this.index;
|
|
3411
|
-
const quoted = this.at() === '"';
|
|
3412
|
-
const text = this.scalar(stops);
|
|
3413
|
-
if (!quoted) this.notNearToken(text, at);
|
|
3414
|
-
return { text, quoted };
|
|
3441
|
+
fullName = parsed.fullName;
|
|
3415
3442
|
}
|
|
3416
|
-
|
|
3417
|
-
|
|
3443
|
+
const client = await context.client();
|
|
3444
|
+
const projectId = await context.resolveProjectId();
|
|
3445
|
+
const first = await client.projects.connectGit(projectId, fullName);
|
|
3446
|
+
if (first.ok) {
|
|
3447
|
+
connected(context, first.project);
|
|
3448
|
+
return;
|
|
3418
3449
|
}
|
|
3419
|
-
|
|
3420
|
-
|
|
3450
|
+
if (!first.installUrl) {
|
|
3451
|
+
throw new NaroError(
|
|
3452
|
+
"REPOSITORY_NOT_ACCESSIBLE",
|
|
3453
|
+
`Naro cannot reach ${fullName}, and no install URL was offered. Connect it from the dashboard.`
|
|
3454
|
+
);
|
|
3421
3455
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3456
|
+
output.log(`
|
|
3457
|
+
\u25B8 Naro needs access to ${fullName}.`);
|
|
3458
|
+
output.log(` Opening ${first.installUrl}`);
|
|
3459
|
+
deps.openBrowser(first.installUrl);
|
|
3460
|
+
output.log(" Waiting for the installation to finish...\n");
|
|
3461
|
+
await waitForInstall(context, projectId, fullName, deps);
|
|
3462
|
+
}
|
|
3463
|
+
async function waitForInstall(context, projectId, fullName, deps) {
|
|
3464
|
+
const client = await context.client();
|
|
3465
|
+
const project = await client.projects.get(projectId);
|
|
3466
|
+
const teams = await client.teams.list();
|
|
3467
|
+
const teamSlug = teams.find((team) => team.id === project.teamId)?.slug;
|
|
3468
|
+
if (teamSlug) {
|
|
3469
|
+
const attempts = deps.pollAttempts ?? 60;
|
|
3470
|
+
const interval = deps.pollIntervalMs ?? 3e3;
|
|
3471
|
+
for (let attempt2 = 0; attempt2 < attempts; attempt2 += 1) {
|
|
3472
|
+
await deps.sleep(interval);
|
|
3473
|
+
let reachable = false;
|
|
3474
|
+
try {
|
|
3475
|
+
const repositories = await client.github.listRepositories(teamSlug);
|
|
3476
|
+
reachable = repositories.some(
|
|
3477
|
+
(repository) => repository.fullName.toLowerCase() === fullName.toLowerCase()
|
|
3478
|
+
);
|
|
3479
|
+
} catch {
|
|
3480
|
+
}
|
|
3481
|
+
if (!reachable) continue;
|
|
3482
|
+
const retry = await client.projects.connectGit(projectId, fullName);
|
|
3483
|
+
if (retry.ok) {
|
|
3484
|
+
connected(context, retry.project);
|
|
3485
|
+
return;
|
|
3486
|
+
}
|
|
3432
3487
|
}
|
|
3433
3488
|
}
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
|
|
3456
|
-
const
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
3489
|
+
context.output.result(
|
|
3490
|
+
{ projectId, repositoryFullName: null, autoDeploy: false },
|
|
3491
|
+
[
|
|
3492
|
+
`\u25B8 Still waiting on access to ${fullName}.`,
|
|
3493
|
+
" Finish the installation, then run: naro git connect"
|
|
3494
|
+
]
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
async function runGitDisconnect(context) {
|
|
3498
|
+
const client = await context.client();
|
|
3499
|
+
const projectId = await context.resolveProjectId();
|
|
3500
|
+
const project = await client.projects.disconnectGit(projectId);
|
|
3501
|
+
context.output.result({ projectId: project.id, autoDeploy: false }, [
|
|
3502
|
+
`\u2705 ${project.name} no longer deploys on push.`,
|
|
3503
|
+
" Deploy from here with: naro deploy"
|
|
3504
|
+
]);
|
|
3505
|
+
}
|
|
3506
|
+
async function offerAutoDeploy(context, projectId, overrides) {
|
|
3507
|
+
const deps = { ...defaults2, ...overrides };
|
|
3508
|
+
if (overrides.canPrompt === false) return;
|
|
3509
|
+
try {
|
|
3510
|
+
if (readProjectLink(context.cwd)?.autoDeployDeclined === true) return;
|
|
3511
|
+
const connection = overrides.gitConnection;
|
|
3512
|
+
if (connection.state === "connected" || connection.state === "none") return;
|
|
3513
|
+
const fullName = connection.fullName;
|
|
3514
|
+
if (!fullName) return;
|
|
3515
|
+
let wanted;
|
|
3516
|
+
if (connection.state === "unreachable") {
|
|
3517
|
+
const installed = connection.installedAccounts ?? [];
|
|
3518
|
+
context.output.log(
|
|
3519
|
+
`
|
|
3520
|
+
\u25B8 Automatic deploys need the Naro GitHub App on \`${connection.repoOwner ?? fullName}\`.`
|
|
3521
|
+
);
|
|
3522
|
+
if (overrides.teamSlug && installed.length > 0)
|
|
3523
|
+
context.output.log(
|
|
3524
|
+
` Team ${overrides.teamSlug} has it on: ${installed.join(", ")}.`
|
|
3525
|
+
);
|
|
3526
|
+
wanted = await context.prompter.confirm(
|
|
3527
|
+
"Install it for this team now? (opens your browser)"
|
|
3528
|
+
);
|
|
3529
|
+
} else {
|
|
3530
|
+
wanted = await context.prompter.confirm(
|
|
3531
|
+
"Should your changes deploy automatically from now on?"
|
|
3532
|
+
);
|
|
3471
3533
|
}
|
|
3472
|
-
|
|
3473
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3534
|
+
if (!wanted) {
|
|
3535
|
+
const link = readProjectLink(context.cwd);
|
|
3536
|
+
writeProjectLink(context.cwd, {
|
|
3537
|
+
...link ?? { projectId },
|
|
3538
|
+
autoDeployDeclined: true
|
|
3539
|
+
});
|
|
3540
|
+
context.output.log(
|
|
3541
|
+
"\u25B8 Not now. Later, if you change your mind: naro git connect"
|
|
3542
|
+
);
|
|
3543
|
+
return;
|
|
3477
3544
|
}
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3482
|
-
|
|
3545
|
+
const client = await context.client();
|
|
3546
|
+
const result = await client.projects.connectGit(projectId, fullName);
|
|
3547
|
+
if (result.ok) {
|
|
3548
|
+
connected(context, result.project);
|
|
3549
|
+
return;
|
|
3550
|
+
}
|
|
3551
|
+
if (!result.installUrl) return;
|
|
3552
|
+
context.output.log(`
|
|
3553
|
+
\u25B8 Naro needs access to ${fullName}.`);
|
|
3554
|
+
context.output.log(` Opening ${result.installUrl}`);
|
|
3555
|
+
deps.openBrowser(result.installUrl);
|
|
3556
|
+
context.output.log(" Waiting for the installation to finish...\n");
|
|
3557
|
+
await waitForInstall(context, projectId, fullName, deps);
|
|
3558
|
+
} catch (error) {
|
|
3559
|
+
context.output.log(
|
|
3560
|
+
`\u25B8 Could not set up automatic deploys (${error instanceof Error ? error.message : String(error)}).`
|
|
3561
|
+
);
|
|
3562
|
+
context.output.log(" Try later with: naro git connect");
|
|
3483
3563
|
}
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
// src/commands/link.ts
|
|
3567
|
+
var SWITCH_ACCOUNT = "Sign in as a different account\u2026";
|
|
3568
|
+
async function resolveTeam(context, teams, deps = {}) {
|
|
3569
|
+
const explicit = context.flags.team ?? process.env.NARO_TEAM;
|
|
3570
|
+
if (explicit) {
|
|
3571
|
+
const match = teams.find((team) => team.slug === explicit);
|
|
3572
|
+
if (!match)
|
|
3573
|
+
throw new NaroError(
|
|
3574
|
+
"NOT_FOUND",
|
|
3575
|
+
`You are not a member of team "${explicit}".`
|
|
3576
|
+
);
|
|
3577
|
+
return match;
|
|
3490
3578
|
}
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
const
|
|
3494
|
-
|
|
3495
|
-
let text = "";
|
|
3496
|
-
for (; ; ) {
|
|
3497
|
-
if (this.index >= this.text.length)
|
|
3498
|
-
refuse(`has an unclosed quote ${this.where(start)}`);
|
|
3499
|
-
const char = this.at();
|
|
3500
|
-
this.index++;
|
|
3501
|
-
if (char === '"') return text;
|
|
3502
|
-
if (char === "\\" && this.index < this.text.length) {
|
|
3503
|
-
text += this.at();
|
|
3504
|
-
this.index++;
|
|
3505
|
-
} else text += char;
|
|
3506
|
-
}
|
|
3579
|
+
const linked = context.projectLink();
|
|
3580
|
+
if (linked?.teamSlug) {
|
|
3581
|
+
const match = teams.find((team) => team.slug === linked.teamSlug);
|
|
3582
|
+
if (match) return match;
|
|
3507
3583
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3584
|
+
if (teams.length === 0)
|
|
3585
|
+
throw new NaroError("NOT_FOUND", "Your account has no teams yet.");
|
|
3586
|
+
const only = teams.length === 1 ? teams[0] : null;
|
|
3587
|
+
const deploying = (deps.intent ?? "deploy") === "deploy";
|
|
3588
|
+
if (only && !deploying) return only;
|
|
3589
|
+
const canPrompt = deps.canPrompt?.() ?? (!context.flags.json && !context.flags.yes && isInteractive());
|
|
3590
|
+
if (!canPrompt)
|
|
3591
|
+
throw new NaroError(
|
|
3592
|
+
"TEAM_REQUIRED",
|
|
3593
|
+
`Cannot pick a team without a prompt. Pass --team <slug>, set NARO_TEAM, or run \`naro link\` first.
|
|
3594
|
+
Your teams: ${teams.map((team) => team.slug).join(", ")}`
|
|
3595
|
+
);
|
|
3596
|
+
const account = deps.accountLabel ? await deps.accountLabel() : "this account";
|
|
3597
|
+
if (only) {
|
|
3598
|
+
const label2 = only.isPersonal ? `${only.name} (personal)` : only.name;
|
|
3599
|
+
context.output.log(`
|
|
3600
|
+
${account} \u2192 ${label2}`);
|
|
3601
|
+
const ok = await context.prompter.confirm(
|
|
3602
|
+
"Deploy to this account and team?"
|
|
3603
|
+
);
|
|
3604
|
+
if (ok) return only;
|
|
3605
|
+
return switchAndRetry(context, deps);
|
|
3522
3606
|
}
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3607
|
+
const options = [
|
|
3608
|
+
...teams.map(
|
|
3609
|
+
(team) => `${team.name}${team.isPersonal ? " (personal)" : ""}`
|
|
3610
|
+
),
|
|
3611
|
+
SWITCH_ACCOUNT
|
|
3612
|
+
];
|
|
3613
|
+
const index = await context.prompter.select(
|
|
3614
|
+
deploying ? `Deploying as ${account} \u2014 which team owns this project?` : `Signed in as ${account}. Which team?`,
|
|
3615
|
+
options
|
|
3616
|
+
);
|
|
3617
|
+
const picked = teams[index];
|
|
3618
|
+
if (picked) return picked;
|
|
3619
|
+
return switchAndRetry(context, deps);
|
|
3620
|
+
}
|
|
3621
|
+
async function switchAndRetry(context, deps) {
|
|
3622
|
+
if (!deps.switchAccount || !deps.listTeams)
|
|
3623
|
+
throw new NaroError(
|
|
3624
|
+
"CANCELED",
|
|
3625
|
+
"Canceled. Switch accounts with `naro logout && naro login`."
|
|
3626
|
+
);
|
|
3627
|
+
await deps.switchAccount();
|
|
3628
|
+
const teams = await deps.listTeams();
|
|
3629
|
+
return resolveTeam(context, teams, deps);
|
|
3630
|
+
}
|
|
3631
|
+
function saveLink(context, project, teamSlug) {
|
|
3632
|
+
writeProjectLink(context.cwd, {
|
|
3633
|
+
projectId: project.id,
|
|
3634
|
+
teamSlug,
|
|
3635
|
+
projectName: project.name
|
|
3636
|
+
});
|
|
3637
|
+
if (ensureGitignoreHasNaro(context.cwd))
|
|
3638
|
+
context.output.log("\u25B8 added .naro to .gitignore");
|
|
3639
|
+
}
|
|
3640
|
+
async function runLink(context) {
|
|
3641
|
+
const client = await context.client();
|
|
3642
|
+
const teams = await client.teams.list();
|
|
3643
|
+
const team = await resolveTeam(context, teams, { intent: "read" });
|
|
3644
|
+
const projects = await client.projects.list(team.slug);
|
|
3645
|
+
let project;
|
|
3646
|
+
if (context.flags.project) {
|
|
3647
|
+
project = projects.find(
|
|
3648
|
+
(candidate) => candidate.id === context.flags.project
|
|
3649
|
+
);
|
|
3650
|
+
if (!project) {
|
|
3651
|
+
project = await client.projects.get(context.flags.project);
|
|
3565
3652
|
}
|
|
3566
|
-
}
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
const token = this.token(",)");
|
|
3597
|
-
return token ? token.value : this.bareOrQuoted(",)").text;
|
|
3598
|
-
}
|
|
3653
|
+
} else if (context.flags.name) {
|
|
3654
|
+
project = projects.find(
|
|
3655
|
+
(candidate) => candidate.name === context.flags.name || candidate.slug === context.flags.name
|
|
3656
|
+
);
|
|
3657
|
+
if (!project)
|
|
3658
|
+
throw new NaroError(
|
|
3659
|
+
"PROJECT_NOT_FOUND",
|
|
3660
|
+
`No project named "${context.flags.name}" in team ${team.slug}. Run \`naro deploy\` to create it.`
|
|
3661
|
+
);
|
|
3662
|
+
} else {
|
|
3663
|
+
const guess = slugify(context.cwd.split("/").at(-1) ?? "app");
|
|
3664
|
+
const exact = projects.find(
|
|
3665
|
+
(candidate) => candidate.name === guess || candidate.slug === guess
|
|
3666
|
+
);
|
|
3667
|
+
if (exact && (context.flags.yes || context.flags.json)) {
|
|
3668
|
+
project = exact;
|
|
3669
|
+
} else if (projects.length === 0) {
|
|
3670
|
+
throw new NaroError(
|
|
3671
|
+
"PROJECT_NOT_FOUND",
|
|
3672
|
+
`Team ${team.slug} has no projects yet. Run \`naro deploy\` to create one.`
|
|
3673
|
+
);
|
|
3674
|
+
} else {
|
|
3675
|
+
const options = projects.map(
|
|
3676
|
+
(candidate) => `${candidate.name} (${candidate.subdomain})`
|
|
3677
|
+
);
|
|
3678
|
+
const index = await context.prompter.select(
|
|
3679
|
+
`Link ${context.cwd} to which project?`,
|
|
3680
|
+
options
|
|
3681
|
+
);
|
|
3682
|
+
project = projects[index];
|
|
3599
3683
|
}
|
|
3600
3684
|
}
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3685
|
+
if (!project)
|
|
3686
|
+
throw new NaroError("PROJECT_NOT_FOUND", "No project selected.");
|
|
3687
|
+
const owningTeam = teams.find((candidate) => candidate.id === project.teamId) ?? team;
|
|
3688
|
+
saveLink(context, project, owningTeam.slug);
|
|
3689
|
+
context.output.result(
|
|
3690
|
+
{
|
|
3691
|
+
projectId: project.id,
|
|
3692
|
+
projectName: project.name,
|
|
3693
|
+
teamSlug: owningTeam.slug,
|
|
3694
|
+
subdomain: project.subdomain
|
|
3695
|
+
},
|
|
3696
|
+
[`\u2705 Linked to ${owningTeam.slug}/${project.name} (.naro/project.json)`]
|
|
3697
|
+
);
|
|
3604
3698
|
}
|
|
3605
|
-
function
|
|
3606
|
-
const
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3699
|
+
async function runUnlink(context) {
|
|
3700
|
+
const existing = readProjectLink(context.cwd);
|
|
3701
|
+
const removed = removeProjectLink(context.cwd);
|
|
3702
|
+
context.output.result(
|
|
3703
|
+
{ removed, ...existing ? { projectId: existing.projectId } : {} },
|
|
3704
|
+
[removed ? "\u2705 Unlinked (.naro removed)" : "Nothing to unlink here."]
|
|
3705
|
+
);
|
|
3610
3706
|
}
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
const
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3707
|
+
|
|
3708
|
+
// src/commands/deploy.ts
|
|
3709
|
+
async function runDeploy(context, deps = { deploy: deployDirectory, offerAutoDeploy }) {
|
|
3710
|
+
const { flags, output } = context;
|
|
3711
|
+
const link = flags.project ? { projectId: flags.project, teamSlug: void 0 } : readProjectLink(context.cwd);
|
|
3712
|
+
if (flags.prod && !flags.yes && !flags.json && isInteractive()) {
|
|
3713
|
+
const confirmed = await context.prompter.confirm("Deploy to production?");
|
|
3714
|
+
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
3715
|
+
}
|
|
3716
|
+
output.log(`
|
|
3717
|
+
\u{1F680} Deploying ${context.cwd}
|
|
3718
|
+
`);
|
|
3719
|
+
const canPrompt = deps.canPrompt?.() ?? (!flags.json && !flags.yes && isInteractive());
|
|
3720
|
+
const team = await resolveDestination(context, link, canPrompt);
|
|
3721
|
+
const account = await accountLabel(context);
|
|
3722
|
+
output.log(` ${account} \u2192 ${team.slug}
|
|
3723
|
+
`);
|
|
3724
|
+
const result = await deps.deploy(
|
|
3725
|
+
context.cwd,
|
|
3726
|
+
{
|
|
3727
|
+
name: flags.name,
|
|
3728
|
+
projectId: link?.projectId,
|
|
3729
|
+
teamSlug: team.slug,
|
|
3730
|
+
rootDirectory: flags.root ?? null,
|
|
3731
|
+
branch: flags.branch,
|
|
3732
|
+
production: flags.prod,
|
|
3733
|
+
...canPrompt && {
|
|
3734
|
+
onMissingEnv: (keys) => promptForMissingEnv(
|
|
3735
|
+
{ cwd: context.cwd, prompter: context.prompter, output },
|
|
3736
|
+
keys
|
|
3737
|
+
)
|
|
3738
|
+
},
|
|
3739
|
+
token: flags.token,
|
|
3740
|
+
allowLogin: !flags.json && !flags.yes
|
|
3741
|
+
},
|
|
3742
|
+
(message) => output.log(message)
|
|
3743
|
+
);
|
|
3744
|
+
const existing = readProjectLink(context.cwd);
|
|
3745
|
+
writeProjectLink(context.cwd, {
|
|
3746
|
+
projectId: result.projectId,
|
|
3747
|
+
teamSlug: result.teamSlug,
|
|
3748
|
+
projectName: result.projectSlug,
|
|
3749
|
+
...existing?.autoDeployDeclined === true ? { autoDeployDeclined: true } : {}
|
|
3750
|
+
});
|
|
3751
|
+
if (!link) {
|
|
3752
|
+
if (ensureGitignoreHasNaro(context.cwd))
|
|
3753
|
+
output.log("\u25B8 added .naro to .gitignore");
|
|
3754
|
+
output.log("\u25B8 linked (.naro/project.json)");
|
|
3755
|
+
}
|
|
3756
|
+
await deps.offerAutoDeploy?.(context, result.projectId, {
|
|
3757
|
+
canPrompt,
|
|
3758
|
+
gitConnection: result.gitConnection,
|
|
3759
|
+
teamSlug: result.teamSlug
|
|
3760
|
+
});
|
|
3761
|
+
const inspect = `${controlPlaneUrl()}${projectPath(result.teamSlug, result.projectSlug)}`;
|
|
3762
|
+
context.output.result(
|
|
3763
|
+
{
|
|
3764
|
+
deploymentId: result.deploymentId,
|
|
3765
|
+
projectId: result.projectId,
|
|
3766
|
+
status: result.status,
|
|
3767
|
+
url: result.url,
|
|
3768
|
+
target: result.production ? "production" : "preview",
|
|
3769
|
+
teamSlug: result.teamSlug,
|
|
3770
|
+
inspectUrl: inspect,
|
|
3771
|
+
missingEnvKeys: result.missingEnvKeys
|
|
3772
|
+
},
|
|
3773
|
+
[
|
|
3774
|
+
"",
|
|
3775
|
+
`\u2705 Deployment ready (${result.production ? "production" : "preview"})`,
|
|
3776
|
+
"",
|
|
3777
|
+
` ${result.url}`,
|
|
3778
|
+
` Inspect ${inspect}`
|
|
3779
|
+
]
|
|
3780
|
+
);
|
|
3618
3781
|
}
|
|
3619
|
-
function
|
|
3620
|
-
const
|
|
3621
|
-
|
|
3782
|
+
async function resolveDestination(context, link, canPrompt) {
|
|
3783
|
+
const client = await context.client();
|
|
3784
|
+
const teams = await client.teams.list();
|
|
3785
|
+
const deps = scopeDeps(context, canPrompt);
|
|
3786
|
+
if (link?.teamSlug) {
|
|
3787
|
+
const match = teams.find((team) => team.slug === link.teamSlug);
|
|
3788
|
+
if (match) return match;
|
|
3789
|
+
} else if (link) {
|
|
3790
|
+
let project;
|
|
3791
|
+
try {
|
|
3792
|
+
project = await client.projects.get(link.projectId);
|
|
3793
|
+
} catch (error) {
|
|
3794
|
+
if (isNaroError(error) && error.code !== "NOT_FOUND") throw error;
|
|
3795
|
+
throw new NaroError(
|
|
3796
|
+
"PROJECT_NOT_FOUND",
|
|
3797
|
+
`This directory is linked to a project ${await accountLabel(context)} cannot see.
|
|
3798
|
+
The CLI signs in separately from the dashboard \u2014 check with \`naro whoami\`.
|
|
3799
|
+
Wrong account? naro logout && naro login
|
|
3800
|
+
Project gone? remove .naro, then naro deploy`,
|
|
3801
|
+
{ cause: error }
|
|
3802
|
+
);
|
|
3803
|
+
}
|
|
3804
|
+
const match = teams.find((team) => team.id === project.teamId);
|
|
3805
|
+
if (match) return match;
|
|
3806
|
+
}
|
|
3807
|
+
return resolveTeam(context, teams, deps);
|
|
3622
3808
|
}
|
|
3623
|
-
function
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3809
|
+
function scopeDeps(context, canPrompt) {
|
|
3810
|
+
return {
|
|
3811
|
+
canPrompt: () => canPrompt,
|
|
3812
|
+
accountLabel: () => accountLabel(context),
|
|
3813
|
+
switchAccount: async () => {
|
|
3814
|
+
clearSession();
|
|
3815
|
+
await runLogin(context);
|
|
3816
|
+
},
|
|
3817
|
+
listTeams: async () => (await context.client()).teams.list()
|
|
3818
|
+
};
|
|
3631
3819
|
}
|
|
3632
|
-
function
|
|
3820
|
+
async function accountLabel(context) {
|
|
3633
3821
|
try {
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3822
|
+
const { user } = await context.auth();
|
|
3823
|
+
if (!user) return "this account";
|
|
3824
|
+
const name = user.email ?? user.id;
|
|
3825
|
+
return user.github ? `${name} (${user.github})` : name;
|
|
3826
|
+
} catch {
|
|
3827
|
+
return "this account";
|
|
3638
3828
|
}
|
|
3639
3829
|
}
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
return attempt(() => {
|
|
3645
|
-
if (options.group === false) {
|
|
3646
|
-
const alone = items(text, options.tokens, 0);
|
|
3647
|
-
if (alone.length === 1 && alone[0]) return alone[0];
|
|
3648
|
-
}
|
|
3649
|
-
const nodes = items(text, options.tokens, 1);
|
|
3650
|
-
return kind === "or" ? { or: nodes } : { and: nodes };
|
|
3651
|
-
});
|
|
3830
|
+
|
|
3831
|
+
// src/commands/deployments.ts
|
|
3832
|
+
function shortSha(sha) {
|
|
3833
|
+
return sha ? sha.slice(0, 7) : "-";
|
|
3652
3834
|
}
|
|
3653
|
-
function
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3835
|
+
function formatDeploymentLine(deployment) {
|
|
3836
|
+
return [
|
|
3837
|
+
deployment.id.padEnd(38),
|
|
3838
|
+
deployment.status.padEnd(10),
|
|
3839
|
+
deployment.branch.padEnd(16),
|
|
3840
|
+
shortSha(deployment.gitCommitSha).padEnd(9),
|
|
3841
|
+
deployment.url ?? ""
|
|
3842
|
+
].join(" ");
|
|
3658
3843
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3844
|
+
async function runDeployments(context) {
|
|
3845
|
+
const client = await context.client();
|
|
3846
|
+
const projectId = await context.resolveProjectId();
|
|
3847
|
+
const deployments = await client.deployments.list(projectId);
|
|
3848
|
+
context.output.result({ projectId, deployments }, [
|
|
3849
|
+
`Deployments for ${projectId}:`,
|
|
3850
|
+
"",
|
|
3851
|
+
[
|
|
3852
|
+
"ID".padEnd(38),
|
|
3853
|
+
"STATUS".padEnd(10),
|
|
3854
|
+
"BRANCH".padEnd(16),
|
|
3855
|
+
"COMMIT".padEnd(9),
|
|
3856
|
+
"URL"
|
|
3857
|
+
].join(" "),
|
|
3858
|
+
...deployments.map(formatDeploymentLine)
|
|
3859
|
+
]);
|
|
3860
|
+
}
|
|
3861
|
+
async function runInspect(context) {
|
|
3862
|
+
const [deploymentId] = context.positionalOrThrow(
|
|
3863
|
+
1,
|
|
3864
|
+
"Usage: naro inspect <deployment-id>"
|
|
3865
|
+
);
|
|
3866
|
+
const client = await context.client();
|
|
3867
|
+
const detail = await client.deployments.get(deploymentId);
|
|
3868
|
+
const { deployment } = detail;
|
|
3869
|
+
const lastLogs = detail.logs.slice(-15);
|
|
3870
|
+
context.output.result({ deployment, logs: detail.logs }, [
|
|
3871
|
+
`Deployment ${deployment.id}`,
|
|
3872
|
+
` status: ${deployment.status}${deployment.errorCode ? ` (${deployment.errorCode})` : ""}`,
|
|
3873
|
+
` branch: ${deployment.branch}`,
|
|
3874
|
+
` commit: ${deployment.gitCommitSha ?? "-"}`,
|
|
3875
|
+
` url: ${deployment.url ?? "-"}`,
|
|
3876
|
+
` created: ${deployment.createdAt}`,
|
|
3877
|
+
` finished: ${deployment.finishedAt ?? "-"}`,
|
|
3878
|
+
...deployment.errorMessage ? [` error: ${deployment.errorMessage}`] : [],
|
|
3879
|
+
...lastLogs.length > 0 ? [
|
|
3880
|
+
"",
|
|
3881
|
+
"Recent build logs:",
|
|
3882
|
+
...lastLogs.map((log) => ` [${log.stage}] ${log.message}`)
|
|
3883
|
+
] : []
|
|
3884
|
+
]);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
// src/commands/domains.ts
|
|
3888
|
+
var defaults3 = {
|
|
3889
|
+
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
3890
|
+
now: () => Date.now()
|
|
3674
3891
|
};
|
|
3675
|
-
var
|
|
3676
|
-
var
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
"
|
|
3680
|
-
"deleteRule",
|
|
3681
|
-
"ownerColumn",
|
|
3682
|
-
"selectWhen",
|
|
3683
|
-
"insertWhen",
|
|
3684
|
-
"updateWhen",
|
|
3685
|
-
"updateCheck",
|
|
3686
|
-
"deleteWhen",
|
|
3687
|
-
"signedIn"
|
|
3688
|
-
];
|
|
3689
|
-
function isRule(value) {
|
|
3690
|
-
return POLICY_RULES.includes(value);
|
|
3892
|
+
var FIRST_DELAY_MS = 5e3;
|
|
3893
|
+
var MAX_DELAY_MS = 3e4;
|
|
3894
|
+
var WAIT_TIMEOUT_MS = 10 * 6e4;
|
|
3895
|
+
function isReady(domain) {
|
|
3896
|
+
return domain.ready ?? (domain.status === "active" && domain.sslStatus === "active");
|
|
3691
3897
|
}
|
|
3692
|
-
function
|
|
3693
|
-
|
|
3898
|
+
function isTerminal(domain) {
|
|
3899
|
+
return domain.ownership === "deleted" || domain.status === "blocked" || domain.status === "moved" || domain.status === "deleted";
|
|
3694
3900
|
}
|
|
3695
|
-
|
|
3696
|
-
|
|
3901
|
+
var AXIS_DONE = /* @__PURE__ */ new Set(["verified", "active", "live"]);
|
|
3902
|
+
var AXIS_FAILED = /* @__PURE__ */ new Set(["failed", "deleted"]);
|
|
3903
|
+
function axisMark(value) {
|
|
3904
|
+
if (AXIS_DONE.has(value)) return "\u2713";
|
|
3905
|
+
if (AXIS_FAILED.has(value)) return "\u2717";
|
|
3906
|
+
return "\xB7";
|
|
3697
3907
|
}
|
|
3698
|
-
function
|
|
3699
|
-
const
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
const path = checked.path.replace(/^\./, "");
|
|
3708
|
-
refuse2(
|
|
3709
|
-
path === "" ? `${flag} ${checked.reason}.` : `${flag}: ${path} ${checked.reason}.`
|
|
3710
|
-
);
|
|
3711
|
-
}
|
|
3712
|
-
return tree;
|
|
3908
|
+
function axisLines(domain) {
|
|
3909
|
+
const axes = [
|
|
3910
|
+
["ownership", domain.ownership],
|
|
3911
|
+
["certificate", domain.certificate],
|
|
3912
|
+
["routing", domain.routing]
|
|
3913
|
+
];
|
|
3914
|
+
return axes.filter((axis) => axis[1] !== void 0).map(
|
|
3915
|
+
([name, value]) => ` ${`${name}:`.padEnd(13)}${axisMark(value)} ${value}`
|
|
3916
|
+
);
|
|
3713
3917
|
}
|
|
3714
|
-
function
|
|
3715
|
-
const
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
|
|
3721
|
-
if (flags[RULE_FLAGS[action]] !== void 0)
|
|
3722
|
-
refuse2(`--${action} and ${flag} cannot be used together.`);
|
|
3723
|
-
const condition = { [tree]: conditionTree(flag, text) };
|
|
3724
|
-
if (action === "update" && flags.updateCheck !== void 0)
|
|
3725
|
-
condition.check = conditionTree("--update-check", flags.updateCheck);
|
|
3726
|
-
if (flags.signedIn) condition.role = "authenticated";
|
|
3727
|
-
conditions[action] = condition;
|
|
3728
|
-
}
|
|
3729
|
-
if (flags.updateCheck !== void 0 && flags.updateWhen === void 0)
|
|
3730
|
-
refuse2("--update-check needs --update-when.");
|
|
3731
|
-
if (flags.signedIn && Object.keys(conditions).length === 0)
|
|
3732
|
-
refuse2(
|
|
3733
|
-
"--signed-in needs --select-when, --insert-when, --update-when or --delete-when."
|
|
3734
|
-
);
|
|
3735
|
-
return conditions;
|
|
3918
|
+
function axisSummary(domain) {
|
|
3919
|
+
const axes = [
|
|
3920
|
+
domain.ownership,
|
|
3921
|
+
domain.certificate,
|
|
3922
|
+
domain.routing
|
|
3923
|
+
].filter((value) => value !== void 0);
|
|
3924
|
+
return axes.length > 0 ? axes.join(" \xB7 ") : `${domain.status} \xB7 ssl ${domain.sslStatus}`;
|
|
3736
3925
|
}
|
|
3737
|
-
function
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
try {
|
|
3746
|
-
body = JSON.parse(text);
|
|
3747
|
-
} catch {
|
|
3748
|
-
refuse2(`${path} is not JSON.`);
|
|
3749
|
-
}
|
|
3750
|
-
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
3751
|
-
refuse2(`${path} must hold a JSON object.`);
|
|
3752
|
-
const { table: named, ...patch } = body;
|
|
3753
|
-
if (Object.hasOwn(body, "table")) {
|
|
3754
|
-
if (typeof named !== "string")
|
|
3755
|
-
refuse2(`${path}: table must be a string, the table's name.`);
|
|
3756
|
-
if (table !== void 0 && asciiLower(named) !== asciiLower(table))
|
|
3757
|
-
refuse2(
|
|
3758
|
-
`${path} is the policy of ${printable(named)}, not ${printable(table)}. Remove its "table" to set it on ${printable(table)}.`
|
|
3926
|
+
function recordLines(domain) {
|
|
3927
|
+
const instructions = domain.instructions ?? [];
|
|
3928
|
+
if (instructions.length > 0) {
|
|
3929
|
+
const lines2 = [" Required DNS records:"];
|
|
3930
|
+
for (const instruction of instructions) {
|
|
3931
|
+
lines2.push(
|
|
3932
|
+
` ${instruction.type.padEnd(5)} ${instruction.name}`,
|
|
3933
|
+
` host: ${instruction.host.padEnd(24)} value: ${instruction.value}`
|
|
3759
3934
|
);
|
|
3935
|
+
if (instruction.note) lines2.push(` note: ${instruction.note}`);
|
|
3936
|
+
}
|
|
3937
|
+
return lines2;
|
|
3760
3938
|
}
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3939
|
+
if (domain.verification.length === 0) return [];
|
|
3940
|
+
const lines = [" Required DNS records:"];
|
|
3941
|
+
for (const record of domain.verification) {
|
|
3942
|
+
lines.push(
|
|
3943
|
+
` Type: ${record.type.toUpperCase().padEnd(6)} Name: ${record.name}`,
|
|
3944
|
+
` Value: ${record.value}`
|
|
3765
3945
|
);
|
|
3766
|
-
return patch;
|
|
3767
|
-
}
|
|
3768
|
-
function asciiLower(text) {
|
|
3769
|
-
return text.replace(/[A-Z]/g, (letter) => letter.toLowerCase());
|
|
3770
|
-
}
|
|
3771
|
-
function policyPatch(flags, context, table) {
|
|
3772
|
-
if (flags.fromFile !== void 0) {
|
|
3773
|
-
if (OTHER_FLAGS.some(
|
|
3774
|
-
(key) => flags[key] !== void 0 && flags[key] !== false
|
|
3775
|
-
))
|
|
3776
|
-
refuse2(
|
|
3777
|
-
"--from-file takes the whole policy: leave out the other policy options."
|
|
3778
|
-
);
|
|
3779
|
-
if (!context) refuse2(SET_USAGE);
|
|
3780
|
-
return patchFromFile(context, flags.fromFile, table);
|
|
3781
3946
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3947
|
+
return lines;
|
|
3948
|
+
}
|
|
3949
|
+
function domainLines(domain) {
|
|
3950
|
+
const lines = [
|
|
3951
|
+
` ${domain.hostname.padEnd(36)} ${domain.status.padEnd(12)} ssl: ${domain.sslStatus}`,
|
|
3952
|
+
...axisLines(domain)
|
|
3953
|
+
];
|
|
3954
|
+
if (domain.redirectTo) {
|
|
3955
|
+
const status = domain.redirectStatus ?? 308;
|
|
3956
|
+
lines.push(` redirects to ${domain.redirectTo} (${String(status)})`);
|
|
3789
3957
|
}
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
if (
|
|
3793
|
-
patch.ownerColumn = flags.ownerColumn;
|
|
3958
|
+
if (domain.cause) {
|
|
3959
|
+
lines.push(` \u2717 ${domain.cause.message}`);
|
|
3960
|
+
if (domain.cause.fix) lines.push(` \u2192 ${domain.cause.fix}`);
|
|
3794
3961
|
}
|
|
3795
|
-
if (
|
|
3796
|
-
|
|
3797
|
-
|
|
3962
|
+
if (isReady(domain)) return lines;
|
|
3963
|
+
lines.push(...recordLines(domain));
|
|
3964
|
+
const guidance = domain.guidance;
|
|
3965
|
+
if (guidance && guidance.notes.length > 0) {
|
|
3966
|
+
lines.push(
|
|
3967
|
+
guidance.apex ? ` Apex domain (${domain.hostname}):` : " Note:"
|
|
3798
3968
|
);
|
|
3799
|
-
|
|
3800
|
-
}
|
|
3801
|
-
|
|
3802
|
-
contains: "cs",
|
|
3803
|
-
containedBy: "cd",
|
|
3804
|
-
overlaps: "ov"
|
|
3805
|
-
};
|
|
3806
|
-
function isAuthUid(value) {
|
|
3807
|
-
return typeof value === "object" && value !== null && value.auth === "uid";
|
|
3808
|
-
}
|
|
3809
|
-
function nearAuthUid(text) {
|
|
3810
|
-
return text.replace(/\s+/g, "").toLowerCase().replace(/\(\)$/, "") === "auth.uid";
|
|
3811
|
-
}
|
|
3812
|
-
var DECIMAL2 = /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/;
|
|
3813
|
-
function readsAsNumber(value) {
|
|
3814
|
-
const text = String(value);
|
|
3815
|
-
if (!DECIMAL2.test(text) || !Object.is(Number(text), value)) return false;
|
|
3816
|
-
return text.includes(".") || Number.isSafeInteger(value);
|
|
3817
|
-
}
|
|
3818
|
-
function stringText(value, slot) {
|
|
3819
|
-
if (slot === "is") return void 0;
|
|
3820
|
-
if (slot === "like" && value.includes("*")) return void 0;
|
|
3821
|
-
const bare = value !== "" && value.trim() === value && !/[,()"{}\\]/.test(value) && !/^(?:true|false|null)$/i.test(value) && !nearAuthUid(value) && !(slot === "array" && DECIMAL2.test(value));
|
|
3822
|
-
return bare ? value : quoteFilterValue(value);
|
|
3969
|
+
for (const note of guidance.notes) lines.push(` ${note}`);
|
|
3970
|
+
}
|
|
3971
|
+
return lines;
|
|
3823
3972
|
}
|
|
3824
|
-
function
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3973
|
+
async function runDomainsList(context) {
|
|
3974
|
+
const client = await context.client();
|
|
3975
|
+
const projectId = await context.resolveProjectId();
|
|
3976
|
+
const domains = await client.domains.list(projectId);
|
|
3977
|
+
const pending = domains.filter((domain) => !isReady(domain));
|
|
3978
|
+
context.output.result(
|
|
3979
|
+
{ projectId, domains },
|
|
3980
|
+
domains.length === 0 ? ["No custom domains. Add one with `naro domains add example.com`."] : [
|
|
3981
|
+
`Domains (${String(domains.length)}):`,
|
|
3982
|
+
...domains.flatMap(domainLines),
|
|
3983
|
+
// list reads our stored copy; only verify goes and looks again.
|
|
3984
|
+
...pending.length > 0 ? ["", "Re-check DNS now: naro domains verify"] : []
|
|
3985
|
+
]
|
|
3986
|
+
);
|
|
3834
3987
|
}
|
|
3835
|
-
function
|
|
3836
|
-
|
|
3837
|
-
|
|
3988
|
+
async function runDomainsAdd(context) {
|
|
3989
|
+
const [hostname] = context.positionalOrThrow(
|
|
3990
|
+
1,
|
|
3991
|
+
"Usage: naro domains add <hostname> [--no-www]"
|
|
3992
|
+
);
|
|
3993
|
+
const client = await context.client();
|
|
3994
|
+
const projectId = await context.resolveProjectId();
|
|
3995
|
+
const withWww = context.flags.noWww !== true;
|
|
3996
|
+
const domain = await client.domains.add(projectId, hostname, {
|
|
3997
|
+
withWww
|
|
3998
|
+
});
|
|
3999
|
+
const companion = domain.companion;
|
|
4000
|
+
const apex = domain.apex ?? domain.guidance?.apex ?? false;
|
|
4001
|
+
const alternative = domain.guidance?.alternative ?? `www.${domain.hostname}`;
|
|
4002
|
+
context.output.result(
|
|
4003
|
+
{ projectId, domain, withWww, ...companion ? { companion } : {} },
|
|
4004
|
+
[
|
|
4005
|
+
`\u2705 Domain added: ${domain.hostname}`,
|
|
4006
|
+
"",
|
|
4007
|
+
...domainLines(domain),
|
|
4008
|
+
...companion ? [
|
|
4009
|
+
"",
|
|
4010
|
+
` Also registered ${companion.hostname} \u2014 an apex domain often cannot hold`,
|
|
4011
|
+
" the CNAME above, and this form always can.",
|
|
4012
|
+
"",
|
|
4013
|
+
...domainLines(companion)
|
|
4014
|
+
] : [],
|
|
4015
|
+
...!companion && apex && !withWww ? [
|
|
4016
|
+
"",
|
|
4017
|
+
` --no-www: ${domain.hostname} was registered on its own. If your registrar`,
|
|
4018
|
+
` refuses the record above, run \`naro domains add ${alternative}\`.`
|
|
4019
|
+
] : [],
|
|
4020
|
+
"",
|
|
4021
|
+
isReady(domain) ? "The domain is active." : "Status: Pending verification \u2014 add the DNS records above, then run `naro domains verify`."
|
|
4022
|
+
]
|
|
4023
|
+
);
|
|
3838
4024
|
}
|
|
3839
|
-
function
|
|
3840
|
-
if (
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
4025
|
+
async function verifyAll(client, projectId, hostname, auto = false) {
|
|
4026
|
+
if (hostname !== void 0)
|
|
4027
|
+
return [
|
|
4028
|
+
await client.domains.verifyByHostname(projectId, hostname, { auto })
|
|
4029
|
+
];
|
|
4030
|
+
const listed = await client.domains.list(projectId);
|
|
4031
|
+
const verified = [];
|
|
4032
|
+
for (const domain of listed)
|
|
4033
|
+
verified.push(await client.domains.verify(projectId, domain.id, { auto }));
|
|
4034
|
+
return verified;
|
|
3844
4035
|
}
|
|
3845
|
-
function
|
|
3846
|
-
const
|
|
3847
|
-
|
|
3848
|
-
const
|
|
3849
|
-
const
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
4036
|
+
async function runDomainsVerify(context, overrides = {}) {
|
|
4037
|
+
const deps = { ...defaults3, ...overrides };
|
|
4038
|
+
const client = await context.client();
|
|
4039
|
+
const projectId = await context.resolveProjectId();
|
|
4040
|
+
const hostname = context.positionals[0];
|
|
4041
|
+
const wait = context.flags.wait === true;
|
|
4042
|
+
const progress = (line) => {
|
|
4043
|
+
if (!context.flags.json) context.output.log(line);
|
|
4044
|
+
};
|
|
4045
|
+
let domains = await verifyAll(client, projectId, hostname);
|
|
4046
|
+
if (domains.length === 0) {
|
|
4047
|
+
context.output.result({ projectId, domains, ready: false }, [
|
|
4048
|
+
"No custom domains. Add one with `naro domains add example.com`."
|
|
4049
|
+
]);
|
|
4050
|
+
return;
|
|
3857
4051
|
}
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
4052
|
+
let timedOut = false;
|
|
4053
|
+
let abandoned = false;
|
|
4054
|
+
if (wait && !domains.every(isReady)) {
|
|
4055
|
+
const deadline = deps.now() + WAIT_TIMEOUT_MS;
|
|
4056
|
+
let delay = FIRST_DELAY_MS;
|
|
4057
|
+
for (; ; ) {
|
|
4058
|
+
const pending = domains.filter((domain) => !isReady(domain));
|
|
4059
|
+
if (pending.length > 0 && pending.every(isTerminal)) {
|
|
4060
|
+
abandoned = true;
|
|
4061
|
+
break;
|
|
4062
|
+
}
|
|
4063
|
+
const remaining = deadline - deps.now();
|
|
4064
|
+
if (remaining <= 0) {
|
|
4065
|
+
timedOut = true;
|
|
4066
|
+
break;
|
|
4067
|
+
}
|
|
4068
|
+
for (const domain of pending)
|
|
4069
|
+
progress(
|
|
4070
|
+
isTerminal(domain) ? `\u26A0 ${domain.hostname}: ${axisSummary(domain)} \u2014 waiting cannot fix this one` : `\u25B8 ${domain.hostname}: ${axisSummary(domain)}`
|
|
4071
|
+
);
|
|
4072
|
+
await deps.sleep(Math.min(delay, remaining));
|
|
4073
|
+
delay = Math.min(delay * 2, MAX_DELAY_MS);
|
|
4074
|
+
domains = await verifyAll(client, projectId, hostname, true);
|
|
4075
|
+
if (domains.every(isReady)) break;
|
|
4076
|
+
}
|
|
3876
4077
|
}
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
4078
|
+
const ready = domains.every(isReady);
|
|
4079
|
+
const body = domains.flatMap(domainLines);
|
|
4080
|
+
if (!ready) {
|
|
4081
|
+
for (const line of body) progress(line);
|
|
4082
|
+
const notReady = domains.filter((domain) => !isReady(domain));
|
|
4083
|
+
const names = notReady.map((domain) => domain.hostname).join(", ");
|
|
4084
|
+
const details = { projectId, domains, ready: false };
|
|
4085
|
+
if (timedOut) {
|
|
4086
|
+
const cause2 = notReady[0]?.cause;
|
|
4087
|
+
throw new NaroError(
|
|
4088
|
+
"TIMEOUT",
|
|
4089
|
+
`${names} did not go live within 10 minutes.${cause2 ? ` ${cause2.message}` : ""} DNS changes can take longer to propagate \u2014 re-run \`naro domains verify\` later.`,
|
|
4090
|
+
{ details }
|
|
4091
|
+
);
|
|
4092
|
+
}
|
|
4093
|
+
if (abandoned) {
|
|
4094
|
+
const blocked = domains.find(isTerminal);
|
|
4095
|
+
throw new NaroError(
|
|
4096
|
+
blocked?.cause?.code ?? "DOMAIN_VERIFICATION_FAILED",
|
|
4097
|
+
blocked?.cause?.message ?? `${names} cannot be verified (status: ${blocked?.status ?? "unknown"}). Remove it and add it again.`,
|
|
4098
|
+
{ details }
|
|
4099
|
+
);
|
|
4100
|
+
}
|
|
4101
|
+
const cause = notReady[0]?.cause;
|
|
4102
|
+
throw new NaroError(
|
|
4103
|
+
cause?.code ?? "DOMAIN_NOT_READY",
|
|
4104
|
+
`${names} is not live yet.${cause ? ` ${cause.message}` : ""} Add the records above, then run \`naro domains verify --wait\`.`,
|
|
4105
|
+
{ details }
|
|
4106
|
+
);
|
|
3881
4107
|
}
|
|
3882
|
-
|
|
4108
|
+
context.output.result({ projectId, domains, ready }, [
|
|
4109
|
+
...body,
|
|
4110
|
+
"",
|
|
4111
|
+
domains.length === 1 ? `\u2705 ${domains[0]?.hostname ?? ""} is live.` : "\u2705 Every domain is live."
|
|
4112
|
+
]);
|
|
3883
4113
|
}
|
|
3884
|
-
function
|
|
3885
|
-
|
|
4114
|
+
async function runDomainsRemove(context) {
|
|
4115
|
+
const [hostname] = context.positionalOrThrow(
|
|
4116
|
+
1,
|
|
4117
|
+
"Usage: naro domains remove <hostname>"
|
|
4118
|
+
);
|
|
4119
|
+
await confirmOrAbort(
|
|
4120
|
+
context,
|
|
4121
|
+
`Remove ${String(hostname)}? Traffic to it will stop routing here.`
|
|
4122
|
+
);
|
|
4123
|
+
const client = await context.client();
|
|
4124
|
+
const projectId = await context.resolveProjectId();
|
|
4125
|
+
await client.domains.remove(projectId, hostname);
|
|
4126
|
+
context.output.result({ projectId, hostname, removed: true }, [
|
|
4127
|
+
`\u2705 Removed ${String(hostname)}`
|
|
4128
|
+
]);
|
|
3886
4129
|
}
|
|
3887
|
-
|
|
3888
|
-
|
|
4130
|
+
|
|
4131
|
+
// src/commands/env.ts
|
|
4132
|
+
import { existsSync, writeFileSync as writeFileSync4 } from "fs";
|
|
4133
|
+
import { join as join4 } from "path";
|
|
4134
|
+
var TARGETS = /* @__PURE__ */ new Set(["production", "preview", "all"]);
|
|
4135
|
+
var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
4136
|
+
function parseTarget(value) {
|
|
4137
|
+
if (value === void 0) return "all";
|
|
4138
|
+
if (!TARGETS.has(value))
|
|
4139
|
+
throw new NaroError(
|
|
4140
|
+
"VALIDATION_FAILED",
|
|
4141
|
+
"Environment must be production, preview or all."
|
|
4142
|
+
);
|
|
4143
|
+
return value;
|
|
3889
4144
|
}
|
|
3890
|
-
function
|
|
3891
|
-
if (
|
|
3892
|
-
return
|
|
4145
|
+
function maskedValue(row) {
|
|
4146
|
+
if (!row.isSecret) return row.value ?? "";
|
|
4147
|
+
return "\u2022\u2022\u2022\u2022\u2022\u2022 (secret)";
|
|
3893
4148
|
}
|
|
3894
|
-
function
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
const
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
4149
|
+
async function runEnvList(context) {
|
|
4150
|
+
const client = await context.client();
|
|
4151
|
+
const projectId = await context.resolveProjectId();
|
|
4152
|
+
const reveal = context.flags.reveal;
|
|
4153
|
+
const rows = await client.env.list(projectId, { reveal });
|
|
4154
|
+
context.output.result(
|
|
4155
|
+
{
|
|
4156
|
+
projectId,
|
|
4157
|
+
envVars: rows.map((row) => ({
|
|
4158
|
+
id: row.id,
|
|
4159
|
+
key: row.key,
|
|
4160
|
+
target: row.target,
|
|
4161
|
+
isSecret: row.isSecret,
|
|
4162
|
+
value: row.isSecret && !reveal ? null : row.value
|
|
4163
|
+
}))
|
|
4164
|
+
},
|
|
4165
|
+
[
|
|
4166
|
+
`Environment variables (${String(rows.length)}):`,
|
|
4167
|
+
...rows.map(
|
|
4168
|
+
(row) => ` ${row.key.padEnd(32)} ${row.target.padEnd(12)} ${reveal ? row.value ?? "" : maskedValue(row)}`
|
|
4169
|
+
)
|
|
4170
|
+
]
|
|
4171
|
+
);
|
|
3908
4172
|
}
|
|
3909
|
-
function
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
...POLICY_ACTIONS.map((action) => cellOf(policy[action])),
|
|
3914
|
-
policy.ownerColumn === null ? "-" : printable(policy.ownerColumn)
|
|
3915
|
-
]);
|
|
3916
|
-
const widths = header.map(
|
|
3917
|
-
(title, index) => Math.max(title.length, ...rows.map((row) => row[index]?.length ?? 0))
|
|
4173
|
+
async function runEnvAdd(context) {
|
|
4174
|
+
context.positionalOrThrow(
|
|
4175
|
+
1,
|
|
4176
|
+
"Usage: naro env add KEY [production|preview|all]"
|
|
3918
4177
|
);
|
|
3919
|
-
const
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
4178
|
+
const [key, targetArg] = context.positionals;
|
|
4179
|
+
if (!key || !KEY_RE.test(key) || key.length > 256)
|
|
4180
|
+
throw new NaroError(
|
|
4181
|
+
"VALIDATION_FAILED",
|
|
4182
|
+
"Keys must look like MY_VARIABLE (letters, digits, underscore)."
|
|
4183
|
+
);
|
|
4184
|
+
const target = parseTarget(targetArg);
|
|
4185
|
+
let value;
|
|
4186
|
+
if (!process.stdin.isTTY) {
|
|
4187
|
+
value = await readStdin();
|
|
4188
|
+
} else {
|
|
4189
|
+
value = await context.prompter.askHidden(`? What's the value of ${key}? `);
|
|
4190
|
+
}
|
|
4191
|
+
if (value.length === 0)
|
|
4192
|
+
throw new NaroError("VALIDATION_FAILED", `No value provided for ${key}.`);
|
|
4193
|
+
const isSecret = !key.startsWith("NEXT_PUBLIC_");
|
|
4194
|
+
const client = await context.client();
|
|
4195
|
+
const projectId = await context.resolveProjectId();
|
|
4196
|
+
await client.env.set(projectId, [{ key, value, target, isSecret }]);
|
|
4197
|
+
context.output.result({ projectId, key, target, isSecret }, [
|
|
4198
|
+
`\u2705 Added ${key} (${target}${isSecret ? ", secret" : ""})`
|
|
4199
|
+
]);
|
|
4200
|
+
}
|
|
4201
|
+
async function runEnvRemove(context) {
|
|
4202
|
+
context.positionalOrThrow(
|
|
4203
|
+
1,
|
|
4204
|
+
"Usage: naro env remove KEY [production|preview|all]"
|
|
3923
4205
|
);
|
|
3924
|
-
const
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
})
|
|
3930
|
-
)
|
|
4206
|
+
const [key, targetArg] = context.positionals;
|
|
4207
|
+
const target = targetArg === void 0 ? void 0 : parseTarget(targetArg);
|
|
4208
|
+
await confirmOrAbort(
|
|
4209
|
+
context,
|
|
4210
|
+
`Remove ${String(key)}${target ? ` (${target})` : ""} from this project?`
|
|
3931
4211
|
);
|
|
3932
|
-
|
|
3933
|
-
const
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
""
|
|
3937
|
-
|
|
3938
|
-
];
|
|
4212
|
+
const client = await context.client();
|
|
4213
|
+
const projectId = await context.resolveProjectId();
|
|
4214
|
+
const removed = await client.env.remove(projectId, key, target);
|
|
4215
|
+
context.output.result({ projectId, key, removed }, [
|
|
4216
|
+
`\u2705 Removed ${String(key)} (${String(removed)} entr${removed === 1 ? "y" : "ies"})`
|
|
4217
|
+
]);
|
|
3939
4218
|
}
|
|
3940
|
-
async function
|
|
4219
|
+
async function runEnvPull(context) {
|
|
4220
|
+
const file = context.positionals[0] ?? ".env.local";
|
|
4221
|
+
const surface = context.flags.env === "production" ? "production" : "preview";
|
|
4222
|
+
if (context.flags.env && !["production", "preview"].includes(context.flags.env))
|
|
4223
|
+
throw new NaroError(
|
|
4224
|
+
"VALIDATION_FAILED",
|
|
4225
|
+
"--env must be production or preview."
|
|
4226
|
+
);
|
|
3941
4227
|
const client = await context.client();
|
|
3942
4228
|
const projectId = await context.resolveProjectId();
|
|
3943
|
-
const
|
|
4229
|
+
const rows = await client.env.list(projectId, { reveal: true });
|
|
4230
|
+
const selected = rows.filter(
|
|
4231
|
+
(row) => row.target === "all" || row.target === surface
|
|
4232
|
+
);
|
|
4233
|
+
const vars = Object.fromEntries(
|
|
4234
|
+
selected.map((row) => [row.key, row.value ?? ""])
|
|
4235
|
+
);
|
|
4236
|
+
const path = join4(context.cwd, file);
|
|
4237
|
+
if (existsSync(path) && isInteractive() && !context.flags.yes && !context.flags.json) {
|
|
4238
|
+
const confirmed = await context.prompter.confirm(`Overwrite ${file}?`);
|
|
4239
|
+
if (!confirmed) throw new NaroError("CANCELED", "Canceled.");
|
|
4240
|
+
}
|
|
4241
|
+
writeFileSync4(
|
|
4242
|
+
path,
|
|
4243
|
+
`# Pulled from Naro (${surface}) \u2014 do not commit secrets.
|
|
4244
|
+
${serializeDotenv(vars)}`,
|
|
4245
|
+
{ mode: 384 }
|
|
4246
|
+
);
|
|
3944
4247
|
context.output.result(
|
|
3945
|
-
{ projectId,
|
|
3946
|
-
|
|
3947
|
-
"No policies yet: every table denies anon and signed-in requests.",
|
|
3948
|
-
"Open one with `naro policy set <table> --select public`."
|
|
3949
|
-
] : [
|
|
3950
|
-
...policyTable(policies),
|
|
3951
|
-
"",
|
|
3952
|
-
"Tables not listed deny anon and signed-in requests. The service key skips policies."
|
|
3953
|
-
]
|
|
4248
|
+
{ projectId, file, environment: surface, count: selected.length },
|
|
4249
|
+
[`\u2705 Pulled ${String(selected.length)} variable(s) into ${file}`]
|
|
3954
4250
|
);
|
|
3955
4251
|
}
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
4252
|
+
|
|
4253
|
+
// src/commands/gen.ts
|
|
4254
|
+
import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
|
|
4255
|
+
import { dirname as dirname2, resolve as resolve4 } from "path";
|
|
4256
|
+
var LANG_MESSAGE = "naro gen types writes TypeScript only. Pass --lang typescript, or leave --lang out.";
|
|
4257
|
+
async function runGenTypes(context) {
|
|
4258
|
+
const { lang } = context.flags;
|
|
4259
|
+
if (lang !== void 0 && lang !== "typescript")
|
|
4260
|
+
throw new NaroError("VALIDATION_FAILED", LANG_MESSAGE);
|
|
3959
4261
|
const client = await context.client();
|
|
3960
4262
|
const projectId = await context.resolveProjectId();
|
|
3961
|
-
const
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
4263
|
+
const tables = await client.db.tables(projectId);
|
|
4264
|
+
const read = await policiesForTypes(client, projectId);
|
|
4265
|
+
const types = generateTypes(
|
|
4266
|
+
tables,
|
|
4267
|
+
read.policies === void 0 ? {} : { policies: read.policies }
|
|
4268
|
+
);
|
|
4269
|
+
const undescribed = tables.flatMap(
|
|
4270
|
+
(table) => table.error === void 0 ? [] : [{ name: table.name, error: table.error }]
|
|
3965
4271
|
);
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4272
|
+
for (const { name, error } of undescribed)
|
|
4273
|
+
context.output.warn(
|
|
4274
|
+
`\u26A0 ${printable(name)} could not be described and was generated as an empty interface: ${printable(error)}`
|
|
4275
|
+
);
|
|
4276
|
+
if (read.policies === void 0)
|
|
4277
|
+
context.output.warn(
|
|
4278
|
+
`\u26A0 The backend's policies could not be read (${printable(read.reason)}), so owner columns stay required in Insert.`
|
|
4279
|
+
);
|
|
4280
|
+
const extra = {
|
|
4281
|
+
...undescribed.length > 0 ? { undescribed } : {},
|
|
4282
|
+
...read.policies === void 0 ? { policies: "unavailable", policiesReason: read.reason } : {}
|
|
4283
|
+
};
|
|
4284
|
+
if (context.flags.output) {
|
|
4285
|
+
const path = resolve4(context.cwd, context.flags.output);
|
|
4286
|
+
mkdirSync4(dirname2(path), { recursive: true });
|
|
4287
|
+
writeFileSync5(path, types);
|
|
4288
|
+
context.output.result(
|
|
4289
|
+
{
|
|
4290
|
+
projectId,
|
|
4291
|
+
output: context.flags.output,
|
|
4292
|
+
tables: tables.length,
|
|
4293
|
+
...extra
|
|
4294
|
+
},
|
|
4295
|
+
[
|
|
4296
|
+
`\u2705 Wrote types for ${String(tables.length)} table(s) to ${context.flags.output}`
|
|
4297
|
+
]
|
|
4298
|
+
);
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
context.output.result({ projectId, types, ...extra }, [
|
|
4302
|
+
types.replace(/\n$/, "")
|
|
3969
4303
|
]);
|
|
3970
4304
|
}
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
4305
|
+
|
|
4306
|
+
// src/commands/logs.ts
|
|
4307
|
+
var FOLLOW_INTERVAL_MS = 3e3;
|
|
4308
|
+
function sleep(ms) {
|
|
4309
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
4310
|
+
}
|
|
4311
|
+
function formatBuildLog(log) {
|
|
4312
|
+
return `[${log.stage}] ${log.level === "ERROR" ? "\u2717 " : ""}${log.message}`;
|
|
4313
|
+
}
|
|
4314
|
+
function formatRuntimeLog(entry) {
|
|
4315
|
+
const time = new Date(entry.ts).toISOString();
|
|
4316
|
+
const request = entry.method && entry.path ? ` ${entry.method} ${entry.path}${entry.status ? ` ${String(entry.status)}` : ""}` : "";
|
|
4317
|
+
return `${time} [${entry.level}]${request} ${entry.message}`.trimEnd();
|
|
4318
|
+
}
|
|
4319
|
+
async function latestProductionDeploymentId(client, projectId) {
|
|
4320
|
+
const deployments = await client.deployments.list(projectId);
|
|
4321
|
+
const latest = deployments.find(
|
|
4322
|
+
(deployment) => deployment.status === "READY"
|
|
4323
|
+
);
|
|
4324
|
+
const fallback = deployments[0];
|
|
4325
|
+
const chosen = latest ?? fallback;
|
|
4326
|
+
if (!chosen)
|
|
4327
|
+
throw new NaroError(
|
|
4328
|
+
"DEPLOYMENT_NOT_FOUND",
|
|
4329
|
+
"This project has no deployments yet."
|
|
4330
|
+
);
|
|
4331
|
+
return chosen.id;
|
|
4332
|
+
}
|
|
4333
|
+
async function buildLogs(context, client, deploymentId) {
|
|
4334
|
+
if (!context.flags.follow) {
|
|
4335
|
+
const detail = await client.deployments.get(deploymentId);
|
|
4336
|
+
context.output.result(
|
|
4337
|
+
{ deploymentId, type: "build", logs: detail.logs },
|
|
4338
|
+
detail.logs.map(formatBuildLog)
|
|
4339
|
+
);
|
|
4340
|
+
return;
|
|
4341
|
+
}
|
|
4342
|
+
let lastSequence = -1;
|
|
4343
|
+
for (; ; ) {
|
|
4344
|
+
const detail = await client.deployments.get(deploymentId);
|
|
4345
|
+
for (const log of detail.logs) {
|
|
4346
|
+
if (log.sequence > lastSequence) {
|
|
4347
|
+
lastSequence = log.sequence;
|
|
4348
|
+
context.output.log(formatBuildLog(log));
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
if (isTerminalStatus(detail.deployment.status)) {
|
|
4352
|
+
context.output.result(
|
|
4353
|
+
{
|
|
4354
|
+
deploymentId,
|
|
4355
|
+
type: "build",
|
|
4356
|
+
status: detail.deployment.status,
|
|
4357
|
+
logs: detail.logs
|
|
4358
|
+
},
|
|
4359
|
+
[`\u2014 deployment ${detail.deployment.status} \u2014`]
|
|
4360
|
+
);
|
|
4361
|
+
return;
|
|
4362
|
+
}
|
|
4363
|
+
await sleep(FOLLOW_INTERVAL_MS);
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
async function runtimeLogs(context, client, projectId) {
|
|
4367
|
+
const environment = context.flags.env === "preview" ? "preview" : "production";
|
|
4368
|
+
if (context.flags.env && !["production", "preview"].includes(context.flags.env))
|
|
4369
|
+
throw new NaroError(
|
|
4370
|
+
"VALIDATION_FAILED",
|
|
4371
|
+
"--env must be production or preview."
|
|
4372
|
+
);
|
|
4373
|
+
const options = {
|
|
4374
|
+
environment,
|
|
4375
|
+
...context.flags.level ? { level: context.flags.level } : {}
|
|
4376
|
+
};
|
|
4377
|
+
if (!context.flags.follow) {
|
|
4378
|
+
const entries = await client.logs.runtime(projectId, options);
|
|
4379
|
+
context.output.result(
|
|
4380
|
+
{ projectId, type: "runtime", environment, entries },
|
|
4381
|
+
entries.length > 0 ? entries.map(formatRuntimeLog) : ["(no runtime logs yet \u2014 the worker logs on each request)"]
|
|
4382
|
+
);
|
|
4383
|
+
return;
|
|
4384
|
+
}
|
|
4385
|
+
context.output.log(
|
|
4386
|
+
`Following ${environment} runtime logs (Ctrl-C to stop)...`
|
|
3976
4387
|
);
|
|
4388
|
+
let lastTs = -1;
|
|
4389
|
+
let boundary = /* @__PURE__ */ new Set();
|
|
4390
|
+
for (; ; ) {
|
|
4391
|
+
const entries = await client.logs.runtime(projectId, options);
|
|
4392
|
+
for (const entry of entries) {
|
|
4393
|
+
if (entry.ts < lastTs) continue;
|
|
4394
|
+
const key = JSON.stringify(entry);
|
|
4395
|
+
if (entry.ts === lastTs) {
|
|
4396
|
+
if (boundary.has(key)) continue;
|
|
4397
|
+
} else {
|
|
4398
|
+
lastTs = entry.ts;
|
|
4399
|
+
boundary = /* @__PURE__ */ new Set();
|
|
4400
|
+
}
|
|
4401
|
+
boundary.add(key);
|
|
4402
|
+
context.output.log(formatRuntimeLog(entry));
|
|
4403
|
+
}
|
|
4404
|
+
await sleep(FOLLOW_INTERVAL_MS);
|
|
4405
|
+
}
|
|
4406
|
+
}
|
|
4407
|
+
async function runLogs(context) {
|
|
3977
4408
|
const client = await context.client();
|
|
3978
|
-
const
|
|
3979
|
-
const
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
4409
|
+
const [deploymentId] = context.positionals;
|
|
4410
|
+
const type = context.flags.type ?? (deploymentId !== void 0 ? "build" : "runtime");
|
|
4411
|
+
if (type !== "build" && type !== "runtime")
|
|
4412
|
+
throw new NaroError(
|
|
4413
|
+
"VALIDATION_FAILED",
|
|
4414
|
+
"--type must be build or runtime."
|
|
4415
|
+
);
|
|
4416
|
+
if (type === "runtime") {
|
|
4417
|
+
if (deploymentId)
|
|
4418
|
+
throw new NaroError(
|
|
4419
|
+
"VALIDATION_FAILED",
|
|
4420
|
+
"Runtime logs are per project, not per deployment. Drop the deployment id or use --type build."
|
|
4421
|
+
);
|
|
4422
|
+
const projectId = await context.resolveProjectId();
|
|
4423
|
+
await runtimeLogs(context, client, projectId);
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
const target = deploymentId ?? await latestProductionDeploymentId(
|
|
4427
|
+
client,
|
|
4428
|
+
await context.resolveProjectId()
|
|
4429
|
+
);
|
|
4430
|
+
await buildLogs(context, client, target);
|
|
3983
4431
|
}
|
|
3984
4432
|
|
|
3985
4433
|
// src/commands/projects.ts
|
|
@@ -4191,9 +4639,16 @@ Usage:
|
|
|
4191
4639
|
naro backend keys [--reveal] API keys; --reveal prints the service key (a secret; needs database write)
|
|
4192
4640
|
naro backend keys rotate --anon|--service
|
|
4193
4641
|
Replace a key; the old one stops working at once (asks first)
|
|
4194
|
-
naro backend auth Sign-in settings: email, Google, GitHub, redirects, callback URLs
|
|
4642
|
+
naro backend auth Sign-in settings: email, Google, GitHub, redirects, callback URLs,
|
|
4643
|
+
sign-ups and the minimum password length
|
|
4644
|
+
naro backend auth signups <on|off>
|
|
4645
|
+
off: nobody signs up on their own, by email or Google/GitHub;
|
|
4646
|
+
the accounts you create (naro backend users create) sign in
|
|
4647
|
+
naro backend auth password --min-length <n>
|
|
4648
|
+
The fewest characters a new password takes (6-128; 6 by default)
|
|
4195
4649
|
naro backend auth email set --provider resend --from "App <no-reply@example.com>" --api-key-stdin
|
|
4196
4650
|
Confirmation and reset emails from your Resend account
|
|
4651
|
+
(email and password sign-in needs no setup)
|
|
4197
4652
|
naro backend auth email remove Turn email off (asks first)
|
|
4198
4653
|
naro backend auth oauth set <google|github> --client-id <id> --client-secret-stdin
|
|
4199
4654
|
naro backend auth oauth remove <google|github> Turn a provider off (asks first)
|
|
@@ -4205,15 +4660,31 @@ Usage:
|
|
|
4205
4660
|
Exits 1, changing nothing, when <url> is not in the list
|
|
4206
4661
|
naro backend auth require-verification <on|off>
|
|
4207
4662
|
Refuse sign-in until the email is confirmed (needs email)
|
|
4208
|
-
naro
|
|
4663
|
+
naro backend users The project's users: id, username, email, verified, created
|
|
4664
|
+
naro backend users create --username <name> --password-stdin
|
|
4665
|
+
Create a confirmed account (--email <address> instead of or with
|
|
4666
|
+
--username); works while sign-ups are closed. --password <p>
|
|
4667
|
+
works too, but lands in shell history
|
|
4668
|
+
naro backend users delete <id|username|email>
|
|
4669
|
+
Delete a user and end its sessions (asks first; --yes to skip)
|
|
4670
|
+
naro policy list [--table <t>] Row-level security policies as CREATE POLICY (no policy = deny)
|
|
4671
|
+
naro policy create "CREATE POLICY <name> ON <table> \u2026" | --file <policies.sql>
|
|
4672
|
+
Postgres's CREATE POLICY: AS PERMISSIVE|RESTRICTIVE,
|
|
4673
|
+
FOR ALL|SELECT|INSERT|UPDATE|DELETE, TO anon|authenticated|public,
|
|
4674
|
+
USING (\u2026), WITH CHECK (\u2026); a file may hold DROP POLICY too
|
|
4675
|
+
(run in order, stops at the first refusal)
|
|
4676
|
+
[--allow-unindexed] Accept a subquery on the outer row that no index serves
|
|
4677
|
+
(refused otherwise, naming the index to create)
|
|
4678
|
+
naro policy drop <name> --table <t> DROP POLICY (asks first)
|
|
4209
4679
|
naro policy set <table> [--select <rule>] [--insert <rule>] [--update <rule>] [--delete <rule>]
|
|
4210
4680
|
[--owner <column>] Rules: none | public | authenticated | owner (owner needs --owner)
|
|
4211
4681
|
[--select-when <filter>] [--insert-when <filter>] [--update-when <filter>]
|
|
4212
4682
|
[--update-check <filter>] [--delete-when <filter>] [--signed-in]
|
|
4213
4683
|
Or a condition per action, in Supabase filter syntax;
|
|
4214
4684
|
auth.uid() is the signed-in user's id
|
|
4215
|
-
[--from-file <policy.json>] Or the whole policy as JSON
|
|
4216
|
-
|
|
4685
|
+
[--from-file <policy.json>] Or the whole policy as JSON. Writes the policies naro_select,
|
|
4686
|
+
naro_insert, naro_update, naro_delete (shorthand for CREATE POLICY)
|
|
4687
|
+
naro policy remove <table> Drop every policy on it: back to deny (asks first)
|
|
4217
4688
|
|
|
4218
4689
|
naro promote <deployment-id> Publish a build to production
|
|
4219
4690
|
naro rollback [deployment-id] Roll production back to the previous build
|
|
@@ -4230,7 +4701,7 @@ Global options:
|
|
|
4230
4701
|
--prod Deploy/target production
|
|
4231
4702
|
--wait Poll until the domain goes live (naro domains verify)
|
|
4232
4703
|
--no-www Register the apex alone (naro domains add)
|
|
4233
|
-
--file <path> SQL file to run (naro db query)
|
|
4704
|
+
--file <path> SQL file to run (naro db query, naro policy create)
|
|
4234
4705
|
--dry-run Show what would change without changing it (naro db push, db restore, db reset, db table, db index)
|
|
4235
4706
|
--output <file> Write to a file instead of stdout (naro gen types, db dump)
|
|
4236
4707
|
--lang <lang> Language of the types; typescript, the default, is the only one (naro gen types)
|
|
@@ -4238,7 +4709,8 @@ Global options:
|
|
|
4238
4709
|
--strict Fail on warnings too, not only errors (naro db lint)
|
|
4239
4710
|
--schema-only Dump the schema without rows (naro db dump)
|
|
4240
4711
|
--data-only Dump rows without the schema (naro db dump)
|
|
4241
|
-
--table <a,b> Dump only these tables, comma-separated (naro db dump)
|
|
4712
|
+
--table <a,b> Dump only these tables, comma-separated (naro db dump); the policy's table
|
|
4713
|
+
(naro policy list, naro policy drop)
|
|
4242
4714
|
--bookmark <id> Restore point: a bookmark from db dump or db restore (naro db restore)
|
|
4243
4715
|
--timestamp <time> Restore point: RFC 3339 with a timezone, or Unix seconds (naro db restore)
|
|
4244
4716
|
--status <s> applied or reverted (naro migration repair)
|
|
@@ -4249,6 +4721,11 @@ Global options:
|
|
|
4249
4721
|
--client-id <id> The OAuth app's client ID (naro backend auth oauth set)
|
|
4250
4722
|
--api-key-stdin | --client-secret-stdin
|
|
4251
4723
|
Read the secret from stdin (a pipe, or a hidden prompt); never from argv
|
|
4724
|
+
--username <name> | --email <address>
|
|
4725
|
+
The new user's username (3-30 of a-z, 0-9, _ and .) and/or email (naro backend users create)
|
|
4726
|
+
--password-stdin Read the new user's password from stdin (a pipe, or a hidden prompt)
|
|
4727
|
+
--password <p> The password in argv: works, but lands in shell history (naro backend users create)
|
|
4728
|
+
--min-length <n> Fewest characters of a new password, 6-128 (naro backend auth password)
|
|
4252
4729
|
--select <rule> Rule for reads; --insert, --update, --delete likewise (naro policy set)
|
|
4253
4730
|
--owner <column> Column that holds the signed-in user's id, for the owner rule (naro policy set, db table create)
|
|
4254
4731
|
--select-when <filter> Condition for reads, e.g. 'user_id.eq.auth.uid()'; --insert-when (what a new
|
|
@@ -4315,6 +4792,29 @@ function routeBackendAuth(words) {
|
|
|
4315
4792
|
handler: runBackendAuthRequireVerification,
|
|
4316
4793
|
positionals: words.slice(1)
|
|
4317
4794
|
};
|
|
4795
|
+
// Spec 2026-09-26 §2.1, §2.4.
|
|
4796
|
+
case "signups":
|
|
4797
|
+
return { handler: runBackendAuthSignups, positionals: words.slice(1) };
|
|
4798
|
+
case "password":
|
|
4799
|
+
return { handler: runBackendAuthPassword, positionals: words.slice(1) };
|
|
4800
|
+
default:
|
|
4801
|
+
return null;
|
|
4802
|
+
}
|
|
4803
|
+
}
|
|
4804
|
+
function routeBackendUsers(words) {
|
|
4805
|
+
const [action] = words;
|
|
4806
|
+
const rest = words.slice(1);
|
|
4807
|
+
switch (action) {
|
|
4808
|
+
case void 0:
|
|
4809
|
+
case "list":
|
|
4810
|
+
case "ls":
|
|
4811
|
+
return rest.length === 0 ? { handler: runBackendUsersList, positionals: [] } : null;
|
|
4812
|
+
case "create":
|
|
4813
|
+
return { handler: runBackendUsersCreate, positionals: rest };
|
|
4814
|
+
case "delete":
|
|
4815
|
+
case "remove":
|
|
4816
|
+
case "rm":
|
|
4817
|
+
return { handler: runBackendUsersDelete, positionals: rest };
|
|
4318
4818
|
default:
|
|
4319
4819
|
return null;
|
|
4320
4820
|
}
|
|
@@ -4484,6 +4984,8 @@ function route(parsed) {
|
|
|
4484
4984
|
return null;
|
|
4485
4985
|
case "auth":
|
|
4486
4986
|
return routeBackendAuth(positionals.slice(1));
|
|
4987
|
+
case "users":
|
|
4988
|
+
return routeBackendUsers(positionals.slice(1));
|
|
4487
4989
|
default:
|
|
4488
4990
|
return null;
|
|
4489
4991
|
}
|
|
@@ -4495,6 +4997,13 @@ function route(parsed) {
|
|
|
4495
4997
|
return { handler: runPolicyList, positionals: positionals.slice(1) };
|
|
4496
4998
|
case "set":
|
|
4497
4999
|
return { handler: runPolicySet, positionals: positionals.slice(1) };
|
|
5000
|
+
case "create":
|
|
5001
|
+
return {
|
|
5002
|
+
handler: runPolicyCreate,
|
|
5003
|
+
positionals: positionals.slice(1)
|
|
5004
|
+
};
|
|
5005
|
+
case "drop":
|
|
5006
|
+
return { handler: runPolicyDrop, positionals: positionals.slice(1) };
|
|
4498
5007
|
case "remove":
|
|
4499
5008
|
case "rm":
|
|
4500
5009
|
return {
|