• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / devtools-shared / 16367244197

18 Jul 2025 09:32AM UTC coverage: 72.518%. First build
16367244197

Pull #564

github

lerouxb
vectorEmbedding index types are apparently not a thing
Pull Request #564: fix(constants): vectorEmbedding index types are apparently not a thing COMPASS-9574

1508 of 2369 branches covered (63.66%)

Branch coverage included in aggregate %.

3255 of 4199 relevant lines covered (77.52%)

594.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

4.41
/packages/oidc-mock-provider/src/index.ts
1
import { once } from 'events';
1✔
2
import type {
3
  IncomingMessage,
4
  ServerResponse,
5
  Server as HTTPServer,
6
  RequestListener,
7
} from 'http';
8
import { createServer as createHTTPServer } from 'http';
1✔
9
import type { AddressInfo } from 'net';
10
import crypto from 'crypto';
1✔
11
import type { KeyPairKeyObjectResult } from 'crypto';
12
import { promisify } from 'util';
1✔
13
import { URLSearchParams } from 'url';
1✔
14

15
async function randomString(n: number, enc: BufferEncoding) {
16
  return (await promisify(crypto.randomBytes)(n)).toString(enc);
×
17
}
18
function toJSONtoUTF8toBase64Url(obj: unknown): string {
19
  return Buffer.from(JSON.stringify(obj), 'utf8').toString('base64url');
×
20
}
21

22
export interface TokenMetadata {
23
  // Using lower_snake_case here for some of the standard OIDC
24
  // parameters that are defined this way.
25
  client_id: string;
26
  scope: string;
27
  nonce?: string;
28
}
29

30
export type MaybePromise<T> = T | PromiseLike<T>;
31

32
export interface OIDCMockProviderConfig {
33
  /**
34
   * expires_in, payload: Return fields to be included in the generated Access and ID Tokens.
35
   * This should include e.g. `sub` and any other OIDC claims that are relevant.
36
   *
37
   * skipIdToken: Exclude ID Token
38
   *
39
   * customIdTokenPayload: Custom overrides in payload data for the ID token
40
   */
41
  getTokenPayload(metadata: TokenMetadata): MaybePromise<{
42
    expires_in: number;
43
    payload: Record<string, unknown>;
44
    customIdTokenPayload?: Record<string, unknown>;
45
    skipIdToken?: boolean;
46
  }>;
47

48
  /**
49
   * Allow override special handling for specific types of requests.
50
   */
51
  overrideRequestHandler?(
52
    url: string,
53
    req: IncomingMessage,
54
    res: ServerResponse,
55
  ): MaybePromise<void>;
56

57
  /**
58
   * Optional port number for the server to listen on.
59
   */
60
  port?: number;
61

62
  /**
63
   * Optional hostname for the server to listen on.
64
   */
65
  hostname?: string;
66

67
  /**
68
   * Optional bind to all IPv4 and IPv6 addresses.
69
   */
70
  bindIpAll?: boolean;
71

72
  /**
73
   * Optional additional fields to be returned when the OIDC configuration is accessed.
74
   */
75
  additionalIssuerMetadata?: () => Record<string, unknown>;
76

77
  /**
78
   * Optional replacement for the HTTP 'createServer' function, e.g. to specify a HTTPS
79
   * server with TLS settings.
80
   */
81
  createHTTPServer?: (requestListener: RequestListener) => HTTPServer;
82
}
83

84
/**
85
 * A mock OIDC authorization server (AS) implementation.
86
 *
87
 * This mock will happily give out valid tokens with arbitrary contents, so
88
 * it is absolutely unusable for usage outside of testing!
89
 */
90
export class OIDCMockProvider {
1✔
91
  public httpServer: HTTPServer;
92
  public issuer: string; // URL of the HTTP server
93

94
  // This provider only supports a single RSA key currently.
95
  private kid: string;
96
  private keys: KeyPairKeyObjectResult;
97

98
  private state = new Map<string, unknown>();
×
99
  private config: OIDCMockProviderConfig;
100

101
  private constructor(config: OIDCMockProviderConfig) {
102
    this.httpServer = (config.createHTTPServer ?? createHTTPServer)(
×
103
      (req, res) => void this.handleRequest(req, res),
×
104
    );
105
    this.config = config;
×
106

107
    // Initialized in .init().
108
    this.issuer = '';
×
109
    this.kid = '';
×
110
    this.keys = {} as unknown as typeof this.keys;
×
111
  }
112

113
  private async init(): Promise<this> {
114
    this.httpServer.listen(
×
115
      this.config.port ?? 0,
×
116
      this.config.bindIpAll ? '::' : this.config.hostname,
×
117
    );
118
    await once(this.httpServer, 'listening');
×
119
    const { port } = this.httpServer.address() as AddressInfo;
×
120
    this.issuer = `${
×
121
      'setSecureContext' in this.httpServer ? 'https' : 'http'
×
122
    }://${this.config.hostname ?? 'localhost'}:${port}`;
×
123
    this.kid = await randomString(8, 'hex');
×
124
    this.keys = await promisify(crypto.generateKeyPair)('rsa', {
×
125
      modulusLength: 2048,
126
    });
127
    return this;
×
128
  }
129

130
  public static async create(
131
    config: OIDCMockProviderConfig,
132
  ): Promise<OIDCMockProvider> {
133
    return await new this(config).init();
×
134
  }
135

136
  public async close(): Promise<void> {
137
    this.httpServer.close();
×
138
    await once(this.httpServer, 'close');
×
139
  }
140

141
  private async handleRequest(
142
    req: IncomingMessage,
143
    res: ServerResponse,
144
  ): Promise<void> {
145
    res.setHeader('Content-Type', 'application/json');
×
146
    try {
×
147
      const url = new URL(req.url || '/', this.issuer);
×
148
      if (req.method === 'POST') {
×
149
        // For simplicity, just merge POST parameters with GET parameters...
150
        if (
×
151
          !/^application\/x-www-form-urlencoded(;charset=utf-?8)?$/i.test(
152
            req.headers['content-type'] ?? '',
×
153
          )
154
        ) {
155
          throw new Error(
×
156
            'Only accepting application/x-www-form-urlencoded POST bodies',
157
          );
158
        }
159
        let body = '';
×
160
        for await (const chunk of req.setEncoding('utf8')) {
×
161
          body += chunk;
×
162
        }
163
        const parsed = new URLSearchParams(body);
×
164
        url.search = new URLSearchParams({
×
165
          ...Object.fromEntries(url.searchParams),
166
          ...Object.fromEntries(parsed),
167
        }).toString();
168
      }
169
      let result: unknown;
170
      await this.config.overrideRequestHandler?.(url.toString(), req, res);
×
171
      if (res.writableEnded) return;
×
172
      switch (url.pathname) {
×
173
        case '/.well-known/openid-configuration':
174
          result = {
×
175
            issuer: this.issuer,
176
            token_endpoint: new URL('/token', this.issuer).toString(),
177
            authorization_endpoint: new URL(
178
              '/authorize',
179
              this.issuer,
180
            ).toString(),
181
            jwks_uri: new URL('/jwks', this.issuer).toString(),
182
            device_authorization_endpoint: new URL(
183
              '/device',
184
              this.issuer,
185
            ).toString(),
186
            ...this.config.additionalIssuerMetadata?.(),
187
          };
188
          break;
×
189
        case '/jwks':
190
          // Provide this server's public key in JWK format
191
          result = {
×
192
            keys: [
193
              {
194
                alg: 'RS256',
195
                kid: this.kid,
196
                ...this.keys.publicKey.export({ format: 'jwk' }),
197
              },
198
            ],
199
          };
200
          break;
×
201
        case '/authorize': {
202
          // Authorization code flow entry point, immediately redirect to client with success
203
          const {
204
            client_id,
205
            scope,
206
            response_type,
207
            redirect_uri,
208
            code_challenge,
209
            code_challenge_method,
210
            state,
211
            nonce,
212
          } = Object.fromEntries(url.searchParams);
×
213
          if (response_type !== 'code') {
×
214
            throw new Error(`unknown response_type ${response_type}`);
×
215
          }
216
          const redirectTo = new URL(redirect_uri);
×
217
          redirectTo.search = new URLSearchParams({
×
218
            code: await this.storeForSingleRetrieval({
219
              client_id,
220
              scope,
221
              code_challenge,
222
              code_challenge_method,
223
              nonce,
224
            }),
225
            state,
226
          }).toString();
227
          res.statusCode = 307;
×
228
          res.setHeader('Location', redirectTo.toString());
×
229
          res.end(`Moved to ${redirectTo.toString()}`);
×
230
          break;
×
231
        }
232
        case '/token': {
233
          // Provide a token after successful Auth Code Flow/Device Auth Flow
234
          const { code, code_verifier, device_code } = Object.fromEntries(
×
235
            url.searchParams,
236
          );
237
          const {
238
            client_id,
239
            scope,
240
            code_challenge,
241
            code_challenge_method,
242
            isDeviceCode,
243
            nonce,
244
          } = this.retrieveFromStorage(device_code ?? code);
×
245

246
          if (!isDeviceCode) {
×
247
            // Verify the code challenge. Not strictly necessary here since
248
            // we assume the OIDC implementation we are using to be correct
249
            // and this server is test-only, but also not a lot of extra work
250
            // to add this.
251
            if (code_challenge_method !== 'S256') {
×
252
              throw new Error(
×
253
                `Unsupported code challenge method ${String(
254
                  code_challenge_method,
255
                )}`,
256
              );
257
            }
258

259
            const expectedChallenge = crypto
×
260
              .createHash('sha256')
261
              .update(code_verifier)
262
              .digest('hex');
263
            const actualChallenge = Buffer.from(
×
264
              // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
265
              code_challenge,
266
              'base64',
267
            ).toString('hex');
268
            if (expectedChallenge !== actualChallenge) {
×
269
              throw new Error('Challenge mismatch');
×
270
            }
271
          }
272

273
          const { access_token, id_token, expires_in } = await this.issueToken({
×
274
            client_id,
275
            scope,
276
            nonce,
277
          });
278

279
          // Issue a token response:
280
          result = {
×
281
            access_token: access_token,
282
            id_token: id_token,
283
            refresh_token: await this.storeForSingleRetrieval({
284
              id_token,
285
              access_token,
286
            }),
287
            token_type: 'Bearer',
288
            expires_in,
289
          };
290
          break;
×
291
        }
292
        case '/device': {
293
          const { client_id, scope } = Object.fromEntries(url.searchParams);
×
294
          const device_code = await this.storeForSingleRetrieval({
×
295
            client_id,
296
            scope,
297
            isDeviceCode: true,
298
          });
299
          result = {
×
300
            device_code,
301
            user_code: await randomString(8, 'hex'),
302
            verification_uri: new URL('/verify-device', this.issuer).toString(),
303
            expires_in: 3600,
304
            interval: 1,
305
          };
306
          break;
×
307
        }
308
        case '/verify-device':
309
          // The Device Auth Flow requires a URL to point users at.
310
          // We can just use this as a dummy endpoint.
311
          res.statusCode = 200;
×
312
          result = { status: 'Verified!' };
×
313
          break;
×
314
        default:
315
          res.statusCode = 404;
×
316
          result = { error: 'not found:' + url.pathname };
×
317
          break;
×
318
      }
319
      res.end(JSON.stringify(result));
×
320
    } catch (err) {
321
      res.statusCode = 500;
×
322
      res.end(
×
323
        JSON.stringify({
324
          error:
325
            typeof err === 'object' && err && 'message' in err
×
326
              ? (err as Error).message
327
              : String(err),
328
        }),
329
      );
330
    }
331
  }
332

333
  private async issueToken(metadata: TokenMetadata): Promise<{
334
    expires_in: number;
335
    access_token: string;
336
    id_token: string | undefined;
337
  }> {
338
    const { expires_in, payload, skipIdToken, customIdTokenPayload } =
339
      await this.config.getTokenPayload(metadata);
×
340
    const currentTimeInSeconds = Math.floor(Date.now() / 1000);
×
341
    const header = {
×
342
      alg: 'RS256',
343
      typ: 'JWT',
344
      kid: this.kid,
345
    };
346
    const fullPayload = {
×
347
      jti: await randomString(8, 'hex'),
348
      iat: currentTimeInSeconds,
349
      exp: currentTimeInSeconds + expires_in,
350
      client_id: metadata.client_id,
351
      scope: metadata.scope,
352
      iss: this.issuer,
353
      aud: metadata.client_id,
354
      nonce: metadata.nonce,
355
      ...payload,
356
    };
357
    const makeToken = (payload: Record<string, unknown>) => {
×
358
      const signedMaterial =
359
        toJSONtoUTF8toBase64Url(header) +
×
360
        '.' +
361
        toJSONtoUTF8toBase64Url(payload);
362
      const signature = crypto
×
363
        .createSign('RSA-SHA256')
364
        .update(signedMaterial)
365
        .sign(this.keys.privateKey, 'base64url');
366
      return `${signedMaterial}.${signature}`;
×
367
    };
368
    return {
×
369
      expires_in,
370
      access_token: makeToken(fullPayload),
371
      // In an ID Token, aud === client_id, in an Access Token, not necessarily
372
      id_token: skipIdToken
×
373
        ? undefined
374
        : makeToken({
375
            ...fullPayload,
376
            aud: metadata.client_id,
377
            ...customIdTokenPayload,
378
          }),
379
    };
380
  }
381

382
  // Store a value for later re-use in another HTTP request/response pair.
383
  private async storeForSingleRetrieval(payload: unknown): Promise<string> {
384
    const id = await randomString(12, 'base64');
×
385
    this.state.set(id, payload);
×
386
    return id;
×
387
  }
388

389
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
390
  private retrieveFromStorage(id: string): any {
391
    const entry = this.state.get(id);
×
392
    this.state.delete(id);
×
393
    return entry;
×
394
  }
395
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc