cypress-mailisk 3.0.3 → 3.2.0

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.
package/README.md CHANGED
@@ -212,6 +212,56 @@ cy.mailiskListSmsNumbers().then((response) => {
212
212
  });
213
213
  ```
214
214
 
215
+ ### TOTP authenticator devices
216
+
217
+ These commands manage saved Mailisk Authenticator devices and generate OTP codes through the Mailisk API. Use an organisation API key for these endpoints.
218
+
219
+ ```js
220
+ cy.mailiskDeviceList({ issuer: 'GitHub', username: 'qa@example.com' }).then((response) => {
221
+ const devices = response.items;
222
+ });
223
+
224
+ cy.mailiskDeviceCreate({
225
+ name: 'GitHub staging',
226
+ shared_secret: 'JBSWY3DPEHPK3PXP',
227
+ }).then((device) => {
228
+ cy.mailiskDeviceOtpByDeviceId(device.id).then((otp) => {
229
+ expect(otp.code).to.match(/^\d{6}$/);
230
+ });
231
+ });
232
+
233
+ cy.mailiskDeviceCreateCustom({
234
+ secret: 'JBSWY3DPEHPK3PXP',
235
+ username: 'qa@example.com',
236
+ issuer: 'GitHub',
237
+ digits: 6,
238
+ period: 30,
239
+ algorithm: 'SHA1',
240
+ });
241
+
242
+ cy.mailiskDeviceCreateFromBase32SecretKey({
243
+ base32_secret_key: 'JBSWY3DPEHPK3PXP',
244
+ issuer: 'GitHub',
245
+ });
246
+
247
+ cy.mailiskDeviceCreateFromOtpAuthUrl({
248
+ otp_auth_url: 'otpauth://totp/GitHub:qa@example.com?secret=JBSWY3DPEHPK3PXP&issuer=GitHub',
249
+ });
250
+
251
+ cy.mailiskDeviceOtpBySharedSecret('JBSWY3DPEHPK3PXP'); // one-off code without saving a device
252
+ cy.mailiskDeviceDelete('9b1f6ec0-b90d-4bd8-8dd0-f6b2d5138273');
253
+ ```
254
+
255
+ To avoid receiving a TOTP code right before it expires, pass min_seconds_until_expire. If the current code has fewer seconds remaining, Mailisk waits for the next code before returning.
256
+
257
+ ```js
258
+ cy.mailiskDeviceOtpByDeviceId(device.id, { min_seconds_until_expire: 10 }).then((otp) => {
259
+ cy.get('#code').type(otp.code);
260
+ });
261
+
262
+ cy.mailiskDeviceOtpBySharedSecret('JBSWY3DPEHPK3PXP', { min_seconds_until_expire: 10 });
263
+ ```
264
+
215
265
  ## Common test cases
216
266
 
217
267
  ### Working with email attachments
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cypress-mailisk",
3
- "version": "3.0.3",
3
+ "version": "3.2.0",
4
4
  "description": "Mailisk library for Cypress",
5
5
  "keywords": [
6
6
  "mailisk",
@@ -219,6 +219,139 @@ export interface SendVirtualSmsParams {
219
219
  body: string;
220
220
  }
221
221
 
222
+ export type TotpAlgorithm = 'SHA1' | 'SHA256' | 'SHA512';
223
+
224
+ export type KnownTotpDeviceSource = 'shared_secret' | 'custom' | 'base32_secret_key' | 'otpauth_url';
225
+
226
+ export type TotpDeviceSource = KnownTotpDeviceSource | (string & {});
227
+
228
+ export interface TotpDevice {
229
+ /** Unique identifier for the saved authenticator device */
230
+ id: string;
231
+ /** Unique identifier for the organisation */
232
+ organisation_id: string;
233
+ /** Device display name */
234
+ name: string;
235
+ /** Account label, if one is specified */
236
+ username?: string | null;
237
+ /** Issuer/app label, if one is specified */
238
+ issuer?: string | null;
239
+ /** Number of digits in generated OTP codes */
240
+ digits: number;
241
+ /** OTP validity period in seconds */
242
+ period: number;
243
+ /** TOTP hash algorithm */
244
+ algorithm: TotpAlgorithm;
245
+ /** Source used to create the saved device */
246
+ source: TotpDeviceSource;
247
+ /** Optional expiration timestamp */
248
+ expires_at?: string | null;
249
+ /** Date and time the device was created */
250
+ created_at: string;
251
+ /** Date and time the device was updated */
252
+ updated_at: string;
253
+ }
254
+
255
+ export interface TotpDeviceListParams {
256
+ /** Maximum number of devices returned. */
257
+ limit?: number;
258
+ /** Number of devices to skip. */
259
+ offset?: number;
260
+ /** Case-insensitive partial username match. */
261
+ username?: string;
262
+ /** Case-insensitive partial issuer match. */
263
+ issuer?: string;
264
+ }
265
+
266
+ export interface TotpDeviceListResponse {
267
+ total_count: number;
268
+ options: TotpDeviceListParams;
269
+ items: TotpDevice[];
270
+ }
271
+
272
+ export interface CreateTotpDeviceParams {
273
+ /** Base32 shared secret. */
274
+ shared_secret: string;
275
+ /** Optional device display name. */
276
+ name?: string;
277
+ /** Optional future ISO expiration timestamp. */
278
+ expires_at?: string;
279
+ }
280
+
281
+ export interface CreateCustomTotpDeviceParams {
282
+ /** Base32 shared secret. */
283
+ secret: string;
284
+ /** Optional device display name. */
285
+ name?: string;
286
+ /** Optional account label. */
287
+ username?: string;
288
+ /** Optional issuer/app label. */
289
+ issuer?: string;
290
+ /** Number of digits in generated OTP codes. */
291
+ digits?: 6 | 8;
292
+ /** OTP validity period in seconds. */
293
+ period?: number;
294
+ /** TOTP hash algorithm. */
295
+ algorithm?: TotpAlgorithm;
296
+ /** Optional future ISO expiration timestamp. */
297
+ expires_at?: string;
298
+ }
299
+
300
+ export interface CreateBase32SecretKeyTotpDeviceParams {
301
+ /** Base32 secret key. */
302
+ base32_secret_key: string;
303
+ /** Optional device display name. */
304
+ name?: string;
305
+ /** Optional account label. */
306
+ username?: string;
307
+ /** Optional issuer/app label. */
308
+ issuer?: string;
309
+ /** Number of digits in generated OTP codes. */
310
+ digits?: 6 | 8;
311
+ /** OTP validity period in seconds. */
312
+ period?: number;
313
+ /** TOTP hash algorithm. */
314
+ algorithm?: TotpAlgorithm;
315
+ /** Optional future ISO expiration timestamp. */
316
+ expires_at?: string;
317
+ }
318
+
319
+ export interface CreateOtpAuthUrlTotpDeviceParams {
320
+ /** otpauth://totp URL with a secret query parameter. */
321
+ otp_auth_url: string;
322
+ /** Optional device display name override. */
323
+ name?: string;
324
+ /** Optional account label fallback. */
325
+ username?: string;
326
+ /** Optional issuer/app label fallback. */
327
+ issuer?: string;
328
+ /** Number of digits in generated OTP codes, used only if missing from the URL. */
329
+ digits?: 6 | 8;
330
+ /** OTP validity period in seconds, used only if missing from the URL. */
331
+ period?: number;
332
+ /** TOTP hash algorithm, used only if missing from the URL. */
333
+ algorithm?: TotpAlgorithm;
334
+ /** Optional future ISO expiration timestamp. */
335
+ expires_at?: string;
336
+ }
337
+
338
+ export interface TotpOtpResponse {
339
+ /** Generated one-time password code. */
340
+ code: string;
341
+ /** ISO timestamp when the code expires. */
342
+ expires: string;
343
+ }
344
+
345
+ export interface TotpOtpParams {
346
+ /**
347
+ * Minimum number of seconds the generated code must remain valid.
348
+ *
349
+ * When the current TOTP code expires sooner than this value, the API waits
350
+ * for the next code before responding.
351
+ */
352
+ min_seconds_until_expire?: number;
353
+ }
354
+
222
355
  declare global {
223
356
  namespace Cypress {
224
357
  interface Chainable {
@@ -292,6 +425,144 @@ declare global {
292
425
  */
293
426
  options?: Partial<Cypress.RequestOptions>,
294
427
  ): Cypress.Chainable<ListSmsNumbersResponse>;
428
+
429
+ mailiskDeviceList(
430
+ /**
431
+ * List filters and pagination options.
432
+ */
433
+ params?: TotpDeviceListParams,
434
+ /**
435
+ * Request options.
436
+ *
437
+ * See https://docs.cypress.io/api/commands/request#Arguments
438
+ */
439
+ options?: Partial<Cypress.RequestOptions>,
440
+ ): Cypress.Chainable<TotpDeviceListResponse>;
441
+
442
+ mailiskDeviceCreate(
443
+ /**
444
+ * Saved device input using default TOTP settings.
445
+ */
446
+ input: CreateTotpDeviceParams,
447
+ /**
448
+ * Request options.
449
+ *
450
+ * See https://docs.cypress.io/api/commands/request#Arguments
451
+ */
452
+ options?: Partial<Cypress.RequestOptions>,
453
+ ): Cypress.Chainable<TotpDevice>;
454
+
455
+ mailiskDeviceCreateCustom(
456
+ /**
457
+ * Saved device input using custom TOTP settings.
458
+ */
459
+ input: CreateCustomTotpDeviceParams,
460
+ /**
461
+ * Request options.
462
+ *
463
+ * See https://docs.cypress.io/api/commands/request#Arguments
464
+ */
465
+ options?: Partial<Cypress.RequestOptions>,
466
+ ): Cypress.Chainable<TotpDevice>;
467
+
468
+ mailiskDeviceCreateFromBase32SecretKey(
469
+ /**
470
+ * Saved device input using a Base32 secret key.
471
+ */
472
+ input: CreateBase32SecretKeyTotpDeviceParams,
473
+ /**
474
+ * Request options.
475
+ *
476
+ * See https://docs.cypress.io/api/commands/request#Arguments
477
+ */
478
+ options?: Partial<Cypress.RequestOptions>,
479
+ ): Cypress.Chainable<TotpDevice>;
480
+
481
+ mailiskDeviceCreateFromOtpAuthUrl(
482
+ /**
483
+ * Saved device input using an otpauth URL.
484
+ */
485
+ input: CreateOtpAuthUrlTotpDeviceParams,
486
+ /**
487
+ * Request options.
488
+ *
489
+ * See https://docs.cypress.io/api/commands/request#Arguments
490
+ */
491
+ options?: Partial<Cypress.RequestOptions>,
492
+ ): Cypress.Chainable<TotpDevice>;
493
+
494
+ mailiskDeviceOtpByDeviceId(
495
+ /**
496
+ * Saved device ID.
497
+ */
498
+ deviceId: string,
499
+ /**
500
+ * Request options.
501
+ *
502
+ * See https://docs.cypress.io/api/commands/request#Arguments
503
+ */
504
+ options?: Partial<Cypress.RequestOptions>,
505
+ ): Cypress.Chainable<TotpOtpResponse>;
506
+
507
+ mailiskDeviceOtpByDeviceId(
508
+ /**
509
+ * Saved device ID.
510
+ */
511
+ deviceId: string,
512
+ /**
513
+ * OTP generation parameters.
514
+ */
515
+ params?: TotpOtpParams,
516
+ /**
517
+ * Request options.
518
+ *
519
+ * See https://docs.cypress.io/api/commands/request#Arguments
520
+ */
521
+ options?: Partial<Cypress.RequestOptions>,
522
+ ): Cypress.Chainable<TotpOtpResponse>;
523
+
524
+ mailiskDeviceOtpBySharedSecret(
525
+ /**
526
+ * Shared secret for one-off OTP generation.
527
+ */
528
+ sharedSecret: string,
529
+ /**
530
+ * Request options.
531
+ *
532
+ * See https://docs.cypress.io/api/commands/request#Arguments
533
+ */
534
+ options?: Partial<Cypress.RequestOptions>,
535
+ ): Cypress.Chainable<TotpOtpResponse>;
536
+
537
+ mailiskDeviceOtpBySharedSecret(
538
+ /**
539
+ * Shared secret for one-off OTP generation.
540
+ */
541
+ sharedSecret: string,
542
+ /**
543
+ * OTP generation parameters.
544
+ */
545
+ params?: TotpOtpParams,
546
+ /**
547
+ * Request options.
548
+ *
549
+ * See https://docs.cypress.io/api/commands/request#Arguments
550
+ */
551
+ options?: Partial<Cypress.RequestOptions>,
552
+ ): Cypress.Chainable<TotpOtpResponse>;
553
+
554
+ mailiskDeviceDelete(
555
+ /**
556
+ * Saved device ID.
557
+ */
558
+ deviceId: string,
559
+ /**
560
+ * Request options.
561
+ *
562
+ * See https://docs.cypress.io/api/commands/request#Arguments
563
+ */
564
+ options?: Partial<Cypress.RequestOptions>,
565
+ ): Cypress.Chainable<void>;
295
566
  }
296
567
  }
297
568
  }
@@ -10,6 +10,14 @@ class MailiskCommands {
10
10
  'mailiskDownloadAttachment',
11
11
  'mailiskSearchSms',
12
12
  'mailiskListSmsNumbers',
13
+ 'mailiskDeviceList',
14
+ 'mailiskDeviceCreate',
15
+ 'mailiskDeviceCreateCustom',
16
+ 'mailiskDeviceCreateFromBase32SecretKey',
17
+ 'mailiskDeviceCreateFromOtpAuthUrl',
18
+ 'mailiskDeviceOtpByDeviceId',
19
+ 'mailiskDeviceOtpBySharedSecret',
20
+ 'mailiskDeviceDelete',
13
21
  ];
14
22
  }
15
23
 
@@ -186,6 +194,98 @@ class MailiskCommands {
186
194
  mailiskListSmsNumbers(options = {}) {
187
195
  return this._withRequest((request) => request.get(`api/sms/numbers`, options));
188
196
  }
197
+
198
+ _buildUrlParams(params = {}) {
199
+ const urlParams = new URLSearchParams();
200
+ for (const key in params) {
201
+ let value = params[key];
202
+ if (typeof value === 'string') {
203
+ value = value.trim();
204
+ }
205
+ if (value !== undefined && value !== null && value !== '') {
206
+ urlParams.set(key, value.toString());
207
+ }
208
+ }
209
+ return urlParams;
210
+ }
211
+
212
+ _requireNonEmptyString(value, label) {
213
+ if (typeof value !== 'string' || value.trim() === '') {
214
+ throw new Error(`${label} must be a non-empty string.`);
215
+ }
216
+ return value.trim();
217
+ }
218
+
219
+ _hasTotpOtpParam(value) {
220
+ return (
221
+ value &&
222
+ typeof value === 'object' &&
223
+ Object.prototype.hasOwnProperty.call(value, 'min_seconds_until_expire')
224
+ );
225
+ }
226
+
227
+ _getTotpOtpArgs(paramsOrOptions = {}, options = {}, hasRequestOptions = false) {
228
+ if (hasRequestOptions || this._hasTotpOtpParam(paramsOrOptions)) {
229
+ return [{ ...(paramsOrOptions || {}) }, options];
230
+ }
231
+ return [{}, paramsOrOptions];
232
+ }
233
+
234
+ _buildTotpOtpUrlParams(params = {}) {
235
+ return this._buildUrlParams(params);
236
+ }
237
+
238
+ _buildTotpOtpBody(params = {}) {
239
+ const body = {};
240
+ if (params.min_seconds_until_expire != null) {
241
+ body.min_seconds_until_expire = params.min_seconds_until_expire;
242
+ }
243
+ return body;
244
+ }
245
+
246
+ mailiskDeviceList(params = {}, options = {}) {
247
+ const urlParams = this._buildUrlParams(params);
248
+ const query = urlParams.toString();
249
+ const path = query ? `api/devices?${query}` : 'api/devices';
250
+ return this._withRequest((request) => request.get(path, options));
251
+ }
252
+
253
+ mailiskDeviceCreate(input, options = {}) {
254
+ return this._withRequest((request) => request.post('api/devices', input, options));
255
+ }
256
+
257
+ mailiskDeviceCreateCustom(input, options = {}) {
258
+ return this._withRequest((request) => request.post('api/devices/custom', input, options));
259
+ }
260
+
261
+ mailiskDeviceCreateFromBase32SecretKey(input, options = {}) {
262
+ return this._withRequest((request) => request.post('api/devices/base32-secret-key', input, options));
263
+ }
264
+
265
+ mailiskDeviceCreateFromOtpAuthUrl(input, options = {}) {
266
+ return this._withRequest((request) => request.post('api/devices/otpauth-url', input, options));
267
+ }
268
+
269
+ mailiskDeviceOtpByDeviceId(deviceId, paramsOrOptions = {}, options = {}) {
270
+ const [params, requestOptions] = this._getTotpOtpArgs(paramsOrOptions, options, arguments.length >= 3);
271
+ const encodedDeviceId = encodeURIComponent(this._requireNonEmptyString(deviceId, 'deviceId'));
272
+ const urlParams = this._buildTotpOtpUrlParams(params);
273
+ const query = urlParams.toString();
274
+ const path = query ? `api/devices/${encodedDeviceId}/otp?${query}` : `api/devices/${encodedDeviceId}/otp`;
275
+ return this._withRequest((request) => request.get(path, requestOptions));
276
+ }
277
+
278
+ mailiskDeviceOtpBySharedSecret(sharedSecret, paramsOrOptions = {}, options = {}) {
279
+ const [params, requestOptions] = this._getTotpOtpArgs(paramsOrOptions, options, arguments.length >= 3);
280
+ const normalizedSharedSecret = this._requireNonEmptyString(sharedSecret, 'sharedSecret');
281
+ const body = { shared_secret: normalizedSharedSecret, ...this._buildTotpOtpBody(params) };
282
+ return this._withRequest((request) => request.post('api/devices/otp', body, requestOptions));
283
+ }
284
+
285
+ mailiskDeviceDelete(deviceId, options = {}) {
286
+ const encodedDeviceId = encodeURIComponent(this._requireNonEmptyString(deviceId, 'deviceId'));
287
+ return this._withRequest((request) => request.del(`api/devices/${encodedDeviceId}`, options).then(() => undefined));
288
+ }
189
289
  }
190
290
 
191
291
  module.exports = MailiskCommands;
package/src/request.js CHANGED
@@ -6,6 +6,7 @@ class Request {
6
6
  this.apiKey = options.apiKey;
7
7
  this.headers = {
8
8
  Accept: 'application/json',
9
+ 'Content-Type': 'application/json',
9
10
  'X-Api-Key': `${this.apiKey}`,
10
11
  'User-Agent': `cypress-mailisk/${pkg.version}`,
11
12
  };
@@ -22,6 +23,7 @@ class Request {
22
23
  url: `${this.apiUrl}${path}`,
23
24
  headers: {
24
25
  Accept: this.headers.Accept,
26
+ 'Content-Type': this.headers['Content-Type'],
25
27
  'X-Api-Key': this.headers['X-Api-Key'],
26
28
  'User-Agent': this.headers['User-Agent'],
27
29
  },