@tuturuuu/utils 0.33.2 → 0.34.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 (33) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +5 -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__/daily-operation-download-isolation.test.ts +137 -0
  8. package/src/__tests__/offline-download-guard.test.ts +340 -0
  9. package/src/__tests__/username-policy.test.ts +70 -0
  10. package/src/account-benefits-server.ts +43 -0
  11. package/src/account-benefits.ts +45 -0
  12. package/src/api-proxy-guard.ts +8 -42
  13. package/src/api-proxy-read-policy.ts +43 -0
  14. package/src/common/metadata.test.ts +13 -1
  15. package/src/constants.ts +3 -0
  16. package/src/email/client.ts +1 -5
  17. package/src/email/reviewer-domain.d.mts +1 -0
  18. package/src/email/reviewer-domain.mjs +6 -0
  19. package/src/google-calendar-import-fence.test.ts +182 -0
  20. package/src/google-calendar-import-fence.ts +166 -0
  21. package/src/offline-download-guard.ts +160 -0
  22. package/src/platform-release.test.ts +1 -1
  23. package/src/platform-release.ts +1 -1
  24. package/src/playground-schema.test.ts +119 -0
  25. package/src/playground-schema.ts +173 -0
  26. package/src/playground-templates.ts +65 -0
  27. package/src/release-note-policy-core.cjs +28 -0
  28. package/src/release-note-policy-core.d.cts +5 -0
  29. package/src/release-note-policy.json +9 -0
  30. package/src/release-note-policy.ts +4 -0
  31. package/src/username-policy.json +87 -0
  32. package/src/username-policy.ts +18 -0
  33. package/vitest.config.ts +7 -0
@@ -0,0 +1,160 @@
1
+ import { verifyTurnstileToken } from '@tuturuuu/turnstile/server';
2
+ import { Ratelimit } from '@upstash/ratelimit';
3
+ import type { NextRequest } from 'next/server';
4
+ import { NextResponse } from 'next/server';
5
+ import { extractIPFromRequest } from './abuse-protection/edge';
6
+ import { getCachedTrustEntries } from './abuse-protection/edge-trust';
7
+ import {
8
+ buildProxySessionSubjectKey,
9
+ getProxySessionSubjectKeyFromCookieHeader,
10
+ } from './api-proxy-guard';
11
+ import {
12
+ getUpstashRatelimitRedisClient,
13
+ getUpstashRestRedisClient,
14
+ } from './upstash-rest';
15
+
16
+ export const OFFLINE_DOWNLOAD_HEADER = 'x-tuturuuu-offline-download';
17
+ const CLEARANCE_SECONDS = 15 * 60;
18
+ const READ_PATH =
19
+ /^\/api\/(?:v1\/)?workspaces\/[^/]+\/(?:finance|inventory|transactions|wallets|tags|members|products|product-categories|product-units|product-warehouses|tasks|task-boards|boards|labels|task-projects|task-initiatives|habit-trackers|habits|calendar)(?:[-/]|$)/u;
20
+ const USER_READ_PATH =
21
+ /^\/api\/v1\/(?:users\/me\/tasks|calendar|users\/calendar-settings)(?:\/|$)/u;
22
+
23
+ export function isOfflineCapableRead(
24
+ request: Pick<NextRequest, 'method' | 'url'>
25
+ ) {
26
+ if (request.method !== 'GET' && request.method !== 'HEAD') return false;
27
+ const pathname = new URL(request.url).pathname.replace(
28
+ /^\/api\/v1\/mobile-calendar(?=\/api\/)/u,
29
+ ''
30
+ );
31
+ return (
32
+ READ_PATH.test(pathname) ||
33
+ USER_READ_PATH.test(pathname) ||
34
+ /^\/api\/v1\/exchange-rates\/?$/u.test(pathname)
35
+ );
36
+ }
37
+
38
+ function unavailable() {
39
+ return NextResponse.json(
40
+ {
41
+ message:
42
+ 'Download protection is temporarily unavailable. Try again later.',
43
+ },
44
+ {
45
+ status: 503,
46
+ headers: { 'Cache-Control': 'no-store', 'Retry-After': '30' },
47
+ }
48
+ );
49
+ }
50
+
51
+ function challenge() {
52
+ return NextResponse.json(
53
+ {
54
+ code: 'ABUSE_CHALLENGE_REQUIRED',
55
+ message: 'Additional verification is required before retrying.',
56
+ },
57
+ {
58
+ status: 403,
59
+ headers: {
60
+ 'Cache-Control': 'no-store',
61
+ 'X-Abuse-Challenge': 'turnstile',
62
+ },
63
+ }
64
+ );
65
+ }
66
+
67
+ /** Additional protection is scoped to explicitly marked offline bulk downloads.
68
+ * Ordinary enterprise reads retain their existing API/auth protections.
69
+ * Clearance never satisfies authentication, authorization, IP blocks, required
70
+ * MFA, or either rate-limit budget. Runtime failures remain fail-closed. */
71
+ export async function guardOfflineDownloadRequest(request: NextRequest) {
72
+ if (!isOfflineCapableRead(request) || process.env.NODE_ENV !== 'production')
73
+ return null;
74
+ const bulk = request.headers.get(OFFLINE_DOWNLOAD_HEADER) === '1';
75
+ if (!bulk) return null;
76
+ if (process.env.OFFLINE_DOWNLOAD_PROTECTION_ENABLED !== 'true') {
77
+ // Keep baseline API/auth protections while this additional protection is
78
+ // unprovisioned. Offline prefetch is unavailable until explicitly enabled.
79
+ return unavailable();
80
+ }
81
+ try {
82
+ const redis = await getUpstashRestRedisClient();
83
+ const limiterRedis = await getUpstashRatelimitRedisClient();
84
+ if (!redis || !limiterRedis) return unavailable();
85
+ const bearer = request.headers
86
+ .get('authorization')
87
+ ?.match(/^Bearer\s+([^\s]+)$/i)?.[1];
88
+ const sessionKey = bearer
89
+ ? await buildProxySessionSubjectKey('bearer', bearer)
90
+ : await getProxySessionSubjectKeyFromCookieHeader(
91
+ request.headers.get('cookie')
92
+ );
93
+ const ip = extractIPFromRequest(request.headers);
94
+ // IP remains bounded even when a caller rotates unverified bearer values.
95
+ // Internal authenticated gateways may lack a client IP. Never combine
96
+ // their unrelated sessions into a shared unknown-IP bucket. Public ingress
97
+ // still supplies a real IP, retaining the rotating-token budget.
98
+ if (ip === 'unknown' && !sessionKey) return unavailable();
99
+ const subjects = [
100
+ ...(ip !== 'unknown'
101
+ ? [{ key: `ip:${ip}`, minute: 1200, hour: 30000 }]
102
+ : []),
103
+ ...(sessionKey ? [{ key: sessionKey, minute: 600, hour: 12000 }] : []),
104
+ ];
105
+ for (const subject of subjects) {
106
+ for (const [window, limit] of [
107
+ ['1 m', subject.minute],
108
+ ['1 h', subject.hour],
109
+ ] as const) {
110
+ const limiter = new Ratelimit({
111
+ redis: limiterRedis,
112
+ limiter: Ratelimit.slidingWindow(limit, window),
113
+ prefix: `offline:read:${window}`,
114
+ });
115
+ const result = await limiter.limit(subject.key);
116
+ if (!result.success) {
117
+ return NextResponse.json(
118
+ {
119
+ message:
120
+ 'Read limit reached. Try again after the indicated delay.',
121
+ },
122
+ {
123
+ status: 429,
124
+ headers: {
125
+ 'Cache-Control': 'no-store',
126
+ 'Retry-After': `${Math.max(1, Math.ceil((result.reset - Date.now()) / 1000))}`,
127
+ 'X-RateLimit-Policy': 'offline-capable-read',
128
+ },
129
+ }
130
+ );
131
+ }
132
+ }
133
+ }
134
+ if (!sessionKey)
135
+ return NextResponse.json(
136
+ { message: 'Authentication required' },
137
+ { status: 401 }
138
+ );
139
+ const trust = (await getCachedTrustEntries([sessionKey])).get(sessionKey);
140
+ // Reuse established account trust. This check does not write trust markers.
141
+ if (trust?.verified && (trust.m > 1 || trust.mode === 'unlimited'))
142
+ return null;
143
+ const clearanceKey = `offline:clearance:${sessionKey}`;
144
+ if (await redis.get(clearanceKey)) return null;
145
+ const token = request.headers.get('x-tuturuuu-turnstile-token');
146
+ if (!token) return challenge();
147
+ try {
148
+ await verifyTurnstileToken(request, token, {
149
+ remoteIp: ip === 'unknown' ? undefined : ip,
150
+ });
151
+ } catch {
152
+ return challenge();
153
+ }
154
+ await redis.set(clearanceKey, 'passed', { ex: CLEARANCE_SECONDS });
155
+ return null;
156
+ } catch {
157
+ // A distributed guard must not silently fall back to per-process limits.
158
+ return unavailable();
159
+ }
160
+ }
@@ -7,7 +7,7 @@ import {
7
7
 
8
8
  describe('platform release metadata', () => {
9
9
  it('uses the centralized shared browser app version', () => {
10
- expect(TUTURUUU_PLATFORM_VERSION).toBe('0.64.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.66.0'); // x-release-please-version
11
11
  });
12
12
 
13
13
  it('preserves the source timestamp in builtAt and derives a short hash', () => {
@@ -1,6 +1,6 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.64.0'; // x-release-please-version
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.66.0'; // x-release-please-version
4
4
 
5
5
  export type PlatformBuildMetadataInput = {
6
6
  /** ISO timestamp of the source commit used for the build, not deployment time. */
@@ -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,87 @@
1
+ {
2
+ "common": [
3
+ "admin",
4
+ "administrator",
5
+ "anonymous",
6
+ "account",
7
+ "accounts",
8
+ "artist",
9
+ "artists",
10
+ "creator",
11
+ "creators",
12
+ "dashboard",
13
+ "default",
14
+ "everyone",
15
+ "example",
16
+ "guest",
17
+ "help",
18
+ "home",
19
+ "login",
20
+ "logout",
21
+ "moderator",
22
+ "moderation",
23
+ "official",
24
+ "owner",
25
+ "personal",
26
+ "privacy",
27
+ "profile",
28
+ "public",
29
+ "root",
30
+ "security",
31
+ "settings",
32
+ "staff",
33
+ "support",
34
+ "system",
35
+ "terms",
36
+ "test",
37
+ "testing",
38
+ "unknown",
39
+ "user",
40
+ "username",
41
+ "users",
42
+ "website",
43
+ "webmaster"
44
+ ],
45
+ "brands": [
46
+ "adobe",
47
+ "amazon",
48
+ "android",
49
+ "apple",
50
+ "chatgpt",
51
+ "claude",
52
+ "cloudflare",
53
+ "discord",
54
+ "exocorpse",
55
+ "facebook",
56
+ "github",
57
+ "gmail",
58
+ "google",
59
+ "instagram",
60
+ "linkedin",
61
+ "meta",
62
+ "microsoft",
63
+ "netflix",
64
+ "nintendo",
65
+ "nvidia",
66
+ "openai",
67
+ "paypal",
68
+ "samsung",
69
+ "slack",
70
+ "snapchat",
71
+ "sony",
72
+ "spotify",
73
+ "stripe",
74
+ "telegram",
75
+ "tesla",
76
+ "tiktok",
77
+ "tulletin",
78
+ "tulettin",
79
+ "tuturuuu",
80
+ "twitch",
81
+ "twitter",
82
+ "vercel",
83
+ "whatsapp",
84
+ "windows",
85
+ "youtube"
86
+ ]
87
+ }
@@ -0,0 +1,18 @@
1
+ import reserved from './username-policy.json';
2
+
3
+ export const USERNAME_MIN_LENGTH = 5;
4
+ export const USERNAME_MAX_LENGTH = 32;
5
+ export const USERNAME_CHANGE_INTERVAL_DAYS = 14;
6
+ export const DISPLAY_NAME_CHANGE_LIMIT = 2;
7
+ export const DISPLAY_NAME_CHANGE_WINDOW_DAYS = 7;
8
+ const reservedPatterns = [...reserved.common, ...reserved.brands].map(
9
+ (brand) =>
10
+ new RegExp(`^(official)?${brand}(official|support|admin|team|[0-9]+)?$`)
11
+ );
12
+ export function isReservedUsername(value: string) {
13
+ const normalized = value.toLowerCase().replaceAll('_', '');
14
+ return reservedPatterns.some((pattern) => pattern.test(normalized));
15
+ }
16
+ export function isValidNewUsername(value: string) {
17
+ return /^[a-z0-9][a-z0-9_]{4,31}$/.test(value) && !isReservedUsername(value);
18
+ }