netlify-cli 27.14.2 → 27.15.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,19 +5,9 @@ import { normalizeRepoUrl } from '../../utils/normalize-repo-url.js';
5
5
  import { chalk, logAndThrowError, log, getToken, netlifyCommand } from '../../utils/command-helpers.js';
6
6
  import { runGit } from '../../utils/run-git.js';
7
7
  import execa from '../../utils/execa.js';
8
- import { NETLIFY_GIT_HOST } from '../git-credential/git-credential.js';
8
+ import { isNetlifyGitUrl, NETLIFY_GIT_HOST } from '../../utils/netlify-git.js';
9
9
  import { link } from '../link/link.js';
10
10
  import { startSpinner } from '../../lib/spinner.js';
11
- const NETLIFY_GIT_SERVICE_HOST = 'hgit.services-prod.nsvcs.net';
12
- const isNetlifyGitServiceUrl = (repoUrl) => {
13
- try {
14
- const host = new URL(repoUrl).host;
15
- return host === NETLIFY_GIT_HOST || host === NETLIFY_GIT_SERVICE_HOST;
16
- }
17
- catch {
18
- return false;
19
- }
20
- };
21
11
  const getTargetDir = async (defaultDir) => {
22
12
  return await input({
23
13
  message: 'Where should we clone the repository?',
@@ -192,7 +182,7 @@ export const clone = async (options, command, args) => {
192
182
  }
193
183
  siteSpinner.success(`Found site ${chalk.cyan(siteInfo.name)}`);
194
184
  const connectedRepoUrl = siteInfo.build_settings?.repo_url;
195
- if (connectedRepoUrl && isNetlifyGitServiceUrl(connectedRepoUrl)) {
185
+ if (connectedRepoUrl && isNetlifyGitUrl(connectedRepoUrl)) {
196
186
  log(`Site is connected to Netlify's managed git service.`);
197
187
  log(`Cloning from Netlify's managed git service...`);
198
188
  log();
@@ -19,6 +19,7 @@ import { NETLIFY_CYAN_HEX, NETLIFYDEVERR, NETLIFYDEVLOG, chalk, logAndThrowError
19
19
  import { DEFAULT_CONCURRENT_HASH, DEFAULT_DEPLOY_TIMEOUT } from '../../utils/deploy/constants.js';
20
20
  import { deploySite } from '../../utils/deploy/deploy-site.js';
21
21
  import { getDeploySourceFields } from '../../utils/deploy/deploy-source.js';
22
+ import { getDeployUrls } from '../../utils/deploy/deploy-urls.js';
22
23
  import { uploadSourceZip } from '../../utils/deploy/upload-source-zip.js';
23
24
  import { getEnvelopeEnv } from '../../utils/env/index.js';
24
25
  import { mergeDeployEnvVars } from '../../utils/env/deploy-env-vars.js';
@@ -472,24 +473,11 @@ const runDeploy = async ({ alias, api, command, config, deployFolder, deployTime
472
473
  }
473
474
  return reportDeployError({ error: error, failAndExit: logAndThrowError });
474
475
  }
475
- const siteUrl = results.deploy.ssl_url || results.deploy.url;
476
- const deployUrl = results.deploy.deploy_ssl_url || results.deploy.deploy_url;
477
- const logsUrl = `${results.deploy.admin_url}/deploys/${results.deploy.id}`;
478
- let functionLogsUrl = `${results.deploy.admin_url}/logs/functions`;
479
- let edgeFunctionLogsUrl = `${results.deploy.admin_url}/logs/edge-functions`;
480
- if (!deployToProduction) {
481
- functionLogsUrl += `?scope=deploy:${deployId}`;
482
- edgeFunctionLogsUrl += `?scope=deployid:${deployId}`;
483
- }
484
476
  return {
485
477
  siteId: results.deploy.site_id,
486
478
  siteName: results.deploy.name,
487
479
  deployId: results.deployId,
488
- siteUrl,
489
- deployUrl,
490
- logsUrl,
491
- functionLogsUrl,
492
- edgeFunctionLogsUrl,
480
+ ...getDeployUrls(results.deploy, { deployToProduction }),
493
481
  sourceZipFileName: uploadSourceZipResult?.sourceZipFileName,
494
482
  };
495
483
  };
@@ -1,6 +1,5 @@
1
1
  import type { Readable, Writable } from 'stream';
2
2
  import type { GitCredentialOptionValues } from './option_values.js';
3
- export declare const NETLIFY_GIT_HOST = "git.netlify.com";
4
3
  export declare const parseGitCredentialInput: (input: Readable) => Promise<Partial<Record<string, string>>>;
5
4
  export declare const writeCredentials: (output: Writable, token: string) => void;
6
5
  export declare const gitCredential: (operation: string, _options: GitCredentialOptionValues) => Promise<void>;
@@ -1,7 +1,7 @@
1
1
  import process from 'process';
2
2
  import readline from 'readline';
3
3
  import { getToken } from '../../utils/command-helpers.js';
4
- export const NETLIFY_GIT_HOST = 'git.netlify.com';
4
+ import { NETLIFY_GIT_HOST } from '../../utils/netlify-git.js';
5
5
  export const parseGitCredentialInput = async (input) => {
6
6
  const rl = readline.createInterface({
7
7
  input,
@@ -1,6 +1,7 @@
1
1
  import { select } from '@inquirer/prompts';
2
2
  import { log, chalk } from '../../utils/command-helpers.js';
3
3
  import { getWebSocket } from '../../utils/websockets/index.js';
4
+ import { SOCKETEER_URL } from '../../utils/socketeer.js';
4
5
  export function getName({ deploy, userId }) {
5
6
  let normalisedName = '';
6
7
  const isUserDeploy = deploy.user_id === userId;
@@ -51,7 +52,7 @@ export const logsBuild = async (_options, command) => {
51
52
  deploy = deploys.find((dep) => dep.id === result) || deploy;
52
53
  }
53
54
  const { id } = deploy;
54
- const ws = getWebSocket(`wss://socketeer.services.netlify.com/build/logs`);
55
+ const ws = getWebSocket(`${SOCKETEER_URL}/build/logs`);
55
56
  ws.on('open', function open() {
56
57
  ws.send(JSON.stringify({ deploy_id: id, site_id: siteId, access_token: client.accessToken }));
57
58
  });
@@ -78,14 +78,15 @@ export const createLogsCommand = (program) => {
78
78
  'functions',
79
79
  'edge-functions',
80
80
  'deploy',
81
+ 'server',
81
82
  ]))
82
83
  .addOption(new Option('--function <name...>', 'Filter to specific functions by name'))
83
84
  .addOption(new Option('--edge-function <name...>', 'Filter to specific edge functions by name or path'))
84
85
  .addOption(new Option('--since <time>', 'Start of the historical log window. Accepts a duration (e.g. 10m, 1h, 24h) or an ISO 8601 timestamp. Defaults to 10m'))
85
86
  .addOption(new Option('--until <time>', 'End of the historical log window. Accepts a duration or an ISO 8601 timestamp (defaults to now)'))
86
87
  .addOption(new Option('-f, --follow', 'Stream logs in real time instead of showing historical logs'))
87
- .addOption(new Option('-u, --url <url>', 'Show logs for the deploy behind the given URL. Supports deploy permalinks and branch subdomains. Can be used with all sources.'))
88
- .addOption(new Option('-d, --deploy-id <deploy-id>', 'Show logs for a specific deploy by ID, including failed builds. Can be used with all sources.'))
88
+ .addOption(new Option('-u, --url <url>', 'Show logs for the deploy behind the given URL. Supports deploy permalinks and branch subdomains. Can be used with all sources except server.'))
89
+ .addOption(new Option('-d, --deploy-id <deploy-id>', 'Show logs for a specific deploy by ID, including failed builds. Can be used with all sources except server.'))
89
90
  .addOption(new Option('-l, --level <levels...>', `Log levels to include. Choices are:${CLI_LOG_LEVEL_CHOICES_STRING}`))
90
91
  .addOption(new Option('--json', 'Output logs as JSON Lines'))
91
92
  .addExamples([
@@ -96,6 +97,8 @@ export const createLogsCommand = (program) => {
96
97
  'netlify logs --source functions --function checkout --since 24h',
97
98
  'netlify logs --source edge-functions --since 30m',
98
99
  'netlify logs --source deploy --source functions --since 1h',
100
+ 'netlify logs --source server --since 30m',
101
+ 'netlify logs --follow --source server',
99
102
  'netlify logs --follow',
100
103
  'netlify logs --follow --source functions --source edge-functions',
101
104
  'netlify logs --json --since 1h',
@@ -4,7 +4,7 @@ export declare const DEPLOY_ID_RE: RegExp;
4
4
  export declare const SOURCE_INDICATORS: Record<LogEntry['source'], string>;
5
5
  export declare const SOURCE_LABELS: Record<LogEntry['source'], string>;
6
6
  export interface LogEntry {
7
- source: 'function' | 'edge-function' | 'deploy';
7
+ source: 'function' | 'edge-function' | 'deploy' | 'server';
8
8
  name: string;
9
9
  ts: number;
10
10
  level: string;
@@ -7,11 +7,13 @@ export const SOURCE_INDICATORS = {
7
7
  function: '𝒇',
8
8
  'edge-function': '🌐',
9
9
  deploy: '🚀',
10
+ server: '🖥️',
10
11
  };
11
12
  export const SOURCE_LABELS = {
12
13
  function: 'Function',
13
14
  'edge-function': 'Edge function',
14
15
  deploy: 'Deploy',
16
+ server: 'Server',
15
17
  };
16
18
  const DURATION_LIKE_RE = /^\d+(\.\d+)?\s*[a-z]/i;
17
19
  export const parseTimeValue = (input, now = Date.now()) => {
@@ -1,7 +1,7 @@
1
1
  import type { NetlifyAPI } from '@netlify/api';
2
2
  import type BaseCommand from '../base-command.js';
3
3
  import type { LogsOptionValues } from './option_values.js';
4
- type Source = 'functions' | 'edge-functions' | 'deploy';
4
+ type Source = 'functions' | 'edge-functions' | 'deploy' | 'server';
5
5
  export declare const logsCommand: (options: LogsOptionValues, command: BaseCommand) => Promise<undefined>;
6
6
  export declare const runFollowMode: ({ sources, client, siteId, accessToken, deployId, deployTargeted, functionNames, edgeFunctionNames, levelsToPrint, json, }: {
7
7
  sources: Source[];
@@ -1,10 +1,11 @@
1
- import { chalk, log, logAndThrowError, netlifyCommand } from '../../utils/command-helpers.js';
1
+ import { chalk, log, logAndThrowError, logError, netlifyCommand, warn } from '../../utils/command-helpers.js';
2
2
  import { createColorAssigner, DEPLOY_ID_RE, formatJsonLine, formatLogLine, parseTimeValue, resolveDeployIdFromUrl, SOURCE_INDICATORS, SOURCE_LABELS, } from './log-api.js';
3
3
  import { LOG_LEVELS_LIST, CLI_LOG_LEVEL_CHOICES_STRING } from './log-levels.js';
4
4
  import { fetchDeployHistoricalLogs, findCurrentBuildingDeploy, findLatestFinishedDeploy, findLatestReadyDeploy, isDeployFinished, streamDeploy, } from './sources/deploy.js';
5
5
  import { fetchEdgeFunctionHistoricalLogs, streamEdgeFunctions } from './sources/edge-functions.js';
6
6
  import { fetchFunctionHistoricalLogs, listFunctions, selectFunctions, streamFunctions, validateFunctionCount, } from './sources/functions.js';
7
- const VALID_SOURCES = ['functions', 'edge-functions', 'deploy'];
7
+ import { fetchServerHistoricalLogs, streamServer } from './sources/server.js';
8
+ const VALID_SOURCES = ['functions', 'edge-functions', 'deploy', 'server'];
8
9
  const DEFAULT_SINCE = '10m';
9
10
  const DEPLOY_STREAM_IDLE_CLOSE_MS = 3_000;
10
11
  const parseSources = (rawSources) => {
@@ -23,6 +24,7 @@ const SOURCE_TO_ENTRY_SOURCE = {
23
24
  functions: 'function',
24
25
  'edge-functions': 'edge-function',
25
26
  deploy: 'deploy',
27
+ server: 'server',
26
28
  };
27
29
  const humanizeTimeRange = (sinceValue, untilValue) => {
28
30
  if (!sinceValue && !untilValue) {
@@ -45,6 +47,8 @@ const printHeader = (sources, timeDescription, isFollow) => {
45
47
  return 'edge functions';
46
48
  case 'deploy':
47
49
  return 'deploy';
50
+ case 'server':
51
+ return 'server';
48
52
  }
49
53
  });
50
54
  const sourceList = sourceNames.length <= 2
@@ -122,6 +126,9 @@ export const logsCommand = async (options, command) => {
122
126
  sources = ['functions', 'edge-functions'];
123
127
  }
124
128
  }
129
+ if (sources.includes('server') && (options.deployId || options.url)) {
130
+ return logAndThrowError('Server logs cannot be filtered by deploy. Remove --deploy-id and --url, or the server source.');
131
+ }
125
132
  let deployId;
126
133
  let deployTargeted = false;
127
134
  if (options.deployId) {
@@ -265,6 +272,19 @@ const runHistoricalMode = async ({ sources, client, siteId, accessToken, deployI
265
272
  });
266
273
  allEntries.push(...entries);
267
274
  }
275
+ let serverLogsTruncated = false;
276
+ if (sources.includes('server')) {
277
+ const { entries, truncated } = await fetchServerHistoricalLogs({
278
+ apiBase: client.basePath,
279
+ siteId,
280
+ accessToken,
281
+ from,
282
+ to,
283
+ levels: levelsToPrint,
284
+ });
285
+ allEntries.push(...entries);
286
+ serverLogsTruncated = truncated;
287
+ }
268
288
  allEntries.sort((a, b) => a.ts - b.ts);
269
289
  if (allEntries.length === 0) {
270
290
  log('No logs found for the given time range.');
@@ -278,6 +298,15 @@ const runHistoricalMode = async ({ sources, client, siteId, accessToken, deployI
278
298
  const key = `${entry.source}:${entry.name}`;
279
299
  printEntry(entry, levelsToPrint, json, assignColor(key));
280
300
  }
301
+ if (serverLogsTruncated) {
302
+ const message = 'Only the most recent server logs are shown. Narrow --since/--until to see older ones.';
303
+ if (json) {
304
+ process.stderr.write(`Warning: ${message}\n`);
305
+ }
306
+ else {
307
+ warn(message);
308
+ }
309
+ }
281
310
  };
282
311
  export const runFollowMode = async ({ sources, client, siteId, accessToken, deployId, deployTargeted, functionNames, edgeFunctionNames, levelsToPrint, json, }) => {
283
312
  const assignColor = createColorAssigner();
@@ -316,5 +345,15 @@ export const runFollowMode = async ({ sources, client, siteId, accessToken, depl
316
345
  if (sources.includes('edge-functions') && deployId) {
317
346
  streamEdgeFunctions(siteId, deployId, accessToken, edgeFunctionNames, onEntry);
318
347
  }
348
+ if (sources.includes('server')) {
349
+ streamServer({
350
+ apiBase: client.basePath,
351
+ siteId,
352
+ accessToken,
353
+ levels: levelsToPrint,
354
+ onEntry,
355
+ onError: logError,
356
+ });
357
+ }
319
358
  };
320
359
  //# sourceMappingURL=logs.js.map
@@ -1,4 +1,5 @@
1
1
  import { getWebSocket } from '../../../utils/websockets/index.js';
2
+ import { SOCKETEER_URL } from '../../../utils/socketeer.js';
2
3
  const DEPLOY_LOG_REPLAY_TIMEOUT_MS = 30_000;
3
4
  const DEPLOY_STREAM_START_TIMEOUT_MS = 20_000;
4
5
  const parseDeployLogTimestamp = (message) => {
@@ -29,7 +30,7 @@ const parseDeployLogMessage = (data) => {
29
30
  };
30
31
  export const fetchDeployHistoricalLogs = async ({ siteId, accessToken, deployId, from, to, }) => {
31
32
  const collected = [];
32
- const ws = getWebSocket('wss://socketeer.services.netlify.com/build/logs');
33
+ const ws = getWebSocket(`${SOCKETEER_URL}/build/logs`);
33
34
  await new Promise((resolve, reject) => {
34
35
  let settled = false;
35
36
  const finish = (error) => {
@@ -84,7 +85,7 @@ export const fetchDeployHistoricalLogs = async ({ siteId, accessToken, deployId,
84
85
  .sort((a, b) => a.ts - b.ts);
85
86
  };
86
87
  export const streamDeploy = (siteId, deployId, accessToken, onEntry, onClose, { closeWhenIdleMs } = {}) => {
87
- const ws = getWebSocket('wss://socketeer.services.netlify.com/build/logs');
88
+ const ws = getWebSocket(`${SOCKETEER_URL}/build/logs`);
88
89
  let closeTimer;
89
90
  const scheduleClose = (ms) => {
90
91
  clearTimeout(closeTimer);
@@ -1,4 +1,5 @@
1
1
  import { getWebSocket } from '../../../utils/websockets/index.js';
2
+ import { SOCKETEER_URL } from '../../../utils/socketeer.js';
2
3
  import { buildEdgeFunctionLogsUrl, debugFetch, fetchHistoricalLogs } from '../log-api.js';
3
4
  export const listEdgeFunctions = async (apiBase, accessToken, deployId) => {
4
5
  const response = await debugFetch(`${apiBase}/api/v1/deploys/${encodeURIComponent(deployId)}/edge_functions`, {
@@ -41,7 +42,7 @@ export const fetchEdgeFunctionHistoricalLogs = async ({ siteId, accessToken, fro
41
42
  }));
42
43
  };
43
44
  export const streamEdgeFunctions = (siteId, deployId, accessToken, filterNames, onEntry) => {
44
- const ws = getWebSocket('wss://socketeer.services.netlify.com/edge-function/logs');
45
+ const ws = getWebSocket(`${SOCKETEER_URL}/edge-function/logs`);
45
46
  ws.on('open', () => {
46
47
  ws.send(JSON.stringify({
47
48
  deploy_id: deployId,
@@ -1,4 +1,5 @@
1
1
  import { getWebSocket } from '../../../utils/websockets/index.js';
2
+ import { SOCKETEER_URL } from '../../../utils/socketeer.js';
2
3
  import { buildFunctionLogsUrl, fetchHistoricalLogs } from '../log-api.js';
3
4
  const MAX_CONCURRENT_FUNCTIONS = 10;
4
5
  export const listFunctions = async (client, siteId, deployId) => {
@@ -45,7 +46,7 @@ export const fetchFunctionHistoricalLogs = async ({ functions, siteId, accessTok
45
46
  export const streamFunctions = (functions, siteId, accessToken, onEntry) => {
46
47
  const sockets = [];
47
48
  for (const fn of functions) {
48
- const ws = getWebSocket('wss://socketeer.services.netlify.com/function/logs');
49
+ const ws = getWebSocket(`${SOCKETEER_URL}/function/logs`);
49
50
  sockets.push(ws);
50
51
  ws.on('open', () => {
51
52
  ws.send(JSON.stringify({
@@ -0,0 +1,22 @@
1
+ import type { LogEntry } from '../log-api.js';
2
+ export declare const fetchServerHistoricalLogs: (options: {
3
+ apiBase: string;
4
+ siteId: string;
5
+ accessToken: string | null | undefined;
6
+ from: number;
7
+ to: number;
8
+ levels?: string[];
9
+ }) => Promise<{
10
+ entries: LogEntry[];
11
+ truncated: boolean;
12
+ }>;
13
+ export declare const streamServer: ({ apiBase, siteId, accessToken, levels, onEntry, onError, intervalMs, }: {
14
+ apiBase: string;
15
+ siteId: string;
16
+ accessToken: string | null | undefined;
17
+ levels?: string[];
18
+ onEntry: (entry: LogEntry) => void;
19
+ onError?: (error: Error) => void;
20
+ intervalMs?: number;
21
+ }) => (() => void);
22
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1,141 @@
1
+ import { debugFetch } from '../log-api.js';
2
+ // The API serves up to 500 lines a page and stops paging 10,000 lines back.
3
+ const PER_PAGE = 500;
4
+ const MAX_PAGES = 1 + 10_000 / PER_PAGE;
5
+ // Lines are stored a few seconds after their time, so each poll looks back
6
+ // this far and drops what the previous poll already returned.
7
+ const LOOK_BACK_MS = 10_000;
8
+ const POLL_INTERVAL_MS = 2_000;
9
+ // Retrying cannot change these answers, so following stops on them.
10
+ const PERMANENT_FAILURE_STATUSES = new Set([401, 403, 404]);
11
+ // The API filters by at most one severity, and only these, though lines can
12
+ // carry other levels too.
13
+ const FILTERABLE_SEVERITIES = new Set(['error', 'warn', 'info']);
14
+ const severityFor = (levels) => levels?.length === 1 && FILTERABLE_SEVERITIES.has(levels[0]) ? levels[0] : undefined;
15
+ class ServerLogsRequestError extends Error {
16
+ status;
17
+ constructor(status, statusText) {
18
+ super(`Failed to fetch server logs: ${status.toString()} ${statusText}`);
19
+ this.status = status;
20
+ }
21
+ }
22
+ const lineKey = (line) => `${line.timestamp.toString()}|${line.actor_id}|${line.severity}|${line.body}`;
23
+ const countKeys = (lines) => {
24
+ const counts = new Map();
25
+ for (const line of lines) {
26
+ const key = lineKey(line);
27
+ counts.set(key, (counts.get(key) ?? 0) + 1);
28
+ }
29
+ return counts;
30
+ };
31
+ const toEntry = (line) => ({
32
+ source: 'server',
33
+ // The dashboard shows instances by the first 8 characters of their ID too.
34
+ name: line.actor_id ? line.actor_id.slice(0, 8) : 'server',
35
+ ts: line.timestamp,
36
+ level: line.severity ? line.severity.toUpperCase() : 'INFO',
37
+ message: line.body,
38
+ });
39
+ const fetchServerLogLines = async ({ apiBase, siteId, accessToken, from, to, levels, }) => {
40
+ // The API pages newest first and sends millisecond timestamps, though it
41
+ // orders by finer ones, so reversing is what keeps same-millisecond lines in order.
42
+ const lines = [];
43
+ const severity = severityFor(levels);
44
+ for (let page = 1; page <= MAX_PAGES; page++) {
45
+ const url = new URL(`${apiBase}/sites/${encodeURIComponent(siteId)}/observability/server/logs`);
46
+ url.searchParams.set('from_ts', String(from));
47
+ url.searchParams.set('to_ts', String(to));
48
+ url.searchParams.set('page', String(page));
49
+ url.searchParams.set('per_page', String(PER_PAGE));
50
+ if (severity) {
51
+ url.searchParams.set('severity', severity);
52
+ }
53
+ const response = await debugFetch(url.toString(), {
54
+ headers: {
55
+ Authorization: `Bearer ${accessToken ?? ''}`,
56
+ },
57
+ });
58
+ if (!response.ok) {
59
+ throw new ServerLogsRequestError(response.status, response.statusText);
60
+ }
61
+ const body = (await response.json());
62
+ lines.push(...(body.logs ?? []));
63
+ if (!body.has_next_page) {
64
+ return { lines: lines.reverse(), truncated: false };
65
+ }
66
+ }
67
+ return { lines: lines.reverse(), truncated: true };
68
+ };
69
+ export const fetchServerHistoricalLogs = async (options) => {
70
+ const { lines, truncated } = await fetchServerLogLines(options);
71
+ return { entries: lines.map(toEntry), truncated };
72
+ };
73
+ export const streamServer = ({ apiBase, siteId, accessToken, levels, onEntry, onError, intervalMs = POLL_INTERVAL_MS, }) => {
74
+ let newest = Date.now();
75
+ let seen = new Map();
76
+ let failing = false;
77
+ let truncating = false;
78
+ let stopped = false;
79
+ let timer;
80
+ const poll = async () => {
81
+ try {
82
+ const { lines, truncated } = await fetchServerLogLines({
83
+ apiBase,
84
+ siteId,
85
+ accessToken,
86
+ levels,
87
+ from: newest - LOOK_BACK_MS,
88
+ to: Date.now(),
89
+ });
90
+ if (stopped) {
91
+ return;
92
+ }
93
+ // Lines carry no ID, so identical ones are told apart by how many of them
94
+ // the previous poll returned.
95
+ const unclaimed = new Map(seen);
96
+ const fresh = lines
97
+ .filter((line) => {
98
+ const key = lineKey(line);
99
+ const count = unclaimed.get(key) ?? 0;
100
+ unclaimed.set(key, count - 1);
101
+ return count <= 0;
102
+ })
103
+ .sort((a, b) => a.timestamp - b.timestamp);
104
+ for (const line of fresh) {
105
+ onEntry(toEntry(line));
106
+ }
107
+ // The API pages newest first, so a truncated window loses its oldest lines for good.
108
+ if (truncated && !truncating) {
109
+ onError?.(new Error('Server logs are arriving faster than they can be shown, so some lines were skipped.'));
110
+ }
111
+ truncating = truncated;
112
+ // The next window starts inside this one, so only these lines can repeat.
113
+ seen = countKeys(lines);
114
+ newest = fresh.reduce((max, line) => Math.max(max, line.timestamp), newest);
115
+ failing = false;
116
+ }
117
+ catch (error) {
118
+ const failure = error instanceof Error ? error : new Error(String(error));
119
+ if (error instanceof ServerLogsRequestError && PERMANENT_FAILURE_STATUSES.has(error.status)) {
120
+ stopped = true;
121
+ onError?.(failure);
122
+ return;
123
+ }
124
+ // The next poll's window still covers what this one missed, so a run of
125
+ // failures is reported once.
126
+ if (!failing) {
127
+ failing = true;
128
+ onError?.(failure);
129
+ }
130
+ }
131
+ if (!stopped) {
132
+ timer = setTimeout(() => void poll(), intervalMs);
133
+ }
134
+ };
135
+ void poll();
136
+ return () => {
137
+ stopped = true;
138
+ clearTimeout(timer);
139
+ };
140
+ };
141
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1,20 @@
1
+ export interface DeployUrls {
2
+ siteUrl: string;
3
+ deployUrl: string;
4
+ logsUrl: string;
5
+ functionLogsUrl: string;
6
+ edgeFunctionLogsUrl: string;
7
+ }
8
+ interface DeployUrlFields {
9
+ id?: string;
10
+ ssl_url?: string;
11
+ url?: string;
12
+ deploy_ssl_url?: string;
13
+ deploy_url?: string;
14
+ admin_url?: string;
15
+ }
16
+ export declare const getDeployUrls: (deploy: DeployUrlFields, { deployToProduction }: {
17
+ deployToProduction: boolean;
18
+ }) => DeployUrls;
19
+ export {};
20
+ //# sourceMappingURL=deploy-urls.d.ts.map
@@ -0,0 +1,14 @@
1
+ export const getDeployUrls = (deploy, { deployToProduction }) => {
2
+ const functionLogsUrl = `${deploy.admin_url ?? ''}/logs/functions`;
3
+ const edgeFunctionLogsUrl = `${deploy.admin_url ?? ''}/logs/edge-functions`;
4
+ return {
5
+ siteUrl: deploy.ssl_url ?? deploy.url ?? '',
6
+ deployUrl: deploy.deploy_ssl_url ?? deploy.deploy_url ?? '',
7
+ logsUrl: `${deploy.admin_url ?? ''}/deploys/${deploy.id ?? ''}`,
8
+ functionLogsUrl: deployToProduction ? functionLogsUrl : `${functionLogsUrl}?scope=deploy:${deploy.id ?? ''}`,
9
+ edgeFunctionLogsUrl: deployToProduction
10
+ ? edgeFunctionLogsUrl
11
+ : `${edgeFunctionLogsUrl}?scope=deployid:${deploy.id ?? ''}`,
12
+ };
13
+ };
14
+ //# sourceMappingURL=deploy-urls.js.map
@@ -9,6 +9,7 @@ interface ServerPayload {
9
9
  routes?: ServerResult['routes'];
10
10
  excluded_routes?: ServerResult['excludedRoutes'];
11
11
  runtime_version?: ServerResult['runtimeVersion'];
12
+ scale?: ServerResult['scale'];
12
13
  }
13
14
  type HashedFunctionFile = FunctionUploadFile & {
14
15
  hash: string;
@@ -205,7 +205,7 @@ const hashServer = async (serverBundle, { concurrentHash, hashAlgorithm, statusC
205
205
  hasherCtor({ concurrentHash, hashAlgorithm }),
206
206
  manifestCollectorCtor(servers, serverShaMap, { statusCb }),
207
207
  ]);
208
- const { excludedRoutes, instanceSize, region, routes, runtimeVersion } = serverBundle;
208
+ const { excludedRoutes, instanceSize, region, routes, runtimeVersion, scale } = serverBundle;
209
209
  const server = {
210
210
  sha: Object.values(servers)[0],
211
211
  ...(region === undefined ? {} : { region }),
@@ -213,6 +213,7 @@ const hashServer = async (serverBundle, { concurrentHash, hashAlgorithm, statusC
213
213
  ...(routes === undefined ? {} : { routes }),
214
214
  ...(excludedRoutes === undefined ? {} : { excluded_routes: excludedRoutes }),
215
215
  ...(runtimeVersion === undefined ? {} : { runtime_version: runtimeVersion }),
216
+ ...(scale === undefined ? {} : { scale }),
216
217
  };
217
218
  return { server, serverShaMap };
218
219
  };
@@ -0,0 +1,3 @@
1
+ export declare const NETLIFY_GIT_HOST = "git.netlify.com";
2
+ export declare const isNetlifyGitUrl: (repoUrl: string) => boolean;
3
+ //# sourceMappingURL=netlify-git.d.ts.map
@@ -0,0 +1,12 @@
1
+ export const NETLIFY_GIT_HOST = 'git.netlify.com';
2
+ const NETLIFY_GIT_SERVICE_HOST = 'hgit.services-prod.nsvcs.net';
3
+ export const isNetlifyGitUrl = (repoUrl) => {
4
+ try {
5
+ const { host } = new URL(repoUrl);
6
+ return host === NETLIFY_GIT_HOST || host === NETLIFY_GIT_SERVICE_HOST;
7
+ }
8
+ catch {
9
+ return false;
10
+ }
11
+ };
12
+ //# sourceMappingURL=netlify-git.js.map
@@ -582,6 +582,9 @@ const onRequest = async ({ api, edgeFunctionsProxy, env, functionsRegistry, func
582
582
  for (let index = 0; index < req.rawHeaders.length; index += 2) {
583
583
  headers.append(req.rawHeaders[index], req.rawHeaders[index + 1]);
584
584
  }
585
+ // The server handler gets a `Request` with no socket, so it reads the
586
+ // client's address from this header to set `context.ip`.
587
+ headers.set('x-forwarded-for', req.socket.remoteAddress ?? '');
585
588
  const response = await serverMatch.handle(new Request(requestURL, {
586
589
  body: req.originalBody,
587
590
  headers,
@@ -0,0 +1,2 @@
1
+ export declare const SOCKETEER_URL: string;
2
+ //# sourceMappingURL=socketeer.d.ts.map
@@ -0,0 +1,2 @@
1
+ export const SOCKETEER_URL = process.env.NETLIFY_SOCKETEER_URL ?? 'wss://socketeer.services.netlify.com';
2
+ //# sourceMappingURL=socketeer.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "netlify-cli",
3
- "version": "27.14.2",
3
+ "version": "27.15.0",
4
4
  "description": "Netlify command line tool",
5
5
  "keywords": [
6
6
  "api",
@@ -57,9 +57,9 @@
57
57
  "@netlify/ai": "^1.0.2",
58
58
  "@netlify/api": "^15.1.3",
59
59
  "@netlify/blobs": "^11.1.6",
60
- "@netlify/build": "^37.5.1",
60
+ "@netlify/build": "^37.7.0",
61
61
  "@netlify/build-info": "^11.3.0",
62
- "@netlify/config": "^25.3.0",
62
+ "@netlify/config": "^25.5.0",
63
63
  "@netlify/dev": "^5.1.10",
64
64
  "@netlify/dev-utils": "^6.0.4",
65
65
  "@netlify/edge-bundler": "^16.1.3",
@@ -71,7 +71,7 @@
71
71
  "@netlify/redirect-matcher": "^0.4.2",
72
72
  "@netlify/redirect-parser": "^16.1.1",
73
73
  "@netlify/server-dev": "^0.1.4",
74
- "@netlify/zip-it-and-ship-it": "^16.4.1",
74
+ "@netlify/zip-it-and-ship-it": "^16.6.0",
75
75
  "@opentelemetry/api": "~1.9.0",
76
76
  "@pnpm/tabtab": "^0.5.4",
77
77
  "ansi-escapes": "^7.3.0",
@@ -139,7 +139,7 @@
139
139
  "semver": "^7.7.2",
140
140
  "source-map-support": "^0.5.21",
141
141
  "terminal-link": "^5.0.0",
142
- "toml": "^4.0.0",
142
+ "toml": "^5.0.0",
143
143
  "tomlify-j0.4": "^3.0.0",
144
144
  "ulid": "^3.0.1",
145
145
  "update-notifier": "^7.3.1",