@vercel/flags
Version:
Flags SDK by Vercel - The feature flags toolkit for Next.js and SvelteKit
1 lines • 11.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/sveltekit/index.ts"],"names":["flagImpl","valuePromise","value"],"mappings":";;;;;;;;;;;;;;;;AACA,SAAS,yBAAyB;AAqBlC,SAAS,sBAAsB;AAE/B,SAAS,eACP,KACA,MAC+B;AAC/B,SAAO,IAAI,eAAe,IAAI;AAChC;AAEA,IAAM,aAAa,oBAAI,QAAkC;AACzD,IAAM,aAAa,oBAAI,QAAyC;AAEhE,SAAS,YAAY,SAAmC;AACtD,QAAM,SAAS,WAAW,IAAI,OAAO;AACrC,MAAI,WAAW;AAAW,WAAO;AAEjC,QAAM,SAAS,eAAe,KAAK,OAAO;AAC1C,aAAW,IAAI,SAAS,MAAM;AAC9B,SAAO;AACT;AAEA,SAAS,YAAY,SAA0C;AAC7D,QAAM,SAAS,WAAW,IAAI,OAAO;AACrC,MAAI,WAAW;AAAW,WAAO;AAEjC,QAAM,SAAS,sBAAsB,KAAK,IAAI,eAAe,OAAO,CAAC;AACrE,aAAW,IAAI,SAAS,MAAM;AAC9B,SAAO;AACT;AAaA,eAAe,sBAAyB,KAAiC;AAEvE,QAAM,UAAU,OAAO,QAAQ,GAAG;AAGlC,QAAM,kBAAkB,MAAM,QAAQ;AAAA,IACpC,QAAQ,IAAI,OAAO,CAAC,KAAK,OAAO,MAAM;AACpC,YAAM,QAAQ,MAAM;AACpB,aAAO,CAAC,KAAK,KAAK;AAAA,IACpB,CAAC;AAAA,EACH;AAGA,SAAO,OAAO,YAAY,eAAe;AAC3C;AAEA,SAAS,UACP,YACiC;AACjC,SAAO,SAAS,OAAO,QAAQ;AAC7B,QAAI,OAAO,WAAW,WAAW,YAAY;AAC3C,aAAO,WAAW,OAAO,MAAM;AAAA,IACjC;AACA,QAAI,OAAO,WAAW,SAAS,WAAW,YAAY;AACpD,aAAO,WAAW,QAAQ,OAAO,EAAE,KAAK,WAAW,KAAK,GAAG,OAAO,CAAC;AAAA,IACrE;AACA,UAAM,IAAI;AAAA,MACR,kDAAkD,WAAW,GAAG;AAAA,IAClE;AAAA,EACF;AACF;AAKO,SAAS,KAAQ,YAAkD;AACxE,QAAM,SAAS,UAAsB,UAAU;AAE/C,QAAM,WAAW,eAAeA,YAAuB;AACrD,UAAM,QAAQ,YAAY,SAAS;AAEnC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAEA,QAAI,eAAe,MAAM,WAAW,WAAW,GAAG,GAAG;AACnD,YAAMC,gBAAe,MAAM,UAAU,WAAW,GAAG;AACnD,UAAI,OAAOA,kBAAiB,aAAa;AACvC,eAAOA;AAAA,MACT;AAAA,IACF;AAEA,UAAM,kBAAkB,MAAM,MAAM,QAAQ,IAAI,uBAAuB;AACvE,UAAM,YAAY,kBACd,MAAM,QAA2B,iBAAiB,MAAM,MAAM,IAC9D;AAEJ,QAAI,aAAa,eAAe,WAAW,WAAW,GAAG,GAAG;AAC1D,YAAMC,SAAQ,UAAU,WAAW,GAAG;AACtC,UAAI,OAAOA,WAAU,aAAa;AAChC,oBAAY,WAAW,KAAKA,MAAK;AACjC,cAAM,UAAU,WAAW,GAAG,IAAI,QAAQ,QAAQA,MAAkB;AACpE,eAAOA;AAAA,MACT;AAAA,IACF;AAEA,UAAM,eAAe;AAAA,MACnB;AAAA,QACE,SAAS,YAAY,MAAM,MAAM,QAAQ,OAAO;AAAA,QAChD,SAAS,YAAY,MAAM,MAAM,QAAQ,OAAO;AAAA,MAClD;AAAA;AAAA,MAEA,EAAE,OAAO,MAAM,MAAM;AAAA,IACvB;AACA,UAAM,UAAU,WAAW,GAAG,IAAI;AAElC,UAAM,QAAQ,MAAM;AACpB,gBAAY,WAAW,KAAK,KAAK;AACjC,WAAO;AAAA,EACT;AAEA,WAAS,MAAM,WAAW;AAC1B,WAAS,eAAe,WAAW;AACnC,WAAS,SAAS,WAAW;AAC7B,WAAS,cAAc,WAAW;AAClC,WAAS,UAAU,WAAW;AAC9B,WAAS,SAAS;AAGlB,SAAO;AACT;AAEO,SAAS,gBACd,OACS;AACT,QAAM,cAAc,OAAO,OAAO,KAAK,EAAE;AAAA,IACvC,CAAC,KAAK,MAAM;AACV,UAAI,EAAE,GAAG,IAAI;AAAA,QACX,SAAS,iBAAiB,EAAE,OAAO;AAAA,QACnC,QAAQ,EAAE;AAAA,QACV,aAAa,EAAE;AAAA,MACjB;AACA,aAAO;AAAA,IACT;AAAA,IACA,CAAC;AAAA,EACH;AAEA,SAAO,EAAE,aAAa,OAAO,CAAC,EAAE;AAClC;AAQA,SAAS,cACP,OACA,QACmB;AACnB,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW,CAAC;AAAA,EACd;AACF;AAEA,IAAM,cAAc,IAAI,kBAAqC;AAsBtD,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AACF,GAGW;AACT,SAAO,SAAS,OAAO,EAAE,OAAO,QAAQ,GAAG;AACzC,QACE;AAAA,IAEA,MAAM,QAAQ,IAAI,SAAS,eAAe,KAC1C,IAAI,IAAI,MAAM,QAAQ,GAAG,EAAE,aAAa,6BACxC;AACA,aAAO,0BAA0B,OAAO,QAAQ,KAAK;AAAA,IACvD;AAEA,UAAM,cAAc,cAAc,OAAO,MAAM;AAC/C,WAAO,YAAY;AAAA,MAAI;AAAA,MAAa,MAClC,QAAQ,OAAO;AAAA,QACb,oBAAoB,OAAO,EAAE,KAAK,MAAM;AACtC,gBAAM,QAAQ,YAAY,SAAS;AACnC,cAAI,CAAC,SAAS,OAAO,KAAK,MAAM,SAAS,EAAE,WAAW;AAAG,mBAAO;AAKhE,gBAAM,sBAAsB,MAAM;AAAA,YAChC,MAAM,sBAAsB,MAAM,SAAS;AAAA,YAC3C;AAAA,UACF;AAEA,iBAAO,KAAK;AAAA,YACV;AAAA,YACA,oDAAoD,kBAAkB,mBAAmB,CAAC;AAAA,UAC5F;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,eAAe,0BACb,OACA,QACA,OACA;AACA,QAAM,SAAS,MAAM;AAAA,IACnB,MAAM,QAAQ,QAAQ,IAAI,eAAe;AAAA,IACzC;AAAA,EACF;AACA,MAAI,CAAC;AAAQ,WAAO,IAAI,SAAS,MAAM,EAAE,QAAQ,IAAI,CAAC;AACtD,SAAO,SAAS,KAAK,gBAAgB,KAAK,CAAC;AAC7C","sourcesContent":["import type { Handle, RequestEvent } from '@sveltejs/kit';\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport {\n type ApiData,\n decrypt,\n encrypt,\n reportValue,\n safeJsonStringify,\n verifyAccess,\n type JsonValue,\n type FlagDefinitionsType,\n} from '..';\nimport { Decide, FlagDeclaration, GenerousOption } from '../types';\nimport {\n type ReadonlyHeaders,\n HeadersAdapter,\n} from '../spec-extension/adapters/headers';\nimport {\n type ReadonlyRequestCookies,\n RequestCookiesAdapter,\n} from '../spec-extension/adapters/request-cookies';\nimport { normalizeOptions } from '../lib/normalize-options';\nimport { RequestCookies } from '@edge-runtime/cookies';\n\nfunction hasOwnProperty<X extends {}, Y extends PropertyKey>(\n obj: X,\n prop: Y,\n): obj is X & Record<Y, unknown> {\n return obj.hasOwnProperty(prop);\n}\n\nconst headersMap = new WeakMap<Headers, ReadonlyHeaders>();\nconst cookiesMap = new WeakMap<Headers, ReadonlyRequestCookies>();\n\nfunction sealHeaders(headers: Headers): ReadonlyHeaders {\n const cached = headersMap.get(headers);\n if (cached !== undefined) return cached;\n\n const sealed = HeadersAdapter.seal(headers);\n headersMap.set(headers, sealed);\n return sealed;\n}\n\nfunction sealCookies(headers: Headers): ReadonlyRequestCookies {\n const cached = cookiesMap.get(headers);\n if (cached !== undefined) return cached;\n\n const sealed = RequestCookiesAdapter.seal(new RequestCookies(headers));\n cookiesMap.set(headers, sealed);\n return sealed;\n}\n\ntype Flag<ReturnValue> = (() => ReturnValue | Promise<ReturnValue>) & {\n key: string;\n description?: string;\n origin?: string | Record<string, unknown>;\n options?: GenerousOption<ReturnValue>[];\n};\n\ntype PromisesMap<T> = {\n [K in keyof T]: Promise<T[K]>;\n};\n\nasync function resolveObjectPromises<T>(obj: PromisesMap<T>): Promise<T> {\n // Convert the object into an array of [key, promise] pairs\n const entries = Object.entries(obj) as [keyof T, Promise<any>][];\n\n // Use Promise.all to wait for all the promises to resolve\n const resolvedEntries = await Promise.all(\n entries.map(async ([key, promise]) => {\n const value = await promise;\n return [key, value] as [keyof T, T[keyof T]];\n }),\n );\n\n // Convert the array of resolved [key, value] pairs back into an object\n return Object.fromEntries(resolvedEntries) as T;\n}\n\nfunction getDecide<ValueType, EntitiesType>(\n definition: FlagDeclaration<ValueType, EntitiesType>,\n): Decide<ValueType, EntitiesType> {\n return function decide(params) {\n if (typeof definition.decide === 'function') {\n return definition.decide(params);\n }\n if (typeof definition.adapter?.decide === 'function') {\n return definition.adapter.decide({ key: definition.key, ...params });\n }\n throw new Error(\n `@vercel/flags: No decide function provided for ${definition.key}`,\n );\n };\n}\n\n/**\n * Declares a feature flag\n */\nexport function flag<T>(definition: FlagDeclaration<T, unknown>): Flag<T> {\n const decide = getDecide<T, unknown>(definition);\n\n const flagImpl = async function flagImpl(): Promise<T> {\n const store = flagStorage.getStore();\n\n if (!store) {\n throw new Error('@vercel/flags: context not found');\n }\n\n if (hasOwnProperty(store.usedFlags, definition.key)) {\n const valuePromise = store.usedFlags[definition.key];\n if (typeof valuePromise !== 'undefined') {\n return valuePromise as Promise<T>;\n }\n }\n\n const overridesCookie = store.event.cookies.get('vercel-flag-overrides');\n const overrides = overridesCookie\n ? await decrypt<Record<string, T>>(overridesCookie, store.secret)\n : undefined;\n\n if (overrides && hasOwnProperty(overrides, definition.key)) {\n const value = overrides[definition.key];\n if (typeof value !== 'undefined') {\n reportValue(definition.key, value);\n store.usedFlags[definition.key] = Promise.resolve(value as JsonValue);\n return value;\n }\n }\n\n const valuePromise = decide(\n {\n headers: sealHeaders(store.event.request.headers),\n cookies: sealCookies(store.event.request.headers),\n },\n // @ts-expect-error not part of the type, but we supply it for convenience\n { event: store.event },\n );\n store.usedFlags[definition.key] = valuePromise as Promise<JsonValue>;\n\n const value = await valuePromise;\n reportValue(definition.key, value);\n return value;\n };\n\n flagImpl.key = definition.key;\n flagImpl.defaultValue = definition.defaultValue;\n flagImpl.origin = definition.origin;\n flagImpl.description = definition.description;\n flagImpl.options = definition.options;\n flagImpl.decide = decide;\n // flagImpl.identify = definition.identify;\n\n return flagImpl;\n}\n\nexport function getProviderData(\n flags: Record<string, Flag<JsonValue>>,\n): ApiData {\n const definitions = Object.values(flags).reduce<FlagDefinitionsType>(\n (acc, d) => {\n acc[d.key] = {\n options: normalizeOptions(d.options),\n origin: d.origin,\n description: d.description,\n };\n return acc;\n },\n {},\n );\n\n return { definitions, hints: [] };\n}\n\ninterface AsyncLocalContext {\n event: RequestEvent<Partial<Record<string, string>>, string | null>;\n secret: string;\n usedFlags: Record<string, Promise<JsonValue>>;\n}\n\nfunction createContext(\n event: RequestEvent<Partial<Record<string, string>>, string | null>,\n secret: string,\n): AsyncLocalContext {\n return {\n event,\n secret,\n usedFlags: {},\n };\n}\n\nconst flagStorage = new AsyncLocalStorage<AsyncLocalContext>();\n\n/**\n * Establishes context for flags, so they have access to the\n * request and cookie.\n *\n * Also registers evaluated flags, except for flags used only after `resolve` calls in other handlers.\n *\n * @example Usage example in src/hooks.server.ts\n *\n * ```ts\n * import { createHandle } from '@vercel/flags/sveltekit';\n * import { FLAGS_SECRET } from '$env/static/private';\n * import * as flags from '$lib/flags';\n *\n * export const handle = createHandle({ secret: FLAGS_SECRET, flags });\n * ```\n *\n * @example Usage example in src/hooks.server.ts with other handlers\n *\n * Note that when composing `createHandle` with `sequence` then `createHandle` should come first. Only handlers after it will be able to access feature flags.\n */\nexport function createHandle({\n secret,\n flags,\n}: {\n secret: string;\n flags?: Record<string, Flag<JsonValue>>;\n}): Handle {\n return function handle({ event, resolve }) {\n if (\n flags &&\n // avoid creating the URL object for every request by checking with includes() first\n event.request.url.includes('/.well-known/') &&\n new URL(event.request.url).pathname === '/.well-known/vercel/flags'\n ) {\n return handleWellKnownFlagsRoute(event, secret, flags);\n }\n\n const flagContext = createContext(event, secret);\n return flagStorage.run(flagContext, () =>\n resolve(event, {\n transformPageChunk: async ({ html }) => {\n const store = flagStorage.getStore();\n if (!store || Object.keys(store.usedFlags).length === 0) return html;\n\n // This is for reporting which flags were used when this page was generated,\n // so the value shows up in Vercel Toolbar, without the client ever being\n // aware of this feature flag.\n const encryptedFlagValues = await encrypt(\n await resolveObjectPromises(store.usedFlags),\n secret,\n );\n\n return html.replace(\n '</body>',\n `<script type=\"application/json\" data-flag-values>${safeJsonStringify(encryptedFlagValues)}</script></body>`,\n );\n },\n }),\n );\n };\n}\n\nasync function handleWellKnownFlagsRoute(\n event: RequestEvent<Partial<Record<string, string>>, string | null>,\n secret: string,\n flags: Record<string, Flag<JsonValue>>,\n) {\n const access = await verifyAccess(\n event.request.headers.get('Authorization'),\n secret,\n );\n if (!access) return new Response(null, { status: 401 });\n return Response.json(getProviderData(flags));\n}\n"]}