@tuturuuu/utils 0.29.0 → 0.30.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/package.json +3 -3
  3. package/src/__tests__/ai-temp-auth.test.ts +32 -0
  4. package/src/__tests__/api-proxy-guard.test.ts +4 -5
  5. package/src/__tests__/app-url.test.ts +1 -0
  6. package/src/__tests__/required-mfa-api-session.test.ts +75 -0
  7. package/src/__tests__/required-mfa-browser.test.ts +73 -0
  8. package/src/__tests__/required-mfa-lineage.test.ts +40 -0
  9. package/src/__tests__/required-mfa-proxy.test.ts +105 -0
  10. package/src/__tests__/required-mfa-request.test.ts +342 -0
  11. package/src/__tests__/required-mfa-runtime.test.ts +109 -0
  12. package/src/__tests__/required-mfa-transfer.test.ts +29 -0
  13. package/src/abuse-protection/__tests__/reputation.test.ts +107 -0
  14. package/src/abuse-protection/reputation.ts +42 -3
  15. package/src/ai-temp-auth.ts +11 -0
  16. package/src/api-proxy-guard.ts +6 -6
  17. package/src/app-coordination-token.ts +247 -0
  18. package/src/commercial-comparison.ts +7 -0
  19. package/src/internal-domains.ts +3 -0
  20. package/src/launchable-apps.ts +12 -0
  21. package/src/onboarding.ts +6 -0
  22. package/src/platform-release.test.ts +1 -1
  23. package/src/platform-release.ts +1 -1
  24. package/src/portless.ts +1 -0
  25. package/src/required-mfa-api-session.ts +30 -0
  26. package/src/required-mfa-app-session.ts +34 -0
  27. package/src/required-mfa-browser.ts +42 -0
  28. package/src/required-mfa-credentials.ts +58 -0
  29. package/src/required-mfa-lineage.ts +22 -0
  30. package/src/required-mfa-policy.ts +188 -0
  31. package/src/required-mfa-proxy.ts +87 -0
  32. package/src/required-mfa-request.ts +147 -0
  33. package/src/required-mfa-runtime.ts +103 -0
  34. package/src/required-mfa-supabase-session.ts +33 -0
  35. package/src/required-mfa-transfer.ts +41 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.30.0](https://github.com/tutur3u/platform/compare/utils-v0.29.1...utils-v0.30.0) (2026-09-25)
4
+
5
+
6
+ ### Features
7
+
8
+ * **auth:** enforce required account MFA lifecycle ([e08f27d](https://github.com/tutur3u/platform/commit/e08f27dbc082f7d9c951bb0792357638f3dcfb0a)) ([#5448](https://github.com/tutur3u/platform/issues/5448)) ([d7a5ed4](https://github.com/tutur3u/platform/commit/d7a5ed453963e619bd1dc3bd8c8a18c5e027317c))
9
+ * **parley:** add private multiplayer training and shared AI Hub billing ([#5505](https://github.com/tutur3u/platform/issues/5505)) ([038e05c](https://github.com/tutur3u/platform/commit/038e05c69a0e4a080f62d6441ae4dd2b15721dba))
10
+ * **parley:** share Meet runtime and integrate private training with AI Hub billing ([66d40e2](https://github.com/tutur3u/platform/commit/66d40e23a8110bd43be652e1181205cf50e506ab))
11
+
12
+ ## [0.29.1](https://github.com/tutur3u/platform/compare/utils-v0.29.0...utils-v0.29.1) (2026-09-23)
13
+
14
+
15
+ ### Bug Fixes
16
+
17
+ * **auth:** reuse completed session-bound challenges ([b63d510](https://github.com/tutur3u/platform/commit/b63d51046b427350d959137cf37058223e544788))
18
+ * **mobile:** repair Live persistence and reuse session verification ([#5436](https://github.com/tutur3u/platform/issues/5436)) ([cb452e1](https://github.com/tutur3u/platform/commit/cb452e1908eab3db2dce9dfd1d57d721284a2e49))
19
+
3
20
  ## [0.29.0](https://github.com/tutur3u/platform/compare/utils-v0.28.0...utils-v0.29.0) (2026-09-20)
4
21
 
5
22
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.29.0",
4
+ "version": "0.30.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -24,7 +24,7 @@
24
24
  "@tiptap/react": "3.31.3",
25
25
  "@tuturuuu/google": "0.1.1",
26
26
  "@tuturuuu/icons": "0.1.1",
27
- "@tuturuuu/internal-api": "0.39.0",
27
+ "@tuturuuu/internal-api": "0.41.0",
28
28
  "@tuturuuu/supabase": "0.5.2",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
30
  "@upstash/redis": "^1.38.4",
@@ -45,7 +45,7 @@
45
45
  "zod": "^4.6.2"
46
46
  },
47
47
  "devDependencies": {
48
- "@tuturuuu/types": "0.31.0",
48
+ "@tuturuuu/types": "0.32.0",
49
49
  "@tuturuuu/typescript-config": "0.1.2",
50
50
  "@types/diff": "^8.0.0",
51
51
  "@types/node": "^26.5.1",
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
2
 
3
3
  const mocks = vi.hoisted(() => ({
4
4
  getRedis: vi.fn(),
5
+ assurance: vi.fn(),
5
6
  redis: {
6
7
  decr: vi.fn(),
7
8
  del: vi.fn(),
@@ -13,6 +14,10 @@ const mocks = vi.hoisted(() => ({
13
14
  },
14
15
  }));
15
16
 
17
+ vi.mock('../required-mfa-app-session', () => ({
18
+ isRequiredMfaProofAllowed: (...args: unknown[]) => mocks.assurance(...args),
19
+ }));
20
+
16
21
  vi.mock('../upstash-rest.js', () => ({
17
22
  getUpstashRestRedisClient: () => mocks.getRedis(),
18
23
  }));
@@ -20,6 +25,7 @@ vi.mock('../upstash-rest.js', () => ({
20
25
  describe('ai temp auth', () => {
21
26
  beforeEach(() => {
22
27
  vi.clearAllMocks();
28
+ mocks.assurance.mockResolvedValue(true);
23
29
  mocks.getRedis.mockResolvedValue(mocks.redis);
24
30
  mocks.redis.get.mockResolvedValue('0');
25
31
  mocks.redis.set.mockResolvedValue('OK');
@@ -29,6 +35,31 @@ describe('ai temp auth', () => {
29
35
  mocks.redis.del.mockResolvedValue(1);
30
36
  });
31
37
 
38
+ it('uses normal session auth instead of cached tokens for required accounts', async () => {
39
+ const { mintAiTempAuthToken } = await import('../ai-temp-auth.js');
40
+ mocks.assurance.mockResolvedValue(false);
41
+ expect(await mintAiTempAuthToken({ user: { id: 'required' } })).toBeNull();
42
+ expect(mocks.redis.set).not.toHaveBeenCalled();
43
+ });
44
+
45
+ it('invalidates an existing cached identity immediately after policy changes', async () => {
46
+ const { mintAiTempAuthToken, validateAiTempAuthRequest } = await import(
47
+ '../ai-temp-auth.js'
48
+ );
49
+ const minted = await mintAiTempAuthToken({ user: { id: 'actor' } });
50
+ const payload = mocks.redis.set.mock.calls[0]![1];
51
+ mocks.redis.get.mockImplementation(async (key: string) =>
52
+ key.startsWith('ai:temp-auth:token:') ? payload : '0'
53
+ );
54
+ mocks.assurance.mockResolvedValue(false);
55
+ const request = { headers: new Headers() };
56
+ const { AI_TEMP_AUTH_HEADER } = await import('../ai-temp-auth.js');
57
+ request.headers.set(AI_TEMP_AUTH_HEADER, minted!.token);
58
+ expect(await validateAiTempAuthRequest(request)).toEqual({
59
+ status: 'revoked',
60
+ });
61
+ });
62
+
32
63
  it('mints a 60-second token and stores only its digest in Redis', async () => {
33
64
  const { mintAiTempAuthToken } = await import('../ai-temp-auth.js');
34
65
 
@@ -159,6 +190,7 @@ describe('ai temp auth', () => {
159
190
  describe('ai credit snapshots', () => {
160
191
  beforeEach(() => {
161
192
  vi.clearAllMocks();
193
+ mocks.assurance.mockResolvedValue(true);
162
194
  mocks.getRedis.mockResolvedValue(mocks.redis);
163
195
  mocks.redis.get.mockResolvedValue(null);
164
196
  mocks.redis.set.mockResolvedValue('OK');
@@ -1,3 +1,7 @@
1
+ vi.mock('../required-mfa-runtime', () => ({
2
+ enforceRequiredMfaRequest: async () => null,
3
+ }));
4
+
1
5
  import { NextRequest } from 'next/server';
2
6
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
7
 
@@ -134,7 +138,6 @@ describe('guardApiProxyRequest', () => {
134
138
  }),
135
139
  { prefixBase: 'proxy:test:api' }
136
140
  );
137
-
138
141
  expect(response?.status).toBe(413);
139
142
  expect(mocks.extractIp).not.toHaveBeenCalled();
140
143
  });
@@ -147,13 +150,11 @@ describe('guardApiProxyRequest', () => {
147
150
  { 'content-type': 'application/json' },
148
151
  JSON.stringify({ value: 'x'.repeat(1024 * 1024) })
149
152
  );
150
-
151
153
  expect(request.headers.get('content-length')).toBeNull();
152
154
 
153
155
  const response = await guardApiProxyRequest(request, {
154
156
  prefixBase: 'proxy:test:api',
155
157
  });
156
-
157
158
  expect(response?.status).toBe(413);
158
159
  expect(mocks.extractIp).not.toHaveBeenCalled();
159
160
  expect(mocks.validateEmoji).not.toHaveBeenCalled();
@@ -172,7 +173,6 @@ describe('guardApiProxyRequest', () => {
172
173
  skipContentValidation: true,
173
174
  }
174
175
  );
175
-
176
176
  expect(response).toBeNull();
177
177
  expect(mocks.validateEmoji).not.toHaveBeenCalled();
178
178
 
@@ -207,7 +207,6 @@ describe('guardApiProxyRequest', () => {
207
207
  prefixBase: 'proxy:test:api',
208
208
  }
209
209
  );
210
-
211
210
  expect(response?.status).toBe(429);
212
211
  expect(response?.headers.get('X-Proxy-Block-Reason')).toBe(
213
212
  'ip-already-blocked'
@@ -287,6 +287,7 @@ describe('Portless app origin registry', () => {
287
287
  '*.tuturuuu.localhost',
288
288
  ]);
289
289
  expect(Object.values(TUTURUUU_PORTLESS_APP_ORIGINS)).toEqual([
290
+ 'https://parley.tuturuuu.localhost',
290
291
  'https://ai.tuturuuu.localhost',
291
292
  'https://apps.tuturuuu.localhost',
292
293
  'https://calendar.tuturuuu.localhost',
@@ -0,0 +1,75 @@
1
+ // @vitest-environment node
2
+ import { NextRequest, NextResponse } from 'next/server';
3
+ import { beforeEach, expect, it, vi } from 'vitest';
4
+ import { guardBrowserApiSession } from '../required-mfa-api-session';
5
+
6
+ const update = vi.hoisted(() => vi.fn());
7
+ vi.mock('@tuturuuu/supabase/next/proxy', () => ({ updateSession: update }));
8
+ beforeEach(() => {
9
+ update.mockReset();
10
+ });
11
+ it('checks refreshed cookies and propagates rotation on a policy rejection', async () => {
12
+ const request = new NextRequest('https://example.com/api/v1/users/me', {
13
+ headers: { cookie: 'sb-project-auth-token=old' },
14
+ });
15
+ update.mockImplementation(async (req: NextRequest) => {
16
+ req.cookies.set('sb-project-auth-token', 'new');
17
+ const res = NextResponse.next();
18
+ res.cookies.set('sb-project-auth-token', 'new');
19
+ return { res, claims: { sub: 'actor' } };
20
+ });
21
+ const response = await guardBrowserApiSession(request, async () => {
22
+ expect(request.cookies.get('sb-project-auth-token')?.value).toBe('new');
23
+ return NextResponse.json({ code: 'MFA_REQUIRED' }, { status: 403 });
24
+ });
25
+ expect(response.status).toBe(403);
26
+ expect(response.headers.get('set-cookie')).toContain(
27
+ 'sb-project-auth-token=new'
28
+ );
29
+ });
30
+ it.each([
31
+ '/api/auth/refresh-app-session',
32
+ '/api/v1/auth/cross-app-session/refresh',
33
+ ])('does not recursively refresh the handoff handler %s', async (path) => {
34
+ const request = new NextRequest(`https://example.com${path}`, {
35
+ headers: { cookie: 'sb-project-auth-token=old' },
36
+ });
37
+ expect((await guardBrowserApiSession(request, async () => null)).status).toBe(
38
+ 200
39
+ );
40
+ expect(update).not.toHaveBeenCalled();
41
+ });
42
+ it('never replaces explicit bearer credentials with ambient browser cookies', async () => {
43
+ const request = new NextRequest('https://example.com/api/v1/users/me', {
44
+ headers: {
45
+ authorization: 'Bearer explicit',
46
+ cookie: 'sb-project-auth-token=old',
47
+ },
48
+ });
49
+ await guardBrowserApiSession(request, async () => null);
50
+ expect(update).not.toHaveBeenCalled();
51
+ });
52
+
53
+ it('removes superseded app credentials from the actual forwarded provider request', async () => {
54
+ const request = new NextRequest('https://example.com/api/v1/users/me', {
55
+ headers: {
56
+ cookie:
57
+ 'sb-project-auth-token=provider; tuturuuu_app_session=stale-app; tuturuuu_web_app_session=other-user',
58
+ },
59
+ });
60
+ update.mockResolvedValue({
61
+ res: NextResponse.next(),
62
+ claims: { sub: 'provider-user' },
63
+ });
64
+ const response = await guardBrowserApiSession(request, async () => {
65
+ expect(request.cookies.has('tuturuuu_app_session')).toBe(false);
66
+ expect(request.cookies.has('tuturuuu_web_app_session')).toBe(false);
67
+ return null;
68
+ });
69
+ expect(response.headers.get('x-middleware-request-cookie')).toContain(
70
+ 'sb-project-auth-token=provider'
71
+ );
72
+ expect(response.headers.get('x-middleware-request-cookie')).not.toContain(
73
+ 'tuturuuu_'
74
+ );
75
+ });
@@ -0,0 +1,73 @@
1
+ import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
2
+ import { beforeEach, expect, it, vi } from 'vitest';
3
+ import { requiresAccountMfa } from '../required-mfa-browser';
4
+
5
+ const rpc = vi.fn();
6
+ const assurance = vi.fn();
7
+ const client = {
8
+ auth: {
9
+ getUser: async () => ({
10
+ data: {
11
+ user: {
12
+ id: 'actor',
13
+ app_metadata: {
14
+ tuturuuu_required_mfa: { required: true, verifiedAfter: 100 },
15
+ },
16
+ },
17
+ },
18
+ error: null,
19
+ }),
20
+ getClaims: async () => ({
21
+ data: { claims: { sub: 'actor', aal: 'aal1', session_id: 'session' } },
22
+ error: null,
23
+ }),
24
+ mfa: { getAuthenticatorAssuranceLevel: assurance },
25
+ },
26
+ rpc,
27
+ } as unknown as TypedSupabaseClient;
28
+ beforeEach(() => {
29
+ vi.clearAllMocks();
30
+ });
31
+ it('keeps a mobile-approved browser on its completed sign-in flow', async () => {
32
+ rpc.mockResolvedValue({ data: true, error: null });
33
+ expect(await requiresAccountMfa(client, { acceptMobileApproval: true })).toBe(
34
+ false
35
+ );
36
+ expect(rpc).toHaveBeenCalledWith('account_required_mfa_satisfied');
37
+ expect(assurance).not.toHaveBeenCalled();
38
+ });
39
+ it.each([
40
+ { data: false, error: null },
41
+ { data: null, error: { code: '42501' } },
42
+ ])(
43
+ 'requires a new challenge after mobile proof expiry or reset',
44
+ async (result) => {
45
+ rpc.mockResolvedValue(result);
46
+ expect(
47
+ await requiresAccountMfa(client, { acceptMobileApproval: true })
48
+ ).toBe(true);
49
+ }
50
+ );
51
+ it('does not let a mobile proof approve another device challenge', async () => {
52
+ expect(await requiresAccountMfa(client)).toBe(true);
53
+ expect(rpc).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it('a cached AAL2 claim still requires current database factor lineage', async () => {
57
+ const claims = vi.spyOn(client.auth, 'getClaims').mockResolvedValue({
58
+ data: {
59
+ claims: {
60
+ sub: 'actor',
61
+ aal: 'aal2',
62
+ session_id: 'session',
63
+ amr: [{ method: 'totp', timestamp: 101 }],
64
+ },
65
+ },
66
+ error: null,
67
+ } as never);
68
+ rpc.mockResolvedValue({ data: false, error: null });
69
+ expect(await requiresAccountMfa(client)).toBe(true);
70
+ rpc.mockResolvedValue({ data: true, error: null });
71
+ expect(await requiresAccountMfa(client)).toBe(false);
72
+ claims.mockRestore();
73
+ });
@@ -0,0 +1,40 @@
1
+ import type { TypedSupabaseClient } from '@tuturuuu/supabase/types';
2
+ import { expect, it, vi } from 'vitest';
3
+ import { bindCurrentMfaFactor } from '../required-mfa-lineage';
4
+
5
+ const proof = {
6
+ sessionId: 'session',
7
+ verifiedAt: 104,
8
+ primaryVerifiedAt: 103,
9
+ factorId: 'removed-factor',
10
+ };
11
+ it('rejects late verification whose original factor was deleted', async () => {
12
+ const rpc = vi.fn().mockResolvedValue({ data: null, error: null });
13
+ expect(
14
+ await bindCurrentMfaFactor(
15
+ { rpc } as unknown as TypedSupabaseClient,
16
+ 'actor',
17
+ proof
18
+ )
19
+ ).toBeNull();
20
+ });
21
+ it('a newly enrolled factor cannot revive an old signed proof', async () => {
22
+ const rpc = vi.fn().mockResolvedValue({ data: 'new-factor', error: null });
23
+ expect(
24
+ await bindCurrentMfaFactor(
25
+ { rpc } as unknown as TypedSupabaseClient,
26
+ 'actor',
27
+ proof
28
+ )
29
+ ).toBeNull();
30
+ });
31
+ it('binds a provider-verified session once, then retains its exact factor id', async () => {
32
+ const rpc = vi.fn().mockResolvedValue({ data: 'new-factor', error: null });
33
+ const admin = { rpc } as unknown as TypedSupabaseClient;
34
+ const raw = { sessionId: 'session', verifiedAt: 104, primaryVerifiedAt: 103 };
35
+ expect(await bindCurrentMfaFactor(admin, 'actor', raw)).toBeNull();
36
+ expect(await bindCurrentMfaFactor(admin, 'actor', raw, true)).toEqual({
37
+ ...raw,
38
+ factorId: 'new-factor',
39
+ });
40
+ });
@@ -0,0 +1,105 @@
1
+ // @vitest-environment node
2
+ import { NextRequest } from 'next/server';
3
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import type { AccountAssuranceDependencies } from '../required-mfa-request';
5
+
6
+ const check = vi.hoisted(() => vi.fn());
7
+ vi.mock('../required-mfa-request', () => ({ checkRequiredAccountMfa: check }));
8
+
9
+ import {
10
+ guardRequiredAccountMfa,
11
+ isAccountAssuranceRecoveryPath,
12
+ } from '../required-mfa-proxy';
13
+
14
+ const dependencies = {} as AccountAssuranceDependencies;
15
+ function request(
16
+ path = '/api/v1/workspaces',
17
+ headers: HeadersInit = { authorization: 'Bearer eyJtest' }
18
+ ) {
19
+ return new NextRequest(`https://tuturuuu.com${path}`, { headers });
20
+ }
21
+
22
+ beforeEach(() => {
23
+ check.mockReset();
24
+ check.mockResolvedValue({ status: 'allowed', userId: 'actor' });
25
+ });
26
+
27
+ describe('required MFA proxy responses', () => {
28
+ it('leaves anonymous requests to route authentication', async () => {
29
+ expect(
30
+ await guardRequiredAccountMfa(
31
+ request('/api/v1/public', {}),
32
+ dependencies,
33
+ { userId: 'actor' }
34
+ )
35
+ ).toBeNull();
36
+ expect(check).not.toHaveBeenCalled();
37
+ });
38
+ it.each([
39
+ ['authorization', 'Bearer eyJtest'],
40
+ ['authorization', 'Bearer ttr_app_test'],
41
+ ['cookie', 'sb-project-auth-token.0=token'],
42
+ ['cookie', 'tuturuuu_web_app_session=token'],
43
+ ['cookie', 'tuturuuu_app_session=token'],
44
+ ])('checks interactive credential %s', async (name, value) => {
45
+ expect(
46
+ await guardRequiredAccountMfa(
47
+ request(undefined, { [name]: value }),
48
+ dependencies,
49
+ { userId: 'actor' }
50
+ )
51
+ ).toBeNull();
52
+ expect(check).toHaveBeenCalledOnce();
53
+ });
54
+ it.each([
55
+ ['required', 403, 'MFA_REQUIRED'],
56
+ ['invalid', 401, 'UNAUTHENTICATED'],
57
+ ['unavailable', 503, 'AUTH_UNAVAILABLE'],
58
+ ])(
59
+ 'returns a bounded no-store response for %s',
60
+ async (status, httpStatus, code) => {
61
+ check.mockResolvedValue({ status, userId: 'private-actor-id' });
62
+ const result = await guardRequiredAccountMfa(request(), dependencies, {
63
+ userId: 'actor',
64
+ });
65
+ expect(result?.status).toBe(httpStatus);
66
+ expect(result?.headers.get('Cache-Control')).toBe('no-store');
67
+ const body = await result!.json();
68
+ expect(body.code).toBe(code);
69
+ expect(JSON.stringify(body)).not.toContain('private-actor-id');
70
+ }
71
+ );
72
+ it.each([
73
+ '/api/auth/verify-app-token',
74
+ '/api/auth/refresh-app-session',
75
+ '/api/v1/auth/cross-app-token/verify',
76
+ '/api/v1/auth/cross-app-session/refresh',
77
+ '/api/cli/auth/refresh',
78
+ '/api/v1/auth/mfa/devices',
79
+ '/api/v1/auth/mfa/mobile/challenges',
80
+ '/api/v1/auth/mfa/mobile/approvals',
81
+ '/api/v1/auth/mfa/mobile/challenges/00000000-0000-4000-8000-000000000001',
82
+ '/api/v1/auth/mfa/mobile/challenges/00000000-0000-4000-8000-000000000001/approve',
83
+ '/api/v1/auth/accounts/logout',
84
+ '/api/v1/auth/accounts/logout-all',
85
+ ])('allows assurance establishment/recovery route %s', async (path) => {
86
+ expect(
87
+ await guardRequiredAccountMfa(request(path), dependencies, {
88
+ userId: 'actor',
89
+ })
90
+ ).toBeNull();
91
+ expect(check).not.toHaveBeenCalled();
92
+ });
93
+ it.each([
94
+ '/api/auth/refresh-app-session/anything',
95
+ '/api/v1/auth/cross-app-session/refresh/anything',
96
+ '/api/v1/auth/mfa/devices/anything',
97
+ '/api/v1/auth/mfa/mobile/challenges/invalid',
98
+ '/api/v1/auth/mfa/mobile/challenges/00000000-0000-4000-8000-000000000001/approve/anything',
99
+ '/api/v1/infrastructure/internal-accounts',
100
+ '/api/v1/users/me',
101
+ '/api/v1/workspaces',
102
+ ])('does not exempt protected or unknown route %s', (path) => {
103
+ expect(isAccountAssuranceRecoveryPath(path)).toBe(false);
104
+ });
105
+ });