@vercel/flags-core 1.9.0-ea4d7de-20260922121332 → 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.
@@ -73,7 +73,7 @@ function getSplitBoundaries(outcome) {
73
73
  function getCompiledRegex(rhs) {
74
74
  const cached = compiledRegexCache.get(rhs);
75
75
  if (cached) return cached;
76
- const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/, ""));
76
+ const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/g, ""));
77
77
  compiledRegexCache.set(rhs, compiled);
78
78
  return compiled;
79
79
  }
@@ -450,7 +450,7 @@ function resolveOutcome(params, outcome) {
450
450
  break;
451
451
  }
452
452
  }
453
- if (exhausted) currentPromille = PROMILLE_SCALE;
453
+ if (exhausted) currentPromille = outcome.finalPromille ?? PROMILLE_SCALE;
454
454
  if (currentPromille <= 0) {
455
455
  return {
456
456
  ...rollFromVariant,
@@ -576,7 +576,7 @@ function bulkEvaluate(flags, shared) {
576
576
  }
577
577
 
578
578
  // package.json
579
- var version = "1.9.0-ea4d7de-20260922121332";
579
+ var version = "1.9.0";
580
580
 
581
581
  // src/lib/report-value.ts
582
582
  function internalReportValue(key, value, data) {
@@ -1038,8 +1038,128 @@ async function readBundledDefinitions(auth) {
1038
1038
  }
1039
1039
 
1040
1040
  // src/utils/ingest.ts
1041
+ import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
1042
+
1043
+ // src/controller/auth.ts
1041
1044
  import { getVercelOidcToken } from "@vercel/oidc";
1042
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
+
1043
1163
  // src/utils/backoff.ts
1044
1164
  var DEFAULT_BASE_MS = 250;
1045
1165
  var DEFAULT_CAP_MS = 5e3;
@@ -1055,6 +1175,20 @@ function getJitteredWaitMs(baseMs, ratio) {
1055
1175
  return Math.floor(min + Math.random() * span);
1056
1176
  }
1057
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
+
1058
1192
  // src/utils/ingest.ts
1059
1193
  var MAX_RETRIES = 3;
1060
1194
  var MAX_EVENTS_PER_REQUEST = 2e3;
@@ -1068,7 +1202,7 @@ var debugLog = (...args) => {
1068
1202
  async function getEvaluatingOidcToken(auth) {
1069
1203
  if (!auth.sdkKey) return void 0;
1070
1204
  try {
1071
- return await getVercelOidcToken();
1205
+ return await getVercelOidcToken2();
1072
1206
  } catch {
1073
1207
  return void 0;
1074
1208
  }
@@ -1078,7 +1212,7 @@ async function getIngestHeaders(options, flushReason) {
1078
1212
  const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
1079
1213
  return {
1080
1214
  "Content-Type": "application/json",
1081
- Authorization: `Bearer ${token}`,
1215
+ ...authHeaders(token, options.auth.sourceProjectId),
1082
1216
  "User-Agent": `VercelFlagsCore/${version}`,
1083
1217
  [FLUSH_REASON_HEADER]: flushReason,
1084
1218
  ...options.metricEnvironment ?? process.env.VERCEL_ENV ? {
@@ -1088,8 +1222,28 @@ async function getIngestHeaders(options, flushReason) {
1088
1222
  ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1089
1223
  };
1090
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
+ }
1091
1237
  async function sendIngestEvents(options, events, flushId, flushReason) {
1092
- 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
+ }
1093
1247
  for (let i = 0; i < eventsToSend.length; i += MAX_EVENTS_PER_REQUEST) {
1094
1248
  await sendIngestChunk(
1095
1249
  options,
@@ -1325,11 +1479,14 @@ var UsageTracker = class {
1325
1479
  flushCount = 0;
1326
1480
  options;
1327
1481
  scheduler;
1482
+ waitUntil;
1483
+ inflightFlushes = /* @__PURE__ */ new Set();
1328
1484
  trackedRequests = /* @__PURE__ */ new WeakSet();
1329
1485
  readEvents = [];
1330
1486
  evaluationEvents = /* @__PURE__ */ new Map();
1331
1487
  constructor(options) {
1332
1488
  this.options = options;
1489
+ this.waitUntil = options.waitUntil;
1333
1490
  this.scheduler = new Scheduler(
1334
1491
  (reason) => this.flushEvents(reason),
1335
1492
  options.waitUntil
@@ -1342,6 +1499,7 @@ var UsageTracker = class {
1342
1499
  async shutdown() {
1343
1500
  await this.scheduler.shutdown();
1344
1501
  await this.flushEvents("shutdown");
1502
+ await Promise.all([...this.inflightFlushes]);
1345
1503
  }
1346
1504
  /**
1347
1505
  * Tracks a config read event. Deduplicates by request context.
@@ -1353,7 +1511,7 @@ var UsageTracker = class {
1353
1511
  if (this.trackedRequests.has(ctx)) return;
1354
1512
  this.trackedRequests.add(ctx);
1355
1513
  this.readEvents.push(new FlagsConfigReadEvent(headers, options));
1356
- this.scheduler.scheduleFlush();
1514
+ this.requestFlush();
1357
1515
  } catch (error) {
1358
1516
  console.error("@vercel/flags-core: Failed to record event:", error);
1359
1517
  }
@@ -1377,7 +1535,7 @@ var UsageTracker = class {
1377
1535
  new FlagsEvaluationEvent(bucketedOptions)
1378
1536
  );
1379
1537
  }
1380
- this.scheduler.scheduleFlush();
1538
+ this.requestFlush();
1381
1539
  } catch (error) {
1382
1540
  console.error(
1383
1541
  "@vercel/flags-core: Failed to record evaluation event:",
@@ -1385,6 +1543,25 @@ var UsageTracker = class {
1385
1543
  );
1386
1544
  }
1387
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
+ }
1388
1565
  /**
1389
1566
  * Send all events to the ingest service
1390
1567
  */
@@ -1472,192 +1649,91 @@ var BundledSource = class {
1472
1649
 
1473
1650
  // src/controller/fetch-datafile.ts
1474
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
+ };
1475
1663
  async function fetchDatafile(options) {
1476
- 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
+ }
1477
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 });
1478
1679
  const timeoutId = setTimeout(
1479
- () => controller.abort(),
1680
+ () => controller.abort(
1681
+ new Error("@vercel/flags-core: Datafile fetch deadline exceeded")
1682
+ ),
1480
1683
  DEFAULT_FETCH_TIMEOUT_MS
1481
1684
  );
1482
- const onExternalAbort = () => controller.abort();
1483
- if (options.signal) {
1484
- if (options.signal.aborted) {
1485
- clearTimeout(timeoutId);
1486
- throw new Error("Fetch aborted");
1487
- }
1488
- options.signal.addEventListener("abort", onExternalAbort, { once: true });
1489
- }
1490
- try {
1685
+ let delay;
1686
+ const fetchAttempt = async () => {
1687
+ const token = await options.auth.resolveToken();
1688
+ signal.throwIfAborted();
1491
1689
  const res = await options.fetch(`${options.host}/v1/datafile`, {
1492
1690
  headers: {
1493
- Authorization: `Bearer ${token}`,
1691
+ ...authHeaders(token, options.auth.sourceProjectId),
1494
1692
  "User-Agent": `VercelFlagsCore/${version}`,
1495
1693
  ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null
1496
1694
  },
1497
- signal: controller.signal
1695
+ signal
1498
1696
  });
1499
- clearTimeout(timeoutId);
1500
- options.signal?.removeEventListener("abort", onExternalAbort);
1697
+ signal.throwIfAborted();
1501
1698
  if (!res.ok) {
1502
- throw new Error(`Failed to fetch data: ${res.statusText}`);
1503
- }
1504
- return res.json();
1505
- } catch (error) {
1506
- clearTimeout(timeoutId);
1507
- options.signal?.removeEventListener("abort", onExternalAbort);
1508
- throw error instanceof Error ? error : new Error("Unknown fetch error");
1509
- }
1510
- }
1511
-
1512
- // src/controller/tagged-data.ts
1513
- function tagData(data, origin) {
1514
- const tagged = { ...data, _origin: origin };
1515
- if (origin === "fetched" || origin === "poll" || origin === "stream") {
1516
- tagged.fetchedAt = Date.now();
1517
- } else if (typeof data.fetchedAt !== "number" || !Number.isFinite(data.fetchedAt) || data.fetchedAt < 0) {
1518
- delete tagged.fetchedAt;
1519
- }
1520
- return tagged;
1521
- }
1522
- function originToMetricsSource(origin) {
1523
- switch (origin) {
1524
- case "stream":
1525
- case "poll":
1526
- case "provided":
1527
- return "in-memory";
1528
- case "fetched":
1529
- return "remote";
1530
- case "bundled":
1531
- return "embedded";
1532
- }
1533
- }
1534
-
1535
- // src/controller/typed-emitter.ts
1536
- var TypedEmitter = class {
1537
- handlers = /* @__PURE__ */ new Map();
1538
- on(event, handler) {
1539
- let set = this.handlers.get(event);
1540
- if (!set) {
1541
- set = /* @__PURE__ */ new Set();
1542
- this.handlers.set(event, set);
1543
- }
1544
- set.add(handler);
1545
- }
1546
- off(event, handler) {
1547
- this.handlers.get(event)?.delete(handler);
1548
- }
1549
- emit(event, ...args) {
1550
- const set = this.handlers.get(event);
1551
- if (set) {
1552
- for (const handler of set) {
1553
- handler(...args);
1554
- }
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
+ );
1555
1705
  }
1556
- }
1557
- };
1558
-
1559
- // src/controller/header-source.ts
1560
- var HeaderSource = class extends TypedEmitter {
1561
- options;
1562
- abortController;
1563
- promise;
1564
- highestObserved = 0;
1565
- lastSeen;
1566
- constructor(options) {
1567
- super();
1568
- this.options = options;
1569
- }
1570
- fetchDatafile() {
1571
- if (this.promise) return this.promise;
1572
- const abortController = new AbortController();
1573
- this.abortController = abortController;
1574
- this.promise = fetchDatafile({
1575
- ...this.options,
1576
- signal: abortController.signal
1577
- }).then((data) => {
1578
- abortController.signal.throwIfAborted();
1579
- this.emit("data", data);
1580
- return data;
1581
- }).finally(() => {
1582
- if (this.abortController === abortController) {
1583
- this.promise = void 0;
1584
- 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
+ ]);
1585
1719
  }
1586
- });
1587
- return this.promise;
1588
- }
1589
- getUpdatedAtHeader(projectId, header) {
1590
- if (!header) {
1591
- return;
1592
- }
1593
- const prefix = `flags_${projectId}=`;
1594
- const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1595
- const timestamp = Number(value);
1596
- return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : void 0;
1597
- }
1598
- async resolveData(currentData, updatedAtHeader) {
1599
- if (!currentData.configUpdatedAt) {
1600
- return;
1601
- }
1602
- if (!updatedAtHeader) {
1603
- return;
1604
- }
1605
- const currentUpdatedAt = Number(currentData.configUpdatedAt);
1606
- if (updatedAtHeader <= currentUpdatedAt) {
1607
- return [currentData, "HIT"];
1608
- }
1609
- const freshAt = Math.max(
1610
- currentData.fetchedAt ?? -Infinity,
1611
- this.lastSeen?.version === currentUpdatedAt ? this.lastSeen.at : -Infinity
1612
- );
1613
- const { staleWhileRevalidateMs } = this.options;
1614
- if (staleWhileRevalidateMs > 0 && Date.now() - freshAt <= staleWhileRevalidateMs) {
1615
- const pending = this.fetchDatafile();
1616
- const signal = this.abortController?.signal;
1617
- const background = pending.catch((error) => {
1618
- if (!signal?.aborted) {
1619
- console.error("@vercel/flags-core: Header refresh failed:", error);
1620
- }
1621
- });
1720
+ signal.throwIfAborted();
1622
1721
  try {
1623
- this.options.waitUntil(background);
1624
- } catch {
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");
1727
+ }
1625
1728
  }
1626
- return [currentData, "STALE"];
1627
- }
1628
- const data = await this.fetchDatafile();
1629
- return [tagData(data, "fetched"), "MISS"];
1630
- }
1631
- observe(version2, currentVersion) {
1632
- this.highestObserved = Math.max(this.highestObserved, version2);
1633
- if (version2 !== currentVersion || version2 !== this.highestObserved) return;
1634
- this.lastSeen = { version: version2, at: Date.now() };
1635
- }
1636
- async read(currentData) {
1637
- const { headers } = getRequestContext();
1638
- const header = headers?.["x-vercel-flags-config-versions"] ?? headers?.["flags-config-versions"];
1639
- const data = currentData ?? tagData(await this.fetchDatafile(), "fetched");
1640
- const updatedAtHeader = this.getUpdatedAtHeader(data.projectId, header);
1641
- if (updatedAtHeader) {
1642
- this.observe(updatedAtHeader, Number(data.configUpdatedAt));
1643
1729
  }
1644
- if (!currentData) return [data, "MISS"];
1645
- return this.resolveData(currentData, updatedAtHeader);
1646
- }
1647
- isAvailable() {
1648
- return this.options.vercel && (this.options.stream.enabled || this.options.polling.enabled);
1649
- }
1650
- /**
1651
- * Abort the current header-driven fetch and discard its pending work.
1652
- */
1653
- stop() {
1654
- this.abortController?.abort();
1655
- this.abortController = void 0;
1656
- this.promise = void 0;
1657
- this.lastSeen = void 0;
1658
- this.highestObserved = 0;
1730
+ } finally {
1731
+ clearTimeout(timeoutId);
1732
+ clearTimeout(delay);
1733
+ signal.removeEventListener("abort", onAbort);
1734
+ options.signal?.removeEventListener("abort", onExternalAbort);
1659
1735
  }
1660
- };
1736
+ }
1661
1737
 
1662
1738
  // src/controller/normalized-options.ts
1663
1739
  import { waitUntil as defaultWaitUntil2 } from "@vercel/functions";
@@ -1665,7 +1741,6 @@ var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1665
1741
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
1666
1742
  var MIN_POLLING_INTERVAL_MS = 3e4;
1667
1743
  var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
1668
- var DEFAULT_STALE_WHILE_REVALIDATE_MS = 1e4;
1669
1744
  function normalizeOptions(options) {
1670
1745
  const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
1671
1746
  const buildStep = options.buildStep ?? autoDetectedBuildStep;
@@ -1698,19 +1773,11 @@ function normalizeOptions(options) {
1698
1773
  initTimeoutMs: options.polling.initTimeoutMs
1699
1774
  };
1700
1775
  }
1701
- const staleWhileRevalidateMs = options.staleWhileRevalidateMs ?? DEFAULT_STALE_WHILE_REVALIDATE_MS;
1702
- if (!Number.isFinite(staleWhileRevalidateMs) || staleWhileRevalidateMs < 0) {
1703
- throw new Error(
1704
- "@vercel/flags-core: staleWhileRevalidateMs must be a finite, non-negative number."
1705
- );
1706
- }
1707
1776
  return {
1708
1777
  auth: options.auth,
1709
1778
  datafile: options.datafile,
1710
1779
  stream,
1711
1780
  polling,
1712
- vercel: options.vercel ?? process.env.VERCEL === "1",
1713
- staleWhileRevalidateMs,
1714
1781
  buildStep,
1715
1782
  fetch: options.fetch ?? globalThis.fetch,
1716
1783
  waitUntil: options.waitUntil ?? defaultWaitUntil2,
@@ -1721,6 +1788,35 @@ function normalizeOptions(options) {
1721
1788
  };
1722
1789
  }
1723
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
+
1724
1820
  // src/controller/polling-source.ts
1725
1821
  var PollingSource = class extends TypedEmitter {
1726
1822
  config;
@@ -1736,13 +1832,16 @@ var PollingSource = class extends TypedEmitter {
1736
1832
  */
1737
1833
  async poll() {
1738
1834
  if (this.abortController?.signal.aborted) return;
1835
+ this.abortController ??= new AbortController();
1836
+ const controller = this.abortController;
1739
1837
  try {
1740
1838
  const data = await fetchDatafile({
1741
1839
  ...this.config,
1742
- signal: this.abortController?.signal
1840
+ signal: controller.signal
1743
1841
  });
1744
1842
  this.emit("data", data);
1745
1843
  } catch (error) {
1844
+ controller.signal.throwIfAborted();
1746
1845
  const err = error instanceof Error ? error : new Error("Unknown poll error");
1747
1846
  this.emit("error", err);
1748
1847
  }
@@ -1754,9 +1853,9 @@ var PollingSource = class extends TypedEmitter {
1754
1853
  */
1755
1854
  startInterval() {
1756
1855
  if (this.intervalId) return;
1757
- this.abortController = new AbortController();
1758
1856
  this.intervalId = setInterval(
1759
- () => void this.poll(),
1857
+ () => void this.poll().catch(() => {
1858
+ }),
1760
1859
  this.config.polling.intervalMs
1761
1860
  );
1762
1861
  }
@@ -1797,8 +1896,8 @@ function backoff(retryCount) {
1797
1896
  return delay + Math.random() * 1e3;
1798
1897
  }
1799
1898
  var UnauthorizedError = class extends Error {
1800
- constructor() {
1801
- super("stream: unauthorized (401)");
1899
+ constructor(sourceProjectId) {
1900
+ super(`stream: ${unauthorizedMessage(sourceProjectId)}`);
1802
1901
  this.name = "UnauthorizedError";
1803
1902
  }
1804
1903
  };
@@ -1858,7 +1957,7 @@ async function connectStream(config, callbacks) {
1858
1957
  throw new TokenResolutionError(error);
1859
1958
  });
1860
1959
  const headers = {
1861
- Authorization: `Bearer ${token}`,
1960
+ ...authHeaders(token, config.sourceProjectId),
1862
1961
  "User-Agent": `VercelFlagsCore/${version}`,
1863
1962
  "X-Retry-Attempt": String(retryCount)
1864
1963
  };
@@ -1880,7 +1979,7 @@ async function connectStream(config, callbacks) {
1880
1979
  if (!response.ok) {
1881
1980
  if (response.status === 401) {
1882
1981
  if (!initialDataReceived) {
1883
- rejectInit(new UnauthorizedError());
1982
+ rejectInit(new UnauthorizedError(config.sourceProjectId));
1884
1983
  }
1885
1984
  abortController.abort();
1886
1985
  break;
@@ -2024,6 +2123,7 @@ var StreamSource = class extends TypedEmitter {
2024
2123
  {
2025
2124
  host: this.options.host,
2026
2125
  resolveToken: () => this.options.auth.resolveToken(),
2126
+ sourceProjectId: this.options.auth.sourceProjectId,
2027
2127
  abortController,
2028
2128
  fetch: this.options.fetch,
2029
2129
  revision: this.revision
@@ -2060,6 +2160,23 @@ var StreamSource = class extends TypedEmitter {
2060
2160
  }
2061
2161
  };
2062
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
+
2063
2180
  // src/controller/index.ts
2064
2181
  function parseConfigUpdatedAt(value) {
2065
2182
  if (typeof value === "number") return value;
@@ -2077,21 +2194,21 @@ var Controller = class {
2077
2194
  data;
2078
2195
  // Memoized data spread for read() / getDatafile().
2079
2196
  // Rebuilt only when `this.data` reference changes (e.g. on stream/poll update).
2080
- // Holds the result of stripping internal metadata; metrics are appended per-call.
2197
+ // Holds the result of stripping `_origin`; metrics are appended per-call.
2081
2198
  dataViewSource = void 0;
2082
2199
  dataViewBase = void 0;
2083
2200
  // Sources (I/O delegates)
2084
2201
  streamSource;
2085
2202
  pollingSource;
2086
2203
  bundledSource;
2087
- headerSource;
2088
2204
  // Usage tracking
2089
2205
  usageTracker;
2090
2206
  isFirstGetData = true;
2091
2207
  // Build-step deduplication
2092
2208
  buildDataPromise = null;
2093
2209
  buildReadTracked = false;
2094
- // 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.
2095
2212
  unauthorized = false;
2096
2213
  constructor(options) {
2097
2214
  this.options = normalizeOptions(options);
@@ -2104,7 +2221,6 @@ var Controller = class {
2104
2221
  auth: this.options.auth,
2105
2222
  readBundledDefinitions
2106
2223
  });
2107
- this.headerSource = new HeaderSource(this.options);
2108
2224
  this.wireSourceEvents();
2109
2225
  if (this.options.datafile) {
2110
2226
  this.data = tagData(this.options.datafile, "provided");
@@ -2113,11 +2229,13 @@ var Controller = class {
2113
2229
  }
2114
2230
  // Source event handlers (stored for cleanup)
2115
2231
  onStreamData = (data) => {
2232
+ this.unauthorized = false;
2116
2233
  if (this.isNewerData(data)) {
2117
2234
  this.data = tagData(data, "stream");
2118
2235
  }
2119
2236
  };
2120
2237
  onStreamPrimed = () => {
2238
+ this.unauthorized = false;
2121
2239
  if (this.state === "degraded" || this.state === "initializing:stream") {
2122
2240
  this.transition("streaming");
2123
2241
  }
@@ -2133,18 +2251,15 @@ var Controller = class {
2133
2251
  }
2134
2252
  };
2135
2253
  onPollData = (data) => {
2254
+ this.unauthorized = false;
2136
2255
  if (this.isNewerData(data)) {
2137
2256
  this.data = tagData(data, "poll");
2138
2257
  }
2139
2258
  };
2140
2259
  onPollError = (error) => {
2260
+ this.noteUnauthorized(error);
2141
2261
  console.error("@vercel/flags-core: Poll failed:", error);
2142
2262
  };
2143
- onFetchedData = (data) => {
2144
- if (this.isNewerData(data)) {
2145
- this.data = tagData(data, "fetched");
2146
- }
2147
- };
2148
2263
  // ---------------------------------------------------------------------------
2149
2264
  // Source event wiring
2150
2265
  // ---------------------------------------------------------------------------
@@ -2155,7 +2270,6 @@ var Controller = class {
2155
2270
  this.streamSource.on("disconnected", this.onStreamDisconnected);
2156
2271
  this.pollingSource.on("data", this.onPollData);
2157
2272
  this.pollingSource.on("error", this.onPollError);
2158
- this.headerSource.on("data", this.onFetchedData);
2159
2273
  }
2160
2274
  unwireSourceEvents() {
2161
2275
  this.streamSource.off("data", this.onStreamData);
@@ -2164,7 +2278,6 @@ var Controller = class {
2164
2278
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2165
2279
  this.pollingSource.off("data", this.onPollData);
2166
2280
  this.pollingSource.off("error", this.onPollError);
2167
- this.headerSource.off("data", this.onFetchedData);
2168
2281
  }
2169
2282
  // ---------------------------------------------------------------------------
2170
2283
  // State machine
@@ -2182,8 +2295,6 @@ var Controller = class {
2182
2295
  return "streaming";
2183
2296
  case "polling":
2184
2297
  return "polling";
2185
- case "vercel":
2186
- return "vercel";
2187
2298
  default:
2188
2299
  return "offline";
2189
2300
  }
@@ -2197,7 +2308,6 @@ var Controller = class {
2197
2308
  * Build step: datafile → bundled → one-time fetch
2198
2309
  * Streaming mode: stream → datafile → bundled
2199
2310
  * Polling mode (no stream): poll → datafile → bundled
2200
- * Vercel mode: datafile → bundled; fetch only on a read
2201
2311
  * Offline mode (neither): datafile → bundled → one-time fetch
2202
2312
  */
2203
2313
  async initialize() {
@@ -2211,20 +2321,13 @@ var Controller = class {
2211
2321
  this.data = tagData(this.options.datafile, "provided");
2212
2322
  }
2213
2323
  if (!this.data) {
2214
- this.transition("initializing:fallback");
2215
- let bundled;
2216
2324
  try {
2217
- bundled = await this.bundledSource.tryLoad();
2325
+ const bundled = await this.bundledSource.tryLoad();
2326
+ if (bundled) {
2327
+ this.data = tagData(bundled, "bundled");
2328
+ }
2218
2329
  } catch {
2219
2330
  }
2220
- if (this.state === "shutdown") {
2221
- throw new Error("@vercel/flags-core: Client is shut down");
2222
- }
2223
- if (bundled) this.data = tagData(bundled, "bundled");
2224
- }
2225
- if (this.headerSource.isAvailable()) {
2226
- this.transition("vercel");
2227
- return;
2228
2331
  }
2229
2332
  if (this.data) {
2230
2333
  if (this.options.stream.enabled) {
@@ -2290,7 +2393,6 @@ var Controller = class {
2290
2393
  this.unwireSourceEvents();
2291
2394
  this.streamSource.stop();
2292
2395
  this.pollingSource.stop();
2293
- this.headerSource.stop();
2294
2396
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2295
2397
  this.transition("shutdown");
2296
2398
  await this.usageTracker.shutdown();
@@ -2326,9 +2428,10 @@ var Controller = class {
2326
2428
  this.data = tagData(fetched, "fetched");
2327
2429
  result = this.data;
2328
2430
  cacheStatus = "MISS";
2329
- } catch {
2330
- throw new Error(
2331
- "@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."
2332
2435
  );
2333
2436
  }
2334
2437
  }
@@ -2371,10 +2474,6 @@ var Controller = class {
2371
2474
  if (this.options.buildStep) {
2372
2475
  return this.resolveDataForBuildStep();
2373
2476
  }
2374
- if (this.state === "vercel") {
2375
- const result = await this.headerSource.read(this.data);
2376
- if (result) return result;
2377
- }
2378
2477
  if (this.data) {
2379
2478
  const cacheStatus = this.isConnected ? "HIT" : "STALE";
2380
2479
  return [this.data, cacheStatus];
@@ -2394,9 +2493,7 @@ var Controller = class {
2394
2493
  await this.streamSource.start();
2395
2494
  return true;
2396
2495
  } catch (error) {
2397
- if (error instanceof UnauthorizedError) {
2398
- this.unauthorized = true;
2399
- }
2496
+ this.noteUnauthorized(error);
2400
2497
  return false;
2401
2498
  }
2402
2499
  }
@@ -2424,9 +2521,7 @@ var Controller = class {
2424
2521
  return true;
2425
2522
  } catch (error) {
2426
2523
  clearTimeout(timeoutId);
2427
- if (error instanceof Error && error.message.includes("401")) {
2428
- this.unauthorized = true;
2429
- }
2524
+ this.noteUnauthorized(error);
2430
2525
  return false;
2431
2526
  }
2432
2527
  }
@@ -2479,6 +2574,11 @@ var Controller = class {
2479
2574
  return false;
2480
2575
  }
2481
2576
  }
2577
+ noteUnauthorized(error) {
2578
+ if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("401")) {
2579
+ this.unauthorized = true;
2580
+ }
2581
+ }
2482
2582
  // ---------------------------------------------------------------------------
2483
2583
  // Build step helpers
2484
2584
  // ---------------------------------------------------------------------------
@@ -2524,10 +2624,11 @@ var Controller = class {
2524
2624
  fetch: this.options.fetch
2525
2625
  });
2526
2626
  return tagData(fetched, "fetched");
2527
- } catch {
2627
+ } catch (error) {
2628
+ this.noteUnauthorized(error);
2528
2629
  }
2529
- throw new Error(
2530
- "@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."
2531
2632
  );
2532
2633
  }
2533
2634
  // ---------------------------------------------------------------------------
@@ -2561,8 +2662,17 @@ var Controller = class {
2561
2662
  } catch {
2562
2663
  }
2563
2664
  }
2564
- throw new Error(
2565
- "@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}`
2566
2676
  );
2567
2677
  }
2568
2678
  /**
@@ -2613,8 +2723,8 @@ var Controller = class {
2613
2723
  } catch {
2614
2724
  }
2615
2725
  }
2616
- throw new Error(
2617
- "@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
2726
+ throw this.noDefinitionsError(
2727
+ ". Provide a datafile or bundled definitions."
2618
2728
  );
2619
2729
  }
2620
2730
  // ---------------------------------------------------------------------------
@@ -2687,88 +2797,6 @@ var Controller = class {
2687
2797
  }
2688
2798
  };
2689
2799
 
2690
- // src/controller/auth.ts
2691
- import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
2692
-
2693
- // src/utils/sdk-keys.ts
2694
- var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
2695
- function parseSdkKeyFromFlagsConnectionString(text) {
2696
- if (SDK_KEY_REGEX.test(text)) return text;
2697
- try {
2698
- if (!text.startsWith("flags:")) return null;
2699
- const params = new URLSearchParams(text.slice(6));
2700
- const sdkKey = params.get("sdkKey");
2701
- if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) return sdkKey;
2702
- } catch {
2703
- }
2704
- return null;
2705
- }
2706
-
2707
- // src/controller/auth.ts
2708
- async function getOidcToken() {
2709
- try {
2710
- return await getVercelOidcToken2();
2711
- } catch {
2712
- throw new Error(
2713
- [
2714
- "@vercel/flags-core: Failed to get OIDC token.",
2715
- "Are you running in a Vercel Environment where OIDC tokens are available?",
2716
- "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
2717
- ].join(" ")
2718
- );
2719
- }
2720
- }
2721
- function getProjectIdFromOidcToken(oidcToken) {
2722
- const tokenParts = oidcToken.split(".");
2723
- if (tokenParts.length !== 3 || !tokenParts[1]) {
2724
- throw new Error("@vercel/flags-core: Invalid OIDC token");
2725
- }
2726
- const payload = JSON.parse(
2727
- Buffer.from(tokenParts[1], "base64url").toString("utf8")
2728
- );
2729
- if (typeof payload.project_id !== "string" || !payload.project_id) {
2730
- throw new Error(
2731
- "@vercel/flags-core: Missing project_id claim in OIDC token"
2732
- );
2733
- }
2734
- return payload.project_id;
2735
- }
2736
- var Authentication = class {
2737
- sdkKey;
2738
- constructor(sdkKeyOrConnectionString) {
2739
- if (sdkKeyOrConnectionString !== void 0) {
2740
- if (typeof sdkKeyOrConnectionString !== "string") {
2741
- throw new Error(
2742
- `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
2743
- );
2744
- }
2745
- const parsed = parseSdkKeyFromFlagsConnectionString(
2746
- sdkKeyOrConnectionString
2747
- );
2748
- if (!parsed) {
2749
- throw new Error("@vercel/flags-core: Missing sdkKey");
2750
- }
2751
- this.sdkKey = parsed;
2752
- }
2753
- }
2754
- async resolveToken() {
2755
- if (this.sdkKey) {
2756
- return this.sdkKey;
2757
- }
2758
- return await getOidcToken();
2759
- }
2760
- async resolveBundledDefinitionsLookup() {
2761
- if (this.sdkKey) {
2762
- return { type: "sdk-key", sdkKey: this.sdkKey };
2763
- }
2764
- const oidcToken = await this.resolveToken();
2765
- return {
2766
- type: "project-id",
2767
- projectId: getProjectIdFromOidcToken(oidcToken)
2768
- };
2769
- }
2770
- };
2771
-
2772
2800
  // src/index.make.ts
2773
2801
  function make(createRawClient, defaults) {
2774
2802
  let _defaultFlagsClient = null;
@@ -2786,7 +2814,11 @@ function make(createRawClient, defaults) {
2786
2814
  });
2787
2815
  return createRawClient({
2788
2816
  controller,
2789
- origin: { provider: "vercel", sdkKey: auth.sdkKey },
2817
+ origin: {
2818
+ provider: "vercel",
2819
+ sdkKey: auth.sdkKey,
2820
+ ...auth.sourceProjectId ? { projectId: auth.sourceProjectId } : null
2821
+ },
2790
2822
  waitUntil: waitUntil3,
2791
2823
  ...experimental_reportExposures ? { experimental_reportExposures } : {}
2792
2824
  });
@@ -2838,4 +2870,4 @@ export {
2838
2870
  resetDefaultFlagsClient,
2839
2871
  createClient
2840
2872
  };
2841
- //# sourceMappingURL=chunk-FKI4L3NY.js.map
2873
+ //# sourceMappingURL=chunk-SLNLPFKU.js.map