@vercel/flags
Version:
Flags SDK by Vercel - The feature flags toolkit for Next.js and SvelteKit
613 lines (605 loc) • 19.2 kB
JavaScript
import {
normalizeOptions
} from "./chunk-ZNKTXALY.js";
import {
HeadersAdapter,
RequestCookiesAdapter,
decrypt,
internalReportValue,
reportValue,
setSpanAttribute,
trace
} from "./chunk-43J6AE7X.js";
import "./chunk-2UDLZC33.js";
// src/next/index.ts
import { RequestCookies } from "@edge-runtime/cookies";
// src/next/async-memoize-one.ts
function memoizeOne(fn, isEqual, { cachePromiseRejection = false } = {}) {
let calledOnce = false;
let oldArgs;
let lastResult;
function memoized(...newArgs) {
if (calledOnce && isEqual(newArgs, oldArgs))
return lastResult;
lastResult = fn.apply(this, newArgs);
if (!cachePromiseRejection && lastResult.catch) {
lastResult.catch(() => calledOnce = false);
}
calledOnce = true;
oldArgs = newArgs;
return lastResult;
}
return memoized;
}
// src/next/overrides.ts
var memoizedDecrypt = memoizeOne(
(text) => decrypt(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/next/serialization.ts
import { CompactSign, base64url, compactVerify } from "jose";
var memoizedVerify = memoizeOne(
(code, secret) => compactVerify(code, base64url.decode(secret), {
algorithms: ["HS256"]
}),
(a, b) => a[0] === b[0] && a[1] === b[1],
// only first two args matter
{ cachePromiseRejection: true }
);
var memoizedSign = memoizeOne(
(uint8Array, secret) => new CompactSign(uint8Array).setProtectedHeader({ alg: "HS256" }).sign(base64url.decode(secret)),
(a, b) => (
// matchedIndices array must be equal
a[0].length === b[0].length && a[0].every((v, i) => b[0][i] === v) && // secrets must be equal
a[1] === b[1]
),
{ cachePromiseRejection: true }
);
function splitUint8Array(array, index) {
const firstHalf = array.slice(0, index);
const secondHalf = array.slice(index);
return [firstHalf, secondHalf];
}
async function deserialize(code, flags, secret) {
const { payload } = await memoizedVerify(code, secret);
const [matchedIndicesArray, valuesUint8Array] = payload.length === flags.length ? [payload] : splitUint8Array(payload, flags.length);
const valuesArray = valuesUint8Array ? (
// re-add opening and closing brackets since we remove them when serializing
JSON.parse(`[${new TextDecoder().decode(valuesUint8Array)}]`)
) : null;
let spilled = 0;
return matchedIndicesArray.reduce(
(acc, valueIndex, index) => {
const flag2 = flags[index];
if (!flag2) {
throw new Error(`@vercel/flags: No flag at index ${index}`);
}
switch (valueIndex) {
case 253 /* BOOLEAN_FALSE */:
acc[flag2.key] = false;
break;
case 254 /* BOOLEAN_TRUE */:
acc[flag2.key] = true;
break;
case 255 /* UNLISTED_VALUE */:
acc[flag2.key] = valuesArray[spilled++];
break;
case 252 /* NULL */:
acc[flag2.key] = null;
break;
default:
acc[flag2.key] = flag2.options?.[valueIndex]?.value;
}
return acc;
},
{}
);
}
var matchIndex = /* @__PURE__ */ function() {
const stringifiedOptionsCache = /* @__PURE__ */ new Map();
return function matchIndex2(options, value) {
const t = typeof value;
if (value === null || t === "boolean" || t === "string" || t === "number") {
return options.findIndex((v) => v.value === value);
}
const stringifiedValue = JSON.stringify(value);
let stringifiedOptions = stringifiedOptionsCache.get(options);
if (!stringifiedOptions) {
stringifiedOptions = options.map((o) => JSON.stringify(o.value));
stringifiedOptionsCache.set(options, stringifiedOptions);
}
return stringifiedOptions.findIndex(
(stringifiedOption) => stringifiedOption === stringifiedValue
);
};
}();
function joinUint8Arrays(array1, array2) {
const combined = new Uint8Array(array1.length + array2.length);
combined.set(array1);
combined.set(array2, array1.length);
return combined;
}
async function serialize(flagSet, flags, secret) {
const unlistedValues = [];
const matchedIndices = new Uint8Array(
flags.map((flag2) => {
const options = Array.isArray(flag2.options) ? flag2.options : [];
const value = flagSet[flag2.key];
if (!Object.prototype.hasOwnProperty.call(flagSet, flag2.key) || value === void 0) {
throw new Error(`@vercel/flags: Missing value for flag "${flag2.key}"`);
}
switch (value) {
case null:
return 252 /* NULL */;
case false:
return 253 /* BOOLEAN_FALSE */;
case true:
return 254 /* BOOLEAN_TRUE */;
}
const matchedIndex = matchIndex(options, value);
if (matchedIndex > -1)
return matchedIndex;
unlistedValues.push(value);
return 255 /* UNLISTED_VALUE */;
})
);
let joined;
if (unlistedValues.length > 0) {
const jsonArray = new TextEncoder().encode(
// slicing removes opening and closing array brackets as they'll always be
// there and we can re-add them when deserializing
JSON.stringify(unlistedValues).slice(1, -1)
);
joined = joinUint8Arrays(matchedIndices, jsonArray);
} else {
joined = matchedIndices;
}
return memoizedSign(joined, secret);
}
// src/next/precompute.ts
async function evaluate(flags) {
return Promise.all(flags.map((flag2) => flag2()));
}
async function precompute(flags) {
const values = await evaluate(flags);
return serialize2(flags, values);
}
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("@vercel/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("@vercel/flags: Can not serialize 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(
"@vercel/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(
"@vercel/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/next/is-internal-next-error.ts
var REACT_POSTPONE_TYPE = Symbol.for("react.postpone");
function isPostpone(error) {
return typeof error === "object" && error !== null && "$$typeof" in error && error.$$typeof === REACT_POSTPONE_TYPE;
}
function isInternalNextError(error) {
if (isPostpone(error))
return true;
if (typeof error !== "object" || error === null || !("digest" in error) || typeof error.digest !== "string") {
return false;
}
const errorCode = error.digest.split(";")[0];
return errorCode === "NEXT_REDIRECT" || errorCode === "DYNAMIC_SERVER_USAGE" || errorCode === "BAILOUT_TO_CLIENT_SIDE_RENDERING" || errorCode === "NEXT_NOT_FOUND";
}
// src/next/dedupe.ts
function createCacheNode() {
return {
s: 0 /* UNTERMINATED */,
v: void 0,
o: null,
p: null
};
}
function dedupe(fn) {
const requestStore = /* @__PURE__ */ new WeakMap();
return async function(...args) {
const { headers } = await import("next/headers");
const h = await headers();
let cacheNode = requestStore.get(h);
if (!cacheNode) {
cacheNode = createCacheNode();
requestStore.set(h, 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;
}
};
}
// src/next/index.ts
var evaluationCache = /* @__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);
}
var transformMap = /* @__PURE__ */ new WeakMap();
var headersMap = /* @__PURE__ */ new WeakMap();
var cookiesMap = /* @__PURE__ */ new WeakMap();
var identifyArgsMap = /* @__PURE__ */ new WeakMap();
function transformToHeaders(incomingHeaders) {
const cached = transformMap.get(incomingHeaders);
if (cached !== void 0)
return cached;
const headers = new Headers();
for (const [key, value] of Object.entries(incomingHeaders)) {
if (Array.isArray(value)) {
value.forEach((item) => headers.append(key, item));
} else if (value !== void 0) {
headers.append(key, value);
}
}
transformMap.set(incomingHeaders, headers);
return headers;
}
function sealHeaders(headers) {
const cached = headersMap.get(headers);
if (cached !== void 0)
return cached;
const sealed = HeadersAdapter.seal(headers);
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));
cookiesMap.set(headers, sealed);
return sealed;
}
function isIdentifyFunction(identify) {
return typeof identify === "function";
}
async function getEntities(identify, dedupeCacheKey, readonlyHeaders, readonlyCookies) {
if (!identify)
return void 0;
if (!isIdentifyFunction(identify))
return identify;
const args = identifyArgsMap.get(dedupeCacheKey);
if (args)
return identify(...args);
const nextArgs = [
{ headers: readonlyHeaders, cookies: readonlyCookies }
];
identifyArgsMap.set(dedupeCacheKey, nextArgs);
return identify(...nextArgs);
}
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(
`@vercel/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 getRun(definition, decide) {
return async function run(options) {
let readonlyHeaders;
let readonlyCookies;
let dedupeCacheKey;
if (options.request) {
const headers = transformToHeaders(options.request.headers);
readonlyHeaders = sealHeaders(headers);
readonlyCookies = sealCookies(headers);
dedupeCacheKey = options.request.headers;
} else {
const { headers, cookies } = await import("next/headers");
const [headersStore, cookiesStore] = await Promise.all([
headers(),
cookies()
]);
readonlyHeaders = headersStore;
readonlyCookies = cookiesStore;
dedupeCacheKey = headersStore;
}
const overrides = await getOverrides(
readonlyCookies.get("vercel-flag-overrides")?.value
);
const entities = await getEntities(
options.identify,
dedupeCacheKey,
readonlyHeaders,
readonlyCookies
);
const entitiesKey = JSON.stringify(entities) ?? "";
const cachedValue = getCachedValuePromise(
readonlyHeaders,
definition.key,
entitiesKey
);
if (cachedValue !== void 0) {
setSpanAttribute("method", "cached");
const value = await cachedValue;
return value;
}
if (overrides && overrides[definition.key] !== void 0) {
setSpanAttribute("method", "override");
const decision2 = overrides[definition.key];
setCachedValuePromise(
readonlyHeaders,
definition.key,
entitiesKey,
Promise.resolve(decision2)
);
internalReportValue(definition.key, decision2, {
reason: "override"
});
return decision2;
}
const decisionPromise = (async () => {
return decide({
headers: readonlyHeaders,
cookies: readonlyCookies,
entities
});
})().then(
(value) => {
if (value !== void 0)
return value;
if (definition.defaultValue !== void 0)
return definition.defaultValue;
throw new Error(
`@vercel/flags: Flag "${definition.key}" must have a defaultValue or a decide function that returns a value`
);
},
(error) => {
if (isInternalNextError(error))
throw error;
if (definition.defaultValue !== void 0) {
if (process.env.NODE_ENV === "development") {
console.info(
`@vercel/flags: Flag "${definition.key}" is falling back to its defaultValue`
);
} else {
console.warn(
`@vercel/flags: Flag "${definition.key}" is falling back to its defaultValue after catching the following error`,
error
);
}
return definition.defaultValue;
}
console.warn(
`@vercel/flags: Flag "${definition.key}" could not be evaluated`
);
throw error;
}
);
setCachedValuePromise(
readonlyHeaders,
definition.key,
entitiesKey,
decisionPromise
);
const decision = await decisionPromise;
if (definition.config?.reportValue !== false) {
reportValue(definition.key, decision);
}
return decision;
};
}
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;
}
function flag(definition) {
const decide = getDecide(definition);
const identify = getIdentify(definition);
const run = getRun(definition, decide);
const origin = getOrigin(definition);
const flag2 = trace(
async (...args) => {
setSpanAttribute("method", "decided");
if (typeof args[0] === "string" && Array.isArray(args[1])) {
const [precomputedCode, precomputedGroup, secret] = args;
if (precomputedCode && precomputedGroup) {
setSpanAttribute("method", "precomputed");
return getPrecomputed(
flag2,
precomputedGroup,
precomputedCode,
secret
);
}
}
if (args[0] && typeof args[0] === "object" && "headers" in args[0]) {
const [request] = args;
return run({ identify, request });
}
return run({ identify, request: void 0 });
},
{
name: "flag",
isVerboseTrace: false,
attributes: { key: definition.key }
}
);
flag2.key = definition.key;
flag2.defaultValue = definition.defaultValue;
flag2.origin = origin;
flag2.options = normalizeOptions(definition.options);
flag2.description = definition.description;
flag2.identify = identify ? trace(identify, {
isVerboseTrace: false,
name: "identify",
attributes: { key: definition.key }
}) : identify;
flag2.decide = trace(decide, {
isVerboseTrace: false,
name: "decide",
attributes: { key: definition.key }
});
flag2.run = trace(run, {
isVerboseTrace: false,
name: "run",
attributes: { key: definition.key }
});
return flag2;
}
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: [] };
}
export {
combine,
dedupe,
deserialize2 as deserialize,
evaluate,
flag,
generatePermutations,
getPrecomputed,
getProviderData,
precompute,
serialize2 as serialize
};
//# sourceMappingURL=next.js.map