@autter/runtime-cli 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -21,26 +21,33 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
23
  BUNDLE_RE: () => BUNDLE_RE,
24
+ CLIENT_KEY_PREFIX: () => CLIENT_KEY_PREFIX,
24
25
  DEBUG_ID_RE: () => DEBUG_ID_RE,
25
26
  DEBUG_ID_REGISTRY: () => DEBUG_ID_REGISTRY,
26
27
  DEFAULT_ENDPOINT: () => DEFAULT_ENDPOINT,
27
28
  FatalUploadError: () => FatalUploadError,
29
+ KEY_ADVICE: () => KEY_ADVICE,
28
30
  MAX_MAP_CHARS: () => MAX_MAP_CHARS,
29
31
  MAX_UPLOAD_BYTES: () => MAX_UPLOAD_BYTES,
30
32
  RELEASE_ENV_VARS: () => RELEASE_ENV_VARS,
31
33
  RELEASE_RE: () => RELEASE_RE,
32
34
  SCOPES_KEY: () => SCOPES_KEY,
33
35
  SCOPES_VERSION: () => SCOPES_VERSION,
36
+ SERVER_KEY_PREFIX: () => SERVER_KEY_PREFIX,
34
37
  SNIPPET_LINE_KEY: () => SNIPPET_LINE_KEY,
38
+ UPLOAD_KEY_PREFIX: () => UPLOAD_KEY_PREFIX,
35
39
  computeScopes: () => computeScopes,
36
40
  debugIdFor: () => debugIdFor,
37
41
  debugIdSnippet: () => debugIdSnippet,
38
42
  findBundles: () => findBundles,
39
43
  formatUploadSummary: () => formatUploadSummary,
44
+ formatVerify: () => formatVerify,
40
45
  gitHead: () => gitHead,
46
+ globToRegExp: () => globToRegExp,
41
47
  hasDebugIdSnippet: () => hasDebugIdSnippet,
42
48
  injectBundle: () => injectBundle,
43
49
  injectDir: () => injectDir,
50
+ isInAppSource: () => isInAppSource,
44
51
  mapPathFor: () => mapPathFor,
45
52
  normalizeDebugId: () => normalizeDebugId,
46
53
  processBuildOutput: () => processBuildOutput,
@@ -49,11 +56,14 @@ __export(src_exports, {
49
56
  resolveEndpoint: () => resolveEndpoint,
50
57
  resolveRelease: () => resolveRelease,
51
58
  servedFilename: () => servedFilename,
59
+ serverUrlPrefix: () => serverUrlPrefix,
52
60
  shiftMappings: () => shiftMappings,
53
61
  snippetLine: () => snippetLine,
54
62
  storedMapJson: () => storedMapJson,
63
+ storedSourcesContent: () => storedSourcesContent,
55
64
  stripSourceMappingUrl: () => stripSourceMappingUrl,
56
- uploadDir: () => uploadDir
65
+ uploadDir: () => uploadDir,
66
+ verifyDir: () => verifyDir
57
67
  });
58
68
  module.exports = __toCommonJS(src_exports);
59
69
 
@@ -723,6 +733,10 @@ var DEFAULT_ENDPOINT = "https://otlp.autter.dev";
723
733
  var MAX_MAP_CHARS = 32 * 1024 * 1024;
724
734
  var MAX_UPLOAD_BYTES = MAX_MAP_CHARS + 1024 * 1024;
725
735
  var LEGACY_MAX_BYTES = 5 * 1024 * 1024;
736
+ var UPLOAD_KEY_PREFIX = "autter_rtu_";
737
+ var SERVER_KEY_PREFIX = "autter_rt_";
738
+ var CLIENT_KEY_PREFIX = "autter_rtc_";
739
+ var KEY_ADVICE = "set AUTTER_RUNTIME_KEY to an upload-only key (autter_rtu_\u2026, recommended for CI: it can only upload source maps) or the full server key (autter_rt_\u2026, also works)";
726
740
  var gzip = (0, import_node_util.promisify)(import_node_zlib.gzip);
727
741
  var CHECK_BATCH = 500;
728
742
  var MAX_ATTEMPTS = 3;
@@ -736,9 +750,56 @@ function servedFilename(urlPrefix, path) {
736
750
  if (!/^[a-z][\w+.-]*:\/\//i.test(prefix) && !prefix.startsWith("/")) prefix = `/${prefix}`;
737
751
  return `${prefix.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
738
752
  }
739
- function storedMapJson(map) {
740
- const { sourcesContent: _omitted, ...safe } = map;
741
- return JSON.stringify(safe);
753
+ var NOT_IN_APP_PATH_PARTS = ["node_modules/", "/site-packages/", "/dist-packages/", "/vendor/", "/.pnpm/"];
754
+ var NOT_IN_APP_PATH_PREFIXES = ["node:", "internal/"];
755
+ var EXTENSION_SCHEMES = ["chrome-extension:", "moz-extension:", "safari-extension:", "safari-web-extension:", "webkit-masked-url:"];
756
+ function isInAppSource(path) {
757
+ if (!path) return true;
758
+ const normalized = path.replace(/\\/g, "/");
759
+ if (NOT_IN_APP_PATH_PARTS.some((part) => normalized.includes(part))) return false;
760
+ if (NOT_IN_APP_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix))) return false;
761
+ const lower = normalized.toLowerCase();
762
+ return !EXTENSION_SCHEMES.some((scheme) => lower.startsWith(scheme));
763
+ }
764
+ var SOURCES_CONTENT_WRAPPER_BYTES = Buffer.byteLength(',"sourcesContent":[]');
765
+ var NULL_ENTRY_BYTES = 4;
766
+ function storedSourcesContent(fields, baseBytes, limit = MAX_MAP_CHARS) {
767
+ const { sources, sourcesContent } = fields;
768
+ if (!Array.isArray(sources) || !Array.isArray(sourcesContent) || sources.length === 0) return null;
769
+ const ignored = /* @__PURE__ */ new Set();
770
+ for (const list of [fields.ignoreList, fields.x_google_ignoreList]) {
771
+ if (!Array.isArray(list)) continue;
772
+ for (const index of list) if (Number.isInteger(index)) ignored.add(index);
773
+ }
774
+ const count = sources.length;
775
+ const candidates = [];
776
+ for (let index = 0; index < count; index++) {
777
+ const source = sources[index];
778
+ const content = sourcesContent[index];
779
+ if (typeof source !== "string" || typeof content !== "string") continue;
780
+ if (ignored.has(index) || !isInAppSource(source)) continue;
781
+ candidates.push({ index, content, bytes: Buffer.byteLength(JSON.stringify(content)) });
782
+ }
783
+ if (candidates.length === 0) return null;
784
+ candidates.sort((a, b) => a.bytes - b.bytes || a.index - b.index);
785
+ let remaining = limit - baseBytes - SOURCES_CONTENT_WRAPPER_BYTES - NULL_ENTRY_BYTES * count - (count - 1);
786
+ const kept = new Array(count).fill(null);
787
+ let any = false;
788
+ for (const candidate of candidates) {
789
+ const extra = candidate.bytes - NULL_ENTRY_BYTES;
790
+ if (extra > remaining) break;
791
+ remaining -= extra;
792
+ kept[candidate.index] = candidate.content;
793
+ any = true;
794
+ }
795
+ return any ? kept : null;
796
+ }
797
+ function storedMapJson(map, options = {}) {
798
+ const { sourcesContent: _content, ...rest } = map;
799
+ const base = JSON.stringify(rest);
800
+ if (options.sourcesContent === false) return base;
801
+ const content = storedSourcesContent(map, Buffer.byteLength(base));
802
+ return content ? JSON.stringify({ ...rest, sourcesContent: content }) : base;
742
803
  }
743
804
  function bytesOf(buffer) {
744
805
  return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
@@ -749,7 +810,7 @@ function fits(json, body) {
749
810
  function uploadBody(release, filename, debugId, json) {
750
811
  return `{"release":${JSON.stringify(release)},"filename":${JSON.stringify(filename)},${debugId ? `"debugId":${JSON.stringify(debugId)},` : ""}"map":${json}}`;
751
812
  }
752
- async function prepare(bundle, urlPrefix, release, log, compress) {
813
+ async function prepare(bundle, urlPrefix, release, log, compress, sourcesContent) {
753
814
  let map;
754
815
  try {
755
816
  map = JSON.parse(await (0, import_promises3.readFile)(bundle.map, "utf8"));
@@ -764,9 +825,10 @@ async function prepare(bundle, urlPrefix, release, log, compress) {
764
825
  const filename = servedFilename(urlPrefix, bundle.path);
765
826
  const { [SCOPES_KEY]: _stale, ...plain } = map;
766
827
  const scopes = computeScopes(code, map);
767
- let json = storedMapJson(plain);
828
+ const stored = { sourcesContent };
829
+ let json = storedMapJson(plain, stored);
768
830
  if ("scopes" in scopes) {
769
- const scoped = storedMapJson({ ...plain, [SCOPES_KEY]: scopes.scopes });
831
+ const scoped = storedMapJson({ ...plain, [SCOPES_KEY]: scopes.scopes }, stored);
770
832
  if (fits(scoped, uploadBody(release, filename, debugId, scoped))) json = scoped;
771
833
  else if (fits(json, uploadBody(release, filename, debugId, json))) {
772
834
  log(`warning: ${bundle.path}: its map is over 32 MB with function scopes, so it is uploaded without them (frames keep call-site names)`);
@@ -791,7 +853,7 @@ async function prepare(bundle, urlPrefix, release, log, compress) {
791
853
  var FatalUploadError = class extends Error {
792
854
  };
793
855
  function sleep(ms) {
794
- return new Promise((resolve2) => setTimeout(resolve2, ms));
856
+ return new Promise((resolve3) => setTimeout(resolve3, ms));
795
857
  }
796
858
  async function uploadDir(dir, options = {}) {
797
859
  const env = options.env ?? process.env;
@@ -813,7 +875,7 @@ async function uploadDir(dir, options = {}) {
813
875
  };
814
876
  const items = [];
815
877
  for (const bundle of await findBundles(dir, options.files)) {
816
- const prepared = await prepare(bundle, options.urlPrefix, release, log, !options.dryRun);
878
+ const prepared = await prepare(bundle, options.urlPrefix, release, log, !options.dryRun, options.sourcesContent !== false);
817
879
  if ("error" in prepared) {
818
880
  summary.failed.push({ path: bundle.path, filename: servedFilename(options.urlPrefix, bundle.path), error: prepared.error });
819
881
  } else if (prepared.mapChars > MAX_MAP_CHARS || prepared.bytes > MAX_UPLOAD_BYTES) {
@@ -831,10 +893,13 @@ async function uploadDir(dir, options = {}) {
831
893
  return summary;
832
894
  }
833
895
  const key = (options.key ?? env.AUTTER_RUNTIME_KEY ?? "").trim();
834
- if (!key) throw new FatalUploadError("no ingest key: set AUTTER_RUNTIME_KEY to a server key (autter_rt_\u2026)");
835
- if (key.startsWith("autter_rtc_")) {
836
- throw new FatalUploadError("AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026); uploads need a server key (autter_rt_\u2026)");
896
+ if (!key) throw new FatalUploadError(`no ingest key: ${KEY_ADVICE}`);
897
+ if (key.startsWith(CLIENT_KEY_PREFIX)) {
898
+ throw new FatalUploadError(`AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026), which cannot upload source maps: ${KEY_ADVICE}`);
837
899
  }
900
+ const rejected = (status, detail) => new FatalUploadError(
901
+ `the ingester rejected the key (${status}): ${detail}` + (key.startsWith(UPLOAD_KEY_PREFIX) ? " (upload keys, autter_rtu_\u2026, need ingester 1.7.0 or later; the server key works with any)" : "")
902
+ );
838
903
  const headers = { "content-type": "application/json", authorization: `Bearer ${key}` };
839
904
  const uploadHeaders = { ...headers, "content-encoding": "gzip" };
840
905
  const skipped = /* @__PURE__ */ new Set();
@@ -856,9 +921,7 @@ async function uploadDir(dir, options = {}) {
856
921
  );
857
922
  break;
858
923
  }
859
- if (res.status === 401 || res.status === 403) {
860
- throw new FatalUploadError(`the ingester rejected the key (${res.status}): ${(await res.text()).slice(0, 300)}`);
861
- }
924
+ if (res.status === 401 || res.status === 403) throw rejected(res.status, (await res.text()).slice(0, 300));
862
925
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
863
926
  const body = await res.json();
864
927
  const present = new Set(Array.isArray(body.present) ? body.present : []);
@@ -889,7 +952,7 @@ async function uploadDir(dir, options = {}) {
889
952
  detail = err.message;
890
953
  }
891
954
  if (status === 401 || status === 403) {
892
- fatal = new FatalUploadError(`the ingester rejected the key (${status}): ${detail}`);
955
+ fatal = rejected(status, detail);
893
956
  return;
894
957
  }
895
958
  const retryable = status === 0 || status === 429 || status >= 500;
@@ -930,9 +993,261 @@ function strip(item) {
930
993
  return { path, filename, debugId, hash, bytes };
931
994
  }
932
995
 
933
- // src/build.ts
996
+ // src/verify.ts
934
997
  var import_node_fs2 = require("fs");
998
+ var import_promises4 = require("fs/promises");
935
999
  var import_node_path2 = require("path");
1000
+ var CHECK_BATCH2 = 500;
1001
+ var SNIPPET_ID_RE = /\(g\._autterDebugIds=g\._autterDebugIds\|\|\{\}\)\[s\]="([0-9a-f-]{36})"/;
1002
+ var NOT_BUNDLES = [
1003
+ [/(^|\/)_(build|ssg|middleware|clientMiddleware)Manifest\.js$/, "Next.js build manifest"],
1004
+ // .next/server: middleware-build-manifest.js, next-font-manifest.js,
1005
+ // server-reference-manifest.js, page_client-reference-manifest.js, …
1006
+ [/(^|\/)[\w.-]*[-_]manifest\.js$/, "Next.js server manifest"],
1007
+ [/(^|\/)chunks\/polyfills-[\w-]+\.js$/, "Next.js polyfills, copied prebuilt without a source map"],
1008
+ [/(^|\/)(sw|service-worker|serviceWorker|firebase-messaging-sw|mockServiceWorker)\.js$/, "service worker (the browser SDK doesn't run there)"],
1009
+ [/(^|\/)workbox-[\w-]+\.js$/, "service worker runtime (the browser SDK doesn't run there)"]
1010
+ ];
1011
+ function globToRegExp(glob) {
1012
+ let re = "";
1013
+ for (let i = 0; i < glob.length; i++) {
1014
+ const ch = glob[i];
1015
+ if (ch === "*" && glob[i + 1] === "*") {
1016
+ re += glob[i + 2] === "/" ? "(?:.*/)?" : ".*";
1017
+ i += glob[i + 2] === "/" ? 2 : 1;
1018
+ } else if (ch === "*") re += "[^/]*";
1019
+ else if (ch === "?") re += "[^/]";
1020
+ else re += ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1021
+ }
1022
+ return new RegExp(glob.includes("/") ? `^${re.replace(/^\.\//, "")}$` : `(^|/)${re}$`);
1023
+ }
1024
+ async function walk2(dir, out) {
1025
+ let entries;
1026
+ try {
1027
+ entries = await (0, import_promises4.readdir)(dir, { withFileTypes: true });
1028
+ } catch {
1029
+ return;
1030
+ }
1031
+ for (const entry of entries) {
1032
+ if (entry.name === "node_modules") continue;
1033
+ const full = (0, import_node_path2.join)(dir, entry.name);
1034
+ if (entry.isDirectory()) await walk2(full, out);
1035
+ else if (entry.isFile()) out.push(full);
1036
+ }
1037
+ }
1038
+ function staticRoots(dir) {
1039
+ for (let current = (0, import_node_path2.dirname)(dir); (0, import_node_path2.dirname)(current) !== current; current = (0, import_node_path2.dirname)(current)) {
1040
+ if ((0, import_node_fs2.existsSync)((0, import_node_path2.join)(current, "package.json"))) return ["public", "static", "src"].map((name) => (0, import_node_path2.join)(current, name));
1041
+ }
1042
+ return [];
1043
+ }
1044
+ async function verifyDir(dir, options = {}) {
1045
+ const root = (0, import_node_path2.resolve)(dir);
1046
+ const env = options.env ?? process.env;
1047
+ const files = [];
1048
+ await walk2(root, files);
1049
+ files.sort();
1050
+ const relPath = (path) => (0, import_node_path2.relative)(root, path).split(import_node_path2.sep).join("/");
1051
+ const ignore = (options.ignore ?? []).map(globToRegExp);
1052
+ const roots = staticRoots(root);
1053
+ const result = {
1054
+ dir: root,
1055
+ ok: true,
1056
+ bundles: [],
1057
+ maps: [],
1058
+ ...options.server ? { server: true } : {},
1059
+ upload: { status: "unverified", endpoint: null, reason: null, checked: 0, missing: 0 },
1060
+ failures: []
1061
+ };
1062
+ const fail = (path, reason) => result.failures.push({ path, reason });
1063
+ for (const file of files) {
1064
+ const path = relPath(file);
1065
+ if (file.endsWith(".map")) {
1066
+ result.maps.push(path);
1067
+ if (!options.allowMaps && !options.server) {
1068
+ fail(path, "source map in the deploy output: it exposes your original source. Delete it after upload (upload --delete-after-upload, or the bundler plugin) or keep it out of the deploy");
1069
+ }
1070
+ continue;
1071
+ }
1072
+ if (!BUNDLE_RE.test(file)) continue;
1073
+ const bytes = await (0, import_promises4.readFile)(file);
1074
+ const bundle = { path, status: "ok", debugId: null, reasons: [], uploaded: null };
1075
+ result.bundles.push(bundle);
1076
+ const skip = (reason) => {
1077
+ bundle.status = "skipped";
1078
+ bundle.reasons.push(reason);
1079
+ };
1080
+ if (ignore.some((re) => re.test(path))) {
1081
+ skip("matches --ignore");
1082
+ continue;
1083
+ }
1084
+ const known = NOT_BUNDLES.find(([re]) => re.test(path));
1085
+ if (known) {
1086
+ skip(known[1]);
1087
+ continue;
1088
+ }
1089
+ let copied = null;
1090
+ for (const staticRoot of roots) {
1091
+ const candidate = (0, import_node_path2.join)(staticRoot, path);
1092
+ if ((0, import_node_fs2.existsSync)(candidate) && (await (0, import_promises4.readFile)(candidate)).equals(bytes)) copied = (0, import_node_path2.relative)((0, import_node_path2.dirname)(staticRoot), candidate).split(import_node_path2.sep).join("/");
1093
+ }
1094
+ if (copied) {
1095
+ skip(`copied unchanged from ${copied}, not built`);
1096
+ continue;
1097
+ }
1098
+ const code = bytes.toString("utf8");
1099
+ const commentId = readDebugIdComment(code);
1100
+ const snippetId = SNIPPET_ID_RE.exec(code)?.[1] ?? null;
1101
+ bundle.debugId = commentId;
1102
+ if (!commentId && !hasDebugIdSnippet(code)) {
1103
+ bundle.reasons.push(
1104
+ "no debug id: its errors can't be matched to a source map. Run `autter-runtime sourcemaps inject` on the build output (or use the bundler plugin with AUTTER_RUNTIME_KEY set) before deploying"
1105
+ );
1106
+ } else if (!commentId) {
1107
+ bundle.reasons.push("has the debug id snippet but no //# debugId= comment: run `autter-runtime sourcemaps inject` on it again");
1108
+ } else if (!hasDebugIdSnippet(code)) {
1109
+ bundle.reasons.push(
1110
+ "has a //# debugId= comment but no registration snippet, so the SDK can't report its id: run `autter-runtime sourcemaps inject` on it"
1111
+ );
1112
+ } else if (snippetId && snippetId !== commentId) {
1113
+ bundle.reasons.push(`its snippet registers debug id ${snippetId} but its comment says ${commentId}: run inject on the original build output`);
1114
+ }
1115
+ const url = readSourceMappingUrl(code);
1116
+ if (url?.startsWith("data:")) {
1117
+ if (!options.server) {
1118
+ bundle.reasons.push("carries an inline source map (data: URL): your original source ships with the bundle. Use hidden or external maps");
1119
+ }
1120
+ } else if (url && !/^[a-z][\w+.-]*:/i.test(url) && !url.startsWith("/")) {
1121
+ let target = url.split(/[?#]/)[0];
1122
+ try {
1123
+ target = decodeURIComponent(target);
1124
+ } catch {
1125
+ }
1126
+ if (!(0, import_node_fs2.existsSync)((0, import_node_path2.resolve)((0, import_node_path2.dirname)(file), target))) {
1127
+ bundle.reasons.push(
1128
+ `its sourceMappingURL points at ${url}, which is not in the output (browsers' devtools request it and get a 404). Remove the comment: upload --delete-after-upload does`
1129
+ );
1130
+ }
1131
+ }
1132
+ if (bundle.reasons.length) {
1133
+ bundle.status = "failed";
1134
+ for (const reason of bundle.reasons) fail(path, reason);
1135
+ }
1136
+ }
1137
+ const counted = result.bundles.filter((b) => b.status !== "skipped");
1138
+ if (!counted.length) fail(".", `no .js/.mjs/.cjs bundles under ${dir}: is this the build output?`);
1139
+ await checkUploads(result, counted, options, env);
1140
+ result.ok = result.failures.length === 0;
1141
+ return result;
1142
+ }
1143
+ async function checkUploads(result, bundles, options, env) {
1144
+ const upload = result.upload;
1145
+ const key = (options.key ?? env.AUTTER_RUNTIME_KEY ?? "").trim();
1146
+ if (!key) {
1147
+ upload.reason = "AUTTER_RUNTIME_KEY is not set: only the local checks ran";
1148
+ return;
1149
+ }
1150
+ upload.endpoint = resolveEndpoint(options.endpoint, env);
1151
+ if (key.startsWith(CLIENT_KEY_PREFIX)) {
1152
+ upload.status = "error";
1153
+ upload.reason = "AUTTER_RUNTIME_KEY is a publishable client key (autter_rtc_\u2026), which can't check source maps: use an upload key (autter_rtu_\u2026) or the server key (autter_rt_\u2026)";
1154
+ result.failures.push({ path: ".", reason: upload.reason });
1155
+ return;
1156
+ }
1157
+ const withIds = bundles.filter((b) => b.debugId);
1158
+ if (!withIds.length) {
1159
+ upload.reason = "no bundle has a debug id to check";
1160
+ return;
1161
+ }
1162
+ let release = "verify";
1163
+ try {
1164
+ release = resolveRelease(options.release, env, options.cwd).release;
1165
+ } catch {
1166
+ }
1167
+ const doFetch = options.fetch ?? fetch;
1168
+ const missing = [];
1169
+ for (let start = 0; start < withIds.length; start += CHECK_BATCH2) {
1170
+ const batch = withIds.slice(start, start + CHECK_BATCH2);
1171
+ let res;
1172
+ try {
1173
+ res = await doFetch(`${upload.endpoint}/v1/sourcemaps/check`, {
1174
+ method: "POST",
1175
+ headers: { "content-type": "application/json", authorization: `Bearer ${key}` },
1176
+ body: JSON.stringify({ release, files: batch.map((b) => ({ filename: servedFilename(options.urlPrefix, b.path), debugId: b.debugId })) })
1177
+ });
1178
+ } catch (err) {
1179
+ upload.status = "error";
1180
+ upload.reason = `could not reach the ingester: ${err.message}`;
1181
+ result.failures.push({ path: ".", reason: upload.reason });
1182
+ return;
1183
+ }
1184
+ if (res.status === 404 || res.status === 400) {
1185
+ upload.status = "unverified";
1186
+ upload.reason = res.status === 404 ? "the ingester predates 1.6.0 (no /v1/sourcemaps/check), so uploads can't be checked: upgrade it" : "the ingester predates 1.7.0, which checks maps by debug id alone, so uploads can't be checked after the maps were deleted: upgrade it";
1187
+ return;
1188
+ }
1189
+ if (res.status === 401 || res.status === 403) {
1190
+ upload.status = "error";
1191
+ upload.reason = `the ingester rejected the key (${res.status}): ${(await res.text()).slice(0, 300)}` + (key.startsWith(UPLOAD_KEY_PREFIX) ? " (upload keys, autter_rtu_\u2026, need ingester 1.7.0 or later)" : "");
1192
+ result.failures.push({ path: ".", reason: upload.reason });
1193
+ return;
1194
+ }
1195
+ if (!res.ok) {
1196
+ upload.status = "error";
1197
+ upload.reason = `the source map check failed: HTTP ${res.status}`;
1198
+ result.failures.push({ path: ".", reason: upload.reason });
1199
+ return;
1200
+ }
1201
+ const body = await res.json();
1202
+ const present = new Set(Array.isArray(body.present) ? body.present : []);
1203
+ batch.forEach((bundle, index) => {
1204
+ bundle.uploaded = present.has(index);
1205
+ if (!bundle.uploaded) missing.push(bundle);
1206
+ });
1207
+ }
1208
+ upload.checked = withIds.length;
1209
+ upload.missing = missing.length;
1210
+ upload.status = missing.length ? "missing" : "verified";
1211
+ upload.reason = missing.length ? `${missing.length} of ${withIds.length} bundle(s) have no map on the ingester` : null;
1212
+ for (const bundle of missing) {
1213
+ const reason = `no map with debug id ${bundle.debugId} on the ingester for this key's repository: upload this build's maps (autter-runtime sourcemaps upload, or the bundler plugin with AUTTER_RUNTIME_KEY set)`;
1214
+ bundle.status = "failed";
1215
+ bundle.reasons.push(reason);
1216
+ result.failures.push({ path: bundle.path, reason });
1217
+ }
1218
+ }
1219
+ function formatVerify(result) {
1220
+ const lines = [];
1221
+ for (const bundle of result.bundles) {
1222
+ if (bundle.status === "ok") lines.push(`ok ${bundle.path} (debug id ${bundle.debugId}${bundle.uploaded ? ", map uploaded" : ""})`);
1223
+ else if (bundle.status === "skipped") lines.push(`skipped ${bundle.path}: ${bundle.reasons.join("; ")}`);
1224
+ else for (const reason of bundle.reasons) lines.push(`FAILED ${bundle.path}: ${reason}`);
1225
+ }
1226
+ for (const map of result.maps) {
1227
+ const failure = result.failures.find((f) => f.path === map);
1228
+ lines.push(
1229
+ failure ? `FAILED ${map}: ${failure.reason}` : result.server ? `note ${map}: source map in the server output (browsers never load it)` : `warning ${map}: source map in the output (allowed by --allow-maps; remove it before deploying)`
1230
+ );
1231
+ }
1232
+ for (const failure of result.failures.filter((f) => f.path === ".")) lines.push(`FAILED ${failure.reason}`);
1233
+ const u = result.upload;
1234
+ lines.push(
1235
+ u.status === "verified" ? `upload: verified, ${u.checked} bundle map(s) on the ingester (${u.endpoint})` : u.status === "missing" ? `upload: ${u.reason} (${u.endpoint})` : `upload: ${u.status}${u.reason ? `, ${u.reason}` : ""}`
1236
+ );
1237
+ const counted = result.bundles.filter((b) => b.status !== "skipped");
1238
+ lines.push(
1239
+ result.ok ? `ok: ${counted.length} bundle(s) with debug ids, ${result.maps.length ? `${result.maps.length} source map(s) left (${result.server ? "server output" : "--allow-maps"})` : "no source maps left"}${u.status === "unverified" ? ", upload not verified" : ""}` : `${result.failures.length} problem(s) in ${result.dir}`
1240
+ );
1241
+ return lines;
1242
+ }
1243
+
1244
+ // src/build.ts
1245
+ var import_node_fs3 = require("fs");
1246
+ var import_node_path3 = require("path");
1247
+ function serverUrlPrefix(projectRoot, dir) {
1248
+ const rel = (0, import_node_path3.relative)(projectRoot, dir).split(import_node_path3.sep).join("/");
1249
+ return !rel || rel.startsWith("..") || (0, import_node_path3.isAbsolute)(rel) ? "/" : `/${rel.replace(/\/+$/, "")}/`;
1250
+ }
936
1251
  function buildKey(options) {
937
1252
  const key = (options.key ?? process.env.AUTTER_RUNTIME_KEY ?? "").trim();
938
1253
  return key || null;
@@ -987,12 +1302,13 @@ async function processBuildOutput(input) {
987
1302
  result.upload = await uploadDir(input.dir, {
988
1303
  files: input.files,
989
1304
  release: options.release,
990
- urlPrefix: options.urlPrefix ?? input.urlPrefix,
1305
+ urlPrefix: input.server ? input.urlPrefix : options.urlPrefix ?? input.urlPrefix,
991
1306
  endpoint: options.endpoint,
992
1307
  key: key ?? void 0,
993
1308
  dryRun: options.dryRun,
994
1309
  concurrency: options.concurrency,
995
1310
  deleteAfterUpload: options.deleteAfterUpload ?? input.deleteAfterUpload,
1311
+ ...options.sourcesContent === false ? { sourcesContent: false } : {},
996
1312
  log
997
1313
  });
998
1314
  for (const line of formatUploadSummary(result.upload)) log(line);
@@ -1017,9 +1333,9 @@ async function processBuildOutput(input) {
1017
1333
  async function cleanUp() {
1018
1334
  if (!input.ownMaps) return;
1019
1335
  const maps = /* @__PURE__ */ new Set([...input.maps ?? [], ...owners.keys()]);
1020
- const rel = (path) => (0, import_node_path2.relative)(input.dir, path).split(import_node_path2.sep).join("/");
1336
+ const rel = (path) => (0, import_node_path3.relative)(input.dir, path).split(import_node_path3.sep).join("/");
1021
1337
  if (options.upload === false || options.deleteAfterUpload === false) {
1022
- const kept = [...maps].filter((map) => (0, import_node_fs2.existsSync)(map));
1338
+ const kept = [...maps].filter((map) => (0, import_node_fs3.existsSync)(map));
1023
1339
  if (kept.length) {
1024
1340
  log(
1025
1341
  `kept ${kept.length} source map(s) this plugin generated (${options.upload === false ? "upload: false" : "deleteAfterUpload: false"}): they expose your source, so upload them with \`autter-runtime sourcemaps upload --delete-after-upload\` or keep them out of the deploy`
@@ -1034,7 +1350,7 @@ async function processBuildOutput(input) {
1034
1350
  if (options.failOnError) throw new Error(`[autter-runtime] ${message}`);
1035
1351
  if (!options.silent) console.warn(`[autter-runtime] ${message}`);
1036
1352
  }
1037
- result.removedMaps = [...maps].filter((map) => !(0, import_node_fs2.existsSync)(map)).map(rel).sort();
1353
+ result.removedMaps = [...maps].filter((map) => !(0, import_node_fs3.existsSync)(map)).map(rel).sort();
1038
1354
  if (result.removedMaps.length) {
1039
1355
  log(`removed ${result.removedMaps.length} source map(s) this plugin generated, so they are not deployed: ${outcome}`);
1040
1356
  }
@@ -1043,26 +1359,33 @@ async function processBuildOutput(input) {
1043
1359
  // Annotate the CommonJS export names for ESM import in node:
1044
1360
  0 && (module.exports = {
1045
1361
  BUNDLE_RE,
1362
+ CLIENT_KEY_PREFIX,
1046
1363
  DEBUG_ID_RE,
1047
1364
  DEBUG_ID_REGISTRY,
1048
1365
  DEFAULT_ENDPOINT,
1049
1366
  FatalUploadError,
1367
+ KEY_ADVICE,
1050
1368
  MAX_MAP_CHARS,
1051
1369
  MAX_UPLOAD_BYTES,
1052
1370
  RELEASE_ENV_VARS,
1053
1371
  RELEASE_RE,
1054
1372
  SCOPES_KEY,
1055
1373
  SCOPES_VERSION,
1374
+ SERVER_KEY_PREFIX,
1056
1375
  SNIPPET_LINE_KEY,
1376
+ UPLOAD_KEY_PREFIX,
1057
1377
  computeScopes,
1058
1378
  debugIdFor,
1059
1379
  debugIdSnippet,
1060
1380
  findBundles,
1061
1381
  formatUploadSummary,
1382
+ formatVerify,
1062
1383
  gitHead,
1384
+ globToRegExp,
1063
1385
  hasDebugIdSnippet,
1064
1386
  injectBundle,
1065
1387
  injectDir,
1388
+ isInAppSource,
1066
1389
  mapPathFor,
1067
1390
  normalizeDebugId,
1068
1391
  processBuildOutput,
@@ -1071,9 +1394,12 @@ async function processBuildOutput(input) {
1071
1394
  resolveEndpoint,
1072
1395
  resolveRelease,
1073
1396
  servedFilename,
1397
+ serverUrlPrefix,
1074
1398
  shiftMappings,
1075
1399
  snippetLine,
1076
1400
  storedMapJson,
1401
+ storedSourcesContent,
1077
1402
  stripSourceMappingUrl,
1078
- uploadDir
1403
+ uploadDir,
1404
+ verifyDir
1079
1405
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as AutterSourceMapsOptions, B as BUNDLE_RE, a as BuildOutputResult, b as BundleWithMap, D as DEFAULT_ENDPOINT, F as FatalUploadError, I as InjectOptions, c as InjectResult, M as MAX_MAP_CHARS, d as MAX_UPLOAD_BYTES, S as SNIPPET_LINE_KEY, U as UploadItem, e as UploadOptions, f as UploadSummary, g as findBundles, h as formatUploadSummary, i as injectBundle, j as injectDir, m as mapPathFor, p as processBuildOutput, r as readSourceMappingUrl, k as resolveEndpoint, s as servedFilename, l as shiftMappings, n as snippetLine, o as storedMapJson, q as stripSourceMappingUrl, u as uploadDir } from './build-CPXR9Xd3.cjs';
1
+ export { A as AutterSourceMapsOptions, B as BUNDLE_RE, a as BuildOutputResult, b as BundleWithMap, C as CLIENT_KEY_PREFIX, D as DEFAULT_ENDPOINT, F as FatalUploadError, I as InjectOptions, c as InjectResult, K as KEY_ADVICE, M as MAX_MAP_CHARS, d as MAX_UPLOAD_BYTES, S as SERVER_KEY_PREFIX, e as SNIPPET_LINE_KEY, U as UPLOAD_KEY_PREFIX, f as UploadItem, g as UploadOptions, h as UploadSummary, i as findBundles, j as formatUploadSummary, k as injectBundle, l as injectDir, m as isInAppSource, n as mapPathFor, p as processBuildOutput, r as readSourceMappingUrl, o as resolveEndpoint, s as servedFilename, q as serverUrlPrefix, t as shiftMappings, u as snippetLine, v as storedMapJson, w as storedSourcesContent, x as stripSourceMappingUrl, y as uploadDir } from './build-DGeoDkTh.cjs';
2
2
 
3
3
  /** 8-4-4-4-12 hex: the debug id shape shared with Rollup, Turbopack, esbuild
4
4
  * and Sentry tooling, and what the ingester accepts. */
@@ -14,13 +14,16 @@ declare function debugIdFor(bytes: Uint8Array | string): string;
14
14
  /** The id in the bundle's last `//# debugId=` comment, if any (ours, or one
15
15
  * a bundler wrote natively). */
16
16
  declare function readDebugIdComment(code: string): string | null;
17
- /** Property the browser SDK reads: `globalThis._autterDebugIds[stack] = id`. */
17
+ /** Property the SDKs read: `globalThis._autterDebugIds[stack] = id` (the
18
+ * browser SDK, and the Node and edge SDKs' `autter.debug_ids`, RF-05). */
18
19
  declare const DEBUG_ID_REGISTRY = "_autterDebugIds";
19
20
  /**
20
21
  * One line of ES5 that records this bundle's id under the stack of an Error
21
22
  * created inside it — the stack's first frame names the script URL the
22
- * browser actually loaded, which the SDK maps back to the id. `g.Error` keeps
23
- * working when the bundle shadows `Error` at top level. Never throws.
23
+ * browser loaded, or the file Node or an edge runtime ran, which the SDK
24
+ * maps back to the id. `g.Error` keeps working when the bundle shadows
25
+ * `Error` at top level. Never throws. The same snippet goes into browser
26
+ * and server bundles.
24
27
  */
25
28
  declare function debugIdSnippet(id: string): string;
26
29
  /** Whether the bundle already carries our registration snippet. (The
@@ -28,6 +31,66 @@ declare function debugIdSnippet(id: string): string;
28
31
  * contains it too.) */
29
32
  declare function hasDebugIdSnippet(code: string): boolean;
30
33
 
34
+ interface VerifyOptions {
35
+ /** Ingester base URL (default as for upload). */
36
+ endpoint?: string;
37
+ /** Default: $AUTTER_RUNTIME_KEY. */
38
+ key?: string;
39
+ /** Sent with the check (a debug id check ignores it). */
40
+ release?: string;
41
+ urlPrefix?: string;
42
+ /** Globs (relative to the directory) of JS files that are not bundles. */
43
+ ignore?: string[];
44
+ /** Report leftover .map files without failing (a step that runs before they are removed). */
45
+ allowMaps?: boolean;
46
+ /** A server bundle directory (tsup, esbuild, webpack or Next server
47
+ * output): leftover maps are reported, not failures — browsers never load
48
+ * them, and Node's own `--enable-source-maps` may use them. */
49
+ server?: boolean;
50
+ env?: NodeJS.ProcessEnv;
51
+ cwd?: string;
52
+ fetch?: typeof fetch;
53
+ }
54
+ interface VerifyBundle {
55
+ /** Relative to the directory. */
56
+ path: string;
57
+ status: "ok" | "failed" | "skipped";
58
+ debugId: string | null;
59
+ /** Why it failed, or why it was skipped. */
60
+ reasons: string[];
61
+ /** Whether the ingester has a map for its debug id (null: not checked). */
62
+ uploaded: boolean | null;
63
+ }
64
+ interface VerifyResult {
65
+ dir: string;
66
+ ok: boolean;
67
+ bundles: VerifyBundle[];
68
+ /** `.map` files in the directory (deploy risks unless allowMaps or server). */
69
+ maps: string[];
70
+ /** True when checked as server output (`--server`). */
71
+ server?: boolean;
72
+ upload: {
73
+ /** verified: every checked bundle's map is on the ingester; missing: some are not;
74
+ * unverified: not checked (no key, or an ingester that can't answer); error: the check failed. */
75
+ status: "verified" | "missing" | "unverified" | "error";
76
+ endpoint: string | null;
77
+ reason: string | null;
78
+ checked: number;
79
+ missing: number;
80
+ };
81
+ /** Every failure, one per file and reason. */
82
+ failures: Array<{
83
+ path: string;
84
+ reason: string;
85
+ }>;
86
+ }
87
+ /** A tiny glob: `**` any path, `*` any name part, `?` one character. A
88
+ * pattern without "/" matches the file name anywhere. */
89
+ declare function globToRegExp(glob: string): RegExp;
90
+ declare function verifyDir(dir: string, options?: VerifyOptions): Promise<VerifyResult>;
91
+ /** Human-readable report. */
92
+ declare function formatVerify(result: VerifyResult): string[];
93
+
31
94
  /**
32
95
  * Function scopes for scope-based frame names (like Sentry's
33
96
  * js-source-scopes and PostHog cymbal's `token.scope()`).
@@ -102,4 +165,4 @@ declare function gitHead(cwd?: string): string | null;
102
165
  */
103
166
  declare function resolveRelease(explicit?: string | null, env?: NodeJS.ProcessEnv, cwd?: string): ResolvedRelease;
104
167
 
105
- export { DEBUG_ID_RE, DEBUG_ID_REGISTRY, RELEASE_ENV_VARS, RELEASE_RE, type ResolvedRelease, SCOPES_KEY, SCOPES_VERSION, type ScopesResult, type SourceMapScopes, computeScopes, debugIdFor, debugIdSnippet, gitHead, hasDebugIdSnippet, normalizeDebugId, readDebugIdComment, resolveRelease };
168
+ export { DEBUG_ID_RE, DEBUG_ID_REGISTRY, RELEASE_ENV_VARS, RELEASE_RE, type ResolvedRelease, SCOPES_KEY, SCOPES_VERSION, type ScopesResult, type SourceMapScopes, type VerifyBundle, type VerifyOptions, type VerifyResult, computeScopes, debugIdFor, debugIdSnippet, formatVerify, gitHead, globToRegExp, hasDebugIdSnippet, normalizeDebugId, readDebugIdComment, resolveRelease, verifyDir };