naro-cli 0.17.0 → 0.19.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 +384 -24
- package/dist/{chunk-PYRN5XR6.js → chunk-DEEFHVDZ.js} +1616 -230
- package/dist/deploy.d.ts +38 -1
- package/dist/deploy.js +8 -4
- package/dist/env-consent.d.ts +9 -1
- package/dist/env-surface.d.ts +12 -0
- package/dist/index.js +782 -237
- package/dist/prompt.d.ts +16 -2
- package/dist/working-tree.d.ts +74 -0
- package/package.json +6 -6
|
@@ -5,9 +5,9 @@ import {
|
|
|
5
5
|
|
|
6
6
|
// src/deploy.ts
|
|
7
7
|
import { existsSync as existsSync7 } from "fs";
|
|
8
|
-
import {
|
|
8
|
+
import { mkdtemp as mkdtemp2, readFile as readFile14, rm as rm5 } from "fs/promises";
|
|
9
9
|
import { tmpdir as tmpdir2 } from "os";
|
|
10
|
-
import { basename as
|
|
10
|
+
import { basename as basename4, dirname as dirname6, join as join25, resolve as resolve11 } from "path";
|
|
11
11
|
import { fileURLToPath } from "url";
|
|
12
12
|
|
|
13
13
|
// ../../packages/build/src/artifact-upload.ts
|
|
@@ -26,7 +26,8 @@ function artifactUrlKey(kind, name) {
|
|
|
26
26
|
return `${kind}/${name}`;
|
|
27
27
|
}
|
|
28
28
|
var ARTIFACT_URL_BATCH_SIZE = 100;
|
|
29
|
-
var ARTIFACT_UPLOAD_CONCURRENCY =
|
|
29
|
+
var ARTIFACT_UPLOAD_CONCURRENCY = 24;
|
|
30
|
+
var ARTIFACT_UPLOAD_BYTE_BUDGET = 16 * 1024 * 1024;
|
|
30
31
|
function parseArtifactPutUrls(payload) {
|
|
31
32
|
if (typeof payload !== "object" || payload === null) return null;
|
|
32
33
|
const { urls, disabled } = payload;
|
|
@@ -38,39 +39,60 @@ function parseArtifactPutUrls(payload) {
|
|
|
38
39
|
}
|
|
39
40
|
return resolved;
|
|
40
41
|
}
|
|
41
|
-
async function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
async function eachWithinBudget(items, limit, budget, weigh, work) {
|
|
43
|
+
if (items.length === 0) return;
|
|
44
|
+
await new Promise((resolve12, reject) => {
|
|
45
|
+
let next = 0;
|
|
46
|
+
let running = 0;
|
|
47
|
+
let weight = 0;
|
|
48
|
+
let finished = 0;
|
|
49
|
+
const pump = () => {
|
|
50
|
+
while (next < items.length && running < limit) {
|
|
47
51
|
const item = items[next];
|
|
52
|
+
const itemWeight = weigh(item);
|
|
53
|
+
if (running > 0 && weight + itemWeight > budget) return;
|
|
48
54
|
next += 1;
|
|
49
|
-
|
|
55
|
+
running += 1;
|
|
56
|
+
weight += itemWeight;
|
|
57
|
+
work(item).then(() => {
|
|
58
|
+
running -= 1;
|
|
59
|
+
weight -= itemWeight;
|
|
60
|
+
finished += 1;
|
|
61
|
+
if (finished === items.length) resolve12();
|
|
62
|
+
else pump();
|
|
63
|
+
}, reject);
|
|
50
64
|
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
};
|
|
66
|
+
pump();
|
|
67
|
+
});
|
|
54
68
|
}
|
|
55
|
-
async function uploadArtifactFiles(artifact, ports) {
|
|
69
|
+
async function uploadArtifactFiles(artifact, ports, limits = {}) {
|
|
70
|
+
const concurrency = limits.concurrency ?? ARTIFACT_UPLOAD_CONCURRENCY;
|
|
71
|
+
const byteBudget = limits.byteBudget ?? ARTIFACT_UPLOAD_BYTE_BUDGET;
|
|
56
72
|
const entries = artifactUploadEntries(artifact);
|
|
57
73
|
let proxied = 0;
|
|
58
74
|
for (let index = 0; index < entries.length; index += ARTIFACT_URL_BATCH_SIZE) {
|
|
59
75
|
const batch = entries.slice(index, index + ARTIFACT_URL_BATCH_SIZE);
|
|
60
76
|
const urls = await ports.resolvePutUrls(batch);
|
|
61
77
|
const viaProxy = [];
|
|
62
|
-
await
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
78
|
+
await eachWithinBudget(
|
|
79
|
+
batch,
|
|
80
|
+
concurrency,
|
|
81
|
+
byteBudget,
|
|
82
|
+
(entry) => entry.file.data.byteLength,
|
|
83
|
+
async (entry) => {
|
|
84
|
+
const url2 = urls?.get(artifactUrlKey(entry.kind, entry.file.name));
|
|
85
|
+
if (!url2) {
|
|
86
|
+
viaProxy.push(entry);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
await ports.putSigned(url2, entry);
|
|
91
|
+
} catch {
|
|
92
|
+
viaProxy.push(entry);
|
|
93
|
+
}
|
|
72
94
|
}
|
|
73
|
-
|
|
95
|
+
);
|
|
74
96
|
for (const entry of viaProxy) {
|
|
75
97
|
proxied += 1;
|
|
76
98
|
await ports.putProxied(entry);
|
|
@@ -305,6 +327,125 @@ var STALL_WINDOW_MS = 10 * 6e4;
|
|
|
305
327
|
var MIN_SUPPORTED_NEXT_MAJOR = 15;
|
|
306
328
|
|
|
307
329
|
// ../../packages/core/src/errors.ts
|
|
330
|
+
var DEPLOYMENT_ERROR_CODES = [
|
|
331
|
+
"CLONE_FAILED",
|
|
332
|
+
"UNSUPPORTED_PACKAGE_MANAGER",
|
|
333
|
+
"INSTALL_FAILED",
|
|
334
|
+
"FRAMEWORK_DETECTION_FAILED",
|
|
335
|
+
"BUILD_FAILED",
|
|
336
|
+
"OPENNEXT_FAILED",
|
|
337
|
+
"NEXT_VERSION_UNSUPPORTED",
|
|
338
|
+
"NO_DEPLOYMENT_STRATEGY",
|
|
339
|
+
"ARTIFACT_INVALID",
|
|
340
|
+
"DEPLOY_FAILED",
|
|
341
|
+
"QUEUE_PUBLISH_FAILED",
|
|
342
|
+
"GIT_NOT_CONNECTED",
|
|
343
|
+
"UNAUTHORIZED",
|
|
344
|
+
/** Signed in, but the role does not carry the permission for this write. */
|
|
345
|
+
"FORBIDDEN",
|
|
346
|
+
"NOT_FOUND",
|
|
347
|
+
"DEPLOYMENT_NOT_PROMOTABLE",
|
|
348
|
+
"DEPLOYMENT_NOT_CANCELABLE",
|
|
349
|
+
"SUBDOMAIN_TAKEN",
|
|
350
|
+
"FLAG_EXISTS",
|
|
351
|
+
"DOMAIN_EXISTS",
|
|
352
|
+
/** known-issues #127: the project already has the most custom domains it can have (20). */
|
|
353
|
+
"DOMAIN_LIMIT_REACHED",
|
|
354
|
+
/** known-issues #127: the project's serving domains would not fit in its Naro worker's NARO_PRODUCTION_ORIGINS. */
|
|
355
|
+
"DOMAIN_NAMES_TOO_LONG",
|
|
356
|
+
/** The server has no Cloudflare credentials for custom domains, so no domain can be added, checked or removed. */
|
|
357
|
+
"DOMAINS_NOT_CONFIGURED",
|
|
358
|
+
/** Another project holds the hostname; a TXT ownership check was opened (known-issues #156). */
|
|
359
|
+
"DOMAIN_VERIFICATION_REQUIRED",
|
|
360
|
+
/** The ownership check ran past its seven days; adding the domain again opens a new one. */
|
|
361
|
+
"DOMAIN_CLAIM_EXPIRED",
|
|
362
|
+
"DNS_ZONE_EXISTS",
|
|
363
|
+
"DNS_DELEGATION_NOT_ENABLED",
|
|
364
|
+
/** naro DNS record editor (spec 2026-10-17-dns-delegation-stage2 §3): a record a naro project or Cloudflare answers on. */
|
|
365
|
+
"DNS_RECORD_PROTECTED",
|
|
366
|
+
/** The zone holds as many records as Cloudflare allows it. */
|
|
367
|
+
"DNS_RECORD_QUOTA",
|
|
368
|
+
/** Cloudflare refused the record; the message carries its reason. */
|
|
369
|
+
"DNS_RECORD_REJECTED",
|
|
370
|
+
"RULE_EXISTS",
|
|
371
|
+
"STORAGE_EXISTS",
|
|
372
|
+
/** The project's Cloudflare D1 could not be provisioned or reused. */
|
|
373
|
+
"DATABASE_PROVISION_FAILED",
|
|
374
|
+
/**
|
|
375
|
+
* A stored env value no longer decrypts: ENV_ENCRYPTION_KEY is missing or
|
|
376
|
+
* is not the key it was saved with (known-issues #5).
|
|
377
|
+
*/
|
|
378
|
+
"ENV_DECRYPT_FAILED",
|
|
379
|
+
/** naro db tooling (spec §11). The kill switch DATABASE_TOOLING_ENABLED is unset. */
|
|
380
|
+
"DATABASE_TOOLING_DISABLED",
|
|
381
|
+
/** The project uses an external database naro does not manage. */
|
|
382
|
+
"DATABASE_NOT_ENABLED",
|
|
383
|
+
/** No provisioned D1 yet and the caller may not auto-provision one. */
|
|
384
|
+
"DATABASE_NOT_READY",
|
|
385
|
+
/** A migration by that name is already in the ledger (B1b). */
|
|
386
|
+
"DATABASE_MIGRATION_EXISTS",
|
|
387
|
+
/** SQLite or Cloudflare rejected the statement; the message is Cloudflare's, sanitized. */
|
|
388
|
+
"DATABASE_QUERY_FAILED",
|
|
389
|
+
/** A write was sent in read-only mode, or the read-only paranoia scan refused it. */
|
|
390
|
+
"DATABASE_READ_ONLY",
|
|
391
|
+
"DATABASE_STATEMENT_TOO_LARGE",
|
|
392
|
+
"DATABASE_TOO_MANY_PARAMS",
|
|
393
|
+
"DATABASE_TOO_MANY_STATEMENTS",
|
|
394
|
+
/** The D1 response passed the byte cap before it was parsed. */
|
|
395
|
+
"DATABASE_RESULT_TOO_LARGE",
|
|
396
|
+
/** D1 reported a timeout or overload — retry with a narrower query. */
|
|
397
|
+
"DATABASE_OVERLOADED",
|
|
398
|
+
/** Cloudflare 5xx or transport failure. */
|
|
399
|
+
"DATABASE_UNAVAILABLE",
|
|
400
|
+
/** Cloudflare refused the export — `result.status: "error"` inside a 200, or a 4xx (spec §16.5). */
|
|
401
|
+
"DATABASE_EXPORT_FAILED",
|
|
402
|
+
/** Cloudflare answered a Time Travel restore with a 5xx — live, an unknown bookmark. */
|
|
403
|
+
"DATABASE_RESTORE_FAILED",
|
|
404
|
+
/** `db pull`: the d1_migrations ledger is not empty, so there is no baseline to record. */
|
|
405
|
+
"DATABASE_HAS_MIGRATIONS",
|
|
406
|
+
/** `db pull`: no tables, views, indexes or triggers to record. */
|
|
407
|
+
"DATABASE_EMPTY",
|
|
408
|
+
/** PR C: the project's D1 wrote more rows today (UTC) than its daily limit; writes wait for 00:00 UTC. */
|
|
409
|
+
"DATABASE_WRITE_QUOTA_EXCEEDED",
|
|
410
|
+
/** PR C: the meter job could not read Cloudflare's D1 analytics, so it changed nothing. */
|
|
411
|
+
"DATABASE_METERING_FAILED",
|
|
412
|
+
/** PR D: D1 refused a write because the database reached its 10 GB limit, which cannot be raised. */
|
|
413
|
+
"DATABASE_FULL",
|
|
414
|
+
/** Usage limits spec 2026-10-18 §6: a hobby team created its 100 deployments this UTC month. */
|
|
415
|
+
"USAGE_LIMIT_REACHED",
|
|
416
|
+
/** A hobby team has its 3 projects; no project can be added. */
|
|
417
|
+
"PROJECT_LIMIT_REACHED",
|
|
418
|
+
/** A hobby team has its 1 member; no member or invite can be added. */
|
|
419
|
+
"MEMBER_LIMIT_REACHED",
|
|
420
|
+
/** Custom domains are not part of the Hobby plan. */
|
|
421
|
+
"PLAN_DOMAINS_NOT_INCLUDED",
|
|
422
|
+
/** Named environments (spec 2026-10-18-named-environments §15): the name is not lowercase letters, digits and single hyphens, or is over 20 characters. */
|
|
423
|
+
"ENVIRONMENT_NAME_INVALID",
|
|
424
|
+
/** `production` and `preview` are reserved names. */
|
|
425
|
+
"ENVIRONMENT_NAME_RESERVED",
|
|
426
|
+
/** `<subdomain>--<name>` would be longer than the 63-character DNS label limit. */
|
|
427
|
+
"ENVIRONMENT_NAME_TOO_LONG",
|
|
428
|
+
/** The project's subdomain cannot carry environments (it is `xn`). */
|
|
429
|
+
"ENVIRONMENT_HOST_INVALID",
|
|
430
|
+
"ENVIRONMENT_EXISTS",
|
|
431
|
+
/** Another environment of the project already deploys this branch. */
|
|
432
|
+
"ENVIRONMENT_BRANCH_TAKEN",
|
|
433
|
+
/** The branch deploys to production. */
|
|
434
|
+
"ENVIRONMENT_BRANCH_IS_PRODUCTION",
|
|
435
|
+
/** The plan's environment count is full. */
|
|
436
|
+
"ENVIRONMENT_LIMIT_REACHED",
|
|
437
|
+
/** The project uses a Naro database, which environments do not support yet (stages 1 and 2 only). */
|
|
438
|
+
"ENVIRONMENT_DATABASE_UNSUPPORTED",
|
|
439
|
+
"ENVIRONMENT_NOT_FOUND",
|
|
440
|
+
/** The project's new default branch is the one an environment deploys. */
|
|
441
|
+
"BRANCH_USED_BY_ENVIRONMENT",
|
|
442
|
+
/** A new subdomain would push an environment's label past 63 characters. */
|
|
443
|
+
"SUBDOMAIN_TOO_LONG_FOR_ENVIRONMENT",
|
|
444
|
+
/** A deployment failure: the environment was deleted before its deployment finished. */
|
|
445
|
+
"ENVIRONMENT_DELETED",
|
|
446
|
+
/** The edge refused the environment's protection change; the database change is rolled back. */
|
|
447
|
+
"ENVIRONMENT_PROTECTION_PUBLISH_FAILED"
|
|
448
|
+
];
|
|
308
449
|
var DeploymentError = class extends Error {
|
|
309
450
|
code;
|
|
310
451
|
/** HTTP status from the upstream response that caused this error, when known. */
|
|
@@ -14950,7 +15091,13 @@ var createProjectSchema = external_exports.object({
|
|
|
14950
15091
|
rootDirectory: optionalRootDirectory
|
|
14951
15092
|
});
|
|
14952
15093
|
var createDeploymentSchema = external_exports.object({
|
|
14953
|
-
branch: external_exports.string().trim().max(240).optional().transform((value) => value || null)
|
|
15094
|
+
branch: external_exports.string().trim().max(240).optional().transform((value) => value || null),
|
|
15095
|
+
/**
|
|
15096
|
+
* The surface to deploy to: `production`, `preview`, or the slug of one of
|
|
15097
|
+
* the project's environments. Whether the name exists is the route's to say;
|
|
15098
|
+
* 63 is the DNS label a worker name has to fit in.
|
|
15099
|
+
*/
|
|
15100
|
+
environment: external_exports.string().trim().min(1).max(63).optional()
|
|
14954
15101
|
});
|
|
14955
15102
|
var connectRepositorySchema = external_exports.object({
|
|
14956
15103
|
// Web-only route context: the only consumer is apps/web's
|
|
@@ -15213,6 +15360,40 @@ function splitStatements(sql) {
|
|
|
15213
15360
|
};
|
|
15214
15361
|
}
|
|
15215
15362
|
|
|
15363
|
+
// ../../packages/core/src/surface.ts
|
|
15364
|
+
var PRODUCTION_SURFACE = { kind: "production" };
|
|
15365
|
+
var PREVIEW_SURFACE = { kind: "preview" };
|
|
15366
|
+
var ENVIRONMENT_SLUG_MAX_LENGTH = 20;
|
|
15367
|
+
var RESERVED_ENVIRONMENT_SLUGS = [
|
|
15368
|
+
"production",
|
|
15369
|
+
"preview",
|
|
15370
|
+
"all"
|
|
15371
|
+
];
|
|
15372
|
+
var SLUG_PATTERN = /^[a-z0-9](-?[a-z0-9])*$/;
|
|
15373
|
+
var ENVIRONMENT_SEPARATOR = "--";
|
|
15374
|
+
var PREVIEW_SUFFIX = "-preview";
|
|
15375
|
+
function environmentSlugProblem(value) {
|
|
15376
|
+
if (value.length < 1 || value.length > ENVIRONMENT_SLUG_MAX_LENGTH)
|
|
15377
|
+
return "invalid";
|
|
15378
|
+
if (!SLUG_PATTERN.test(value)) return "invalid";
|
|
15379
|
+
if (RESERVED_ENVIRONMENT_SLUGS.includes(value))
|
|
15380
|
+
return "reserved";
|
|
15381
|
+
return null;
|
|
15382
|
+
}
|
|
15383
|
+
function environmentWorkerName(subdomain, slug) {
|
|
15384
|
+
return `${subdomain}${ENVIRONMENT_SEPARATOR}${slug}`;
|
|
15385
|
+
}
|
|
15386
|
+
function workerNameForSurface(subdomain, surface) {
|
|
15387
|
+
switch (surface.kind) {
|
|
15388
|
+
case "production":
|
|
15389
|
+
return subdomain;
|
|
15390
|
+
case "preview":
|
|
15391
|
+
return `${subdomain}${PREVIEW_SUFFIX}`;
|
|
15392
|
+
case "environment":
|
|
15393
|
+
return environmentWorkerName(subdomain, surface.slug);
|
|
15394
|
+
}
|
|
15395
|
+
}
|
|
15396
|
+
|
|
15216
15397
|
// ../../packages/build/src/next-version.ts
|
|
15217
15398
|
var NEXT_RELEASE_DATES = {
|
|
15218
15399
|
16: "2025-10-21",
|
|
@@ -15222,8 +15403,8 @@ var NEXT_RELEASE_DATES = {
|
|
|
15222
15403
|
12: "2021-10-26"
|
|
15223
15404
|
};
|
|
15224
15405
|
var SUPPORT_WINDOW_YEARS = 2;
|
|
15225
|
-
var
|
|
15226
|
-
var
|
|
15406
|
+
var VERIFIED_15_FLOOR = { major: 15, minor: 5, patch: 21 };
|
|
15407
|
+
var VERIFIED_16_FLOOR = { major: 16, minor: 2, patch: 11 };
|
|
15227
15408
|
function parseNextVersion(raw) {
|
|
15228
15409
|
const match = /^\s*(\d+)\.(\d+)\.(\d+)/.exec(raw);
|
|
15229
15410
|
if (!match) return null;
|
|
@@ -15249,9 +15430,9 @@ function supportEndsAt(major) {
|
|
|
15249
15430
|
function compare(a, b) {
|
|
15250
15431
|
return a.major - b.major || a.minor - b.minor || a.patch - b.patch;
|
|
15251
15432
|
}
|
|
15252
|
-
function
|
|
15253
|
-
if (version2.major === 15) return compare(version2,
|
|
15254
|
-
if (version2.major === 16) return compare(version2,
|
|
15433
|
+
function withinVerifiedRange(version2) {
|
|
15434
|
+
if (version2.major === 15) return compare(version2, VERIFIED_15_FLOOR) >= 0;
|
|
15435
|
+
if (version2.major === 16) return compare(version2, VERIFIED_16_FLOOR) >= 0;
|
|
15255
15436
|
return version2.major > 16;
|
|
15256
15437
|
}
|
|
15257
15438
|
function formatDate(date5) {
|
|
@@ -15267,11 +15448,11 @@ function classifyNextVersion(raw, now) {
|
|
|
15267
15448
|
};
|
|
15268
15449
|
}
|
|
15269
15450
|
const windowEnd = supportEndsAt(version2.major);
|
|
15270
|
-
if (!windowEnd || version2.major <
|
|
15451
|
+
if (!windowEnd || version2.major < VERIFIED_15_FLOOR.major) {
|
|
15271
15452
|
return {
|
|
15272
15453
|
status: "too-old",
|
|
15273
15454
|
version: version2,
|
|
15274
|
-
reason: `Next.js ${raw} is too old to deploy.
|
|
15455
|
+
reason: `Next.js ${raw} is too old to deploy. Naro deploys Next.js 15 or newer. Upgrade to Next.js 16.`
|
|
15275
15456
|
};
|
|
15276
15457
|
}
|
|
15277
15458
|
if (now > windowEnd) {
|
|
@@ -15281,11 +15462,11 @@ function classifyNextVersion(raw, now) {
|
|
|
15281
15462
|
reason: `Next.js ${version2.major} left its support window on ${formatDate(windowEnd)}. The build continues, but unsupported releases stop receiving security patches \u2014 upgrade to Next.js 16. See https://nextjs.org/support-policy`
|
|
15282
15463
|
};
|
|
15283
15464
|
}
|
|
15284
|
-
if (!
|
|
15465
|
+
if (!withinVerifiedRange(version2)) {
|
|
15285
15466
|
return {
|
|
15286
15467
|
status: "outside-peer-range",
|
|
15287
15468
|
version: version2,
|
|
15288
|
-
reason: `Next.js ${raw} is outside the range
|
|
15469
|
+
reason: `Next.js ${raw} is outside the range Naro verifies deploys on (>=15.5.21 <16 || >=16.2.11). The build may fail in ways Naro cannot explain.`
|
|
15289
15470
|
};
|
|
15290
15471
|
}
|
|
15291
15472
|
return { status: "supported", version: version2, reason: "" };
|
|
@@ -15294,9 +15475,9 @@ function classifyNextVersion(raw, now) {
|
|
|
15294
15475
|
// ../../packages/build/src/package-manager.ts
|
|
15295
15476
|
import { access, readFile as readFile2 } from "fs/promises";
|
|
15296
15477
|
import { join as join2 } from "path";
|
|
15297
|
-
function cacheEnv(dir) {
|
|
15478
|
+
function cacheEnv(dir, manager) {
|
|
15298
15479
|
return {
|
|
15299
|
-
npm_config_store_dir: dir,
|
|
15480
|
+
...manager === "pnpm" ? { npm_config_store_dir: dir } : {},
|
|
15300
15481
|
npm_config_cache: dir,
|
|
15301
15482
|
YARN_CACHE_FOLDER: dir,
|
|
15302
15483
|
YARN_ENABLE_GLOBAL_CACHE: "0"
|
|
@@ -15310,7 +15491,7 @@ function withSharedCache(spec, dir) {
|
|
|
15310
15491
|
...spec.sharedCacheAddArgs ?? [],
|
|
15311
15492
|
...spec.cacheArgs(dir)
|
|
15312
15493
|
],
|
|
15313
|
-
addEnv: { ...spec.addEnv, ...cacheEnv(dir) }
|
|
15494
|
+
addEnv: { ...spec.addEnv, ...cacheEnv(dir, spec.name) }
|
|
15314
15495
|
};
|
|
15315
15496
|
}
|
|
15316
15497
|
async function exists(path) {
|
|
@@ -15805,6 +15986,32 @@ var VITE_CONFIG_NAMES = [
|
|
|
15805
15986
|
"vite.config.cts"
|
|
15806
15987
|
];
|
|
15807
15988
|
var GENERATED_VITE_CONFIG = "vite.config.js";
|
|
15989
|
+
var GENERATED_WORKER_ENTRY = "naro-worker.js";
|
|
15990
|
+
var VINEXT_WORKER_ENTRY_SOURCE = `// Written by naro: vinext's Worker, with getCloudflareContext() given its env.
|
|
15991
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
15992
|
+
import handler from "vinext/server/fetch-handler";
|
|
15993
|
+
|
|
15994
|
+
export * from "vinext/server/fetch-handler";
|
|
15995
|
+
|
|
15996
|
+
const requests = new AsyncLocalStorage();
|
|
15997
|
+
|
|
15998
|
+
Object.defineProperty(globalThis, Symbol.for("__cloudflare-context__"), {
|
|
15999
|
+
configurable: false,
|
|
16000
|
+
get() {
|
|
16001
|
+
return requests.getStore();
|
|
16002
|
+
},
|
|
16003
|
+
set: undefined,
|
|
16004
|
+
});
|
|
16005
|
+
|
|
16006
|
+
export default {
|
|
16007
|
+
...handler,
|
|
16008
|
+
fetch(request, env, ctx) {
|
|
16009
|
+
return requests.run({ env, ctx, cf: request.cf }, () =>
|
|
16010
|
+
handler.fetch(request, env, ctx),
|
|
16011
|
+
);
|
|
16012
|
+
},
|
|
16013
|
+
};
|
|
16014
|
+
`;
|
|
15808
16015
|
var OUTPUT_DIR = "dist";
|
|
15809
16016
|
var SERVER_DIR = "server";
|
|
15810
16017
|
var CLIENT_DIR = "client";
|
|
@@ -15828,6 +16035,13 @@ async function createVinextBuildConfig(appRoot, deploymentId, writer = async (pa
|
|
|
15828
16035
|
);
|
|
15829
16036
|
}
|
|
15830
16037
|
}
|
|
16038
|
+
const workerEntryPath = join6(appRoot, GENERATED_WORKER_ENTRY);
|
|
16039
|
+
if (await exists3(workerEntryPath)) {
|
|
16040
|
+
throw new DeploymentError(
|
|
16041
|
+
"ARTIFACT_INVALID",
|
|
16042
|
+
`The project already has a ${GENERATED_WORKER_ENTRY}. naro writes its vinext Worker entry under that name and will not overwrite a file of the project's.`
|
|
16043
|
+
);
|
|
16044
|
+
}
|
|
15831
16045
|
const safeDeploymentId = deploymentId.replace(/[^a-z0-9-]/gi, "-");
|
|
15832
16046
|
const viteConfigPath = join6(appRoot, GENERATED_VITE_CONFIG);
|
|
15833
16047
|
const wranglerConfigPath = join6(appRoot, "wrangler.jsonc");
|
|
@@ -15847,6 +16061,7 @@ async function createVinextBuildConfig(appRoot, deploymentId, writer = async (pa
|
|
|
15847
16061
|
""
|
|
15848
16062
|
].join("\n")
|
|
15849
16063
|
);
|
|
16064
|
+
await writer(workerEntryPath, VINEXT_WORKER_ENTRY_SOURCE);
|
|
15850
16065
|
await writer(
|
|
15851
16066
|
wranglerConfigPath,
|
|
15852
16067
|
`${JSON.stringify(
|
|
@@ -15854,7 +16069,7 @@ async function createVinextBuildConfig(appRoot, deploymentId, writer = async (pa
|
|
|
15854
16069
|
name: `naro-vinext-${safeDeploymentId}`,
|
|
15855
16070
|
compatibility_date: VINEXT_COMPATIBILITY_DATE,
|
|
15856
16071
|
compatibility_flags: ["nodejs_compat"],
|
|
15857
|
-
main:
|
|
16072
|
+
main: `./${GENERATED_WORKER_ENTRY}`,
|
|
15858
16073
|
assets: {
|
|
15859
16074
|
directory: `${OUTPUT_DIR}/${CLIENT_DIR}`,
|
|
15860
16075
|
binding: "ASSETS"
|
|
@@ -16456,9 +16671,17 @@ function formatLogLine(raw) {
|
|
|
16456
16671
|
}
|
|
16457
16672
|
|
|
16458
16673
|
// ../../packages/build/src/redact.ts
|
|
16459
|
-
var
|
|
16674
|
+
var KNOWN_SECRET_ASSIGNMENT = /\b(CLOUDFLARE_API_TOKEN|SUPABASE_SERVICE_ROLE_KEY|BUILD_CALLBACK_SECRET)=([^\s]+)/gi;
|
|
16675
|
+
var SECRET_NAME_ASSIGNMENT = /\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASSWD|_KEY))(\s*[:=]\s*)(["']?)([^\s"']+)/g;
|
|
16676
|
+
var AUTHORIZATION = /\b(authorization["']?\s*[:=]\s*["']?)(?:([A-Za-z][\w-]*)\s+)?[^\r\n]+/gi;
|
|
16677
|
+
var GITHUB_TOKEN = /\b(?:gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,})/g;
|
|
16678
|
+
var JWT = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g;
|
|
16679
|
+
var PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g;
|
|
16460
16680
|
function redactLog(message) {
|
|
16461
|
-
return message.replace(
|
|
16681
|
+
return message.replace(PRIVATE_KEY, "[REDACTED PRIVATE KEY]").replace(
|
|
16682
|
+
AUTHORIZATION,
|
|
16683
|
+
(_match, prefix, scheme) => scheme ? `${prefix}${scheme} [REDACTED]` : `${prefix}[REDACTED]`
|
|
16684
|
+
).replace(/\bBearer\s+[^\s]+/gi, "Bearer [REDACTED]").replace(/(\b[a-z][a-z0-9+.-]*:\/\/)[^/@\s]+@/gi, "$1[REDACTED]@").replace(KNOWN_SECRET_ASSIGNMENT, "$1=[REDACTED]").replace(SECRET_NAME_ASSIGNMENT, "$1$2$3[REDACTED]").replace(GITHUB_TOKEN, "[REDACTED]").replace(JWT, "[REDACTED]");
|
|
16462
16685
|
}
|
|
16463
16686
|
|
|
16464
16687
|
// ../../packages/build/src/process.ts
|
|
@@ -16851,10 +17074,10 @@ async function exists4(path) {
|
|
|
16851
17074
|
return false;
|
|
16852
17075
|
}
|
|
16853
17076
|
}
|
|
16854
|
-
async function findSourceFile(appRoot,
|
|
17077
|
+
async function findSourceFile(appRoot, basename5) {
|
|
16855
17078
|
for (const dir of SOURCE_DIRECTORIES) {
|
|
16856
17079
|
for (const ext of SOURCE_EXTENSIONS2) {
|
|
16857
|
-
const rel = dir ? `${dir}/${
|
|
17080
|
+
const rel = dir ? `${dir}/${basename5}.${ext}` : `${basename5}.${ext}`;
|
|
16858
17081
|
if (await exists4(join11(appRoot, rel))) return rel;
|
|
16859
17082
|
}
|
|
16860
17083
|
}
|
|
@@ -16921,6 +17144,12 @@ async function collectProjectFacts(appRoot) {
|
|
|
16921
17144
|
function label(strategy) {
|
|
16922
17145
|
return `P${String(strategy.priority)} ${strategy.id}`;
|
|
16923
17146
|
}
|
|
17147
|
+
function clause(text2) {
|
|
17148
|
+
return text2.endsWith(".") && !text2.endsWith("..") ? text2.slice(0, -1) : text2;
|
|
17149
|
+
}
|
|
17150
|
+
function sentence(text2) {
|
|
17151
|
+
return text2.endsWith(".") ? text2 : `${text2}.`;
|
|
17152
|
+
}
|
|
16924
17153
|
function describeOutcome(outcome) {
|
|
16925
17154
|
switch (outcome.status) {
|
|
16926
17155
|
case "success":
|
|
@@ -16945,7 +17174,7 @@ async function evaluateOne(strategy, context, observer) {
|
|
|
16945
17174
|
await observer.onLog({
|
|
16946
17175
|
level: outcome.status === "success" ? "INFO" : "WARN",
|
|
16947
17176
|
stage: "BUILDING",
|
|
16948
|
-
message: `${label(strategy)} ${describeOutcome(outcome)}
|
|
17177
|
+
message: sentence(`${label(strategy)} ${describeOutcome(outcome)}`)
|
|
16949
17178
|
});
|
|
16950
17179
|
return { id: strategy.id, priority: strategy.priority, outcome };
|
|
16951
17180
|
}
|
|
@@ -17043,10 +17272,13 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
|
|
|
17043
17272
|
attempts
|
|
17044
17273
|
};
|
|
17045
17274
|
}
|
|
17275
|
+
const othersLeft = candidates.some(
|
|
17276
|
+
(candidate) => candidate.id !== pinnedStrategy.id
|
|
17277
|
+
);
|
|
17046
17278
|
await observer.onLog({
|
|
17047
17279
|
level: "WARN",
|
|
17048
17280
|
stage: "BUILDING",
|
|
17049
|
-
message: `Stored strategy ${label(pinnedStrategy)} no longer works; resolving again with the remaining candidates.`
|
|
17281
|
+
message: othersLeft ? `Stored strategy ${label(pinnedStrategy)} no longer works; resolving again with the remaining candidates.` : `Stored strategy ${label(pinnedStrategy)} no longer works, and no other deployment strategy is left to try for Next.js ${context.nextVersion}.`
|
|
17050
17282
|
});
|
|
17051
17283
|
}
|
|
17052
17284
|
} else if (options.pinned) {
|
|
@@ -17059,13 +17291,14 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
|
|
|
17059
17291
|
const remaining = candidates.filter(
|
|
17060
17292
|
(candidate) => !attempts.some((attempt) => attempt.id === candidate.id)
|
|
17061
17293
|
);
|
|
17062
|
-
|
|
17063
|
-
|
|
17064
|
-
|
|
17065
|
-
|
|
17066
|
-
|
|
17067
|
-
|
|
17068
|
-
|
|
17294
|
+
if (remaining.length > 0)
|
|
17295
|
+
await observer.onLog({
|
|
17296
|
+
level: "INFO",
|
|
17297
|
+
stage: "BUILDING",
|
|
17298
|
+
message: `Resolving deployment strategy for Next.js ${context.nextVersion}: ${remaining.map(label).join(
|
|
17299
|
+
", "
|
|
17300
|
+
)}${options.concurrency > 1 && remaining.length > 1 ? " (in parallel)" : ""}.`
|
|
17301
|
+
});
|
|
17069
17302
|
if (options.concurrency > 1) {
|
|
17070
17303
|
const results = await runPool(
|
|
17071
17304
|
remaining.map(
|
|
@@ -17083,11 +17316,25 @@ async function resolveDeploymentStrategy(strategies, context, observer, options)
|
|
|
17083
17316
|
}
|
|
17084
17317
|
const best = pickBest(candidates, attempts);
|
|
17085
17318
|
if (!best) {
|
|
17319
|
+
const summary = attempts.map(
|
|
17320
|
+
(attempt) => clause(
|
|
17321
|
+
`P${String(attempt.priority)} ${attempt.id} ${describeOutcome(attempt.outcome)}`
|
|
17322
|
+
)
|
|
17323
|
+
).join("; ");
|
|
17324
|
+
if (attempts.some(
|
|
17325
|
+
(attempt) => attempt.outcome.status === "failed" && attempt.outcome.appBuild
|
|
17326
|
+
))
|
|
17327
|
+
throw new DeploymentError(
|
|
17328
|
+
"BUILD_FAILED",
|
|
17329
|
+
sentence(
|
|
17330
|
+
`The app's own build failed for Next.js ${context.nextVersion}: ${summary}`
|
|
17331
|
+
)
|
|
17332
|
+
);
|
|
17086
17333
|
throw new DeploymentError(
|
|
17087
17334
|
"NO_DEPLOYMENT_STRATEGY",
|
|
17088
|
-
|
|
17089
|
-
|
|
17090
|
-
)
|
|
17335
|
+
sentence(
|
|
17336
|
+
`No deployment strategy succeeded for Next.js ${context.nextVersion}: ${summary}`
|
|
17337
|
+
)
|
|
17091
17338
|
);
|
|
17092
17339
|
}
|
|
17093
17340
|
await observer.onLog({
|
|
@@ -17299,7 +17546,8 @@ async function runStep(context, spec, observer) {
|
|
|
17299
17546
|
try {
|
|
17300
17547
|
return {
|
|
17301
17548
|
ok: true,
|
|
17302
|
-
result: await context.ports.runCommand(spec, capture.observer)
|
|
17549
|
+
result: await context.ports.runCommand(spec, capture.observer),
|
|
17550
|
+
output: capture.lines
|
|
17303
17551
|
};
|
|
17304
17552
|
} catch (error51) {
|
|
17305
17553
|
return { ok: false, error: error51, output: capture.lines };
|
|
@@ -17315,6 +17563,9 @@ function describeFailure(step) {
|
|
|
17315
17563
|
function failed(stage, reason) {
|
|
17316
17564
|
return { status: "failed", stage, reason };
|
|
17317
17565
|
}
|
|
17566
|
+
function appBuildFailed(reason) {
|
|
17567
|
+
return { status: "failed", stage: "build", reason, appBuild: true };
|
|
17568
|
+
}
|
|
17318
17569
|
async function info(observer, message) {
|
|
17319
17570
|
await observer.onLog({ level: "INFO", stage: "BUILDING", message });
|
|
17320
17571
|
}
|
|
@@ -17597,6 +17848,8 @@ import { readFile as readFile11 } from "fs/promises";
|
|
|
17597
17848
|
import { join as join14 } from "path";
|
|
17598
17849
|
var PROXY_TO_MIDDLEWARE_VARIANT = "proxy-to-middleware";
|
|
17599
17850
|
var NODE_MIDDLEWARE_REFUSAL = /Node\.js middleware is not (?:currently )?supported/i;
|
|
17851
|
+
var NODE_MIDDLEWARE_EXPERIMENTAL = /Node\.js middleware support is experimental/i;
|
|
17852
|
+
var NODE_MIDDLEWARE_NOT_DEPLOYED = 'Node.js-runtime middleware (runtime: "nodejs") is only an experiment in OpenNext Cloudflare, and Naro does not deploy it. Remove runtime: "nodejs" to build it as Edge middleware';
|
|
17600
17853
|
async function buildDeclaresNodeMiddleware(appRoot) {
|
|
17601
17854
|
try {
|
|
17602
17855
|
const manifest2 = JSON.parse(
|
|
@@ -17611,7 +17864,7 @@ async function buildDeclaresNodeMiddleware(appRoot) {
|
|
|
17611
17864
|
}
|
|
17612
17865
|
}
|
|
17613
17866
|
function createOpenNextCloudflareStrategy(options) {
|
|
17614
|
-
async function attempt(context, roots, observer) {
|
|
17867
|
+
async function attempt(context, roots, observer, ownBuild) {
|
|
17615
17868
|
let platform;
|
|
17616
17869
|
try {
|
|
17617
17870
|
platform = await options.createPlatformBuildConfig(
|
|
@@ -17621,7 +17874,7 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17621
17874
|
} catch (error51) {
|
|
17622
17875
|
return {
|
|
17623
17876
|
ok: false,
|
|
17624
|
-
nodeMiddleware:
|
|
17877
|
+
nodeMiddleware: null,
|
|
17625
17878
|
outcome: failed("prepare", describeError(error51))
|
|
17626
17879
|
};
|
|
17627
17880
|
}
|
|
@@ -17645,8 +17898,15 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17645
17898
|
if (!nextBuild.ok) {
|
|
17646
17899
|
return {
|
|
17647
17900
|
ok: false,
|
|
17648
|
-
nodeMiddleware:
|
|
17649
|
-
outcome: failed("build", describeFailure(nextBuild))
|
|
17901
|
+
nodeMiddleware: null,
|
|
17902
|
+
outcome: ownBuild ? appBuildFailed(describeFailure(nextBuild)) : failed("build", describeFailure(nextBuild))
|
|
17903
|
+
};
|
|
17904
|
+
}
|
|
17905
|
+
if (await buildDeclaresNodeMiddleware(roots.appRoot)) {
|
|
17906
|
+
return {
|
|
17907
|
+
ok: false,
|
|
17908
|
+
nodeMiddleware: "experimental",
|
|
17909
|
+
outcome: failed("build", NODE_MIDDLEWARE_NOT_DEPLOYED)
|
|
17650
17910
|
};
|
|
17651
17911
|
}
|
|
17652
17912
|
const openNext = await runStep(
|
|
@@ -17670,16 +17930,25 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17670
17930
|
observer
|
|
17671
17931
|
);
|
|
17672
17932
|
if (!openNext.ok) {
|
|
17673
|
-
const
|
|
17933
|
+
const refused = openNext.output.some(
|
|
17934
|
+
(line) => NODE_MIDDLEWARE_REFUSAL.test(line)
|
|
17935
|
+
);
|
|
17674
17936
|
return {
|
|
17675
17937
|
ok: false,
|
|
17676
|
-
nodeMiddleware,
|
|
17938
|
+
nodeMiddleware: refused ? "refused" : null,
|
|
17677
17939
|
outcome: failed(
|
|
17678
17940
|
"build",
|
|
17679
|
-
|
|
17941
|
+
refused ? NODE_MIDDLEWARE_NOT_DEPLOYED : describeFailure(openNext)
|
|
17680
17942
|
)
|
|
17681
17943
|
};
|
|
17682
17944
|
}
|
|
17945
|
+
if (openNext.output.some((line) => NODE_MIDDLEWARE_EXPERIMENTAL.test(line))) {
|
|
17946
|
+
return {
|
|
17947
|
+
ok: false,
|
|
17948
|
+
nodeMiddleware: "experimental",
|
|
17949
|
+
outcome: failed("build", NODE_MIDDLEWARE_NOT_DEPLOYED)
|
|
17950
|
+
};
|
|
17951
|
+
}
|
|
17683
17952
|
const layout = {
|
|
17684
17953
|
appRoot: roots.appRoot,
|
|
17685
17954
|
wranglerConfigPath: platform.wranglerConfigPath,
|
|
@@ -17694,7 +17963,7 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17694
17963
|
if (!bundle.ok) {
|
|
17695
17964
|
return {
|
|
17696
17965
|
ok: false,
|
|
17697
|
-
nodeMiddleware:
|
|
17966
|
+
nodeMiddleware: null,
|
|
17698
17967
|
outcome: failed("bundle", describeFailure(bundle))
|
|
17699
17968
|
};
|
|
17700
17969
|
}
|
|
@@ -17708,7 +17977,7 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17708
17977
|
} catch (error51) {
|
|
17709
17978
|
return {
|
|
17710
17979
|
ok: false,
|
|
17711
|
-
nodeMiddleware:
|
|
17980
|
+
nodeMiddleware: null,
|
|
17712
17981
|
outcome: failed("bundle", describeError(error51))
|
|
17713
17982
|
};
|
|
17714
17983
|
}
|
|
@@ -17716,7 +17985,7 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17716
17985
|
if (!smoke.ok) {
|
|
17717
17986
|
return {
|
|
17718
17987
|
ok: false,
|
|
17719
|
-
nodeMiddleware:
|
|
17988
|
+
nodeMiddleware: null,
|
|
17720
17989
|
outcome: failed("smoke", smoke.reason)
|
|
17721
17990
|
};
|
|
17722
17991
|
}
|
|
@@ -17739,7 +18008,7 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17739
18008
|
observer,
|
|
17740
18009
|
"P2 opennext-cloudflare: building the project as written."
|
|
17741
18010
|
);
|
|
17742
|
-
const first = await attempt(context, context.workspace, observer);
|
|
18011
|
+
const first = await attempt(context, context.workspace, observer, true);
|
|
17743
18012
|
if (first.ok) {
|
|
17744
18013
|
return {
|
|
17745
18014
|
status: "success",
|
|
@@ -17751,7 +18020,7 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17751
18020
|
return first.outcome;
|
|
17752
18021
|
await warn(
|
|
17753
18022
|
observer,
|
|
17754
|
-
`P2 opennext-cloudflare: OpenNext refused the Node.js middleware compiled from ${context.facts.proxyFile}. Retrying in a temporary workspace with it rewritten to Edge middleware.`
|
|
18023
|
+
first.nodeMiddleware === "experimental" ? `P2 opennext-cloudflare: OpenNext supports the Node.js middleware compiled from ${context.facts.proxyFile} only as an experiment it does not maintain. Retrying in a temporary workspace with it rewritten to Edge middleware.` : `P2 opennext-cloudflare: OpenNext refused the Node.js middleware compiled from ${context.facts.proxyFile}. Retrying in a temporary workspace with it rewritten to Edge middleware.`
|
|
17755
18024
|
);
|
|
17756
18025
|
}
|
|
17757
18026
|
let fork;
|
|
@@ -17771,8 +18040,47 @@ function createOpenNextCloudflareStrategy(options) {
|
|
|
17771
18040
|
observer,
|
|
17772
18041
|
`Converted ${conversion.file ?? context.facts.proxyFile} to edge middleware for this build only. Next.js 16 proxy runs on the Node.js runtime, which Cloudflare does not support; middleware runs on the Edge runtime, so Node.js-only APIs are unavailable. Your repository is unchanged.`
|
|
17773
18042
|
);
|
|
17774
|
-
const second = await attempt(context, fork, observer);
|
|
17775
|
-
if (!second.ok)
|
|
18043
|
+
const second = await attempt(context, fork, observer, rememberedRewrite);
|
|
18044
|
+
if (!second.ok) {
|
|
18045
|
+
if (!rememberedRewrite || second.outcome.status !== "failed" || !second.outcome.appBuild)
|
|
18046
|
+
return second.outcome;
|
|
18047
|
+
await info(
|
|
18048
|
+
observer,
|
|
18049
|
+
`P2 opennext-cloudflare: the rewritten copy did not build; building the project as written once to tell the app's own error from the rewrite's.`
|
|
18050
|
+
);
|
|
18051
|
+
const asWritten = await attempt(
|
|
18052
|
+
context,
|
|
18053
|
+
context.workspace,
|
|
18054
|
+
observer,
|
|
18055
|
+
true
|
|
18056
|
+
);
|
|
18057
|
+
if (asWritten.ok) {
|
|
18058
|
+
await info(
|
|
18059
|
+
observer,
|
|
18060
|
+
"P2 opennext-cloudflare: the project as written builds and ships as it is; the rewrite is no longer needed."
|
|
18061
|
+
);
|
|
18062
|
+
return {
|
|
18063
|
+
status: "success",
|
|
18064
|
+
artifact: {
|
|
18065
|
+
kind: "cloudflare-worker",
|
|
18066
|
+
artifact: asWritten.artifact
|
|
18067
|
+
},
|
|
18068
|
+
notes: []
|
|
18069
|
+
};
|
|
18070
|
+
}
|
|
18071
|
+
if (asWritten.outcome.status === "failed" && asWritten.outcome.appBuild) {
|
|
18072
|
+
await warn(
|
|
18073
|
+
observer,
|
|
18074
|
+
"P2 opennext-cloudflare: the project as written does not build either, so the error is the app's own."
|
|
18075
|
+
);
|
|
18076
|
+
return asWritten.outcome;
|
|
18077
|
+
}
|
|
18078
|
+
await warn(
|
|
18079
|
+
observer,
|
|
18080
|
+
"P2 opennext-cloudflare: the project as written builds, so the rewritten copy's error is the Edge rewrite's (a Node-only API reached from proxy.ts), not the app's code."
|
|
18081
|
+
);
|
|
18082
|
+
return failed("build", second.outcome.reason);
|
|
18083
|
+
}
|
|
17776
18084
|
return {
|
|
17777
18085
|
status: "success",
|
|
17778
18086
|
artifact: { kind: "cloudflare-worker", artifact: second.artifact },
|
|
@@ -18244,13 +18552,20 @@ var LocalBuildExecutor = class {
|
|
|
18244
18552
|
},
|
|
18245
18553
|
observer
|
|
18246
18554
|
);
|
|
18247
|
-
const gitCommitSha = revision.stdout.at(-1)?.trim() ?? "";
|
|
18248
|
-
if (!/^[a-f\d]{40
|
|
18555
|
+
const gitCommitSha = (revision.stdout.at(-1)?.trim() ?? "").toLowerCase();
|
|
18556
|
+
if (!/^(?:[a-f\d]{40}|[a-f\d]{64})$/.test(gitCommitSha)) {
|
|
18249
18557
|
throw new DeploymentError(
|
|
18250
18558
|
"CLONE_FAILED",
|
|
18251
18559
|
"Git returned an invalid commit SHA."
|
|
18252
18560
|
);
|
|
18253
18561
|
}
|
|
18562
|
+
const reportedSha = gitCommitSha.length === 40 ? gitCommitSha : null;
|
|
18563
|
+
if (!reportedSha)
|
|
18564
|
+
await info2(
|
|
18565
|
+
observer,
|
|
18566
|
+
"CLONING",
|
|
18567
|
+
"This repository uses SHA-256 commit ids; Naro records SHA-1 commits only, so this deployment shows no commit."
|
|
18568
|
+
);
|
|
18254
18569
|
const subject = await this.dependencies.runCommand(
|
|
18255
18570
|
{
|
|
18256
18571
|
command: "git",
|
|
@@ -18264,7 +18579,7 @@ var LocalBuildExecutor = class {
|
|
|
18264
18579
|
);
|
|
18265
18580
|
const gitCommitMessage = [...subject.stdout.at(-1)?.trim() ?? ""].slice(0, 300).join("");
|
|
18266
18581
|
await observer.onMetadata?.({
|
|
18267
|
-
gitCommitSha,
|
|
18582
|
+
...reportedSha ? { gitCommitSha: reportedSha } : {},
|
|
18268
18583
|
...gitCommitMessage ? { gitCommitMessage } : {}
|
|
18269
18584
|
});
|
|
18270
18585
|
const appRoot = this.dependencies.resolveAppRoot(
|
|
@@ -18346,7 +18661,7 @@ var LocalBuildExecutor = class {
|
|
|
18346
18661
|
env: {
|
|
18347
18662
|
NODE_ENV: "development",
|
|
18348
18663
|
NPM_CONFIG_USERCONFIG: packageManagerConfigPath,
|
|
18349
|
-
...cachePorts ? cacheEnv(pmCacheDir) : {}
|
|
18664
|
+
...cachePorts ? cacheEnv(pmCacheDir, packageManager.name) : {}
|
|
18350
18665
|
},
|
|
18351
18666
|
timeoutMs: 10 * 6e4,
|
|
18352
18667
|
stage: "INSTALLING",
|
|
@@ -18732,7 +19047,8 @@ function isExpired(session, skewSeconds = 60) {
|
|
|
18732
19047
|
|
|
18733
19048
|
// ../../packages/sdk/src/supabase-auth.ts
|
|
18734
19049
|
import {
|
|
18735
|
-
createClient
|
|
19050
|
+
createClient,
|
|
19051
|
+
isAuthRetryableFetchError
|
|
18736
19052
|
} from "@supabase/supabase-js";
|
|
18737
19053
|
|
|
18738
19054
|
// ../../packages/sdk/src/config.ts
|
|
@@ -18894,8 +19210,8 @@ function sendPage(res, status, html) {
|
|
|
18894
19210
|
async function startCallbackServer() {
|
|
18895
19211
|
let resolveCode;
|
|
18896
19212
|
let rejectCode;
|
|
18897
|
-
const codePromise = new Promise((
|
|
18898
|
-
resolveCode =
|
|
19213
|
+
const codePromise = new Promise((resolve12, reject) => {
|
|
19214
|
+
resolveCode = resolve12;
|
|
18899
19215
|
rejectCode = reject;
|
|
18900
19216
|
});
|
|
18901
19217
|
const server = createServer2((req, res) => {
|
|
@@ -18924,7 +19240,7 @@ async function startCallbackServer() {
|
|
|
18924
19240
|
sendPage(res, 200, successPage());
|
|
18925
19241
|
resolveCode(code);
|
|
18926
19242
|
});
|
|
18927
|
-
await new Promise((
|
|
19243
|
+
await new Promise((resolve12) => server.listen(0, "127.0.0.1", resolve12));
|
|
18928
19244
|
const port = server.address().port;
|
|
18929
19245
|
return {
|
|
18930
19246
|
port,
|
|
@@ -18932,7 +19248,12 @@ async function startCallbackServer() {
|
|
|
18932
19248
|
let timer;
|
|
18933
19249
|
const timeout = new Promise((_, reject) => {
|
|
18934
19250
|
timer = setTimeout(
|
|
18935
|
-
() => reject(
|
|
19251
|
+
() => reject(
|
|
19252
|
+
new NaroError(
|
|
19253
|
+
"TIMEOUT",
|
|
19254
|
+
`Sign-in timed out after ${String(Math.round(timeoutMs / 1e3))}s waiting for the browser. Run \`naro login\` again, or set NARO_TOKEN.`
|
|
19255
|
+
)
|
|
19256
|
+
),
|
|
18936
19257
|
timeoutMs
|
|
18937
19258
|
);
|
|
18938
19259
|
timer.unref?.();
|
|
@@ -18970,20 +19291,44 @@ function createAuthClient(env = process.env) {
|
|
|
18970
19291
|
}
|
|
18971
19292
|
});
|
|
18972
19293
|
}
|
|
18973
|
-
function
|
|
18974
|
-
const meta3 =
|
|
19294
|
+
function toSessionUser(user) {
|
|
19295
|
+
const meta3 = user.user_metadata ?? {};
|
|
18975
19296
|
const github = meta3.user_name ?? meta3.preferred_username ?? null;
|
|
19297
|
+
return { id: user.id, email: user.email ?? null, github };
|
|
19298
|
+
}
|
|
19299
|
+
function toStoredSession(session) {
|
|
18976
19300
|
return {
|
|
18977
19301
|
access_token: session.access_token,
|
|
18978
19302
|
refresh_token: session.refresh_token,
|
|
18979
19303
|
expires_at: session.expires_at ?? 0,
|
|
18980
|
-
user:
|
|
18981
|
-
id: session.user.id,
|
|
18982
|
-
email: session.user.email ?? null,
|
|
18983
|
-
github
|
|
18984
|
-
}
|
|
19304
|
+
user: toSessionUser(session.user)
|
|
18985
19305
|
};
|
|
18986
19306
|
}
|
|
19307
|
+
async function userForAccessToken(accessToken, client = createAuthClient()) {
|
|
19308
|
+
const { data, error: error51 } = await client.auth.getUser(accessToken);
|
|
19309
|
+
if (error51?.status === 429) {
|
|
19310
|
+
throw new NaroError(
|
|
19311
|
+
"RATE_LIMITED",
|
|
19312
|
+
"Checking the access token failed: Naro's sign-in service is rate limiting this machine. Try again shortly.",
|
|
19313
|
+
{ cause: error51 }
|
|
19314
|
+
);
|
|
19315
|
+
}
|
|
19316
|
+
if (error51 && (isAuthRetryableFetchError(error51) || (error51.status ?? 0) >= 500)) {
|
|
19317
|
+
throw new NaroError(
|
|
19318
|
+
"NETWORK_ERROR",
|
|
19319
|
+
"Checking the access token failed: could not reach Naro's sign-in service.",
|
|
19320
|
+
{ cause: error51 }
|
|
19321
|
+
);
|
|
19322
|
+
}
|
|
19323
|
+
if (error51 || !data?.user) {
|
|
19324
|
+
throw new NaroError(
|
|
19325
|
+
"NOT_LOGGED_IN",
|
|
19326
|
+
"The access token is invalid or has expired. Generate a new one with `naro token`, or run `naro login`.",
|
|
19327
|
+
{ cause: error51 ?? void 0 }
|
|
19328
|
+
);
|
|
19329
|
+
}
|
|
19330
|
+
return toSessionUser(data.user);
|
|
19331
|
+
}
|
|
18987
19332
|
async function refreshStoredSession(refreshToken, client = createAuthClient()) {
|
|
18988
19333
|
const { data, error: error51 } = await client.auth.refreshSession({
|
|
18989
19334
|
refresh_token: refreshToken
|
|
@@ -19024,16 +19369,19 @@ async function login(deps) {
|
|
|
19024
19369
|
}
|
|
19025
19370
|
|
|
19026
19371
|
// ../../packages/sdk/src/auth.ts
|
|
19372
|
+
function suppliedToken(options = {}) {
|
|
19373
|
+
if (options.explicitToken)
|
|
19374
|
+
return { token: options.explicitToken, source: "flag" };
|
|
19375
|
+
const fromEnv = (options.env ?? process.env).NARO_TOKEN;
|
|
19376
|
+
return fromEnv ? { token: fromEnv, source: "env" } : null;
|
|
19377
|
+
}
|
|
19027
19378
|
async function resolveAuth(options = {}) {
|
|
19028
19379
|
const env = options.env ?? process.env;
|
|
19029
19380
|
const home = options.home ?? homedir2();
|
|
19030
|
-
|
|
19031
|
-
|
|
19032
|
-
|
|
19033
|
-
|
|
19034
|
-
if (env.NARO_TOKEN) {
|
|
19035
|
-
const token = env.NARO_TOKEN;
|
|
19036
|
-
return { getAccessToken: async () => token, source: "env" };
|
|
19381
|
+
const supplied = suppliedToken({ explicitToken: options.explicitToken, env });
|
|
19382
|
+
if (supplied) {
|
|
19383
|
+
const { token, source } = supplied;
|
|
19384
|
+
return { getAccessToken: async () => token, source };
|
|
19037
19385
|
}
|
|
19038
19386
|
const refresh = options.refresh ?? refreshStoredSession;
|
|
19039
19387
|
let session = loadStoredSession(home);
|
|
@@ -19109,6 +19457,14 @@ function openBrowser(url2) {
|
|
|
19109
19457
|
child.unref();
|
|
19110
19458
|
}
|
|
19111
19459
|
|
|
19460
|
+
// ../../packages/sdk/src/environments.ts
|
|
19461
|
+
function isReservedEnvTarget(value) {
|
|
19462
|
+
return RESERVED_ENVIRONMENT_SLUGS.includes(value);
|
|
19463
|
+
}
|
|
19464
|
+
function isEnvTargetName(value) {
|
|
19465
|
+
return environmentSlugProblem(value) !== "invalid";
|
|
19466
|
+
}
|
|
19467
|
+
|
|
19112
19468
|
// ../../packages/sdk/src/insights.ts
|
|
19113
19469
|
var SPEED_INSIGHTS_METRICS = [
|
|
19114
19470
|
"lcp",
|
|
@@ -19179,6 +19535,7 @@ function deploymentInfo(row) {
|
|
|
19179
19535
|
projectId: row.project_id,
|
|
19180
19536
|
status: row.status,
|
|
19181
19537
|
branch: row.branch,
|
|
19538
|
+
environment: row.environment_slug ?? null,
|
|
19182
19539
|
url: row.preview_url,
|
|
19183
19540
|
artifactKey: row.artifact_key ?? null,
|
|
19184
19541
|
errorCode: row.error_code,
|
|
@@ -19220,6 +19577,7 @@ var NaroClient = class {
|
|
|
19220
19577
|
insights;
|
|
19221
19578
|
analytics;
|
|
19222
19579
|
protection;
|
|
19580
|
+
environments;
|
|
19223
19581
|
constructor(options) {
|
|
19224
19582
|
this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
|
|
19225
19583
|
this.getAccessToken = options.getAccessToken;
|
|
@@ -19238,6 +19596,7 @@ var NaroClient = class {
|
|
|
19238
19596
|
this.insights = new InsightsApi(this);
|
|
19239
19597
|
this.analytics = new AnalyticsApi(this);
|
|
19240
19598
|
this.protection = new ProtectionApi(this);
|
|
19599
|
+
this.environments = new EnvironmentsApi(this);
|
|
19241
19600
|
}
|
|
19242
19601
|
async request(path, options) {
|
|
19243
19602
|
const token = await this.getAccessToken();
|
|
@@ -19282,6 +19641,20 @@ var TeamsApi = class {
|
|
|
19282
19641
|
);
|
|
19283
19642
|
return body.teams;
|
|
19284
19643
|
}
|
|
19644
|
+
/**
|
|
19645
|
+
* The team's usage for a UTC month, the current one without `month`
|
|
19646
|
+
* (spec 2026-10-18-usage-cli-mcp). The month goes as given: the server
|
|
19647
|
+
* checks it and answers 400 VALIDATION_FAILED otherwise.
|
|
19648
|
+
*/
|
|
19649
|
+
async usage(teamSlug, query = {}) {
|
|
19650
|
+
const params = new URLSearchParams();
|
|
19651
|
+
if (query.month !== void 0) params.set("month", query.month);
|
|
19652
|
+
const text2 = params.toString();
|
|
19653
|
+
return this.client.request(
|
|
19654
|
+
`/api/teams/${encodeURIComponent(teamSlug)}/usage${text2 ? `?${text2}` : ""}`,
|
|
19655
|
+
{ context: "Read team usage" }
|
|
19656
|
+
);
|
|
19657
|
+
}
|
|
19285
19658
|
};
|
|
19286
19659
|
var ProjectsApi = class {
|
|
19287
19660
|
constructor(client) {
|
|
@@ -19388,7 +19761,10 @@ var DeploymentsApi = class {
|
|
|
19388
19761
|
{
|
|
19389
19762
|
method: "POST",
|
|
19390
19763
|
// createDeploymentSchema accepts string | undefined — never null.
|
|
19391
|
-
body:
|
|
19764
|
+
body: {
|
|
19765
|
+
...options.branch ? { branch: options.branch } : {},
|
|
19766
|
+
...options.environment ? { environment: options.environment } : {}
|
|
19767
|
+
},
|
|
19392
19768
|
context: "Create deployment"
|
|
19393
19769
|
}
|
|
19394
19770
|
);
|
|
@@ -19404,7 +19780,7 @@ var DeploymentsApi = class {
|
|
|
19404
19780
|
async rollbackTarget(projectId, defaultBranch) {
|
|
19405
19781
|
const deployments = await this.list(projectId);
|
|
19406
19782
|
const production = deployments.filter(
|
|
19407
|
-
(deployment) => deployment.status === "READY" && deployment.branch === defaultBranch
|
|
19783
|
+
(deployment) => deployment.status === "READY" && deployment.environment === null && deployment.branch === defaultBranch
|
|
19408
19784
|
);
|
|
19409
19785
|
const current = production[0];
|
|
19410
19786
|
if (!current) {
|
|
@@ -19444,7 +19820,7 @@ var DeploymentsApi = class {
|
|
|
19444
19820
|
async wait(deploymentId, options = {}) {
|
|
19445
19821
|
const intervalMs = options.intervalMs ?? 2500;
|
|
19446
19822
|
const timeoutMs = options.timeoutMs ?? 15 * 6e4;
|
|
19447
|
-
const sleep2 = options.sleep ?? ((ms) => new Promise((
|
|
19823
|
+
const sleep2 = options.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
|
|
19448
19824
|
const startedAt = Date.now();
|
|
19449
19825
|
let lastStatus = null;
|
|
19450
19826
|
let lastSequence = -1;
|
|
@@ -19507,11 +19883,16 @@ var EnvApi = class {
|
|
|
19507
19883
|
{ method: "DELETE", context: "Remove environment variable" }
|
|
19508
19884
|
);
|
|
19509
19885
|
}
|
|
19510
|
-
/**
|
|
19886
|
+
/**
|
|
19887
|
+
* Removes a key's rows: the one `target` names, or, with no target, the
|
|
19888
|
+
* `production`, `preview` and `all` rows. An environment's value goes only
|
|
19889
|
+
* when its slug is the target, so removing a key everywhere else does not
|
|
19890
|
+
* take a QA or staging value with it.
|
|
19891
|
+
*/
|
|
19511
19892
|
async remove(projectId, key, target2) {
|
|
19512
19893
|
const rows = await this.list(projectId);
|
|
19513
19894
|
const matches = rows.filter(
|
|
19514
|
-
(row) => row.key === key && (target2 === void 0
|
|
19895
|
+
(row) => row.key === key && (target2 === void 0 ? isReservedEnvTarget(row.target) : row.target === target2)
|
|
19515
19896
|
);
|
|
19516
19897
|
if (matches.length === 0) {
|
|
19517
19898
|
throw new NaroError(
|
|
@@ -19804,6 +20185,55 @@ var ProtectionApi = class {
|
|
|
19804
20185
|
);
|
|
19805
20186
|
}
|
|
19806
20187
|
};
|
|
20188
|
+
var EnvironmentsApi = class {
|
|
20189
|
+
constructor(client) {
|
|
20190
|
+
this.client = client;
|
|
20191
|
+
}
|
|
20192
|
+
base(projectId) {
|
|
20193
|
+
return `/api/projects/${encodeURIComponent(projectId)}/environments`;
|
|
20194
|
+
}
|
|
20195
|
+
/** The project's environments and how many its plan allows (null: no limit). */
|
|
20196
|
+
async list(projectId) {
|
|
20197
|
+
return this.client.request(this.base(projectId), {
|
|
20198
|
+
context: "List environments"
|
|
20199
|
+
});
|
|
20200
|
+
}
|
|
20201
|
+
/** One environment by its slug, from the list: the routes address it by id. */
|
|
20202
|
+
async get(projectId, slug) {
|
|
20203
|
+
const { environments } = await this.list(projectId);
|
|
20204
|
+
const environment = environments.find((entry) => entry.slug === slug);
|
|
20205
|
+
if (!environment)
|
|
20206
|
+
throw new NaroError("ENVIRONMENT_NOT_FOUND", "Environment not found.");
|
|
20207
|
+
return environment;
|
|
20208
|
+
}
|
|
20209
|
+
async create(projectId, input) {
|
|
20210
|
+
const body = await this.client.request(
|
|
20211
|
+
this.base(projectId),
|
|
20212
|
+
{
|
|
20213
|
+
method: "POST",
|
|
20214
|
+
// JSON drops a key that is undefined and keeps a null, which is what
|
|
20215
|
+
// the route reads: no copyEnvFrom is "preview", null copies nothing.
|
|
20216
|
+
body: input,
|
|
20217
|
+
context: "Create environment"
|
|
20218
|
+
}
|
|
20219
|
+
);
|
|
20220
|
+
return body.environment;
|
|
20221
|
+
}
|
|
20222
|
+
async update(projectId, environmentId, patch) {
|
|
20223
|
+
const body = await this.client.request(
|
|
20224
|
+
`${this.base(projectId)}/${encodeURIComponent(environmentId)}`,
|
|
20225
|
+
{ method: "PATCH", body: patch, context: "Update environment" }
|
|
20226
|
+
);
|
|
20227
|
+
return body.environment;
|
|
20228
|
+
}
|
|
20229
|
+
/** Removes the environment, its worker and its protection. */
|
|
20230
|
+
async delete(projectId, environmentId) {
|
|
20231
|
+
await this.client.request(
|
|
20232
|
+
`${this.base(projectId)}/${encodeURIComponent(environmentId)}`,
|
|
20233
|
+
{ method: "DELETE", context: "Delete environment" }
|
|
20234
|
+
);
|
|
20235
|
+
}
|
|
20236
|
+
};
|
|
19807
20237
|
var EXPORT_FIRST_POLL_MS = 1e3;
|
|
19808
20238
|
var EXPORT_MAX_POLL_MS = 5e3;
|
|
19809
20239
|
var EXPORT_TIMEOUT_MS = 15 * 6e4;
|
|
@@ -19941,7 +20371,7 @@ var DbApi = class {
|
|
|
19941
20371
|
*/
|
|
19942
20372
|
async export(projectId, options = {}, wait = {}) {
|
|
19943
20373
|
const timeoutMs = wait.timeoutMs ?? EXPORT_TIMEOUT_MS;
|
|
19944
|
-
const sleep2 = wait.sleep ?? ((ms) => new Promise((
|
|
20374
|
+
const sleep2 = wait.sleep ?? ((ms) => new Promise((resolve12) => setTimeout(resolve12, ms)));
|
|
19945
20375
|
const given = wait.pollIntervalMs;
|
|
19946
20376
|
const firstPollMs = given !== void 0 && Number.isFinite(given) && given > 0 ? given : EXPORT_FIRST_POLL_MS;
|
|
19947
20377
|
const maxPollMs = Math.max(EXPORT_MAX_POLL_MS, firstPollMs);
|
|
@@ -19988,10 +20418,12 @@ var DbApi = class {
|
|
|
19988
20418
|
* `d1_migrations` — all operations or none. `dryRun` returns the SQL and
|
|
19989
20419
|
* the name without applying anything. Save `sql` as `migrations/<name>` so
|
|
19990
20420
|
* `naro db reset` re-applies it; a refused operation's index is in
|
|
19991
|
-
* `details.operation
|
|
19992
|
-
*
|
|
19993
|
-
*
|
|
19994
|
-
*
|
|
20421
|
+
* `details.operation`, or — refused by the policy translation, or failed
|
|
20422
|
+
* in SQLite — in `statementIndex` (one statement per operation). The Data API policies go in the same migration: a
|
|
20423
|
+
* rename carries the table's and rewrites the others that read it, a
|
|
20424
|
+
* dropped table's go with it, and what an earlier table left under a name
|
|
20425
|
+
* the change gives is removed — `removedPolicies` lists what went. A change
|
|
20426
|
+
* that fails changes none of them.
|
|
19995
20427
|
*/
|
|
19996
20428
|
async schema(projectId, operations, options = {}) {
|
|
19997
20429
|
return this.client.request(
|
|
@@ -20099,6 +20531,7 @@ function listFields(options) {
|
|
|
20099
20531
|
if (options.limit !== void 0) body.limit = options.limit;
|
|
20100
20532
|
if (options.offset !== void 0) body.offset = options.offset;
|
|
20101
20533
|
if (options.sortBy !== void 0) body.sortBy = options.sortBy;
|
|
20534
|
+
if (options.search !== void 0) body.search = options.search;
|
|
20102
20535
|
return body;
|
|
20103
20536
|
}
|
|
20104
20537
|
var BackendApi = class {
|
|
@@ -20629,10 +21062,13 @@ var StorageApi = class {
|
|
|
20629
21062
|
* What is directly below a folder of a bucket — its folders first, by
|
|
20630
21063
|
* name, then its files — through the control plane's object proxy
|
|
20631
21064
|
* (Storage 3-1e): the project's Naro worker lists it with the stored
|
|
20632
|
-
* service key, so every object is listed whatever the policies say.
|
|
20633
|
-
*
|
|
20634
|
-
*
|
|
20635
|
-
*
|
|
21065
|
+
* service key, so every object is listed whatever the policies say.
|
|
21066
|
+
* `search` keeps the names below the folder that start with it (Storage
|
|
21067
|
+
* 3-1g); a backend from before it answers `INVALID_REQUEST` with
|
|
21068
|
+
* `details.path` `search` until `naro backend enable` updates it. Needs
|
|
21069
|
+
* `storage.manage`, Storage on and the backend on and ready
|
|
21070
|
+
* (`STORAGE_NOT_ENABLED`, `BACKEND_OFF`, `BACKEND_NOT_READY`, 409); the
|
|
21071
|
+
* worker's refusals — `BUCKET_NOT_FOUND`, `INVALID_REQUEST` with
|
|
20636
21072
|
* `details.path` and `details.reason` — come through as they are.
|
|
20637
21073
|
*/
|
|
20638
21074
|
async listObjects(projectId, bucket, options = {}) {
|
|
@@ -20735,7 +21171,7 @@ import { join as join19 } from "path";
|
|
|
20735
21171
|
function parseDotenv(text2) {
|
|
20736
21172
|
const out = {};
|
|
20737
21173
|
for (const line of text2.split(/\r?\n/)) {
|
|
20738
|
-
const trimmed = line.trim();
|
|
21174
|
+
const trimmed = line.trim().replace(/^export\s+(?=[^=\s])/, "");
|
|
20739
21175
|
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
20740
21176
|
const eq = trimmed.indexOf("=");
|
|
20741
21177
|
if (eq <= 0) continue;
|
|
@@ -21295,7 +21731,7 @@ var CATALOG = {
|
|
|
21295
21731
|
},
|
|
21296
21732
|
INVALID_REQUEST: {
|
|
21297
21733
|
status: 400,
|
|
21298
|
-
message: () => "The storage request is not valid."
|
|
21734
|
+
message: (vars) => vars?.subject === "broadcast" ? "The broadcast request is not valid." : vars?.subject === "realtime" ? "The realtime request is not valid." : "The storage request is not valid."
|
|
21299
21735
|
},
|
|
21300
21736
|
LENGTH_REQUIRED: {
|
|
21301
21737
|
status: 411,
|
|
@@ -21356,6 +21792,21 @@ var CATALOG = {
|
|
|
21356
21792
|
BACKEND_OUTDATED: {
|
|
21357
21793
|
status: 409,
|
|
21358
21794
|
message: () => STORAGE_MIGRATION_PENDING_MESSAGE
|
|
21795
|
+
},
|
|
21796
|
+
// Realtime 3-2a (design §6.4): see ErrorVars. 426 carries `Upgrade:
|
|
21797
|
+
// websocket` (the worker adds it); RATE_LIMITED is 429 like
|
|
21798
|
+
// OBJECT_RATE_LIMITED, and the worker adds both retry headers.
|
|
21799
|
+
UPGRADE_REQUIRED: {
|
|
21800
|
+
status: 426,
|
|
21801
|
+
message: () => "This endpoint takes WebSocket connections only."
|
|
21802
|
+
},
|
|
21803
|
+
REALTIME_NOT_ENABLED: {
|
|
21804
|
+
status: 404,
|
|
21805
|
+
message: () => "Realtime is not enabled for this project."
|
|
21806
|
+
},
|
|
21807
|
+
RATE_LIMITED: {
|
|
21808
|
+
status: 429,
|
|
21809
|
+
message: () => "Too many broadcast requests to this project right now. Try again in a second."
|
|
21359
21810
|
}
|
|
21360
21811
|
};
|
|
21361
21812
|
function dataError(code, ...args) {
|
|
@@ -21750,6 +22201,72 @@ function declaredCollations(sql, columns) {
|
|
|
21750
22201
|
return collations;
|
|
21751
22202
|
}
|
|
21752
22203
|
|
|
22204
|
+
// ../../packages/data-core/src/realtime-table.ts
|
|
22205
|
+
var REALTIME_MESSAGES = "realtime.messages";
|
|
22206
|
+
var REALTIME_MESSAGES_NAMES = ["messages", REALTIME_MESSAGES];
|
|
22207
|
+
var REALTIME_POLICY_COLUMNS = [
|
|
22208
|
+
"topic",
|
|
22209
|
+
"extension",
|
|
22210
|
+
"private"
|
|
22211
|
+
];
|
|
22212
|
+
var REALTIME_COLUMN_TYPES = {
|
|
22213
|
+
topic: "TEXT",
|
|
22214
|
+
extension: "TEXT",
|
|
22215
|
+
private: "BOOLEAN"
|
|
22216
|
+
};
|
|
22217
|
+
var realtimeColumns = REALTIME_POLICY_COLUMNS.map(
|
|
22218
|
+
(name, cid) => ({
|
|
22219
|
+
name,
|
|
22220
|
+
cid,
|
|
22221
|
+
type: REALTIME_COLUMN_TYPES[name],
|
|
22222
|
+
notNull: true,
|
|
22223
|
+
primaryKey: 0,
|
|
22224
|
+
defaultValue: null,
|
|
22225
|
+
generated: false,
|
|
22226
|
+
collation: "BINARY"
|
|
22227
|
+
})
|
|
22228
|
+
);
|
|
22229
|
+
var FIXED_COLUMNS = "REALTIME_MESSAGES_TABLE is shared by every project: its columns cannot change";
|
|
22230
|
+
var FixedColumns = class extends Map {
|
|
22231
|
+
constructor(columns) {
|
|
22232
|
+
super();
|
|
22233
|
+
for (const column2 of columns) super.set(column2.name, column2);
|
|
22234
|
+
}
|
|
22235
|
+
set() {
|
|
22236
|
+
throw new TypeError(FIXED_COLUMNS);
|
|
22237
|
+
}
|
|
22238
|
+
delete() {
|
|
22239
|
+
throw new TypeError(FIXED_COLUMNS);
|
|
22240
|
+
}
|
|
22241
|
+
clear() {
|
|
22242
|
+
throw new TypeError(FIXED_COLUMNS);
|
|
22243
|
+
}
|
|
22244
|
+
};
|
|
22245
|
+
var REALTIME_MESSAGES_TABLE = (() => {
|
|
22246
|
+
for (const column2 of realtimeColumns) Object.freeze(column2);
|
|
22247
|
+
Object.freeze(realtimeColumns);
|
|
22248
|
+
return Object.freeze({
|
|
22249
|
+
name: REALTIME_MESSAGES,
|
|
22250
|
+
kind: "table",
|
|
22251
|
+
columns: realtimeColumns,
|
|
22252
|
+
byName: Object.freeze(new FixedColumns(realtimeColumns)),
|
|
22253
|
+
sql: null
|
|
22254
|
+
});
|
|
22255
|
+
})();
|
|
22256
|
+
var REALTIME_SUBQUERY_PROBLEM = "realtime.messages has no rows to read; it can only be the target of a policy";
|
|
22257
|
+
var REALTIME_COMMAND_PROBLEM = "realtime.messages takes SELECT, INSERT and ALL policies only.";
|
|
22258
|
+
var REALTIME_COLUMNS_PROBLEM = "realtime.messages policies are checked once per join, so they can read topic, extension and private only";
|
|
22259
|
+
var REALTIME_TOPIC_PROBLEM = "realtime.topic() is available only in policies of realtime.messages";
|
|
22260
|
+
function quotedRealtimeMessagesProblem(written) {
|
|
22261
|
+
return `a table named "${written}" cannot be used in policies, as naro keeps the name for Realtime: write realtime.messages, without quotes, for Realtime's messages`;
|
|
22262
|
+
}
|
|
22263
|
+
function quotedRealtimeAliasProblem(written) {
|
|
22264
|
+
return `an alias named "${written}" cannot be used in policies, as naro keeps the name for Realtime: give the subquery another alias`;
|
|
22265
|
+
}
|
|
22266
|
+
function realtimeFunctionProblem(name) {
|
|
22267
|
+
return `function realtime.${name} is not available in policies: use realtime.topic()`;
|
|
22268
|
+
}
|
|
22269
|
+
|
|
21753
22270
|
// ../../packages/data-core/src/schema.ts
|
|
21754
22271
|
function columnAffinity(column2) {
|
|
21755
22272
|
const type = asciiLower(column2.type);
|
|
@@ -21902,11 +22419,14 @@ function storageFromRows(rows) {
|
|
|
21902
22419
|
function resolveTable(snapshot, name) {
|
|
21903
22420
|
if (isSystemTable(name)) return void 0;
|
|
21904
22421
|
const folded = asciiLower(name);
|
|
21905
|
-
if (folded === STORAGE_OBJECTS
|
|
22422
|
+
if (folded === STORAGE_OBJECTS || folded === REALTIME_MESSAGES)
|
|
22423
|
+
return void 0;
|
|
21906
22424
|
return snapshot.tables.get(folded);
|
|
21907
22425
|
}
|
|
21908
22426
|
function resolvePolicyTable(snapshot, name) {
|
|
21909
|
-
|
|
22427
|
+
const folded = asciiLower(name);
|
|
22428
|
+
if (folded === STORAGE_OBJECTS) return snapshot.storage;
|
|
22429
|
+
if (folded === REALTIME_MESSAGES) return REALTIME_MESSAGES_TABLE;
|
|
21910
22430
|
return resolveTable(snapshot, name);
|
|
21911
22431
|
}
|
|
21912
22432
|
async function readTableNames(db) {
|
|
@@ -22127,7 +22647,8 @@ var VALUE_KINDS = /* @__PURE__ */ new Set([
|
|
|
22127
22647
|
"jwt",
|
|
22128
22648
|
"func",
|
|
22129
22649
|
"cast",
|
|
22130
|
-
"storage"
|
|
22650
|
+
"storage",
|
|
22651
|
+
"realtime"
|
|
22131
22652
|
]);
|
|
22132
22653
|
function isValueExpr(expr) {
|
|
22133
22654
|
return VALUE_KINDS.has(expr.kind);
|
|
@@ -22241,6 +22762,8 @@ function truthProblem(operand) {
|
|
|
22241
22762
|
const call = operand.fn === "foldername" ? "(storage.foldername(\u2026))[n]" : `storage.${operand.fn}(\u2026)`;
|
|
22242
22763
|
return `${call} ${text2}: compare it with text, ${call} = 'true'`;
|
|
22243
22764
|
}
|
|
22765
|
+
case "realtime":
|
|
22766
|
+
return `realtime.topic() ${text2}: compare it with text, realtime.topic() = 'room:1'`;
|
|
22244
22767
|
}
|
|
22245
22768
|
}
|
|
22246
22769
|
function truthTestProblem(operand) {
|
|
@@ -22261,6 +22784,7 @@ function isTextValue(value) {
|
|
|
22261
22784
|
case "jwt":
|
|
22262
22785
|
case "cast":
|
|
22263
22786
|
case "storage":
|
|
22787
|
+
case "realtime":
|
|
22264
22788
|
return true;
|
|
22265
22789
|
case "func":
|
|
22266
22790
|
return value.name !== "length" && value.name !== "coalesce";
|
|
@@ -22578,12 +23102,15 @@ function readSubquery(state, start, kind) {
|
|
|
22578
23102
|
stream.expectKeyword(`from`);
|
|
22579
23103
|
const table = readTableName(stream);
|
|
22580
23104
|
let alias = null;
|
|
22581
|
-
|
|
22582
|
-
|
|
22583
|
-
|
|
22584
|
-
|
|
22585
|
-
|
|
22586
|
-
|
|
23105
|
+
const as = stream.acceptKeyword("as");
|
|
23106
|
+
const named = stream.peek();
|
|
23107
|
+
if (as || named.kind === "ident" && (named.quoted || !isReservedWord(named)))
|
|
23108
|
+
alias = readIdentifier(stream, "an alias");
|
|
23109
|
+
if (alias !== null && asciiLower(alias) === REALTIME_MESSAGES)
|
|
23110
|
+
throw new PolicySyntaxError(
|
|
23111
|
+
quotedRealtimeAliasProblem(alias),
|
|
23112
|
+
named.position
|
|
23113
|
+
);
|
|
22587
23114
|
let where = null;
|
|
22588
23115
|
if (stream.acceptKeyword("where")) {
|
|
22589
23116
|
const token = stream.peek();
|
|
@@ -22623,10 +23150,15 @@ function isName(token, word) {
|
|
|
22623
23150
|
function atStorageObjects(stream) {
|
|
22624
23151
|
return isName(stream.peek(), "storage") && isOperator(stream.peek(1), ".") && isName(stream.peek(2), "objects");
|
|
22625
23152
|
}
|
|
23153
|
+
function atRealtimeMessages(stream) {
|
|
23154
|
+
return isName(stream.peek(), "realtime") && isOperator(stream.peek(1), ".") && isName(stream.peek(2), "messages");
|
|
23155
|
+
}
|
|
22626
23156
|
function readTableName(stream) {
|
|
22627
23157
|
const first = stream.peek();
|
|
22628
23158
|
if (atStorageObjects(stream))
|
|
22629
23159
|
throw new PolicySyntaxError(STORAGE_SUBQUERY_PROBLEM, first.position);
|
|
23160
|
+
if (atRealtimeMessages(stream))
|
|
23161
|
+
throw new PolicySyntaxError(REALTIME_SUBQUERY_PROBLEM, first.position);
|
|
22630
23162
|
let token = first;
|
|
22631
23163
|
let name = readIdentifier(stream, "a table name");
|
|
22632
23164
|
if (stream.acceptOperator(".")) {
|
|
@@ -22643,14 +23175,27 @@ function readTableName(stream) {
|
|
|
22643
23175
|
quotedStorageObjectsProblem(name),
|
|
22644
23176
|
token.position
|
|
22645
23177
|
);
|
|
23178
|
+
if (asciiLower(name) === REALTIME_MESSAGES)
|
|
23179
|
+
throw new PolicySyntaxError(
|
|
23180
|
+
quotedRealtimeMessagesProblem(name),
|
|
23181
|
+
token.position
|
|
23182
|
+
);
|
|
22646
23183
|
return name;
|
|
22647
23184
|
}
|
|
22648
23185
|
function readPolicyTarget(stream) {
|
|
22649
|
-
if (
|
|
22650
|
-
|
|
22651
|
-
|
|
22652
|
-
|
|
22653
|
-
|
|
23186
|
+
if (atStorageObjects(stream)) {
|
|
23187
|
+
stream.next();
|
|
23188
|
+
stream.next();
|
|
23189
|
+
stream.next();
|
|
23190
|
+
return STORAGE_OBJECTS;
|
|
23191
|
+
}
|
|
23192
|
+
if (atRealtimeMessages(stream)) {
|
|
23193
|
+
stream.next();
|
|
23194
|
+
stream.next();
|
|
23195
|
+
stream.next();
|
|
23196
|
+
return REALTIME_MESSAGES;
|
|
23197
|
+
}
|
|
23198
|
+
return readTableName(stream);
|
|
22654
23199
|
}
|
|
22655
23200
|
function readNumber(token, negative) {
|
|
22656
23201
|
const value = Number(token.value) * (negative ? -1 : 1);
|
|
@@ -22798,6 +23343,12 @@ function readPrimary(state) {
|
|
|
22798
23343
|
return stream.unexpected(token);
|
|
22799
23344
|
}
|
|
22800
23345
|
case "ident": {
|
|
23346
|
+
if (isName(token, "realtime") && isOperator(stream.peek(), ".") && stream.peek(1).kind === "ident") {
|
|
23347
|
+
if (isOperator(stream.peek(2), "("))
|
|
23348
|
+
return readRealtimeCall(state, token);
|
|
23349
|
+
if (isName(stream.peek(1), "messages") && isOperator(stream.peek(2), "."))
|
|
23350
|
+
return readRealtimeColumn(state);
|
|
23351
|
+
}
|
|
22801
23352
|
if (isName(token, "storage") && isOperator(stream.peek(), ".") && stream.peek(1).kind === "ident" && isOperator(stream.peek(2), "("))
|
|
22802
23353
|
return readStorageCall(state, token);
|
|
22803
23354
|
if (!token.quoted) {
|
|
@@ -22817,6 +23368,11 @@ function readPrimary(state) {
|
|
|
22817
23368
|
if (RESERVED2.has(word)) return stream.unexpected(token);
|
|
22818
23369
|
}
|
|
22819
23370
|
if (isOperator(stream.peek(), ".")) {
|
|
23371
|
+
if (asciiLower(token.value) === REALTIME_MESSAGES)
|
|
23372
|
+
throw new PolicySyntaxError(
|
|
23373
|
+
quotedRealtimeMessagesProblem(token.value),
|
|
23374
|
+
token.position
|
|
23375
|
+
);
|
|
22820
23376
|
stream.next();
|
|
22821
23377
|
const name = stream.peek();
|
|
22822
23378
|
if (name.kind !== "ident" || !name.quoted && isReservedWord(name))
|
|
@@ -22855,6 +23411,33 @@ function readStorageCall(state, start) {
|
|
|
22855
23411
|
if (fn === "foldername") state.foldernames.push({ node, token: start });
|
|
22856
23412
|
return node;
|
|
22857
23413
|
}
|
|
23414
|
+
function readRealtimeCall(state, start) {
|
|
23415
|
+
const { stream } = state;
|
|
23416
|
+
stream.next();
|
|
23417
|
+
const name = stream.next();
|
|
23418
|
+
if (!isName(name, "topic"))
|
|
23419
|
+
throw new PolicySyntaxError(
|
|
23420
|
+
realtimeFunctionProblem(name.value),
|
|
23421
|
+
start.position
|
|
23422
|
+
);
|
|
23423
|
+
stream.expectOperator("(");
|
|
23424
|
+
stream.expectOperator(")");
|
|
23425
|
+
return { kind: "realtime", fn: "topic" };
|
|
23426
|
+
}
|
|
23427
|
+
function readRealtimeColumn(state) {
|
|
23428
|
+
const { stream } = state;
|
|
23429
|
+
stream.next();
|
|
23430
|
+
stream.next();
|
|
23431
|
+
stream.next();
|
|
23432
|
+
const name = readIdentifier(stream, "a column name");
|
|
23433
|
+
const next = stream.peek();
|
|
23434
|
+
if (isOperator(next, "(") || isOperator(next, "."))
|
|
23435
|
+
throw new PolicySyntaxError(
|
|
23436
|
+
`realtime.messages.${name} is not a column: naro reads realtime.messages.column`,
|
|
23437
|
+
next.position
|
|
23438
|
+
);
|
|
23439
|
+
return { kind: "column", qualifier: REALTIME_MESSAGES, name };
|
|
23440
|
+
}
|
|
22858
23441
|
function readNameColumn(state, fn) {
|
|
22859
23442
|
const { stream } = state;
|
|
22860
23443
|
const first = stream.peek();
|
|
@@ -22869,6 +23452,11 @@ function readNameColumn(state, fn) {
|
|
|
22869
23452
|
}
|
|
22870
23453
|
if (!isColumn || !isOperator(after, ")"))
|
|
22871
23454
|
throw new PolicySyntaxError(storageHelperColumnProblem(fn), first.position);
|
|
23455
|
+
if (qualified && asciiLower(first.value) === REALTIME_MESSAGES)
|
|
23456
|
+
throw new PolicySyntaxError(
|
|
23457
|
+
quotedRealtimeMessagesProblem(first.value),
|
|
23458
|
+
first.position
|
|
23459
|
+
);
|
|
22872
23460
|
count(state, first);
|
|
22873
23461
|
for (let index = qualified ? 4 : 2; index > 0; index--) stream.next();
|
|
22874
23462
|
return {
|
|
@@ -22998,7 +23586,10 @@ function printValue(expr) {
|
|
|
22998
23586
|
case "literal":
|
|
22999
23587
|
return printLiteral(expr.value);
|
|
23000
23588
|
case "column":
|
|
23001
|
-
|
|
23589
|
+
if (expr.qualifier === null) return quoteIdent(expr.name);
|
|
23590
|
+
if (asciiLower(expr.qualifier) === REALTIME_MESSAGES)
|
|
23591
|
+
return `${REALTIME_MESSAGES}.${quoteIdent(expr.name)}`;
|
|
23592
|
+
return `${quoteIdent(expr.qualifier)}.${quoteIdent(expr.name)}`;
|
|
23002
23593
|
case "auth":
|
|
23003
23594
|
return `auth.${expr.fn}()`;
|
|
23004
23595
|
case "jwt":
|
|
@@ -23011,6 +23602,8 @@ function printValue(expr) {
|
|
|
23011
23602
|
const call = `storage.${expr.fn}(${printValue(expr.column)})`;
|
|
23012
23603
|
return expr.index === null ? call : `(${call})[${String(expr.index)}]`;
|
|
23013
23604
|
}
|
|
23605
|
+
case "realtime":
|
|
23606
|
+
return "realtime.topic()";
|
|
23014
23607
|
}
|
|
23015
23608
|
}
|
|
23016
23609
|
function printSubquery(subquery) {
|
|
@@ -23053,20 +23646,20 @@ function printExpr(expr) {
|
|
|
23053
23646
|
}
|
|
23054
23647
|
function unqualifiedInSubquery(expr) {
|
|
23055
23648
|
let found;
|
|
23056
|
-
function value(node,
|
|
23649
|
+
function value(node, inside2) {
|
|
23057
23650
|
if (found !== void 0) return;
|
|
23058
23651
|
switch (node.kind) {
|
|
23059
23652
|
case "column":
|
|
23060
|
-
if (
|
|
23653
|
+
if (inside2 && node.qualifier === null) found = node;
|
|
23061
23654
|
return;
|
|
23062
23655
|
case "func":
|
|
23063
|
-
for (const arg of node.args) value(arg,
|
|
23656
|
+
for (const arg of node.args) value(arg, inside2);
|
|
23064
23657
|
return;
|
|
23065
23658
|
case "cast":
|
|
23066
|
-
value(node.operand,
|
|
23659
|
+
value(node.operand, inside2);
|
|
23067
23660
|
return;
|
|
23068
23661
|
case "storage":
|
|
23069
|
-
value(node.column,
|
|
23662
|
+
value(node.column, inside2);
|
|
23070
23663
|
return;
|
|
23071
23664
|
default:
|
|
23072
23665
|
return;
|
|
@@ -23076,40 +23669,40 @@ function unqualifiedInSubquery(expr) {
|
|
|
23076
23669
|
if (node.select !== "*") value(node.select, true);
|
|
23077
23670
|
if (node.where !== null) visit(node.where, true);
|
|
23078
23671
|
}
|
|
23079
|
-
function visit(node,
|
|
23672
|
+
function visit(node, inside2) {
|
|
23080
23673
|
if (found !== void 0) return;
|
|
23081
23674
|
if (isValueExpr(node)) {
|
|
23082
|
-
value(node,
|
|
23675
|
+
value(node, inside2);
|
|
23083
23676
|
return;
|
|
23084
23677
|
}
|
|
23085
23678
|
switch (node.kind) {
|
|
23086
23679
|
case "and":
|
|
23087
23680
|
case "or":
|
|
23088
|
-
for (const arg of node.args) visit(arg,
|
|
23681
|
+
for (const arg of node.args) visit(arg, inside2);
|
|
23089
23682
|
return;
|
|
23090
23683
|
case "not":
|
|
23091
|
-
visit(node.arg,
|
|
23684
|
+
visit(node.arg, inside2);
|
|
23092
23685
|
return;
|
|
23093
23686
|
case "compare":
|
|
23094
23687
|
case "distinct":
|
|
23095
|
-
value(node.left,
|
|
23096
|
-
value(node.right,
|
|
23688
|
+
value(node.left, inside2);
|
|
23689
|
+
value(node.right, inside2);
|
|
23097
23690
|
return;
|
|
23098
23691
|
case "is":
|
|
23099
23692
|
case "like":
|
|
23100
23693
|
case "array":
|
|
23101
|
-
value(node.operand,
|
|
23694
|
+
value(node.operand, inside2);
|
|
23102
23695
|
return;
|
|
23103
23696
|
case "in":
|
|
23104
|
-
value(node.operand,
|
|
23105
|
-
for (const item of node.list) value(item,
|
|
23697
|
+
value(node.operand, inside2);
|
|
23698
|
+
for (const item of node.list) value(item, inside2);
|
|
23106
23699
|
return;
|
|
23107
23700
|
case "between":
|
|
23108
23701
|
for (const item of [node.operand, node.low, node.high])
|
|
23109
|
-
value(item,
|
|
23702
|
+
value(item, inside2);
|
|
23110
23703
|
return;
|
|
23111
23704
|
case "inSelect":
|
|
23112
|
-
value(node.operand,
|
|
23705
|
+
value(node.operand, inside2);
|
|
23113
23706
|
subquery(node.subquery);
|
|
23114
23707
|
return;
|
|
23115
23708
|
case "exists":
|
|
@@ -23186,9 +23779,11 @@ function quotePolicyName(name) {
|
|
|
23186
23779
|
return /^[a-z_][a-z0-9_]*$/.test(name) && quoteIdent(name) === name ? name : `"${name.replaceAll('"', '""')}"`;
|
|
23187
23780
|
}
|
|
23188
23781
|
function policyShapeProblem(policy) {
|
|
23189
|
-
const { name, command, using, check: check2 } = policy;
|
|
23782
|
+
const { table, name, command, using, check: check2 } = policy;
|
|
23190
23783
|
if (name === "" || [...name].length > MAX_POLICY_NAME_LENGTH || name.includes("\0"))
|
|
23191
23784
|
return `Policy names are 1 to ${String(MAX_POLICY_NAME_LENGTH)} characters.`;
|
|
23785
|
+
if (asciiLower(table) === REALTIME_MESSAGES && (command === "update" || command === "delete"))
|
|
23786
|
+
return REALTIME_COMMAND_PROBLEM;
|
|
23192
23787
|
const upper = command.toUpperCase();
|
|
23193
23788
|
if ((command === "select" || command === "delete") && check2 !== null)
|
|
23194
23789
|
return "WITH CHECK cannot be applied to SELECT or DELETE.";
|
|
@@ -23203,7 +23798,8 @@ function policyShapeProblem(policy) {
|
|
|
23203
23798
|
return void 0;
|
|
23204
23799
|
}
|
|
23205
23800
|
function policyTableSql(table) {
|
|
23206
|
-
|
|
23801
|
+
const folded = asciiLower(table);
|
|
23802
|
+
return folded === STORAGE_OBJECTS || folded === REALTIME_MESSAGES ? folded : quoteIdent(table);
|
|
23207
23803
|
}
|
|
23208
23804
|
function policySql(policy) {
|
|
23209
23805
|
const parts = [
|
|
@@ -23945,6 +24541,7 @@ function unreadableConversions(policy) {
|
|
|
23945
24541
|
// ../../packages/data-core/src/rls-compile.ts
|
|
23946
24542
|
var MAX_INLINE_DEPTH = 4;
|
|
23947
24543
|
var MAX_STATEMENT_SUBQUERIES = 16;
|
|
24544
|
+
var STATEMENT_SUBQUERIES_REASON = `the policies read more than ${String(MAX_STATEMENT_SUBQUERIES)} subqueries in one statement, counting the policies of the tables they read`;
|
|
23948
24545
|
var MAX_POLICY_VALUES = 50;
|
|
23949
24546
|
var MAX_POLICY_STATEMENT_BYTES = 5e4;
|
|
23950
24547
|
function policyInvalid(policy, action) {
|
|
@@ -23996,7 +24593,7 @@ function resolveColumn(env, ref) {
|
|
|
23996
24593
|
if (!column2)
|
|
23997
24594
|
return blamed(
|
|
23998
24595
|
env,
|
|
23999
|
-
`column ${ref.qualifier}.${ref.name} does not exist: ${tableLabel(source.table)} has no column ${ref.name}`
|
|
24596
|
+
source.table.name === REALTIME_MESSAGES ? REALTIME_COLUMNS_PROBLEM : `column ${ref.qualifier}.${ref.name} does not exist: ${tableLabel(source.table)} has no column ${ref.name}`
|
|
24000
24597
|
);
|
|
24001
24598
|
return { source, column: column2 };
|
|
24002
24599
|
}
|
|
@@ -24012,6 +24609,8 @@ function resolveColumn(env, ref) {
|
|
|
24012
24609
|
if (env.scopes.length > 1) return blamed(env, unqualifiedProblem(ref));
|
|
24013
24610
|
return { source, column: column2 };
|
|
24014
24611
|
}
|
|
24612
|
+
if (env.scopes.length === 1 && env.scopes[0]?.table.name === REALTIME_MESSAGES)
|
|
24613
|
+
return blamed(env, REALTIME_COLUMNS_PROBLEM);
|
|
24015
24614
|
return blamed(
|
|
24016
24615
|
env,
|
|
24017
24616
|
missingColumnReason(
|
|
@@ -24032,8 +24631,9 @@ function valueSql(env, expr, textual = null) {
|
|
|
24032
24631
|
if (expr.value === null) return "NULL";
|
|
24033
24632
|
if (typeof expr.value === "boolean") return expr.value ? "1" : "0";
|
|
24034
24633
|
return bind(env, expr.value);
|
|
24035
|
-
case "column":
|
|
24036
|
-
|
|
24634
|
+
case "column":
|
|
24635
|
+
case "realtime": {
|
|
24636
|
+
const { source, column: column2 } = columnOf(env, expr);
|
|
24037
24637
|
return source.read(column2, env, {
|
|
24038
24638
|
qualified: env.scopes.length > 1,
|
|
24039
24639
|
textual
|
|
@@ -24060,6 +24660,16 @@ function valueSql(env, expr, textual = null) {
|
|
|
24060
24660
|
return storageCallSql(env, expr);
|
|
24061
24661
|
}
|
|
24062
24662
|
}
|
|
24663
|
+
function realtimeTopic(env) {
|
|
24664
|
+
const [source] = env.scopes;
|
|
24665
|
+
const topic = source?.table.byName.get("topic");
|
|
24666
|
+
if (asciiLower(env.blame.policy.table) !== REALTIME_MESSAGES || source?.table.name !== REALTIME_MESSAGES || !topic)
|
|
24667
|
+
return blamed(env, REALTIME_TOPIC_PROBLEM);
|
|
24668
|
+
return { source, column: topic };
|
|
24669
|
+
}
|
|
24670
|
+
function columnOf(env, expr) {
|
|
24671
|
+
return expr.kind === "column" ? resolveColumn(env, expr) : realtimeTopic(env);
|
|
24672
|
+
}
|
|
24063
24673
|
function pathArray(operand) {
|
|
24064
24674
|
return `('["' || replace(replace(${operand()}, '"', '\\"'), '/', '","') || '"]')`;
|
|
24065
24675
|
}
|
|
@@ -24100,14 +24710,14 @@ function storageCallSql(env, expr) {
|
|
|
24100
24710
|
}
|
|
24101
24711
|
function operandSide(env, expr) {
|
|
24102
24712
|
if (expr.kind === "cast") return operandSide(env, expr.operand);
|
|
24103
|
-
if (expr.kind !== "column")
|
|
24713
|
+
if (expr.kind !== "column" && expr.kind !== "realtime")
|
|
24104
24714
|
return {
|
|
24105
24715
|
stored: null,
|
|
24106
24716
|
stable: true,
|
|
24107
24717
|
newColumnish: false,
|
|
24108
24718
|
newCollation: null
|
|
24109
24719
|
};
|
|
24110
|
-
const { source, column: column2 } =
|
|
24720
|
+
const { source, column: column2 } = columnOf(env, expr);
|
|
24111
24721
|
const operand = source.operand(column2);
|
|
24112
24722
|
return {
|
|
24113
24723
|
stored: column2,
|
|
@@ -24246,10 +24856,7 @@ function subquerySql(env, subquery, select) {
|
|
|
24246
24856
|
if (!table) return blamed(env, missingRelationReason(subquery.table));
|
|
24247
24857
|
const { state } = env;
|
|
24248
24858
|
if (++state.subqueries > MAX_STATEMENT_SUBQUERIES)
|
|
24249
|
-
return blamed(
|
|
24250
|
-
env,
|
|
24251
|
-
`the policies read more than ${String(MAX_STATEMENT_SUBQUERIES)} subqueries in one statement, counting the policies of the tables they read`
|
|
24252
|
-
);
|
|
24859
|
+
return blamed(env, STATEMENT_SUBQUERIES_REASON);
|
|
24253
24860
|
if (state.depth + 1 > MAX_INLINE_DEPTH)
|
|
24254
24861
|
return blamed(
|
|
24255
24862
|
env,
|
|
@@ -24976,29 +25583,52 @@ function checkProblemReason(column2, problem) {
|
|
|
24976
25583
|
return `${column2} is a number column, which a WITH CHECK cannot turn into text (lower, upper, length, ::text)`;
|
|
24977
25584
|
}
|
|
24978
25585
|
}
|
|
25586
|
+
function usingProblemReason(name, problem) {
|
|
25587
|
+
switch (problem) {
|
|
25588
|
+
case "generated":
|
|
25589
|
+
return `${name} is a generated column, which a USING judged on a row's values cannot read`;
|
|
25590
|
+
case "collation":
|
|
25591
|
+
return `${name} has a collation naro cannot read, which a USING judged on a row's values needs to compare it`;
|
|
25592
|
+
case "pattern":
|
|
25593
|
+
return `${name} is a number column, which a USING judged on a row's values cannot compare with LIKE or ILIKE`;
|
|
25594
|
+
case "textual":
|
|
25595
|
+
return `${name} is a number column, which a USING judged on a row's values cannot turn into text (lower, upper, length, ::text)`;
|
|
25596
|
+
}
|
|
25597
|
+
}
|
|
24979
25598
|
function isTrue(term) {
|
|
24980
25599
|
return term.sql === "1" && term.params.length === 0;
|
|
24981
25600
|
}
|
|
24982
25601
|
function targetNames(target2) {
|
|
24983
|
-
|
|
25602
|
+
const folded = asciiLower(target2.name);
|
|
25603
|
+
if (folded === STORAGE_OBJECTS) return [STORAGE_OBJECTS_QUALIFIER];
|
|
25604
|
+
if (folded === REALTIME_MESSAGES) return [...REALTIME_MESSAGES_NAMES];
|
|
25605
|
+
return [folded];
|
|
24984
25606
|
}
|
|
24985
25607
|
var Rls = class {
|
|
25608
|
+
/**
|
|
25609
|
+
* `state`: the statement's — a new one, or one the policy parts of the
|
|
25610
|
+
* same statement share, so the cap on its subqueries counts them all
|
|
25611
|
+
* (`policyValue`).
|
|
25612
|
+
*/
|
|
24986
25613
|
constructor(req, snapshot, table, nonce, commands, scope = {
|
|
24987
25614
|
policyTable: table.name,
|
|
24988
25615
|
names: [asciiLower(table.name)],
|
|
24989
25616
|
alias: null
|
|
24990
|
-
}) {
|
|
25617
|
+
}, state = newState2()) {
|
|
24991
25618
|
this.req = req;
|
|
24992
25619
|
this.snapshot = snapshot;
|
|
24993
25620
|
this.table = table;
|
|
24994
25621
|
this.nonce = nonce;
|
|
25622
|
+
this.state = state;
|
|
24995
25623
|
this.policyTable = scope.policyTable;
|
|
24996
25624
|
this.outer = storedSource(table, scope.names, scope.alias);
|
|
24997
25625
|
const all = commands.flatMap((command) => {
|
|
24998
25626
|
const found = this.found(command);
|
|
24999
25627
|
return [...found.permissive, ...found.restrictive];
|
|
25000
25628
|
});
|
|
25001
|
-
|
|
25629
|
+
const key = asciiLower(table.name);
|
|
25630
|
+
if (hasSubqueries(all) && !this.state.active.includes(key))
|
|
25631
|
+
this.state.active.push(key);
|
|
25002
25632
|
for (const command of commands) {
|
|
25003
25633
|
const found = this.found(command);
|
|
25004
25634
|
for (const policy of [...found.permissive, ...found.restrictive]) {
|
|
@@ -25012,7 +25642,6 @@ var Rls = class {
|
|
|
25012
25642
|
}
|
|
25013
25643
|
}
|
|
25014
25644
|
}
|
|
25015
|
-
state = newState2();
|
|
25016
25645
|
/** Restrictive policies named by the checks' markers. */
|
|
25017
25646
|
restrictive = [];
|
|
25018
25647
|
outer;
|
|
@@ -25486,6 +26115,72 @@ function checkFragment(target2, req, snapshot, command, row, nonce, stored, with
|
|
|
25486
26115
|
}
|
|
25487
26116
|
return [schemaGuard(name, sql, command, checked), ...terms];
|
|
25488
26117
|
}
|
|
26118
|
+
var Unsupported = class extends Error {
|
|
26119
|
+
constructor(columnName, problem, policy) {
|
|
26120
|
+
super(problem);
|
|
26121
|
+
this.columnName = columnName;
|
|
26122
|
+
this.problem = problem;
|
|
26123
|
+
this.policy = policy;
|
|
26124
|
+
}
|
|
26125
|
+
};
|
|
26126
|
+
function policyValue(target2, req, snapshot, command, row, nonce, state = newState2()) {
|
|
26127
|
+
const checked = checkNonceOf(nonce, "policyValue");
|
|
26128
|
+
const source = rowSource(target2, row, "insert", void 0);
|
|
26129
|
+
const rls = new Rls(
|
|
26130
|
+
req,
|
|
26131
|
+
snapshot,
|
|
26132
|
+
target2.table,
|
|
26133
|
+
checked,
|
|
26134
|
+
[command],
|
|
26135
|
+
{
|
|
26136
|
+
policyTable: target2.name,
|
|
26137
|
+
names: targetNames(target2),
|
|
26138
|
+
alias: null
|
|
26139
|
+
},
|
|
26140
|
+
state
|
|
26141
|
+
);
|
|
26142
|
+
const found = rls.found(command);
|
|
26143
|
+
const first = found.permissive[0];
|
|
26144
|
+
if (!first) return { ok: true, fragment: { sql: "0", params: [] } };
|
|
26145
|
+
const mode = {
|
|
26146
|
+
action: command,
|
|
26147
|
+
problem: (name, problem, policy) => {
|
|
26148
|
+
throw new Unsupported(name, problem, policy);
|
|
26149
|
+
}
|
|
26150
|
+
};
|
|
26151
|
+
let group;
|
|
26152
|
+
try {
|
|
26153
|
+
group = groupTerms(
|
|
26154
|
+
{ ...rls.env(mode, first, command), scopes: [source] },
|
|
26155
|
+
[source],
|
|
26156
|
+
command,
|
|
26157
|
+
found.permissive,
|
|
26158
|
+
found.restrictive,
|
|
26159
|
+
command === "select" ? (policy) => policy.usingExpr : (policy) => policy.checkExpr ?? policy.usingExpr
|
|
26160
|
+
);
|
|
26161
|
+
} catch (error51) {
|
|
26162
|
+
if (!(error51 instanceof Unsupported)) throw error51;
|
|
26163
|
+
return {
|
|
26164
|
+
ok: false,
|
|
26165
|
+
unsupported: command === "select" ? usingProblemReason(error51.columnName, error51.problem) : checkProblemReason(error51.columnName, error51.problem),
|
|
26166
|
+
policy: error51.policy.name
|
|
26167
|
+
};
|
|
26168
|
+
}
|
|
26169
|
+
const kept = [
|
|
26170
|
+
group.permissive,
|
|
26171
|
+
...group.restrictive.map((entry) => entry.term)
|
|
26172
|
+
].filter((term) => !isTrue(term));
|
|
26173
|
+
const [only] = kept;
|
|
26174
|
+
if (!only) return { ok: true, fragment: { sql: "1", params: [] } };
|
|
26175
|
+
if (kept.length === 1) return { ok: true, fragment: only };
|
|
26176
|
+
return {
|
|
26177
|
+
ok: true,
|
|
26178
|
+
fragment: {
|
|
26179
|
+
sql: `(${kept.map((term) => term.sql).join(" AND ")})`,
|
|
26180
|
+
params: kept.flatMap((term) => term.params)
|
|
26181
|
+
}
|
|
26182
|
+
};
|
|
26183
|
+
}
|
|
25489
26184
|
function compileSelect(query, table, rls) {
|
|
25490
26185
|
if (query.columns !== "*" && query.columns.length === 0)
|
|
25491
26186
|
fail(invalidQuery("columns", "must name at least one column"));
|
|
@@ -25769,6 +26464,12 @@ function resolve8(scopes, qualifier, name) {
|
|
|
25769
26464
|
}
|
|
25770
26465
|
return void 0;
|
|
25771
26466
|
}
|
|
26467
|
+
function realtimeTopic2(scopes) {
|
|
26468
|
+
const [root] = scopes;
|
|
26469
|
+
if (root?.table.name !== REALTIME_MESSAGES) return void 0;
|
|
26470
|
+
const column2 = root.table.byName.get("topic");
|
|
26471
|
+
return column2 ? { column: column2, scope: 0 } : void 0;
|
|
26472
|
+
}
|
|
25772
26473
|
function valueScopes(scopes, value) {
|
|
25773
26474
|
switch (value.kind) {
|
|
25774
26475
|
case "column": {
|
|
@@ -25781,6 +26482,10 @@ function valueScopes(scopes, value) {
|
|
|
25781
26482
|
return valueScopes(scopes, value.operand);
|
|
25782
26483
|
case "storage":
|
|
25783
26484
|
return valueScopes(scopes, value.column);
|
|
26485
|
+
case "realtime": {
|
|
26486
|
+
const found = realtimeTopic2(scopes);
|
|
26487
|
+
return found ? [found.scope] : [];
|
|
26488
|
+
}
|
|
25784
26489
|
default:
|
|
25785
26490
|
return [];
|
|
25786
26491
|
}
|
|
@@ -25841,16 +26546,19 @@ function columnAffinityOf(column2) {
|
|
|
25841
26546
|
if (affinity2 === "BLOB") return "blob";
|
|
25842
26547
|
return "numeric";
|
|
25843
26548
|
}
|
|
26549
|
+
function operandColumn(scopes, value) {
|
|
26550
|
+
if (value.kind === "realtime") return realtimeTopic2(scopes);
|
|
26551
|
+
if (value.kind !== "column") return void 0;
|
|
26552
|
+
return resolve8(scopes, value.qualifier, value.name);
|
|
26553
|
+
}
|
|
25844
26554
|
function valueAffinity(scopes, value) {
|
|
25845
26555
|
if (value.kind === "cast") return "text";
|
|
25846
|
-
|
|
25847
|
-
const found = resolve8(scopes, value.qualifier, value.name);
|
|
26556
|
+
const found = operandColumn(scopes, value);
|
|
25848
26557
|
return found ? columnAffinityOf(found.column) : "none";
|
|
25849
26558
|
}
|
|
25850
26559
|
function valueCollation(scopes, value) {
|
|
25851
26560
|
if (value.kind === "cast") return valueCollation(scopes, value.operand);
|
|
25852
|
-
|
|
25853
|
-
const found = resolve8(scopes, value.qualifier, value.name);
|
|
26561
|
+
const found = operandColumn(scopes, value);
|
|
25854
26562
|
return found ? found.column.collation : void 0;
|
|
25855
26563
|
}
|
|
25856
26564
|
function waysIn(scopes, where) {
|
|
@@ -25913,7 +26621,8 @@ function blocked(table, indexes, way) {
|
|
|
25913
26621
|
return void 0;
|
|
25914
26622
|
}
|
|
25915
26623
|
function problemMessage(policy, outer, table, ways, why) {
|
|
25916
|
-
const
|
|
26624
|
+
const runs = outer.name === REALTIME_MESSAGES ? "runs once for every channel join it checks" : `runs once for every row of ${tableLabel(outer)} it looks at`;
|
|
26625
|
+
const start = `Policy ${policy.name} on ${tableLabel(outer)}: its subquery on ${table.name} ${runs}`;
|
|
25917
26626
|
const override = "or send it with allow_unindexed=1 (naro policy create --allow-unindexed) to accept a scan of every row";
|
|
25918
26627
|
const [way] = ways;
|
|
25919
26628
|
if (!way)
|
|
@@ -25932,7 +26641,8 @@ function unindexedSubqueries(snapshot, indexes, policies) {
|
|
|
25932
26641
|
if (policy.invalid !== null) continue;
|
|
25933
26642
|
const outer = resolvePolicyTable(snapshot, policy.table);
|
|
25934
26643
|
if (!outer) continue;
|
|
25935
|
-
const
|
|
26644
|
+
const folded = asciiLower(policy.table);
|
|
26645
|
+
const outerNames = folded === STORAGE_OBJECTS ? [STORAGE_OBJECTS_QUALIFIER] : folded === REALTIME_MESSAGES ? REALTIME_MESSAGES_NAMES : [asciiLower(outer.name)];
|
|
25936
26646
|
const visit = (scopes, expr) => {
|
|
25937
26647
|
if (expr === null || isValueExpr(expr)) return;
|
|
25938
26648
|
switch (expr.kind) {
|
|
@@ -25977,13 +26687,182 @@ function unindexedSubqueries(snapshot, indexes, policies) {
|
|
|
25977
26687
|
return;
|
|
25978
26688
|
}
|
|
25979
26689
|
};
|
|
25980
|
-
const root = [{ table: outer, names:
|
|
26690
|
+
const root = [{ table: outer, names: outerNames }];
|
|
25981
26691
|
visit(root, policy.usingExpr);
|
|
25982
26692
|
visit(root, policy.checkExpr);
|
|
25983
26693
|
}
|
|
25984
26694
|
return found;
|
|
25985
26695
|
}
|
|
25986
26696
|
|
|
26697
|
+
// ../../packages/data-core/src/realtime-protocol.ts
|
|
26698
|
+
var REALTIME_PROTOCOL_VERSION = "1.0.0";
|
|
26699
|
+
var REALTIME_CLOSE = {
|
|
26700
|
+
UNSUPPORTED_PROTOCOL: {
|
|
26701
|
+
code: 4e3,
|
|
26702
|
+
message: `This server speaks the Naro realtime protocol ${REALTIME_PROTOCOL_VERSION}. Update naro-js.`
|
|
26703
|
+
},
|
|
26704
|
+
INVALID_API_KEY: {
|
|
26705
|
+
code: 4001,
|
|
26706
|
+
message: "The API key is missing or not valid for this project."
|
|
26707
|
+
},
|
|
26708
|
+
REALTIME_NOT_ENABLED: {
|
|
26709
|
+
code: 4004,
|
|
26710
|
+
message: "Realtime is not enabled for this project."
|
|
26711
|
+
},
|
|
26712
|
+
CLIENT_FLOODING: {
|
|
26713
|
+
code: 4008,
|
|
26714
|
+
message: "This connection sent messages faster than a connection may."
|
|
26715
|
+
},
|
|
26716
|
+
TOO_MANY_CONNECTIONS: {
|
|
26717
|
+
code: 4029,
|
|
26718
|
+
message: "This project has reached its limit of concurrent realtime connections. Try again later."
|
|
26719
|
+
},
|
|
26720
|
+
CONNECTION_RATE_LIMITED: {
|
|
26721
|
+
code: 4029,
|
|
26722
|
+
message: "Too many new realtime connections to this project right now. Try again in a few seconds."
|
|
26723
|
+
},
|
|
26724
|
+
INVALID_SESSION: {
|
|
26725
|
+
code: 4401,
|
|
26726
|
+
message: "The session is expired or not valid. Sign in again."
|
|
26727
|
+
},
|
|
26728
|
+
HEARTBEAT_TIMEOUT: {
|
|
26729
|
+
code: 4408,
|
|
26730
|
+
message: "No heartbeat arrived for 75 seconds."
|
|
26731
|
+
},
|
|
26732
|
+
BACKEND_OFF: {
|
|
26733
|
+
code: 4410,
|
|
26734
|
+
message: "The project backend was turned off or deleted. Realtime stopped."
|
|
26735
|
+
},
|
|
26736
|
+
INTERNAL_ERROR: {
|
|
26737
|
+
code: 4500,
|
|
26738
|
+
message: "Something went wrong. Try again."
|
|
26739
|
+
},
|
|
26740
|
+
REALTIME_RESTARTING: {
|
|
26741
|
+
code: 4503,
|
|
26742
|
+
message: "Realtime is restarting to apply a change to this project."
|
|
26743
|
+
}
|
|
26744
|
+
};
|
|
26745
|
+
|
|
26746
|
+
// ../../packages/data-core/src/realtime-attachment.ts
|
|
26747
|
+
var ACCESS = {
|
|
26748
|
+
BROADCAST_READ: 1,
|
|
26749
|
+
BROADCAST_WRITE: 2,
|
|
26750
|
+
PRESENCE_READ: 4,
|
|
26751
|
+
PRESENCE_WRITE: 8
|
|
26752
|
+
};
|
|
26753
|
+
var MAX_ACCESS_BITS = ACCESS.BROADCAST_READ | ACCESS.BROADCAST_WRITE | ACCESS.PRESENCE_READ | ACCESS.PRESENCE_WRITE;
|
|
26754
|
+
|
|
26755
|
+
// ../../packages/data-core/src/realtime-policy.ts
|
|
26756
|
+
var FULL_ACCESS = Object.freeze({
|
|
26757
|
+
broadcastRead: true,
|
|
26758
|
+
broadcastWrite: true,
|
|
26759
|
+
presenceRead: true,
|
|
26760
|
+
presenceWrite: true
|
|
26761
|
+
});
|
|
26762
|
+
var NO_ACCESS = Object.freeze({
|
|
26763
|
+
broadcastRead: false,
|
|
26764
|
+
broadcastWrite: false,
|
|
26765
|
+
presenceRead: false,
|
|
26766
|
+
presenceWrite: false
|
|
26767
|
+
});
|
|
26768
|
+
var PUBLIC_READ = Object.freeze({
|
|
26769
|
+
broadcastRead: true,
|
|
26770
|
+
broadcastWrite: false,
|
|
26771
|
+
presenceRead: true,
|
|
26772
|
+
presenceWrite: false
|
|
26773
|
+
});
|
|
26774
|
+
var ALL_BITS = ACCESS.BROADCAST_READ | ACCESS.BROADCAST_WRITE | ACCESS.PRESENCE_READ | ACCESS.PRESENCE_WRITE;
|
|
26775
|
+
var VALUES = [
|
|
26776
|
+
{ column: "sb", command: "select", extension: "broadcast" },
|
|
26777
|
+
{ column: "ib", command: "insert", extension: "broadcast" },
|
|
26778
|
+
{ column: "sp", command: "select", extension: "presence" },
|
|
26779
|
+
{ column: "ip", command: "insert", extension: "presence" }
|
|
26780
|
+
];
|
|
26781
|
+
var TARGET = {
|
|
26782
|
+
name: REALTIME_MESSAGES,
|
|
26783
|
+
table: REALTIME_MESSAGES_TABLE
|
|
26784
|
+
};
|
|
26785
|
+
var REALTIME_VALUES_REASON = `the realtime.messages policies need more than ${String(MAX_PARAMETERS)} values in one statement`;
|
|
26786
|
+
var REALTIME_BYTES_REASON = `the realtime.messages policies need more than ${String(MAX_STATEMENT_BYTES)} bytes of SQL in one statement`;
|
|
26787
|
+
function constantOf(fragment) {
|
|
26788
|
+
if (fragment.params.length > 0) return void 0;
|
|
26789
|
+
if (fragment.sql === "1") return true;
|
|
26790
|
+
if (fragment.sql === "0") return false;
|
|
26791
|
+
return void 0;
|
|
26792
|
+
}
|
|
26793
|
+
function privateChannelAccess(input) {
|
|
26794
|
+
const { snapshot, context, policies, channel, nonce } = input;
|
|
26795
|
+
if (context.role === "service") return { ok: true, access: FULL_ACCESS };
|
|
26796
|
+
const req = { ...rlsIdentity(context), policies: policies ?? [] };
|
|
26797
|
+
const state = newState2();
|
|
26798
|
+
const fragments = [];
|
|
26799
|
+
for (const { command, extension } of VALUES) {
|
|
26800
|
+
let value;
|
|
26801
|
+
try {
|
|
26802
|
+
value = policyValue(
|
|
26803
|
+
TARGET,
|
|
26804
|
+
req,
|
|
26805
|
+
snapshot,
|
|
26806
|
+
command,
|
|
26807
|
+
{ topic: channel, extension, private: 1 },
|
|
26808
|
+
nonce,
|
|
26809
|
+
state
|
|
26810
|
+
);
|
|
26811
|
+
} catch (error51) {
|
|
26812
|
+
if (!(error51 instanceof Failure)) throw error51;
|
|
26813
|
+
return error51.reason === void 0 ? { ok: false, error: error51.error } : { ok: false, error: error51.error, reason: error51.reason };
|
|
26814
|
+
}
|
|
26815
|
+
if (!value.ok)
|
|
26816
|
+
return {
|
|
26817
|
+
ok: false,
|
|
26818
|
+
error: dataError("POLICY_INVALID", {
|
|
26819
|
+
action: command,
|
|
26820
|
+
table: REALTIME_MESSAGES,
|
|
26821
|
+
policy: value.policy
|
|
26822
|
+
}),
|
|
26823
|
+
reason: value.unsupported
|
|
26824
|
+
};
|
|
26825
|
+
fragments.push(value.fragment);
|
|
26826
|
+
}
|
|
26827
|
+
const constants = fragments.map(constantOf);
|
|
26828
|
+
const [sb, ib, sp, ip] = constants;
|
|
26829
|
+
if (sb !== void 0 && ib !== void 0 && sp !== void 0 && ip !== void 0)
|
|
26830
|
+
return {
|
|
26831
|
+
ok: true,
|
|
26832
|
+
access: {
|
|
26833
|
+
broadcastRead: sb,
|
|
26834
|
+
broadcastWrite: ib,
|
|
26835
|
+
presenceRead: sp,
|
|
26836
|
+
presenceWrite: ip
|
|
26837
|
+
}
|
|
26838
|
+
};
|
|
26839
|
+
const sql = `SELECT ${VALUES.map(
|
|
26840
|
+
({ column: column2 }, index) => `(CASE WHEN ${String(fragments[index]?.sql)} THEN 1 ELSE 0 END) AS ${column2}`
|
|
26841
|
+
).join(", ")}`;
|
|
26842
|
+
const params = fragments.flatMap((fragment) => fragment.params);
|
|
26843
|
+
if (params.length > MAX_PARAMETERS)
|
|
26844
|
+
return {
|
|
26845
|
+
ok: false,
|
|
26846
|
+
error: dataError("POLICY_INVALID", {
|
|
26847
|
+
action: "select",
|
|
26848
|
+
table: REALTIME_MESSAGES,
|
|
26849
|
+
values: MAX_PARAMETERS
|
|
26850
|
+
}),
|
|
26851
|
+
reason: REALTIME_VALUES_REASON
|
|
26852
|
+
};
|
|
26853
|
+
if (utf8ByteLength(sql) > MAX_STATEMENT_BYTES)
|
|
26854
|
+
return {
|
|
26855
|
+
ok: false,
|
|
26856
|
+
error: dataError("POLICY_INVALID", {
|
|
26857
|
+
action: "select",
|
|
26858
|
+
table: REALTIME_MESSAGES,
|
|
26859
|
+
bytes: MAX_STATEMENT_BYTES
|
|
26860
|
+
}),
|
|
26861
|
+
reason: REALTIME_BYTES_REASON
|
|
26862
|
+
};
|
|
26863
|
+
return { ok: true, statement: { sql, params } };
|
|
26864
|
+
}
|
|
26865
|
+
|
|
25987
26866
|
// ../../packages/data-core/src/storage-sql.ts
|
|
25988
26867
|
var STORAGE_NONCE_MAX_DIGITS = 64;
|
|
25989
26868
|
function nonceOf(nonce) {
|
|
@@ -26134,6 +27013,16 @@ function readObject2(i) {
|
|
|
26134
27013
|
[[i.bucket, i.name]]
|
|
26135
27014
|
);
|
|
26136
27015
|
}
|
|
27016
|
+
function readObjects(i) {
|
|
27017
|
+
return compose(
|
|
27018
|
+
[
|
|
27019
|
+
`SELECT ${OBJECT_COLUMNS} FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.name IN (SELECT value FROM json_each(?)) AND (`,
|
|
27020
|
+
i.using,
|
|
27021
|
+
")"
|
|
27022
|
+
],
|
|
27023
|
+
[[i.bucket, JSON.stringify([...new Set(i.names)])]]
|
|
27024
|
+
);
|
|
27025
|
+
}
|
|
26137
27026
|
var SORT_COLUMNS = {
|
|
26138
27027
|
name: "name",
|
|
26139
27028
|
created_at: "created_at",
|
|
@@ -26144,6 +27033,8 @@ var WIDEST_LIST_SORT = {
|
|
|
26144
27033
|
column: "updated_at",
|
|
26145
27034
|
order: "desc"
|
|
26146
27035
|
};
|
|
27036
|
+
var WIDEST_LIST_SEARCH = "s";
|
|
27037
|
+
var SEARCH_CLAUSE = " AND instr(lower(o.name), lower(?)) = 1";
|
|
26147
27038
|
function listObjects(i) {
|
|
26148
27039
|
const column2 = Object.hasOwn(SORT_COLUMNS, i.sort.column) ? SORT_COLUMNS[i.sort.column] : void 0;
|
|
26149
27040
|
const order = Object.hasOwn(SORT_ORDERS, i.sort.order) ? SORT_ORDERS[i.sort.order] : void 0;
|
|
@@ -26155,17 +27046,27 @@ function listObjects(i) {
|
|
|
26155
27046
|
const restValues = root ? [] : [p];
|
|
26156
27047
|
const under = root ? "1" : "o.name >= ? || '/' AND o.name < ? || '0'";
|
|
26157
27048
|
const underValues = root ? [] : [p, p];
|
|
27049
|
+
const searching = i.search !== "";
|
|
27050
|
+
const search = searching ? SEARCH_CLAUSE : "";
|
|
27051
|
+
const searchValues = searching ? [`${root ? "" : `${p}/`}${i.search}`] : [];
|
|
26158
27052
|
return compose(
|
|
26159
27053
|
[
|
|
26160
|
-
`SELECT * FROM (SELECT 0 AS kind, substr(${rest}, 1, instr(${rest}, '/') - 1) AS name, NULL AS id, NULL AS bucket_id, NULL AS owner, NULL AS owner_id, NULL AS version, NULL AS created_at, NULL AS updated_at, NULL AS metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND ${under} AND instr(${rest}, '/') > 0 AND (`,
|
|
27054
|
+
`SELECT * FROM (SELECT 0 AS kind, substr(${rest}, 1, instr(${rest}, '/') - 1) AS name, NULL AS id, NULL AS bucket_id, NULL AS owner, NULL AS owner_id, NULL AS version, NULL AS created_at, NULL AS updated_at, NULL AS metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND ${under} AND instr(${rest}, '/') > 0${search} AND (`,
|
|
26161
27055
|
i.using,
|
|
26162
|
-
`) GROUP BY 2 UNION ALL SELECT 1, ${rest}, o.id, o.bucket_id, o.owner, o.owner_id, o.version, o.created_at, o.updated_at, o.metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.parent =
|
|
27056
|
+
`) GROUP BY 2 UNION ALL SELECT 1, ${rest}, o.id, o.bucket_id, o.owner, o.owner_id, o.version, o.created_at, o.updated_at, o.metadata FROM ${STORAGE_OBJECTS_TABLE} AS o WHERE o.bucket_id = ? AND o.parent = ?${search} AND (`,
|
|
26163
27057
|
i.using,
|
|
26164
27058
|
`)) ORDER BY kind, CASE WHEN kind = 0 THEN name END ASC, ${column2} ${order}, name LIMIT ? OFFSET ?`
|
|
26165
27059
|
],
|
|
26166
27060
|
[
|
|
26167
|
-
[
|
|
26168
|
-
|
|
27061
|
+
[
|
|
27062
|
+
...restValues,
|
|
27063
|
+
...restValues,
|
|
27064
|
+
i.bucket,
|
|
27065
|
+
...underValues,
|
|
27066
|
+
...restValues,
|
|
27067
|
+
...searchValues
|
|
27068
|
+
],
|
|
27069
|
+
[...restValues, i.bucket, p, ...searchValues],
|
|
26169
27070
|
[i.limit, i.offset]
|
|
26170
27071
|
]
|
|
26171
27072
|
);
|
|
@@ -26446,24 +27347,77 @@ function storageStatements(select, update, remove, insertChecks, updateCheck) {
|
|
|
26446
27347
|
if (select)
|
|
26447
27348
|
statements.push(
|
|
26448
27349
|
readObject2({ bucket: value, name: value, using: select }),
|
|
27350
|
+
// Several names at once (3-1g): as many values as readObject's, 32
|
|
27351
|
+
// bytes more — the listing below is always wider, but the worker
|
|
27352
|
+
// builds this one too.
|
|
27353
|
+
readObjects({ bucket: value, names: [], using: select }),
|
|
26449
27354
|
listObjects({
|
|
26450
27355
|
bucket: value,
|
|
26451
27356
|
prefix: "p",
|
|
26452
27357
|
limit: 0,
|
|
26453
27358
|
offset: 0,
|
|
26454
27359
|
sort: WIDEST_LIST_SORT,
|
|
27360
|
+
search: WIDEST_LIST_SEARCH,
|
|
26455
27361
|
using: select
|
|
26456
27362
|
})
|
|
26457
27363
|
);
|
|
26458
27364
|
return statements;
|
|
26459
27365
|
}
|
|
27366
|
+
var REALTIME_PROBE_CHANNEL = "naro-policy-check";
|
|
27367
|
+
var REALTIME_PROBE_NONCE = "f".repeat(64);
|
|
27368
|
+
var JOIN_STATEMENT = "a join checks the SELECT and INSERT policies of realtime.messages in one statement, for broadcast and again for presence";
|
|
27369
|
+
function joinReason(reason) {
|
|
27370
|
+
if (reason === STATEMENT_SUBQUERIES_REASON)
|
|
27371
|
+
return `the SELECT and INSERT policies of realtime.messages share one budget of ${String(MAX_STATEMENT_SUBQUERIES)} subqueries per join, counting the policies of the tables they read \u2014 a join checks them in one statement, for broadcast and again for presence`;
|
|
27372
|
+
if (reason === REALTIME_VALUES_REASON || reason === REALTIME_BYTES_REASON)
|
|
27373
|
+
return JOIN_STATEMENT;
|
|
27374
|
+
return reason;
|
|
27375
|
+
}
|
|
27376
|
+
function realtimeRefusals(snapshot, policies) {
|
|
27377
|
+
const refusals = [];
|
|
27378
|
+
const applying = (command) => policies.filter(
|
|
27379
|
+
(policy) => asciiLower(policy.table) !== REALTIME_MESSAGES || policy.command === command || policy.command === "all"
|
|
27380
|
+
);
|
|
27381
|
+
for (const set2 of [policies, applying("select"), applying("insert")])
|
|
27382
|
+
for (const context of STORAGE_CONTEXTS) {
|
|
27383
|
+
const plan = privateChannelAccess({
|
|
27384
|
+
snapshot,
|
|
27385
|
+
context,
|
|
27386
|
+
policies: set2,
|
|
27387
|
+
channel: REALTIME_PROBE_CHANNEL,
|
|
27388
|
+
nonce: REALTIME_PROBE_NONCE
|
|
27389
|
+
});
|
|
27390
|
+
if (plan.ok) continue;
|
|
27391
|
+
const reason = joinReason(plan.reason);
|
|
27392
|
+
refusals.push(
|
|
27393
|
+
reason === void 0 ? { error: plan.error } : { error: plan.error, reason }
|
|
27394
|
+
);
|
|
27395
|
+
}
|
|
27396
|
+
return refusals;
|
|
27397
|
+
}
|
|
26460
27398
|
function policyOn(snapshot, name) {
|
|
26461
27399
|
const table = resolvePolicyTable(snapshot, name);
|
|
26462
27400
|
if (!table) return void 0;
|
|
26463
|
-
|
|
27401
|
+
const folded = asciiLower(name);
|
|
27402
|
+
if (folded === STORAGE_OBJECTS)
|
|
27403
|
+
return {
|
|
27404
|
+
table,
|
|
27405
|
+
label: STORAGE_OBJECTS,
|
|
27406
|
+
names: [STORAGE_OBJECTS_QUALIFIER]
|
|
27407
|
+
};
|
|
27408
|
+
if (folded === REALTIME_MESSAGES)
|
|
27409
|
+
return {
|
|
27410
|
+
table,
|
|
27411
|
+
label: REALTIME_MESSAGES,
|
|
27412
|
+
names: REALTIME_MESSAGES_NAMES
|
|
27413
|
+
};
|
|
27414
|
+
return { table, label: table.name, names: [asciiLower(table.name)] };
|
|
26464
27415
|
}
|
|
26465
27416
|
function tableLabelOf(snapshot, name) {
|
|
26466
|
-
|
|
27417
|
+
const folded = asciiLower(name);
|
|
27418
|
+
if (folded === STORAGE_OBJECTS) return STORAGE_OBJECTS;
|
|
27419
|
+
if (folded === REALTIME_MESSAGES) return REALTIME_MESSAGES;
|
|
27420
|
+
return resolveTable(snapshot, name)?.name ?? name;
|
|
26467
27421
|
}
|
|
26468
27422
|
function namedInRefusal(message, policies) {
|
|
26469
27423
|
let found;
|
|
@@ -26603,6 +27557,9 @@ function problems(snapshot, policies, options) {
|
|
|
26603
27557
|
policy: ""
|
|
26604
27558
|
});
|
|
26605
27559
|
}
|
|
27560
|
+
if (policiesOn(loaded, REALTIME_MESSAGES).length > 0)
|
|
27561
|
+
for (const { error: error51, reason } of realtimeRefusals(snapshot, loaded))
|
|
27562
|
+
refused(REALTIME_MESSAGES, error51, reason);
|
|
26606
27563
|
if (options.indexes && options.allowUnindexed !== true)
|
|
26607
27564
|
for (const problem of unindexedSubqueries(
|
|
26608
27565
|
snapshot,
|
|
@@ -26719,16 +27676,16 @@ function readCreate(stream, start) {
|
|
|
26719
27676
|
let check2 = null;
|
|
26720
27677
|
let position = start.position;
|
|
26721
27678
|
if (stream.acceptKeyword("using")) {
|
|
26722
|
-
const
|
|
26723
|
-
using =
|
|
26724
|
-
position =
|
|
27679
|
+
const clause2 = readClause(stream);
|
|
27680
|
+
using = clause2.text;
|
|
27681
|
+
position = clause2.position;
|
|
26725
27682
|
}
|
|
26726
27683
|
if (isKeyword(stream.peek(), "with")) {
|
|
26727
27684
|
stream.next();
|
|
26728
27685
|
stream.expectKeyword("check");
|
|
26729
|
-
const
|
|
26730
|
-
check2 =
|
|
26731
|
-
position =
|
|
27686
|
+
const clause2 = readClause(stream);
|
|
27687
|
+
check2 = clause2.text;
|
|
27688
|
+
position = clause2.position;
|
|
26732
27689
|
}
|
|
26733
27690
|
if (!roles.ok) throw new Error("unreachable");
|
|
26734
27691
|
readEnd(stream);
|
|
@@ -27242,9 +28199,42 @@ COMMENT ON POLICY ${quotePolicyName(policy.name)} ON ${policyTableSql(policy.tab
|
|
|
27242
28199
|
function unreadablePolicyComment(policy) {
|
|
27243
28200
|
return `-- Policy ${commentText(quotePolicyName(policy.name))} on ${commentText(policyTableSql(policy.table))} cannot be read (${commentText(policy.invalid)}); the requests it applies to are refused until it is dropped and created again.`;
|
|
27244
28201
|
}
|
|
28202
|
+
var STORED_POLICIES_SQL = `SELECT table_name, name, kind, command, roles, using_expr, check_expr, owner_column, updated_at FROM ${NAMED_POLICY_TABLE} ORDER BY table_name, name`;
|
|
27245
28203
|
var SYSTEM_PREDICATE = systemTablePredicate("name");
|
|
27246
28204
|
var STORAGE_EXISTS = `EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ${sqlStringLiteral(STORAGE_OBJECTS_TABLE)})`;
|
|
27247
28205
|
|
|
28206
|
+
// ../../packages/data-core/src/realtime-limits.ts
|
|
28207
|
+
var HOBBY_REALTIME_LIMITS = Object.freeze({
|
|
28208
|
+
connections: 200,
|
|
28209
|
+
newConnectionsPerSecond: 10,
|
|
28210
|
+
messagesPerSecond: 100,
|
|
28211
|
+
joinsPerSecond: 100,
|
|
28212
|
+
presencePerSecond: 20,
|
|
28213
|
+
broadcastPayloadBytes: 262144,
|
|
28214
|
+
policyChecksPerSecond: 100,
|
|
28215
|
+
restRequestsPerSecond: 10
|
|
28216
|
+
});
|
|
28217
|
+
var PRO_REALTIME_LIMITS = Object.freeze({
|
|
28218
|
+
connections: 500,
|
|
28219
|
+
newConnectionsPerSecond: 25,
|
|
28220
|
+
messagesPerSecond: 500,
|
|
28221
|
+
joinsPerSecond: 500,
|
|
28222
|
+
presencePerSecond: 50,
|
|
28223
|
+
broadcastPayloadBytes: 3072e3,
|
|
28224
|
+
policyChecksPerSecond: 250,
|
|
28225
|
+
restRequestsPerSecond: 25
|
|
28226
|
+
});
|
|
28227
|
+
var ENTERPRISE_REALTIME_LIMITS = Object.freeze({
|
|
28228
|
+
connections: 2e3,
|
|
28229
|
+
newConnectionsPerSecond: 100,
|
|
28230
|
+
messagesPerSecond: 1e3,
|
|
28231
|
+
joinsPerSecond: 1e3,
|
|
28232
|
+
presencePerSecond: 1e3,
|
|
28233
|
+
broadcastPayloadBytes: 3072e3,
|
|
28234
|
+
policyChecksPerSecond: 500,
|
|
28235
|
+
restRequestsPerSecond: 100
|
|
28236
|
+
});
|
|
28237
|
+
|
|
27248
28238
|
// ../../packages/data-core/src/redirect-origins.ts
|
|
27249
28239
|
var LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
|
|
27250
28240
|
"localhost",
|
|
@@ -27795,7 +28785,10 @@ var POLICY_WARNING_RULES = [
|
|
|
27795
28785
|
function policyIssues(input) {
|
|
27796
28786
|
const present = /* @__PURE__ */ new Set([
|
|
27797
28787
|
...input.tables.map(asciiLower),
|
|
27798
|
-
...input.snapshot.storage ? [STORAGE_OBJECTS] : []
|
|
28788
|
+
...input.snapshot.storage ? [STORAGE_OBJECTS] : [],
|
|
28789
|
+
// Realtime's messages are on every backend (Storage 3-1a Task 5와 같은
|
|
28790
|
+
// 자리, Realtime 3-2a).
|
|
28791
|
+
REALTIME_MESSAGES
|
|
27799
28792
|
]);
|
|
27800
28793
|
const found = [
|
|
27801
28794
|
...policySetProblems(
|
|
@@ -28131,6 +29124,7 @@ function policiesFromFiles(files) {
|
|
|
28131
29124
|
const tables = /* @__PURE__ */ new Set();
|
|
28132
29125
|
const forget = (table) => {
|
|
28133
29126
|
if (sameTable(table, STORAGE_OBJECTS)) return;
|
|
29127
|
+
if (sameTable(table, REALTIME_MESSAGES)) return;
|
|
28134
29128
|
policies = policies.filter((policy) => !sameTable(policy.table, table));
|
|
28135
29129
|
};
|
|
28136
29130
|
for (const [file2, sql] of files.entries()) {
|
|
@@ -28150,9 +29144,20 @@ function policiesFromFiles(files) {
|
|
|
28150
29144
|
tables,
|
|
28151
29145
|
refused: { file: file2, reason: planned.message }
|
|
28152
29146
|
};
|
|
28153
|
-
for (const statement2 of planned.plan.statements) {
|
|
29147
|
+
for (const [index, statement2] of planned.plan.statements.entries()) {
|
|
28154
29148
|
if (statement2.kind === "create-policy") {
|
|
28155
29149
|
const { table, name } = statement2.policy;
|
|
29150
|
+
if (policies.some(
|
|
29151
|
+
(policy) => sameTable(policy.table, table) && policy.name === name
|
|
29152
|
+
))
|
|
29153
|
+
return {
|
|
29154
|
+
policies,
|
|
29155
|
+
tables,
|
|
29156
|
+
refused: {
|
|
29157
|
+
file: file2,
|
|
29158
|
+
reason: `statement ${String(index + 1)} of ${String(planned.plan.statements.length)} failed and the migration was rolled back: policy "${name}" for table "${table}" already exists`
|
|
29159
|
+
}
|
|
29160
|
+
};
|
|
28156
29161
|
policies.push({ table, name });
|
|
28157
29162
|
} else if (statement2.kind === "drop-policy") {
|
|
28158
29163
|
policies = policies.filter(
|
|
@@ -28170,6 +29175,10 @@ function policiesFromFiles(files) {
|
|
|
28170
29175
|
if (!tables.has(asciiLower(head.to))) forget(head.to);
|
|
28171
29176
|
tables.delete(asciiLower(head.table));
|
|
28172
29177
|
tables.add(asciiLower(head.to));
|
|
29178
|
+
if (!sameTable(head.table, STORAGE_OBJECTS) && !sameTable(head.table, REALTIME_MESSAGES))
|
|
29179
|
+
policies = policies.map(
|
|
29180
|
+
(policy) => sameTable(policy.table, head.table) ? { ...policy, table: head.to } : policy
|
|
29181
|
+
);
|
|
28173
29182
|
}
|
|
28174
29183
|
}
|
|
28175
29184
|
}
|
|
@@ -28179,10 +29188,12 @@ function policiesFromFiles(files) {
|
|
|
28179
29188
|
function policiesLostByReset(input) {
|
|
28180
29189
|
const present = new Set(input.tables.map(asciiLower));
|
|
28181
29190
|
if (input.storage === true) present.add(STORAGE_OBJECTS);
|
|
29191
|
+
present.add(REALTIME_MESSAGES);
|
|
28182
29192
|
const made = policiesFromFiles(input.files);
|
|
28183
29193
|
if (made.refused) return { lost: [], withTables: [], refused: made.refused };
|
|
28184
29194
|
const remade = new Set(made.tables);
|
|
28185
29195
|
if (input.storage === true) remade.add(STORAGE_OBJECTS);
|
|
29196
|
+
remade.add(REALTIME_MESSAGES);
|
|
28186
29197
|
const gone = input.current.filter(
|
|
28187
29198
|
(policy) => present.has(asciiLower(policy.table)) && !made.policies.some(
|
|
28188
29199
|
(other) => sameTable(other.table, policy.table) && other.name === policy.name
|
|
@@ -28226,30 +29237,78 @@ function storedPolicySql(row) {
|
|
|
28226
29237
|
return { sql, statements: [create, sql.slice(create.length + 1)] };
|
|
28227
29238
|
}
|
|
28228
29239
|
|
|
29240
|
+
// ../../packages/sdk/src/usage.ts
|
|
29241
|
+
var USAGE_METRICS = ["requests", "builds", "domains"];
|
|
29242
|
+
var USAGE_NEAR_PERCENT = 80;
|
|
29243
|
+
function usageMonthOf(now) {
|
|
29244
|
+
return `${String(now.getUTCFullYear()).padStart(4, "0")}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`;
|
|
29245
|
+
}
|
|
29246
|
+
function summarizeTeamUsage(report, currentMonth) {
|
|
29247
|
+
let requests = 0;
|
|
29248
|
+
let builds = 0;
|
|
29249
|
+
for (const entry of report.days) {
|
|
29250
|
+
requests += entry.requests;
|
|
29251
|
+
builds += entry.builds;
|
|
29252
|
+
}
|
|
29253
|
+
const used = {
|
|
29254
|
+
requests,
|
|
29255
|
+
builds,
|
|
29256
|
+
domains: report.days.at(-1)?.domains ?? 0
|
|
29257
|
+
};
|
|
29258
|
+
const meters = USAGE_METRICS.map((metric) => {
|
|
29259
|
+
const limit = report.limits?.[metric] ?? null;
|
|
29260
|
+
return {
|
|
29261
|
+
metric,
|
|
29262
|
+
used: used[metric],
|
|
29263
|
+
limit,
|
|
29264
|
+
percent: limit ? Math.floor(used[metric] * 100 / limit) : null
|
|
29265
|
+
};
|
|
29266
|
+
});
|
|
29267
|
+
return {
|
|
29268
|
+
limitsKnown: report.limits !== void 0,
|
|
29269
|
+
meters,
|
|
29270
|
+
status: usageStatus(report, currentMonth, requests)
|
|
29271
|
+
};
|
|
29272
|
+
}
|
|
29273
|
+
function usageStatus(report, currentMonth, requests) {
|
|
29274
|
+
const { mode, limitedThisMonth } = report.enforcement;
|
|
29275
|
+
if (mode === "off" || report.team.plan !== "hobby" || report.month !== currentMonth)
|
|
29276
|
+
return null;
|
|
29277
|
+
if (limitedThisMonth) return mode === "enforce" ? "paused" : "over";
|
|
29278
|
+
const limit = report.limits?.requests ?? null;
|
|
29279
|
+
if (limit === null) return null;
|
|
29280
|
+
return requests * 100 >= limit * USAGE_NEAR_PERCENT ? "near" : null;
|
|
29281
|
+
}
|
|
29282
|
+
|
|
28229
29283
|
// src/env-consent.ts
|
|
28230
29284
|
import { existsSync as existsSync6 } from "fs";
|
|
28231
29285
|
import { join as join23 } from "path";
|
|
28232
29286
|
var DOTENV_FILES = [".env.local", ".env.production", ".env"];
|
|
28233
|
-
var
|
|
29287
|
+
var PUBLIC_PREFIX = "NEXT_PUBLIC_";
|
|
29288
|
+
function maskSecretValue(value) {
|
|
29289
|
+
return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
|
|
29290
|
+
}
|
|
28234
29291
|
function maskEnvValue(key, value) {
|
|
28235
|
-
if (
|
|
29292
|
+
if (key.startsWith(PUBLIC_PREFIX))
|
|
28236
29293
|
return value.length > 60 ? `${value.slice(0, 60)}\u2026` : value;
|
|
28237
|
-
return value
|
|
29294
|
+
return maskSecretValue(value);
|
|
28238
29295
|
}
|
|
28239
29296
|
function findDotenvFile(dir) {
|
|
28240
29297
|
for (const name of DOTENV_FILES) if (existsSync6(join23(dir, name))) return name;
|
|
28241
29298
|
return null;
|
|
28242
29299
|
}
|
|
28243
|
-
function envAddGuidance(keys) {
|
|
29300
|
+
function envAddGuidance(keys, environment) {
|
|
29301
|
+
const suffix = environment === void 0 ? "" : ` ${environment}`;
|
|
28244
29302
|
return [
|
|
28245
29303
|
" Set them yourself:",
|
|
28246
|
-
...keys.map((key) => ` npx naro-cli env add ${key}`),
|
|
29304
|
+
...keys.map((key) => ` npx naro-cli env add ${key}${suffix}`),
|
|
28247
29305
|
" (values come from a hidden prompt or stdin, never from argv)"
|
|
28248
29306
|
];
|
|
28249
29307
|
}
|
|
28250
29308
|
async function promptForMissingEnv(ctx, keys) {
|
|
28251
29309
|
const guide = (remaining2) => {
|
|
28252
|
-
for (const line of envAddGuidance(remaining2
|
|
29310
|
+
for (const line of envAddGuidance(remaining2, ctx.environment))
|
|
29311
|
+
ctx.output.log(line);
|
|
28253
29312
|
};
|
|
28254
29313
|
const file2 = findDotenvFile(ctx.cwd);
|
|
28255
29314
|
if (!file2) {
|
|
@@ -28287,6 +29346,20 @@ async function promptForMissingEnv(ctx, keys) {
|
|
|
28287
29346
|
return collected;
|
|
28288
29347
|
}
|
|
28289
29348
|
|
|
29349
|
+
// src/env-surface.ts
|
|
29350
|
+
function pullEnv(rows, surface) {
|
|
29351
|
+
const chosen = /* @__PURE__ */ new Map();
|
|
29352
|
+
for (const row of rows) {
|
|
29353
|
+
if (row.target !== "all" && row.target !== surface) continue;
|
|
29354
|
+
const held = chosen.get(row.key);
|
|
29355
|
+
if (!held || held.target === "all" && row.target !== "all")
|
|
29356
|
+
chosen.set(row.key, row);
|
|
29357
|
+
}
|
|
29358
|
+
return Object.fromEntries(
|
|
29359
|
+
[...chosen.values()].map((row) => [row.key, row.value ?? ""])
|
|
29360
|
+
);
|
|
29361
|
+
}
|
|
29362
|
+
|
|
28290
29363
|
// src/git-info.ts
|
|
28291
29364
|
import { execFile as execFile2 } from "child_process";
|
|
28292
29365
|
var SUPPORTED_REMOTE = [
|
|
@@ -28294,10 +29367,10 @@ var SUPPORTED_REMOTE = [
|
|
|
28294
29367
|
/^ssh:\/\/[^\s]+$/,
|
|
28295
29368
|
/^git@[A-Za-z0-9.-]+:[A-Za-z0-9._/-]+(\.git)?$/
|
|
28296
29369
|
];
|
|
28297
|
-
var defaultExec = (args, cwd) => new Promise((
|
|
29370
|
+
var defaultExec = (args, cwd) => new Promise((resolve12, reject) => {
|
|
28298
29371
|
execFile2("git", args, { cwd }, (error51, stdout) => {
|
|
28299
29372
|
if (error51) reject(error51);
|
|
28300
|
-
else
|
|
29373
|
+
else resolve12(stdout);
|
|
28301
29374
|
});
|
|
28302
29375
|
});
|
|
28303
29376
|
async function tryExec(exec, args, cwd) {
|
|
@@ -28446,6 +29519,225 @@ async function uploadArtifact(artifact, opts) {
|
|
|
28446
29519
|
};
|
|
28447
29520
|
}
|
|
28448
29521
|
|
|
29522
|
+
// src/working-tree.ts
|
|
29523
|
+
import { execFile as execFile3 } from "child_process";
|
|
29524
|
+
import {
|
|
29525
|
+
cp as cp2,
|
|
29526
|
+
lstat,
|
|
29527
|
+
mkdir as mkdir3,
|
|
29528
|
+
readFile as readFile13,
|
|
29529
|
+
readlink,
|
|
29530
|
+
realpath,
|
|
29531
|
+
rm as rm4,
|
|
29532
|
+
stat as stat3,
|
|
29533
|
+
symlink,
|
|
29534
|
+
unlink,
|
|
29535
|
+
writeFile as writeFile7
|
|
29536
|
+
} from "fs/promises";
|
|
29537
|
+
import {
|
|
29538
|
+
basename as basename3,
|
|
29539
|
+
dirname as dirname5,
|
|
29540
|
+
isAbsolute as isAbsolute4,
|
|
29541
|
+
join as join24,
|
|
29542
|
+
relative as relative6,
|
|
29543
|
+
resolve as resolve10,
|
|
29544
|
+
sep as sep4
|
|
29545
|
+
} from "path";
|
|
29546
|
+
var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
|
|
29547
|
+
var ENV_FILE = /^\.env(\.(?!(?:example|sample|template)$)[^\\/]+)?$/i;
|
|
29548
|
+
function git(cwd, args) {
|
|
29549
|
+
return new Promise((done, fail2) => {
|
|
29550
|
+
execFile3(
|
|
29551
|
+
"git",
|
|
29552
|
+
args,
|
|
29553
|
+
{ cwd, encoding: "buffer", maxBuffer: 64 * 1024 * 1024 },
|
|
29554
|
+
(error51, stdout) => {
|
|
29555
|
+
if (error51) fail2(error51);
|
|
29556
|
+
else done(stdout);
|
|
29557
|
+
}
|
|
29558
|
+
);
|
|
29559
|
+
});
|
|
29560
|
+
}
|
|
29561
|
+
async function committedEnvFiles(sourceDir) {
|
|
29562
|
+
let listing;
|
|
29563
|
+
try {
|
|
29564
|
+
listing = await git(sourceDir, ["ls-tree", "-r", "-z", "HEAD"]);
|
|
29565
|
+
} catch (error51) {
|
|
29566
|
+
return error51.code === "ENOENT" ? null : [];
|
|
29567
|
+
}
|
|
29568
|
+
return listing.toString("utf8").split("\0").flatMap((entry) => {
|
|
29569
|
+
const tab = entry.indexOf(" ");
|
|
29570
|
+
if (tab < 0) return [];
|
|
29571
|
+
const [mode] = entry.slice(0, tab).split(" ");
|
|
29572
|
+
const path = entry.slice(tab + 1);
|
|
29573
|
+
if (!ENV_FILE.test(basename3(path)) || COPY_SKIP.test(path)) return [];
|
|
29574
|
+
if (mode === "100644" || mode === "100755")
|
|
29575
|
+
return [{ path, symlink: false }];
|
|
29576
|
+
if (mode === "120000") return [{ path, symlink: true }];
|
|
29577
|
+
return [];
|
|
29578
|
+
});
|
|
29579
|
+
}
|
|
29580
|
+
async function staysInside(link, text2, root) {
|
|
29581
|
+
try {
|
|
29582
|
+
return inside(await realpath(link), root);
|
|
29583
|
+
} catch (error51) {
|
|
29584
|
+
return error51.code === "ENOENT" && !text2.split(/[\\/]/).includes("..");
|
|
29585
|
+
}
|
|
29586
|
+
}
|
|
29587
|
+
function climbsOnlyFirst(text2) {
|
|
29588
|
+
const segments = text2.split(/[\\/]/).filter((s) => s !== "" && s !== ".");
|
|
29589
|
+
const down = segments.findIndex((segment) => segment !== "..");
|
|
29590
|
+
return down < 0 || !segments.slice(down).includes("..");
|
|
29591
|
+
}
|
|
29592
|
+
function inside(path, root) {
|
|
29593
|
+
const rel = relative6(root, path);
|
|
29594
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
29595
|
+
}
|
|
29596
|
+
async function keptOnly(items, keep, remove) {
|
|
29597
|
+
const verdicts = await Promise.all(items.map(keep));
|
|
29598
|
+
const removed = items.filter((_, index) => !verdicts[index]);
|
|
29599
|
+
for (const item of removed) await remove(item);
|
|
29600
|
+
return removed;
|
|
29601
|
+
}
|
|
29602
|
+
async function readLocal(path) {
|
|
29603
|
+
const found = await stat3(path).catch(() => null);
|
|
29604
|
+
if (!found?.isFile()) return null;
|
|
29605
|
+
return readFile13(path).catch(() => null);
|
|
29606
|
+
}
|
|
29607
|
+
async function realAncestor(dir) {
|
|
29608
|
+
for (let current = dir; ; current = dirname5(current)) {
|
|
29609
|
+
try {
|
|
29610
|
+
return await realpath(current);
|
|
29611
|
+
} catch {
|
|
29612
|
+
if (dirname5(current) === current)
|
|
29613
|
+
throw new Error(`No ancestor of ${dir}`);
|
|
29614
|
+
}
|
|
29615
|
+
}
|
|
29616
|
+
}
|
|
29617
|
+
function sameText(a, b) {
|
|
29618
|
+
return a.equals(b) || a.toString("utf8").replace(/\r\n/g, "\n") === b.toString("utf8").replace(/\r\n/g, "\n");
|
|
29619
|
+
}
|
|
29620
|
+
async function copyWorkingTree(source, checkout) {
|
|
29621
|
+
const sourceDir = await realpath(source);
|
|
29622
|
+
const inWorkingTree = [];
|
|
29623
|
+
const absoluteLinks = [];
|
|
29624
|
+
const linksOutside = [];
|
|
29625
|
+
const linksElsewhere = [];
|
|
29626
|
+
const keptLinks = [];
|
|
29627
|
+
await cp2(sourceDir, checkout, {
|
|
29628
|
+
recursive: true,
|
|
29629
|
+
// Relative symlinks must stay relative: resolved, they would point
|
|
29630
|
+
// back into the developer's real tree, and a strategy's temporary
|
|
29631
|
+
// rewrite (proxy -> middleware, vinext toolchain) would land there.
|
|
29632
|
+
verbatimSymlinks: true,
|
|
29633
|
+
filter: async (src) => {
|
|
29634
|
+
if (COPY_SKIP.test(src)) return false;
|
|
29635
|
+
if (src === sourceDir) return true;
|
|
29636
|
+
const entry = await lstat(src);
|
|
29637
|
+
if (entry.isFIFO() || entry.isSocket() || entry.isCharacterDevice() || entry.isBlockDevice())
|
|
29638
|
+
return false;
|
|
29639
|
+
if (ENV_FILE.test(basename3(src)) && !entry.isDirectory()) {
|
|
29640
|
+
inWorkingTree.push(relative6(sourceDir, src).split(sep4).join("/"));
|
|
29641
|
+
return false;
|
|
29642
|
+
}
|
|
29643
|
+
if (entry.isSymbolicLink()) {
|
|
29644
|
+
const target2 = await readlink(src);
|
|
29645
|
+
const path = relative6(sourceDir, src).split(sep4).join("/");
|
|
29646
|
+
if (isAbsolute4(target2)) {
|
|
29647
|
+
absoluteLinks.push(path);
|
|
29648
|
+
return false;
|
|
29649
|
+
}
|
|
29650
|
+
if (!inside(resolve10(dirname5(src), target2), sourceDir)) {
|
|
29651
|
+
linksOutside.push(path);
|
|
29652
|
+
return false;
|
|
29653
|
+
}
|
|
29654
|
+
if (!await staysInside(src, target2, sourceDir)) {
|
|
29655
|
+
linksElsewhere.push(path);
|
|
29656
|
+
return false;
|
|
29657
|
+
}
|
|
29658
|
+
keptLinks.push({ path, text: target2 });
|
|
29659
|
+
}
|
|
29660
|
+
return true;
|
|
29661
|
+
}
|
|
29662
|
+
});
|
|
29663
|
+
const root = await realpath(checkout);
|
|
29664
|
+
const strays = await keptOnly(
|
|
29665
|
+
keptLinks,
|
|
29666
|
+
async ({ path, text: text2 }) => {
|
|
29667
|
+
try {
|
|
29668
|
+
return inside(await realpath(join24(checkout, path)), root);
|
|
29669
|
+
} catch (error51) {
|
|
29670
|
+
return error51.code === "ENOENT" && climbsOnlyFirst(text2);
|
|
29671
|
+
}
|
|
29672
|
+
},
|
|
29673
|
+
({ path }) => unlink(join24(checkout, path))
|
|
29674
|
+
);
|
|
29675
|
+
linksElsewhere.push(...strays.map(({ path }) => path));
|
|
29676
|
+
const committed = await committedEnvFiles(sourceDir);
|
|
29677
|
+
const committedVersion = [];
|
|
29678
|
+
const outsideProject = [];
|
|
29679
|
+
const envLinks = [];
|
|
29680
|
+
for (const { path, symlink: isLink } of committed ?? []) {
|
|
29681
|
+
if ([...absoluteLinks, ...linksOutside, ...linksElsewhere].some(
|
|
29682
|
+
(link) => path.startsWith(`${link}/`)
|
|
29683
|
+
)) {
|
|
29684
|
+
outsideProject.push(path);
|
|
29685
|
+
continue;
|
|
29686
|
+
}
|
|
29687
|
+
const target2 = join24(checkout, path);
|
|
29688
|
+
if (!inside(await realAncestor(dirname5(target2)), root)) {
|
|
29689
|
+
outsideProject.push(path);
|
|
29690
|
+
continue;
|
|
29691
|
+
}
|
|
29692
|
+
try {
|
|
29693
|
+
await mkdir3(dirname5(target2), { recursive: true });
|
|
29694
|
+
} catch {
|
|
29695
|
+
outsideProject.push(path);
|
|
29696
|
+
continue;
|
|
29697
|
+
}
|
|
29698
|
+
await rm4(target2, { recursive: true, force: true });
|
|
29699
|
+
const content = await git(sourceDir, ["show", `HEAD:./${path}`]);
|
|
29700
|
+
if (isLink) {
|
|
29701
|
+
const linkTarget = content.toString("utf8");
|
|
29702
|
+
const pointsAt = resolve10(dirname5(target2), linkTarget);
|
|
29703
|
+
if (isAbsolute4(linkTarget) || !inside(pointsAt, checkout)) {
|
|
29704
|
+
outsideProject.push(path);
|
|
29705
|
+
continue;
|
|
29706
|
+
}
|
|
29707
|
+
await symlink(linkTarget, target2);
|
|
29708
|
+
envLinks.push(path);
|
|
29709
|
+
continue;
|
|
29710
|
+
}
|
|
29711
|
+
await writeFile7(target2, content, { flag: "wx" });
|
|
29712
|
+
const local = await readLocal(join24(sourceDir, path));
|
|
29713
|
+
if (!local || !sameText(local, content)) committedVersion.push(path);
|
|
29714
|
+
}
|
|
29715
|
+
const envStrays = await keptOnly(
|
|
29716
|
+
envLinks,
|
|
29717
|
+
async (path) => {
|
|
29718
|
+
const real = await realpath(join24(checkout, path)).catch(() => null);
|
|
29719
|
+
return real !== null && inside(real, root);
|
|
29720
|
+
},
|
|
29721
|
+
(path) => unlink(join24(checkout, path))
|
|
29722
|
+
);
|
|
29723
|
+
outsideProject.push(...envStrays);
|
|
29724
|
+
const inCommit = new Set(
|
|
29725
|
+
(committed ?? []).map(({ path }) => path.normalize("NFC"))
|
|
29726
|
+
);
|
|
29727
|
+
const leftOut = inWorkingTree.filter(
|
|
29728
|
+
(path) => !inCommit.has(path.normalize("NFC"))
|
|
29729
|
+
);
|
|
29730
|
+
return {
|
|
29731
|
+
leftOut,
|
|
29732
|
+
committedVersion,
|
|
29733
|
+
outsideProject,
|
|
29734
|
+
absoluteLinks,
|
|
29735
|
+
linksOutside,
|
|
29736
|
+
linksElsewhere,
|
|
29737
|
+
gitUnavailable: committed === null
|
|
29738
|
+
};
|
|
29739
|
+
}
|
|
29740
|
+
|
|
28449
29741
|
// src/deploy.ts
|
|
28450
29742
|
function parseDotenv2(text2) {
|
|
28451
29743
|
return parseDotenv(text2);
|
|
@@ -28471,7 +29763,7 @@ function eventsObserver(cp3, deploymentId, token, fetchImpl = fetch, logIngestUr
|
|
|
28471
29763
|
} catch (error51) {
|
|
28472
29764
|
lastError = error51;
|
|
28473
29765
|
await new Promise(
|
|
28474
|
-
(
|
|
29766
|
+
(resolve12) => setTimeout(resolve12, 500 * (attempt + 1))
|
|
28475
29767
|
);
|
|
28476
29768
|
}
|
|
28477
29769
|
}
|
|
@@ -28519,9 +29811,23 @@ function eventsObserver(cp3, deploymentId, token, fetchImpl = fetch, logIngestUr
|
|
|
28519
29811
|
await post({ type: "metadata", metadata: rest }).catch(() => {
|
|
28520
29812
|
});
|
|
28521
29813
|
},
|
|
28522
|
-
flush: flushLogs
|
|
29814
|
+
flush: flushLogs,
|
|
29815
|
+
fail: async (code, message) => {
|
|
29816
|
+
await flushLogs();
|
|
29817
|
+
await postStatus({
|
|
29818
|
+
type: "status",
|
|
29819
|
+
status: "FAILED",
|
|
29820
|
+
error: { code, message }
|
|
29821
|
+
}).catch(() => {
|
|
29822
|
+
});
|
|
29823
|
+
}
|
|
28523
29824
|
};
|
|
28524
29825
|
}
|
|
29826
|
+
var DEPLOYMENT_CODES = new Set(DEPLOYMENT_ERROR_CODES);
|
|
29827
|
+
function failureCode(error51, fallback) {
|
|
29828
|
+
const code = error51 && typeof error51 === "object" ? error51.code : void 0;
|
|
29829
|
+
return typeof code === "string" && DEPLOYMENT_CODES.has(code) ? code : fallback;
|
|
29830
|
+
}
|
|
28525
29831
|
function slugify2(value) {
|
|
28526
29832
|
const slug = value.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 54);
|
|
28527
29833
|
return slug || "app";
|
|
@@ -28529,20 +29835,19 @@ function slugify2(value) {
|
|
|
28529
29835
|
async function projectName(sourceDir) {
|
|
28530
29836
|
try {
|
|
28531
29837
|
const pkg = JSON.parse(
|
|
28532
|
-
await
|
|
29838
|
+
await readFile14(join25(sourceDir, "package.json"), "utf8")
|
|
28533
29839
|
);
|
|
28534
29840
|
if (pkg.name) return pkg.name;
|
|
28535
29841
|
} catch {
|
|
28536
29842
|
}
|
|
28537
|
-
return
|
|
29843
|
+
return basename4(sourceDir);
|
|
28538
29844
|
}
|
|
28539
|
-
var COPY_SKIP = /(^|[\\/])(node_modules|\.next|\.open-next|\.git|\.wrangler|\.turbo|\.naro)([\\/]|$)/;
|
|
28540
29845
|
function resolveBin(name) {
|
|
28541
|
-
let dir =
|
|
29846
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
28542
29847
|
for (; ; ) {
|
|
28543
|
-
const candidate =
|
|
29848
|
+
const candidate = join25(dir, "node_modules", ".bin", name);
|
|
28544
29849
|
if (existsSync7(candidate)) return candidate;
|
|
28545
|
-
const parent =
|
|
29850
|
+
const parent = dirname6(dir);
|
|
28546
29851
|
if (parent === dir) return name;
|
|
28547
29852
|
dir = parent;
|
|
28548
29853
|
}
|
|
@@ -28565,22 +29870,59 @@ async function detectPackageManagerLenient(root) {
|
|
|
28565
29870
|
};
|
|
28566
29871
|
}
|
|
28567
29872
|
}
|
|
28568
|
-
function
|
|
29873
|
+
function workingTreeNotices(copy) {
|
|
29874
|
+
const notices = [];
|
|
29875
|
+
const how = "Local values reach a build only through the env prompt or `naro env add`.";
|
|
29876
|
+
if (copy.gitUnavailable && copy.leftOut.length > 0)
|
|
29877
|
+
notices.push({
|
|
29878
|
+
level: "WARN",
|
|
29879
|
+
message: `Left out of the build because git could not be run to read the commit: ${copy.leftOut.join(", ")}. Install git to build with the committed ones. ${how}`
|
|
29880
|
+
});
|
|
29881
|
+
else if (copy.leftOut.length > 0)
|
|
29882
|
+
notices.push({
|
|
29883
|
+
level: "WARN",
|
|
29884
|
+
message: `Left out of the build because they are not committed: ${copy.leftOut.join(", ")}. ${how}`
|
|
29885
|
+
});
|
|
29886
|
+
if (copy.outsideProject.length > 0)
|
|
29887
|
+
notices.push({
|
|
29888
|
+
level: "WARN",
|
|
29889
|
+
message: `Left out of the build because they would land outside the project or in a place that could not be found: ${copy.outsideProject.join(", ")}.`
|
|
29890
|
+
});
|
|
29891
|
+
if (copy.absoluteLinks.length > 0)
|
|
29892
|
+
notices.push({
|
|
29893
|
+
level: "WARN",
|
|
29894
|
+
message: `Left out of the build because they are symbolic links to an absolute path: ${copy.absoluteLinks.join(", ")}. A git deploy of this commit cannot follow them either; copy what the build needs into the project.`
|
|
29895
|
+
});
|
|
29896
|
+
if (copy.linksOutside.length > 0)
|
|
29897
|
+
notices.push({
|
|
29898
|
+
level: "WARN",
|
|
29899
|
+
message: `Left out of the build because they are symbolic links to outside this folder: ${copy.linksOutside.join(", ")}. To keep links between a repository's folders, deploy from the repository root with --root <app folder>.`
|
|
29900
|
+
});
|
|
29901
|
+
if (copy.linksElsewhere.length > 0)
|
|
29902
|
+
notices.push({
|
|
29903
|
+
level: "WARN",
|
|
29904
|
+
message: `Left out of the build because they are symbolic links that lead out of this folder on disk, or to a place that could not be found: ${copy.linksElsewhere.join(", ")}. Copy what the build needs into the project.`
|
|
29905
|
+
});
|
|
29906
|
+
if (copy.committedVersion.length > 0)
|
|
29907
|
+
notices.push({
|
|
29908
|
+
level: "INFO",
|
|
29909
|
+
message: `Built with the committed version of ${copy.committedVersion.join(", ")}; the local edits stay on this machine.`
|
|
29910
|
+
});
|
|
29911
|
+
return notices;
|
|
29912
|
+
}
|
|
29913
|
+
function localDependencies(sourceDir, say = () => void 0) {
|
|
28569
29914
|
return {
|
|
28570
|
-
createWorkspace: () => mkdtemp2(
|
|
28571
|
-
removeWorkspace: (path) =>
|
|
29915
|
+
createWorkspace: () => mkdtemp2(join25(tmpdir2(), "naro-build-")),
|
|
29916
|
+
removeWorkspace: (path) => rm5(path, { recursive: true, force: true }),
|
|
28572
29917
|
async runCommand(spec, observer) {
|
|
28573
29918
|
if (spec.command === "git" && spec.args[0] === "clone") {
|
|
28574
29919
|
const checkout = spec.args.at(-1);
|
|
28575
29920
|
if (!checkout) throw new Error("Missing checkout path.");
|
|
28576
|
-
await
|
|
28577
|
-
|
|
28578
|
-
|
|
28579
|
-
|
|
28580
|
-
|
|
28581
|
-
verbatimSymlinks: true,
|
|
28582
|
-
filter: (src) => !COPY_SKIP.test(src)
|
|
28583
|
-
});
|
|
29921
|
+
const copy = await copyWorkingTree(sourceDir, checkout);
|
|
29922
|
+
for (const notice of workingTreeNotices(copy)) {
|
|
29923
|
+
say(`${notice.level === "WARN" ? "\u26A0" : "\u25B8"} ${notice.message}`);
|
|
29924
|
+
await observer.onLog({ ...notice, stage: spec.stage });
|
|
29925
|
+
}
|
|
28584
29926
|
return { stdout: [], stderr: [] };
|
|
28585
29927
|
}
|
|
28586
29928
|
if (spec.command === "git" && spec.args[0] === "rev-parse") {
|
|
@@ -28608,7 +29950,14 @@ function localDependencies(sourceDir) {
|
|
|
28608
29950
|
async function deployDirectory(sourceDir, options, log2) {
|
|
28609
29951
|
const cpUrl = controlPlaneUrl();
|
|
28610
29952
|
const production = options.production ?? false;
|
|
28611
|
-
const
|
|
29953
|
+
const environment = production ? void 0 : options.environment;
|
|
29954
|
+
if (environment === "production" || environment === "preview")
|
|
29955
|
+
throw new NaroError(
|
|
29956
|
+
"VALIDATION_FAILED",
|
|
29957
|
+
"Pass production: true for production; preview is the default."
|
|
29958
|
+
);
|
|
29959
|
+
const surface = production ? "production" : environment ?? "preview";
|
|
29960
|
+
const appRoot = options.rootDirectory ? resolve11(sourceDir, options.rootDirectory) : sourceDir;
|
|
28612
29961
|
const framework = detectFramework2(appRoot);
|
|
28613
29962
|
if (!framework.deployable && !options.force) {
|
|
28614
29963
|
throw new NaroError(
|
|
@@ -28630,12 +29979,14 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28630
29979
|
getAccessToken: () => auth.getAccessToken(),
|
|
28631
29980
|
...options.fetch ? { fetch: options.fetch } : {}
|
|
28632
29981
|
});
|
|
29982
|
+
if (environment !== void 0 && options.projectId)
|
|
29983
|
+
await client.environments.get(options.projectId, environment);
|
|
28633
29984
|
const name = slugify2(options.name ?? await projectName(appRoot));
|
|
28634
|
-
const
|
|
28635
|
-
const branch = options.branch ??
|
|
28636
|
-
const repositoryRoot = [
|
|
29985
|
+
const git2 = await (options.gitInfo ?? detectGitInfo)(sourceDir);
|
|
29986
|
+
const branch = options.branch ?? git2.branch;
|
|
29987
|
+
const repositoryRoot = [git2.prefix, options.rootDirectory].filter(Boolean).join("/") || null;
|
|
28637
29988
|
if (options.projectId) log2(`\u25B8 linked project ${options.projectId}`);
|
|
28638
|
-
else if (
|
|
29989
|
+
else if (git2.repositoryUrl) log2(`\u25B8 linking ${git2.repositoryUrl}`);
|
|
28639
29990
|
if (options.env && Object.keys(options.env).length > 0)
|
|
28640
29991
|
log2(`\u25B8 syncing ${String(Object.keys(options.env).length)} env var(s)`);
|
|
28641
29992
|
const scanned = scanEnvReferences(sourceDir).map(
|
|
@@ -28647,7 +29998,7 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28647
29998
|
if (options.projectId) {
|
|
28648
29999
|
try {
|
|
28649
30000
|
const rows = await client.env.list(options.projectId);
|
|
28650
|
-
existing = new Set(
|
|
30001
|
+
existing = new Set(Object.keys(pullEnv(rows, surface)));
|
|
28651
30002
|
} catch {
|
|
28652
30003
|
}
|
|
28653
30004
|
}
|
|
@@ -28659,8 +30010,10 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28659
30010
|
log2(
|
|
28660
30011
|
`\u25B8 ${String(missing.length)} env var(s) needed, not set on this project: ${missing.join(", ")}`
|
|
28661
30012
|
);
|
|
28662
|
-
|
|
28663
|
-
|
|
30013
|
+
const canCollect = environment === void 0 || options.projectId;
|
|
30014
|
+
if (options.onMissingEnv && canCollect)
|
|
30015
|
+
collected = await options.onMissingEnv(missing);
|
|
30016
|
+
else for (const line of envAddGuidance(missing, environment)) log2(line);
|
|
28664
30017
|
}
|
|
28665
30018
|
const missingEnvKeys = missing.filter((key) => !(key in collected));
|
|
28666
30019
|
const envToSend = { ...options.env, ...collected };
|
|
@@ -28669,15 +30022,18 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28669
30022
|
...options.projectId ? { projectId: options.projectId } : { name },
|
|
28670
30023
|
...options.teamSlug ? { teamSlug: options.teamSlug } : {},
|
|
28671
30024
|
production,
|
|
28672
|
-
...
|
|
30025
|
+
...options.environment !== void 0 && {
|
|
30026
|
+
environment: options.environment
|
|
30027
|
+
},
|
|
30028
|
+
...git2.repositoryUrl != null && { repositoryUrl: git2.repositoryUrl },
|
|
28673
30029
|
...branch != null && { branch },
|
|
28674
30030
|
...repositoryRoot != null && { rootDirectory: repositoryRoot },
|
|
28675
30031
|
...Object.keys(envToSend).length > 0 && { env: envToSend },
|
|
28676
|
-
...
|
|
30032
|
+
...git2.commit != null && { commit: git2.commit }
|
|
28677
30033
|
});
|
|
28678
30034
|
if (ctx.envSkipped?.length)
|
|
28679
30035
|
log2(`\u26A0 skipped env: ${ctx.envSkipped.join(", ")}`);
|
|
28680
|
-
const executor = options.executor ?? new LocalBuildExecutor(localDependencies(sourceDir));
|
|
30036
|
+
const executor = options.executor ?? new LocalBuildExecutor(localDependencies(sourceDir, log2));
|
|
28681
30037
|
const observer = eventsObserver(
|
|
28682
30038
|
cpUrl,
|
|
28683
30039
|
ctx.deploymentId,
|
|
@@ -28708,18 +30064,29 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28708
30064
|
);
|
|
28709
30065
|
artifact = result.artifact;
|
|
28710
30066
|
} catch (error51) {
|
|
28711
|
-
|
|
28712
|
-
|
|
28713
|
-
|
|
28714
|
-
|
|
30067
|
+
const message = error51 instanceof Error ? error51.message : "The local build failed.";
|
|
30068
|
+
const buildCode = failureCode(error51, "BUILD_FAILED");
|
|
30069
|
+
await observer.fail(buildCode, message);
|
|
30070
|
+
throw new NaroError("BUILD_FAILED", message, {
|
|
30071
|
+
details: { deploymentId: ctx.deploymentId, buildCode },
|
|
30072
|
+
cause: error51
|
|
30073
|
+
});
|
|
30074
|
+
}
|
|
30075
|
+
let uploadSummary;
|
|
30076
|
+
try {
|
|
30077
|
+
({ uploadSummary } = await uploadArtifact(artifact, {
|
|
30078
|
+
controlPlaneUrl: cpUrl,
|
|
30079
|
+
deploymentId: ctx.deploymentId,
|
|
30080
|
+
callbackToken: ctx.callbackToken,
|
|
30081
|
+
...options.fetch ? { fetch: options.fetch } : {}
|
|
30082
|
+
}));
|
|
30083
|
+
} catch (error51) {
|
|
30084
|
+
await observer.fail(
|
|
30085
|
+
failureCode(error51, "DEPLOY_FAILED"),
|
|
30086
|
+
error51 instanceof Error ? error51.message : "The upload failed."
|
|
28715
30087
|
);
|
|
30088
|
+
throw error51;
|
|
28716
30089
|
}
|
|
28717
|
-
const { uploadSummary } = await uploadArtifact(artifact, {
|
|
28718
|
-
controlPlaneUrl: cpUrl,
|
|
28719
|
-
deploymentId: ctx.deploymentId,
|
|
28720
|
-
callbackToken: ctx.callbackToken,
|
|
28721
|
-
...options.fetch ? { fetch: options.fetch } : {}
|
|
28722
|
-
});
|
|
28723
30090
|
log2(`\u25B8 ${uploadSummary}`);
|
|
28724
30091
|
await observer.onLog({
|
|
28725
30092
|
level: "INFO",
|
|
@@ -28728,7 +30095,12 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28728
30095
|
});
|
|
28729
30096
|
await observer.flush();
|
|
28730
30097
|
log2("\u25B8 publishing worker");
|
|
28731
|
-
const detail = await waitForFinish(
|
|
30098
|
+
const detail = await waitForFinish(
|
|
30099
|
+
client,
|
|
30100
|
+
ctx,
|
|
30101
|
+
log2,
|
|
30102
|
+
options.pollIntervalMs ?? 2500
|
|
30103
|
+
);
|
|
28732
30104
|
const url2 = deploymentUrl(detail, ctx, production);
|
|
28733
30105
|
if (detail.deployment.status === "FAILED") {
|
|
28734
30106
|
throw new NaroError(
|
|
@@ -28743,15 +30115,17 @@ async function deployDirectory(sourceDir, options, log2) {
|
|
|
28743
30115
|
status: detail.deployment.status,
|
|
28744
30116
|
url: url2,
|
|
28745
30117
|
production,
|
|
30118
|
+
// A control plane from before named environments sends no environment.
|
|
30119
|
+
environment: ctx.environment ?? null,
|
|
28746
30120
|
missingEnvKeys,
|
|
28747
30121
|
teamSlug: ctx.teamSlug,
|
|
28748
30122
|
projectSlug: ctx.projectSlug,
|
|
28749
30123
|
gitConnection: ctx.gitConnection
|
|
28750
30124
|
};
|
|
28751
30125
|
}
|
|
28752
|
-
async function waitForFinish(client, ctx, log2) {
|
|
30126
|
+
async function waitForFinish(client, ctx, log2, intervalMs) {
|
|
28753
30127
|
return client.deployments.wait(ctx.deploymentId, {
|
|
28754
|
-
intervalMs
|
|
30128
|
+
intervalMs,
|
|
28755
30129
|
timeoutMs: 10 * 6e4,
|
|
28756
30130
|
onStatus: (status) => {
|
|
28757
30131
|
if (status === "READY") log2("\u2713 worker deployed");
|
|
@@ -28760,9 +30134,10 @@ async function waitForFinish(client, ctx, log2) {
|
|
|
28760
30134
|
}
|
|
28761
30135
|
function deploymentUrl(detail, ctx, production) {
|
|
28762
30136
|
if (detail.deployment.url) return detail.deployment.url;
|
|
28763
|
-
const surface = ctx.
|
|
30137
|
+
const surface = ctx.environment ? { kind: "environment", slug: ctx.environment, environmentId: null } : production ? PRODUCTION_SURFACE : PREVIEW_SURFACE;
|
|
30138
|
+
const worker = workerNameForSurface(ctx.subdomain ?? ctx.projectId, surface);
|
|
28764
30139
|
const host = new URL(ctx.controlPlaneUrl || controlPlaneUrl()).hostname;
|
|
28765
|
-
return `https://${
|
|
30140
|
+
return `https://${worker}.${host}`;
|
|
28766
30141
|
}
|
|
28767
30142
|
|
|
28768
30143
|
export {
|
|
@@ -28780,10 +30155,14 @@ export {
|
|
|
28780
30155
|
isExpired,
|
|
28781
30156
|
controlPlaneUrl,
|
|
28782
30157
|
createAuthClient,
|
|
30158
|
+
userForAccessToken,
|
|
28783
30159
|
refreshStoredSession,
|
|
28784
30160
|
login,
|
|
30161
|
+
suppliedToken,
|
|
28785
30162
|
resolveAuth,
|
|
28786
30163
|
openBrowser,
|
|
30164
|
+
isReservedEnvTarget,
|
|
30165
|
+
isEnvTargetName,
|
|
28787
30166
|
SPEED_INSIGHTS_METRICS,
|
|
28788
30167
|
SPEED_INSIGHTS_FILTER_KEYS,
|
|
28789
30168
|
SPEED_INSIGHTS_MIN_DEPLOYMENT_SAMPLES,
|
|
@@ -28799,6 +30178,7 @@ export {
|
|
|
28799
30178
|
cellText,
|
|
28800
30179
|
renderRows,
|
|
28801
30180
|
summarizeStatement,
|
|
30181
|
+
parseDotenv,
|
|
28802
30182
|
serializeDotenv,
|
|
28803
30183
|
MAX_SINGLE_UPLOAD_BYTES,
|
|
28804
30184
|
parseCacheControl,
|
|
@@ -28844,11 +30224,17 @@ export {
|
|
|
28844
30224
|
resetPoliciesWithTablesMessage,
|
|
28845
30225
|
resetFileRefusedMessage,
|
|
28846
30226
|
storedPolicySql,
|
|
30227
|
+
usageMonthOf,
|
|
30228
|
+
summarizeTeamUsage,
|
|
30229
|
+
maskEnvValue,
|
|
28847
30230
|
promptForMissingEnv,
|
|
30231
|
+
pullEnv,
|
|
28848
30232
|
detectGitInfo,
|
|
28849
|
-
parseDotenv2
|
|
30233
|
+
parseDotenv2,
|
|
28850
30234
|
readProjectEnv2 as readProjectEnv,
|
|
28851
30235
|
eventsObserver,
|
|
28852
30236
|
slugify2 as slugify,
|
|
30237
|
+
workingTreeNotices,
|
|
30238
|
+
localDependencies,
|
|
28853
30239
|
deployDirectory
|
|
28854
30240
|
};
|