Skip to content

Commit

Permalink
Merge #1739
Browse files Browse the repository at this point in the history
1739: Separate token specific code into its own export r=flevi29 a=flevi29

# Pull Request

## Related issue
Fixes #1690

## What does this PR do?
- generalizes the `MeiliSearch` client into a single universal export by stripping Node.js crypto specific token code into its own separate exported module
- `apiKey` option is now mandatory in `generateTenantToken` function

### Migration

Old:
```typescript
import { MeiliSearch } from "meilisearch";

const client = new MeiliSearch({ host: "http://127.0.0.1:7700", apiKey: "masterKey" });
const token = await client.generateTenantToken("e489fe16-3381-431b-bee3-00430192915d");

// ...
```

New:
```typescript
import { generateTenantToken } from "meilisearch/token";

const token = await generateTenantToken("e489fe16-3381-431b-bee3-00430192915d", [], { apiKey: "masterKey" });

// ...
```

## PR checklist
Please check if your PR fulfills the following requirements:
- [x] Does this PR fix an existing issue, or have you listed the changes applied in the PR description (and why they are needed)?
- [x] Have you read the contributing guidelines?
- [x] Have you made sure that the title is accurate and descriptive of the changes?

Thank you so much for contributing to Meilisearch!


Co-authored-by: F. Levi <[email protected]>
  • Loading branch information
meili-bors[bot] and flevi29 authored Oct 28, 2024
2 parents e224f5c + 834fd67 commit 4fe62f3
Show file tree
Hide file tree
Showing 19 changed files with 152 additions and 231 deletions.
4 changes: 3 additions & 1 deletion .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,8 @@ authorization_header_1: |-
const client = new MeiliSearch({ host: 'http://localhost:7700', apiKey: 'masterKey' })
client.getKeys()
tenant_token_guide_generate_sdk_1: |-
import { generateTenantToken } from 'meilisearch/token'
const searchRules = {
patient_medical_records: {
filter: 'user_id = 1'
Expand All @@ -709,7 +711,7 @@ tenant_token_guide_generate_sdk_1: |-
const apiKeyUid = '85c3c2f9-bdd6-41f1-abd8-11fcf80e0f76'
const expiresAt = new Date('2025-12-20') // optional
const token = await client.generateTenantToken(apiKeyUid, searchRules, {
const token = await generateTenantToken(apiKeyUid, searchRules, {
apiKey: apiKey,
expiresAt: expiresAt,
})
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
"import": "./dist/bundles/meilisearch.mjs",
"require": "./dist/bundles/meilisearch.cjs",
"default": "./dist/bundles/meilisearch.umd.js"
},
"./token": {
"types": "./dist/types/token.d.ts",
"import": "./dist/bundles/token.mjs",
"require": "./dist/bundles/token.cjs",
"default": "./dist/bundles/token.cjs"
}
},
"sideEffects": false,
Expand Down
57 changes: 38 additions & 19 deletions rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,22 @@ const PLUGINS = [
}),
];

const INDEX_INPUT = "src/index.ts";
const TOKEN_INPUT = "src/token.ts";

const INDEX_EXPORTS = pkg.exports["."];
const TOKEN_EXPORTS = pkg.exports["./token"];

module.exports = [
// browser-friendly UMD build
// Index
{
input: "src/browser.ts", // directory to transpilation of typescript
input: INDEX_INPUT,
output: {
name: "window",
extend: true,
file: getOutputFileName(
// will add .min. in filename if in production env
resolve(ROOT, pkg.jsdelivr),
resolve(ROOT, INDEX_EXPORTS.browser),
env === "production",
),
format: "umd",
Expand Down Expand Up @@ -67,36 +73,49 @@ module.exports = [
}),
// nodePolyfills
json(),
env === "production" ? terser() : {}, // will minify the file in production mode
env === "production" ? terser() : {},
],
},
{
input: INDEX_INPUT,
output: [
{
file: INDEX_EXPORTS.import,
exports: "named",
format: "es",
sourcemap: env === "production",
},
],
plugins: PLUGINS,
},
{
input: INDEX_INPUT,
output: {
file: INDEX_EXPORTS.require,
exports: "named",
format: "cjs",
sourcemap: env === "production",
},
plugins: PLUGINS,
},

// ES module (for bundlers) build.
// Token
{
input: "src/index.ts",
input: TOKEN_INPUT,
output: [
{
file: getOutputFileName(
resolve(ROOT, pkg.module),
env === "production",
),
file: TOKEN_EXPORTS.import,
exports: "named",
format: "es",
sourcemap: env === "production", // create sourcemap for error reporting in production mode
sourcemap: env === "production",
},
],
plugins: PLUGINS,
},
// Common JS build (Node).
// Compatible only in a nodeJS environment.
{
input: "src/index.ts",
input: TOKEN_INPUT,
output: {
file: getOutputFileName(
// will add .min. in filename if in production env
resolve(ROOT, pkg.main),
env === "production",
),
file: TOKEN_EXPORTS.require,
exports: "named",
format: "cjs",
sourcemap: env === "production", // create sourcemap for error reporting in production mode
Expand Down
7 changes: 0 additions & 7 deletions src/browser.ts

This file was deleted.

10 changes: 0 additions & 10 deletions src/clients/browser-client.ts

This file was deleted.

36 changes: 0 additions & 36 deletions src/clients/node-client.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export * from "./errors";
export * from "./indexes";
export * from "./enqueued-task";
export * from "./task";
import { MeiliSearch } from "./clients/node-client";
import { MeiliSearch } from "./meilisearch";

export { MeiliSearch, MeiliSearch as Meilisearch };
export default MeiliSearch;
39 changes: 6 additions & 33 deletions src/clients/client.ts → src/meilisearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Copyright: 2019, MeiliSearch
*/

import { Index } from "../indexes";
import { Index } from "./indexes";
import {
KeyCreation,
Config,
Expand All @@ -16,8 +16,6 @@ import {
Stats,
Version,
ErrorStatusCode,
TokenSearchRules,
TokenOptions,
TasksQuery,
WaitOptions,
KeyUpdate,
Expand All @@ -34,12 +32,12 @@ import {
MultiSearchResponse,
SearchResponse,
FederatedMultiSearchParams,
} from "../types";
import { HttpRequests } from "../http-requests";
import { TaskClient, Task } from "../task";
import { EnqueuedTask } from "../enqueued-task";
} from "./types";
import { HttpRequests } from "./http-requests";
import { TaskClient, Task } from "./task";
import { EnqueuedTask } from "./enqueued-task";

class Client {
export class MeiliSearch {
config: Config;
httpRequest: HttpRequests;
tasks: TaskClient;
Expand Down Expand Up @@ -470,29 +468,4 @@ class Client {

return new EnqueuedTask(task);
}

///
/// TOKENS
///

/**
* Generate a tenant token
*
* @param apiKeyUid - The uid of the api key used as issuer of the token.
* @param searchRules - Search rules that are applied to every search.
* @param options - Token options to customize some aspect of the token.
* @returns The token in JWT format.
*/
generateTenantToken(
_apiKeyUid: string,
_searchRules: TokenSearchRules,
_options?: TokenOptions,
): Promise<string> {
const error = new Error();
error.message = `Meilisearch: failed to generate a tenant token. Generation of a token only works in a node environment \n ${error.stack}.`;

return Promise.reject(error);
}
}

export { Client };
90 changes: 39 additions & 51 deletions src/token.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Config, TokenSearchRules, TokenOptions } from "./types";
import { TokenSearchRules, TokenOptions } from "./types";
import { MeiliSearchError } from "./errors";
import { validateUuid4 } from "./utils";

Expand Down Expand Up @@ -51,14 +51,15 @@ function createHeader() {
* @param uid - The uid of the api key used as issuer of the token.
* @param expiresAt - Date at which the token expires.
*/
function validateTokenParameters(tokenParams: {
function validateTokenParameters({
searchRules,
apiKeyUid,
expiresAt,
}: {
searchRules: TokenSearchRules;
uid: string;
apiKey: string;
apiKeyUid: string;
expiresAt?: Date;
}) {
const { searchRules, uid, apiKey, expiresAt } = tokenParams;

if (expiresAt) {
if (!(expiresAt instanceof Date)) {
throw new MeiliSearchError(
Expand All @@ -79,19 +80,13 @@ function validateTokenParameters(tokenParams: {
}
}

if (!apiKey || typeof apiKey !== "string") {
throw new MeiliSearchError(
`Meilisearch: The API key used for the token generation must exist and be of type string.`,
);
}

if (!uid || typeof uid !== "string") {
if (!apiKeyUid || typeof apiKeyUid !== "string") {
throw new MeiliSearchError(
`Meilisearch: The uid of the api key used for the token generation must exist, be of type string and comply to the uuid4 format.`,
);
}

if (!validateUuid4(uid)) {
if (!validateUuid4(apiKeyUid)) {
throw new MeiliSearchError(
`Meilisearch: The uid of your key is not a valid uuid4. To find out the uid of your key use getKey().`,
);
Expand All @@ -106,53 +101,46 @@ function validateTokenParameters(tokenParams: {
* @param expiresAt - Date at which the token expires.
* @returns The payload encoded in base64.
*/
function createPayload(payloadParams: {
function createPayload({
searchRules,
apiKeyUid,
expiresAt,
}: {
searchRules: TokenSearchRules;
uid: string;
apiKeyUid: string;
expiresAt?: Date;
}): string {
const { searchRules, uid, expiresAt } = payloadParams;

const payload = {
searchRules,
apiKeyUid: uid,
apiKeyUid,
exp: expiresAt ? Math.floor(expiresAt.getTime() / 1000) : undefined,
};

return encode64(payload).replace(/=/g, "");
}

class Token {
config: Config;

constructor(config: Config) {
this.config = config;
}

/**
* Generate a tenant token
*
* @param apiKeyUid - The uid of the api key used as issuer of the token.
* @param searchRules - Search rules that are applied to every search.
* @param options - Token options to customize some aspect of the token.
* @returns The token in JWT format.
*/
async generateTenantToken(
apiKeyUid: string,
searchRules: TokenSearchRules,
options?: TokenOptions,
): Promise<string> {
const apiKey = options?.apiKey || this.config.apiKey || "";
const uid = apiKeyUid || "";
const expiresAt = options?.expiresAt;

validateTokenParameters({ apiKey, uid, expiresAt, searchRules });

const encodedHeader = createHeader();
const encodedPayload = createPayload({ searchRules, uid, expiresAt });
const signature = await sign(apiKey, encodedHeader, encodedPayload);
/**
* Generate a tenant token
*
* @param apiKeyUid - The uid of the api key used as issuer of the token.
* @param searchRules - Search rules that are applied to every search.
* @param options - Token options to customize some aspect of the token.
* @returns The token in JWT format.
*/
export async function generateTenantToken(
apiKeyUid: string,
searchRules: TokenSearchRules,
{ apiKey, expiresAt }: TokenOptions,
): Promise<string> {
validateTokenParameters({ apiKeyUid, expiresAt, searchRules });

const encodedHeader = createHeader();
const encodedPayload = createPayload({
searchRules,
apiKeyUid,
expiresAt,
});
const signature = await sign(apiKey, encodedHeader, encodedPayload);

return `${encodedHeader}.${encodedPayload}.${signature}`;
}
return `${encodedHeader}.${encodedPayload}.${signature}`;
}
export { Token };
2 changes: 1 addition & 1 deletion src/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,6 @@ export type TokenSearchRules =
| string[];

export type TokenOptions = {
apiKey?: string;
apiKey: string;
expiresAt?: Date;
};
Loading

0 comments on commit 4fe62f3

Please sign in to comment.