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