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.
@@ -5,9 +5,9 @@ import {
5
5
 
6
6
  // src/deploy.ts
7
7
  import { existsSync as existsSync7 } from "fs";
8
- import { cp as cp2, mkdtemp as mkdtemp2, readFile as readFile13, rm as rm4 } from "fs/promises";
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 basename3, dirname as dirname5, join as join24, resolve as resolve10 } from "path";
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 = 8;
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 eachWithLimit(items, limit, work) {
42
- let next = 0;
43
- const lanes = Array.from(
44
- { length: Math.min(limit, items.length) },
45
- async () => {
46
- while (next < items.length) {
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
- await work(item);
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
- await Promise.all(lanes);
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 eachWithLimit(batch, ARTIFACT_UPLOAD_CONCURRENCY, async (entry) => {
63
- const url2 = urls?.get(artifactUrlKey(entry.kind, entry.file.name));
64
- if (!url2) {
65
- viaProxy.push(entry);
66
- return;
67
- }
68
- try {
69
- await ports.putSigned(url2, entry);
70
- } catch {
71
- viaProxy.push(entry);
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 PEER_LOWER = { major: 15, minor: 5, patch: 21 };
15226
- var PEER_16_FLOOR = { major: 16, minor: 2, patch: 11 };
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 withinPeerRange(version2) {
15253
- if (version2.major === 15) return compare(version2, PEER_LOWER) >= 0;
15254
- if (version2.major === 16) return compare(version2, PEER_16_FLOOR) >= 0;
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 < PEER_LOWER.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. @opennextjs/cloudflare supports Next.js 15.5.21 and newer. Upgrade to Next.js 16.`
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 (!withinPeerRange(version2)) {
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 @opennextjs/cloudflare declares support for (>=15.5.21 <16 || >=16.2.11). The build may fail in ways naro cannot explain.`
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: "vinext/server/fetch-handler",
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 SECRET_ASSIGNMENT = /\b(CLOUDFLARE_API_TOKEN|SUPABASE_SERVICE_ROLE_KEY|BUILD_CALLBACK_SECRET)=([^\s]+)/gi;
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(/\bBearer\s+[^\s]+/gi, "Bearer [REDACTED]").replace(/(https?:\/\/)[^/@\s]+@/gi, "$1[REDACTED]@").replace(SECRET_ASSIGNMENT, "$1=[REDACTED]");
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, basename4) {
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}/${basename4}.${ext}` : `${basename4}.${ext}`;
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
- await observer.onLog({
17063
- level: "INFO",
17064
- stage: "BUILDING",
17065
- message: `Resolving deployment strategy for Next.js ${context.nextVersion}: ${remaining.map(label).join(
17066
- ", "
17067
- )}${options.concurrency > 1 && remaining.length > 1 ? " (in parallel)" : ""}.`
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
- `No deployment strategy succeeded for Next.js ${context.nextVersion}: ${attempts.map(
17089
- (attempt) => `P${String(attempt.priority)} ${attempt.id} ${describeOutcome(attempt.outcome)}`
17090
- ).join("; ")}.`
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: false,
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: false,
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 nodeMiddleware = openNext.output.some((line) => NODE_MIDDLEWARE_REFUSAL.test(line)) || await buildDeclaresNodeMiddleware(roots.appRoot);
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
- nodeMiddleware ? "OpenNext Cloudflare does not support Node.js middleware (Next.js 16 proxy.ts)." : describeFailure(openNext)
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: false,
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: false,
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: false,
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) return second.outcome;
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,64}$/i.test(gitCommitSha)) {
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((resolve11, reject) => {
18898
- resolveCode = resolve11;
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((resolve11) => server.listen(0, "127.0.0.1", resolve11));
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(new Error("Timed out waiting for GitHub callback.")),
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 toStoredSession(session) {
18974
- const meta3 = session.user.user_metadata ?? {};
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
- if (options.explicitToken) {
19031
- const token = options.explicitToken;
19032
- return { getAccessToken: async () => token, source: "flag" };
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: options.branch ? { branch: options.branch } : {},
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((resolve11) => setTimeout(resolve11, ms)));
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
- /** Removes every row for a key (optionally narrowed to one target). */
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 || row.target === target2)
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((resolve11) => setTimeout(resolve11, ms)));
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`. Policies left under a name the change gives to a
19992
- * table are removed before it runs and stay removed if it then fails:
19993
- * that refusal carries them in `details.removedPolicies`, shaped as the
19994
- * result's `removedPolicies`.
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. There
20633
- * is no search. Needs `storage.manage`, Storage on and the backend on and
20634
- * ready (`STORAGE_NOT_ENABLED`, `BACKEND_OFF`, `BACKEND_NOT_READY`, 409);
20635
- * the worker's refusals — `BUCKET_NOT_FOUND`, `INVALID_REQUEST` with
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) return void 0;
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
- if (asciiLower(name) === STORAGE_OBJECTS) return snapshot.storage;
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
- if (stream.acceptKeyword("as")) alias = readIdentifier(stream, "an alias");
22582
- else {
22583
- const next = stream.peek();
22584
- if (next.kind === "ident" && (next.quoted || !isReservedWord(next)))
22585
- alias = readIdentifier(stream, "an alias");
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 (!atStorageObjects(stream)) return readTableName(stream);
22650
- stream.next();
22651
- stream.next();
22652
- stream.next();
22653
- return STORAGE_OBJECTS;
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
- return expr.qualifier === null ? quoteIdent(expr.name) : `${quoteIdent(expr.qualifier)}.${quoteIdent(expr.name)}`;
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, inside) {
23649
+ function value(node, inside2) {
23057
23650
  if (found !== void 0) return;
23058
23651
  switch (node.kind) {
23059
23652
  case "column":
23060
- if (inside && node.qualifier === null) found = node;
23653
+ if (inside2 && node.qualifier === null) found = node;
23061
23654
  return;
23062
23655
  case "func":
23063
- for (const arg of node.args) value(arg, inside);
23656
+ for (const arg of node.args) value(arg, inside2);
23064
23657
  return;
23065
23658
  case "cast":
23066
- value(node.operand, inside);
23659
+ value(node.operand, inside2);
23067
23660
  return;
23068
23661
  case "storage":
23069
- value(node.column, inside);
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, inside) {
23672
+ function visit(node, inside2) {
23080
23673
  if (found !== void 0) return;
23081
23674
  if (isValueExpr(node)) {
23082
- value(node, inside);
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, inside);
23681
+ for (const arg of node.args) visit(arg, inside2);
23089
23682
  return;
23090
23683
  case "not":
23091
- visit(node.arg, inside);
23684
+ visit(node.arg, inside2);
23092
23685
  return;
23093
23686
  case "compare":
23094
23687
  case "distinct":
23095
- value(node.left, inside);
23096
- value(node.right, inside);
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, inside);
23694
+ value(node.operand, inside2);
23102
23695
  return;
23103
23696
  case "in":
23104
- value(node.operand, inside);
23105
- for (const item of node.list) value(item, inside);
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, inside);
23702
+ value(item, inside2);
23110
23703
  return;
23111
23704
  case "inSelect":
23112
- value(node.operand, inside);
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
- return asciiLower(table) === STORAGE_OBJECTS ? STORAGE_OBJECTS : quoteIdent(table);
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
- const { source, column: column2 } = resolveColumn(env, expr);
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 } = resolveColumn(env, expr);
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
- return asciiLower(target2.name) === STORAGE_OBJECTS ? [STORAGE_OBJECTS_QUALIFIER] : [asciiLower(target2.name)];
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
- if (hasSubqueries(all)) this.state.active.push(asciiLower(table.name));
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
- if (value.kind !== "column") return "none";
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
- if (value.kind !== "column") return void 0;
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 start = `Policy ${policy.name} on ${tableLabel(outer)}: its subquery on ${table.name} runs once for every row of ${tableLabel(outer)} it looks at`;
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 outerName = asciiLower(policy.table) === STORAGE_OBJECTS ? STORAGE_OBJECTS_QUALIFIER : asciiLower(outer.name);
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: [outerName] }];
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 = ? AND (`,
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
- [...restValues, ...restValues, i.bucket, ...underValues, ...restValues],
26168
- [...restValues, i.bucket, p],
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
- return asciiLower(name) === STORAGE_OBJECTS ? { table, label: STORAGE_OBJECTS, names: [STORAGE_OBJECTS_QUALIFIER] } : { table, label: table.name, names: [asciiLower(table.name)] };
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
- return asciiLower(name) === STORAGE_OBJECTS ? STORAGE_OBJECTS : resolveTable(snapshot, name)?.name ?? name;
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 clause = readClause(stream);
26723
- using = clause.text;
26724
- position = clause.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 clause = readClause(stream);
26730
- check2 = clause.text;
26731
- position = clause.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 PUBLIC_PREFIXES = ["NEXT_PUBLIC_", "VITE_", "REACT_APP_", "PUBLIC_"];
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 (PUBLIC_PREFIXES.some((prefix) => key.startsWith(prefix)))
29292
+ if (key.startsWith(PUBLIC_PREFIX))
28236
29293
  return value.length > 60 ? `${value.slice(0, 60)}\u2026` : value;
28237
- return value.length <= 8 ? "****" : `${value.slice(0, 4)}****`;
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)) ctx.output.log(line);
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((resolve11, reject) => {
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 resolve11(stdout);
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
- (resolve11) => setTimeout(resolve11, 500 * (attempt + 1))
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 readFile13(join24(sourceDir, "package.json"), "utf8")
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 basename3(sourceDir);
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 = dirname5(fileURLToPath(import.meta.url));
29846
+ let dir = dirname6(fileURLToPath(import.meta.url));
28542
29847
  for (; ; ) {
28543
- const candidate = join24(dir, "node_modules", ".bin", name);
29848
+ const candidate = join25(dir, "node_modules", ".bin", name);
28544
29849
  if (existsSync7(candidate)) return candidate;
28545
- const parent = dirname5(dir);
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 localDependencies(sourceDir) {
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(join24(tmpdir2(), "naro-build-")),
28571
- removeWorkspace: (path) => rm4(path, { recursive: true, force: true }),
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 cp2(sourceDir, checkout, {
28577
- recursive: true,
28578
- // Relative symlinks must stay relative: resolved, they would point
28579
- // back into the developer's real tree, and a strategy's temporary
28580
- // rewrite (proxy -> middleware, vinext toolchain) would land there.
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 appRoot = options.rootDirectory ? resolve10(sourceDir, options.rootDirectory) : sourceDir;
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 git = await (options.gitInfo ?? detectGitInfo)(sourceDir);
28635
- const branch = options.branch ?? git.branch;
28636
- const repositoryRoot = [git.prefix, options.rootDirectory].filter(Boolean).join("/") || null;
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 (git.repositoryUrl) log2(`\u25B8 linking ${git.repositoryUrl}`);
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(rows.map((row) => row.key));
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
- if (options.onMissingEnv) collected = await options.onMissingEnv(missing);
28663
- else for (const line of envAddGuidance(missing)) log2(line);
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
- ...git.repositoryUrl != null && { repositoryUrl: git.repositoryUrl },
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
- ...git.commit != null && { commit: git.commit }
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
- throw new NaroError(
28712
- "BUILD_FAILED",
28713
- error51 instanceof Error ? error51.message : "The local build failed.",
28714
- { details: { deploymentId: ctx.deploymentId }, cause: error51 }
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(client, ctx, log2);
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: 2500,
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.subdomain ?? ctx.projectId;
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://${surface}${production ? "" : "-preview"}.${host}`;
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 as parseDotenv,
30233
+ parseDotenv2,
28850
30234
  readProjectEnv2 as readProjectEnv,
28851
30235
  eventsObserver,
28852
30236
  slugify2 as slugify,
30237
+ workingTreeNotices,
30238
+ localDependencies,
28853
30239
  deployDirectory
28854
30240
  };