@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.
@@ -4,6 +4,9 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // src/index.default.ts
8
+ import { waitUntil as waitUntil2 } from "@vercel/functions";
9
+
7
10
  // src/controller-fns.ts
8
11
  var controller_fns_exports = {};
9
12
  __export(controller_fns_exports, {
@@ -70,7 +73,7 @@ function getSplitBoundaries(outcome) {
70
73
  function getCompiledRegex(rhs) {
71
74
  const cached = compiledRegexCache.get(rhs);
72
75
  if (cached) return cached;
73
- const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/, ""));
76
+ const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/g, ""));
74
77
  compiledRegexCache.set(rhs, compiled);
75
78
  return compiled;
76
79
  }
@@ -447,7 +450,7 @@ function resolveOutcome(params, outcome) {
447
450
  break;
448
451
  }
449
452
  }
450
- if (exhausted) currentPromille = PROMILLE_SCALE;
453
+ if (exhausted) currentPromille = outcome.finalPromille ?? PROMILLE_SCALE;
451
454
  if (currentPromille <= 0) {
452
455
  return {
453
456
  ...rollFromVariant,
@@ -573,7 +576,7 @@ function bulkEvaluate(flags, shared) {
573
576
  }
574
577
 
575
578
  // package.json
576
- var version = "1.9.0-da0b044-20260915081607";
579
+ var version = "1.9.0";
577
580
 
578
581
  // src/lib/report-value.ts
579
582
  function internalReportValue(key, value, data) {
@@ -792,7 +795,7 @@ async function bulkEvaluate2(id, flags, entities) {
792
795
  }
793
796
 
794
797
  // src/create-raw-client.ts
795
- import { waitUntil } from "@vercel/functions";
798
+ import { waitUntil as defaultWaitUntil } from "@vercel/functions";
796
799
  import { dequal } from "dequal/lite";
797
800
  var idCount = 0;
798
801
  async function performInitialize(instance, initFn) {
@@ -808,9 +811,11 @@ function createCreateRawClient(fns) {
808
811
  return function createRawClient({
809
812
  controller,
810
813
  origin,
811
- experimental_reportExposures
814
+ experimental_reportExposures,
815
+ waitUntil: waitUntil3 = defaultWaitUntil
812
816
  }) {
813
817
  const id = idCount++;
818
+ const pendingExposureReports = /* @__PURE__ */ new Set();
814
819
  controllerInstanceMap.set(id, {
815
820
  controller,
816
821
  initialized: false,
@@ -828,8 +833,10 @@ function createCreateRawClient(fns) {
828
833
  );
829
834
  }
830
835
  })();
836
+ pendingExposureReports.add(pending);
837
+ void pending.finally(() => pendingExposureReports.delete(pending));
831
838
  try {
832
- waitUntil(pending);
839
+ waitUntil3(pending);
833
840
  } catch {
834
841
  }
835
842
  }
@@ -864,6 +871,9 @@ function createCreateRawClient(fns) {
864
871
  },
865
872
  shutdown: async () => {
866
873
  await fns.shutdown(id);
874
+ while (pendingExposureReports.size > 0) {
875
+ await Promise.all(pendingExposureReports);
876
+ }
867
877
  controllerInstanceMap.delete(id);
868
878
  },
869
879
  getDatafile: async () => {
@@ -1028,8 +1038,128 @@ async function readBundledDefinitions(auth) {
1028
1038
  }
1029
1039
 
1030
1040
  // src/utils/ingest.ts
1041
+ import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
1042
+
1043
+ // src/controller/auth.ts
1031
1044
  import { getVercelOidcToken } from "@vercel/oidc";
1032
1045
 
1046
+ // src/utils/sdk-keys.ts
1047
+ var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
1048
+ function isValidSdkKey(value) {
1049
+ return SDK_KEY_REGEX.test(value);
1050
+ }
1051
+ var PROJECT_ID_REGEX = /^[A-Za-z0-9_]{1,64}$/;
1052
+ function isValidProjectId(value) {
1053
+ return PROJECT_ID_REGEX.test(value);
1054
+ }
1055
+ function parseFlagsConnectionString(text) {
1056
+ if (SDK_KEY_REGEX.test(text)) return { sdkKey: text, projectId: null };
1057
+ if (!text.startsWith("flags:")) return null;
1058
+ try {
1059
+ const params = new URLSearchParams(text.slice(6));
1060
+ return {
1061
+ sdkKey: params.get("sdkKey") || null,
1062
+ projectId: params.get("projectId") || null
1063
+ };
1064
+ } catch {
1065
+ return null;
1066
+ }
1067
+ }
1068
+
1069
+ // src/controller/auth.ts
1070
+ var SOURCE_PROJECT_HEADER = "X-Vercel-Flags-Project-Id";
1071
+ function authHeaders(token, sourceProjectId) {
1072
+ return {
1073
+ Authorization: `Bearer ${token}`,
1074
+ ...sourceProjectId ? { [SOURCE_PROJECT_HEADER]: sourceProjectId } : null
1075
+ };
1076
+ }
1077
+ function unauthorizedMessage(sourceProjectId) {
1078
+ if (!sourceProjectId) return "unauthorized (401)";
1079
+ 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.`;
1080
+ }
1081
+ async function getOidcToken() {
1082
+ try {
1083
+ return await getVercelOidcToken();
1084
+ } catch {
1085
+ throw new Error(
1086
+ [
1087
+ "@vercel/flags-core: Failed to get OIDC token.",
1088
+ "Are you running in a Vercel Environment where OIDC tokens are available?",
1089
+ "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
1090
+ ].join(" ")
1091
+ );
1092
+ }
1093
+ }
1094
+ function getProjectIdFromOidcToken(oidcToken) {
1095
+ const tokenParts = oidcToken.split(".");
1096
+ if (tokenParts.length !== 3 || !tokenParts[1]) {
1097
+ throw new Error("@vercel/flags-core: Invalid OIDC token");
1098
+ }
1099
+ const payload = JSON.parse(
1100
+ Buffer.from(tokenParts[1], "base64url").toString("utf8")
1101
+ );
1102
+ if (typeof payload.project_id !== "string" || !payload.project_id) {
1103
+ throw new Error(
1104
+ "@vercel/flags-core: Missing project_id claim in OIDC token"
1105
+ );
1106
+ }
1107
+ return payload.project_id;
1108
+ }
1109
+ var Authentication = class {
1110
+ sdkKey;
1111
+ sourceProjectId;
1112
+ constructor(sdkKeyOrConnectionString) {
1113
+ if (sdkKeyOrConnectionString !== void 0) {
1114
+ if (typeof sdkKeyOrConnectionString !== "string") {
1115
+ throw new Error(
1116
+ `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
1117
+ );
1118
+ }
1119
+ const parsed = parseFlagsConnectionString(sdkKeyOrConnectionString);
1120
+ if (parsed?.sdkKey && parsed.projectId) {
1121
+ throw new Error(
1122
+ "@vercel/flags-core: A connection string must contain either sdkKey or projectId, not both"
1123
+ );
1124
+ }
1125
+ if (parsed?.sdkKey) {
1126
+ if (!isValidSdkKey(parsed.sdkKey)) {
1127
+ throw new Error("@vercel/flags-core: Missing sdkKey");
1128
+ }
1129
+ this.sdkKey = parsed.sdkKey;
1130
+ } else if (parsed?.projectId) {
1131
+ if (!isValidProjectId(parsed.projectId)) {
1132
+ throw new Error(
1133
+ "@vercel/flags-core: Invalid projectId in connection string"
1134
+ );
1135
+ }
1136
+ this.sourceProjectId = parsed.projectId;
1137
+ } else {
1138
+ throw new Error("@vercel/flags-core: Missing sdkKey");
1139
+ }
1140
+ }
1141
+ }
1142
+ async resolveToken() {
1143
+ if (this.sdkKey) {
1144
+ return this.sdkKey;
1145
+ }
1146
+ return await getOidcToken();
1147
+ }
1148
+ async resolveBundledDefinitionsLookup() {
1149
+ if (this.sdkKey) {
1150
+ return { type: "sdk-key", sdkKey: this.sdkKey };
1151
+ }
1152
+ if (this.sourceProjectId) {
1153
+ return { type: "project-id", projectId: this.sourceProjectId };
1154
+ }
1155
+ const oidcToken = await this.resolveToken();
1156
+ return {
1157
+ type: "project-id",
1158
+ projectId: getProjectIdFromOidcToken(oidcToken)
1159
+ };
1160
+ }
1161
+ };
1162
+
1033
1163
  // src/utils/backoff.ts
1034
1164
  var DEFAULT_BASE_MS = 250;
1035
1165
  var DEFAULT_CAP_MS = 5e3;
@@ -1045,6 +1175,20 @@ function getJitteredWaitMs(baseMs, ratio) {
1045
1175
  return Math.floor(min + Math.random() * span);
1046
1176
  }
1047
1177
 
1178
+ // src/utils/runtime-ingest.ts
1179
+ var FLAGS_CONTEXT_SYMBOL = /* @__PURE__ */ Symbol.for("@vercel/flags-context");
1180
+ function getRuntimeIngestFor(auth) {
1181
+ return auth.sourceProjectId ? void 0 : getRuntimeIngest();
1182
+ }
1183
+ function getRuntimeIngest() {
1184
+ try {
1185
+ const context = globalThis[FLAGS_CONTEXT_SYMBOL];
1186
+ return typeof context?.ingest === "function" ? context.ingest : void 0;
1187
+ } catch {
1188
+ return void 0;
1189
+ }
1190
+ }
1191
+
1048
1192
  // src/utils/ingest.ts
1049
1193
  var MAX_RETRIES = 3;
1050
1194
  var MAX_EVENTS_PER_REQUEST = 2e3;
@@ -1058,7 +1202,7 @@ var debugLog = (...args) => {
1058
1202
  async function getEvaluatingOidcToken(auth) {
1059
1203
  if (!auth.sdkKey) return void 0;
1060
1204
  try {
1061
- return await getVercelOidcToken();
1205
+ return await getVercelOidcToken2();
1062
1206
  } catch {
1063
1207
  return void 0;
1064
1208
  }
@@ -1068,7 +1212,7 @@ async function getIngestHeaders(options, flushReason) {
1068
1212
  const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
1069
1213
  return {
1070
1214
  "Content-Type": "application/json",
1071
- Authorization: `Bearer ${token}`,
1215
+ ...authHeaders(token, options.auth.sourceProjectId),
1072
1216
  "User-Agent": `VercelFlagsCore/${version}`,
1073
1217
  [FLUSH_REASON_HEADER]: flushReason,
1074
1218
  ...options.metricEnvironment ?? process.env.VERCEL_ENV ? {
@@ -1078,8 +1222,28 @@ async function getIngestHeaders(options, flushReason) {
1078
1222
  ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1079
1223
  };
1080
1224
  }
1225
+ function getRuntimeIngestHeaders(options, flushReason) {
1226
+ return {
1227
+ "Content-Type": "application/json",
1228
+ ...options.auth.sdkKey ? { Authorization: `Bearer ${options.auth.sdkKey}` } : null,
1229
+ "User-Agent": `VercelFlagsCore/${version}`,
1230
+ [FLUSH_REASON_HEADER]: flushReason,
1231
+ ...options.metricEnvironment ?? process.env.VERCEL_ENV ? {
1232
+ "X-Vercel-Env": options.metricEnvironment ?? process.env.VERCEL_ENV
1233
+ } : null,
1234
+ ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1235
+ };
1236
+ }
1081
1237
  async function sendIngestEvents(options, events, flushId, flushReason) {
1082
- const eventsToSend = events.map((event) => event.ingestEvent());
1238
+ let eventsToSend = events.map((event) => event.ingestEvent());
1239
+ const runtimeIngest = getRuntimeIngestFor(options.auth);
1240
+ if (runtimeIngest) {
1241
+ const headers = getRuntimeIngestHeaders(options, flushReason);
1242
+ eventsToSend = eventsToSend.filter(
1243
+ (event) => !runtimeIngest({ headers, body: [event] })
1244
+ );
1245
+ if (eventsToSend.length === 0) return;
1246
+ }
1083
1247
  for (let i = 0; i < eventsToSend.length; i += MAX_EVENTS_PER_REQUEST) {
1084
1248
  await sendIngestChunk(
1085
1249
  options,
@@ -1143,15 +1307,17 @@ function getRequestContext() {
1143
1307
  }
1144
1308
 
1145
1309
  // src/utils/scheduler.ts
1146
- import { waitUntil as waitUntil2 } from "@vercel/functions";
1310
+ import { waitUntil } from "@vercel/functions";
1147
1311
  var IDLE_FLUSH_WAIT_MS = 5e3;
1148
1312
  var IDLE_FLUSH_JITTER_RATIO = 0.2;
1149
1313
  var MAX_FLUSH_WAIT_MS = 6e4;
1150
1314
  var Scheduler = class {
1151
- constructor(onFlush) {
1315
+ constructor(onFlush, scheduleTask = waitUntil) {
1152
1316
  this.onFlush = onFlush;
1317
+ this.scheduleTask = scheduleTask;
1153
1318
  }
1154
1319
  onFlush;
1320
+ scheduleTask;
1155
1321
  resolveWait = null;
1156
1322
  pending = null;
1157
1323
  idleTimeout = null;
@@ -1166,7 +1332,7 @@ var Scheduler = class {
1166
1332
  await this.onFlush(reason);
1167
1333
  })();
1168
1334
  try {
1169
- waitUntil2(this.pending);
1335
+ this.scheduleTask(this.pending);
1170
1336
  } catch {
1171
1337
  }
1172
1338
  this.maxTimeout = setTimeout(
@@ -1313,12 +1479,18 @@ var UsageTracker = class {
1313
1479
  flushCount = 0;
1314
1480
  options;
1315
1481
  scheduler;
1482
+ waitUntil;
1483
+ inflightFlushes = /* @__PURE__ */ new Set();
1316
1484
  trackedRequests = /* @__PURE__ */ new WeakSet();
1317
1485
  readEvents = [];
1318
1486
  evaluationEvents = /* @__PURE__ */ new Map();
1319
1487
  constructor(options) {
1320
1488
  this.options = options;
1321
- this.scheduler = new Scheduler((reason) => this.flushEvents(reason));
1489
+ this.waitUntil = options.waitUntil;
1490
+ this.scheduler = new Scheduler(
1491
+ (reason) => this.flushEvents(reason),
1492
+ options.waitUntil
1493
+ );
1322
1494
  }
1323
1495
  /**
1324
1496
  * Triggers an immediate flush of any pending events.
@@ -1327,6 +1499,7 @@ var UsageTracker = class {
1327
1499
  async shutdown() {
1328
1500
  await this.scheduler.shutdown();
1329
1501
  await this.flushEvents("shutdown");
1502
+ await Promise.all([...this.inflightFlushes]);
1330
1503
  }
1331
1504
  /**
1332
1505
  * Tracks a config read event. Deduplicates by request context.
@@ -1338,7 +1511,7 @@ var UsageTracker = class {
1338
1511
  if (this.trackedRequests.has(ctx)) return;
1339
1512
  this.trackedRequests.add(ctx);
1340
1513
  this.readEvents.push(new FlagsConfigReadEvent(headers, options));
1341
- this.scheduler.scheduleFlush();
1514
+ this.requestFlush();
1342
1515
  } catch (error) {
1343
1516
  console.error("@vercel/flags-core: Failed to record event:", error);
1344
1517
  }
@@ -1362,7 +1535,7 @@ var UsageTracker = class {
1362
1535
  new FlagsEvaluationEvent(bucketedOptions)
1363
1536
  );
1364
1537
  }
1365
- this.scheduler.scheduleFlush();
1538
+ this.requestFlush();
1366
1539
  } catch (error) {
1367
1540
  console.error(
1368
1541
  "@vercel/flags-core: Failed to record evaluation event:",
@@ -1370,6 +1543,25 @@ var UsageTracker = class {
1370
1543
  );
1371
1544
  }
1372
1545
  }
1546
+ /**
1547
+ * Flushes immediately when the runtime provides an ingest transport,
1548
+ * otherwise falls back to the time-based scheduler.
1549
+ */
1550
+ requestFlush() {
1551
+ if (getRuntimeIngestFor(this.options.auth)) {
1552
+ const flush = this.flushEvents("immediate").catch((error) => {
1553
+ console.error("@vercel/flags-core: Failed to flush events:", error);
1554
+ });
1555
+ this.inflightFlushes.add(flush);
1556
+ void flush.finally(() => this.inflightFlushes.delete(flush));
1557
+ try {
1558
+ this.waitUntil?.(flush);
1559
+ } catch {
1560
+ }
1561
+ } else {
1562
+ this.scheduler.scheduleFlush();
1563
+ }
1564
+ }
1373
1565
  /**
1374
1566
  * Send all events to the ingest service
1375
1567
  */
@@ -1457,169 +1649,94 @@ var BundledSource = class {
1457
1649
 
1458
1650
  // src/controller/fetch-datafile.ts
1459
1651
  var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
1652
+ var DEFAULT_MAX_ATTEMPTS = 3;
1653
+ var DatafileHttpError = class extends Error {
1654
+ constructor(status, statusText) {
1655
+ super(`Failed to fetch data: ${statusText}`);
1656
+ this.status = status;
1657
+ }
1658
+ status;
1659
+ get retryable() {
1660
+ return this.status === 408 || this.status === 429 || this.status >= 500;
1661
+ }
1662
+ };
1460
1663
  async function fetchDatafile(options) {
1461
- const token = await options.auth.resolveToken();
1664
+ options.signal?.throwIfAborted();
1665
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
1666
+ if (!Number.isInteger(maxAttempts) || maxAttempts < 1) {
1667
+ throw new Error("maxAttempts must be a positive integer");
1668
+ }
1462
1669
  const controller = new AbortController();
1670
+ const { signal } = controller;
1671
+ let rejectAbort;
1672
+ const aborted = new Promise((_, reject) => {
1673
+ rejectAbort = reject;
1674
+ });
1675
+ const onAbort = () => rejectAbort(signal.reason);
1676
+ signal.addEventListener("abort", onAbort, { once: true });
1677
+ const onExternalAbort = () => controller.abort(options.signal?.reason);
1678
+ options.signal?.addEventListener("abort", onExternalAbort, { once: true });
1463
1679
  const timeoutId = setTimeout(
1464
- () => controller.abort(),
1680
+ () => controller.abort(
1681
+ new Error("@vercel/flags-core: Datafile fetch deadline exceeded")
1682
+ ),
1465
1683
  DEFAULT_FETCH_TIMEOUT_MS
1466
1684
  );
1467
- const onExternalAbort = () => controller.abort();
1468
- if (options.signal) {
1469
- if (options.signal.aborted) {
1470
- clearTimeout(timeoutId);
1471
- throw new Error("Fetch aborted");
1472
- }
1473
- options.signal.addEventListener("abort", onExternalAbort, { once: true });
1474
- }
1475
- try {
1685
+ let delay;
1686
+ const fetchAttempt = async () => {
1687
+ const token = await options.auth.resolveToken();
1688
+ signal.throwIfAborted();
1476
1689
  const res = await options.fetch(`${options.host}/v1/datafile`, {
1477
1690
  headers: {
1478
- Authorization: `Bearer ${token}`,
1691
+ ...authHeaders(token, options.auth.sourceProjectId),
1479
1692
  "User-Agent": `VercelFlagsCore/${version}`,
1480
1693
  ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null
1481
1694
  },
1482
- signal: controller.signal
1695
+ signal
1483
1696
  });
1484
- clearTimeout(timeoutId);
1485
- options.signal?.removeEventListener("abort", onExternalAbort);
1697
+ signal.throwIfAborted();
1486
1698
  if (!res.ok) {
1487
- throw new Error(`Failed to fetch data: ${res.statusText}`);
1488
- }
1489
- return res.json();
1490
- } catch (error) {
1491
- clearTimeout(timeoutId);
1492
- options.signal?.removeEventListener("abort", onExternalAbort);
1493
- throw error instanceof Error ? error : new Error("Unknown fetch error");
1494
- }
1495
- }
1496
-
1497
- // src/controller/header-source.ts
1498
- import { waitUntil as waitUntil3 } from "@vercel/functions";
1499
-
1500
- // src/controller/tagged-data.ts
1501
- function tagData(data, origin) {
1502
- return Object.assign(data, { _origin: origin });
1503
- }
1504
- function originToMetricsSource(origin) {
1505
- switch (origin) {
1506
- case "stream":
1507
- case "poll":
1508
- case "provided":
1509
- return "in-memory";
1510
- case "fetched":
1511
- return "remote";
1512
- case "bundled":
1513
- return "embedded";
1514
- }
1515
- }
1516
-
1517
- // src/controller/typed-emitter.ts
1518
- var TypedEmitter = class {
1519
- handlers = /* @__PURE__ */ new Map();
1520
- on(event, handler) {
1521
- let set = this.handlers.get(event);
1522
- if (!set) {
1523
- set = /* @__PURE__ */ new Set();
1524
- this.handlers.set(event, set);
1525
- }
1526
- set.add(handler);
1527
- }
1528
- off(event, handler) {
1529
- this.handlers.get(event)?.delete(handler);
1530
- }
1531
- emit(event, ...args) {
1532
- const set = this.handlers.get(event);
1533
- if (set) {
1534
- for (const handler of set) {
1535
- handler(...args);
1536
- }
1699
+ void res.body?.cancel().catch(() => {
1700
+ });
1701
+ throw new DatafileHttpError(
1702
+ res.status,
1703
+ res.status === 401 && options.auth.sourceProjectId ? unauthorizedMessage(options.auth.sourceProjectId) : res.statusText
1704
+ );
1537
1705
  }
1538
- }
1539
- };
1540
-
1541
- // src/controller/header-source.ts
1542
- var HeaderSource = class extends TypedEmitter {
1543
- options;
1544
- abortController;
1545
- promise;
1546
- constructor(options) {
1547
- super();
1548
- this.options = options;
1549
- }
1550
- fetchDatafile() {
1551
- if (this.promise) return this.promise;
1552
- const abortController = new AbortController();
1553
- this.abortController = abortController;
1554
- this.promise = fetchDatafile({
1555
- ...this.options,
1556
- signal: abortController.signal
1557
- }).then((data) => {
1558
- abortController.signal.throwIfAborted();
1559
- this.emit("data", data);
1560
- return data;
1561
- }).finally(() => {
1562
- if (this.abortController === abortController) {
1563
- this.promise = void 0;
1564
- this.abortController = void 0;
1706
+ const data = await res.json();
1707
+ signal.throwIfAborted();
1708
+ return data;
1709
+ };
1710
+ try {
1711
+ for (let attempt = 0; ; attempt++) {
1712
+ if (attempt > 0) {
1713
+ await Promise.race([
1714
+ new Promise((resolve) => {
1715
+ delay = setTimeout(resolve, 100 * 2 ** (attempt - 1));
1716
+ }),
1717
+ aborted
1718
+ ]);
1565
1719
  }
1566
- });
1567
- return this.promise;
1568
- }
1569
- getUpdatedAtHeader(projectId) {
1570
- const ctx = getRequestContext();
1571
- const header = ctx.headers?.["x-vercel-flags-config-versions"] ?? ctx.headers?.["flags-config-versions"];
1572
- if (!header) {
1573
- return;
1574
- }
1575
- const prefix = `flags_${projectId}=`;
1576
- const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1577
- const timestamp = Number(value);
1578
- return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : void 0;
1579
- }
1580
- async resolveData(currentData) {
1581
- if (!currentData.configUpdatedAt) {
1582
- return;
1583
- }
1584
- const updatedAtHeader = this.getUpdatedAtHeader(currentData.projectId);
1585
- if (!updatedAtHeader) {
1586
- return;
1587
- }
1588
- const currentUpdatedAt = Number(currentData.configUpdatedAt);
1589
- if (updatedAtHeader <= currentUpdatedAt) {
1590
- return [currentData, "HIT"];
1591
- }
1592
- if (updatedAtHeader <= currentUpdatedAt + 1e4) {
1593
- const pending = this.fetchDatafile();
1594
- const signal = this.abortController?.signal;
1595
- const background = pending.catch((error) => {
1596
- if (!signal?.aborted) {
1597
- console.error("@vercel/flags-core: Header refresh failed:", error);
1720
+ signal.throwIfAborted();
1721
+ try {
1722
+ return await Promise.race([fetchAttempt(), aborted]);
1723
+ } catch (error) {
1724
+ signal.throwIfAborted();
1725
+ if (attempt === maxAttempts - 1 || error instanceof DatafileHttpError && !error.retryable) {
1726
+ throw error instanceof Error ? error : new Error("Unknown fetch error");
1598
1727
  }
1599
- });
1600
- waitUntil3(background);
1601
- return [currentData, "STALE"];
1728
+ }
1602
1729
  }
1603
- const data = await this.fetchDatafile();
1604
- return [tagData(data, "fetched"), "MISS"];
1605
- }
1606
- read(currentData) {
1607
- return this.resolveData(currentData);
1608
- }
1609
- isAvailable(projectId) {
1610
- return !!this.getUpdatedAtHeader(projectId);
1611
- }
1612
- /**
1613
- * Abort the current header-driven fetch and discard its pending work.
1614
- */
1615
- stop() {
1616
- this.abortController?.abort();
1617
- this.abortController = void 0;
1618
- this.promise = void 0;
1730
+ } finally {
1731
+ clearTimeout(timeoutId);
1732
+ clearTimeout(delay);
1733
+ signal.removeEventListener("abort", onAbort);
1734
+ options.signal?.removeEventListener("abort", onExternalAbort);
1619
1735
  }
1620
- };
1736
+ }
1621
1737
 
1622
1738
  // src/controller/normalized-options.ts
1739
+ import { waitUntil as defaultWaitUntil2 } from "@vercel/functions";
1623
1740
  var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1624
1741
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
1625
1742
  var MIN_POLLING_INTERVAL_MS = 3e4;
@@ -1663,6 +1780,7 @@ function normalizeOptions(options) {
1663
1780
  polling,
1664
1781
  buildStep,
1665
1782
  fetch: options.fetch ?? globalThis.fetch,
1783
+ waitUntil: options.waitUntil ?? defaultWaitUntil2,
1666
1784
  host: "https://flags.vercel.com",
1667
1785
  metricEnvironment: options.metricEnvironment,
1668
1786
  clientName: options.clientName,
@@ -1670,6 +1788,35 @@ function normalizeOptions(options) {
1670
1788
  };
1671
1789
  }
1672
1790
 
1791
+ // src/controller/typed-emitter.ts
1792
+ var TypedEmitter = class {
1793
+ handlers = /* @__PURE__ */ new Map();
1794
+ on(event, handler) {
1795
+ let set = this.handlers.get(event);
1796
+ if (!set) {
1797
+ set = /* @__PURE__ */ new Set();
1798
+ this.handlers.set(event, set);
1799
+ }
1800
+ set.add(handler);
1801
+ }
1802
+ off(event, handler) {
1803
+ const set = this.handlers.get(event);
1804
+ if (!set) return;
1805
+ set.delete(handler);
1806
+ if (set.size === 0) {
1807
+ this.handlers.delete(event);
1808
+ }
1809
+ }
1810
+ emit(event, ...args) {
1811
+ const set = this.handlers.get(event);
1812
+ if (set) {
1813
+ for (const handler of set) {
1814
+ handler(...args);
1815
+ }
1816
+ }
1817
+ }
1818
+ };
1819
+
1673
1820
  // src/controller/polling-source.ts
1674
1821
  var PollingSource = class extends TypedEmitter {
1675
1822
  config;
@@ -1685,13 +1832,16 @@ var PollingSource = class extends TypedEmitter {
1685
1832
  */
1686
1833
  async poll() {
1687
1834
  if (this.abortController?.signal.aborted) return;
1835
+ this.abortController ??= new AbortController();
1836
+ const controller = this.abortController;
1688
1837
  try {
1689
1838
  const data = await fetchDatafile({
1690
1839
  ...this.config,
1691
- signal: this.abortController?.signal
1840
+ signal: controller.signal
1692
1841
  });
1693
1842
  this.emit("data", data);
1694
1843
  } catch (error) {
1844
+ controller.signal.throwIfAborted();
1695
1845
  const err = error instanceof Error ? error : new Error("Unknown poll error");
1696
1846
  this.emit("error", err);
1697
1847
  }
@@ -1703,9 +1853,9 @@ var PollingSource = class extends TypedEmitter {
1703
1853
  */
1704
1854
  startInterval() {
1705
1855
  if (this.intervalId) return;
1706
- this.abortController = new AbortController();
1707
1856
  this.intervalId = setInterval(
1708
- () => void this.poll(),
1857
+ () => void this.poll().catch(() => {
1858
+ }),
1709
1859
  this.config.polling.intervalMs
1710
1860
  );
1711
1861
  }
@@ -1746,8 +1896,8 @@ function backoff(retryCount) {
1746
1896
  return delay + Math.random() * 1e3;
1747
1897
  }
1748
1898
  var UnauthorizedError = class extends Error {
1749
- constructor() {
1750
- super("stream: unauthorized (401)");
1899
+ constructor(sourceProjectId) {
1900
+ super(`stream: ${unauthorizedMessage(sourceProjectId)}`);
1751
1901
  this.name = "UnauthorizedError";
1752
1902
  }
1753
1903
  };
@@ -1807,7 +1957,7 @@ async function connectStream(config, callbacks) {
1807
1957
  throw new TokenResolutionError(error);
1808
1958
  });
1809
1959
  const headers = {
1810
- Authorization: `Bearer ${token}`,
1960
+ ...authHeaders(token, config.sourceProjectId),
1811
1961
  "User-Agent": `VercelFlagsCore/${version}`,
1812
1962
  "X-Retry-Attempt": String(retryCount)
1813
1963
  };
@@ -1829,7 +1979,7 @@ async function connectStream(config, callbacks) {
1829
1979
  if (!response.ok) {
1830
1980
  if (response.status === 401) {
1831
1981
  if (!initialDataReceived) {
1832
- rejectInit(new UnauthorizedError());
1982
+ rejectInit(new UnauthorizedError(config.sourceProjectId));
1833
1983
  }
1834
1984
  abortController.abort();
1835
1985
  break;
@@ -1973,6 +2123,7 @@ var StreamSource = class extends TypedEmitter {
1973
2123
  {
1974
2124
  host: this.options.host,
1975
2125
  resolveToken: () => this.options.auth.resolveToken(),
2126
+ sourceProjectId: this.options.auth.sourceProjectId,
1976
2127
  abortController,
1977
2128
  fetch: this.options.fetch,
1978
2129
  revision: this.revision
@@ -2009,6 +2160,23 @@ var StreamSource = class extends TypedEmitter {
2009
2160
  }
2010
2161
  };
2011
2162
 
2163
+ // src/controller/tagged-data.ts
2164
+ function tagData(data, origin) {
2165
+ return Object.assign(data, { _origin: origin });
2166
+ }
2167
+ function originToMetricsSource(origin) {
2168
+ switch (origin) {
2169
+ case "stream":
2170
+ case "poll":
2171
+ case "provided":
2172
+ return "in-memory";
2173
+ case "fetched":
2174
+ return "remote";
2175
+ case "bundled":
2176
+ return "embedded";
2177
+ }
2178
+ }
2179
+
2012
2180
  // src/controller/index.ts
2013
2181
  function parseConfigUpdatedAt(value) {
2014
2182
  if (typeof value === "number") return value;
@@ -2033,14 +2201,14 @@ var Controller = class {
2033
2201
  streamSource;
2034
2202
  pollingSource;
2035
2203
  bundledSource;
2036
- headerSource;
2037
2204
  // Usage tracking
2038
2205
  usageTracker;
2039
2206
  isFirstGetData = true;
2040
2207
  // Build-step deduplication
2041
2208
  buildDataPromise = null;
2042
2209
  buildReadTracked = false;
2043
- // Suppresses usage tracking when the SDK key is unauthorized
2210
+ // Suppresses usage tracking while the credential is rejected. Set on a 401
2211
+ // from any source, cleared as soon as a source delivers data again.
2044
2212
  unauthorized = false;
2045
2213
  constructor(options) {
2046
2214
  this.options = normalizeOptions(options);
@@ -2053,7 +2221,6 @@ var Controller = class {
2053
2221
  auth: this.options.auth,
2054
2222
  readBundledDefinitions
2055
2223
  });
2056
- this.headerSource = new HeaderSource(this.options);
2057
2224
  this.wireSourceEvents();
2058
2225
  if (this.options.datafile) {
2059
2226
  this.data = tagData(this.options.datafile, "provided");
@@ -2062,11 +2229,13 @@ var Controller = class {
2062
2229
  }
2063
2230
  // Source event handlers (stored for cleanup)
2064
2231
  onStreamData = (data) => {
2232
+ this.unauthorized = false;
2065
2233
  if (this.isNewerData(data)) {
2066
2234
  this.data = tagData(data, "stream");
2067
2235
  }
2068
2236
  };
2069
2237
  onStreamPrimed = () => {
2238
+ this.unauthorized = false;
2070
2239
  if (this.state === "degraded" || this.state === "initializing:stream") {
2071
2240
  this.transition("streaming");
2072
2241
  }
@@ -2082,18 +2251,15 @@ var Controller = class {
2082
2251
  }
2083
2252
  };
2084
2253
  onPollData = (data) => {
2254
+ this.unauthorized = false;
2085
2255
  if (this.isNewerData(data)) {
2086
2256
  this.data = tagData(data, "poll");
2087
2257
  }
2088
2258
  };
2089
2259
  onPollError = (error) => {
2260
+ this.noteUnauthorized(error);
2090
2261
  console.error("@vercel/flags-core: Poll failed:", error);
2091
2262
  };
2092
- onFetchedData = (data) => {
2093
- if (this.isNewerData(data)) {
2094
- this.data = tagData(data, "fetched");
2095
- }
2096
- };
2097
2263
  // ---------------------------------------------------------------------------
2098
2264
  // Source event wiring
2099
2265
  // ---------------------------------------------------------------------------
@@ -2104,7 +2270,6 @@ var Controller = class {
2104
2270
  this.streamSource.on("disconnected", this.onStreamDisconnected);
2105
2271
  this.pollingSource.on("data", this.onPollData);
2106
2272
  this.pollingSource.on("error", this.onPollError);
2107
- this.headerSource.on("data", this.onFetchedData);
2108
2273
  }
2109
2274
  unwireSourceEvents() {
2110
2275
  this.streamSource.off("data", this.onStreamData);
@@ -2113,7 +2278,6 @@ var Controller = class {
2113
2278
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2114
2279
  this.pollingSource.off("data", this.onPollData);
2115
2280
  this.pollingSource.off("error", this.onPollError);
2116
- this.headerSource.off("data", this.onFetchedData);
2117
2281
  }
2118
2282
  // ---------------------------------------------------------------------------
2119
2283
  // State machine
@@ -2131,8 +2295,6 @@ var Controller = class {
2131
2295
  return "streaming";
2132
2296
  case "polling":
2133
2297
  return "polling";
2134
- case "vercel":
2135
- return "vercel";
2136
2298
  default:
2137
2299
  return "offline";
2138
2300
  }
@@ -2168,9 +2330,7 @@ var Controller = class {
2168
2330
  }
2169
2331
  }
2170
2332
  if (this.data) {
2171
- if (this.headerSource.isAvailable(this.data.projectId)) {
2172
- this.transition("vercel");
2173
- } else if (this.options.stream.enabled) {
2333
+ if (this.options.stream.enabled) {
2174
2334
  this.transition("initializing:stream");
2175
2335
  await this.tryInitializeStream();
2176
2336
  } else if (this.options.polling.enabled) {
@@ -2233,7 +2393,6 @@ var Controller = class {
2233
2393
  this.unwireSourceEvents();
2234
2394
  this.streamSource.stop();
2235
2395
  this.pollingSource.stop();
2236
- this.headerSource.stop();
2237
2396
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2238
2397
  this.transition("shutdown");
2239
2398
  await this.usageTracker.shutdown();
@@ -2269,9 +2428,10 @@ var Controller = class {
2269
2428
  this.data = tagData(fetched, "fetched");
2270
2429
  result = this.data;
2271
2430
  cacheStatus = "MISS";
2272
- } catch {
2273
- throw new Error(
2274
- "@vercel/flags-core: No flag definitions available. Initialize the client or provide a datafile."
2431
+ } catch (error) {
2432
+ this.noteUnauthorized(error);
2433
+ throw this.noDefinitionsError(
2434
+ ". Initialize the client or provide a datafile."
2275
2435
  );
2276
2436
  }
2277
2437
  }
@@ -2315,12 +2475,6 @@ var Controller = class {
2315
2475
  return this.resolveDataForBuildStep();
2316
2476
  }
2317
2477
  if (this.data) {
2318
- if (this.mode === "vercel") {
2319
- const data = await this.headerSource.read(this.data);
2320
- if (data) {
2321
- return data;
2322
- }
2323
- }
2324
2478
  const cacheStatus = this.isConnected ? "HIT" : "STALE";
2325
2479
  return [this.data, cacheStatus];
2326
2480
  }
@@ -2339,9 +2493,7 @@ var Controller = class {
2339
2493
  await this.streamSource.start();
2340
2494
  return true;
2341
2495
  } catch (error) {
2342
- if (error instanceof UnauthorizedError) {
2343
- this.unauthorized = true;
2344
- }
2496
+ this.noteUnauthorized(error);
2345
2497
  return false;
2346
2498
  }
2347
2499
  }
@@ -2369,9 +2521,7 @@ var Controller = class {
2369
2521
  return true;
2370
2522
  } catch (error) {
2371
2523
  clearTimeout(timeoutId);
2372
- if (error instanceof Error && error.message.includes("401")) {
2373
- this.unauthorized = true;
2374
- }
2524
+ this.noteUnauthorized(error);
2375
2525
  return false;
2376
2526
  }
2377
2527
  }
@@ -2424,6 +2574,11 @@ var Controller = class {
2424
2574
  return false;
2425
2575
  }
2426
2576
  }
2577
+ noteUnauthorized(error) {
2578
+ if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("401")) {
2579
+ this.unauthorized = true;
2580
+ }
2581
+ }
2427
2582
  // ---------------------------------------------------------------------------
2428
2583
  // Build step helpers
2429
2584
  // ---------------------------------------------------------------------------
@@ -2469,10 +2624,11 @@ var Controller = class {
2469
2624
  fetch: this.options.fetch
2470
2625
  });
2471
2626
  return tagData(fetched, "fetched");
2472
- } catch {
2627
+ } catch (error) {
2628
+ this.noteUnauthorized(error);
2473
2629
  }
2474
- throw new Error(
2475
- "@vercel/flags-core: No flag definitions available during build. Provide a datafile or bundled definitions."
2630
+ throw this.noDefinitionsError(
2631
+ " during build. Provide a datafile or bundled definitions."
2476
2632
  );
2477
2633
  }
2478
2634
  // ---------------------------------------------------------------------------
@@ -2506,8 +2662,17 @@ var Controller = class {
2506
2662
  } catch {
2507
2663
  }
2508
2664
  }
2509
- throw new Error(
2510
- "@vercel/flags-core: No flag definitions available. Bundled definitions not found."
2665
+ throw this.noDefinitionsError(". Bundled definitions not found.");
2666
+ }
2667
+ /**
2668
+ * `detail` continues the sentence "No flag definitions available", so it
2669
+ * starts with either "." or " during build.".
2670
+ */
2671
+ noDefinitionsError(detail) {
2672
+ const { sourceProjectId } = this.options.auth;
2673
+ const reason = this.unauthorized && sourceProjectId ? ` Request was ${unauthorizedMessage(sourceProjectId)}` : "";
2674
+ return new Error(
2675
+ `@vercel/flags-core: No flag definitions available${detail}${reason}`
2511
2676
  );
2512
2677
  }
2513
2678
  /**
@@ -2558,8 +2723,8 @@ var Controller = class {
2558
2723
  } catch {
2559
2724
  }
2560
2725
  }
2561
- throw new Error(
2562
- "@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
2726
+ throw this.noDefinitionsError(
2727
+ ". Provide a datafile or bundled definitions."
2563
2728
  );
2564
2729
  }
2565
2730
  // ---------------------------------------------------------------------------
@@ -2632,90 +2797,8 @@ var Controller = class {
2632
2797
  }
2633
2798
  };
2634
2799
 
2635
- // src/controller/auth.ts
2636
- import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
2637
-
2638
- // src/utils/sdk-keys.ts
2639
- var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
2640
- function parseSdkKeyFromFlagsConnectionString(text) {
2641
- if (SDK_KEY_REGEX.test(text)) return text;
2642
- try {
2643
- if (!text.startsWith("flags:")) return null;
2644
- const params = new URLSearchParams(text.slice(6));
2645
- const sdkKey = params.get("sdkKey");
2646
- if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) return sdkKey;
2647
- } catch {
2648
- }
2649
- return null;
2650
- }
2651
-
2652
- // src/controller/auth.ts
2653
- async function getOidcToken() {
2654
- try {
2655
- return await getVercelOidcToken2();
2656
- } catch {
2657
- throw new Error(
2658
- [
2659
- "@vercel/flags-core: Failed to get OIDC token.",
2660
- "Are you running in a Vercel Environment where OIDC tokens are available?",
2661
- "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
2662
- ].join(" ")
2663
- );
2664
- }
2665
- }
2666
- function getProjectIdFromOidcToken(oidcToken) {
2667
- const tokenParts = oidcToken.split(".");
2668
- if (tokenParts.length !== 3 || !tokenParts[1]) {
2669
- throw new Error("@vercel/flags-core: Invalid OIDC token");
2670
- }
2671
- const payload = JSON.parse(
2672
- Buffer.from(tokenParts[1], "base64url").toString("utf8")
2673
- );
2674
- if (typeof payload.project_id !== "string" || !payload.project_id) {
2675
- throw new Error(
2676
- "@vercel/flags-core: Missing project_id claim in OIDC token"
2677
- );
2678
- }
2679
- return payload.project_id;
2680
- }
2681
- var Authentication = class {
2682
- sdkKey;
2683
- constructor(sdkKeyOrConnectionString) {
2684
- if (sdkKeyOrConnectionString !== void 0) {
2685
- if (typeof sdkKeyOrConnectionString !== "string") {
2686
- throw new Error(
2687
- `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
2688
- );
2689
- }
2690
- const parsed = parseSdkKeyFromFlagsConnectionString(
2691
- sdkKeyOrConnectionString
2692
- );
2693
- if (!parsed) {
2694
- throw new Error("@vercel/flags-core: Missing sdkKey");
2695
- }
2696
- this.sdkKey = parsed;
2697
- }
2698
- }
2699
- async resolveToken() {
2700
- if (this.sdkKey) {
2701
- return this.sdkKey;
2702
- }
2703
- return await getOidcToken();
2704
- }
2705
- async resolveBundledDefinitionsLookup() {
2706
- if (this.sdkKey) {
2707
- return { type: "sdk-key", sdkKey: this.sdkKey };
2708
- }
2709
- const oidcToken = await this.resolveToken();
2710
- return {
2711
- type: "project-id",
2712
- projectId: getProjectIdFromOidcToken(oidcToken)
2713
- };
2714
- }
2715
- };
2716
-
2717
2800
  // src/index.make.ts
2718
- function make(createRawClient) {
2801
+ function make(createRawClient, defaults) {
2719
2802
  let _defaultFlagsClient = null;
2720
2803
  function createClient2(sdkKeyOrConnectionStringOrOptions, options) {
2721
2804
  const optionsOnly = typeof sdkKeyOrConnectionStringOrOptions === "object" && sdkKeyOrConnectionStringOrOptions !== null;
@@ -2723,10 +2806,20 @@ function make(createRawClient) {
2723
2806
  const createClientOptions = optionsOnly ? sdkKeyOrConnectionStringOrOptions : options;
2724
2807
  const { experimental_reportExposures, ...controllerOptions } = createClientOptions ?? {};
2725
2808
  const auth = new Authentication(sdkKeyOrConnectionString);
2726
- const controller = new Controller({ auth, ...controllerOptions });
2809
+ const waitUntil3 = controllerOptions.waitUntil ?? defaults.waitUntil;
2810
+ const controller = new Controller({
2811
+ auth,
2812
+ ...controllerOptions,
2813
+ waitUntil: waitUntil3
2814
+ });
2727
2815
  return createRawClient({
2728
2816
  controller,
2729
- origin: { provider: "vercel", sdkKey: auth.sdkKey },
2817
+ origin: {
2818
+ provider: "vercel",
2819
+ sdkKey: auth.sdkKey,
2820
+ ...auth.sourceProjectId ? { projectId: auth.sourceProjectId } : null
2821
+ },
2822
+ waitUntil: waitUntil3,
2730
2823
  ...experimental_reportExposures ? { experimental_reportExposures } : {}
2731
2824
  });
2732
2825
  }
@@ -2765,7 +2858,7 @@ var {
2765
2858
  * Create a flags client using an SDK key, connection string, or Vercel OIDC.
2766
2859
  */
2767
2860
  createClient
2768
- } = make(createCreateRawClient(controller_fns_exports));
2861
+ } = make(createCreateRawClient(controller_fns_exports), { waitUntil: waitUntil2 });
2769
2862
 
2770
2863
  export {
2771
2864
  ResolutionReason,
@@ -2777,4 +2870,4 @@ export {
2777
2870
  resetDefaultFlagsClient,
2778
2871
  createClient
2779
2872
  };
2780
- //# sourceMappingURL=chunk-4MFMRMFV.js.map
2873
+ //# sourceMappingURL=chunk-SLNLPFKU.js.map