1- import type { TokenSearchRules , TokenOptions } from "./types" ;
1+ import type { TenantTokenGeneratorOptions } from "./types" ;
22
33const UUID_V4_REGEXP = / ^ [ 0 - 9 a - f ] { 8 } \b (?: - [ 0 - 9 a - f ] { 4 } \b ) { 3 } - [ 0 - 9 a - f ] { 12 } $ / i;
44function isValidUUIDv4 ( uuid : string ) : boolean {
@@ -12,32 +12,25 @@ function encodeToBase64(data: unknown): string {
1212}
1313
1414// missing crypto global for Node.js 18 https://nodejs.org/api/globals.html#crypto_1
15- // TODO: Improve error handling?
16- const compatCrypto =
15+ const cryptoPonyfill =
1716 typeof crypto === "undefined"
1817 ? import ( "node:crypto" ) . then ( ( v ) => v . webcrypto )
1918 : Promise . resolve ( crypto ) ;
2019
2120const textEncoder = new TextEncoder ( ) ;
2221
23- /**
24- * Create the header of the token.
25- *
26- * @param apiKey - API key used to sign the token.
27- * @param encodedHeader - Header of the token in base64.
28- * @param encodedPayload - Payload of the token in base64.
29- * @returns The signature of the token in base64.
30- */
22+ /** Create the signature of the token. */
3123async function sign (
3224 apiKey : string ,
33- encodedHeader : string ,
3425 encodedPayload : string ,
26+ encodedHeader : string ,
3527) : Promise < string > {
36- const crypto = await compatCrypto ;
28+ const crypto = await cryptoPonyfill ;
3729
3830 const cryptoKey = await crypto . subtle . importKey (
3931 "raw" ,
4032 textEncoder . encode ( apiKey ) ,
33+ // TODO: Does alg depend on this too?
4134 { name : "HMAC" , hash : "SHA-256" } ,
4235 false ,
4336 [ "sign" ] ,
@@ -58,76 +51,100 @@ async function sign(
5851 return digest ;
5952}
6053
61- /**
62- * Create the header of the token.
63- *
64- * @returns The header encoded in base64.
65- */
66- function createHeader ( ) : string {
67- const header = {
68- alg : "HS256" ,
69- typ : "JWT" ,
70- } ;
71-
54+ /** Create the header of the token. */
55+ function getHeader ( {
56+ algorithm : alg = "HS256" ,
57+ } : TenantTokenGeneratorOptions ) : string {
58+ const header = { alg, typ : "JWT" } ;
7259 return encodeToBase64 ( header ) . replace ( / = / g, "" ) ;
7360}
7461
75- /**
76- * Create the payload of the token.
77- *
78- * @param searchRules - Search rules that are applied to every search.
79- * @param uid - The uid of the api key used as issuer of the token.
80- * @param expiresAt - Date at which the token expires.
81- * @returns The payload encoded in base64.
82- */
83- function createPayload ( {
84- searchRules,
62+ /** Create the payload of the token. */
63+ function getPayload ( {
64+ searchRules = [ ] ,
8565 apiKeyUid,
8666 expiresAt,
87- } : {
88- searchRules : TokenSearchRules ;
89- apiKeyUid : string ;
90- expiresAt ?: Date ;
91- } ) : string {
67+ } : TenantTokenGeneratorOptions ) : string {
68+ if ( ! isValidUUIDv4 ( apiKeyUid ) ) {
69+ throw new Error ( "the uid of your key is not a valid UUIDv4" ) ;
70+ }
71+
9272 const payload = {
9373 searchRules,
9474 apiKeyUid,
95- exp : expiresAt ? Math . floor ( expiresAt . getTime ( ) / 1000 ) : undefined ,
75+ exp : expiresAt
76+ ? Math . floor (
77+ ( typeof expiresAt === "number" ? expiresAt : expiresAt . getTime ( ) ) /
78+ 1000 ,
79+ )
80+ : undefined ,
9681 } ;
9782
9883 return encodeToBase64 ( payload ) . replace ( / = / g, "" ) ;
9984}
10085
10186/**
102- * Generate a tenant token
87+ * Try to detect if the script is running in a server-side runtime.
88+ *
89+ * @remarks
90+ * This is not a silver bullet method for determining the environment.
91+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgent | User agent }
92+ * can be spoofed, `process` can be patched. Never the less theoretically it
93+ * should prevent misuse for the overwhelming majority of cases.
94+ */
95+ function tryDetectEnvironment ( ) : void {
96+ // https://min-common-api.proposal.wintercg.org/#navigator-useragent-requirements
97+ if (
98+ typeof navigator !== "undefined" &&
99+ Object . hasOwn ( navigator , "userAgent" )
100+ ) {
101+ const { userAgent } = navigator ;
102+
103+ if (
104+ userAgent . startsWith ( "Node" ) ||
105+ userAgent . startsWith ( "Deno" ) ||
106+ userAgent . startsWith ( "Bun" ) ||
107+ userAgent . startsWith ( "Cloudflare-Workers" )
108+ ) {
109+ return ;
110+ }
111+ }
112+
113+ // Node.js prior to v21.1.0 doesn't have the above global
114+ // https://nodejs.org/api/globals.html#navigatoruseragent
115+ if (
116+ Object . hasOwn ( globalThis , "process" ) &&
117+ Object . hasOwn ( globalThis . process , "versions" ) &&
118+ Object . hasOwn ( globalThis . process . versions , "node" )
119+ ) {
120+ return ;
121+ }
122+
123+ throw new Error ( "TODO" ) ;
124+ }
125+
126+ /**
127+ * Generate a tenant token.
103128 *
104- * @param apiKeyUid - The uid of the api key used as issuer of the token.
105- * @param searchRules - Search rules that are applied to every search.
106- * @param options - Token options to customize some aspect of the token.
107- * @returns The token in JWT format.
129+ * @remarks
130+ * TODO: Describe how this should be used safely.
131+ * @param options - Options object for tenant token generation
132+ * @returns The token in JWT (JSON Web Token) format
133+ * @see {@link https://www.meilisearch.com/docs/learn/security/generate_tenant_token_sdk | Using tenant tokens with an official SDK }
134+ * @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference | Tenant token payload reference }
108135 */
109136export async function generateTenantToken (
110- apiKeyUid : string ,
111- searchRules : TokenSearchRules ,
112- { apiKey, expiresAt } : TokenOptions ,
137+ options : TenantTokenGeneratorOptions ,
113138) : Promise < string > {
114- if ( expiresAt !== undefined && expiresAt . getTime ( ) < Date . now ( ) ) {
115- throw new Error ( "the `expiresAt` field must be a date in the future" ) ;
116- }
139+ const { apiKey, force = false } = options ;
117140
118- if ( ! isValidUUIDv4 ( apiKeyUid ) ) {
119- throw new Error (
120- "the uid of your key is not a valid UUIDv4; to find out the uid of your key use `getKey()`" ,
121- ) ;
141+ if ( ! force ) {
142+ tryDetectEnvironment ( ) ;
122143 }
123144
124- const encodedHeader = createHeader ( ) ;
125- const encodedPayload = createPayload ( {
126- searchRules,
127- apiKeyUid,
128- expiresAt,
129- } ) ;
130- const signature = await sign ( apiKey , encodedHeader , encodedPayload ) ;
145+ const encodedPayload = getPayload ( options ) ;
146+ const encodedHeader = getHeader ( options ) ;
147+ const signature = await sign ( apiKey , encodedPayload , encodedHeader ) ;
131148
132149 return `${ encodedHeader } .${ encodedPayload } .${ signature } ` ;
133150}
0 commit comments