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