@takaro/auth 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ export var PERMISSIONS;
2
+ (function (PERMISSIONS) {
3
+ PERMISSIONS["ROOT"] = "ROOT";
4
+ PERMISSIONS["MANAGE_USERS"] = "MANAGE_USERS";
5
+ PERMISSIONS["READ_USERS"] = "READ_USERS";
6
+ PERMISSIONS["MANAGE_ROLES"] = "MANAGE_ROLES";
7
+ PERMISSIONS["READ_ROLES"] = "READ_ROLES";
8
+ PERMISSIONS["MANAGE_GAMESERVERS"] = "MANAGE_GAMESERVERS";
9
+ PERMISSIONS["READ_GAMESERVERS"] = "READ_GAMESERVERS";
10
+ PERMISSIONS["READ_MODULES"] = "READ_MODULES";
11
+ PERMISSIONS["MANAGE_MODULES"] = "MANAGE_MODULES";
12
+ PERMISSIONS["READ_PLAYERS"] = "READ_PLAYERS";
13
+ PERMISSIONS["MANAGE_PLAYERS"] = "MANAGE_PLAYERS";
14
+ PERMISSIONS["MANAGE_SETTINGS"] = "MANAGE_SETTINGS";
15
+ PERMISSIONS["READ_SETTINGS"] = "READ_SETTINGS";
16
+ PERMISSIONS["READ_VARIABLES"] = "READ_VARIABLES";
17
+ PERMISSIONS["MANAGE_VARIABLES"] = "MANAGE_VARIABLES";
18
+ PERMISSIONS["READ_EVENTS"] = "READ_EVENTS";
19
+ PERMISSIONS["MANAGE_EVENTS"] = "MANAGE_EVENTS";
20
+ PERMISSIONS["READ_ITEMS"] = "READ_ITEMS";
21
+ PERMISSIONS["MANAGE_ITEMS"] = "MANAGE_ITEMS";
22
+ })(PERMISSIONS || (PERMISSIONS = {}));
23
+ export const PERMISSION_DETAILS = {
24
+ [PERMISSIONS.ROOT]: {
25
+ permission: PERMISSIONS.ROOT,
26
+ friendlyName: 'Root Access',
27
+ description: 'Full access to all systems and resources',
28
+ },
29
+ [PERMISSIONS.MANAGE_USERS]: {
30
+ permission: PERMISSIONS.MANAGE_USERS,
31
+ friendlyName: 'Manage Users',
32
+ description: 'Can create, update, and delete users',
33
+ },
34
+ [PERMISSIONS.READ_USERS]: {
35
+ permission: PERMISSIONS.READ_USERS,
36
+ friendlyName: 'Read Users',
37
+ description: 'Can view user details',
38
+ },
39
+ [PERMISSIONS.MANAGE_ROLES]: {
40
+ permission: PERMISSIONS.MANAGE_ROLES,
41
+ friendlyName: 'Manage Roles',
42
+ description: 'Can create, update, and delete roles',
43
+ },
44
+ [PERMISSIONS.READ_ROLES]: {
45
+ permission: PERMISSIONS.READ_ROLES,
46
+ friendlyName: 'Read Roles',
47
+ description: 'Can view role details',
48
+ },
49
+ [PERMISSIONS.MANAGE_GAMESERVERS]: {
50
+ permission: PERMISSIONS.MANAGE_GAMESERVERS,
51
+ friendlyName: 'Manage Game Servers',
52
+ description: 'Can create, update, and delete game servers',
53
+ },
54
+ [PERMISSIONS.READ_GAMESERVERS]: {
55
+ permission: PERMISSIONS.READ_GAMESERVERS,
56
+ friendlyName: 'Read Game Servers',
57
+ description: 'Can view game server details',
58
+ },
59
+ [PERMISSIONS.READ_MODULES]: {
60
+ permission: PERMISSIONS.READ_MODULES,
61
+ friendlyName: 'Read Modules',
62
+ description: 'Can view module details',
63
+ },
64
+ [PERMISSIONS.MANAGE_MODULES]: {
65
+ permission: PERMISSIONS.MANAGE_MODULES,
66
+ friendlyName: 'Manage Modules',
67
+ description: 'Can create, update, and delete modules',
68
+ },
69
+ [PERMISSIONS.READ_PLAYERS]: {
70
+ permission: PERMISSIONS.READ_PLAYERS,
71
+ friendlyName: 'Read Players',
72
+ description: 'Can view player details',
73
+ },
74
+ [PERMISSIONS.MANAGE_PLAYERS]: {
75
+ permission: PERMISSIONS.MANAGE_PLAYERS,
76
+ friendlyName: 'Manage Players',
77
+ description: 'Can create, update, and delete players',
78
+ },
79
+ [PERMISSIONS.MANAGE_SETTINGS]: {
80
+ permission: PERMISSIONS.MANAGE_SETTINGS,
81
+ friendlyName: 'Manage Settings',
82
+ description: 'Can modify settings',
83
+ },
84
+ [PERMISSIONS.READ_SETTINGS]: {
85
+ permission: PERMISSIONS.READ_SETTINGS,
86
+ friendlyName: 'Read Settings',
87
+ description: 'Can view settings',
88
+ },
89
+ [PERMISSIONS.READ_VARIABLES]: {
90
+ permission: PERMISSIONS.READ_VARIABLES,
91
+ friendlyName: 'Read Variables',
92
+ description: 'Can view variables',
93
+ },
94
+ [PERMISSIONS.MANAGE_VARIABLES]: {
95
+ permission: PERMISSIONS.MANAGE_VARIABLES,
96
+ friendlyName: 'Manage Variables',
97
+ description: 'Can create, update, and delete variables',
98
+ },
99
+ [PERMISSIONS.READ_EVENTS]: {
100
+ permission: PERMISSIONS.READ_EVENTS,
101
+ friendlyName: 'Read Events',
102
+ description: 'Can view event details',
103
+ },
104
+ [PERMISSIONS.MANAGE_EVENTS]: {
105
+ permission: PERMISSIONS.MANAGE_EVENTS,
106
+ friendlyName: 'Manage Events',
107
+ description: 'Can create, update, and delete events',
108
+ },
109
+ [PERMISSIONS.READ_ITEMS]: {
110
+ permission: PERMISSIONS.READ_ITEMS,
111
+ friendlyName: 'Read Items',
112
+ description: 'Can view item details',
113
+ },
114
+ [PERMISSIONS.MANAGE_ITEMS]: {
115
+ permission: PERMISSIONS.MANAGE_ITEMS,
116
+ friendlyName: 'Manage Items',
117
+ description: 'Can create, update, and delete items',
118
+ },
119
+ };
120
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/lib/permissions.ts"],"names":[],"mappings":"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"}
package/dist/main.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { IAuthConfig, configSchema as authConfigSchema } from './config.js';
2
+ export { ory, AUDIENCES } from './lib/ory.js';
3
+ export * from './lib/permissions.js';
package/dist/main.js ADDED
@@ -0,0 +1,4 @@
1
+ export { configSchema as authConfigSchema } from './config.js';
2
+ export { ory, AUDIENCES } from './lib/ory.js';
3
+ export * from './lib/permissions.js';
4
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,YAAY,IAAI,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAC9C,cAAc,sBAAsB,CAAC"}
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "@takaro/auth",
3
+ "version": "0.0.1",
4
+ "description": "An opinionated auth handler",
5
+ "main": "dist/main.js",
6
+ "types": "dist/main.d.ts",
7
+ "type": "module",
8
+ "scripts": {
9
+ "start:dev": "tsc --watch --preserveWatchOutput -p ./tsconfig.build.json",
10
+ "build": "tsc -p ./tsconfig.build.json",
11
+ "test": "npm run test:unit --if-present && npm run test:integration --if-present",
12
+ "test:unit": "echo 'No tests (yet :))'",
13
+ "test:integration": "mocha --config ../../.mocharc.js src/**/*.integration.test.ts"
14
+ },
15
+ "keywords": [],
16
+ "author": "",
17
+ "license": "ISC",
18
+ "dependencies": {
19
+ "convict": "^6.2.3"
20
+ },
21
+ "devDependencies": {
22
+ "@takaro/test": "0.0.1",
23
+ "@types/convict": "^6.1.1"
24
+ }
25
+ }
package/src/config.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { Config, IBaseConfig } from '@takaro/config';
2
+
3
+ export interface IAuthConfig extends IBaseConfig {
4
+ kratos: {
5
+ publicUrl: string;
6
+ adminUrl: string;
7
+ };
8
+ hydra: {
9
+ publicUrl: string;
10
+ adminUrl: string;
11
+ adminClientId: string;
12
+ adminClientSecret: string;
13
+ };
14
+ takaro: {
15
+ url: string;
16
+ };
17
+ }
18
+
19
+ export const configSchema = {
20
+ kratos: {
21
+ publicUrl: {
22
+ doc: 'The URL of the Kratos public API',
23
+ format: String,
24
+ default: 'http://kratos:4433',
25
+ env: 'KRATOS_URL',
26
+ },
27
+ adminUrl: {
28
+ doc: 'The URL of the Kratos admin API',
29
+ format: String,
30
+ default: 'http://kratos:4434',
31
+ env: 'KRATOS_ADMIN_URL',
32
+ },
33
+ },
34
+ hydra: {
35
+ publicUrl: {
36
+ doc: 'The URL of the Takaro OAuth server',
37
+ format: String,
38
+ default: 'http://hydra:4444',
39
+ env: 'TAKARO_OAUTH_HOST',
40
+ },
41
+ adminUrl: {
42
+ doc: 'The URL of the Takaro OAuth admin server',
43
+ format: String,
44
+ default: 'http://hydra:4445',
45
+ env: 'TAKARO_OAUTH_ADMIN_HOST',
46
+ },
47
+ adminClientId: {
48
+ doc: 'The client ID to use when authenticating with the Takaro server',
49
+ format: String,
50
+ default: null,
51
+ env: 'ADMIN_CLIENT_ID',
52
+ },
53
+ adminClientSecret: {
54
+ doc: 'The client secret to use when authenticating with the Takaro server',
55
+ format: String,
56
+ default: null,
57
+ env: 'ADMIN_CLIENT_SECRET',
58
+ },
59
+ },
60
+ takaro: {
61
+ url: {
62
+ doc: 'The URL of the Takaro server',
63
+ format: String,
64
+ default: 'http://localhost:3000',
65
+ env: 'TAKARO_HOST',
66
+ },
67
+ },
68
+ };
69
+
70
+ export const config = new Config<IAuthConfig>([configSchema]);
@@ -0,0 +1,26 @@
1
+ import { ory } from '../ory.js';
2
+ import { faker } from '@faker-js/faker';
3
+ import { expect } from '@takaro/test';
4
+
5
+ describe('Ory', () => {
6
+ it('Create and delete identities', async () => {
7
+ // First, create a bunch of identities
8
+ const totalIdentities = 150;
9
+ const identities = await Promise.all(
10
+ Array.from({ length: totalIdentities }).map(() =>
11
+ ory.createIdentity(faker.internet.email(), 'password', 'domainId')
12
+ )
13
+ );
14
+
15
+ // Fetch the first one by ID
16
+
17
+ const firstIdentity = await ory.getIdentity(identities[0].id);
18
+ expect(firstIdentity.email).to.be.eq(identities[0].email);
19
+
20
+ // Delete them all
21
+ await ory.deleteIdentitiesForDomain('domainId');
22
+
23
+ // Make sure they're gone
24
+ expect(ory.getIdentity(identities[0].id)).to.eventually.be.rejectedWith('Not Found');
25
+ });
26
+ });
package/src/lib/ory.ts ADDED
@@ -0,0 +1,277 @@
1
+ import { Configuration, CreateIdentityBody, FrontendApi, IdentityApi, OAuth2Api } from '@ory/client';
2
+ import { config } from '../config.js';
3
+ import { errors, logger, TakaroDTO } from '@takaro/util';
4
+ import { createAxiosClient } from './oryAxiosClient.js';
5
+ import { paginateIdentities } from './paginationHelpers.js';
6
+ import { Request } from 'express';
7
+ import { IsBoolean, IsNumber, IsString } from 'class-validator';
8
+
9
+ enum IDENTITY_SCHEMA {
10
+ USER = 'user_v0',
11
+ }
12
+
13
+ export enum AUDIENCES {
14
+ // Used for various sysadmin tasks in the Takaro API
15
+ TAKARO_API_ADMIN = 't:api:admin',
16
+ }
17
+
18
+ export interface ITakaroIdentity {
19
+ id: string;
20
+ email: string;
21
+ domainId: string;
22
+ }
23
+
24
+ export class TakaroTokenDTO extends TakaroDTO<TakaroTokenDTO> {
25
+ @IsBoolean()
26
+ active: boolean;
27
+ @IsString()
28
+ clientId: string;
29
+ @IsNumber()
30
+ exp: number;
31
+ @IsNumber()
32
+ iat: number;
33
+ @IsString()
34
+ iss: string;
35
+ @IsString()
36
+ sub: string;
37
+ @IsString({ each: true })
38
+ aud: string[];
39
+ }
40
+
41
+ function metadataTypeguard(metadata: unknown): metadata is { domainId: string } {
42
+ return typeof metadata === 'object' && metadata !== null && 'domainId' in metadata;
43
+ }
44
+
45
+ class Ory {
46
+ private authToken: string | null = null;
47
+ private log = logger('ory');
48
+
49
+ private adminClient: OAuth2Api;
50
+ private identityClient: IdentityApi;
51
+ private frontendClient: FrontendApi;
52
+
53
+ constructor() {
54
+ this.identityClient = new IdentityApi(
55
+ new Configuration({
56
+ basePath: config.get('kratos.adminUrl'),
57
+ }),
58
+ undefined,
59
+ createAxiosClient(config.get('kratos.adminUrl'))
60
+ );
61
+
62
+ this.frontendClient = new FrontendApi(
63
+ new Configuration({
64
+ basePath: config.get('kratos.publicUrl'),
65
+ }),
66
+ undefined,
67
+ createAxiosClient(config.get('kratos.publicUrl'))
68
+ );
69
+ this.adminClient = new OAuth2Api(
70
+ new Configuration({
71
+ basePath: config.get('hydra.adminUrl'),
72
+ }),
73
+ undefined,
74
+ createAxiosClient(config.get('hydra.adminUrl'))
75
+ );
76
+ }
77
+
78
+ get OAuth2URL() {
79
+ return config.get('hydra.publicUrl');
80
+ }
81
+
82
+ async deleteIdentitiesForDomain(domainId: string) {
83
+ for await (const identities of paginateIdentities(this.identityClient)) {
84
+ for (const identity of identities) {
85
+ if (
86
+ identity.metadata_public &&
87
+ metadataTypeguard(identity.metadata_public) &&
88
+ identity.metadata_public.domainId === domainId
89
+ ) {
90
+ await this.deleteIdentity(identity.id);
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ async getIdentity(id: string): Promise<ITakaroIdentity> {
97
+ const res = await this.identityClient.getIdentity({
98
+ id,
99
+ });
100
+
101
+ if (!res.data.metadata_public) {
102
+ this.log.warn('Identity has no metadata_public', {
103
+ identity: res.data.id,
104
+ });
105
+ throw new errors.ForbiddenError();
106
+ }
107
+
108
+ if (!metadataTypeguard(res.data.metadata_public)) {
109
+ this.log.warn('Identity metadata_public is not of type {domainId: string}', { identity: res.data.id });
110
+ throw new errors.ForbiddenError();
111
+ }
112
+
113
+ return {
114
+ id: res.data.id,
115
+ email: res.data.traits.email,
116
+ domainId: res.data.metadata_public.domainId,
117
+ };
118
+ }
119
+
120
+ async createIdentity(email: string, domainId: string, password?: string): Promise<ITakaroIdentity> {
121
+ const body: CreateIdentityBody = {
122
+ schema_id: IDENTITY_SCHEMA.USER,
123
+ traits: {
124
+ email,
125
+ },
126
+ metadata_public: {
127
+ domainId,
128
+ },
129
+ };
130
+
131
+ if (password) {
132
+ body.credentials = {
133
+ password: {
134
+ config: {
135
+ password,
136
+ },
137
+ },
138
+ };
139
+ }
140
+
141
+ const res = await this.identityClient.createIdentity({
142
+ createIdentityBody: body,
143
+ });
144
+
145
+ return {
146
+ id: res.data.id,
147
+ email: res.data.traits.email,
148
+ domainId,
149
+ };
150
+ }
151
+
152
+ async deleteIdentity(id: string): Promise<void> {
153
+ await this.identityClient.deleteIdentity({
154
+ id,
155
+ });
156
+ }
157
+
158
+ async getIdentityFromReq(req: Request): Promise<ITakaroIdentity> {
159
+ const tokenFromAuthHeader = req.headers['authorization']?.replace('Bearer ', '');
160
+
161
+ const sessionRes = await this.frontendClient.toSession({
162
+ cookie: req.headers.cookie,
163
+ xSessionToken: tokenFromAuthHeader,
164
+ });
165
+
166
+ if (!sessionRes.data.identity!.metadata_public) {
167
+ this.log.warn('Identity has no metadata_public', {
168
+ identity: sessionRes.data.identity!.id,
169
+ });
170
+ throw new errors.ForbiddenError();
171
+ }
172
+
173
+ if (!metadataTypeguard(sessionRes.data.identity!.metadata_public)) {
174
+ this.log.warn('Identity metadata_public is not of type {domainId: string}', {
175
+ identity: sessionRes.data.identity!.id,
176
+ });
177
+ throw new errors.ForbiddenError();
178
+ }
179
+
180
+ return {
181
+ id: sessionRes.data.identity!.id,
182
+ email: sessionRes.data.identity!.traits.email,
183
+ domainId: sessionRes.data.identity!.metadata_public.domainId,
184
+ };
185
+ }
186
+
187
+ async submitApiLogin(username: string, password: string) {
188
+ const flow = await this.frontendClient.createNativeLoginFlow({
189
+ refresh: true,
190
+ });
191
+ return this.frontendClient.updateLoginFlow({
192
+ flow: flow.data.id,
193
+ updateLoginFlowBody: {
194
+ password,
195
+ identifier: username,
196
+ method: 'password',
197
+ },
198
+ });
199
+ }
200
+
201
+ async apiLogout(req: Request) {
202
+ const tokenFromAuthHeader = req.headers['authorization']?.replace('Bearer ', '');
203
+
204
+ if (!tokenFromAuthHeader) return true;
205
+
206
+ return this.frontendClient.performNativeLogout({
207
+ performNativeLogoutBody: {
208
+ session_token: tokenFromAuthHeader,
209
+ },
210
+ });
211
+ }
212
+
213
+ async introspectToken(token: string): Promise<TakaroTokenDTO> {
214
+ const introspectRes = await this.adminClient.introspectOAuth2Token({
215
+ token,
216
+ });
217
+
218
+ const data = new TakaroTokenDTO({
219
+ active: introspectRes.data.active,
220
+ clientId: introspectRes.data.client_id,
221
+ aud: introspectRes.data.aud,
222
+ exp: introspectRes.data.exp,
223
+ iat: introspectRes.data.iat,
224
+ iss: introspectRes.data.iss,
225
+ sub: introspectRes.data.sub,
226
+ });
227
+
228
+ try {
229
+ // Check for correctness of the data
230
+ // DOES NOT CHECK FOR EXPIRATION
231
+ await data.validate();
232
+ } catch (error) {
233
+ this.log.warn('Introspected token has invalid shape', { error });
234
+ throw new errors.ForbiddenError();
235
+ }
236
+
237
+ return data;
238
+ }
239
+
240
+ // Currently, this is only used for creating the admin-auth client.
241
+ // ...In the future we should make this more generic and allow for
242
+ // creating any API client perhaps?
243
+ async createOIDCClient(): Promise<{
244
+ clientId: string;
245
+ clientSecret: string;
246
+ }> {
247
+ const client = await this.adminClient.createOAuth2Client({
248
+ oAuth2Client: {
249
+ grant_types: ['client_credentials'],
250
+ audience: [AUDIENCES.TAKARO_API_ADMIN],
251
+ },
252
+ });
253
+
254
+ if (!client.data.client_id || !client.data.client_secret) {
255
+ this.log.error('Could not create OIDC client', { client });
256
+ throw new errors.InternalServerError();
257
+ }
258
+
259
+ return {
260
+ clientId: client.data.client_id,
261
+ clientSecret: client.data.client_secret,
262
+ };
263
+ }
264
+
265
+ async getRecoveryFlow(id: string) {
266
+ const recoveryRes = await this.identityClient.createRecoveryLinkForIdentity({
267
+ createRecoveryLinkForIdentityBody: {
268
+ identity_id: id,
269
+ expires_in: '24h',
270
+ },
271
+ });
272
+
273
+ return recoveryRes.data;
274
+ }
275
+ }
276
+
277
+ export const ory = new Ory();
@@ -0,0 +1,68 @@
1
+ import axios, { AxiosError } from 'axios';
2
+ import { addCounterToAxios, errors, logger } from '@takaro/util';
3
+
4
+ const log = logger('ory:http');
5
+
6
+ export function createAxiosClient(baseURL: string) {
7
+ const client = axios.create({
8
+ baseURL,
9
+ headers: {
10
+ 'Content-Type': 'application/json',
11
+ 'User-Agent': 'Takaro-Agent',
12
+ },
13
+ });
14
+
15
+ addCounterToAxios(client, {
16
+ name: 'ory_api_requests_total',
17
+ help: 'Total number of requests to the Ory API',
18
+ });
19
+
20
+ client.interceptors.request.use((request) => {
21
+ log.silly(`➡️ ${request.method?.toUpperCase()} ${request.url}`, {
22
+ method: request.method,
23
+ url: request.url,
24
+ });
25
+ return request;
26
+ });
27
+
28
+ client.interceptors.response.use(
29
+ (response) => {
30
+ log.silly(
31
+ `⬅️ ${response.request.method?.toUpperCase()} ${response.request.path} ${response.status} ${
32
+ response.statusText
33
+ }`,
34
+ {
35
+ status: response.status,
36
+ method: response.request.method,
37
+ url: response.request.url,
38
+ }
39
+ );
40
+
41
+ return response;
42
+ },
43
+ (error: AxiosError) => {
44
+ let details = {};
45
+
46
+ if (error.response?.data) {
47
+ const data = error.response.data as Record<string, unknown>;
48
+ details = JSON.stringify(data.error_description);
49
+ }
50
+
51
+ log.error(`☠️ Request errored: [${error.response?.status}] ${details}`, {
52
+ status: error.response?.status,
53
+ statusText: error.response?.statusText,
54
+ method: error.config?.method,
55
+ url: error.config?.url,
56
+ response: error.response?.data,
57
+ });
58
+
59
+ if (error.response?.status === 409) {
60
+ return Promise.reject(new errors.ConflictError('User with this identifier already exists'));
61
+ }
62
+
63
+ return Promise.reject(error);
64
+ }
65
+ );
66
+
67
+ return client;
68
+ }
@@ -0,0 +1,46 @@
1
+ import { IdentityApi } from '@ory/client';
2
+ import { parse, URLSearchParams } from 'url';
3
+
4
+ export async function* paginateIdentities(adminClient: IdentityApi, page = undefined, perPage = 100) {
5
+ let nextPage: number | undefined = page;
6
+
7
+ while (true) {
8
+ const response = await adminClient.listIdentities({
9
+ page: nextPage,
10
+ perPage,
11
+ });
12
+
13
+ if (response.data.length === 0) {
14
+ // Stop the iteration if there are no more items
15
+ break;
16
+ }
17
+
18
+ yield response.data;
19
+
20
+ // Parse Link header
21
+ const linkHeader = response.headers.link;
22
+ const links = linkHeader.split(',');
23
+ const nextLink = links.find((link: string) => link.includes('rel="next"'));
24
+
25
+ if (!nextLink) {
26
+ break;
27
+ }
28
+
29
+ // Extract the 'next' page URL
30
+ const match = nextLink.match(/<(.*)>/);
31
+ const url = match ? match[1] : undefined;
32
+
33
+ if (!url) {
34
+ break;
35
+ }
36
+
37
+ const parsedUrl = parse(url);
38
+ const params = new URLSearchParams(parsedUrl.query || '');
39
+ const nextPageToken = params.get('page');
40
+ if (nextPageToken) {
41
+ nextPage = parseInt(nextPageToken, 10);
42
+ } else {
43
+ break; // stop if there is no next page
44
+ }
45
+ }
46
+ }