UNPKG

meilisearch

Version:

The Meilisearch JS client for Node.js and the browser.

167 lines (145 loc) 5.46 kB
import type { TenantTokenGeneratorOptions, TenantTokenHeader, TokenClaims, } from "./types/index.js"; function getOptionsWithDefaults(options: TenantTokenGeneratorOptions) { const { searchRules = ["*"], algorithm = "HS256", force = false, ...restOfOptions } = options; return { searchRules, algorithm, force, ...restOfOptions }; } // TODO: There's no point in this, or maybe even the above fn type TenantTokenGeneratorOptionsWithDefaults = ReturnType< typeof getOptionsWithDefaults >; const UUID_V4_REGEXP = /^[0-9a-f]{8}\b(?:-[0-9a-f]{4}\b){3}-[0-9a-f]{12}$/i; function isValidUUIDv4(uuid: string): boolean { return UUID_V4_REGEXP.test(uuid); } function encodeToBase64(data: unknown): string { // TODO: instead of btoa use Uint8Array.prototype.toBase64() when it becomes available in supported runtime versions // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Uint8Array/toBase64 return btoa(typeof data === "string" ? data : JSON.stringify(data)); } const textEncoder = new TextEncoder(); /** Create the signature of the token. */ async function sign( { apiKey, algorithm }: TenantTokenGeneratorOptionsWithDefaults, encodedPayload: string, encodedHeader: string, ): Promise<string> { const cryptoKey = await crypto.subtle.importKey( // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#raw "raw", textEncoder.encode(apiKey), // https://developer.mozilla.org/en-US/docs/Web/API/HmacImportParams#instance_properties { name: "HMAC", hash: `SHA-${algorithm.slice(2)}` }, // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#extractable false, // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#keyusages ["sign"], ); const signature = await crypto.subtle.sign( "HMAC", cryptoKey, textEncoder.encode(`${encodedHeader}.${encodedPayload}`), ); // TODO: Same problem as in `encodeToBase64` above const digest = btoa(String.fromCharCode(...new Uint8Array(signature))) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=/g, ""); return digest; } /** Create the header of the token. */ function getHeader({ algorithm: alg, }: TenantTokenGeneratorOptionsWithDefaults): string { const header: TenantTokenHeader = { alg, typ: "JWT" }; return encodeToBase64(header).replace(/=/g, ""); } /** Create the payload of the token. */ function getPayload({ searchRules, apiKeyUid, expiresAt, }: TenantTokenGeneratorOptionsWithDefaults): string { if (!isValidUUIDv4(apiKeyUid)) { throw new Error("the uid of your key is not a valid UUIDv4"); } const payload: TokenClaims = { searchRules, apiKeyUid }; if (expiresAt !== undefined) { payload.exp = typeof expiresAt === "number" ? expiresAt : // To get from a Date object the number of seconds since Unix epoch, i.e. Unix timestamp: Math.floor(expiresAt.getTime() / 1000); } return encodeToBase64(payload).replace(/=/g, ""); } /** * Try to detect if the script is running in a server-side runtime. * * @remarks * There is no silver bullet way for determining the environment. Even so, this * is the recommended way according to * {@link https://min-common-api.proposal.wintercg.org/#navigator-useragent-requirements | WinterCG specs}. * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent | User agent } * can be spoofed, `process` can be patched. Even so it should prevent misuse * for the overwhelming majority of cases. */ function tryDetectEnvironment(): void { if (typeof navigator !== "undefined" && "userAgent" in navigator) { const { userAgent } = navigator; if ( userAgent.startsWith("Node") || userAgent.startsWith("Deno") || userAgent.startsWith("Bun") || userAgent.startsWith("Cloudflare-Workers") ) { return; } } // Node.js prior to v21.1.0 doesn't have the above global // https://nodejs.org/api/globals.html#navigatoruseragent const versions = globalThis.process?.versions; if (versions !== undefined && Object.hasOwn(versions, "node")) { return; } throw new Error( "failed to detect a server-side environment; do not generate tokens on the frontend in production!\n" + "use the `force` option to disable environment detection, consult the documentation (Use at your own risk!)", ); } /** * Generate a tenant token. * * @remarks * Warning: while this can be used in browsers with * {@link TenantTokenGeneratorOptions.force}, it is only intended for server * side. Don't use this in production on the frontend, unless you really know * what you're doing! * @param options - Options object for tenant token generation * @returns The token in JWT (JSON Web Token) format * @see {@link https://www.meilisearch.com/docs/learn/security/basic_security | Securing your project} */ export async function generateTenantToken( options: TenantTokenGeneratorOptions, ): Promise<string> { const optionsWithDefaults = getOptionsWithDefaults(options); if (!optionsWithDefaults.force) { tryDetectEnvironment(); } const encodedPayload = getPayload(optionsWithDefaults); const encodedHeader = getHeader(optionsWithDefaults); const signature = await sign( optionsWithDefaults, encodedPayload, encodedHeader, ); return `${encodedHeader}.${encodedPayload}.${signature}`; }