founder-schema 0.1.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.
@@ -0,0 +1,228 @@
1
+ /**
2
+ * founder-schema — Shared helpers and core enums
3
+ *
4
+ * The stable building blocks reused across every Founder Circle schema.
5
+ * Anything that both the backend and a client need to agree on lives here.
6
+ *
7
+ * @module founder-schema/common
8
+ */
9
+ import { z } from 'zod';
10
+ /**
11
+ * Converts empty / whitespace-only strings to `undefined` so Zod `.optional()`
12
+ * treats them as "not provided".
13
+ *
14
+ * Why this exists: an HTML form submits `""` for a cleared field, and without
15
+ * this an optional field would be stored as an empty string rather than left
16
+ * alone. Jansathi shipped that bug in dating onboarding.
17
+ */
18
+ export declare const emptyStringToUndefined: (value: unknown) => unknown;
19
+ /** Optional trimmed string. Empty strings are coerced to `undefined`. */
20
+ export declare const optionalTrimmedString: (maxLength?: number) => z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
21
+ /** A Mongo ObjectId as it crosses the wire: 24 lowercase hex characters. */
22
+ export declare const objectIdSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
23
+ /**
24
+ * A URL safe to store and, after the consumer's own check, to render.
25
+ *
26
+ * Use this for EVERY member-supplied URL. Never bare `z.url()`: it accepts any
27
+ * scheme the URL parser accepts, so `javascript:alert(document.cookie)`,
28
+ * `data:text/html,<script>...`, `vbscript:` and `file:///etc/passwd` all pass.
29
+ * Each of those is stored XSS the moment a client renders it as an `href`, and
30
+ * `data:` is the same in an `<img src>`.
31
+ *
32
+ * `hostname` is restricted too, not only the scheme. A bare host like
33
+ * `localhost` or a literal IP is refused, which closes server-side request
34
+ * forgery: this product runs on EC2 with an instance role, so a member-supplied
35
+ * `http://169.254.169.254/latest/meta-data/` that anything server-side ever
36
+ * fetches is a credential read.
37
+ *
38
+ * Validating here is necessary and not sufficient. A consumer still treats a
39
+ * stored URL as untrusted at render time, because rows written before this
40
+ * existed were never checked.
41
+ *
42
+ * ⚠️ IF THIS EVER GROWS A HOST ALLOWLIST, compare the PARSED hostname for
43
+ * exact equality. Never `includes`, never `endsWith`, never a regex over the
44
+ * whole URL. Each of these is a real bypass of a naive "is it YouTube" check,
45
+ * and all three read as obviously fine:
46
+ *
47
+ * https://evil.example/?next=youtube.com/watch contains the string
48
+ * youtube.com.evil.example starts with it
49
+ * youtu.be.evil.example a suffix check passes it
50
+ *
51
+ * The same mistake in the opposite direction to the `javascript:` hole: there
52
+ * the scheme was too permissive, here the host check only LOOKS strict.
53
+ * (krish hit this rendering inline video, 2026-09-21, and handles it by parsing
54
+ * and comparing the hostname exactly.)
55
+ */
56
+ export declare const httpUrlSchema: z.ZodURL;
57
+ /**
58
+ * Public handle a founder picks and can change later.
59
+ *
60
+ * Rules are deliberately narrow because the handle appears in URLs and in
61
+ * mentions: lowercase letters, digits, underscore and a single dot, 3 to 30
62
+ * characters, must start with a letter. Reserved words are refused by the
63
+ * backend, not here, so the list can change without a schema publish.
64
+ */
65
+ export declare const USERNAME_MIN = 3;
66
+ export declare const USERNAME_MAX = 30;
67
+ export declare const usernameSchema: z.ZodString;
68
+ export type Username = z.infer<typeof usernameSchema>;
69
+ /**
70
+ * Stage of the founder's current venture.
71
+ *
72
+ * Ordered from earliest to latest so a range filter can compare by index. Add
73
+ * new stages at the END or the stored ordinal of existing rows shifts.
74
+ */
75
+ export declare const STARTUP_STAGE_VALUES: readonly ["idea", "validation", "mvp", "pre_seed", "seed", "series_a_plus", "profitable", "exited"];
76
+ export declare const startupStageSchema: z.ZodEnum<{
77
+ idea: "idea";
78
+ validation: "validation";
79
+ mvp: "mvp";
80
+ pre_seed: "pre_seed";
81
+ seed: "seed";
82
+ series_a_plus: "series_a_plus";
83
+ profitable: "profitable";
84
+ exited: "exited";
85
+ }>;
86
+ export type StartupStage = z.infer<typeof startupStageSchema>;
87
+ /**
88
+ * Industry the venture operates in. Kept as a closed list so Discover filters
89
+ * are a single indexed equality match rather than a text search.
90
+ */
91
+ export declare const INDUSTRY_VALUES: readonly ["agritech", "ai_ml", "b2b_saas", "consumer", "climate", "d2c", "deeptech", "edtech", "fintech", "gaming", "healthtech", "logistics", "manufacturing", "marketplace", "media", "mobility", "proptech", "robotics", "services", "space", "other"];
92
+ export declare const industrySchema: z.ZodEnum<{
93
+ agritech: "agritech";
94
+ ai_ml: "ai_ml";
95
+ b2b_saas: "b2b_saas";
96
+ consumer: "consumer";
97
+ climate: "climate";
98
+ d2c: "d2c";
99
+ deeptech: "deeptech";
100
+ edtech: "edtech";
101
+ fintech: "fintech";
102
+ gaming: "gaming";
103
+ healthtech: "healthtech";
104
+ logistics: "logistics";
105
+ manufacturing: "manufacturing";
106
+ marketplace: "marketplace";
107
+ media: "media";
108
+ mobility: "mobility";
109
+ proptech: "proptech";
110
+ robotics: "robotics";
111
+ services: "services";
112
+ space: "space";
113
+ other: "other";
114
+ }>;
115
+ export type Industry = z.infer<typeof industrySchema>;
116
+ /**
117
+ * What the founder brings. Free-text skills were considered and rejected: two
118
+ * people typing "growth" and "Growth Marketing" would never match each other in
119
+ * a filter, and the Discover tab is the whole point of the product.
120
+ */
121
+ export declare const SKILL_VALUES: readonly ["engineering", "product", "design", "data", "growth", "sales", "marketing", "operations", "finance", "legal", "hr_people", "supply_chain", "research", "fundraising", "community"];
122
+ export declare const skillSchema: z.ZodEnum<{
123
+ engineering: "engineering";
124
+ product: "product";
125
+ design: "design";
126
+ data: "data";
127
+ growth: "growth";
128
+ sales: "sales";
129
+ marketing: "marketing";
130
+ operations: "operations";
131
+ finance: "finance";
132
+ legal: "legal";
133
+ hr_people: "hr_people";
134
+ supply_chain: "supply_chain";
135
+ research: "research";
136
+ fundraising: "fundraising";
137
+ community: "community";
138
+ }>;
139
+ export type Skill = z.infer<typeof skillSchema>;
140
+ /**
141
+ * What the member is here for. ONE list, replacing three.
142
+ *
143
+ * This was `lookingFor` with six values. The design draws eight "Goals &
144
+ * Passion" tiles and four more on the business step; all three were the same
145
+ * idea at three grains, and shipping them together would have meant nobody
146
+ * could say which one Discover filters on - the answer settled by whichever
147
+ * query got written first. Merged on hary's ruling, 2026-09-21, while there
148
+ * were still no rows in any database.
149
+ *
150
+ * ⚠️ Stored per member and used as a Mongoose enum. Append only; removing a
151
+ * value orphans documents already saved with it.
152
+ *
153
+ * ⚠️ `start_company` READS ODDLY next to its neighbours and is correct. It was
154
+ * queried on the grounds that it restates `venture.stage: 'idea'`; it does not,
155
+ * and the reason is worth keeping so nobody re-opens it. Inferring it from the
156
+ * venture works only for a member who ALREADY HAS a venture row, and somebody
157
+ * who wants to start a company is precisely the member with no venture at all,
158
+ * so there is nothing to infer from. It also cannot express a founder with a
159
+ * running company who wants to start another. Every other value is an ask of
160
+ * other people and this one is an ask of the product, which is the whole of the
161
+ * oddness. (hary, 2026-09-21.)
162
+ */
163
+ export declare const GOAL_VALUES: readonly ["find_cofounder", "find_investors", "find_customers", "hiring", "partnerships", "mentorship", "grow_network", "business_ideas", "market_insights", "learn_startup", "start_company"];
164
+ export declare const goalSchema: z.ZodEnum<{
165
+ find_cofounder: "find_cofounder";
166
+ find_investors: "find_investors";
167
+ find_customers: "find_customers";
168
+ hiring: "hiring";
169
+ partnerships: "partnerships";
170
+ mentorship: "mentorship";
171
+ grow_network: "grow_network";
172
+ business_ideas: "business_ideas";
173
+ market_insights: "market_insights";
174
+ learn_startup: "learn_startup";
175
+ start_company: "start_company";
176
+ }>;
177
+ export type Goal = z.infer<typeof goalSchema>;
178
+ /**
179
+ * Demographic only. Nothing here computes anything from it, unlike the sibling
180
+ * gym product where gender feeds body-metric maths.
181
+ *
182
+ * Takes the family's three values and ADDS one rather than renaming any: a
183
+ * network for founders should let somebody decline the question without having
184
+ * to describe themselves as "other".
185
+ */
186
+ export declare const GENDER_VALUES: readonly ["male", "female", "other", "prefer_not_to_say"];
187
+ export declare const genderSchema: z.ZodEnum<{
188
+ other: "other";
189
+ male: "male";
190
+ female: "female";
191
+ prefer_not_to_say: "prefer_not_to_say";
192
+ }>;
193
+ export type Gender = z.infer<typeof genderSchema>;
194
+ /**
195
+ * Indian mobile number: ten raw digits, no country code, matching the standard
196
+ * used across this family. Display adds the `+91`.
197
+ *
198
+ * ⚠️ WHERE IT IS STORED IS NOT A SCHEMA QUESTION, and it has been ruled: the
199
+ * phone lives on the account mirror, never as a second copy on the profile.
200
+ * Two stored copies drift the first time somebody edits one, and the profile's
201
+ * would be the copy nobody keeps in sync.
202
+ */
203
+ export declare const phoneSchema: z.ZodString;
204
+ /**
205
+ * Where the founder is based. City and country are what Discover filters on;
206
+ * coordinates are optional and only present when the member granted location.
207
+ *
208
+ * Note for later: this is a DECLARED location, not a GPS reading. Jansathi's
209
+ * rule applies here too — never write a declared location from a GPS capture.
210
+ */
211
+ export declare const locationSchema: z.ZodObject<{
212
+ city: z.ZodString;
213
+ state: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
214
+ country: z.ZodDefault<z.ZodString>;
215
+ }, z.core.$strip>;
216
+ export type FounderLocation = z.infer<typeof locationSchema>;
217
+ /**
218
+ * Cursor pagination query. Feed and Discover both use it.
219
+ *
220
+ * Cursor, not page number: the community feed changes under the reader, and an
221
+ * offset would repeat or skip posts as new ones land.
222
+ */
223
+ export declare const paginationQuerySchema: z.ZodObject<{
224
+ limit: z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
225
+ cursor: z.ZodOptional<z.ZodString>;
226
+ }, z.core.$strip>;
227
+ export type PaginationQuery = z.infer<typeof paginationQuerySchema>;
228
+ //# sourceMappingURL=common.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAAI,OAAO,OAAO,KAAG,OAGvD,CAAC;AAEF,yEAAyE;AACzE,eAAO,MAAM,qBAAqB,GAAI,kBAAe,yDAC8B,CAAC;AAEpF,4EAA4E;AAC5E,eAAO,MAAM,cAAc,wDAGS,CAAC;AAIrC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,aAAa,UAGxB,CAAC;AAIH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,YAAY,KAAK,CAAC;AAE/B,eAAO,MAAM,cAAc,aASxB,CAAC;AACJ,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAItD;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,qGASvB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;;;;;;EAA+B,CAAC;AAC/D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D;;;GAGG;AACH,eAAO,MAAM,eAAe,2PAsBlB,CAAC;AACX,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;EAA0B,CAAC;AACtD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,YAAY,8LAgBf,CAAC;AACX,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;EAAuB,CAAC;AAChD,MAAM,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,WAAW,CAAC,CAAC;AAEhD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,WAAW,gMAYd,CAAC;AACX,eAAO,MAAM,UAAU;;;;;;;;;;;;EAAsB,CAAC;AAC9C,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAI9C;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,2DAA4D,CAAC;AACvF,eAAO,MAAM,YAAY;;;;;EAAwB,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAIlD;;;;;;;;GAQG;AACH,eAAO,MAAM,WAAW,aAGkC,CAAC;AAI3D;;;;;;GAMG;AACH,eAAO,MAAM,cAAc;;;;iBAIzB,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAI7D;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB;;;iBAShC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC"}
package/dist/common.js ADDED
@@ -0,0 +1,248 @@
1
+ /**
2
+ * founder-schema — Shared helpers and core enums
3
+ *
4
+ * The stable building blocks reused across every Founder Circle schema.
5
+ * Anything that both the backend and a client need to agree on lives here.
6
+ *
7
+ * @module founder-schema/common
8
+ */
9
+ import { z } from 'zod';
10
+ // ─── String helpers ──────────────────────────────────────────────────────────
11
+ /**
12
+ * Converts empty / whitespace-only strings to `undefined` so Zod `.optional()`
13
+ * treats them as "not provided".
14
+ *
15
+ * Why this exists: an HTML form submits `""` for a cleared field, and without
16
+ * this an optional field would be stored as an empty string rather than left
17
+ * alone. Jansathi shipped that bug in dating onboarding.
18
+ */
19
+ export const emptyStringToUndefined = (value) => {
20
+ if (typeof value === 'string' && value.trim() === '')
21
+ return undefined;
22
+ return value;
23
+ };
24
+ /** Optional trimmed string. Empty strings are coerced to `undefined`. */
25
+ export const optionalTrimmedString = (maxLength = 100) => z.preprocess(emptyStringToUndefined, z.string().trim().max(maxLength).optional());
26
+ /** A Mongo ObjectId as it crosses the wire: 24 lowercase hex characters. */
27
+ export const objectIdSchema = z
28
+ .string()
29
+ .regex(/^[0-9a-f]{24}$/i, 'Invalid id')
30
+ .transform((v) => v.toLowerCase());
31
+ // ─── URLs ────────────────────────────────────────────────────────────────────
32
+ /**
33
+ * A URL safe to store and, after the consumer's own check, to render.
34
+ *
35
+ * Use this for EVERY member-supplied URL. Never bare `z.url()`: it accepts any
36
+ * scheme the URL parser accepts, so `javascript:alert(document.cookie)`,
37
+ * `data:text/html,<script>...`, `vbscript:` and `file:///etc/passwd` all pass.
38
+ * Each of those is stored XSS the moment a client renders it as an `href`, and
39
+ * `data:` is the same in an `<img src>`.
40
+ *
41
+ * `hostname` is restricted too, not only the scheme. A bare host like
42
+ * `localhost` or a literal IP is refused, which closes server-side request
43
+ * forgery: this product runs on EC2 with an instance role, so a member-supplied
44
+ * `http://169.254.169.254/latest/meta-data/` that anything server-side ever
45
+ * fetches is a credential read.
46
+ *
47
+ * Validating here is necessary and not sufficient. A consumer still treats a
48
+ * stored URL as untrusted at render time, because rows written before this
49
+ * existed were never checked.
50
+ *
51
+ * ⚠️ IF THIS EVER GROWS A HOST ALLOWLIST, compare the PARSED hostname for
52
+ * exact equality. Never `includes`, never `endsWith`, never a regex over the
53
+ * whole URL. Each of these is a real bypass of a naive "is it YouTube" check,
54
+ * and all three read as obviously fine:
55
+ *
56
+ * https://evil.example/?next=youtube.com/watch contains the string
57
+ * youtube.com.evil.example starts with it
58
+ * youtu.be.evil.example a suffix check passes it
59
+ *
60
+ * The same mistake in the opposite direction to the `javascript:` hole: there
61
+ * the scheme was too permissive, here the host check only LOOKS strict.
62
+ * (krish hit this rendering inline video, 2026-09-21, and handles it by parsing
63
+ * and comparing the hostname exactly.)
64
+ */
65
+ export const httpUrlSchema = z.url({
66
+ protocol: /^https?$/,
67
+ hostname: z.regexes.domain,
68
+ });
69
+ // ─── Username ────────────────────────────────────────────────────────────────
70
+ /**
71
+ * Public handle a founder picks and can change later.
72
+ *
73
+ * Rules are deliberately narrow because the handle appears in URLs and in
74
+ * mentions: lowercase letters, digits, underscore and a single dot, 3 to 30
75
+ * characters, must start with a letter. Reserved words are refused by the
76
+ * backend, not here, so the list can change without a schema publish.
77
+ */
78
+ export const USERNAME_MIN = 3;
79
+ export const USERNAME_MAX = 30;
80
+ export const usernameSchema = z
81
+ .string()
82
+ .trim()
83
+ .toLowerCase()
84
+ .min(USERNAME_MIN, `Username must be at least ${USERNAME_MIN} characters`)
85
+ .max(USERNAME_MAX, `Username must be at most ${USERNAME_MAX} characters`)
86
+ .regex(/^[a-z][a-z0-9_]*(\.[a-z0-9_]+)?$/, 'Use letters, numbers and underscore. One dot allowed, and it cannot start with a dot.');
87
+ // ─── Founder-facing enums ────────────────────────────────────────────────────
88
+ /**
89
+ * Stage of the founder's current venture.
90
+ *
91
+ * Ordered from earliest to latest so a range filter can compare by index. Add
92
+ * new stages at the END or the stored ordinal of existing rows shifts.
93
+ */
94
+ export const STARTUP_STAGE_VALUES = [
95
+ 'idea',
96
+ 'validation',
97
+ 'mvp',
98
+ 'pre_seed',
99
+ 'seed',
100
+ 'series_a_plus',
101
+ 'profitable',
102
+ 'exited',
103
+ ];
104
+ export const startupStageSchema = z.enum(STARTUP_STAGE_VALUES);
105
+ /**
106
+ * Industry the venture operates in. Kept as a closed list so Discover filters
107
+ * are a single indexed equality match rather than a text search.
108
+ */
109
+ export const INDUSTRY_VALUES = [
110
+ 'agritech',
111
+ 'ai_ml',
112
+ 'b2b_saas',
113
+ 'consumer',
114
+ 'climate',
115
+ 'd2c',
116
+ 'deeptech',
117
+ 'edtech',
118
+ 'fintech',
119
+ 'gaming',
120
+ 'healthtech',
121
+ 'logistics',
122
+ 'manufacturing',
123
+ 'marketplace',
124
+ 'media',
125
+ 'mobility',
126
+ 'proptech',
127
+ 'robotics',
128
+ 'services',
129
+ 'space',
130
+ 'other',
131
+ ];
132
+ export const industrySchema = z.enum(INDUSTRY_VALUES);
133
+ /**
134
+ * What the founder brings. Free-text skills were considered and rejected: two
135
+ * people typing "growth" and "Growth Marketing" would never match each other in
136
+ * a filter, and the Discover tab is the whole point of the product.
137
+ */
138
+ export const SKILL_VALUES = [
139
+ 'engineering',
140
+ 'product',
141
+ 'design',
142
+ 'data',
143
+ 'growth',
144
+ 'sales',
145
+ 'marketing',
146
+ 'operations',
147
+ 'finance',
148
+ 'legal',
149
+ 'hr_people',
150
+ 'supply_chain',
151
+ 'research',
152
+ 'fundraising',
153
+ 'community',
154
+ ];
155
+ export const skillSchema = z.enum(SKILL_VALUES);
156
+ /**
157
+ * What the member is here for. ONE list, replacing three.
158
+ *
159
+ * This was `lookingFor` with six values. The design draws eight "Goals &
160
+ * Passion" tiles and four more on the business step; all three were the same
161
+ * idea at three grains, and shipping them together would have meant nobody
162
+ * could say which one Discover filters on - the answer settled by whichever
163
+ * query got written first. Merged on hary's ruling, 2026-09-21, while there
164
+ * were still no rows in any database.
165
+ *
166
+ * ⚠️ Stored per member and used as a Mongoose enum. Append only; removing a
167
+ * value orphans documents already saved with it.
168
+ *
169
+ * ⚠️ `start_company` READS ODDLY next to its neighbours and is correct. It was
170
+ * queried on the grounds that it restates `venture.stage: 'idea'`; it does not,
171
+ * and the reason is worth keeping so nobody re-opens it. Inferring it from the
172
+ * venture works only for a member who ALREADY HAS a venture row, and somebody
173
+ * who wants to start a company is precisely the member with no venture at all,
174
+ * so there is nothing to infer from. It also cannot express a founder with a
175
+ * running company who wants to start another. Every other value is an ask of
176
+ * other people and this one is an ask of the product, which is the whole of the
177
+ * oddness. (hary, 2026-09-21.)
178
+ */
179
+ export const GOAL_VALUES = [
180
+ 'find_cofounder',
181
+ 'find_investors',
182
+ 'find_customers',
183
+ 'hiring',
184
+ 'partnerships',
185
+ 'mentorship',
186
+ 'grow_network',
187
+ 'business_ideas',
188
+ 'market_insights',
189
+ 'learn_startup',
190
+ 'start_company',
191
+ ];
192
+ export const goalSchema = z.enum(GOAL_VALUES);
193
+ // ─── Gender ──────────────────────────────────────────────────────────────────
194
+ /**
195
+ * Demographic only. Nothing here computes anything from it, unlike the sibling
196
+ * gym product where gender feeds body-metric maths.
197
+ *
198
+ * Takes the family's three values and ADDS one rather than renaming any: a
199
+ * network for founders should let somebody decline the question without having
200
+ * to describe themselves as "other".
201
+ */
202
+ export const GENDER_VALUES = ['male', 'female', 'other', 'prefer_not_to_say'];
203
+ export const genderSchema = z.enum(GENDER_VALUES);
204
+ // ─── Phone ───────────────────────────────────────────────────────────────────
205
+ /**
206
+ * Indian mobile number: ten raw digits, no country code, matching the standard
207
+ * used across this family. Display adds the `+91`.
208
+ *
209
+ * ⚠️ WHERE IT IS STORED IS NOT A SCHEMA QUESTION, and it has been ruled: the
210
+ * phone lives on the account mirror, never as a second copy on the profile.
211
+ * Two stored copies drift the first time somebody edits one, and the profile's
212
+ * would be the copy nobody keeps in sync.
213
+ */
214
+ export const phoneSchema = z
215
+ .string()
216
+ .trim()
217
+ .regex(/^[6-9]\d{9}$/, 'Enter a 10 digit mobile number');
218
+ // ─── Location ────────────────────────────────────────────────────────────────
219
+ /**
220
+ * Where the founder is based. City and country are what Discover filters on;
221
+ * coordinates are optional and only present when the member granted location.
222
+ *
223
+ * Note for later: this is a DECLARED location, not a GPS reading. Jansathi's
224
+ * rule applies here too — never write a declared location from a GPS capture.
225
+ */
226
+ export const locationSchema = z.object({
227
+ city: z.string().trim().min(1).max(80),
228
+ state: optionalTrimmedString(80),
229
+ country: z.string().trim().min(1).max(80).default('India'),
230
+ });
231
+ // ─── Pagination ──────────────────────────────────────────────────────────────
232
+ /**
233
+ * Cursor pagination query. Feed and Discover both use it.
234
+ *
235
+ * Cursor, not page number: the community feed changes under the reader, and an
236
+ * offset would repeat or skip posts as new ones land.
237
+ */
238
+ export const paginationQuerySchema = z.object({
239
+ /**
240
+ * `?limit=` with nothing after it is treated as "not provided", not as a
241
+ * validation failure. A client that clears the control sends the empty
242
+ * string, and `z.coerce.number()` turns that into 0, which then fails
243
+ * `.min(1)`: the member sees a 400 for a page they never asked to size.
244
+ */
245
+ limit: z.preprocess(emptyStringToUndefined, z.coerce.number().int().min(1).max(50).default(20)),
246
+ cursor: z.string().trim().min(1).optional(),
247
+ });
248
+ //# sourceMappingURL=common.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"common.js","sourceRoot":"","sources":["../src/common.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — Shared helpers and core enums\n *\n * The stable building blocks reused across every Founder Circle schema.\n * Anything that both the backend and a client need to agree on lives here.\n *\n * @module founder-schema/common\n */\n\nimport { z } from 'zod';\n\n// ─── String helpers ──────────────────────────────────────────────────────────\n\n/**\n * Converts empty / whitespace-only strings to `undefined` so Zod `.optional()`\n * treats them as \"not provided\".\n *\n * Why this exists: an HTML form submits `\"\"` for a cleared field, and without\n * this an optional field would be stored as an empty string rather than left\n * alone. Jansathi shipped that bug in dating onboarding.\n */\nexport const emptyStringToUndefined = (value: unknown): unknown => {\n if (typeof value === 'string' && value.trim() === '') return undefined;\n return value;\n};\n\n/** Optional trimmed string. Empty strings are coerced to `undefined`. */\nexport const optionalTrimmedString = (maxLength = 100) =>\n z.preprocess(emptyStringToUndefined, z.string().trim().max(maxLength).optional());\n\n/** A Mongo ObjectId as it crosses the wire: 24 lowercase hex characters. */\nexport const objectIdSchema = z\n .string()\n .regex(/^[0-9a-f]{24}$/i, 'Invalid id')\n .transform((v) => v.toLowerCase());\n\n// ─── URLs ────────────────────────────────────────────────────────────────────\n\n/**\n * A URL safe to store and, after the consumer's own check, to render.\n *\n * Use this for EVERY member-supplied URL. Never bare `z.url()`: it accepts any\n * scheme the URL parser accepts, so `javascript:alert(document.cookie)`,\n * `data:text/html,<script>...`, `vbscript:` and `file:///etc/passwd` all pass.\n * Each of those is stored XSS the moment a client renders it as an `href`, and\n * `data:` is the same in an `<img src>`.\n *\n * `hostname` is restricted too, not only the scheme. A bare host like\n * `localhost` or a literal IP is refused, which closes server-side request\n * forgery: this product runs on EC2 with an instance role, so a member-supplied\n * `http://169.254.169.254/latest/meta-data/` that anything server-side ever\n * fetches is a credential read.\n *\n * Validating here is necessary and not sufficient. A consumer still treats a\n * stored URL as untrusted at render time, because rows written before this\n * existed were never checked.\n *\n * ⚠️ IF THIS EVER GROWS A HOST ALLOWLIST, compare the PARSED hostname for\n * exact equality. Never `includes`, never `endsWith`, never a regex over the\n * whole URL. Each of these is a real bypass of a naive \"is it YouTube\" check,\n * and all three read as obviously fine:\n *\n * https://evil.example/?next=youtube.com/watch contains the string\n * youtube.com.evil.example starts with it\n * youtu.be.evil.example a suffix check passes it\n *\n * The same mistake in the opposite direction to the `javascript:` hole: there\n * the scheme was too permissive, here the host check only LOOKS strict.\n * (krish hit this rendering inline video, 2026-09-21, and handles it by parsing\n * and comparing the hostname exactly.)\n */\nexport const httpUrlSchema = z.url({\n protocol: /^https?$/,\n hostname: z.regexes.domain,\n});\n\n// ─── Username ────────────────────────────────────────────────────────────────\n\n/**\n * Public handle a founder picks and can change later.\n *\n * Rules are deliberately narrow because the handle appears in URLs and in\n * mentions: lowercase letters, digits, underscore and a single dot, 3 to 30\n * characters, must start with a letter. Reserved words are refused by the\n * backend, not here, so the list can change without a schema publish.\n */\nexport const USERNAME_MIN = 3;\nexport const USERNAME_MAX = 30;\n\nexport const usernameSchema = z\n .string()\n .trim()\n .toLowerCase()\n .min(USERNAME_MIN, `Username must be at least ${USERNAME_MIN} characters`)\n .max(USERNAME_MAX, `Username must be at most ${USERNAME_MAX} characters`)\n .regex(\n /^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)?$/,\n 'Use letters, numbers and underscore. One dot allowed, and it cannot start with a dot.',\n );\nexport type Username = z.infer<typeof usernameSchema>;\n\n// ─── Founder-facing enums ────────────────────────────────────────────────────\n\n/**\n * Stage of the founder's current venture.\n *\n * Ordered from earliest to latest so a range filter can compare by index. Add\n * new stages at the END or the stored ordinal of existing rows shifts.\n */\nexport const STARTUP_STAGE_VALUES = [\n 'idea',\n 'validation',\n 'mvp',\n 'pre_seed',\n 'seed',\n 'series_a_plus',\n 'profitable',\n 'exited',\n] as const;\nexport const startupStageSchema = z.enum(STARTUP_STAGE_VALUES);\nexport type StartupStage = z.infer<typeof startupStageSchema>;\n\n/**\n * Industry the venture operates in. Kept as a closed list so Discover filters\n * are a single indexed equality match rather than a text search.\n */\nexport const INDUSTRY_VALUES = [\n 'agritech',\n 'ai_ml',\n 'b2b_saas',\n 'consumer',\n 'climate',\n 'd2c',\n 'deeptech',\n 'edtech',\n 'fintech',\n 'gaming',\n 'healthtech',\n 'logistics',\n 'manufacturing',\n 'marketplace',\n 'media',\n 'mobility',\n 'proptech',\n 'robotics',\n 'services',\n 'space',\n 'other',\n] as const;\nexport const industrySchema = z.enum(INDUSTRY_VALUES);\nexport type Industry = z.infer<typeof industrySchema>;\n\n/**\n * What the founder brings. Free-text skills were considered and rejected: two\n * people typing \"growth\" and \"Growth Marketing\" would never match each other in\n * a filter, and the Discover tab is the whole point of the product.\n */\nexport const SKILL_VALUES = [\n 'engineering',\n 'product',\n 'design',\n 'data',\n 'growth',\n 'sales',\n 'marketing',\n 'operations',\n 'finance',\n 'legal',\n 'hr_people',\n 'supply_chain',\n 'research',\n 'fundraising',\n 'community',\n] as const;\nexport const skillSchema = z.enum(SKILL_VALUES);\nexport type Skill = z.infer<typeof skillSchema>;\n\n/**\n * What the member is here for. ONE list, replacing three.\n *\n * This was `lookingFor` with six values. The design draws eight \"Goals &\n * Passion\" tiles and four more on the business step; all three were the same\n * idea at three grains, and shipping them together would have meant nobody\n * could say which one Discover filters on - the answer settled by whichever\n * query got written first. Merged on hary's ruling, 2026-09-21, while there\n * were still no rows in any database.\n *\n * ⚠️ Stored per member and used as a Mongoose enum. Append only; removing a\n * value orphans documents already saved with it.\n *\n * ⚠️ `start_company` READS ODDLY next to its neighbours and is correct. It was\n * queried on the grounds that it restates `venture.stage: 'idea'`; it does not,\n * and the reason is worth keeping so nobody re-opens it. Inferring it from the\n * venture works only for a member who ALREADY HAS a venture row, and somebody\n * who wants to start a company is precisely the member with no venture at all,\n * so there is nothing to infer from. It also cannot express a founder with a\n * running company who wants to start another. Every other value is an ask of\n * other people and this one is an ask of the product, which is the whole of the\n * oddness. (hary, 2026-09-21.)\n */\nexport const GOAL_VALUES = [\n 'find_cofounder',\n 'find_investors',\n 'find_customers',\n 'hiring',\n 'partnerships',\n 'mentorship',\n 'grow_network',\n 'business_ideas',\n 'market_insights',\n 'learn_startup',\n 'start_company',\n] as const;\nexport const goalSchema = z.enum(GOAL_VALUES);\nexport type Goal = z.infer<typeof goalSchema>;\n\n// ─── Gender ──────────────────────────────────────────────────────────────────\n\n/**\n * Demographic only. Nothing here computes anything from it, unlike the sibling\n * gym product where gender feeds body-metric maths.\n *\n * Takes the family's three values and ADDS one rather than renaming any: a\n * network for founders should let somebody decline the question without having\n * to describe themselves as \"other\".\n */\nexport const GENDER_VALUES = ['male', 'female', 'other', 'prefer_not_to_say'] as const;\nexport const genderSchema = z.enum(GENDER_VALUES);\nexport type Gender = z.infer<typeof genderSchema>;\n\n// ─── Phone ───────────────────────────────────────────────────────────────────\n\n/**\n * Indian mobile number: ten raw digits, no country code, matching the standard\n * used across this family. Display adds the `+91`.\n *\n * ⚠️ WHERE IT IS STORED IS NOT A SCHEMA QUESTION, and it has been ruled: the\n * phone lives on the account mirror, never as a second copy on the profile.\n * Two stored copies drift the first time somebody edits one, and the profile's\n * would be the copy nobody keeps in sync.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(/^[6-9]\\d{9}$/, 'Enter a 10 digit mobile number');\n\n// ─── Location ────────────────────────────────────────────────────────────────\n\n/**\n * Where the founder is based. City and country are what Discover filters on;\n * coordinates are optional and only present when the member granted location.\n *\n * Note for later: this is a DECLARED location, not a GPS reading. Jansathi's\n * rule applies here too — never write a declared location from a GPS capture.\n */\nexport const locationSchema = z.object({\n city: z.string().trim().min(1).max(80),\n state: optionalTrimmedString(80),\n country: z.string().trim().min(1).max(80).default('India'),\n});\nexport type FounderLocation = z.infer<typeof locationSchema>;\n\n// ─── Pagination ──────────────────────────────────────────────────────────────\n\n/**\n * Cursor pagination query. Feed and Discover both use it.\n *\n * Cursor, not page number: the community feed changes under the reader, and an\n * offset would repeat or skip posts as new ones land.\n */\nexport const paginationQuerySchema = z.object({\n /**\n * `?limit=` with nothing after it is treated as \"not provided\", not as a\n * validation failure. A client that clears the control sends the empty\n * string, and `z.coerce.number()` turns that into 0, which then fails\n * `.min(1)`: the member sees a 400 for a page they never asked to size.\n */\n limit: z.preprocess(emptyStringToUndefined, z.coerce.number().int().min(1).max(50).default(20)),\n cursor: z.string().trim().min(1).optional(),\n});\nexport type PaginationQuery = z.infer<typeof paginationQuerySchema>;\n"]}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * founder-schema — Discover search and filters
3
+ *
4
+ * Search is by name; the filters are industry, skill, startup stage and
5
+ * location. The community surface the Discover tab will grow later is not
6
+ * modelled yet.
7
+ *
8
+ * @module founder-schema/discover
9
+ */
10
+ import { z } from 'zod';
11
+ /**
12
+ * Every free-text filter here is `optionalTrimmedString`, so an EMPTY one is
13
+ * treated as "not provided" rather than as a validation failure.
14
+ *
15
+ * A search box the member has just cleared sends `?q=`, and a select they reset
16
+ * sends `?city=`. Refusing those turns an ordinary interaction into a 400 with
17
+ * nothing useful to show for it: the member asked for fewer filters, not for an
18
+ * error. This is the one place the create-body rule and the patch-body rule
19
+ * agree, because a query has nothing to erase.
20
+ */
21
+ export declare const discoverQuerySchema: z.ZodObject<{
22
+ limit: z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
23
+ cursor: z.ZodOptional<z.ZodString>;
24
+ q: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
25
+ industry: z.ZodPreprocess<z.ZodOptional<z.ZodArray<z.ZodEnum<{
26
+ agritech: "agritech";
27
+ ai_ml: "ai_ml";
28
+ b2b_saas: "b2b_saas";
29
+ consumer: "consumer";
30
+ climate: "climate";
31
+ d2c: "d2c";
32
+ deeptech: "deeptech";
33
+ edtech: "edtech";
34
+ fintech: "fintech";
35
+ gaming: "gaming";
36
+ healthtech: "healthtech";
37
+ logistics: "logistics";
38
+ manufacturing: "manufacturing";
39
+ marketplace: "marketplace";
40
+ media: "media";
41
+ mobility: "mobility";
42
+ proptech: "proptech";
43
+ robotics: "robotics";
44
+ services: "services";
45
+ space: "space";
46
+ other: "other";
47
+ }>>>, unknown>;
48
+ skill: z.ZodPreprocess<z.ZodOptional<z.ZodArray<z.ZodEnum<{
49
+ engineering: "engineering";
50
+ product: "product";
51
+ design: "design";
52
+ data: "data";
53
+ growth: "growth";
54
+ sales: "sales";
55
+ marketing: "marketing";
56
+ operations: "operations";
57
+ finance: "finance";
58
+ legal: "legal";
59
+ hr_people: "hr_people";
60
+ supply_chain: "supply_chain";
61
+ research: "research";
62
+ fundraising: "fundraising";
63
+ community: "community";
64
+ }>>>, unknown>;
65
+ stage: z.ZodPreprocess<z.ZodOptional<z.ZodArray<z.ZodEnum<{
66
+ idea: "idea";
67
+ validation: "validation";
68
+ mvp: "mvp";
69
+ pre_seed: "pre_seed";
70
+ seed: "seed";
71
+ series_a_plus: "series_a_plus";
72
+ profitable: "profitable";
73
+ exited: "exited";
74
+ }>>>, unknown>;
75
+ city: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
76
+ country: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
77
+ }, z.core.$strip>;
78
+ export type DiscoverQuery = z.infer<typeof discoverQuerySchema>;
79
+ /**
80
+ * The filter set the Discover UI renders. Served by the backend rather than
81
+ * hard-coded in each client, so adding an industry does not need three
82
+ * releases.
83
+ */
84
+ export declare const discoverFacetsSchema: z.ZodObject<{
85
+ industries: z.ZodArray<z.ZodObject<{
86
+ value: z.ZodEnum<{
87
+ agritech: "agritech";
88
+ ai_ml: "ai_ml";
89
+ b2b_saas: "b2b_saas";
90
+ consumer: "consumer";
91
+ climate: "climate";
92
+ d2c: "d2c";
93
+ deeptech: "deeptech";
94
+ edtech: "edtech";
95
+ fintech: "fintech";
96
+ gaming: "gaming";
97
+ healthtech: "healthtech";
98
+ logistics: "logistics";
99
+ manufacturing: "manufacturing";
100
+ marketplace: "marketplace";
101
+ media: "media";
102
+ mobility: "mobility";
103
+ proptech: "proptech";
104
+ robotics: "robotics";
105
+ services: "services";
106
+ space: "space";
107
+ other: "other";
108
+ }>;
109
+ label: z.ZodString;
110
+ count: z.ZodNumber;
111
+ }, z.core.$strip>>;
112
+ skills: z.ZodArray<z.ZodObject<{
113
+ value: z.ZodEnum<{
114
+ engineering: "engineering";
115
+ product: "product";
116
+ design: "design";
117
+ data: "data";
118
+ growth: "growth";
119
+ sales: "sales";
120
+ marketing: "marketing";
121
+ operations: "operations";
122
+ finance: "finance";
123
+ legal: "legal";
124
+ hr_people: "hr_people";
125
+ supply_chain: "supply_chain";
126
+ research: "research";
127
+ fundraising: "fundraising";
128
+ community: "community";
129
+ }>;
130
+ label: z.ZodString;
131
+ count: z.ZodNumber;
132
+ }, z.core.$strip>>;
133
+ stages: z.ZodArray<z.ZodObject<{
134
+ value: z.ZodEnum<{
135
+ idea: "idea";
136
+ validation: "validation";
137
+ mvp: "mvp";
138
+ pre_seed: "pre_seed";
139
+ seed: "seed";
140
+ series_a_plus: "series_a_plus";
141
+ profitable: "profitable";
142
+ exited: "exited";
143
+ }>;
144
+ label: z.ZodString;
145
+ count: z.ZodNumber;
146
+ }, z.core.$strip>>;
147
+ cities: z.ZodArray<z.ZodObject<{
148
+ value: z.ZodString;
149
+ count: z.ZodNumber;
150
+ }, z.core.$strip>>;
151
+ }, z.core.$strip>;
152
+ export type DiscoverFacets = z.infer<typeof discoverFacetsSchema>;
153
+ //# sourceMappingURL=discover.d.ts.map