UNPKG

@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 (705 loc) 26.7 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } async function _asyncNullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return await rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _chunkQLJ65HXQcjs = require('./chunk-QLJ65HXQ.cjs'); var _chunkROBQSGBHcjs = require('./chunk-ROBQSGBH.cjs'); var _chunkMEG7RDX7cjs = require('./chunk-MEG7RDX7.cjs'); // src/web-router/index.ts var _cookies = require('@edge-runtime/cookies'); var _context = require('@web-widget/context'); // src/web-router/overrides.ts var memoizedDecrypt = _chunkQLJ65HXQcjs.memoizeOne.call(void 0, (text) => _chunkROBQSGBHcjs.decryptOverrides.call(void 0, 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 _nullishCoalesce(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 Promise.resolve().then(() => _interopRequireWildcard(require("@web-widget/context"))); const store = context3().state._flag; const secret = _nullishCoalesce(_optionalChain([store, 'optionalAccess', _ => _.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 _chunkQLJ65HXQcjs.serialize.call(void 0, 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 _chunkQLJ65HXQcjs.deserialize.call(void 0, 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) => _chunkQLJ65HXQcjs.serialize.call(void 0, 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 = _optionalChain([process, 'optionalAccess', _2 => _2.env, 'optionalAccess', _3 => _3.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 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.context.call(void 0, ).state._flag; if (!_optionalChain([store, 'optionalAccess', _4 => _4.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.context.call(void 0, ).state._flag; if (!_optionalChain([store, 'optionalAccess', _5 => _5.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 = _optionalChain([evaluationCache, 'access', _6 => _6.get, 'call', _7 => _7(headers), 'optionalAccess', _8 => _8.get, 'call', _9 => _9(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 = _chunkROBQSGBHcjs.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 = _chunkROBQSGBHcjs.RequestCookiesAdapter.seal( new (0, _cookies.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 _optionalChain([definition, 'access', _10 => _10.adapter, 'optionalAccess', _11 => _11.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 _optionalChain([definition, 'access', _12 => _12.adapter, 'optionalAccess', _13 => _13.identify]) === "function") { return definition.adapter.identify(params); } return definition.identify; }; } function getOrigin(definition) { if (definition.origin) return definition.origin; if (typeof _optionalChain([definition, 'access', _14 => _14.adapter, 'optionalAccess', _15 => _15.origin]) === "function") return definition.adapter.origin(definition.key); return _optionalChain([definition, 'access', _16 => _16.adapter, 'optionalAccess', _17 => _17.origin]); } var requestMap = /* @__PURE__ */ new WeakMap(); function getRun(definition, decide) { return async function run(options) { let store = _context.context.call(void 0, ).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 = _optionalChain([cookies, 'access', _18 => _18.get, 'call', _19 => _19("vercel-flag-overrides"), 'optionalAccess', _20 => _20.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 = _nullishCoalesce(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") { _chunkROBQSGBHcjs.setSpanAttribute.call(void 0, "method", "override"); const resolvedPromise = Promise.resolve(value2); setCachedValuePromise( store.request.headers, definition.key, entitiesKey, resolvedPromise ); _chunkROBQSGBHcjs.internalReportValue.call(void 0, 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 (_optionalChain([definition, 'access', _21 => _21.config, 'optionalAccess', _22 => _22.reportValue]) !== false) { _chunkROBQSGBHcjs.reportValue.call(void 0, 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 = _chunkROBQSGBHcjs.trace.call(void 0, async function flagImpl2(...args) { _chunkROBQSGBHcjs.setSpanAttribute.call(void 0, "method", "decided"); if (typeof args[0] === "string" && Array.isArray(args[1])) { _chunkROBQSGBHcjs.setSpanAttribute.call(void 0, "method", "precomputed"); const [precomputedCode, precomputedGroup, secret] = args; const effectiveSecret = await _asyncNullishCoalesce(await _asyncNullishCoalesce(secret, async () => ( _optionalChain([_context.context.call(void 0, ), 'access', _23 => _23.state, 'access', _24 => _24._flag, 'optionalAccess', _25 => _25.secret]))), async () => ( await tryGetSecret())); const tempFlag = { key: definition.key }; return getPrecomputed( tempFlag, precomputedGroup, precomputedCode, effectiveSecret ); } let store = _context.context.call(void 0, ).state._flag; if (!store) { if (args[0] instanceof Request) { const cached = requestMap.get(args[0]); if (!cached) { store = createContext( args[0], await _asyncNullishCoalesce(args[1], async () => ( 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 = _optionalChain([cookies, 'access', _26 => _26.get, 'call', _27 => _27("vercel-flag-overrides"), 'optionalAccess', _28 => _28.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 = _nullishCoalesce(JSON.stringify(entities), () => ( "")); const cachedValue = getCachedValuePromise( store.request.headers, definition.key, entitiesKey ); if (cachedValue !== void 0) { _chunkROBQSGBHcjs.setSpanAttribute.call(void 0, "method", "cached"); const value2 = await cachedValue; return value2; } if (overrides && hasOwnProperty(overrides, definition.key)) { const value2 = overrides[definition.key]; if (typeof value2 !== "undefined") { _chunkROBQSGBHcjs.setSpanAttribute.call(void 0, "method", "override"); const resolvedPromise = Promise.resolve(value2); store.usedFlags[definition.key] = resolvedPromise; setCachedValuePromise( store.request.headers, definition.key, entitiesKey, resolvedPromise ); _chunkROBQSGBHcjs.internalReportValue.call(void 0, 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 (_optionalChain([definition, 'access', _29 => _29.config, 'optionalAccess', _30 => _30.reportValue]) !== false) { _chunkROBQSGBHcjs.reportValue.call(void 0, 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 = _chunkQLJ65HXQcjs.normalizeOptions.call(void 0, definition.options); flagImpl.decide = _chunkROBQSGBHcjs.trace.call(void 0, decide, { isVerboseTrace: false, name: "decide", attributes: { key: definition.key } }); flagImpl.identify = identify ? _chunkROBQSGBHcjs.trace.call(void 0, identify, { isVerboseTrace: false, name: "identify", attributes: { key: definition.key } }) : identify; flagImpl.run = _chunkROBQSGBHcjs.trace.call(void 0, 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) { await _asyncNullishCoalesce(secret, async () => ( (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(); } _context.context.call(void 0, ).state._flag = createContext(request, secret, params); const result = await next(); if (result && result instanceof Response && result.body) { const contentType = _optionalChain([result, 'access', _31 => _31.headers, 'access', _32 => _32.get, 'call', _33 => _33("content-type"), 'optionalAccess', _34 => _34.toLowerCase, 'call', _35 => _35()]); if (contentType && contentType.includes("text/html")) { if (isVercelEnvironment()) { const transformStream = createFlagScriptInjectionTransform( async () => { const store = _context.context.call(void 0, ).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 _chunkQLJ65HXQcjs.serialize.call(void 0, flagValues, flagsArray, secret // secret is guaranteed to be defined at this point ); return _chunkMEG7RDX7cjs.safeJsonStringify.call(void 0, 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 _chunkROBQSGBHcjs.verifyAccess.call(void 0, 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": _chunkROBQSGBHcjs.version } }); } function createFlagsDiscoveryEndpoint(getApiData, options) { return async (context3) => { const access = await _chunkROBQSGBHcjs.verifyAccess.call(void 0, context3.request.headers.get("authorization"), _optionalChain([options, 'optionalAccess', _36 => _36.secret]) ); if (!access) return Response.json(null, { status: 401 }); const apiData = await getApiData(context3); return Response.json(apiData, { headers: { "x-flags-sdk-version": _chunkROBQSGBHcjs.version } }); }; } exports.clearDedupeCacheForCurrentRequest = clearDedupeCacheForCurrentRequest; exports.combine = combine; exports.createFlagsDiscoveryEndpoint = createFlagsDiscoveryEndpoint; exports.createHandle = createHandle; exports.dedupe = dedupe; exports.deserialize = deserialize2; exports.evaluate = evaluate; exports.flag = flag; exports.generatePermutations = generatePermutations; exports.getPrecomputed = getPrecomputed; exports.getProviderData = getProviderData; exports.precompute = precompute; exports.serialize = serialize2; //# sourceMappingURL=web-router.cjs.map