Skip to content

Commit cebab9a

Browse files
committed
Add environment detection, add/improve docs and types, simplify
1 parent 7b8c769 commit cebab9a

File tree

3 files changed

+196
-86
lines changed

3 files changed

+196
-86
lines changed

src/token.ts

Lines changed: 79 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TokenSearchRules, TokenOptions } from "./types";
1+
import type { TenantTokenGeneratorOptions } from "./types";
22

33
const UUID_V4_REGEXP = /^[0-9a-f]{8}\b(?:-[0-9a-f]{4}\b){3}-[0-9a-f]{12}$/i;
44
function 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

2120
const 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. */
3123
async 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
*/
109136
export 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
}

src/types/types.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1108,15 +1108,47 @@ export const ErrorStatusCode = {
11081108
export type ErrorStatusCode =
11091109
(typeof ErrorStatusCode)[keyof typeof ErrorStatusCode];
11101110

1111-
export type TokenIndexRules = {
1112-
[field: string]: any;
1113-
filter?: Filter;
1114-
};
1111+
/** @see {@link TokenSearchRules} */
1112+
export type TokenIndexRules = { filter?: Filter };
1113+
1114+
/**
1115+
* {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference#search-rules}
1116+
*
1117+
* @remarks
1118+
* Not well documented.
1119+
* @see {@link https://github.com/meilisearch/meilisearch/blob/b21d7aedf9096539041362d438e973a18170f3fc/crates/meilisearch-auth/src/lib.rs#L271-L277 | GitHub source code}
1120+
*/
11151121
export type TokenSearchRules =
11161122
| Record<string, TokenIndexRules | null>
11171123
| string[];
11181124

1119-
export type TokenOptions = {
1125+
/** Options object for tenant token generation. */
1126+
export type TenantTokenGeneratorOptions = {
1127+
/**
1128+
* The uid of the api key used as issuer of the token.
1129+
*
1130+
* @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference#api-key-uid}
1131+
*/
1132+
apiKeyUid: string;
1133+
/** Search rules that are applied to every search. */
1134+
searchRules?: TokenSearchRules;
1135+
/** API key used to sign the token. */
11201136
apiKey: string;
1121-
expiresAt?: Date;
1137+
/**
1138+
* {@link https://en.wikipedia.org/wiki/Unix_time | UNIX timestamp} or
1139+
* {@link Date} object at which the token expires.
1140+
*
1141+
* @see {@link https://www.meilisearch.com/docs/learn/security/tenant_token_reference#expiry-date}
1142+
*/
1143+
expiresAt?: number | Date;
1144+
/**
1145+
* Encryption algorithm used by TODO what. Supported values are HS256, HS384,
1146+
* HS512.
1147+
*/
1148+
algorithm?: `HS${256 | 384 | 512}`;
1149+
/**
1150+
* By default if a non-safe environment is detected, an error is thrown.
1151+
* Setting this to `true` skips environment detection.
1152+
*/
1153+
force?: boolean;
11221154
};

0 commit comments

Comments
 (0)