@vercel/flags-core 1.9.0-da0b044-20260915081607 → 1.9.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.
@@ -1,5 +1,6 @@
1
1
  // src/index.next-js.ts
2
2
  import { cacheLife } from "next/cache";
3
+ import { after } from "next/server";
3
4
 
4
5
  // src/evaluate.ts
5
6
  import { xxHash32 as hashInput } from "js-xxhash";
@@ -55,7 +56,7 @@ function getSplitBoundaries(outcome) {
55
56
  function getCompiledRegex(rhs) {
56
57
  const cached = compiledRegexCache.get(rhs);
57
58
  if (cached) return cached;
58
- const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/, ""));
59
+ const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/g, ""));
59
60
  compiledRegexCache.set(rhs, compiled);
60
61
  return compiled;
61
62
  }
@@ -432,7 +433,7 @@ function resolveOutcome(params, outcome) {
432
433
  break;
433
434
  }
434
435
  }
435
- if (exhausted) currentPromille = PROMILLE_SCALE;
436
+ if (exhausted) currentPromille = outcome.finalPromille ?? PROMILLE_SCALE;
436
437
  if (currentPromille <= 0) {
437
438
  return {
438
439
  ...rollFromVariant,
@@ -558,7 +559,7 @@ function bulkEvaluate(flags, shared) {
558
559
  }
559
560
 
560
561
  // package.json
561
- var version = "1.9.0-da0b044-20260915081607";
562
+ var version = "1.9.0";
562
563
 
563
564
  // src/lib/report-value.ts
564
565
  function internalReportValue(key, value, data) {
@@ -777,7 +778,7 @@ async function bulkEvaluate2(id, flags, entities) {
777
778
  }
778
779
 
779
780
  // src/create-raw-client.ts
780
- import { waitUntil } from "@vercel/functions";
781
+ import { waitUntil as defaultWaitUntil } from "@vercel/functions";
781
782
  import { dequal } from "dequal/lite";
782
783
  var idCount = 0;
783
784
  async function performInitialize(instance, initFn) {
@@ -793,9 +794,11 @@ function createCreateRawClient(fns) {
793
794
  return function createRawClient({
794
795
  controller,
795
796
  origin,
796
- experimental_reportExposures
797
+ experimental_reportExposures,
798
+ waitUntil: waitUntil2 = defaultWaitUntil
797
799
  }) {
798
800
  const id = idCount++;
801
+ const pendingExposureReports = /* @__PURE__ */ new Set();
799
802
  controllerInstanceMap.set(id, {
800
803
  controller,
801
804
  initialized: false,
@@ -813,8 +816,10 @@ function createCreateRawClient(fns) {
813
816
  );
814
817
  }
815
818
  })();
819
+ pendingExposureReports.add(pending);
820
+ void pending.finally(() => pendingExposureReports.delete(pending));
816
821
  try {
817
- waitUntil(pending);
822
+ waitUntil2(pending);
818
823
  } catch {
819
824
  }
820
825
  }
@@ -849,6 +854,9 @@ function createCreateRawClient(fns) {
849
854
  },
850
855
  shutdown: async () => {
851
856
  await fns.shutdown(id);
857
+ while (pendingExposureReports.size > 0) {
858
+ await Promise.all(pendingExposureReports);
859
+ }
852
860
  controllerInstanceMap.delete(id);
853
861
  },
854
862
  getDatafile: async () => {
@@ -1013,8 +1021,128 @@ async function readBundledDefinitions(auth) {
1013
1021
  }
1014
1022
 
1015
1023
  // src/utils/ingest.ts
1024
+ import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
1025
+
1026
+ // src/controller/auth.ts
1016
1027
  import { getVercelOidcToken } from "@vercel/oidc";
1017
1028
 
1029
+ // src/utils/sdk-keys.ts
1030
+ var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
1031
+ function isValidSdkKey(value) {
1032
+ return SDK_KEY_REGEX.test(value);
1033
+ }
1034
+ var PROJECT_ID_REGEX = /^[A-Za-z0-9_]{1,64}$/;
1035
+ function isValidProjectId(value) {
1036
+ return PROJECT_ID_REGEX.test(value);
1037
+ }
1038
+ function parseFlagsConnectionString(text) {
1039
+ if (SDK_KEY_REGEX.test(text)) return { sdkKey: text, projectId: null };
1040
+ if (!text.startsWith("flags:")) return null;
1041
+ try {
1042
+ const params = new URLSearchParams(text.slice(6));
1043
+ return {
1044
+ sdkKey: params.get("sdkKey") || null,
1045
+ projectId: params.get("projectId") || null
1046
+ };
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+
1052
+ // src/controller/auth.ts
1053
+ var SOURCE_PROJECT_HEADER = "X-Vercel-Flags-Project-Id";
1054
+ function authHeaders(token, sourceProjectId) {
1055
+ return {
1056
+ Authorization: `Bearer ${token}`,
1057
+ ...sourceProjectId ? { [SOURCE_PROJECT_HEADER]: sourceProjectId } : null
1058
+ };
1059
+ }
1060
+ function unauthorizedMessage(sourceProjectId) {
1061
+ if (!sourceProjectId) return "unauthorized (401)";
1062
+ return `unauthorized (401): this deployment is not allowed to read the flags of project "${sourceProjectId}". Check the connection string and that the project is connected.`;
1063
+ }
1064
+ async function getOidcToken() {
1065
+ try {
1066
+ return await getVercelOidcToken();
1067
+ } catch {
1068
+ throw new Error(
1069
+ [
1070
+ "@vercel/flags-core: Failed to get OIDC token.",
1071
+ "Are you running in a Vercel Environment where OIDC tokens are available?",
1072
+ "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
1073
+ ].join(" ")
1074
+ );
1075
+ }
1076
+ }
1077
+ function getProjectIdFromOidcToken(oidcToken) {
1078
+ const tokenParts = oidcToken.split(".");
1079
+ if (tokenParts.length !== 3 || !tokenParts[1]) {
1080
+ throw new Error("@vercel/flags-core: Invalid OIDC token");
1081
+ }
1082
+ const payload = JSON.parse(
1083
+ Buffer.from(tokenParts[1], "base64url").toString("utf8")
1084
+ );
1085
+ if (typeof payload.project_id !== "string" || !payload.project_id) {
1086
+ throw new Error(
1087
+ "@vercel/flags-core: Missing project_id claim in OIDC token"
1088
+ );
1089
+ }
1090
+ return payload.project_id;
1091
+ }
1092
+ var Authentication = class {
1093
+ sdkKey;
1094
+ sourceProjectId;
1095
+ constructor(sdkKeyOrConnectionString) {
1096
+ if (sdkKeyOrConnectionString !== void 0) {
1097
+ if (typeof sdkKeyOrConnectionString !== "string") {
1098
+ throw new Error(
1099
+ `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
1100
+ );
1101
+ }
1102
+ const parsed = parseFlagsConnectionString(sdkKeyOrConnectionString);
1103
+ if (parsed?.sdkKey && parsed.projectId) {
1104
+ throw new Error(
1105
+ "@vercel/flags-core: A connection string must contain either sdkKey or projectId, not both"
1106
+ );
1107
+ }
1108
+ if (parsed?.sdkKey) {
1109
+ if (!isValidSdkKey(parsed.sdkKey)) {
1110
+ throw new Error("@vercel/flags-core: Missing sdkKey");
1111
+ }
1112
+ this.sdkKey = parsed.sdkKey;
1113
+ } else if (parsed?.projectId) {
1114
+ if (!isValidProjectId(parsed.projectId)) {
1115
+ throw new Error(
1116
+ "@vercel/flags-core: Invalid projectId in connection string"
1117
+ );
1118
+ }
1119
+ this.sourceProjectId = parsed.projectId;
1120
+ } else {
1121
+ throw new Error("@vercel/flags-core: Missing sdkKey");
1122
+ }
1123
+ }
1124
+ }
1125
+ async resolveToken() {
1126
+ if (this.sdkKey) {
1127
+ return this.sdkKey;
1128
+ }
1129
+ return await getOidcToken();
1130
+ }
1131
+ async resolveBundledDefinitionsLookup() {
1132
+ if (this.sdkKey) {
1133
+ return { type: "sdk-key", sdkKey: this.sdkKey };
1134
+ }
1135
+ if (this.sourceProjectId) {
1136
+ return { type: "project-id", projectId: this.sourceProjectId };
1137
+ }
1138
+ const oidcToken = await this.resolveToken();
1139
+ return {
1140
+ type: "project-id",
1141
+ projectId: getProjectIdFromOidcToken(oidcToken)
1142
+ };
1143
+ }
1144
+ };
1145
+
1018
1146
  // src/utils/backoff.ts
1019
1147
  var DEFAULT_BASE_MS = 250;
1020
1148
  var DEFAULT_CAP_MS = 5e3;
@@ -1030,6 +1158,20 @@ function getJitteredWaitMs(baseMs, ratio) {
1030
1158
  return Math.floor(min + Math.random() * span);
1031
1159
  }
1032
1160
 
1161
+ // src/utils/runtime-ingest.ts
1162
+ var FLAGS_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@vercel/flags-context");
1163
+ function getRuntimeIngestFor(auth) {
1164
+ return auth.sourceProjectId ? void 0 : getRuntimeIngest();
1165
+ }
1166
+ function getRuntimeIngest() {
1167
+ try {
1168
+ const context = globalThis[FLAGS_CONTEXT_SYMBOL];
1169
+ return typeof context?.ingest === "function" ? context.ingest : void 0;
1170
+ } catch {
1171
+ return void 0;
1172
+ }
1173
+ }
1174
+
1033
1175
  // src/utils/ingest.ts
1034
1176
  var MAX_RETRIES = 3;
1035
1177
  var MAX_EVENTS_PER_REQUEST = 2e3;
@@ -1043,7 +1185,7 @@ var debugLog = (...args) => {
1043
1185
  async function getEvaluatingOidcToken(auth) {
1044
1186
  if (!auth.sdkKey) return void 0;
1045
1187
  try {
1046
- return await getVercelOidcToken();
1188
+ return await getVercelOidcToken2();
1047
1189
  } catch {
1048
1190
  return void 0;
1049
1191
  }
@@ -1053,7 +1195,7 @@ async function getIngestHeaders(options, flushReason) {
1053
1195
  const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
1054
1196
  return {
1055
1197
  "Content-Type": "application/json",
1056
- Authorization: `Bearer ${token}`,
1198
+ ...authHeaders(token, options.auth.sourceProjectId),
1057
1199
  "User-Agent": `VercelFlagsCore/${version}`,
1058
1200
  [FLUSH_REASON_HEADER]: flushReason,
1059
1201
  ...options.metricEnvironment ?? process.env.VERCEL_ENV ? {
@@ -1063,8 +1205,28 @@ async function getIngestHeaders(options, flushReason) {
1063
1205
  ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1064
1206
  };
1065
1207
  }
1208
+ function getRuntimeIngestHeaders(options, flushReason) {
1209
+ return {
1210
+ "Content-Type": "application/json",
1211
+ ...options.auth.sdkKey ? { Authorization: `Bearer ${options.auth.sdkKey}` } : null,
1212
+ "User-Agent": `VercelFlagsCore/${version}`,
1213
+ [FLUSH_REASON_HEADER]: flushReason,
1214
+ ...options.metricEnvironment ?? process.env.VERCEL_ENV ? {
1215
+ "X-Vercel-Env": options.metricEnvironment ?? process.env.VERCEL_ENV
1216
+ } : null,
1217
+ ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1218
+ };
1219
+ }
1066
1220
  async function sendIngestEvents(options, events, flushId, flushReason) {
1067
- const eventsToSend = events.map((event) => event.ingestEvent());
1221
+ let eventsToSend = events.map((event) => event.ingestEvent());
1222
+ const runtimeIngest = getRuntimeIngestFor(options.auth);
1223
+ if (runtimeIngest) {
1224
+ const headers = getRuntimeIngestHeaders(options, flushReason);
1225
+ eventsToSend = eventsToSend.filter(
1226
+ (event) => !runtimeIngest({ headers, body: [event] })
1227
+ );
1228
+ if (eventsToSend.length === 0) return;
1229
+ }
1068
1230
  for (let i = 0; i < eventsToSend.length; i += MAX_EVENTS_PER_REQUEST) {
1069
1231
  await sendIngestChunk(
1070
1232
  options,
@@ -1128,15 +1290,17 @@ function getRequestContext() {
1128
1290
  }
1129
1291
 
1130
1292
  // src/utils/scheduler.ts
1131
- import { waitUntil as waitUntil2 } from "@vercel/functions";
1293
+ import { waitUntil } from "@vercel/functions";
1132
1294
  var IDLE_FLUSH_WAIT_MS = 5e3;
1133
1295
  var IDLE_FLUSH_JITTER_RATIO = 0.2;
1134
1296
  var MAX_FLUSH_WAIT_MS = 6e4;
1135
1297
  var Scheduler = class {
1136
- constructor(onFlush) {
1298
+ constructor(onFlush, scheduleTask = waitUntil) {
1137
1299
  this.onFlush = onFlush;
1300
+ this.scheduleTask = scheduleTask;
1138
1301
  }
1139
1302
  onFlush;
1303
+ scheduleTask;
1140
1304
  resolveWait = null;
1141
1305
  pending = null;
1142
1306
  idleTimeout = null;
@@ -1151,7 +1315,7 @@ var Scheduler = class {
1151
1315
  await this.onFlush(reason);
1152
1316
  })();
1153
1317
  try {
1154
- waitUntil2(this.pending);
1318
+ this.scheduleTask(this.pending);
1155
1319
  } catch {
1156
1320
  }
1157
1321
  this.maxTimeout = setTimeout(
@@ -1298,12 +1462,18 @@ var UsageTracker = class {
1298
1462
  flushCount = 0;
1299
1463
  options;
1300
1464
  scheduler;
1465
+ waitUntil;
1466
+ inflightFlushes = /* @__PURE__ */ new Set();
1301
1467
  trackedRequests = /* @__PURE__ */ new WeakSet();
1302
1468
  readEvents = [];
1303
1469
  evaluationEvents = /* @__PURE__ */ new Map();
1304
1470
  constructor(options) {
1305
1471
  this.options = options;
1306
- this.scheduler = new Scheduler((reason) => this.flushEvents(reason));
1472
+ this.waitUntil = options.waitUntil;
1473
+ this.scheduler = new Scheduler(
1474
+ (reason) => this.flushEvents(reason),
1475
+ options.waitUntil
1476
+ );
1307
1477
  }
1308
1478
  /**
1309
1479
  * Triggers an immediate flush of any pending events.
@@ -1312,6 +1482,7 @@ var UsageTracker = class {
1312
1482
  async shutdown() {
1313
1483
  await this.scheduler.shutdown();
1314
1484
  await this.flushEvents("shutdown");
1485
+ await Promise.all([...this.inflightFlushes]);
1315
1486
  }
1316
1487
  /**
1317
1488
  * Tracks a config read event. Deduplicates by request context.
@@ -1323,7 +1494,7 @@ var UsageTracker = class {
1323
1494
  if (this.trackedRequests.has(ctx)) return;
1324
1495
  this.trackedRequests.add(ctx);
1325
1496
  this.readEvents.push(new FlagsConfigReadEvent(headers, options));
1326
- this.scheduler.scheduleFlush();
1497
+ this.requestFlush();
1327
1498
  } catch (error) {
1328
1499
  console.error("@vercel/flags-core: Failed to record event:", error);
1329
1500
  }
@@ -1347,7 +1518,7 @@ var UsageTracker = class {
1347
1518
  new FlagsEvaluationEvent(bucketedOptions)
1348
1519
  );
1349
1520
  }
1350
- this.scheduler.scheduleFlush();
1521
+ this.requestFlush();
1351
1522
  } catch (error) {
1352
1523
  console.error(
1353
1524
  "@vercel/flags-core: Failed to record evaluation event:",
@@ -1355,6 +1526,25 @@ var UsageTracker = class {
1355
1526
  );
1356
1527
  }
1357
1528
  }
1529
+ /**
1530
+ * Flushes immediately when the runtime provides an ingest transport,
1531
+ * otherwise falls back to the time-based scheduler.
1532
+ */
1533
+ requestFlush() {
1534
+ if (getRuntimeIngestFor(this.options.auth)) {
1535
+ const flush = this.flushEvents("immediate").catch((error) => {
1536
+ console.error("@vercel/flags-core: Failed to flush events:", error);
1537
+ });
1538
+ this.inflightFlushes.add(flush);
1539
+ void flush.finally(() => this.inflightFlushes.delete(flush));
1540
+ try {
1541
+ this.waitUntil?.(flush);
1542
+ } catch {
1543
+ }
1544
+ } else {
1545
+ this.scheduler.scheduleFlush();
1546
+ }
1547
+ }
1358
1548
  /**
1359
1549
  * Send all events to the ingest service
1360
1550
  */
@@ -1442,169 +1632,94 @@ var BundledSource = class {
1442
1632
 
1443
1633
  // src/controller/fetch-datafile.ts
1444
1634
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1635
+ var DEFAULT_MAX_ATTEMPTS = 3;
1636
+ var DatafileHttpError = class extends Error {
1637
+ constructor(status, statusText) {
1638
+ super(`Failed to fetch data: ${statusText}`);
1639
+ this.status = status;
1640
+ }
1641
+ status;
1642
+ get retryable() {
1643
+ return this.status === 408 || this.status === 429 || this.status >= 500;
1644
+ }
1645
+ };
1445
1646
  async function fetchDatafile(options) {
1446
- const token = await options.auth.resolveToken();
1647
+ options.signal?.throwIfAborted();
1648
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1649
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
1650
+ throw new Error("maxAttempts must be a positive integer");
1651
+ }
1447
1652
  const controller = new AbortController();
1653
+ const { signal } = controller;
1654
+ let rejectAbort;
1655
+ const aborted = new Promise((_, reject) => {
1656
+ rejectAbort = reject;
1657
+ });
1658
+ const onAbort = () => rejectAbort(signal.reason);
1659
+ signal.addEventListener("abort", onAbort, { once: true });
1660
+ const onExternalAbort = () => controller.abort(options.signal?.reason);
1661
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true });
1448
1662
  const timeoutId = setTimeout(
1449
- () => controller.abort(),
1663
+ () => controller.abort(
1664
+ new Error("@vercel/flags-core: Datafile fetch deadline exceeded")
1665
+ ),
1450
1666
  DEFAULT_FETCH_TIMEOUT_MS
1451
1667
  );
1452
- const onExternalAbort = () => controller.abort();
1453
- if (options.signal) {
1454
- if (options.signal.aborted) {
1455
- clearTimeout(timeoutId);
1456
- throw new Error("Fetch aborted");
1457
- }
1458
- options.signal.addEventListener("abort", onExternalAbort, { once: true });
1459
- }
1460
- try {
1668
+ let delay;
1669
+ const fetchAttempt = async () => {
1670
+ const token = await options.auth.resolveToken();
1671
+ signal.throwIfAborted();
1461
1672
  const res = await options.fetch(`${options.host}/v1/datafile`, {
1462
1673
  headers: {
1463
- Authorization: `Bearer ${token}`,
1674
+ ...authHeaders(token, options.auth.sourceProjectId),
1464
1675
  "User-Agent": `VercelFlagsCore/${version}`,
1465
1676
  ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null
1466
1677
  },
1467
- signal: controller.signal
1678
+ signal
1468
1679
  });
1469
- clearTimeout(timeoutId);
1470
- options.signal?.removeEventListener("abort", onExternalAbort);
1680
+ signal.throwIfAborted();
1471
1681
  if (!res.ok) {
1472
- throw new Error(`Failed to fetch data: ${res.statusText}`);
1473
- }
1474
- return res.json();
1475
- } catch (error) {
1476
- clearTimeout(timeoutId);
1477
- options.signal?.removeEventListener("abort", onExternalAbort);
1478
- throw error instanceof Error ? error : new Error("Unknown fetch error");
1479
- }
1480
- }
1481
-
1482
- // src/controller/header-source.ts
1483
- import { waitUntil as waitUntil3 } from "@vercel/functions";
1484
-
1485
- // src/controller/tagged-data.ts
1486
- function tagData(data, origin) {
1487
- return Object.assign(data, { _origin: origin });
1488
- }
1489
- function originToMetricsSource(origin) {
1490
- switch (origin) {
1491
- case "stream":
1492
- case "poll":
1493
- case "provided":
1494
- return "in-memory";
1495
- case "fetched":
1496
- return "remote";
1497
- case "bundled":
1498
- return "embedded";
1499
- }
1500
- }
1501
-
1502
- // src/controller/typed-emitter.ts
1503
- var TypedEmitter = class {
1504
- handlers = /* @__PURE__ */ new Map();
1505
- on(event, handler) {
1506
- let set = this.handlers.get(event);
1507
- if (!set) {
1508
- set = /* @__PURE__ */ new Set();
1509
- this.handlers.set(event, set);
1510
- }
1511
- set.add(handler);
1512
- }
1513
- off(event, handler) {
1514
- this.handlers.get(event)?.delete(handler);
1515
- }
1516
- emit(event, ...args) {
1517
- const set = this.handlers.get(event);
1518
- if (set) {
1519
- for (const handler of set) {
1520
- handler(...args);
1521
- }
1682
+ void res.body?.cancel().catch(() => {
1683
+ });
1684
+ throw new DatafileHttpError(
1685
+ res.status,
1686
+ res.status === 401 && options.auth.sourceProjectId ? unauthorizedMessage(options.auth.sourceProjectId) : res.statusText
1687
+ );
1522
1688
  }
1523
- }
1524
- };
1525
-
1526
- // src/controller/header-source.ts
1527
- var HeaderSource = class extends TypedEmitter {
1528
- options;
1529
- abortController;
1530
- promise;
1531
- constructor(options) {
1532
- super();
1533
- this.options = options;
1534
- }
1535
- fetchDatafile() {
1536
- if (this.promise) return this.promise;
1537
- const abortController = new AbortController();
1538
- this.abortController = abortController;
1539
- this.promise = fetchDatafile({
1540
- ...this.options,
1541
- signal: abortController.signal
1542
- }).then((data) => {
1543
- abortController.signal.throwIfAborted();
1544
- this.emit("data", data);
1545
- return data;
1546
- }).finally(() => {
1547
- if (this.abortController === abortController) {
1548
- this.promise = void 0;
1549
- this.abortController = void 0;
1689
+ const data = await res.json();
1690
+ signal.throwIfAborted();
1691
+ return data;
1692
+ };
1693
+ try {
1694
+ for (let attempt = 0; ; attempt++) {
1695
+ if (attempt > 0) {
1696
+ await Promise.race([
1697
+ new Promise((resolve) => {
1698
+ delay = setTimeout(resolve, 100 * 2 ** (attempt - 1));
1699
+ }),
1700
+ aborted
1701
+ ]);
1550
1702
  }
1551
- });
1552
- return this.promise;
1553
- }
1554
- getUpdatedAtHeader(projectId) {
1555
- const ctx = getRequestContext();
1556
- const header = ctx.headers?.["x-vercel-flags-config-versions"] ?? ctx.headers?.["flags-config-versions"];
1557
- if (!header) {
1558
- return;
1559
- }
1560
- const prefix = `flags_${projectId}=`;
1561
- const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1562
- const timestamp = Number(value);
1563
- return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : void 0;
1564
- }
1565
- async resolveData(currentData) {
1566
- if (!currentData.configUpdatedAt) {
1567
- return;
1568
- }
1569
- const updatedAtHeader = this.getUpdatedAtHeader(currentData.projectId);
1570
- if (!updatedAtHeader) {
1571
- return;
1572
- }
1573
- const currentUpdatedAt = Number(currentData.configUpdatedAt);
1574
- if (updatedAtHeader <= currentUpdatedAt) {
1575
- return [currentData, "HIT"];
1576
- }
1577
- if (updatedAtHeader <= currentUpdatedAt + 1e4) {
1578
- const pending = this.fetchDatafile();
1579
- const signal = this.abortController?.signal;
1580
- const background = pending.catch((error) => {
1581
- if (!signal?.aborted) {
1582
- console.error("@vercel/flags-core: Header refresh failed:", error);
1703
+ signal.throwIfAborted();
1704
+ try {
1705
+ return await Promise.race([fetchAttempt(), aborted]);
1706
+ } catch (error) {
1707
+ signal.throwIfAborted();
1708
+ if (attempt === maxAttempts - 1 || error instanceof DatafileHttpError && !error.retryable) {
1709
+ throw error instanceof Error ? error : new Error("Unknown fetch error");
1583
1710
  }
1584
- });
1585
- waitUntil3(background);
1586
- return [currentData, "STALE"];
1711
+ }
1587
1712
  }
1588
- const data = await this.fetchDatafile();
1589
- return [tagData(data, "fetched"), "MISS"];
1590
- }
1591
- read(currentData) {
1592
- return this.resolveData(currentData);
1593
- }
1594
- isAvailable(projectId) {
1595
- return !!this.getUpdatedAtHeader(projectId);
1596
- }
1597
- /**
1598
- * Abort the current header-driven fetch and discard its pending work.
1599
- */
1600
- stop() {
1601
- this.abortController?.abort();
1602
- this.abortController = void 0;
1603
- this.promise = void 0;
1713
+ } finally {
1714
+ clearTimeout(timeoutId);
1715
+ clearTimeout(delay);
1716
+ signal.removeEventListener("abort", onAbort);
1717
+ options.signal?.removeEventListener("abort", onExternalAbort);
1604
1718
  }
1605
- };
1719
+ }
1606
1720
 
1607
1721
  // src/controller/normalized-options.ts
1722
+ import { waitUntil as defaultWaitUntil2 } from "@vercel/functions";
1608
1723
  var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1609
1724
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
1610
1725
  var MIN_POLLING_INTERVAL_MS = 3e4;
@@ -1648,6 +1763,7 @@ function normalizeOptions(options) {
1648
1763
  polling,
1649
1764
  buildStep,
1650
1765
  fetch: options.fetch ?? globalThis.fetch,
1766
+ waitUntil: options.waitUntil ?? defaultWaitUntil2,
1651
1767
  host: "https://flags.vercel.com",
1652
1768
  metricEnvironment: options.metricEnvironment,
1653
1769
  clientName: options.clientName,
@@ -1655,6 +1771,35 @@ function normalizeOptions(options) {
1655
1771
  };
1656
1772
  }
1657
1773
 
1774
+ // src/controller/typed-emitter.ts
1775
+ var TypedEmitter = class {
1776
+ handlers = /* @__PURE__ */ new Map();
1777
+ on(event, handler) {
1778
+ let set = this.handlers.get(event);
1779
+ if (!set) {
1780
+ set = /* @__PURE__ */ new Set();
1781
+ this.handlers.set(event, set);
1782
+ }
1783
+ set.add(handler);
1784
+ }
1785
+ off(event, handler) {
1786
+ const set = this.handlers.get(event);
1787
+ if (!set) return;
1788
+ set.delete(handler);
1789
+ if (set.size === 0) {
1790
+ this.handlers.delete(event);
1791
+ }
1792
+ }
1793
+ emit(event, ...args) {
1794
+ const set = this.handlers.get(event);
1795
+ if (set) {
1796
+ for (const handler of set) {
1797
+ handler(...args);
1798
+ }
1799
+ }
1800
+ }
1801
+ };
1802
+
1658
1803
  // src/controller/polling-source.ts
1659
1804
  var PollingSource = class extends TypedEmitter {
1660
1805
  config;
@@ -1670,13 +1815,16 @@ var PollingSource = class extends TypedEmitter {
1670
1815
  */
1671
1816
  async poll() {
1672
1817
  if (this.abortController?.signal.aborted) return;
1818
+ this.abortController ??= new AbortController();
1819
+ const controller = this.abortController;
1673
1820
  try {
1674
1821
  const data = await fetchDatafile({
1675
1822
  ...this.config,
1676
- signal: this.abortController?.signal
1823
+ signal: controller.signal
1677
1824
  });
1678
1825
  this.emit("data", data);
1679
1826
  } catch (error) {
1827
+ controller.signal.throwIfAborted();
1680
1828
  const err = error instanceof Error ? error : new Error("Unknown poll error");
1681
1829
  this.emit("error", err);
1682
1830
  }
@@ -1688,9 +1836,9 @@ var PollingSource = class extends TypedEmitter {
1688
1836
  */
1689
1837
  startInterval() {
1690
1838
  if (this.intervalId) return;
1691
- this.abortController = new AbortController();
1692
1839
  this.intervalId = setInterval(
1693
- () => void this.poll(),
1840
+ () => void this.poll().catch(() => {
1841
+ }),
1694
1842
  this.config.polling.intervalMs
1695
1843
  );
1696
1844
  }
@@ -1731,8 +1879,8 @@ function backoff(retryCount) {
1731
1879
  return delay + Math.random() * 1e3;
1732
1880
  }
1733
1881
  var UnauthorizedError = class extends Error {
1734
- constructor() {
1735
- super("stream: unauthorized (401)");
1882
+ constructor(sourceProjectId) {
1883
+ super(`stream: ${unauthorizedMessage(sourceProjectId)}`);
1736
1884
  this.name = "UnauthorizedError";
1737
1885
  }
1738
1886
  };
@@ -1792,7 +1940,7 @@ async function connectStream(config, callbacks) {
1792
1940
  throw new TokenResolutionError(error);
1793
1941
  });
1794
1942
  const headers = {
1795
- Authorization: `Bearer ${token}`,
1943
+ ...authHeaders(token, config.sourceProjectId),
1796
1944
  "User-Agent": `VercelFlagsCore/${version}`,
1797
1945
  "X-Retry-Attempt": String(retryCount)
1798
1946
  };
@@ -1814,7 +1962,7 @@ async function connectStream(config, callbacks) {
1814
1962
  if (!response.ok) {
1815
1963
  if (response.status === 401) {
1816
1964
  if (!initialDataReceived) {
1817
- rejectInit(new UnauthorizedError());
1965
+ rejectInit(new UnauthorizedError(config.sourceProjectId));
1818
1966
  }
1819
1967
  abortController.abort();
1820
1968
  break;
@@ -1958,6 +2106,7 @@ var StreamSource = class extends TypedEmitter {
1958
2106
  {
1959
2107
  host: this.options.host,
1960
2108
  resolveToken: () => this.options.auth.resolveToken(),
2109
+ sourceProjectId: this.options.auth.sourceProjectId,
1961
2110
  abortController,
1962
2111
  fetch: this.options.fetch,
1963
2112
  revision: this.revision
@@ -1994,6 +2143,23 @@ var StreamSource = class extends TypedEmitter {
1994
2143
  }
1995
2144
  };
1996
2145
 
2146
+ // src/controller/tagged-data.ts
2147
+ function tagData(data, origin) {
2148
+ return Object.assign(data, { _origin: origin });
2149
+ }
2150
+ function originToMetricsSource(origin) {
2151
+ switch (origin) {
2152
+ case "stream":
2153
+ case "poll":
2154
+ case "provided":
2155
+ return "in-memory";
2156
+ case "fetched":
2157
+ return "remote";
2158
+ case "bundled":
2159
+ return "embedded";
2160
+ }
2161
+ }
2162
+
1997
2163
  // src/controller/index.ts
1998
2164
  function parseConfigUpdatedAt(value) {
1999
2165
  if (typeof value === "number") return value;
@@ -2018,14 +2184,14 @@ var Controller = class {
2018
2184
  streamSource;
2019
2185
  pollingSource;
2020
2186
  bundledSource;
2021
- headerSource;
2022
2187
  // Usage tracking
2023
2188
  usageTracker;
2024
2189
  isFirstGetData = true;
2025
2190
  // Build-step deduplication
2026
2191
  buildDataPromise = null;
2027
2192
  buildReadTracked = false;
2028
- // Suppresses usage tracking when the SDK key is unauthorized
2193
+ // Suppresses usage tracking while the credential is rejected. Set on a 401
2194
+ // from any source, cleared as soon as a source delivers data again.
2029
2195
  unauthorized = false;
2030
2196
  constructor(options) {
2031
2197
  this.options = normalizeOptions(options);
@@ -2038,7 +2204,6 @@ var Controller = class {
2038
2204
  auth: this.options.auth,
2039
2205
  readBundledDefinitions
2040
2206
  });
2041
- this.headerSource = new HeaderSource(this.options);
2042
2207
  this.wireSourceEvents();
2043
2208
  if (this.options.datafile) {
2044
2209
  this.data = tagData(this.options.datafile, "provided");
@@ -2047,11 +2212,13 @@ var Controller = class {
2047
2212
  }
2048
2213
  // Source event handlers (stored for cleanup)
2049
2214
  onStreamData = (data) => {
2215
+ this.unauthorized = false;
2050
2216
  if (this.isNewerData(data)) {
2051
2217
  this.data = tagData(data, "stream");
2052
2218
  }
2053
2219
  };
2054
2220
  onStreamPrimed = () => {
2221
+ this.unauthorized = false;
2055
2222
  if (this.state === "degraded" || this.state === "initializing:stream") {
2056
2223
  this.transition("streaming");
2057
2224
  }
@@ -2067,18 +2234,15 @@ var Controller = class {
2067
2234
  }
2068
2235
  };
2069
2236
  onPollData = (data) => {
2237
+ this.unauthorized = false;
2070
2238
  if (this.isNewerData(data)) {
2071
2239
  this.data = tagData(data, "poll");
2072
2240
  }
2073
2241
  };
2074
2242
  onPollError = (error) => {
2243
+ this.noteUnauthorized(error);
2075
2244
  console.error("@vercel/flags-core: Poll failed:", error);
2076
2245
  };
2077
- onFetchedData = (data) => {
2078
- if (this.isNewerData(data)) {
2079
- this.data = tagData(data, "fetched");
2080
- }
2081
- };
2082
2246
  // ---------------------------------------------------------------------------
2083
2247
  // Source event wiring
2084
2248
  // ---------------------------------------------------------------------------
@@ -2089,7 +2253,6 @@ var Controller = class {
2089
2253
  this.streamSource.on("disconnected", this.onStreamDisconnected);
2090
2254
  this.pollingSource.on("data", this.onPollData);
2091
2255
  this.pollingSource.on("error", this.onPollError);
2092
- this.headerSource.on("data", this.onFetchedData);
2093
2256
  }
2094
2257
  unwireSourceEvents() {
2095
2258
  this.streamSource.off("data", this.onStreamData);
@@ -2098,7 +2261,6 @@ var Controller = class {
2098
2261
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2099
2262
  this.pollingSource.off("data", this.onPollData);
2100
2263
  this.pollingSource.off("error", this.onPollError);
2101
- this.headerSource.off("data", this.onFetchedData);
2102
2264
  }
2103
2265
  // ---------------------------------------------------------------------------
2104
2266
  // State machine
@@ -2116,8 +2278,6 @@ var Controller = class {
2116
2278
  return "streaming";
2117
2279
  case "polling":
2118
2280
  return "polling";
2119
- case "vercel":
2120
- return "vercel";
2121
2281
  default:
2122
2282
  return "offline";
2123
2283
  }
@@ -2153,9 +2313,7 @@ var Controller = class {
2153
2313
  }
2154
2314
  }
2155
2315
  if (this.data) {
2156
- if (this.headerSource.isAvailable(this.data.projectId)) {
2157
- this.transition("vercel");
2158
- } else if (this.options.stream.enabled) {
2316
+ if (this.options.stream.enabled) {
2159
2317
  this.transition("initializing:stream");
2160
2318
  await this.tryInitializeStream();
2161
2319
  } else if (this.options.polling.enabled) {
@@ -2218,7 +2376,6 @@ var Controller = class {
2218
2376
  this.unwireSourceEvents();
2219
2377
  this.streamSource.stop();
2220
2378
  this.pollingSource.stop();
2221
- this.headerSource.stop();
2222
2379
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2223
2380
  this.transition("shutdown");
2224
2381
  await this.usageTracker.shutdown();
@@ -2254,9 +2411,10 @@ var Controller = class {
2254
2411
  this.data = tagData(fetched, "fetched");
2255
2412
  result = this.data;
2256
2413
  cacheStatus = "MISS";
2257
- } catch {
2258
- throw new Error(
2259
- "@vercel/flags-core: No flag definitions available. Initialize the client or provide a datafile."
2414
+ } catch (error) {
2415
+ this.noteUnauthorized(error);
2416
+ throw this.noDefinitionsError(
2417
+ ". Initialize the client or provide a datafile."
2260
2418
  );
2261
2419
  }
2262
2420
  }
@@ -2300,12 +2458,6 @@ var Controller = class {
2300
2458
  return this.resolveDataForBuildStep();
2301
2459
  }
2302
2460
  if (this.data) {
2303
- if (this.mode === "vercel") {
2304
- const data = await this.headerSource.read(this.data);
2305
- if (data) {
2306
- return data;
2307
- }
2308
- }
2309
2461
  const cacheStatus = this.isConnected ? "HIT" : "STALE";
2310
2462
  return [this.data, cacheStatus];
2311
2463
  }
@@ -2324,9 +2476,7 @@ var Controller = class {
2324
2476
  await this.streamSource.start();
2325
2477
  return true;
2326
2478
  } catch (error) {
2327
- if (error instanceof UnauthorizedError) {
2328
- this.unauthorized = true;
2329
- }
2479
+ this.noteUnauthorized(error);
2330
2480
  return false;
2331
2481
  }
2332
2482
  }
@@ -2354,9 +2504,7 @@ var Controller = class {
2354
2504
  return true;
2355
2505
  } catch (error) {
2356
2506
  clearTimeout(timeoutId);
2357
- if (error instanceof Error && error.message.includes("401")) {
2358
- this.unauthorized = true;
2359
- }
2507
+ this.noteUnauthorized(error);
2360
2508
  return false;
2361
2509
  }
2362
2510
  }
@@ -2409,6 +2557,11 @@ var Controller = class {
2409
2557
  return false;
2410
2558
  }
2411
2559
  }
2560
+ noteUnauthorized(error) {
2561
+ if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("401")) {
2562
+ this.unauthorized = true;
2563
+ }
2564
+ }
2412
2565
  // ---------------------------------------------------------------------------
2413
2566
  // Build step helpers
2414
2567
  // ---------------------------------------------------------------------------
@@ -2454,10 +2607,11 @@ var Controller = class {
2454
2607
  fetch: this.options.fetch
2455
2608
  });
2456
2609
  return tagData(fetched, "fetched");
2457
- } catch {
2610
+ } catch (error) {
2611
+ this.noteUnauthorized(error);
2458
2612
  }
2459
- throw new Error(
2460
- "@vercel/flags-core: No flag definitions available during build. Provide a datafile or bundled definitions."
2613
+ throw this.noDefinitionsError(
2614
+ " during build. Provide a datafile or bundled definitions."
2461
2615
  );
2462
2616
  }
2463
2617
  // ---------------------------------------------------------------------------
@@ -2491,8 +2645,17 @@ var Controller = class {
2491
2645
  } catch {
2492
2646
  }
2493
2647
  }
2494
- throw new Error(
2495
- "@vercel/flags-core: No flag definitions available. Bundled definitions not found."
2648
+ throw this.noDefinitionsError(". Bundled definitions not found.");
2649
+ }
2650
+ /**
2651
+ * `detail` continues the sentence "No flag definitions available", so it
2652
+ * starts with either "." or " during build.".
2653
+ */
2654
+ noDefinitionsError(detail) {
2655
+ const { sourceProjectId } = this.options.auth;
2656
+ const reason = this.unauthorized && sourceProjectId ? ` Request was ${unauthorizedMessage(sourceProjectId)}` : "";
2657
+ return new Error(
2658
+ `@vercel/flags-core: No flag definitions available${detail}${reason}`
2496
2659
  );
2497
2660
  }
2498
2661
  /**
@@ -2543,8 +2706,8 @@ var Controller = class {
2543
2706
  } catch {
2544
2707
  }
2545
2708
  }
2546
- throw new Error(
2547
- "@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
2709
+ throw this.noDefinitionsError(
2710
+ ". Provide a datafile or bundled definitions."
2548
2711
  );
2549
2712
  }
2550
2713
  // ---------------------------------------------------------------------------
@@ -2617,90 +2780,8 @@ var Controller = class {
2617
2780
  }
2618
2781
  };
2619
2782
 
2620
- // src/controller/auth.ts
2621
- import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
2622
-
2623
- // src/utils/sdk-keys.ts
2624
- var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
2625
- function parseSdkKeyFromFlagsConnectionString(text) {
2626
- if (SDK_KEY_REGEX.test(text)) return text;
2627
- try {
2628
- if (!text.startsWith("flags:")) return null;
2629
- const params = new URLSearchParams(text.slice(6));
2630
- const sdkKey = params.get("sdkKey");
2631
- if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) return sdkKey;
2632
- } catch {
2633
- }
2634
- return null;
2635
- }
2636
-
2637
- // src/controller/auth.ts
2638
- async function getOidcToken() {
2639
- try {
2640
- return await getVercelOidcToken2();
2641
- } catch {
2642
- throw new Error(
2643
- [
2644
- "@vercel/flags-core: Failed to get OIDC token.",
2645
- "Are you running in a Vercel Environment where OIDC tokens are available?",
2646
- "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
2647
- ].join(" ")
2648
- );
2649
- }
2650
- }
2651
- function getProjectIdFromOidcToken(oidcToken) {
2652
- const tokenParts = oidcToken.split(".");
2653
- if (tokenParts.length !== 3 || !tokenParts[1]) {
2654
- throw new Error("@vercel/flags-core: Invalid OIDC token");
2655
- }
2656
- const payload = JSON.parse(
2657
- Buffer.from(tokenParts[1], "base64url").toString("utf8")
2658
- );
2659
- if (typeof payload.project_id !== "string" || !payload.project_id) {
2660
- throw new Error(
2661
- "@vercel/flags-core: Missing project_id claim in OIDC token"
2662
- );
2663
- }
2664
- return payload.project_id;
2665
- }
2666
- var Authentication = class {
2667
- sdkKey;
2668
- constructor(sdkKeyOrConnectionString) {
2669
- if (sdkKeyOrConnectionString !== void 0) {
2670
- if (typeof sdkKeyOrConnectionString !== "string") {
2671
- throw new Error(
2672
- `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
2673
- );
2674
- }
2675
- const parsed = parseSdkKeyFromFlagsConnectionString(
2676
- sdkKeyOrConnectionString
2677
- );
2678
- if (!parsed) {
2679
- throw new Error("@vercel/flags-core: Missing sdkKey");
2680
- }
2681
- this.sdkKey = parsed;
2682
- }
2683
- }
2684
- async resolveToken() {
2685
- if (this.sdkKey) {
2686
- return this.sdkKey;
2687
- }
2688
- return await getOidcToken();
2689
- }
2690
- async resolveBundledDefinitionsLookup() {
2691
- if (this.sdkKey) {
2692
- return { type: "sdk-key", sdkKey: this.sdkKey };
2693
- }
2694
- const oidcToken = await this.resolveToken();
2695
- return {
2696
- type: "project-id",
2697
- projectId: getProjectIdFromOidcToken(oidcToken)
2698
- };
2699
- }
2700
- };
2701
-
2702
2783
  // src/index.make.ts
2703
- function make(createRawClient) {
2784
+ function make(createRawClient, defaults) {
2704
2785
  let _defaultFlagsClient = null;
2705
2786
  function createClient2(sdkKeyOrConnectionStringOrOptions, options) {
2706
2787
  const optionsOnly = typeof sdkKeyOrConnectionStringOrOptions === "object" && sdkKeyOrConnectionStringOrOptions !== null;
@@ -2708,10 +2789,20 @@ function make(createRawClient) {
2708
2789
  const createClientOptions = optionsOnly ? sdkKeyOrConnectionStringOrOptions : options;
2709
2790
  const { experimental_reportExposures, ...controllerOptions } = createClientOptions ?? {};
2710
2791
  const auth = new Authentication(sdkKeyOrConnectionString);
2711
- const controller = new Controller({ auth, ...controllerOptions });
2792
+ const waitUntil2 = controllerOptions.waitUntil ?? defaults.waitUntil;
2793
+ const controller = new Controller({
2794
+ auth,
2795
+ ...controllerOptions,
2796
+ waitUntil: waitUntil2
2797
+ });
2712
2798
  return createRawClient({
2713
2799
  controller,
2714
- origin: { provider: "vercel", sdkKey: auth.sdkKey },
2800
+ origin: {
2801
+ provider: "vercel",
2802
+ sdkKey: auth.sdkKey,
2803
+ ...auth.sourceProjectId ? { projectId: auth.sourceProjectId } : null
2804
+ },
2805
+ waitUntil: waitUntil2,
2715
2806
  ...experimental_reportExposures ? { experimental_reportExposures } : {}
2716
2807
  });
2717
2808
  }
@@ -2774,7 +2865,8 @@ var cachedFns = {
2774
2865
  }
2775
2866
  };
2776
2867
  var { flagsClient, resetDefaultFlagsClient, createClient } = make(
2777
- createCreateRawClient(cachedFns)
2868
+ createCreateRawClient(cachedFns),
2869
+ { waitUntil: after }
2778
2870
  );
2779
2871
 
2780
2872
  export {
@@ -2787,4 +2879,4 @@ export {
2787
2879
  resetDefaultFlagsClient,
2788
2880
  createClient
2789
2881
  };
2790
- //# sourceMappingURL=chunk-P6CH6LXX.js.map
2882
+ //# sourceMappingURL=chunk-TDZBUVBG.js.map