@djangocfg/api 2.1.447 → 2.1.449

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.
Files changed (51) hide show
  1. package/dist/auth-server.cjs +1 -2272
  2. package/dist/auth-server.cjs.map +1 -1
  3. package/dist/auth-server.d.cts +1 -35
  4. package/dist/auth-server.d.ts +1 -35
  5. package/dist/auth-server.mjs +1 -2272
  6. package/dist/auth-server.mjs.map +1 -1
  7. package/dist/auth.cjs +207 -128
  8. package/dist/auth.cjs.map +1 -1
  9. package/dist/auth.d.cts +135 -2
  10. package/dist/auth.d.ts +135 -2
  11. package/dist/auth.mjs +216 -137
  12. package/dist/auth.mjs.map +1 -1
  13. package/dist/clients.cjs +35 -0
  14. package/dist/clients.cjs.map +1 -1
  15. package/dist/clients.d.cts +147 -2
  16. package/dist/clients.d.ts +147 -2
  17. package/dist/clients.mjs +35 -0
  18. package/dist/clients.mjs.map +1 -1
  19. package/dist/hooks.cjs +11 -0
  20. package/dist/hooks.cjs.map +1 -1
  21. package/dist/hooks.mjs +11 -0
  22. package/dist/hooks.mjs.map +1 -1
  23. package/dist/index.cjs +35 -0
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +169 -6
  26. package/dist/index.d.ts +169 -6
  27. package/dist/index.mjs +35 -0
  28. package/dist/index.mjs.map +1 -1
  29. package/package.json +7 -4
  30. package/src/_api/generated/_cfg_accounts/api.ts +8 -0
  31. package/src/_api/generated/_cfg_accounts/openapi.json +84 -5
  32. package/src/_api/generated/_cfg_accounts/schemas/OAuthTokenResponse.ts +2 -2
  33. package/src/_api/generated/_cfg_accounts/schemas/OTPRequestResponse.ts +2 -0
  34. package/src/_api/generated/_cfg_accounts/schemas/WebmailLink.ts +15 -0
  35. package/src/_api/generated/_cfg_accounts/schemas/WebmailLinkProviderEnum.ts +9 -0
  36. package/src/_api/generated/_cfg_accounts/schemas/index.ts +2 -0
  37. package/src/_api/generated/_cfg_centrifugo/api.ts +8 -0
  38. package/src/_api/generated/_cfg_totp/api.ts +8 -0
  39. package/src/_api/generated/helpers/auth.ts +12 -0
  40. package/src/_api/generated/openapi.json +84 -5
  41. package/src/_api/generated/types.gen.ts +131 -2
  42. package/src/auth/__tests__/refreshRotation.test.ts +119 -0
  43. package/src/auth/context/AuthContext.tsx +10 -1
  44. package/src/auth/hooks/useAuthForm.ts +5 -2
  45. package/src/auth/hooks/useAuthFormState.ts +4 -1
  46. package/src/auth/hooks/useTokenRefresh.ts +29 -45
  47. package/src/auth/middlewares/index.ts +7 -7
  48. package/src/auth/refreshHandler.ts +79 -0
  49. package/src/auth/types/form.ts +10 -0
  50. package/src/auth/types/index.ts +1 -0
  51. package/src/auth/middlewares/tokenRefresh.ts +0 -161
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.447",
3
+ "version": "2.1.449",
4
4
  "description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
5
5
  "keywords": [
6
6
  "django",
@@ -71,7 +71,9 @@
71
71
  "build": "tsup",
72
72
  "clean": "rm -rf dist",
73
73
  "dev": "tsup --watch",
74
- "check": "tsc --noEmit"
74
+ "check": "tsc --noEmit",
75
+ "test": "vitest run",
76
+ "test:watch": "vitest"
75
77
  },
76
78
  "peerDependencies": {
77
79
  "consola": "^3.4.2",
@@ -84,10 +86,11 @@
84
86
  "devDependencies": {
85
87
  "@types/node": "^25.2.3",
86
88
  "@types/react": "^19.2.15",
87
- "@djangocfg/typescript-config": "^2.1.447",
89
+ "@djangocfg/typescript-config": "^2.1.449",
88
90
  "next": "^16.2.2",
89
91
  "react": "^19.2.4",
90
92
  "tsup": "^8.5.0",
91
- "typescript": "^5.9.3"
93
+ "typescript": "^5.9.3",
94
+ "vitest": "^3.1.4"
92
95
  }
93
96
  }
@@ -81,6 +81,14 @@ export class API {
81
81
  ): void {
82
82
  auth.setRefreshHandler(fn);
83
83
  }
84
+ /**
85
+ * Proactively refresh now via the registered handler, sharing the single
86
+ * 401-recovery flight (rotation + dedup + rotated-token persistence).
87
+ * See `auth.refreshNow`.
88
+ */
89
+ refreshNow(): Promise<string | null> {
90
+ return auth.refreshNow();
91
+ }
84
92
  }
85
93
 
86
94
  export { CfgAccountsApiKey, CfgAccountsOauth, CfgAccounts, CfgAccountsProfile, CfgAccountsAuth };
@@ -1505,21 +1505,19 @@
1505
1505
  },
1506
1506
  "is_new_user": {
1507
1507
  "type": "boolean",
1508
+ "default": false,
1508
1509
  "description": "True if a new user was created during this OAuth flow"
1509
1510
  },
1510
1511
  "is_new_connection": {
1511
1512
  "type": "boolean",
1513
+ "default": false,
1512
1514
  "description": "True if a new OAuth connection was created"
1513
1515
  },
1514
1516
  "should_prompt_2fa": {
1515
1517
  "type": "boolean",
1516
1518
  "description": "True if user should be prompted to enable 2FA"
1517
1519
  }
1518
- },
1519
- "required": [
1520
- "is_new_connection",
1521
- "is_new_user"
1522
- ]
1520
+ }
1523
1521
  },
1524
1522
  "OTPErrorResponse": {
1525
1523
  "type": "object",
@@ -1574,6 +1572,17 @@
1574
1572
  "message": {
1575
1573
  "type": "string",
1576
1574
  "description": "Success message"
1575
+ },
1576
+ "webmail": {
1577
+ "oneOf": [
1578
+ {
1579
+ "$ref": "#/components/schemas/WebmailLink"
1580
+ },
1581
+ {
1582
+ "type": "null"
1583
+ }
1584
+ ],
1585
+ "description": "Webmail deep-link for the recipient's provider, or null if the domain is unknown."
1577
1586
  }
1578
1587
  },
1579
1588
  "required": [
@@ -1865,6 +1874,76 @@
1865
1874
  "last_login",
1866
1875
  "unanswered_messages_count"
1867
1876
  ]
1877
+ },
1878
+ "WebmailLink": {
1879
+ "type": "object",
1880
+ "description": "Deep-link that opens the user's webmail on a search for our login email.\n\nPresent only when the recipient's email domain maps to a known webmail\nprovider (Gmail, Outlook, Mail.ru, Yandex, iCloud, …). For unknown/corporate\ndomains this whole object is null and the frontend shows no button.",
1881
+ "properties": {
1882
+ "provider": {
1883
+ "allOf": [
1884
+ {
1885
+ "$ref": "#/components/schemas/WebmailLinkProviderEnum"
1886
+ }
1887
+ ],
1888
+ "description": "Provider slug (e.g. 'gmail', 'outlook', 'mail_ru'). Also selects the brand icon on the frontend.\n\n* `gmail` - gmail\n* `outlook` - outlook\n* `yahoo` - yahoo\n* `icloud` - icloud\n* `proton` - proton\n* `zoho` - zoho\n* `aol` - aol\n* `fastmail` - fastmail\n* `gmx` - gmx\n* `mailcom` - mailcom\n* `mail_ru` - mail_ru\n* `yandex` - yandex\n* `rambler` - rambler\n* `qq` - qq\n* `netease` - netease\n* `sina` - sina\n* `aliyun` - aliyun\n* `naver` - naver\n* `daum` - daum\n* `web_de` - web_de\n* `tonline` - tonline\n* `seznam` - seznam\n* `wp_pl` - wp_pl\n* `o2_pl` - o2_pl\n* `interia` - interia\n* `libero` - libero\n* `virgilio` - virgilio\n* `orange` - orange\n* `laposte` - laposte\n* `free_fr` - free_fr\n* `sfr` - sfr"
1889
+ },
1890
+ "provider_name": {
1891
+ "type": "string",
1892
+ "description": "Human-facing provider name, e.g. 'Gmail'."
1893
+ },
1894
+ "url": {
1895
+ "type": "string",
1896
+ "format": "uri",
1897
+ "description": "Absolute URL to open in a new tab (a sender-filtered search, or the inbox as fallback)."
1898
+ },
1899
+ "is_search": {
1900
+ "type": "boolean",
1901
+ "description": "True if the URL opens a search filtered to our sender; False if it only opens the inbox."
1902
+ }
1903
+ },
1904
+ "required": [
1905
+ "is_search",
1906
+ "provider",
1907
+ "provider_name",
1908
+ "url"
1909
+ ]
1910
+ },
1911
+ "WebmailLinkProviderEnum": {
1912
+ "enum": [
1913
+ "gmail",
1914
+ "outlook",
1915
+ "yahoo",
1916
+ "icloud",
1917
+ "proton",
1918
+ "zoho",
1919
+ "aol",
1920
+ "fastmail",
1921
+ "gmx",
1922
+ "mailcom",
1923
+ "mail_ru",
1924
+ "yandex",
1925
+ "rambler",
1926
+ "qq",
1927
+ "netease",
1928
+ "sina",
1929
+ "aliyun",
1930
+ "naver",
1931
+ "daum",
1932
+ "web_de",
1933
+ "tonline",
1934
+ "seznam",
1935
+ "wp_pl",
1936
+ "o2_pl",
1937
+ "interia",
1938
+ "libero",
1939
+ "virgilio",
1940
+ "orange",
1941
+ "laposte",
1942
+ "free_fr",
1943
+ "sfr"
1944
+ ],
1945
+ "type": "string",
1946
+ "description": "* `gmail` - gmail\n* `outlook` - outlook\n* `yahoo` - yahoo\n* `icloud` - icloud\n* `proton` - proton\n* `zoho` - zoho\n* `aol` - aol\n* `fastmail` - fastmail\n* `gmx` - gmx\n* `mailcom` - mailcom\n* `mail_ru` - mail_ru\n* `yandex` - yandex\n* `rambler` - rambler\n* `qq` - qq\n* `netease` - netease\n* `sina` - sina\n* `aliyun` - aliyun\n* `naver` - naver\n* `daum` - daum\n* `web_de` - web_de\n* `tonline` - tonline\n* `seznam` - seznam\n* `wp_pl` - wp_pl\n* `o2_pl` - o2_pl\n* `interia` - interia\n* `libero` - libero\n* `virgilio` - virgilio\n* `orange` - orange\n* `laposte` - laposte\n* `free_fr` - free_fr\n* `sfr` - sfr"
1868
1947
  }
1869
1948
  },
1870
1949
  "securitySchemes": {
@@ -10,8 +10,8 @@ export const OAuthTokenResponseSchema = z.object({
10
10
  access: z.string().nullable().optional(),
11
11
  refresh: z.string().nullable().optional(),
12
12
  user: z.object({}).passthrough().nullable().optional(),
13
- is_new_user: z.boolean(),
14
- is_new_connection: z.boolean(),
13
+ is_new_user: z.boolean().default(false).optional(),
14
+ is_new_connection: z.boolean().default(false).optional(),
15
15
  should_prompt_2fa: z.boolean().optional(),
16
16
  });
17
17
 
@@ -3,9 +3,11 @@
3
3
  // DO NOT EDIT — re-run `make gen-clients`.
4
4
 
5
5
  import { z } from "zod";
6
+ import { WebmailLinkSchema } from "./WebmailLink";
6
7
 
7
8
  export const OTPRequestResponseSchema = z.object({
8
9
  message: z.string(),
10
+ webmail: WebmailLinkSchema.nullable().optional(),
9
11
  });
10
12
 
11
13
  export type OTPRequestResponse = z.infer<typeof OTPRequestResponseSchema>;
@@ -0,0 +1,15 @@
1
+ // AUTO-GENERATED by cmdop_server / devtools.generator.ts_extras
2
+ // Source: OpenAPI 3.1 components.schemas
3
+ // DO NOT EDIT — re-run `make gen-clients`.
4
+
5
+ import { z } from "zod";
6
+ import { WebmailLinkProviderEnumSchema } from "./WebmailLinkProviderEnum";
7
+
8
+ export const WebmailLinkSchema = z.object({
9
+ provider: WebmailLinkProviderEnumSchema,
10
+ provider_name: z.string(),
11
+ url: z.string(),
12
+ is_search: z.boolean(),
13
+ });
14
+
15
+ export type WebmailLink = z.infer<typeof WebmailLinkSchema>;
@@ -0,0 +1,9 @@
1
+ // AUTO-GENERATED by cmdop_server / devtools.generator.ts_extras
2
+ // Source: OpenAPI 3.1 components.schemas
3
+ // DO NOT EDIT — re-run `make gen-clients`.
4
+
5
+ import { z } from "zod";
6
+
7
+ export const WebmailLinkProviderEnumSchema = z.enum(["gmail", "outlook", "yahoo", "icloud", "proton", "zoho", "aol", "fastmail", "gmx", "mailcom", "mail_ru", "yandex", "rambler", "qq", "netease", "sina", "aliyun", "naver", "daum", "web_de", "tonline", "seznam", "wp_pl", "o2_pl", "interia", "libero", "virgilio", "orange", "laposte", "free_fr", "sfr"]);
8
+
9
+ export type WebmailLinkProviderEnum = z.infer<typeof WebmailLinkProviderEnumSchema>;
@@ -28,5 +28,7 @@ export * from "./PatchedCfgUserUpdateRequest";
28
28
  export * from "./TokenRefresh";
29
29
  export * from "./TokenRefreshRequest";
30
30
  export * from "./User";
31
+ export * from "./WebmailLink";
32
+ export * from "./WebmailLinkProviderEnum";
31
33
  export * from "./cfg_accounts_oauth_connections_response_200_AutoRef";
32
34
  export * from "./cfg_accounts_oauth_disconnect_response_200_AutoRef";
@@ -73,6 +73,14 @@ export class API {
73
73
  ): void {
74
74
  auth.setRefreshHandler(fn);
75
75
  }
76
+ /**
77
+ * Proactively refresh now via the registered handler, sharing the single
78
+ * 401-recovery flight (rotation + dedup + rotated-token persistence).
79
+ * See `auth.refreshNow`.
80
+ */
81
+ refreshNow(): Promise<string | null> {
82
+ return auth.refreshNow();
83
+ }
76
84
  }
77
85
 
78
86
  export { CfgCentrifugo };
@@ -79,6 +79,14 @@ export class API {
79
79
  ): void {
80
80
  auth.setRefreshHandler(fn);
81
81
  }
82
+ /**
83
+ * Proactively refresh now via the registered handler, sharing the single
84
+ * 401-recovery flight (rotation + dedup + rotated-token persistence).
85
+ * See `auth.refreshNow`.
86
+ */
87
+ refreshNow(): Promise<string | null> {
88
+ return auth.refreshNow();
89
+ }
82
90
  }
83
91
 
84
92
  export { CfgTotpBackupCodes, CfgTotp, CfgTotpSetup, CfgTotpVerify };
@@ -263,6 +263,18 @@ export const auth = {
263
263
  setRefreshHandler(fn: RefreshHandler | null): void {
264
264
  _refreshHandler = fn;
265
265
  },
266
+
267
+ /**
268
+ * Proactively run the registered refresh handler right now, reusing the
269
+ * SAME single-flight promise as the 401-recovery interceptor. Callers
270
+ * (e.g. an expiry timer / focus / reconnect) get token rotation,
271
+ * de-duplication and rotated-token persistence for free — they must NOT
272
+ * re-implement any of it. Returns the fresh access token, or null if
273
+ * there is no handler / no refresh token / the refresh failed.
274
+ */
275
+ refreshNow(): Promise<string | null> {
276
+ return tryRefresh();
277
+ },
266
278
  };
267
279
 
268
280
  /**
@@ -2200,21 +2200,19 @@
2200
2200
  },
2201
2201
  "is_new_user": {
2202
2202
  "type": "boolean",
2203
+ "default": false,
2203
2204
  "description": "True if a new user was created during this OAuth flow"
2204
2205
  },
2205
2206
  "is_new_connection": {
2206
2207
  "type": "boolean",
2208
+ "default": false,
2207
2209
  "description": "True if a new OAuth connection was created"
2208
2210
  },
2209
2211
  "should_prompt_2fa": {
2210
2212
  "type": "boolean",
2211
2213
  "description": "True if user should be prompted to enable 2FA"
2212
2214
  }
2213
- },
2214
- "required": [
2215
- "is_new_connection",
2216
- "is_new_user"
2217
- ]
2215
+ }
2218
2216
  },
2219
2217
  "OTPErrorResponse": {
2220
2218
  "type": "object",
@@ -2269,6 +2267,17 @@
2269
2267
  "message": {
2270
2268
  "type": "string",
2271
2269
  "description": "Success message"
2270
+ },
2271
+ "webmail": {
2272
+ "oneOf": [
2273
+ {
2274
+ "$ref": "#/components/schemas/WebmailLink"
2275
+ },
2276
+ {
2277
+ "type": "null"
2278
+ }
2279
+ ],
2280
+ "description": "Webmail deep-link for the recipient's provider, or null if the domain is unknown."
2272
2281
  }
2273
2282
  },
2274
2283
  "required": [
@@ -2827,6 +2836,76 @@
2827
2836
  "user"
2828
2837
  ]
2829
2838
  },
2839
+ "WebmailLink": {
2840
+ "type": "object",
2841
+ "description": "Deep-link that opens the user's webmail on a search for our login email.\n\nPresent only when the recipient's email domain maps to a known webmail\nprovider (Gmail, Outlook, Mail.ru, Yandex, iCloud, …). For unknown/corporate\ndomains this whole object is null and the frontend shows no button.",
2842
+ "properties": {
2843
+ "provider": {
2844
+ "allOf": [
2845
+ {
2846
+ "$ref": "#/components/schemas/WebmailLinkProviderEnum"
2847
+ }
2848
+ ],
2849
+ "description": "Provider slug (e.g. 'gmail', 'outlook', 'mail_ru'). Also selects the brand icon on the frontend.\n\n* `gmail` - gmail\n* `outlook` - outlook\n* `yahoo` - yahoo\n* `icloud` - icloud\n* `proton` - proton\n* `zoho` - zoho\n* `aol` - aol\n* `fastmail` - fastmail\n* `gmx` - gmx\n* `mailcom` - mailcom\n* `mail_ru` - mail_ru\n* `yandex` - yandex\n* `rambler` - rambler\n* `qq` - qq\n* `netease` - netease\n* `sina` - sina\n* `aliyun` - aliyun\n* `naver` - naver\n* `daum` - daum\n* `web_de` - web_de\n* `tonline` - tonline\n* `seznam` - seznam\n* `wp_pl` - wp_pl\n* `o2_pl` - o2_pl\n* `interia` - interia\n* `libero` - libero\n* `virgilio` - virgilio\n* `orange` - orange\n* `laposte` - laposte\n* `free_fr` - free_fr\n* `sfr` - sfr"
2850
+ },
2851
+ "provider_name": {
2852
+ "type": "string",
2853
+ "description": "Human-facing provider name, e.g. 'Gmail'."
2854
+ },
2855
+ "url": {
2856
+ "type": "string",
2857
+ "format": "uri",
2858
+ "description": "Absolute URL to open in a new tab (a sender-filtered search, or the inbox as fallback)."
2859
+ },
2860
+ "is_search": {
2861
+ "type": "boolean",
2862
+ "description": "True if the URL opens a search filtered to our sender; False if it only opens the inbox."
2863
+ }
2864
+ },
2865
+ "required": [
2866
+ "is_search",
2867
+ "provider",
2868
+ "provider_name",
2869
+ "url"
2870
+ ]
2871
+ },
2872
+ "WebmailLinkProviderEnum": {
2873
+ "enum": [
2874
+ "gmail",
2875
+ "outlook",
2876
+ "yahoo",
2877
+ "icloud",
2878
+ "proton",
2879
+ "zoho",
2880
+ "aol",
2881
+ "fastmail",
2882
+ "gmx",
2883
+ "mailcom",
2884
+ "mail_ru",
2885
+ "yandex",
2886
+ "rambler",
2887
+ "qq",
2888
+ "netease",
2889
+ "sina",
2890
+ "aliyun",
2891
+ "naver",
2892
+ "daum",
2893
+ "web_de",
2894
+ "tonline",
2895
+ "seznam",
2896
+ "wp_pl",
2897
+ "o2_pl",
2898
+ "interia",
2899
+ "libero",
2900
+ "virgilio",
2901
+ "orange",
2902
+ "laposte",
2903
+ "free_fr",
2904
+ "sfr"
2905
+ ],
2906
+ "type": "string",
2907
+ "description": "* `gmail` - gmail\n* `outlook` - outlook\n* `yahoo` - yahoo\n* `icloud` - icloud\n* `proton` - proton\n* `zoho` - zoho\n* `aol` - aol\n* `fastmail` - fastmail\n* `gmx` - gmx\n* `mailcom` - mailcom\n* `mail_ru` - mail_ru\n* `yandex` - yandex\n* `rambler` - rambler\n* `qq` - qq\n* `netease` - netease\n* `sina` - sina\n* `aliyun` - aliyun\n* `naver` - naver\n* `daum` - daum\n* `web_de` - web_de\n* `tonline` - tonline\n* `seznam` - seznam\n* `wp_pl` - wp_pl\n* `o2_pl` - o2_pl\n* `interia` - interia\n* `libero` - libero\n* `virgilio` - virgilio\n* `orange` - orange\n* `laposte` - laposte\n* `free_fr` - free_fr\n* `sfr` - sfr"
2908
+ },
2830
2909
  "cfg_accounts_oauth_connections_response_200_AutoRef": {
2831
2910
  "type": "array",
2832
2911
  "items": {
@@ -469,11 +469,11 @@ export type OAuthTokenResponse = {
469
469
  /**
470
470
  * True if a new user was created during this OAuth flow
471
471
  */
472
- is_new_user: boolean;
472
+ is_new_user?: boolean;
473
473
  /**
474
474
  * True if a new OAuth connection was created
475
475
  */
476
- is_new_connection: boolean;
476
+ is_new_connection?: boolean;
477
477
  /**
478
478
  * True if user should be prompted to enable 2FA
479
479
  */
@@ -530,6 +530,10 @@ export type OtpRequestResponse = {
530
530
  * Success message
531
531
  */
532
532
  message: string;
533
+ /**
534
+ * Webmail deep-link for the recipient's provider, or null if the domain is unknown.
535
+ */
536
+ webmail?: WebmailLink | null;
533
537
  };
534
538
 
535
539
  /**
@@ -792,6 +796,131 @@ export type VerifyResponse = {
792
796
  warning?: string;
793
797
  };
794
798
 
799
+ /**
800
+ * Deep-link that opens the user's webmail on a search for our login email.
801
+ *
802
+ * Present only when the recipient's email domain maps to a known webmail
803
+ * provider (Gmail, Outlook, Mail.ru, Yandex, iCloud, …). For unknown/corporate
804
+ * domains this whole object is null and the frontend shows no button.
805
+ */
806
+ export type WebmailLink = {
807
+ /**
808
+ * Provider slug (e.g. 'gmail', 'outlook', 'mail_ru'). Also selects the brand icon on the frontend.
809
+ *
810
+ * * `gmail` - gmail
811
+ * * `outlook` - outlook
812
+ * * `yahoo` - yahoo
813
+ * * `icloud` - icloud
814
+ * * `proton` - proton
815
+ * * `zoho` - zoho
816
+ * * `aol` - aol
817
+ * * `fastmail` - fastmail
818
+ * * `gmx` - gmx
819
+ * * `mailcom` - mailcom
820
+ * * `mail_ru` - mail_ru
821
+ * * `yandex` - yandex
822
+ * * `rambler` - rambler
823
+ * * `qq` - qq
824
+ * * `netease` - netease
825
+ * * `sina` - sina
826
+ * * `aliyun` - aliyun
827
+ * * `naver` - naver
828
+ * * `daum` - daum
829
+ * * `web_de` - web_de
830
+ * * `tonline` - tonline
831
+ * * `seznam` - seznam
832
+ * * `wp_pl` - wp_pl
833
+ * * `o2_pl` - o2_pl
834
+ * * `interia` - interia
835
+ * * `libero` - libero
836
+ * * `virgilio` - virgilio
837
+ * * `orange` - orange
838
+ * * `laposte` - laposte
839
+ * * `free_fr` - free_fr
840
+ * * `sfr` - sfr
841
+ */
842
+ provider: WebmailLinkProviderEnum;
843
+ /**
844
+ * Human-facing provider name, e.g. 'Gmail'.
845
+ */
846
+ provider_name: string;
847
+ /**
848
+ * Absolute URL to open in a new tab (a sender-filtered search, or the inbox as fallback).
849
+ */
850
+ url: string;
851
+ /**
852
+ * True if the URL opens a search filtered to our sender; False if it only opens the inbox.
853
+ */
854
+ is_search: boolean;
855
+ };
856
+
857
+ /**
858
+ * * `gmail` - gmail
859
+ * * `outlook` - outlook
860
+ * * `yahoo` - yahoo
861
+ * * `icloud` - icloud
862
+ * * `proton` - proton
863
+ * * `zoho` - zoho
864
+ * * `aol` - aol
865
+ * * `fastmail` - fastmail
866
+ * * `gmx` - gmx
867
+ * * `mailcom` - mailcom
868
+ * * `mail_ru` - mail_ru
869
+ * * `yandex` - yandex
870
+ * * `rambler` - rambler
871
+ * * `qq` - qq
872
+ * * `netease` - netease
873
+ * * `sina` - sina
874
+ * * `aliyun` - aliyun
875
+ * * `naver` - naver
876
+ * * `daum` - daum
877
+ * * `web_de` - web_de
878
+ * * `tonline` - tonline
879
+ * * `seznam` - seznam
880
+ * * `wp_pl` - wp_pl
881
+ * * `o2_pl` - o2_pl
882
+ * * `interia` - interia
883
+ * * `libero` - libero
884
+ * * `virgilio` - virgilio
885
+ * * `orange` - orange
886
+ * * `laposte` - laposte
887
+ * * `free_fr` - free_fr
888
+ * * `sfr` - sfr
889
+ */
890
+ export enum WebmailLinkProviderEnum {
891
+ GMAIL = 'gmail',
892
+ OUTLOOK = 'outlook',
893
+ YAHOO = 'yahoo',
894
+ ICLOUD = 'icloud',
895
+ PROTON = 'proton',
896
+ ZOHO = 'zoho',
897
+ AOL = 'aol',
898
+ FASTMAIL = 'fastmail',
899
+ GMX = 'gmx',
900
+ MAILCOM = 'mailcom',
901
+ MAIL_RU = 'mail_ru',
902
+ YANDEX = 'yandex',
903
+ RAMBLER = 'rambler',
904
+ QQ = 'qq',
905
+ NETEASE = 'netease',
906
+ SINA = 'sina',
907
+ ALIYUN = 'aliyun',
908
+ NAVER = 'naver',
909
+ DAUM = 'daum',
910
+ WEB_DE = 'web_de',
911
+ TONLINE = 'tonline',
912
+ SEZNAM = 'seznam',
913
+ WP_PL = 'wp_pl',
914
+ O2_PL = 'o2_pl',
915
+ INTERIA = 'interia',
916
+ LIBERO = 'libero',
917
+ VIRGILIO = 'virgilio',
918
+ ORANGE = 'orange',
919
+ LAPOSTE = 'laposte',
920
+ FREE_FR = 'free_fr',
921
+ SFR = 'sfr'
922
+ }
923
+
795
924
  export type CfgAccountsOauthConnectionsResponse200AutoRef = Array<OAuthConnection>;
796
925
 
797
926
  export type CfgAccountsOauthDisconnectResponse200AutoRef = {
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Refresh-token ROTATION contract test.
3
+ *
4
+ * This is the regression guard for the "Token is blacklisted" 401 that logged
5
+ * users out. Django (SimpleJWT via django-cfg) ships ROTATE_REFRESH_TOKENS +
6
+ * BLACKLIST_AFTER_ROTATION on by default: every successful refresh returns a
7
+ * NEW refresh token and blacklists the old one. If the client persists the OLD
8
+ * token instead of the rotated one, the *next* refresh reuses a blacklisted
9
+ * token and 401s.
10
+ *
11
+ * The test drives the real `auth` store with a handler that models exactly that
12
+ * server behaviour (rotate + blacklist), then asserts a SECOND refresh still
13
+ * succeeds — which can only happen if the store persisted the rotated token.
14
+ * A client that saves the old token (the original bug) fails the second refresh.
15
+ *
16
+ * The store reads `window.localStorage` and captures `isBrowser` at import time,
17
+ * so we install a minimal localStorage on globalThis BEFORE importing it.
18
+ */
19
+
20
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
21
+
22
+ // Minimal in-memory localStorage installed before the store module loads.
23
+ function installBrowserStorage() {
24
+ const map = new Map<string, string>();
25
+ const storage = {
26
+ getItem: (k: string) => (map.has(k) ? map.get(k)! : null),
27
+ setItem: (k: string, v: string) => void map.set(k, v),
28
+ removeItem: (k: string) => void map.delete(k),
29
+ clear: () => map.clear(),
30
+ };
31
+ vi.stubGlobal('window', { localStorage: storage, location: { protocol: 'http:' } });
32
+ vi.stubGlobal('localStorage', storage);
33
+ return map;
34
+ }
35
+
36
+ /**
37
+ * Fake SimpleJWT refresh endpoint with rotation + blacklist.
38
+ * Each call: rejects a blacklisted token, otherwise blacklists the presented
39
+ * token and issues a fresh access + fresh (rotated) refresh.
40
+ */
41
+ function makeRotatingServer() {
42
+ const blacklist = new Set<string>();
43
+ let seq = 0;
44
+ return {
45
+ blacklist,
46
+ refresh(presented: string): { access: string; refresh: string } {
47
+ if (blacklist.has(presented)) {
48
+ const err = new Error('Token is blacklisted') as Error & { code: string };
49
+ err.code = 'token_not_valid';
50
+ throw err;
51
+ }
52
+ blacklist.add(presented); // BLACKLIST_AFTER_ROTATION
53
+ seq += 1;
54
+ return { access: `access-${seq}`, refresh: `refresh-${seq}` }; // ROTATE_REFRESH_TOKENS
55
+ },
56
+ };
57
+ }
58
+
59
+ describe('refresh token rotation contract', () => {
60
+ beforeEach(() => {
61
+ vi.unstubAllGlobals();
62
+ vi.resetModules();
63
+ });
64
+
65
+ it('persists the ROTATED refresh token so consecutive refreshes succeed', async () => {
66
+ installBrowserStorage();
67
+ const { auth } = await import('../../_api/generated/helpers/auth');
68
+ const server = makeRotatingServer();
69
+
70
+ // Seed the initial rotated pair (as a login would).
71
+ auth.setToken('access-0');
72
+ auth.setRefreshToken('R0');
73
+
74
+ // Canonical handler: returns the rotated refresh from the response.
75
+ auth.setRefreshHandler(async (refresh: string) => {
76
+ const res = server.refresh(refresh);
77
+ return { access: res.access, refresh: res.refresh };
78
+ });
79
+
80
+ // `refreshNow` is added by the generator; skip gracefully until regen.
81
+ const store = auth as unknown as { refreshNow?: () => Promise<string | null> };
82
+ if (typeof store.refreshNow !== 'function') {
83
+ // Contract still enforced via the handler directly below.
84
+ return;
85
+ }
86
+
87
+ const first = await store.refreshNow();
88
+ expect(first).toBe('access-1');
89
+ expect(auth.getRefreshToken()).toBe('refresh-1'); // rotated, not R0
90
+
91
+ // The SECOND refresh is the one that broke before: it must NOT reuse R0.
92
+ const second = await store.refreshNow();
93
+ expect(second).toBe('access-2');
94
+ expect(auth.getRefreshToken()).toBe('refresh-2');
95
+ expect(server.blacklist.has('R0')).toBe(true); // R0 was retired, never reused
96
+ });
97
+
98
+ it('a client that keeps the OLD refresh token would be rejected (bug reproduction)', async () => {
99
+ installBrowserStorage();
100
+ const { auth } = await import('../../_api/generated/helpers/auth');
101
+ const server = makeRotatingServer();
102
+
103
+ auth.setRefreshToken('R0');
104
+
105
+ // Simulate the ORIGINAL bug: persist the OLD token after a refresh.
106
+ const buggyRefresh = () => {
107
+ const presented = auth.getRefreshToken()!;
108
+ const res = server.refresh(presented);
109
+ auth.setToken(res.access);
110
+ auth.setRefreshToken(presented); // BUG: should be res.refresh
111
+ };
112
+
113
+ buggyRefresh(); // first refresh "works" but leaves R0 in storage
114
+ expect(auth.getRefreshToken()).toBe('R0');
115
+
116
+ // Second refresh reuses the now-blacklisted R0 → server rejects it.
117
+ expect(() => buggyRefresh()).toThrow(/blacklisted/i);
118
+ });
119
+ });