1- import type { TenantTokenGeneratorOptions } from "./types" ;
1+ import type { TenantTokenGeneratorOptions , TokenSearchRules } from "./types" ;
2+
3+ function getOptionsWithDefaults ( options : TenantTokenGeneratorOptions ) {
4+ const {
5+ searchRules = [ ] ,
6+ algorithm = "HS256" ,
7+ force = false ,
8+ ...restOfOptions
9+ } = options ;
10+ return { searchRules, algorithm, force, ...restOfOptions } ;
11+ }
12+
13+ type TenantTokenGeneratorOptionsWithDefaults = ReturnType <
14+ typeof getOptionsWithDefaults
15+ > ;
216
317const UUID_V4_REGEXP = / ^ [ 0 - 9 a - f ] { 8 } \b (?: - [ 0 - 9 a - f ] { 4 } \b ) { 3 } - [ 0 - 9 a - f ] { 12 } $ / i;
418function isValidUUIDv4 ( uuid : string ) : boolean {
@@ -21,18 +35,21 @@ const textEncoder = new TextEncoder();
2135
2236/** Create the signature of the token. */
2337async function sign (
24- apiKey : string ,
38+ { apiKey, algorithm } : TenantTokenGeneratorOptionsWithDefaults ,
2539 encodedPayload : string ,
2640 encodedHeader : string ,
2741) : Promise < string > {
2842 const crypto = await cryptoPonyfill ;
2943
3044 const cryptoKey = await crypto . subtle . importKey (
45+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#raw
3146 "raw" ,
3247 textEncoder . encode ( apiKey ) ,
33- // TODO: Does alg depend on this too?
34- { name : "HMAC" , hash : "SHA-256" } ,
48+ // https://developer.mozilla.org/en-US/docs/Web/API/HmacImportParams#instance_properties
49+ { name : "HMAC" , hash : `SHA-${ algorithm . slice ( 2 ) } ` } ,
50+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#extractable
3551 false ,
52+ // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey#keyusages
3653 [ "sign" ] ,
3754 ) ;
3855
@@ -53,32 +70,39 @@ async function sign(
5370
5471/** Create the header of the token. */
5572function getHeader ( {
56- algorithm : alg = "HS256" ,
57- } : TenantTokenGeneratorOptions ) : string {
73+ algorithm : alg ,
74+ } : TenantTokenGeneratorOptionsWithDefaults ) : string {
5875 const header = { alg, typ : "JWT" } ;
5976 return encodeToBase64 ( header ) . replace ( / = / g, "" ) ;
6077}
6178
79+ /**
80+ * @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference | Tenant token payload reference }
81+ * @see {@link https://github.com/meilisearch/meilisearch/blob/b21d7aedf9096539041362d438e973a18170f3fc/crates/meilisearch/src/extractors/authentication/mod.rs#L334-L340 | GitHub source code }
82+ */
83+ type TokenClaims = {
84+ searchRules : TokenSearchRules ;
85+ exp ?: number ;
86+ apiKeyUid : string ;
87+ } ;
88+
6289/** Create the payload of the token. */
6390function getPayload ( {
64- searchRules = [ ] ,
91+ searchRules,
6592 apiKeyUid,
6693 expiresAt,
67- } : TenantTokenGeneratorOptions ) : string {
94+ } : TenantTokenGeneratorOptionsWithDefaults ) : string {
6895 if ( ! isValidUUIDv4 ( apiKeyUid ) ) {
6996 throw new Error ( "the uid of your key is not a valid UUIDv4" ) ;
7097 }
7198
72- const payload = {
73- searchRules,
74- apiKeyUid,
75- exp : expiresAt
76- ? Math . floor (
77- ( typeof expiresAt === "number" ? expiresAt : expiresAt . getTime ( ) ) /
78- 1000 ,
79- )
80- : undefined ,
81- } ;
99+ const payload : TokenClaims = { searchRules, apiKeyUid } ;
100+ if ( expiresAt !== undefined ) {
101+ payload . exp =
102+ typeof expiresAt === "number"
103+ ? expiresAt
104+ : Math . floor ( expiresAt . getTime ( ) / 1000 ) ;
105+ }
82106
83107 return encodeToBase64 ( payload ) . replace ( / = / g, "" ) ;
84108}
@@ -87,13 +111,14 @@ function getPayload({
87111 * Try to detect if the script is running in a server-side runtime.
88112 *
89113 * @remarks
90- * This is not a silver bullet method for determining the environment.
114+ * There is no silver bullet way for determining the environment. Even so, this
115+ * is the recommended way according to
116+ * {@link https://min-common-api.proposal.wintercg.org/#navigator-useragent-requirements | WinterCG specs}.
91117 * {@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.
118+ * can be spoofed, `process` can be patched. It should prevent misuse for the
119+ * overwhelming majority of cases.
94120 */
95121function tryDetectEnvironment ( ) : void {
96- // https://min-common-api.proposal.wintercg.org/#navigator-useragent-requirements
97122 if (
98123 typeof navigator !== "undefined" &&
99124 Object . hasOwn ( navigator , "userAgent" )
@@ -120,31 +145,40 @@ function tryDetectEnvironment(): void {
120145 return ;
121146 }
122147
123- throw new Error ( "TODO" ) ;
148+ throw new Error (
149+ "failed to detect a server-side environment; do not generate tokens on the frontend in production!\n" +
150+ "use the `force` option to disable environment detection, consult the documentation (Use at your own risk!)" ,
151+ ) ;
124152}
125153
126154/**
127155 * Generate a tenant token.
128156 *
129157 * @remarks
130- * TODO: Describe how this should be used safely.
158+ * Warning: while this can be used in browsers with
159+ * {@link TenantTokenGeneratorOptions.force}, it is only intended for server
160+ * side. Don't use this in production on the frontend, unless you really know
161+ * what you're doing!
131162 * @param options - Options object for tenant token generation
132163 * @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 }
164+ * @see {@link https://www.meilisearch.com/docs/learn/security/basic_security | Securing your project }
135165 */
136166export async function generateTenantToken (
137167 options : TenantTokenGeneratorOptions ,
138168) : Promise < string > {
139- const { apiKey , force = false } = options ;
169+ const optionsWithDefaults = getOptionsWithDefaults ( options ) ;
140170
141- if ( ! force ) {
171+ if ( ! optionsWithDefaults . force ) {
142172 tryDetectEnvironment ( ) ;
143173 }
144174
145- const encodedPayload = getPayload ( options ) ;
146- const encodedHeader = getHeader ( options ) ;
147- const signature = await sign ( apiKey , encodedPayload , encodedHeader ) ;
175+ const encodedPayload = getPayload ( optionsWithDefaults ) ;
176+ const encodedHeader = getHeader ( optionsWithDefaults ) ;
177+ const signature = await sign (
178+ optionsWithDefaults ,
179+ encodedPayload ,
180+ encodedHeader ,
181+ ) ;
148182
149183 return `${ encodedHeader } .${ encodedPayload } .${ signature } ` ;
150184}
0 commit comments