@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.
@@ -56,7 +56,7 @@ function getSplitBoundaries(outcome) {
56
56
  function getCompiledRegex(rhs) {
57
57
  const cached = compiledRegexCache.get(rhs);
58
58
  if (cached) return cached;
59
- const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/, ""));
59
+ const compiled = new RegExp(rhs.pattern, rhs.flags.replace(/[gy]/g, ""));
60
60
  compiledRegexCache.set(rhs, compiled);
61
61
  return compiled;
62
62
  }
@@ -433,7 +433,7 @@ function resolveOutcome(params, outcome) {
433
433
  break;
434
434
  }
435
435
  }
436
- if (exhausted) currentPromille = PROMILLE_SCALE;
436
+ if (exhausted) currentPromille = outcome.finalPromille ?? PROMILLE_SCALE;
437
437
  if (currentPromille <= 0) {
438
438
  return {
439
439
  ...rollFromVariant,
@@ -559,7 +559,7 @@ function bulkEvaluate(flags, shared) {
559
559
  }
560
560
 
561
561
  // package.json
562
- var version = "1.9.0-ea4d7de-20260922121332";
562
+ var version = "1.9.0";
563
563
 
564
564
  // src/lib/report-value.ts
565
565
  function internalReportValue(key, value, data) {
@@ -1021,8 +1021,128 @@ async function readBundledDefinitions(auth) {
1021
1021
  }
1022
1022
 
1023
1023
  // src/utils/ingest.ts
1024
+ import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
1025
+
1026
+ // src/controller/auth.ts
1024
1027
  import { getVercelOidcToken } from "@vercel/oidc";
1025
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
+
1026
1146
  // src/utils/backoff.ts
1027
1147
  var DEFAULT_BASE_MS = 250;
1028
1148
  var DEFAULT_CAP_MS = 5e3;
@@ -1038,6 +1158,20 @@ function getJitteredWaitMs(baseMs, ratio) {
1038
1158
  return Math.floor(min + Math.random() * span);
1039
1159
  }
1040
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
+
1041
1175
  // src/utils/ingest.ts
1042
1176
  var MAX_RETRIES = 3;
1043
1177
  var MAX_EVENTS_PER_REQUEST = 2e3;
@@ -1051,7 +1185,7 @@ var debugLog = (...args) => {
1051
1185
  async function getEvaluatingOidcToken(auth) {
1052
1186
  if (!auth.sdkKey) return void 0;
1053
1187
  try {
1054
- return await getVercelOidcToken();
1188
+ return await getVercelOidcToken2();
1055
1189
  } catch {
1056
1190
  return void 0;
1057
1191
  }
@@ -1061,7 +1195,7 @@ async function getIngestHeaders(options, flushReason) {
1061
1195
  const evaluatingOidcToken = await getEvaluatingOidcToken(options.auth);
1062
1196
  return {
1063
1197
  "Content-Type": "application/json",
1064
- Authorization: `Bearer ${token}`,
1198
+ ...authHeaders(token, options.auth.sourceProjectId),
1065
1199
  "User-Agent": `VercelFlagsCore/${version}`,
1066
1200
  [FLUSH_REASON_HEADER]: flushReason,
1067
1201
  ...options.metricEnvironment ?? process.env.VERCEL_ENV ? {
@@ -1071,8 +1205,28 @@ async function getIngestHeaders(options, flushReason) {
1071
1205
  ...isDebugMode ? { "x-vercel-debug-ingest": "1" } : null
1072
1206
  };
1073
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
+ }
1074
1220
  async function sendIngestEvents(options, events, flushId, flushReason) {
1075
- 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
+ }
1076
1230
  for (let i = 0; i < eventsToSend.length; i += MAX_EVENTS_PER_REQUEST) {
1077
1231
  await sendIngestChunk(
1078
1232
  options,
@@ -1308,11 +1462,14 @@ var UsageTracker = class {
1308
1462
  flushCount = 0;
1309
1463
  options;
1310
1464
  scheduler;
1465
+ waitUntil;
1466
+ inflightFlushes = /* @__PURE__ */ new Set();
1311
1467
  trackedRequests = /* @__PURE__ */ new WeakSet();
1312
1468
  readEvents = [];
1313
1469
  evaluationEvents = /* @__PURE__ */ new Map();
1314
1470
  constructor(options) {
1315
1471
  this.options = options;
1472
+ this.waitUntil = options.waitUntil;
1316
1473
  this.scheduler = new Scheduler(
1317
1474
  (reason) => this.flushEvents(reason),
1318
1475
  options.waitUntil
@@ -1325,6 +1482,7 @@ var UsageTracker = class {
1325
1482
  async shutdown() {
1326
1483
  await this.scheduler.shutdown();
1327
1484
  await this.flushEvents("shutdown");
1485
+ await Promise.all([...this.inflightFlushes]);
1328
1486
  }
1329
1487
  /**
1330
1488
  * Tracks a config read event. Deduplicates by request context.
@@ -1336,7 +1494,7 @@ var UsageTracker = class {
1336
1494
  if (this.trackedRequests.has(ctx)) return;
1337
1495
  this.trackedRequests.add(ctx);
1338
1496
  this.readEvents.push(new FlagsConfigReadEvent(headers, options));
1339
- this.scheduler.scheduleFlush();
1497
+ this.requestFlush();
1340
1498
  } catch (error) {
1341
1499
  console.error("@vercel/flags-core: Failed to record event:", error);
1342
1500
  }
@@ -1360,7 +1518,7 @@ var UsageTracker = class {
1360
1518
  new FlagsEvaluationEvent(bucketedOptions)
1361
1519
  );
1362
1520
  }
1363
- this.scheduler.scheduleFlush();
1521
+ this.requestFlush();
1364
1522
  } catch (error) {
1365
1523
  console.error(
1366
1524
  "@vercel/flags-core: Failed to record evaluation event:",
@@ -1368,6 +1526,25 @@ var UsageTracker = class {
1368
1526
  );
1369
1527
  }
1370
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
+ }
1371
1548
  /**
1372
1549
  * Send all events to the ingest service
1373
1550
  */
@@ -1455,192 +1632,91 @@ var BundledSource = class {
1455
1632
 
1456
1633
  // src/controller/fetch-datafile.ts
1457
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
+ };
1458
1646
  async function fetchDatafile(options) {
1459
- 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
+ }
1460
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 });
1461
1662
  const timeoutId = setTimeout(
1462
- () => controller.abort(),
1663
+ () => controller.abort(
1664
+ new Error("@vercel/flags-core: Datafile fetch deadline exceeded")
1665
+ ),
1463
1666
  DEFAULT_FETCH_TIMEOUT_MS
1464
1667
  );
1465
- const onExternalAbort = () => controller.abort();
1466
- if (options.signal) {
1467
- if (options.signal.aborted) {
1468
- clearTimeout(timeoutId);
1469
- throw new Error("Fetch aborted");
1470
- }
1471
- options.signal.addEventListener("abort", onExternalAbort, { once: true });
1472
- }
1473
- try {
1668
+ let delay;
1669
+ const fetchAttempt = async () => {
1670
+ const token = await options.auth.resolveToken();
1671
+ signal.throwIfAborted();
1474
1672
  const res = await options.fetch(`${options.host}/v1/datafile`, {
1475
1673
  headers: {
1476
- Authorization: `Bearer ${token}`,
1674
+ ...authHeaders(token, options.auth.sourceProjectId),
1477
1675
  "User-Agent": `VercelFlagsCore/${version}`,
1478
1676
  ...process.env.VERCEL_ENV ? { "X-Vercel-Env": process.env.VERCEL_ENV } : null
1479
1677
  },
1480
- signal: controller.signal
1678
+ signal
1481
1679
  });
1482
- clearTimeout(timeoutId);
1483
- options.signal?.removeEventListener("abort", onExternalAbort);
1680
+ signal.throwIfAborted();
1484
1681
  if (!res.ok) {
1485
- throw new Error(`Failed to fetch data: ${res.statusText}`);
1486
- }
1487
- return res.json();
1488
- } catch (error) {
1489
- clearTimeout(timeoutId);
1490
- options.signal?.removeEventListener("abort", onExternalAbort);
1491
- throw error instanceof Error ? error : new Error("Unknown fetch error");
1492
- }
1493
- }
1494
-
1495
- // src/controller/tagged-data.ts
1496
- function tagData(data, origin) {
1497
- const tagged = { ...data, _origin: origin };
1498
- if (origin === "fetched" || origin === "poll" || origin === "stream") {
1499
- tagged.fetchedAt = Date.now();
1500
- } else if (typeof data.fetchedAt !== "number" || !Number.isFinite(data.fetchedAt) || data.fetchedAt < 0) {
1501
- delete tagged.fetchedAt;
1502
- }
1503
- return tagged;
1504
- }
1505
- function originToMetricsSource(origin) {
1506
- switch (origin) {
1507
- case "stream":
1508
- case "poll":
1509
- case "provided":
1510
- return "in-memory";
1511
- case "fetched":
1512
- return "remote";
1513
- case "bundled":
1514
- return "embedded";
1515
- }
1516
- }
1517
-
1518
- // src/controller/typed-emitter.ts
1519
- var TypedEmitter = class {
1520
- handlers = /* @__PURE__ */ new Map();
1521
- on(event, handler) {
1522
- let set = this.handlers.get(event);
1523
- if (!set) {
1524
- set = /* @__PURE__ */ new Set();
1525
- this.handlers.set(event, set);
1526
- }
1527
- set.add(handler);
1528
- }
1529
- off(event, handler) {
1530
- this.handlers.get(event)?.delete(handler);
1531
- }
1532
- emit(event, ...args) {
1533
- const set = this.handlers.get(event);
1534
- if (set) {
1535
- for (const handler of set) {
1536
- handler(...args);
1537
- }
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
+ );
1538
1688
  }
1539
- }
1540
- };
1541
-
1542
- // src/controller/header-source.ts
1543
- var HeaderSource = class extends TypedEmitter {
1544
- options;
1545
- abortController;
1546
- promise;
1547
- highestObserved = 0;
1548
- lastSeen;
1549
- constructor(options) {
1550
- super();
1551
- this.options = options;
1552
- }
1553
- fetchDatafile() {
1554
- if (this.promise) return this.promise;
1555
- const abortController = new AbortController();
1556
- this.abortController = abortController;
1557
- this.promise = fetchDatafile({
1558
- ...this.options,
1559
- signal: abortController.signal
1560
- }).then((data) => {
1561
- abortController.signal.throwIfAborted();
1562
- this.emit("data", data);
1563
- return data;
1564
- }).finally(() => {
1565
- if (this.abortController === abortController) {
1566
- this.promise = void 0;
1567
- 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
+ ]);
1568
1702
  }
1569
- });
1570
- return this.promise;
1571
- }
1572
- getUpdatedAtHeader(projectId, header) {
1573
- if (!header) {
1574
- return;
1575
- }
1576
- const prefix = `flags_${projectId}=`;
1577
- const value = header.split(";").map((part) => part.trim()).find((part) => part.startsWith(prefix))?.slice(prefix.length);
1578
- const timestamp = Number(value);
1579
- return Number.isFinite(timestamp) && timestamp > 0 ? timestamp : void 0;
1580
- }
1581
- async resolveData(currentData, updatedAtHeader) {
1582
- if (!currentData.configUpdatedAt) {
1583
- return;
1584
- }
1585
- if (!updatedAtHeader) {
1586
- return;
1587
- }
1588
- const currentUpdatedAt = Number(currentData.configUpdatedAt);
1589
- if (updatedAtHeader <= currentUpdatedAt) {
1590
- return [currentData, "HIT"];
1591
- }
1592
- const freshAt = Math.max(
1593
- currentData.fetchedAt ?? -Infinity,
1594
- this.lastSeen?.version === currentUpdatedAt ? this.lastSeen.at : -Infinity
1595
- );
1596
- const { staleWhileRevalidateMs } = this.options;
1597
- if (staleWhileRevalidateMs > 0 && Date.now() - freshAt <= staleWhileRevalidateMs) {
1598
- const pending = this.fetchDatafile();
1599
- const signal = this.abortController?.signal;
1600
- const background = pending.catch((error) => {
1601
- if (!signal?.aborted) {
1602
- console.error("@vercel/flags-core: Header refresh failed:", error);
1603
- }
1604
- });
1703
+ signal.throwIfAborted();
1605
1704
  try {
1606
- this.options.waitUntil(background);
1607
- } catch {
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");
1710
+ }
1608
1711
  }
1609
- return [currentData, "STALE"];
1610
- }
1611
- const data = await this.fetchDatafile();
1612
- return [tagData(data, "fetched"), "MISS"];
1613
- }
1614
- observe(version2, currentVersion) {
1615
- this.highestObserved = Math.max(this.highestObserved, version2);
1616
- if (version2 !== currentVersion || version2 !== this.highestObserved) return;
1617
- this.lastSeen = { version: version2, at: Date.now() };
1618
- }
1619
- async read(currentData) {
1620
- const { headers } = getRequestContext();
1621
- const header = headers?.["x-vercel-flags-config-versions"] ?? headers?.["flags-config-versions"];
1622
- const data = currentData ?? tagData(await this.fetchDatafile(), "fetched");
1623
- const updatedAtHeader = this.getUpdatedAtHeader(data.projectId, header);
1624
- if (updatedAtHeader) {
1625
- this.observe(updatedAtHeader, Number(data.configUpdatedAt));
1626
1712
  }
1627
- if (!currentData) return [data, "MISS"];
1628
- return this.resolveData(currentData, updatedAtHeader);
1629
- }
1630
- isAvailable() {
1631
- return this.options.vercel && (this.options.stream.enabled || this.options.polling.enabled);
1632
- }
1633
- /**
1634
- * Abort the current header-driven fetch and discard its pending work.
1635
- */
1636
- stop() {
1637
- this.abortController?.abort();
1638
- this.abortController = void 0;
1639
- this.promise = void 0;
1640
- this.lastSeen = void 0;
1641
- this.highestObserved = 0;
1713
+ } finally {
1714
+ clearTimeout(timeoutId);
1715
+ clearTimeout(delay);
1716
+ signal.removeEventListener("abort", onAbort);
1717
+ options.signal?.removeEventListener("abort", onExternalAbort);
1642
1718
  }
1643
- };
1719
+ }
1644
1720
 
1645
1721
  // src/controller/normalized-options.ts
1646
1722
  import { waitUntil as defaultWaitUntil2 } from "@vercel/functions";
@@ -1648,7 +1724,6 @@ var DEFAULT_STREAM_INIT_TIMEOUT_MS = 3e3;
1648
1724
  var DEFAULT_POLLING_INTERVAL_MS = 3e4;
1649
1725
  var MIN_POLLING_INTERVAL_MS = 3e4;
1650
1726
  var DEFAULT_POLLING_INIT_TIMEOUT_MS = 3e3;
1651
- var DEFAULT_STALE_WHILE_REVALIDATE_MS = 1e4;
1652
1727
  function normalizeOptions(options) {
1653
1728
  const autoDetectedBuildStep = process.env.CI === "1" || process.env.NEXT_PHASE === "phase-production-build";
1654
1729
  const buildStep = options.buildStep ?? autoDetectedBuildStep;
@@ -1681,19 +1756,11 @@ function normalizeOptions(options) {
1681
1756
  initTimeoutMs: options.polling.initTimeoutMs
1682
1757
  };
1683
1758
  }
1684
- const staleWhileRevalidateMs = options.staleWhileRevalidateMs ?? DEFAULT_STALE_WHILE_REVALIDATE_MS;
1685
- if (!Number.isFinite(staleWhileRevalidateMs) || staleWhileRevalidateMs < 0) {
1686
- throw new Error(
1687
- "@vercel/flags-core: staleWhileRevalidateMs must be a finite, non-negative number."
1688
- );
1689
- }
1690
1759
  return {
1691
1760
  auth: options.auth,
1692
1761
  datafile: options.datafile,
1693
1762
  stream,
1694
1763
  polling,
1695
- vercel: options.vercel ?? process.env.VERCEL === "1",
1696
- staleWhileRevalidateMs,
1697
1764
  buildStep,
1698
1765
  fetch: options.fetch ?? globalThis.fetch,
1699
1766
  waitUntil: options.waitUntil ?? defaultWaitUntil2,
@@ -1704,6 +1771,35 @@ function normalizeOptions(options) {
1704
1771
  };
1705
1772
  }
1706
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
+
1707
1803
  // src/controller/polling-source.ts
1708
1804
  var PollingSource = class extends TypedEmitter {
1709
1805
  config;
@@ -1719,13 +1815,16 @@ var PollingSource = class extends TypedEmitter {
1719
1815
  */
1720
1816
  async poll() {
1721
1817
  if (this.abortController?.signal.aborted) return;
1818
+ this.abortController ??= new AbortController();
1819
+ const controller = this.abortController;
1722
1820
  try {
1723
1821
  const data = await fetchDatafile({
1724
1822
  ...this.config,
1725
- signal: this.abortController?.signal
1823
+ signal: controller.signal
1726
1824
  });
1727
1825
  this.emit("data", data);
1728
1826
  } catch (error) {
1827
+ controller.signal.throwIfAborted();
1729
1828
  const err = error instanceof Error ? error : new Error("Unknown poll error");
1730
1829
  this.emit("error", err);
1731
1830
  }
@@ -1737,9 +1836,9 @@ var PollingSource = class extends TypedEmitter {
1737
1836
  */
1738
1837
  startInterval() {
1739
1838
  if (this.intervalId) return;
1740
- this.abortController = new AbortController();
1741
1839
  this.intervalId = setInterval(
1742
- () => void this.poll(),
1840
+ () => void this.poll().catch(() => {
1841
+ }),
1743
1842
  this.config.polling.intervalMs
1744
1843
  );
1745
1844
  }
@@ -1780,8 +1879,8 @@ function backoff(retryCount) {
1780
1879
  return delay + Math.random() * 1e3;
1781
1880
  }
1782
1881
  var UnauthorizedError = class extends Error {
1783
- constructor() {
1784
- super("stream: unauthorized (401)");
1882
+ constructor(sourceProjectId) {
1883
+ super(`stream: ${unauthorizedMessage(sourceProjectId)}`);
1785
1884
  this.name = "UnauthorizedError";
1786
1885
  }
1787
1886
  };
@@ -1841,7 +1940,7 @@ async function connectStream(config, callbacks) {
1841
1940
  throw new TokenResolutionError(error);
1842
1941
  });
1843
1942
  const headers = {
1844
- Authorization: `Bearer ${token}`,
1943
+ ...authHeaders(token, config.sourceProjectId),
1845
1944
  "User-Agent": `VercelFlagsCore/${version}`,
1846
1945
  "X-Retry-Attempt": String(retryCount)
1847
1946
  };
@@ -1863,7 +1962,7 @@ async function connectStream(config, callbacks) {
1863
1962
  if (!response.ok) {
1864
1963
  if (response.status === 401) {
1865
1964
  if (!initialDataReceived) {
1866
- rejectInit(new UnauthorizedError());
1965
+ rejectInit(new UnauthorizedError(config.sourceProjectId));
1867
1966
  }
1868
1967
  abortController.abort();
1869
1968
  break;
@@ -2007,6 +2106,7 @@ var StreamSource = class extends TypedEmitter {
2007
2106
  {
2008
2107
  host: this.options.host,
2009
2108
  resolveToken: () => this.options.auth.resolveToken(),
2109
+ sourceProjectId: this.options.auth.sourceProjectId,
2010
2110
  abortController,
2011
2111
  fetch: this.options.fetch,
2012
2112
  revision: this.revision
@@ -2043,6 +2143,23 @@ var StreamSource = class extends TypedEmitter {
2043
2143
  }
2044
2144
  };
2045
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
+
2046
2163
  // src/controller/index.ts
2047
2164
  function parseConfigUpdatedAt(value) {
2048
2165
  if (typeof value === "number") return value;
@@ -2060,21 +2177,21 @@ var Controller = class {
2060
2177
  data;
2061
2178
  // Memoized data spread for read() / getDatafile().
2062
2179
  // Rebuilt only when `this.data` reference changes (e.g. on stream/poll update).
2063
- // Holds the result of stripping internal metadata; metrics are appended per-call.
2180
+ // Holds the result of stripping `_origin`; metrics are appended per-call.
2064
2181
  dataViewSource = void 0;
2065
2182
  dataViewBase = void 0;
2066
2183
  // Sources (I/O delegates)
2067
2184
  streamSource;
2068
2185
  pollingSource;
2069
2186
  bundledSource;
2070
- headerSource;
2071
2187
  // Usage tracking
2072
2188
  usageTracker;
2073
2189
  isFirstGetData = true;
2074
2190
  // Build-step deduplication
2075
2191
  buildDataPromise = null;
2076
2192
  buildReadTracked = false;
2077
- // 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.
2078
2195
  unauthorized = false;
2079
2196
  constructor(options) {
2080
2197
  this.options = normalizeOptions(options);
@@ -2087,7 +2204,6 @@ var Controller = class {
2087
2204
  auth: this.options.auth,
2088
2205
  readBundledDefinitions
2089
2206
  });
2090
- this.headerSource = new HeaderSource(this.options);
2091
2207
  this.wireSourceEvents();
2092
2208
  if (this.options.datafile) {
2093
2209
  this.data = tagData(this.options.datafile, "provided");
@@ -2096,11 +2212,13 @@ var Controller = class {
2096
2212
  }
2097
2213
  // Source event handlers (stored for cleanup)
2098
2214
  onStreamData = (data) => {
2215
+ this.unauthorized = false;
2099
2216
  if (this.isNewerData(data)) {
2100
2217
  this.data = tagData(data, "stream");
2101
2218
  }
2102
2219
  };
2103
2220
  onStreamPrimed = () => {
2221
+ this.unauthorized = false;
2104
2222
  if (this.state === "degraded" || this.state === "initializing:stream") {
2105
2223
  this.transition("streaming");
2106
2224
  }
@@ -2116,18 +2234,15 @@ var Controller = class {
2116
2234
  }
2117
2235
  };
2118
2236
  onPollData = (data) => {
2237
+ this.unauthorized = false;
2119
2238
  if (this.isNewerData(data)) {
2120
2239
  this.data = tagData(data, "poll");
2121
2240
  }
2122
2241
  };
2123
2242
  onPollError = (error) => {
2243
+ this.noteUnauthorized(error);
2124
2244
  console.error("@vercel/flags-core: Poll failed:", error);
2125
2245
  };
2126
- onFetchedData = (data) => {
2127
- if (this.isNewerData(data)) {
2128
- this.data = tagData(data, "fetched");
2129
- }
2130
- };
2131
2246
  // ---------------------------------------------------------------------------
2132
2247
  // Source event wiring
2133
2248
  // ---------------------------------------------------------------------------
@@ -2138,7 +2253,6 @@ var Controller = class {
2138
2253
  this.streamSource.on("disconnected", this.onStreamDisconnected);
2139
2254
  this.pollingSource.on("data", this.onPollData);
2140
2255
  this.pollingSource.on("error", this.onPollError);
2141
- this.headerSource.on("data", this.onFetchedData);
2142
2256
  }
2143
2257
  unwireSourceEvents() {
2144
2258
  this.streamSource.off("data", this.onStreamData);
@@ -2147,7 +2261,6 @@ var Controller = class {
2147
2261
  this.streamSource.off("disconnected", this.onStreamDisconnected);
2148
2262
  this.pollingSource.off("data", this.onPollData);
2149
2263
  this.pollingSource.off("error", this.onPollError);
2150
- this.headerSource.off("data", this.onFetchedData);
2151
2264
  }
2152
2265
  // ---------------------------------------------------------------------------
2153
2266
  // State machine
@@ -2165,8 +2278,6 @@ var Controller = class {
2165
2278
  return "streaming";
2166
2279
  case "polling":
2167
2280
  return "polling";
2168
- case "vercel":
2169
- return "vercel";
2170
2281
  default:
2171
2282
  return "offline";
2172
2283
  }
@@ -2180,7 +2291,6 @@ var Controller = class {
2180
2291
  * Build step: datafile → bundled → one-time fetch
2181
2292
  * Streaming mode: stream → datafile → bundled
2182
2293
  * Polling mode (no stream): poll → datafile → bundled
2183
- * Vercel mode: datafile → bundled; fetch only on a read
2184
2294
  * Offline mode (neither): datafile → bundled → one-time fetch
2185
2295
  */
2186
2296
  async initialize() {
@@ -2194,20 +2304,13 @@ var Controller = class {
2194
2304
  this.data = tagData(this.options.datafile, "provided");
2195
2305
  }
2196
2306
  if (!this.data) {
2197
- this.transition("initializing:fallback");
2198
- let bundled;
2199
2307
  try {
2200
- bundled = await this.bundledSource.tryLoad();
2308
+ const bundled = await this.bundledSource.tryLoad();
2309
+ if (bundled) {
2310
+ this.data = tagData(bundled, "bundled");
2311
+ }
2201
2312
  } catch {
2202
2313
  }
2203
- if (this.state === "shutdown") {
2204
- throw new Error("@vercel/flags-core: Client is shut down");
2205
- }
2206
- if (bundled) this.data = tagData(bundled, "bundled");
2207
- }
2208
- if (this.headerSource.isAvailable()) {
2209
- this.transition("vercel");
2210
- return;
2211
2314
  }
2212
2315
  if (this.data) {
2213
2316
  if (this.options.stream.enabled) {
@@ -2273,7 +2376,6 @@ var Controller = class {
2273
2376
  this.unwireSourceEvents();
2274
2377
  this.streamSource.stop();
2275
2378
  this.pollingSource.stop();
2276
- this.headerSource.stop();
2277
2379
  this.data = this.options.datafile ? tagData(this.options.datafile, "provided") : void 0;
2278
2380
  this.transition("shutdown");
2279
2381
  await this.usageTracker.shutdown();
@@ -2309,9 +2411,10 @@ var Controller = class {
2309
2411
  this.data = tagData(fetched, "fetched");
2310
2412
  result = this.data;
2311
2413
  cacheStatus = "MISS";
2312
- } catch {
2313
- throw new Error(
2314
- "@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."
2315
2418
  );
2316
2419
  }
2317
2420
  }
@@ -2354,10 +2457,6 @@ var Controller = class {
2354
2457
  if (this.options.buildStep) {
2355
2458
  return this.resolveDataForBuildStep();
2356
2459
  }
2357
- if (this.state === "vercel") {
2358
- const result = await this.headerSource.read(this.data);
2359
- if (result) return result;
2360
- }
2361
2460
  if (this.data) {
2362
2461
  const cacheStatus = this.isConnected ? "HIT" : "STALE";
2363
2462
  return [this.data, cacheStatus];
@@ -2377,9 +2476,7 @@ var Controller = class {
2377
2476
  await this.streamSource.start();
2378
2477
  return true;
2379
2478
  } catch (error) {
2380
- if (error instanceof UnauthorizedError) {
2381
- this.unauthorized = true;
2382
- }
2479
+ this.noteUnauthorized(error);
2383
2480
  return false;
2384
2481
  }
2385
2482
  }
@@ -2407,9 +2504,7 @@ var Controller = class {
2407
2504
  return true;
2408
2505
  } catch (error) {
2409
2506
  clearTimeout(timeoutId);
2410
- if (error instanceof Error && error.message.includes("401")) {
2411
- this.unauthorized = true;
2412
- }
2507
+ this.noteUnauthorized(error);
2413
2508
  return false;
2414
2509
  }
2415
2510
  }
@@ -2462,6 +2557,11 @@ var Controller = class {
2462
2557
  return false;
2463
2558
  }
2464
2559
  }
2560
+ noteUnauthorized(error) {
2561
+ if (error instanceof UnauthorizedError || error instanceof Error && error.message.includes("401")) {
2562
+ this.unauthorized = true;
2563
+ }
2564
+ }
2465
2565
  // ---------------------------------------------------------------------------
2466
2566
  // Build step helpers
2467
2567
  // ---------------------------------------------------------------------------
@@ -2507,10 +2607,11 @@ var Controller = class {
2507
2607
  fetch: this.options.fetch
2508
2608
  });
2509
2609
  return tagData(fetched, "fetched");
2510
- } catch {
2610
+ } catch (error) {
2611
+ this.noteUnauthorized(error);
2511
2612
  }
2512
- throw new Error(
2513
- "@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."
2514
2615
  );
2515
2616
  }
2516
2617
  // ---------------------------------------------------------------------------
@@ -2544,8 +2645,17 @@ var Controller = class {
2544
2645
  } catch {
2545
2646
  }
2546
2647
  }
2547
- throw new Error(
2548
- "@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}`
2549
2659
  );
2550
2660
  }
2551
2661
  /**
@@ -2596,8 +2706,8 @@ var Controller = class {
2596
2706
  } catch {
2597
2707
  }
2598
2708
  }
2599
- throw new Error(
2600
- "@vercel/flags-core: No flag definitions available. Provide a datafile or bundled definitions."
2709
+ throw this.noDefinitionsError(
2710
+ ". Provide a datafile or bundled definitions."
2601
2711
  );
2602
2712
  }
2603
2713
  // ---------------------------------------------------------------------------
@@ -2670,88 +2780,6 @@ var Controller = class {
2670
2780
  }
2671
2781
  };
2672
2782
 
2673
- // src/controller/auth.ts
2674
- import { getVercelOidcToken as getVercelOidcToken2 } from "@vercel/oidc";
2675
-
2676
- // src/utils/sdk-keys.ts
2677
- var SDK_KEY_REGEX = /^vf_(?:server|client)_/;
2678
- function parseSdkKeyFromFlagsConnectionString(text) {
2679
- if (SDK_KEY_REGEX.test(text)) return text;
2680
- try {
2681
- if (!text.startsWith("flags:")) return null;
2682
- const params = new URLSearchParams(text.slice(6));
2683
- const sdkKey = params.get("sdkKey");
2684
- if (sdkKey && SDK_KEY_REGEX.test(sdkKey)) return sdkKey;
2685
- } catch {
2686
- }
2687
- return null;
2688
- }
2689
-
2690
- // src/controller/auth.ts
2691
- async function getOidcToken() {
2692
- try {
2693
- return await getVercelOidcToken2();
2694
- } catch {
2695
- throw new Error(
2696
- [
2697
- "@vercel/flags-core: Failed to get OIDC token.",
2698
- "Are you running in a Vercel Environment where OIDC tokens are available?",
2699
- "Did you mean to use an SDK Key instead? Use the environment variable FLAGS or pass it directly to the client."
2700
- ].join(" ")
2701
- );
2702
- }
2703
- }
2704
- function getProjectIdFromOidcToken(oidcToken) {
2705
- const tokenParts = oidcToken.split(".");
2706
- if (tokenParts.length !== 3 || !tokenParts[1]) {
2707
- throw new Error("@vercel/flags-core: Invalid OIDC token");
2708
- }
2709
- const payload = JSON.parse(
2710
- Buffer.from(tokenParts[1], "base64url").toString("utf8")
2711
- );
2712
- if (typeof payload.project_id !== "string" || !payload.project_id) {
2713
- throw new Error(
2714
- "@vercel/flags-core: Missing project_id claim in OIDC token"
2715
- );
2716
- }
2717
- return payload.project_id;
2718
- }
2719
- var Authentication = class {
2720
- sdkKey;
2721
- constructor(sdkKeyOrConnectionString) {
2722
- if (sdkKeyOrConnectionString !== void 0) {
2723
- if (typeof sdkKeyOrConnectionString !== "string") {
2724
- throw new Error(
2725
- `@vercel/flags-core: Invalid sdkKey. Expected string, got ${typeof sdkKeyOrConnectionString}`
2726
- );
2727
- }
2728
- const parsed = parseSdkKeyFromFlagsConnectionString(
2729
- sdkKeyOrConnectionString
2730
- );
2731
- if (!parsed) {
2732
- throw new Error("@vercel/flags-core: Missing sdkKey");
2733
- }
2734
- this.sdkKey = parsed;
2735
- }
2736
- }
2737
- async resolveToken() {
2738
- if (this.sdkKey) {
2739
- return this.sdkKey;
2740
- }
2741
- return await getOidcToken();
2742
- }
2743
- async resolveBundledDefinitionsLookup() {
2744
- if (this.sdkKey) {
2745
- return { type: "sdk-key", sdkKey: this.sdkKey };
2746
- }
2747
- const oidcToken = await this.resolveToken();
2748
- return {
2749
- type: "project-id",
2750
- projectId: getProjectIdFromOidcToken(oidcToken)
2751
- };
2752
- }
2753
- };
2754
-
2755
2783
  // src/index.make.ts
2756
2784
  function make(createRawClient, defaults) {
2757
2785
  let _defaultFlagsClient = null;
@@ -2769,7 +2797,11 @@ function make(createRawClient, defaults) {
2769
2797
  });
2770
2798
  return createRawClient({
2771
2799
  controller,
2772
- origin: { provider: "vercel", sdkKey: auth.sdkKey },
2800
+ origin: {
2801
+ provider: "vercel",
2802
+ sdkKey: auth.sdkKey,
2803
+ ...auth.sourceProjectId ? { projectId: auth.sourceProjectId } : null
2804
+ },
2773
2805
  waitUntil: waitUntil2,
2774
2806
  ...experimental_reportExposures ? { experimental_reportExposures } : {}
2775
2807
  });
@@ -2847,4 +2879,4 @@ export {
2847
2879
  resetDefaultFlagsClient,
2848
2880
  createClient
2849
2881
  };
2850
- //# sourceMappingURL=chunk-C22F472K.js.map
2882
+ //# sourceMappingURL=chunk-TDZBUVBG.js.map