@web-widget/flags-kit
Version:
Flags SDK by Vercel - The feature flags toolkit for Next.js, SvelteKit, and Web Router - Enhanced fork with improved Web Router support
744 lines (736 loc) • 23.1 kB
JavaScript
import {
deserialize,
memoizeOne,
normalizeOptions,
serialize
} from "./chunk-O6VYPARG.js";
import {
HeadersAdapter,
RequestCookiesAdapter,
decryptOverrides,
internalReportValue,
reportValue,
setSpanAttribute,
trace,
verifyAccess,
version
} from "./chunk-BQ2THYCT.js";
import {
safeJsonStringify
} from "./chunk-2UDLZC33.js";
// src/web-router/index.ts
import { RequestCookies } from "@edge-runtime/cookies";
import { context as context2 } from "@web-widget/context";
// src/web-router/overrides.ts
var memoizedDecrypt = memoizeOne(
(text) => decryptOverrides(text),
(a, b) => a[0] === b[0],
// only the first argument gets compared
{ cachePromiseRejection: true }
);
async function getOverrides(cookie) {
if (typeof cookie === "string" && cookie !== "") {
const cookieOverrides = await memoizedDecrypt(cookie);
return cookieOverrides ?? null;
}
return null;
}
// src/web-router/precompute.ts
async function evaluate(flags) {
return Promise.all(flags.map((flag2) => flag2()));
}
async function precompute(flags) {
const values = await evaluate(flags);
const { context: context3 } = await import("@web-widget/context");
const store = context3().state._flag;
const secret = store?.secret ?? process.env.FLAGS_SECRET;
return serialize2(flags, values, secret);
}
function combine(flags, values) {
return Object.fromEntries(flags.map((flag2, i) => [flag2.key, values[i]]));
}
async function serialize2(flags, values, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error("flags: Can not serialize due to missing secret");
}
return serialize(combine(flags, values), flags, secret);
}
async function deserialize2(flags, code, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error("flags: Can not deserialize due to missing secret");
}
return deserialize(code, flags, secret);
}
async function getPrecomputed(flagOrFlags, precomputeFlags, code, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error(
"flags: getPrecomputed was called without a secret. Please set FLAGS_SECRET environment variable."
);
}
const flagSet = await deserialize2(precomputeFlags, code, secret);
if (Array.isArray(flagOrFlags)) {
return flagOrFlags.map((flag2) => flagSet[flag2.key]);
} else {
return flagSet[flagOrFlags.key];
}
}
function* cartesianIterator(items) {
const remainder = items.length > 1 ? cartesianIterator(items.slice(1)) : [[]];
for (let r of remainder)
for (let h of items.at(0))
yield [h, ...r];
}
async function generatePermutations(flags, filter = null, secret = process.env.FLAGS_SECRET) {
if (!secret) {
throw new Error(
"flags: generatePermutations was called without a secret. Please set FLAGS_SECRET environment variable."
);
}
const options = flags.map((flag2) => {
if (!flag2.options)
return [false, true];
return flag2.options.map((option) => option.value);
});
const list = [];
for (const permutation of cartesianIterator(options)) {
const permObject = permutation.reduce(
(acc, value, index) => {
acc[flags[index].key] = value;
return acc;
},
{}
);
if (!filter || filter(permObject))
list.push(permObject);
}
return Promise.all(list.map((values) => serialize(values, flags, secret)));
}
// src/web-router/html-transform.ts
function createFlagScriptInjectionTransform(scriptContent) {
let state = "normal";
let matchBuffer = "";
const bodyEndTag = "</body>";
let injected = false;
return new TransformStream({
async transform(chunk, controller) {
const text = new TextDecoder().decode(chunk);
let outputBuffer = "";
let i = 0;
while (i < text.length) {
const char = text[i];
if (state === "normal") {
if (char === "<") {
state = "partial_match";
matchBuffer = char;
} else {
outputBuffer += char;
}
} else if (state === "partial_match") {
matchBuffer += char;
if (bodyEndTag.startsWith(matchBuffer)) {
if (matchBuffer === bodyEndTag) {
if (!injected) {
const content = await scriptContent();
if (content) {
const scriptTag = `<script type="application/json" data-flag-values>${content}</script>`;
outputBuffer += scriptTag;
injected = true;
}
}
outputBuffer += bodyEndTag;
state = "normal";
matchBuffer = "";
}
} else {
const bufferToOutput = matchBuffer.slice(0, -1);
outputBuffer += bufferToOutput;
state = "normal";
matchBuffer = "";
if (char === "<") {
state = "partial_match";
matchBuffer = char;
} else {
outputBuffer += char;
}
}
}
i++;
}
if (outputBuffer) {
controller.enqueue(new TextEncoder().encode(outputBuffer));
}
},
async flush(controller) {
if (matchBuffer) {
controller.enqueue(new TextEncoder().encode(matchBuffer));
}
}
});
}
// src/web-router/env.ts
var default_secret = process?.env?.FLAGS_SECRET;
async function tryGetSecret(secret) {
secret = secret || default_secret;
if (!secret) {
throw new Error(
"flags: No secret provided. Set an environment variable FLAGS_SECRET or provide a secret to the function."
);
}
return secret;
}
// src/web-router/error-handling.ts
async function safeExecute(fn, defaultValue, errorHandler) {
try {
return await fn();
} catch (error) {
if (errorHandler) {
errorHandler(error);
}
if (defaultValue !== void 0) {
return defaultValue;
}
throw error;
}
}
// src/web-router/dedupe.ts
import { context } from "@web-widget/context";
function createCacheNode() {
return {
s: 0 /* UNTERMINATED */,
v: void 0,
o: null,
p: null
};
}
var cacheRegistry = /* @__PURE__ */ new WeakMap();
function dedupe(fn) {
const requestStore = /* @__PURE__ */ new WeakMap();
const dedupedFn = async function(...args) {
const store = context().state._flag;
if (!store?.request) {
throw new Error(
"dedupe: No request context found. Make sure dedupe is called within a web-router request handler."
);
}
const request = store.request;
let cacheNode = requestStore.get(request);
if (!cacheNode) {
cacheNode = createCacheNode();
requestStore.set(request, cacheNode);
}
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (typeof arg === "function" || typeof arg === "object" && arg !== null) {
let objectCache = cacheNode.o;
if (objectCache === null) {
cacheNode.o = objectCache = /* @__PURE__ */ new WeakMap();
}
const objectNode = objectCache.get(arg);
if (objectNode === void 0) {
cacheNode = createCacheNode();
objectCache.set(arg, cacheNode);
} else {
cacheNode = objectNode;
}
} else {
let primitiveCache = cacheNode.p;
if (primitiveCache === null) {
cacheNode.p = primitiveCache = /* @__PURE__ */ new Map();
}
const primitiveNode = primitiveCache.get(arg);
if (primitiveNode === void 0) {
cacheNode = createCacheNode();
primitiveCache.set(arg, cacheNode);
} else {
cacheNode = primitiveNode;
}
}
}
if (cacheNode.s === 1 /* TERMINATED */) {
return cacheNode.v;
}
if (cacheNode.s === 2 /* ERRORED */) {
throw cacheNode.v;
}
try {
const result = fn.apply(this, args);
cacheNode.s = 1 /* TERMINATED */;
cacheNode.v = result;
return result;
} catch (error) {
cacheNode.s = 2 /* ERRORED */;
cacheNode.v = error;
throw error;
}
};
cacheRegistry.set(dedupedFn, requestStore);
return dedupedFn;
}
function clearDedupeCacheForCurrentRequest(dedupedFn) {
if (typeof dedupedFn !== "function") {
throw new Error("dedupe: not a function");
}
const requestStore = cacheRegistry.get(dedupedFn);
if (!requestStore) {
throw new Error("dedupe: cache not found");
}
const store = context().state._flag;
if (!store?.request) {
throw new Error(
"clearDedupeCacheForCurrentRequest: No request context found. Make sure this is called within a web-router request handler."
);
}
return requestStore.delete(store.request);
}
// src/web-router/index.ts
function hasOwnProperty(obj, prop) {
return obj.hasOwnProperty(prop);
}
var evaluationCache = /* @__PURE__ */ new WeakMap();
var headersMap = /* @__PURE__ */ new WeakMap();
var cookiesMap = /* @__PURE__ */ new WeakMap();
function getCachedValuePromise(headers, flagKey, entitiesKey) {
const map = evaluationCache.get(headers)?.get(flagKey);
if (!map)
return void 0;
return map.get(entitiesKey);
}
function setCachedValuePromise(headers, flagKey, entitiesKey, flagValue) {
const byHeaders = evaluationCache.get(headers);
if (!byHeaders) {
evaluationCache.set(
headers,
/* @__PURE__ */ new Map([[flagKey, /* @__PURE__ */ new Map([[entitiesKey, flagValue]])]])
);
return;
}
const byFlagKey = byHeaders.get(flagKey);
if (!byFlagKey) {
byHeaders.set(flagKey, /* @__PURE__ */ new Map([[entitiesKey, flagValue]]));
return;
}
byFlagKey.set(entitiesKey, flagValue);
}
function sealHeaders(headers) {
const cached = headersMap.get(headers);
if (cached !== void 0)
return cached;
const sealed = HeadersAdapter.seal(
headers,
"Headers cannot be modified in web-router. Headers are read-only during request processing."
);
headersMap.set(headers, sealed);
return sealed;
}
function sealCookies(headers) {
const cached = cookiesMap.get(headers);
if (cached !== void 0)
return cached;
const sealed = RequestCookiesAdapter.seal(
new RequestCookies(headers),
"Cookies cannot be modified in web-router. Use Response.headers to set cookies in the response."
);
cookiesMap.set(headers, sealed);
return sealed;
}
async function resolveObjectPromises(obj) {
const entries = Object.entries(obj);
const resolvedEntries = await Promise.all(
entries.map(async ([key, promise]) => {
const value = await promise;
return [key, value];
})
);
return Object.fromEntries(resolvedEntries);
}
function getDecide(definition) {
return function decide(params) {
if (typeof definition.decide === "function") {
return definition.decide(params);
}
if (typeof definition.adapter?.decide === "function") {
return definition.adapter.decide({ key: definition.key, ...params });
}
throw new Error(`flags: No decide function provided for ${definition.key}`);
};
}
function getIdentify(definition) {
return function identify(params) {
if (typeof definition.identify === "function") {
return definition.identify(params);
}
if (typeof definition.adapter?.identify === "function") {
return definition.adapter.identify(params);
}
return definition.identify;
};
}
function getOrigin(definition) {
if (definition.origin)
return definition.origin;
if (typeof definition.adapter?.origin === "function")
return definition.adapter.origin(definition.key);
return definition.adapter?.origin;
}
var requestMap = /* @__PURE__ */ new WeakMap();
function getRun(definition, decide) {
return async function run(options) {
let store = context2().state._flag;
if (!store) {
if (options.request) {
const cached = requestMap.get(options.request);
if (!cached) {
store = createContext(options.request, await tryGetSecret());
requestMap.set(options.request, store);
} else {
store = cached;
}
} else {
throw new Error("flags: Neither context found nor Request provided");
}
}
const headers = sealHeaders(store.request.headers);
const cookies = sealCookies(store.request.headers);
const overridesCookie = cookies.get("vercel-flag-overrides")?.value;
const overrides = await getOverrides(overridesCookie);
let entities;
if (options.identify) {
if (typeof options.identify === "function") {
entities = await options.identify({
headers,
cookies
});
} else {
entities = options.identify;
}
}
const entitiesKey = JSON.stringify(entities) ?? "";
const cachedValue = getCachedValuePromise(
store.request.headers,
definition.key,
entitiesKey
);
if (cachedValue !== void 0) {
return await cachedValue;
}
if (overrides && hasOwnProperty(overrides, definition.key)) {
const value2 = overrides[definition.key];
if (typeof value2 !== "undefined") {
setSpanAttribute("method", "override");
const resolvedPromise = Promise.resolve(value2);
setCachedValuePromise(
store.request.headers,
definition.key,
entitiesKey,
resolvedPromise
);
internalReportValue(definition.key, value2, {
reason: "override"
});
return value2;
}
}
const valuePromise = safeExecute(
() => decide({
headers,
cookies,
entities
}),
definition.defaultValue,
(error) => {
if (process.env.NODE_ENV === "development") {
console.info(
`flags: Flag "${definition.key}" is falling back to its defaultValue`
);
} else {
console.warn(
`flags: Flag "${definition.key}" is falling back to its defaultValue after catching the following error`,
error
);
}
}
).then((value2) => {
if (value2 !== void 0)
return value2;
if (definition.defaultValue !== void 0)
return definition.defaultValue;
throw new Error(
`flags: Flag "${definition.key}" must have a defaultValue or a decide function that returns a value`
);
});
setCachedValuePromise(
store.request.headers,
definition.key,
entitiesKey,
valuePromise
);
const value = await valuePromise;
if (definition.config?.reportValue !== false) {
reportValue(definition.key, value);
}
return value;
};
}
function flag(definition) {
const decide = getDecide(definition);
const identify = getIdentify(definition);
const run = getRun(definition, decide);
const origin = getOrigin(definition);
const flagImpl = trace(
async function flagImpl2(...args) {
setSpanAttribute("method", "decided");
if (typeof args[0] === "string" && Array.isArray(args[1])) {
setSpanAttribute("method", "precomputed");
const [precomputedCode, precomputedGroup, secret] = args;
const effectiveSecret = secret ?? context2().state._flag?.secret ?? await tryGetSecret();
const tempFlag = { key: definition.key };
return getPrecomputed(
tempFlag,
precomputedGroup,
precomputedCode,
effectiveSecret
);
}
let store = context2().state._flag;
if (!store) {
if (args[0] instanceof Request) {
const cached = requestMap.get(args[0]);
if (!cached) {
store = createContext(
args[0],
args[1] ?? await tryGetSecret()
);
requestMap.set(args[0], store);
} else {
store = cached;
}
} else {
throw new Error("flags: Neither context found nor Request provided");
}
}
const headers = sealHeaders(store.request.headers);
const cookies = sealCookies(store.request.headers);
const overridesCookie = cookies.get("vercel-flag-overrides")?.value;
const overrides = await getOverrides(overridesCookie);
let entities;
if (identify) {
if (!store.identifiers.has(identify)) {
const entities2 = identify({
headers,
cookies
});
store.identifiers.set(identify, entities2);
}
entities = await store.identifiers.get(identify);
}
const entitiesKey = JSON.stringify(entities) ?? "";
const cachedValue = getCachedValuePromise(
store.request.headers,
definition.key,
entitiesKey
);
if (cachedValue !== void 0) {
setSpanAttribute("method", "cached");
const value2 = await cachedValue;
return value2;
}
if (overrides && hasOwnProperty(overrides, definition.key)) {
const value2 = overrides[definition.key];
if (typeof value2 !== "undefined") {
setSpanAttribute("method", "override");
const resolvedPromise = Promise.resolve(value2);
store.usedFlags[definition.key] = resolvedPromise;
setCachedValuePromise(
store.request.headers,
definition.key,
entitiesKey,
resolvedPromise
);
internalReportValue(definition.key, value2, {
reason: "override"
});
return value2;
}
}
const valuePromise = safeExecute(
() => decide({
headers,
cookies,
entities
}),
definition.defaultValue,
(error) => {
if (process.env.NODE_ENV === "development") {
console.info(
`flags: Flag "${definition.key}" is falling back to its defaultValue`
);
} else {
console.warn(
`flags: Flag "${definition.key}" is falling back to its defaultValue after catching the following error`,
error
);
}
}
).then((value2) => {
if (value2 !== void 0)
return value2;
if (definition.defaultValue !== void 0)
return definition.defaultValue;
throw new Error(
`flags: Flag "${definition.key}" must have a defaultValue or a decide function that returns a value`
);
});
store.usedFlags[definition.key] = valuePromise;
setCachedValuePromise(
store.request.headers,
definition.key,
entitiesKey,
valuePromise
);
const value = await valuePromise;
if (definition.config?.reportValue !== false) {
reportValue(definition.key, value);
}
return value;
},
{
name: "flag",
isVerboseTrace: false,
attributes: { key: definition.key }
}
);
flagImpl.key = definition.key;
flagImpl.defaultValue = definition.defaultValue;
flagImpl.origin = origin;
flagImpl.description = definition.description;
flagImpl.options = normalizeOptions(definition.options);
flagImpl.decide = trace(decide, {
isVerboseTrace: false,
name: "decide",
attributes: { key: definition.key }
});
flagImpl.identify = identify ? trace(identify, {
isVerboseTrace: false,
name: "identify",
attributes: { key: definition.key }
}) : identify;
flagImpl.run = trace(run, {
isVerboseTrace: false,
name: "run",
attributes: { key: definition.key }
});
return flagImpl;
}
function getProviderData(flags) {
const definitions = Object.values(flags).filter((i) => !Array.isArray(i)).reduce((acc, d) => {
acc[d.key] = {
options: d.options,
origin: d.origin,
description: d.description,
defaultValue: d.defaultValue,
declaredInCode: true
};
return acc;
}, {});
return { definitions, hints: [] };
}
function createContext(request, secret, params) {
return {
request,
secret,
usedFlags: {},
identifiers: /* @__PURE__ */ new Map()
};
}
function createHandle({
secret,
flags
}) {
return async function handle({ request, params, render }, next) {
secret ?? (secret = await tryGetSecret(secret));
if (flags && // avoid creating the URL object for every request by checking with includes() first
request.url.includes("/.well-known/") && new URL(request.url).pathname === "/.well-known/vercel/flags") {
return handleWellKnownFlagsRoute(request.headers, secret, flags);
}
if (!render) {
return next();
}
context2().state._flag = createContext(request, secret, params);
const result = await next();
if (result && result instanceof Response && result.body) {
const contentType = result.headers.get("content-type")?.toLowerCase();
if (contentType && contentType.includes("text/html")) {
if (isVercelEnvironment()) {
const transformStream = createFlagScriptInjectionTransform(
async () => {
const store = context2().state._flag;
if (!store || Object.keys(store.usedFlags).length === 0)
return "";
const flagValues = await resolveObjectPromises(store.usedFlags);
const flagsArray = Object.keys(flagValues).map((key) => ({
key,
options: void 0
}));
const serializedFlagValues = await serialize(
flagValues,
flagsArray,
secret
// secret is guaranteed to be defined at this point
);
return safeJsonStringify(serializedFlagValues);
}
);
const modifiedBody = result.body.pipeThrough(transformStream);
return new Response(modifiedBody, {
status: result.status,
statusText: result.statusText,
headers: result.headers
});
}
}
}
return result;
};
}
function isVercelEnvironment() {
return !!(process.env.VERCEL || process.env.VERCEL_ENV);
}
async function handleWellKnownFlagsRoute(headers, secret, flags) {
const access = await verifyAccess(headers.get("authorization"), secret);
if (!access)
return new Response(null, { status: 401 });
const providerData = getProviderData(flags);
return Response.json(providerData, {
headers: { "x-flags-sdk-version": version }
});
}
function createFlagsDiscoveryEndpoint(getApiData, options) {
return async (context3) => {
const access = await verifyAccess(
context3.request.headers.get("authorization"),
options?.secret
);
if (!access)
return Response.json(null, { status: 401 });
const apiData = await getApiData(context3);
return Response.json(apiData, {
headers: { "x-flags-sdk-version": version }
});
};
}
export {
clearDedupeCacheForCurrentRequest,
combine,
createFlagsDiscoveryEndpoint,
createHandle,
dedupe,
deserialize2 as deserialize,
evaluate,
flag,
generatePermutations,
getPrecomputed,
getProviderData,
precompute,
serialize2 as serialize
};
//# sourceMappingURL=web-router.js.map