@vinktarhq/cli 0.1.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.
Files changed (121) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +21 -0
  3. package/README.md +378 -0
  4. package/dist/bin.d.ts +2 -0
  5. package/dist/bin.js +28 -0
  6. package/dist/bin.js.map +1 -0
  7. package/dist/bundler/core.d.ts +140 -0
  8. package/dist/bundler/core.js +320 -0
  9. package/dist/bundler/core.js.map +1 -0
  10. package/dist/bundler/esbuild.d.ts +44 -0
  11. package/dist/bundler/esbuild.js +85 -0
  12. package/dist/bundler/esbuild.js.map +1 -0
  13. package/dist/bundler/rollup.d.ts +84 -0
  14. package/dist/bundler/rollup.js +221 -0
  15. package/dist/bundler/rollup.js.map +1 -0
  16. package/dist/bundler/rspack.d.ts +8 -0
  17. package/dist/bundler/rspack.js +8 -0
  18. package/dist/bundler/rspack.js.map +1 -0
  19. package/dist/bundler/webpack.d.ts +83 -0
  20. package/dist/bundler/webpack.js +104 -0
  21. package/dist/bundler/webpack.js.map +1 -0
  22. package/dist/cjs/bundler/core.d.ts +140 -0
  23. package/dist/cjs/bundler/core.js +331 -0
  24. package/dist/cjs/bundler/esbuild.d.ts +44 -0
  25. package/dist/cjs/bundler/esbuild.js +87 -0
  26. package/dist/cjs/bundler/rollup.d.ts +84 -0
  27. package/dist/cjs/bundler/rollup.js +226 -0
  28. package/dist/cjs/bundler/rspack.d.ts +8 -0
  29. package/dist/cjs/bundler/rspack.js +12 -0
  30. package/dist/cjs/bundler/webpack.d.ts +83 -0
  31. package/dist/cjs/bundler/webpack.js +107 -0
  32. package/dist/cjs/cli.d.ts +17 -0
  33. package/dist/cjs/cli.js +358 -0
  34. package/dist/cjs/commands/doctor.d.ts +15 -0
  35. package/dist/cjs/commands/doctor.js +134 -0
  36. package/dist/cjs/commands/inject.d.ts +70 -0
  37. package/dist/cjs/commands/inject.js +123 -0
  38. package/dist/cjs/commands/resolve.d.ts +32 -0
  39. package/dist/cjs/commands/resolve.js +130 -0
  40. package/dist/cjs/commands/upload.d.ts +74 -0
  41. package/dist/cjs/commands/upload.js +254 -0
  42. package/dist/cjs/config.d.ts +51 -0
  43. package/dist/cjs/config.js +152 -0
  44. package/dist/cjs/debug-id.d.ts +176 -0
  45. package/dist/cjs/debug-id.js +323 -0
  46. package/dist/cjs/discover.d.ts +50 -0
  47. package/dist/cjs/discover.js +235 -0
  48. package/dist/cjs/env.d.ts +22 -0
  49. package/dist/cjs/env.js +52 -0
  50. package/dist/cjs/glob.d.ts +34 -0
  51. package/dist/cjs/glob.js +112 -0
  52. package/dist/cjs/http.d.ts +68 -0
  53. package/dist/cjs/http.js +211 -0
  54. package/dist/cjs/index.d.ts +17 -0
  55. package/dist/cjs/index.js +53 -0
  56. package/dist/cjs/limits.d.ts +35 -0
  57. package/dist/cjs/limits.js +38 -0
  58. package/dist/cjs/multipart.d.ts +28 -0
  59. package/dist/cjs/multipart.js +44 -0
  60. package/dist/cjs/package.json +3 -0
  61. package/dist/cjs/upload.d.ts +178 -0
  62. package/dist/cjs/upload.js +394 -0
  63. package/dist/cjs/url.d.ts +25 -0
  64. package/dist/cjs/url.js +43 -0
  65. package/dist/cjs/version.d.ts +10 -0
  66. package/dist/cjs/version.js +13 -0
  67. package/dist/cli.d.ts +17 -0
  68. package/dist/cli.js +354 -0
  69. package/dist/cli.js.map +1 -0
  70. package/dist/commands/doctor.d.ts +15 -0
  71. package/dist/commands/doctor.js +132 -0
  72. package/dist/commands/doctor.js.map +1 -0
  73. package/dist/commands/inject.d.ts +70 -0
  74. package/dist/commands/inject.js +120 -0
  75. package/dist/commands/inject.js.map +1 -0
  76. package/dist/commands/resolve.d.ts +32 -0
  77. package/dist/commands/resolve.js +128 -0
  78. package/dist/commands/resolve.js.map +1 -0
  79. package/dist/commands/upload.d.ts +74 -0
  80. package/dist/commands/upload.js +252 -0
  81. package/dist/commands/upload.js.map +1 -0
  82. package/dist/config.d.ts +51 -0
  83. package/dist/config.js +150 -0
  84. package/dist/config.js.map +1 -0
  85. package/dist/debug-id.d.ts +176 -0
  86. package/dist/debug-id.js +307 -0
  87. package/dist/debug-id.js.map +1 -0
  88. package/dist/discover.d.ts +50 -0
  89. package/dist/discover.js +229 -0
  90. package/dist/discover.js.map +1 -0
  91. package/dist/env.d.ts +22 -0
  92. package/dist/env.js +50 -0
  93. package/dist/env.js.map +1 -0
  94. package/dist/glob.d.ts +34 -0
  95. package/dist/glob.js +109 -0
  96. package/dist/glob.js.map +1 -0
  97. package/dist/http.d.ts +68 -0
  98. package/dist/http.js +203 -0
  99. package/dist/http.js.map +1 -0
  100. package/dist/index.d.ts +17 -0
  101. package/dist/index.js +18 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/limits.d.ts +35 -0
  104. package/dist/limits.js +36 -0
  105. package/dist/limits.js.map +1 -0
  106. package/dist/multipart.d.ts +28 -0
  107. package/dist/multipart.js +41 -0
  108. package/dist/multipart.js.map +1 -0
  109. package/dist/upload.d.ts +178 -0
  110. package/dist/upload.js +383 -0
  111. package/dist/upload.js.map +1 -0
  112. package/dist/url.d.ts +25 -0
  113. package/dist/url.js +40 -0
  114. package/dist/url.js.map +1 -0
  115. package/dist/version.d.ts +10 -0
  116. package/dist/version.js +11 -0
  117. package/dist/version.js.map +1 -0
  118. package/dist/vite.d.ts +74 -0
  119. package/dist/vite.js +166 -0
  120. package/dist/vite.js.map +1 -0
  121. package/package.json +141 -0
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RETRIABLE_STATUS = void 0;
4
+ exports.userAgent = userAgent;
5
+ exports.send = send;
6
+ exports.proxyFor = proxyFor;
7
+ exports.retriable = retriable;
8
+ exports.backoffMs = backoffMs;
9
+ exports.timeoutFor = timeoutFor;
10
+ const node_http_1 = require("node:http");
11
+ const node_https_1 = require("node:https");
12
+ const node_net_1 = require("node:net");
13
+ const version_js_1 = require("./version.js");
14
+ /**
15
+ * `vinktar-cli/<version> (node/<version>)`, plus the plugin when a bundler is driving.
16
+ *
17
+ * The server logs it, and "which of these is the Vite plugin and which is a hand-run CI script" is
18
+ * the first question anyone asks of an ingest log.
19
+ */
20
+ function userAgent(plugin) {
21
+ const base = `vinktar-cli/${version_js_1.VERSION} (node/${process.versions.node})`;
22
+ return plugin === undefined || plugin === '' ? base : `${base} ${plugin}`;
23
+ }
24
+ async function send(options) {
25
+ const headers = {
26
+ 'User-Agent': userAgent(options.plugin),
27
+ ...(options.key === undefined || options.key === '' ? {} : { 'X-Vinktar-Key': options.key }),
28
+ ...options.headers,
29
+ };
30
+ const proxy = proxyFor(options.url, options.env ?? process.env);
31
+ if (proxy !== null)
32
+ return throughProxy(proxy, options, headers);
33
+ const response = await fetch(options.url, {
34
+ method: options.method,
35
+ headers,
36
+ ...(options.body === undefined ? {} : { body: options.body }),
37
+ signal: AbortSignal.timeout(options.timeoutMs),
38
+ });
39
+ const text = await response.text();
40
+ return { status: response.status, text, header: (name) => response.headers.get(name) };
41
+ }
42
+ /**
43
+ * The proxy that should carry a request to `url`, honouring `NO_PROXY`.
44
+ *
45
+ * Lower case wins over upper case, which is what curl does and therefore what people expect:
46
+ * `http_proxy` is the historical spelling and the one a shell profile sets.
47
+ */
48
+ function proxyFor(url, env = process.env) {
49
+ let target;
50
+ try {
51
+ target = new URL(url);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ if (bypassed(target.hostname, env['no_proxy'] ?? env['NO_PROXY'] ?? ''))
57
+ return null;
58
+ const names = target.protocol === 'https:' ? ['https_proxy', 'HTTPS_PROXY'] : ['http_proxy', 'HTTP_PROXY'];
59
+ const value = env[names[0]] ?? env[names[1]] ?? '';
60
+ if (value.trim() === '')
61
+ return null;
62
+ try {
63
+ return new URL(value.includes('://') ? value : `http://${value}`);
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ function bypassed(hostname, list) {
70
+ if (list.trim() === '*')
71
+ return true;
72
+ const host = hostname.toLowerCase();
73
+ return list
74
+ .split(',')
75
+ .map((entry) => entry.trim().replace(/^\./, '').toLowerCase())
76
+ .filter((entry) => entry !== '')
77
+ .some((entry) => host === entry || host.endsWith(`.${entry}`));
78
+ }
79
+ /**
80
+ * Send through a proxy.
81
+ *
82
+ * An https target needs a CONNECT tunnel, because the proxy must not be able to read the key or
83
+ * the source it is carrying. A plain http target is an absolute-URI request, which is the older
84
+ * and simpler form the same proxies still speak.
85
+ */
86
+ async function throughProxy(proxy, options, headers) {
87
+ const target = new URL(options.url);
88
+ const secure = target.protocol === 'https:';
89
+ const socket = secure ? await tunnel(proxy, target, options.timeoutMs) : null;
90
+ return new Promise((resolve, reject) => {
91
+ const perform = secure ? node_https_1.request : node_http_1.request;
92
+ const request = perform(secure
93
+ ? {
94
+ method: options.method,
95
+ host: target.hostname,
96
+ port: target.port === '' ? 443 : Number(target.port),
97
+ path: `${target.pathname}${target.search}`,
98
+ headers,
99
+ createConnection: () => socket,
100
+ timeout: options.timeoutMs,
101
+ }
102
+ : {
103
+ method: options.method,
104
+ host: proxy.hostname,
105
+ port: proxy.port === '' ? 80 : Number(proxy.port),
106
+ // Absolute URI: the proxy needs to know where this is going, since there is no tunnel.
107
+ path: options.url,
108
+ headers: { ...headers, Host: target.host, ...proxyAuth(proxy) },
109
+ timeout: options.timeoutMs,
110
+ }, (response) => {
111
+ const chunks = [];
112
+ response.on('data', (chunk) => chunks.push(chunk));
113
+ response.on('end', () => resolve({
114
+ status: response.statusCode ?? 0,
115
+ text: Buffer.concat(chunks).toString('utf8'),
116
+ header: (name) => {
117
+ const value = response.headers[name.toLowerCase()];
118
+ return typeof value === 'string' ? value : null;
119
+ },
120
+ }));
121
+ });
122
+ request.on('timeout', () => request.destroy(new Error(`timed out after ${options.timeoutMs} ms`)));
123
+ request.on('error', reject);
124
+ if (options.body !== undefined)
125
+ request.write(options.body);
126
+ request.end();
127
+ });
128
+ }
129
+ function proxyAuth(proxy) {
130
+ if (proxy.username === '')
131
+ return {};
132
+ const credentials = `${decodeURIComponent(proxy.username)}:${decodeURIComponent(proxy.password)}`;
133
+ return { 'Proxy-Authorization': `Basic ${Buffer.from(credentials).toString('base64')}` };
134
+ }
135
+ /** Open a CONNECT tunnel to the target, over which TLS is then negotiated end to end. */
136
+ async function tunnel(proxy, target, timeoutMs) {
137
+ return new Promise((resolve, reject) => {
138
+ const port = proxy.port === '' ? (proxy.protocol === 'https:' ? 443 : 80) : Number(proxy.port);
139
+ const host = `${target.hostname}:${target.port === '' ? 443 : target.port}`;
140
+ const auth = proxyAuth(proxy)['Proxy-Authorization'];
141
+ const client = (0, node_net_1.connect)({ host: proxy.hostname, port }, () => {
142
+ client.write(`CONNECT ${host} HTTP/1.1\r\nHost: ${host}\r\n` +
143
+ (auth === undefined ? '' : `Proxy-Authorization: ${auth}\r\n`) +
144
+ '\r\n');
145
+ });
146
+ client.setTimeout(timeoutMs, () => {
147
+ client.destroy();
148
+ reject(new Error(`the proxy at ${proxy.host} did not answer within ${timeoutMs} ms`));
149
+ });
150
+ client.once('error', reject);
151
+ client.once('data', (chunk) => {
152
+ const status = Number(/^HTTP\/1\.[01] (\d{3})/.exec(chunk.toString('latin1'))?.[1] ?? 0);
153
+ if (status === 200) {
154
+ client.setTimeout(0);
155
+ resolve(client);
156
+ return;
157
+ }
158
+ client.destroy();
159
+ reject(new Error(`the proxy at ${proxy.host} refused a tunnel to ${target.host} (HTTP ${status === 0 ? 'unknown' : status})`));
160
+ });
161
+ });
162
+ }
163
+ /** Exposed so the retry policy can be asserted rather than inferred from a stack trace. */
164
+ exports.RETRIABLE_STATUS = new Set([429, 502, 503, 504, 507, 524]);
165
+ const RETRIABLE_CODES = new Set([
166
+ 'ECONNRESET',
167
+ 'ECONNREFUSED',
168
+ 'ETIMEDOUT',
169
+ 'EPIPE',
170
+ 'UND_ERR_SOCKET',
171
+ 'UND_ERR_CONNECT_TIMEOUT',
172
+ ]);
173
+ /**
174
+ * Whether a failed request is worth repeating.
175
+ *
176
+ * `ENOTFOUND` is on the list only for the default host: a self-hosted URL that does not resolve is
177
+ * a typo, and retrying a typo three times with backoff turns a one-second error into a ten-second
178
+ * one and teaches nobody anything.
179
+ */
180
+ function retriable(error, isDefaultHost) {
181
+ const code = error?.cause?.code ?? error?.code ?? '';
182
+ if (code === 'ENOTFOUND' || code === 'EAI_AGAIN')
183
+ return isDefaultHost;
184
+ if (RETRIABLE_CODES.has(code))
185
+ return true;
186
+ return error instanceof Error && /terminated|fetch failed|timed out|socket hang up/i.test(error.message);
187
+ }
188
+ /**
189
+ * How long to wait before trying again.
190
+ *
191
+ * `Retry-After` is honoured when the server sends one — it is the only party that knows when its
192
+ * rate limit resets — and capped, because a proxy answering `Retry-After: 3600` must not hang a
193
+ * deploy for an hour. Otherwise exponential with jitter, so two builds that fail together do not
194
+ * retry together.
195
+ */
196
+ function backoffMs(attempt, retryAfter) {
197
+ const header = Number(retryAfter ?? 0);
198
+ if (Number.isFinite(header) && header > 0)
199
+ return Math.min(60_000, header * 1000);
200
+ return Math.min(10_000, 1_000 * 2 ** attempt) + Math.floor(Math.random() * 250);
201
+ }
202
+ /**
203
+ * The deadline for a request carrying `bytes`.
204
+ *
205
+ * One flat timeout punishes a large upload for being large: 30 s is generous for a handshake and
206
+ * mean for 40 MB over a hotel connection. This is the connect-and-settle allowance plus an
207
+ * assumption of a slow-but-real 1 Mbit/s.
208
+ */
209
+ function timeoutFor(bytes, base) {
210
+ return base + Math.ceil(bytes / 131_072) * 1000;
211
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Programmatic API, for build scripts that would rather call a function than shell out.
3
+ */
4
+ export { run, parse, VERSION } from './cli.js';
5
+ export { inject, audit, type InjectResult, type AuditResult, type AuditEntry } from './commands/inject.js';
6
+ export { upload, type UploadCommandOptions, type UploadSummary, type StoredMap } from './commands/upload.js';
7
+ export { doctor, type DoctorOptions } from './commands/doctor.js';
8
+ export { resolvePosition, type ResolveResult, type Position } from './commands/resolve.js';
9
+ export { resolve as resolveConfig, type Resolved, type Sources } from './config.js';
10
+ export { loadDotEnv, type DotEnv } from './env.js';
11
+ export { matches as globMatches } from './glob.js';
12
+ export { deriveDebugId, registrationSnippet, isDebugId, REGISTRY_GLOBAL, MARKER_GLOBAL, } from './debug-id.js';
13
+ export { discover, isEmptyMap, type Artifact, type Discovery } from './discover.js';
14
+ export { toUrl, normalise } from './url.js';
15
+ export { UploadError, rewrite, type UploadOptions, type UploadResult, type Limits } from './upload.js';
16
+ export { MAX_FILE_BYTES, MAX_NAME_BYTES, MAX_REQUEST_BYTES, RECOMMENDED_BATCH_BYTES, REQUIRED_SCOPE, DEFAULT_HOST, } from './limits.js';
17
+ export { detectRelease, disabled, environmentWarnings, session, finish, type BundlerOptions, type Session, } from './bundler/core.js';
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.finish = exports.session = exports.environmentWarnings = exports.disabled = exports.detectRelease = exports.DEFAULT_HOST = exports.REQUIRED_SCOPE = exports.RECOMMENDED_BATCH_BYTES = exports.MAX_REQUEST_BYTES = exports.MAX_NAME_BYTES = exports.MAX_FILE_BYTES = exports.rewrite = exports.UploadError = exports.normalise = exports.toUrl = exports.isEmptyMap = exports.discover = exports.MARKER_GLOBAL = exports.REGISTRY_GLOBAL = exports.isDebugId = exports.registrationSnippet = exports.deriveDebugId = exports.globMatches = exports.loadDotEnv = exports.resolveConfig = exports.resolvePosition = exports.doctor = exports.upload = exports.audit = exports.inject = exports.VERSION = exports.parse = exports.run = void 0;
4
+ /**
5
+ * Programmatic API, for build scripts that would rather call a function than shell out.
6
+ */
7
+ var cli_js_1 = require("./cli.js");
8
+ Object.defineProperty(exports, "run", { enumerable: true, get: function () { return cli_js_1.run; } });
9
+ Object.defineProperty(exports, "parse", { enumerable: true, get: function () { return cli_js_1.parse; } });
10
+ Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return cli_js_1.VERSION; } });
11
+ var inject_js_1 = require("./commands/inject.js");
12
+ Object.defineProperty(exports, "inject", { enumerable: true, get: function () { return inject_js_1.inject; } });
13
+ Object.defineProperty(exports, "audit", { enumerable: true, get: function () { return inject_js_1.audit; } });
14
+ var upload_js_1 = require("./commands/upload.js");
15
+ Object.defineProperty(exports, "upload", { enumerable: true, get: function () { return upload_js_1.upload; } });
16
+ var doctor_js_1 = require("./commands/doctor.js");
17
+ Object.defineProperty(exports, "doctor", { enumerable: true, get: function () { return doctor_js_1.doctor; } });
18
+ var resolve_js_1 = require("./commands/resolve.js");
19
+ Object.defineProperty(exports, "resolvePosition", { enumerable: true, get: function () { return resolve_js_1.resolvePosition; } });
20
+ var config_js_1 = require("./config.js");
21
+ Object.defineProperty(exports, "resolveConfig", { enumerable: true, get: function () { return config_js_1.resolve; } });
22
+ var env_js_1 = require("./env.js");
23
+ Object.defineProperty(exports, "loadDotEnv", { enumerable: true, get: function () { return env_js_1.loadDotEnv; } });
24
+ var glob_js_1 = require("./glob.js");
25
+ Object.defineProperty(exports, "globMatches", { enumerable: true, get: function () { return glob_js_1.matches; } });
26
+ var debug_id_js_1 = require("./debug-id.js");
27
+ Object.defineProperty(exports, "deriveDebugId", { enumerable: true, get: function () { return debug_id_js_1.deriveDebugId; } });
28
+ Object.defineProperty(exports, "registrationSnippet", { enumerable: true, get: function () { return debug_id_js_1.registrationSnippet; } });
29
+ Object.defineProperty(exports, "isDebugId", { enumerable: true, get: function () { return debug_id_js_1.isDebugId; } });
30
+ Object.defineProperty(exports, "REGISTRY_GLOBAL", { enumerable: true, get: function () { return debug_id_js_1.REGISTRY_GLOBAL; } });
31
+ Object.defineProperty(exports, "MARKER_GLOBAL", { enumerable: true, get: function () { return debug_id_js_1.MARKER_GLOBAL; } });
32
+ var discover_js_1 = require("./discover.js");
33
+ Object.defineProperty(exports, "discover", { enumerable: true, get: function () { return discover_js_1.discover; } });
34
+ Object.defineProperty(exports, "isEmptyMap", { enumerable: true, get: function () { return discover_js_1.isEmptyMap; } });
35
+ var url_js_1 = require("./url.js");
36
+ Object.defineProperty(exports, "toUrl", { enumerable: true, get: function () { return url_js_1.toUrl; } });
37
+ Object.defineProperty(exports, "normalise", { enumerable: true, get: function () { return url_js_1.normalise; } });
38
+ var upload_js_2 = require("./upload.js");
39
+ Object.defineProperty(exports, "UploadError", { enumerable: true, get: function () { return upload_js_2.UploadError; } });
40
+ Object.defineProperty(exports, "rewrite", { enumerable: true, get: function () { return upload_js_2.rewrite; } });
41
+ var limits_js_1 = require("./limits.js");
42
+ Object.defineProperty(exports, "MAX_FILE_BYTES", { enumerable: true, get: function () { return limits_js_1.MAX_FILE_BYTES; } });
43
+ Object.defineProperty(exports, "MAX_NAME_BYTES", { enumerable: true, get: function () { return limits_js_1.MAX_NAME_BYTES; } });
44
+ Object.defineProperty(exports, "MAX_REQUEST_BYTES", { enumerable: true, get: function () { return limits_js_1.MAX_REQUEST_BYTES; } });
45
+ Object.defineProperty(exports, "RECOMMENDED_BATCH_BYTES", { enumerable: true, get: function () { return limits_js_1.RECOMMENDED_BATCH_BYTES; } });
46
+ Object.defineProperty(exports, "REQUIRED_SCOPE", { enumerable: true, get: function () { return limits_js_1.REQUIRED_SCOPE; } });
47
+ Object.defineProperty(exports, "DEFAULT_HOST", { enumerable: true, get: function () { return limits_js_1.DEFAULT_HOST; } });
48
+ var core_js_1 = require("./bundler/core.js");
49
+ Object.defineProperty(exports, "detectRelease", { enumerable: true, get: function () { return core_js_1.detectRelease; } });
50
+ Object.defineProperty(exports, "disabled", { enumerable: true, get: function () { return core_js_1.disabled; } });
51
+ Object.defineProperty(exports, "environmentWarnings", { enumerable: true, get: function () { return core_js_1.environmentWarnings; } });
52
+ Object.defineProperty(exports, "session", { enumerable: true, get: function () { return core_js_1.session; } });
53
+ Object.defineProperty(exports, "finish", { enumerable: true, get: function () { return core_js_1.finish; } });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The limits the ingest API enforces.
3
+ *
4
+ * Fallbacks, not the truth: `POST /v1/sourcemaps/check` publishes what a deployment actually
5
+ * accepts, and the client prefers that (see {@link ../upload.js}). These are what it uses before
6
+ * it has asked, and against an older ingest that does not publish them — so a 60 MB upload fails
7
+ * in a second with a clear message rather than after the bytes have crossed the wire.
8
+ */
9
+ /** Per map. Over this the server answers 413 file_too_large. */
10
+ export declare const MAX_FILE_BYTES = 20971520;
11
+ /** Whole multipart request. */
12
+ export declare const MAX_REQUEST_BYTES = 62914560;
13
+ /**
14
+ * What one request should carry when the server is configured as the protocol says.
15
+ *
16
+ * The server publishes its own figure as `maxBatchBytes` at `POST /v1/sourcemaps/check`, computed
17
+ * from its actual `post_max_size`; this is the same number the protocol recommends, mirrored so a
18
+ * test can assert the two have not drifted.
19
+ */
20
+ export declare const RECOMMENDED_BATCH_BYTES = 16777216;
21
+ /**
22
+ * What one request carries when the server says nothing.
23
+ *
24
+ * An ingest that does not publish `limits` is one deployed before they existed, and a deployment
25
+ * that old is one whose `post_max_size` is very likely PHP's stock 8M. A POST over that is not
26
+ * rejected — the body is silently discarded and the server answers `missing_release`, which reads
27
+ * like a CLI bug and cost an afternoon to diagnose the first time. Batching under the ini default
28
+ * works everywhere; the extra requests cost seconds.
29
+ */
30
+ export declare const CONSERVATIVE_BATCH_BYTES = 6000000;
31
+ /** The longest a release or dist name may be. Over it the server answers `invalid_release`. */
32
+ export declare const MAX_NAME_BYTES = 64;
33
+ /** Source-map upload needs `cli` scope, NOT `write`. A write key alone gets a 403. */
34
+ export declare const REQUIRED_SCOPE = "cli";
35
+ export declare const DEFAULT_HOST = "https://in.vinktar.com";
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /**
3
+ * The limits the ingest API enforces.
4
+ *
5
+ * Fallbacks, not the truth: `POST /v1/sourcemaps/check` publishes what a deployment actually
6
+ * accepts, and the client prefers that (see {@link ../upload.js}). These are what it uses before
7
+ * it has asked, and against an older ingest that does not publish them — so a 60 MB upload fails
8
+ * in a second with a clear message rather than after the bytes have crossed the wire.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.DEFAULT_HOST = exports.REQUIRED_SCOPE = exports.MAX_NAME_BYTES = exports.CONSERVATIVE_BATCH_BYTES = exports.RECOMMENDED_BATCH_BYTES = exports.MAX_REQUEST_BYTES = exports.MAX_FILE_BYTES = void 0;
12
+ /** Per map. Over this the server answers 413 file_too_large. */
13
+ exports.MAX_FILE_BYTES = 20_971_520;
14
+ /** Whole multipart request. */
15
+ exports.MAX_REQUEST_BYTES = 62_914_560;
16
+ /**
17
+ * What one request should carry when the server is configured as the protocol says.
18
+ *
19
+ * The server publishes its own figure as `maxBatchBytes` at `POST /v1/sourcemaps/check`, computed
20
+ * from its actual `post_max_size`; this is the same number the protocol recommends, mirrored so a
21
+ * test can assert the two have not drifted.
22
+ */
23
+ exports.RECOMMENDED_BATCH_BYTES = 16_777_216;
24
+ /**
25
+ * What one request carries when the server says nothing.
26
+ *
27
+ * An ingest that does not publish `limits` is one deployed before they existed, and a deployment
28
+ * that old is one whose `post_max_size` is very likely PHP's stock 8M. A POST over that is not
29
+ * rejected — the body is silently discarded and the server answers `missing_release`, which reads
30
+ * like a CLI bug and cost an afternoon to diagnose the first time. Batching under the ini default
31
+ * works everywhere; the extra requests cost seconds.
32
+ */
33
+ exports.CONSERVATIVE_BATCH_BYTES = 6_000_000;
34
+ /** The longest a release or dist name may be. Over it the server answers `invalid_release`. */
35
+ exports.MAX_NAME_BYTES = 64;
36
+ /** Source-map upload needs `cli` scope, NOT `write`. A write key alone gets a 403. */
37
+ exports.REQUIRED_SCOPE = 'cli';
38
+ exports.DEFAULT_HOST = 'https://in.vinktar.com';
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Building the upload body by hand instead of with `FormData`.
3
+ *
4
+ * Three reasons, in order of how much each one costs when it is missing:
5
+ *
6
+ * 1. **A proxy.** Node's `fetch` ignores `HTTPS_PROXY`, so a request through one has to go out
7
+ * over `node:https`, which needs bytes rather than a `FormData` object.
8
+ * 2. **Per-part compression.** `Content-Encoding` does not survive multipart parsing — PHP hands
9
+ * the handler the raw part and nothing else — so a compressed part is just a part whose bytes
10
+ * happen to be gzip, detected by its magic number. That means putting exact bytes in a part.
11
+ * 3. **Knowing the size before sending.** The ceiling that matters is the encoded request, not the
12
+ * sum of the files, and the framing is not free: a hundred parts is several kilobytes of
13
+ * boundaries and headers.
14
+ */
15
+ export interface Part {
16
+ readonly name: string;
17
+ readonly value: string | Buffer;
18
+ /** Present for a file part, absent for a plain field. */
19
+ readonly filename?: string;
20
+ readonly contentType?: string;
21
+ }
22
+ export interface Multipart {
23
+ readonly body: Buffer;
24
+ readonly contentType: string;
25
+ }
26
+ export declare function multipart(parts: readonly Part[]): Multipart;
27
+ /** What one part costs on the wire beyond its own bytes, for planning a batch. */
28
+ export declare function overheadFor(part: Part): number;
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.multipart = multipart;
4
+ exports.overheadFor = overheadFor;
5
+ const node_crypto_1 = require("node:crypto");
6
+ function multipart(parts) {
7
+ // Random rather than derived from the content: a boundary that appears inside a file would
8
+ // truncate the request, and the odds of guessing 16 random bytes are not worth defending against
9
+ // any other way.
10
+ const boundary = `----vinktar${(0, node_crypto_1.randomBytes)(16).toString('hex')}`;
11
+ const chunks = [];
12
+ for (const part of parts) {
13
+ const disposition = part.filename === undefined
14
+ ? `form-data; name="${escape(part.name)}"`
15
+ : `form-data; name="${escape(part.name)}"; filename="${escape(part.filename)}"`;
16
+ chunks.push(Buffer.from(`--${boundary}\r\nContent-Disposition: ${disposition}\r\n` +
17
+ (part.contentType === undefined ? '' : `Content-Type: ${part.contentType}\r\n`) +
18
+ '\r\n', 'utf8'));
19
+ chunks.push(typeof part.value === 'string' ? Buffer.from(part.value, 'utf8') : part.value);
20
+ chunks.push(Buffer.from('\r\n', 'utf8'));
21
+ }
22
+ chunks.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
23
+ return {
24
+ body: Buffer.concat(chunks),
25
+ contentType: `multipart/form-data; boundary=${boundary}`,
26
+ };
27
+ }
28
+ /**
29
+ * Quote what a header field cannot carry raw.
30
+ *
31
+ * Names here are ours (`files[]`, `urls[]`) and filenames come from a build directory, so this is
32
+ * belt and braces — but a filename with a quote in it would otherwise end the field early and
33
+ * mis-pair every part after it, which is the one failure in this format that is invisible.
34
+ */
35
+ function escape(value) {
36
+ return value.replace(/[\r\n"]/g, (match) => (match === '"' ? '%22' : ''));
37
+ }
38
+ /** What one part costs on the wire beyond its own bytes, for planning a batch. */
39
+ function overheadFor(part) {
40
+ return (100 +
41
+ Buffer.byteLength(part.name) +
42
+ Buffer.byteLength(part.filename ?? '') +
43
+ Buffer.byteLength(part.contentType ?? ''));
44
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,178 @@
1
+ import { type Artifact } from './discover.js';
2
+ /**
3
+ * Uploads source maps to `POST /v1/sourcemaps`.
4
+ *
5
+ * `files[]`, `urls[]` and `debug_ids[]` are **positional parallel arrays**: the server pairs them
6
+ * by index, so appending to one without the others silently mis-pairs every entry after it. They
7
+ * are therefore built in one loop and never separately.
8
+ */
9
+ export interface UploadOptions {
10
+ readonly host: string;
11
+ readonly key: string;
12
+ readonly release: string;
13
+ readonly dist?: string;
14
+ readonly urlPrefix: string;
15
+ /** Per request, before the size allowance. Default 30 s. */
16
+ timeoutMs?: number;
17
+ /** Requests in flight. Default 4, or whatever the server asks for. */
18
+ concurrency?: number;
19
+ /** Attempts per request, including the first. Default 3. */
20
+ maxRetries?: number;
21
+ /** Extra headers, for a gateway that wants one. Never allowed to displace the key. */
22
+ headers?: Readonly<Record<string, string>>;
23
+ /** Appended to the User-Agent, so an ingest log can tell a plugin from a CI script. */
24
+ plugin?: string;
25
+ /**
26
+ * Rewrite `sources` in the uploaded copy. Default true — see {@link rewrite}.
27
+ * The file on disk is never touched.
28
+ */
29
+ rewriteSources?: boolean;
30
+ /** The build root, which absolute `sources` are made relative to. */
31
+ root?: string;
32
+ env?: Record<string, string | undefined>;
33
+ }
34
+ /** What this deployment will actually accept, as it reports it. */
35
+ export interface Limits {
36
+ readonly maxFileBytes: number;
37
+ readonly maxPartBytes: number;
38
+ readonly maxRequestBytes: number;
39
+ readonly recommendedBatchBytes: number;
40
+ readonly concurrency: number;
41
+ readonly compression: readonly string[];
42
+ }
43
+ export interface UploadEntry {
44
+ readonly name: string;
45
+ readonly url: string;
46
+ readonly debugId: string;
47
+ readonly bytes: number;
48
+ /** Content hash of the exact body that would be uploaded. The server's dedupe key. */
49
+ readonly sha256: string;
50
+ /**
51
+ * Content hash of the map FILE as it sits on disk, which is a different string: the uploaded
52
+ * body carries the debug id and, when a line was inserted, a shifted `mappings`.
53
+ *
54
+ * Only the plugins use it, and only to answer one question before deleting a map: are these
55
+ * still the bytes that were uploaded? Turbopack rewrites files while later stages run, and a
56
+ * map deleted there is one the server never received.
57
+ */
58
+ readonly mapSha256: string;
59
+ /**
60
+ * Whether the chunk itself carries the id. When false the id was derived here for the map only,
61
+ * and no frame will ever report it: the map can then match by release + url alone.
62
+ */
63
+ readonly injected: boolean;
64
+ }
65
+ export interface UploadResult {
66
+ readonly stored: number;
67
+ readonly artifacts: ReadonlyArray<{
68
+ file_url?: string;
69
+ debug_id?: string;
70
+ size?: number;
71
+ }>;
72
+ }
73
+ export declare class UploadError extends Error {
74
+ readonly status: number;
75
+ readonly code: string;
76
+ readonly hint?: string | undefined;
77
+ constructor(message: string, status: number, code: string, hint?: string | undefined);
78
+ }
79
+ /**
80
+ * Index one artifact: what it is, what it hashes to, and how big it will be on the wire.
81
+ *
82
+ * The body is built and then DISCARDED. Holding it would mean the whole build sitting in memory
83
+ * before the first request left — a few hundred chunks of a real application is hundreds of
84
+ * megabytes — and it is cheap to rebuild from a local file at the moment it is actually sent.
85
+ *
86
+ * Returns null when there is nothing to upload, rather than throwing: a directory of chunks where
87
+ * only some have maps is normal, not an error.
88
+ */
89
+ export declare function index(artifact: Artifact, urlPrefix: string, options?: Pick<UploadOptions, 'rewriteSources' | 'root'>): Promise<{
90
+ entry: UploadEntry;
91
+ oversized: boolean;
92
+ empty: boolean;
93
+ } | null>;
94
+ /**
95
+ * The exact bytes that go on the wire for this artifact.
96
+ *
97
+ * Deterministic, which is what lets `index` throw the body away and this rebuild it later: the id
98
+ * already injected into the chunk wins, so a second call cannot produce a different map than the
99
+ * one that was hashed.
100
+ */
101
+ export declare function materialise(artifact: Artifact, options?: Pick<UploadOptions, 'rewriteSources' | 'root'>): Promise<string>;
102
+ /**
103
+ * Tidy `sources` in the uploaded copy, never on disk.
104
+ *
105
+ * Two things come out of a bundler that should not reach a server. `webpack:///./src/App.tsx` is
106
+ * a protocol nothing can open, and it is what the UI would print beside every frame. And an
107
+ * absolute path is the build machine's directory layout — `/home/runner/work/acme/acme/src/…`, or
108
+ * worse, someone's home directory — which is both noise and information the ingest never asked
109
+ * for.
110
+ *
111
+ * The uploaded copy only. Rewriting the file would change bytes another tool may already have
112
+ * hashed, and the map on disk is the one a developer opens locally, where an absolute path is
113
+ * exactly what they want.
114
+ */
115
+ export declare function rewrite(mapJson: string, root?: string): string;
116
+ export interface Preflight {
117
+ /** sha256 of the bodies the server already holds. */
118
+ readonly stored: Set<string>;
119
+ /** What this deployment accepts, when it says. Null on an older ingest, or an unreachable one. */
120
+ readonly limits: Limits | null;
121
+ }
122
+ /**
123
+ * Ask the server which of these bodies it already holds, and what it will accept.
124
+ *
125
+ * The dedupe half is purely an optimisation, so every failure mode answers "none": an older ingest
126
+ * with no such route, a proxy in the way, a network blip. Uploading something the server already
127
+ * has is a wasted request; NOT uploading something it does not have is a build with no
128
+ * symbolication, and those are not the same mistake.
129
+ *
130
+ * The limits half matters more than it looks. The protocol says 20 MiB per file and 60 MiB per
131
+ * request, but what a deployment actually accepts is the smaller of that and its PHP
132
+ * `upload_max_filesize` / `post_max_size` — and a POST over `post_max_size` is not rejected, the
133
+ * body is silently dropped and the server answers `missing_release`, which reads like a CLI bug.
134
+ * Asking is the difference between a clear message and an afternoon.
135
+ */
136
+ export declare function preflight(options: UploadOptions, hashes: readonly string[]): Promise<Preflight>;
137
+ /** Four at a time: enough to hide the latency, few enough not to trip the ingest rate limit. */
138
+ export declare const DEFAULT_CONCURRENCY = 4;
139
+ /**
140
+ * Send one batch.
141
+ *
142
+ * Batches are bounded by the request ceiling rather than by count: maps vary from kilobytes to
143
+ * megabytes, so a fixed count would either waste requests or overshoot.
144
+ *
145
+ * Each part is gzipped when the server says it accepts that. `Content-Encoding` does NOT survive
146
+ * multipart parsing — PHP hands the handler the raw part and nothing else — so a compressed part
147
+ * is simply a part whose bytes are gzip, which the server detects by its magic number. Source maps
148
+ * are JSON with long runs of repeated source text and compress by 80–90%, which is the difference
149
+ * between one request and six.
150
+ */
151
+ export declare function send(options: UploadOptions, batch: ReadonlyArray<{
152
+ entry: UploadEntry;
153
+ body: string;
154
+ }>, limits?: Limits | null): Promise<UploadResult>;
155
+ /**
156
+ * Group indexed maps into requests that fit.
157
+ *
158
+ * The budget is approached with headroom, because multipart framing and the field names add bytes
159
+ * the file sizes do not account for, and an oversized request costs the whole batch. Sized on the
160
+ * UNCOMPRESSED bodies even when the parts will be gzipped: compression only ever makes the request
161
+ * smaller, so the bound stays true and one batch's worth of maps is still what sits in memory.
162
+ *
163
+ * Generic over whatever the caller is carrying alongside the entry, so a batch can hold the
164
+ * artifact each map came from and the body can be read back from disk as its batch is sent —
165
+ * nothing here pins the build in memory.
166
+ */
167
+ export declare function batch<T extends {
168
+ readonly entry: UploadEntry;
169
+ }>(items: readonly T[], limits?: Limits | null): T[][];
170
+ /**
171
+ * Run `work` over `items` with a bounded number in flight.
172
+ *
173
+ * Source maps are large and the server is usually far away, so a build of two hundred chunks spent
174
+ * most of its upload waiting on a socket. Bounded rather than unbounded: an unbounded fan-out of
175
+ * 6 MB requests is how a deploy step gets itself rate-limited.
176
+ */
177
+ export declare function pooled<T, R>(items: readonly T[], limit: number, work: (item: T, index: number) => Promise<R>): Promise<R[]>;
178
+ export declare function mb(bytes: number): string;