@tuturuuu/utils 0.33.3 → 0.35.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 (54) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/package.json +6 -4
  3. package/src/__tests__/api-proxy-bearer-trust.test.ts +3 -0
  4. package/src/__tests__/api-proxy-guard.test.ts +6 -13
  5. package/src/__tests__/api-proxy-offline-response.test.ts +48 -0
  6. package/src/__tests__/api-proxy-test-request.ts +19 -0
  7. package/src/__tests__/calendar-provider-readonly.test.ts +45 -0
  8. package/src/__tests__/calendar-recurrence-edit.test.ts +140 -0
  9. package/src/__tests__/calendar-recurrence.test.ts +479 -0
  10. package/src/__tests__/daily-operation-download-isolation.test.ts +137 -0
  11. package/src/__tests__/offline-download-guard.test.ts +340 -0
  12. package/src/__tests__/permissions.test.tsx +21 -0
  13. package/src/__tests__/request-workspace.test.ts +63 -0
  14. package/src/__tests__/tutoring-teacher-hours.test.ts +274 -0
  15. package/src/__tests__/username-policy.test.ts +70 -0
  16. package/src/account-benefits-server.ts +43 -0
  17. package/src/account-benefits.ts +45 -0
  18. package/src/api-proxy-guard.ts +8 -42
  19. package/src/api-proxy-read-policy.ts +43 -0
  20. package/src/calendar-provider-readonly.ts +49 -0
  21. package/src/calendar-recurrence-edit.ts +148 -0
  22. package/src/calendar-recurrence-expansion.ts +131 -0
  23. package/src/calendar-recurrence-local.ts +45 -0
  24. package/src/calendar-recurrence-pattern.ts +54 -0
  25. package/src/calendar-recurrence-providers.ts +361 -0
  26. package/src/calendar-recurrence-validation.ts +180 -0
  27. package/src/calendar-recurrence.ts +5 -0
  28. package/src/calendar-utils.ts +41 -3
  29. package/src/common/metadata.test.ts +0 -6
  30. package/src/constants.ts +3 -0
  31. package/src/deployment-vault-permissions.tsx +26 -0
  32. package/src/desktop-signing-profiles.ts +20 -0
  33. package/src/google-calendar-import-fence.test.ts +182 -0
  34. package/src/google-calendar-import-fence.ts +166 -0
  35. package/src/next-config.ts +8 -1
  36. package/src/offline-download-guard.ts +160 -0
  37. package/src/permissions.tsx +2 -10
  38. package/src/platform-release.test.ts +1 -1
  39. package/src/platform-release.ts +1 -1
  40. package/src/playground-schema.test.ts +119 -0
  41. package/src/playground-schema.ts +173 -0
  42. package/src/playground-templates.ts +65 -0
  43. package/src/release-note-policy-core.cjs +28 -0
  44. package/src/release-note-policy-core.d.cts +5 -0
  45. package/src/release-note-policy.json +9 -0
  46. package/src/release-note-policy.ts +4 -0
  47. package/src/request-workspace.ts +29 -0
  48. package/src/seo-policy.json +196 -0
  49. package/src/seo.test.ts +168 -0
  50. package/src/seo.ts +49 -0
  51. package/src/tutoring-teacher-hours.ts +220 -0
  52. package/src/username-policy.json +87 -0
  53. package/src/username-policy.ts +18 -0
  54. package/vitest.config.ts +7 -0
@@ -0,0 +1,119 @@
1
+ import { expect, it } from 'vitest';
2
+ import {
3
+ PlaygroundDelta,
4
+ PlaygroundFiles,
5
+ PlaygroundJob,
6
+ PlaygroundRunnerExport,
7
+ } from './playground-schema';
8
+
9
+ it.each([
10
+ '../secret',
11
+ 'a/../../secret',
12
+ '/absolute',
13
+ 'node_modules/pkg.js',
14
+ 'a/.git/config',
15
+ 'a//b',
16
+ 'a\\b',
17
+ ])('rejects unsafe path %s', (path) =>
18
+ expect(PlaygroundFiles.safeParse([{ path, content: 'x' }]).success).toBe(
19
+ false
20
+ )
21
+ );
22
+ it('enforces UTF-8 byte limits rather than only character counts', () =>
23
+ expect(
24
+ PlaygroundFiles.safeParse([{ path: 'a', content: 'ấ'.repeat(100000) }])
25
+ .success
26
+ ).toBe(false));
27
+ it('rejects inconsistent delta inventories', () => {
28
+ expect(
29
+ PlaygroundDelta.safeParse({
30
+ revision: 1,
31
+ command: 'run',
32
+ paths: ['a', 'a/b'],
33
+ files: [],
34
+ }).success
35
+ ).toBe(false);
36
+ expect(
37
+ PlaygroundDelta.safeParse({
38
+ revision: 1,
39
+ command: 'run',
40
+ paths: ['a'],
41
+ files: [{ path: 'b', content: 'x' }],
42
+ }).success
43
+ ).toBe(false);
44
+ });
45
+ it('bounds preview requests to approved ports and paths', () => {
46
+ const job = {
47
+ projectId: '00000000-0000-4000-8000-000000000001',
48
+ revision: 0,
49
+ language: 'python',
50
+ operation: 'preview',
51
+ port: 22,
52
+ path: '/',
53
+ };
54
+ expect(PlaygroundJob.safeParse(job).success).toBe(false);
55
+ expect(
56
+ PlaygroundJob.safeParse({ ...job, port: 3000, path: '/\r\nHost:metadata' })
57
+ .success
58
+ ).toBe(false);
59
+ });
60
+
61
+ it('rejects file/directory collisions with intervening sorted siblings', () => {
62
+ const paths = ['a', 'a-b', 'a/b'];
63
+ expect(
64
+ PlaygroundFiles.safeParse(paths.map((path) => ({ path, content: 'x' })))
65
+ .success
66
+ ).toBe(false);
67
+ expect(
68
+ PlaygroundDelta.safeParse({ revision: 1, command: 'run', paths, files: [] })
69
+ .success
70
+ ).toBe(false);
71
+ });
72
+
73
+ it('rejects conflicting runner inventory ancestors', () => {
74
+ expect(
75
+ PlaygroundRunnerExport.safeParse({
76
+ revision: 1,
77
+ paths: ['a', 'a-b', 'a/b'],
78
+ files: [],
79
+ baseline: {},
80
+ }).success
81
+ ).toBe(false);
82
+ });
83
+
84
+ it.each([
85
+ '/../secret',
86
+ '/a/./b',
87
+ '/a//b',
88
+ '//other-host',
89
+ '/%2e%2e/secret',
90
+ '/a%2f../secret',
91
+ '/a\\b',
92
+ '/%',
93
+ ])('rejects unsafe preview path %s', (path) => {
94
+ expect(
95
+ PlaygroundJob.safeParse({
96
+ projectId: '00000000-0000-4000-8000-000000000001',
97
+ revision: 0,
98
+ language: 'python',
99
+ operation: 'preview',
100
+ port: 3000,
101
+ path,
102
+ }).success
103
+ ).toBe(false);
104
+ });
105
+ it.each(['/', '/index.html', '/assets/app.js?version=1'])(
106
+ 'accepts ordinary preview path %s',
107
+ (path) => {
108
+ expect(
109
+ PlaygroundJob.safeParse({
110
+ projectId: '00000000-0000-4000-8000-000000000001',
111
+ revision: 0,
112
+ language: 'python',
113
+ operation: 'preview',
114
+ port: 3000,
115
+ path,
116
+ }).success
117
+ ).toBe(true);
118
+ }
119
+ );
@@ -0,0 +1,173 @@
1
+ import { PLAYGROUND_LANGUAGES } from '@tuturuuu/types/primitives/playgrounds';
2
+ import { z } from 'zod';
3
+
4
+ function hasDirectoryConflict(paths: string[]) {
5
+ const inventory = new Set(paths);
6
+ return paths.some((path) => {
7
+ const pieces = path.split('/');
8
+ return pieces
9
+ .slice(0, -1)
10
+ .some((_, index) => inventory.has(pieces.slice(0, index + 1).join('/')));
11
+ });
12
+ }
13
+
14
+ function validPreviewPath(path: string) {
15
+ if (!path.startsWith('/') || /[\r\n\0\\]/.test(path)) return false;
16
+ try {
17
+ const pathname = decodeURIComponent(path.split(/[?#]/, 1)[0]!);
18
+ return (
19
+ pathname === '/' ||
20
+ (!/[\r\n\0\\]/.test(pathname) &&
21
+ pathname
22
+ .slice(1)
23
+ .split('/')
24
+ .every((part) => part !== '' && part !== '.' && part !== '..'))
25
+ );
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ export const PLAYGROUND_BYTES = 2 * 1024 * 1024;
32
+ export const PlaygroundPath = z
33
+ .string()
34
+ .min(1)
35
+ .max(240)
36
+ .refine(
37
+ (path) =>
38
+ path
39
+ .split('/')
40
+ .every(
41
+ (part) =>
42
+ /^[a-zA-Z0-9_.@+-]+$/.test(part) && part !== '.' && part !== '..'
43
+ ) &&
44
+ !path
45
+ .split('/')
46
+ .some((part) =>
47
+ ['node_modules', '.git', '__pycache__', 'target', '.venv'].includes(
48
+ part
49
+ )
50
+ )
51
+ );
52
+ export const PlaygroundFiles = z
53
+ .array(
54
+ z
55
+ .object({
56
+ path: PlaygroundPath,
57
+ content: z
58
+ .string()
59
+ .max(256 * 1024)
60
+ .refine(
61
+ (value) =>
62
+ !value.includes('\0') &&
63
+ new TextEncoder().encode(value).byteLength <= 256 * 1024
64
+ ),
65
+ })
66
+ .strict()
67
+ )
68
+ .max(128)
69
+ .superRefine((files, ctx) => {
70
+ if (new Set(files.map((file) => file.path)).size !== files.length)
71
+ ctx.addIssue({ code: 'custom', message: 'Duplicate file paths' });
72
+ if (
73
+ files.reduce(
74
+ (size, file) => size + new TextEncoder().encode(file.content).length,
75
+ 0
76
+ ) > PLAYGROUND_BYTES
77
+ )
78
+ ctx.addIssue({
79
+ code: 'custom',
80
+ message: 'Project exceeds storage limit',
81
+ });
82
+ const paths = files.map((file) => file.path).sort();
83
+ if (hasDirectoryConflict(paths))
84
+ ctx.addIssue({
85
+ code: 'custom',
86
+ message: 'File conflicts with directory',
87
+ });
88
+ });
89
+ export const PlaygroundCreate = z
90
+ .object({
91
+ name: z.string().trim().min(1).max(80),
92
+ language: z.enum(PLAYGROUND_LANGUAGES),
93
+ })
94
+ .strict();
95
+ export const PlaygroundSave = z
96
+ .object({
97
+ revision: z.number().int().nonnegative(),
98
+ files: PlaygroundFiles,
99
+ command: z
100
+ .string()
101
+ .min(1)
102
+ .max(4096)
103
+ .refine((value) => !value.includes('\0')),
104
+ })
105
+ .strict();
106
+ export const PlaygroundJob = z
107
+ .object({
108
+ projectId: z.guid(),
109
+ revision: z.number().int().nonnegative(),
110
+ language: z.enum(PLAYGROUND_LANGUAGES),
111
+ operation: z.enum(['run', 'stop', 'preview']),
112
+ files: PlaygroundFiles.optional(),
113
+ command: z.string().max(4096).optional(),
114
+ stdin: z.string().max(65536).optional(),
115
+ port: z.number().int().min(1024).max(65535).optional(),
116
+ path: z.string().max(2048).optional(),
117
+ })
118
+ .strict()
119
+ .superRefine((job, ctx) => {
120
+ if (
121
+ job.operation === 'run' &&
122
+ (!job.files || !job.command || job.command.includes('\0'))
123
+ )
124
+ ctx.addIssue({ code: 'custom', message: 'Missing run payload' });
125
+ if (
126
+ job.operation === 'preview' &&
127
+ (!job.port || !job.path || !validPreviewPath(job.path))
128
+ )
129
+ ctx.addIssue({ code: 'custom', message: 'Invalid preview request' });
130
+ });
131
+ export type PlaygroundJobPayload = z.infer<typeof PlaygroundJob>;
132
+
133
+ /** Full path inventory plus changed text; unchanged file bytes never cross the callback. */
134
+ export const PlaygroundDelta = PlaygroundSave.extend({
135
+ paths: z.array(PlaygroundPath).max(128),
136
+ }).superRefine((value, ctx) => {
137
+ const paths = [...value.paths].sort();
138
+ if (
139
+ new Set(paths).size !== paths.length ||
140
+ hasDirectoryConflict(paths) ||
141
+ value.files.some((file) => !paths.includes(file.path))
142
+ )
143
+ ctx.addIssue({ code: 'custom', message: 'Invalid delta inventory' });
144
+ });
145
+
146
+ /** Authenticated runner export merged into the room before Drive persistence. */
147
+ export const PlaygroundRunnerExport = z
148
+ .object({
149
+ revision: z.number().int().nonnegative(),
150
+ files: PlaygroundFiles,
151
+ paths: z.array(PlaygroundPath).max(128),
152
+ baseline: z.record(PlaygroundPath, z.string().regex(/^[a-f0-9]{64}$/)),
153
+ })
154
+ .strict()
155
+ .refine(
156
+ (value) =>
157
+ new Set(value.paths).size === value.paths.length &&
158
+ !hasDirectoryConflict(value.paths) &&
159
+ value.files.every((file) => value.paths.includes(file.path)),
160
+ 'Invalid runner inventory'
161
+ );
162
+
163
+ export const PlaygroundPreviewCapability = z
164
+ .object({
165
+ aud: z.literal('tuturuuu.playground-preview'),
166
+ ownerId: z.guid(),
167
+ projectId: z.guid(),
168
+ port: z.number().int().min(1024).max(65535),
169
+ meetingId: z.guid().optional(),
170
+ roomId: z.guid().optional(),
171
+ exp: z.number().int().positive(),
172
+ })
173
+ .strict();
@@ -0,0 +1,65 @@
1
+ import type { PlaygroundLanguage } from '@tuturuuu/types/primitives/playgrounds';
2
+ export const playgroundTemplates: Record<
3
+ PlaygroundLanguage,
4
+ { path: string; content: string; command: string }
5
+ > = {
6
+ python: {
7
+ path: 'main.py',
8
+ content: 'print("Hello, playground!")\n',
9
+ command: 'python3 main.py',
10
+ },
11
+ javascript: {
12
+ path: 'main.js',
13
+ content: 'console.log("Hello, playground!");\n',
14
+ command: 'node main.js',
15
+ },
16
+ typescript: {
17
+ path: 'main.ts',
18
+ content: 'console.log("Hello, playground!");\n',
19
+ command: 'bun main.ts',
20
+ },
21
+ c: {
22
+ path: 'main.c',
23
+ content:
24
+ '#include <stdio.h>\nint main(void) { puts("Hello, playground!"); }\n',
25
+ command: 'gcc main.c -o /tmp/main && /tmp/main',
26
+ },
27
+ cpp: {
28
+ path: 'main.cpp',
29
+ content:
30
+ '#include <iostream>\nint main() { std::cout << "Hello, playground!\\n"; }\n',
31
+ command: 'g++ main.cpp -o /tmp/main && /tmp/main',
32
+ },
33
+ java: {
34
+ path: 'Main.java',
35
+ content:
36
+ 'class Main { public static void main(String[] args) { System.out.println("Hello, playground!"); } }\n',
37
+ command: 'javac Main.java && java Main',
38
+ },
39
+ rust: {
40
+ path: 'main.rs',
41
+ content: 'fn main() { println!("Hello, playground!"); }\n',
42
+ command: 'rustc main.rs -o /tmp/main && /tmp/main',
43
+ },
44
+ go: {
45
+ path: 'main.go',
46
+ content:
47
+ 'package main\nimport "fmt"\nfunc main() { fmt.Println("Hello, playground!") }\n',
48
+ command: 'go run main.go',
49
+ },
50
+ ruby: {
51
+ path: 'main.rb',
52
+ content: 'puts "Hello, playground!"\n',
53
+ command: 'ruby main.rb',
54
+ },
55
+ php: {
56
+ path: 'main.php',
57
+ content: '<?php echo "Hello, playground!\\n";\n',
58
+ command: 'php main.php',
59
+ },
60
+ shell: {
61
+ path: 'main.sh',
62
+ content: 'printf "Hello, playground!\\n"\n',
63
+ command: 'sh main.sh',
64
+ },
65
+ };
@@ -0,0 +1,28 @@
1
+ const { patterns } = require('./release-note-policy.json');
2
+ const matchers = patterns.map((pattern) => new RegExp(pattern, 'iu'));
3
+
4
+ /** Omit known Git bookkeeping, never arbitrary product text containing merge. */
5
+ function isReleaseBookkeeping(subject) {
6
+ const description = subject
7
+ .replace(/^\w+(?:\([^)]*\))?!?:\s*/u, '')
8
+ .replace(/^\*\*[^*]+:\*\*\s*/u, '')
9
+ .replace(/\s*\(#\d+\)$/u, '')
10
+ .replace(
11
+ /\s*\(\[[^\]]+\]\(https:\/\/github\.com\/tutur3u\/platform\/(?:commit|issues|pull)\/[^)]+\)\)/gu,
12
+ ''
13
+ )
14
+ .trim();
15
+ return matchers.some((pattern) => pattern.test(description));
16
+ }
17
+
18
+ function filterReleaseBookkeeping(markdown) {
19
+ return markdown
20
+ .split('\n')
21
+ .filter((line) => {
22
+ const bullet = /^\s*[-*]\s+(.+)$/u.exec(line);
23
+ return !bullet || !isReleaseBookkeeping(bullet[1]);
24
+ })
25
+ .join('\n');
26
+ }
27
+
28
+ module.exports = { isReleaseBookkeeping, filterReleaseBookkeeping };
@@ -0,0 +1,5 @@
1
+ declare const policy: {
2
+ isReleaseBookkeeping(subject: string): boolean;
3
+ filterReleaseBookkeeping(markdown: string): string;
4
+ };
5
+ export = policy;
@@ -0,0 +1,9 @@
1
+ {
2
+ "patterns": [
3
+ "^merge (?:remote-tracking )?(?:branch(?:es)?|tag|commit) ['\"][^'\"\\n]+['\"](?: (?:and ['\"][^'\"\\n]+['\"]|of \\S+|into (?:['\"][^'\"\\n]+['\"]|\\S+)))*$",
4
+ "^merge pull request #\\d+ from \\S+$",
5
+ "^merge (?:current |latest )?(?:main|production|origin/(?:main|production|release-please--branches--\\S+))(?: (?:into|to) (?:main|production))?$",
6
+ "^(?:sync|synchronize) (?:current |latest )?(?:main|production)(?: (?:to|into|with|from) (?:main|production))$",
7
+ "^address review and integrate current main$"
8
+ ]
9
+ }
@@ -0,0 +1,4 @@
1
+ import policy from './release-note-policy-core.cjs';
2
+
3
+ /** Filter known Git merge/sync metadata while retaining product merge features. */
4
+ export const isReleaseBookkeeping = policy.isReleaseBookkeeping;
@@ -0,0 +1,29 @@
1
+ import { cache } from 'react';
2
+ import { getWorkspace } from './workspace-helper';
3
+
4
+ // React cache is scoped to a server render/request, never a shared authorization
5
+ // cache. Primitive actor keys let sibling layouts/pages reuse the same read.
6
+ const readWorkspace = cache(
7
+ (
8
+ id: string,
9
+ useAdmin: boolean,
10
+ actorId: string | null,
11
+ email: string | null
12
+ ) =>
13
+ getWorkspace(id, {
14
+ useAdmin,
15
+ ...(actorId ? { user: { id: actorId, email } } : {}),
16
+ })
17
+ );
18
+
19
+ export function getRequestWorkspace(
20
+ id: string,
21
+ options: Parameters<typeof getWorkspace>[1] = {}
22
+ ) {
23
+ return readWorkspace(
24
+ id,
25
+ options.useAdmin ?? false,
26
+ options.user?.id ?? null,
27
+ options.user?.email ?? null
28
+ );
29
+ }
@@ -0,0 +1,196 @@
1
+ {
2
+ "ai": {
3
+ "origin": "https://ai.tuturuuu.com",
4
+ "localePrefix": "never",
5
+ "publicPaths": [],
6
+ "publicPatterns": []
7
+ },
8
+ "apps": {
9
+ "origin": "https://apps.tuturuuu.com",
10
+ "localePrefix": "never",
11
+ "publicPaths": [""],
12
+ "publicPatterns": []
13
+ },
14
+ "calendar": {
15
+ "origin": "https://calendar.tuturuuu.com",
16
+ "localePrefix": "never",
17
+ "publicPaths": [],
18
+ "publicPatterns": []
19
+ },
20
+ "chat": {
21
+ "origin": "https://chat.tuturuuu.com",
22
+ "localePrefix": "never",
23
+ "publicPaths": [],
24
+ "publicPatterns": []
25
+ },
26
+ "cms": {
27
+ "origin": "https://cms.tuturuuu.com",
28
+ "localePrefix": "never",
29
+ "publicPaths": [],
30
+ "publicPatterns": []
31
+ },
32
+ "contacts": {
33
+ "origin": "https://contacts.tuturuuu.com",
34
+ "localePrefix": "never",
35
+ "publicPaths": [],
36
+ "publicPatterns": []
37
+ },
38
+ "drive": {
39
+ "origin": "https://drive.tuturuuu.com",
40
+ "localePrefix": "never",
41
+ "publicPaths": [],
42
+ "publicPatterns": []
43
+ },
44
+ "external": {
45
+ "origin": "https://external.tuturuuu.com",
46
+ "localePrefix": "never",
47
+ "publicPaths": [],
48
+ "publicPatterns": []
49
+ },
50
+ "finance": {
51
+ "origin": "https://finance.tuturuuu.com",
52
+ "localePrefix": "never",
53
+ "publicPaths": [],
54
+ "publicPatterns": []
55
+ },
56
+ "forms": {
57
+ "origin": "https://forms.tuturuuu.com",
58
+ "localePrefix": "never",
59
+ "publicPaths": [""],
60
+ "publicPatterns": ["f/[^/]+"]
61
+ },
62
+ "git": {
63
+ "origin": "https://git.tuturuuu.com",
64
+ "localePrefix": "never",
65
+ "publicPaths": ["tutur3u/platform"],
66
+ "publicPatterns": ["", "[^/]+/[^/]+(?:/.*)?"]
67
+ },
68
+ "hive": {
69
+ "origin": "https://hive.tuturuuu.com",
70
+ "localePrefix": "never",
71
+ "publicPaths": [],
72
+ "publicPatterns": []
73
+ },
74
+ "infrastructure": {
75
+ "origin": "https://infrastructure.tuturuuu.com",
76
+ "localePrefix": "never",
77
+ "publicPaths": [],
78
+ "publicPatterns": []
79
+ },
80
+ "inventory": {
81
+ "origin": "https://inventory.tuturuuu.com",
82
+ "localePrefix": "never",
83
+ "publicPaths": [],
84
+ "publicPatterns": []
85
+ },
86
+ "learn": {
87
+ "origin": "https://learn.tuturuuu.com",
88
+ "localePrefix": "never",
89
+ "publicPaths": [""],
90
+ "publicPatterns": []
91
+ },
92
+ "lettin": {
93
+ "origin": "https://lettin.tuturuuu.com",
94
+ "localePrefix": "never",
95
+ "publicPaths": ["", "worlds", "spaces"],
96
+ "publicPatterns": ["worlds/[^/]+", "creators/[^/]+"]
97
+ },
98
+ "mail": {
99
+ "origin": "https://mail.tuturuuu.com",
100
+ "localePrefix": "never",
101
+ "publicPaths": [],
102
+ "publicPatterns": []
103
+ },
104
+ "meet": {
105
+ "origin": "https://meet.tuturuuu.com",
106
+ "localePrefix": "never",
107
+ "publicPaths": [],
108
+ "publicPatterns": []
109
+ },
110
+ "mind": {
111
+ "origin": "https://mind.tuturuuu.com",
112
+ "localePrefix": "never",
113
+ "publicPaths": [],
114
+ "publicPatterns": []
115
+ },
116
+ "nova": {
117
+ "origin": "https://nova.tuturuuu.com",
118
+ "localePrefix": "as-needed",
119
+ "publicPaths": [
120
+ "",
121
+ "learn",
122
+ "learn/introduction",
123
+ "learn/basic-techniques",
124
+ "learn/advanced-strategies",
125
+ "learn/best-practices",
126
+ "competitions/neo-league/prompt-the-future/about"
127
+ ],
128
+ "publicPatterns": []
129
+ },
130
+ "parley": {
131
+ "origin": "https://parley.tuturuuu.com",
132
+ "localePrefix": "never",
133
+ "publicPaths": [],
134
+ "publicPatterns": []
135
+ },
136
+ "pay": {
137
+ "origin": "https://pay.tuturuuu.com",
138
+ "localePrefix": "never",
139
+ "publicPaths": [],
140
+ "publicPatterns": []
141
+ },
142
+ "playground": {
143
+ "origin": "https://playground.tuturuuu.com",
144
+ "localePrefix": "never",
145
+ "publicPaths": [],
146
+ "publicPatterns": []
147
+ },
148
+ "rewise": {
149
+ "origin": "https://rewise.tuturuuu.com",
150
+ "localePrefix": "never",
151
+ "publicPaths": [],
152
+ "publicPatterns": []
153
+ },
154
+ "shortener": {
155
+ "origin": "https://shortener.tuturuuu.com",
156
+ "localePrefix": "never",
157
+ "publicPaths": [],
158
+ "publicPatterns": []
159
+ },
160
+ "storefront": {
161
+ "origin": "https://storefront.tuturuuu.com",
162
+ "localePrefix": "never",
163
+ "publicPaths": [],
164
+ "publicPatterns": ["[^/]+", "[^/]+/products/[^/]+"]
165
+ },
166
+ "tasks": {
167
+ "origin": "https://tasks.tuturuuu.com",
168
+ "localePrefix": "never",
169
+ "publicPaths": [],
170
+ "publicPatterns": []
171
+ },
172
+ "teach": {
173
+ "origin": "https://teach.tuturuuu.com",
174
+ "localePrefix": "never",
175
+ "publicPaths": [""],
176
+ "publicPatterns": []
177
+ },
178
+ "tools": {
179
+ "origin": "https://tools.tuturuuu.com",
180
+ "localePrefix": "never",
181
+ "publicPaths": ["", "qr", "random"],
182
+ "publicPatterns": []
183
+ },
184
+ "track": {
185
+ "origin": "https://track.tuturuuu.com",
186
+ "localePrefix": "never",
187
+ "publicPaths": [],
188
+ "publicPatterns": []
189
+ },
190
+ "web": {
191
+ "origin": "https://tuturuuu.com",
192
+ "localePrefix": "as-needed",
193
+ "publicPaths": [],
194
+ "publicPatterns": []
195
+ }
196
+ }