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.
- package/README.md +200 -0
- package/dist/chat.d.ts +139 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +146 -0
- package/dist/chat.js.map +1 -0
- package/dist/common.d.ts +228 -0
- package/dist/common.d.ts.map +1 -0
- package/dist/common.js +248 -0
- package/dist/common.js.map +1 -0
- package/dist/discover.d.ts +153 -0
- package/dist/discover.d.ts.map +1 -0
- package/dist/discover.js +54 -0
- package/dist/discover.js.map +1 -0
- package/dist/feed.d.ts +1310 -0
- package/dist/feed.d.ts.map +1 -0
- package/dist/feed.js +593 -0
- package/dist/feed.js.map +1 -0
- package/dist/founder-profile.d.ts +357 -0
- package/dist/founder-profile.d.ts.map +1 -0
- package/dist/founder-profile.js +180 -0
- package/dist/founder-profile.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/onboarding.d.ts +638 -0
- package/dist/onboarding.d.ts.map +1 -0
- package/dist/onboarding.js +547 -0
- package/dist/onboarding.js.map +1 -0
- package/dist/responses/common.d.ts +49 -0
- package/dist/responses/common.d.ts.map +1 -0
- package/dist/responses/common.js +44 -0
- package/dist/responses/common.js.map +1 -0
- package/dist/responses/index.d.ts +7 -0
- package/dist/responses/index.d.ts.map +1 -0
- package/dist/responses/index.js +7 -0
- package/dist/responses/index.js.map +1 -0
- package/package.json +51 -0
package/dist/common.d.ts
ADDED
|
@@ -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
|