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
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Onboarding: steps, enums, and the two shapes of the answers
|
|
3
|
+
*
|
|
4
|
+
* ONE LIST DEFINES THE FLOW. `FOUNDER_ONBOARDING_STEPS` carries the screen
|
|
5
|
+
* title, the help line, the progress denominator AND the fields each step
|
|
6
|
+
* gates on. GymMonk arrived here after keeping two lists in sync by hand, where
|
|
7
|
+
* a step could exist with no title or, worse, be left with no validation gate
|
|
8
|
+
* at all. Dating reached the same place independently. One list makes both
|
|
9
|
+
* impossible.
|
|
10
|
+
*
|
|
11
|
+
* TWO SHAPES, ONE SET OF RULES. A half-filled form legitimately holds `null`
|
|
12
|
+
* for a choice nobody has made yet; the finalize body only ever sees completed
|
|
13
|
+
* answers. So the rules are written once and expressed over both shapes:
|
|
14
|
+
* `onboardingFormSchema` for work in progress and `completeOnboardingBodySchema`
|
|
15
|
+
* for the end. Letting those two drift is how a wizard says a step is optional
|
|
16
|
+
* and then answers 422 at Finish.
|
|
17
|
+
*
|
|
18
|
+
* @module founder-schema/onboarding
|
|
19
|
+
*/
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import { GOAL_VALUES, genderSchema, goalSchema, httpUrlSchema, industrySchema, optionalTrimmedString, phoneSchema, skillSchema, startupStageSchema, } from './common.js';
|
|
22
|
+
// ─── Age ─────────────────────────────────────────────────────────────────────
|
|
23
|
+
/**
|
|
24
|
+
* 18, ruled by the user 2026-09-21. Under-18 would pull in parental-consent
|
|
25
|
+
* duties under the DPDP Act, which is a different and larger problem than a
|
|
26
|
+
* validation rule.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ The SERVER refuses at finalize. This schema exists so the member sees a
|
|
29
|
+
* useful message; it is not the gate.
|
|
30
|
+
*/
|
|
31
|
+
export const MIN_AGE_YEARS = 18;
|
|
32
|
+
/** A sanity ceiling, not a product rule. Catches a mistyped year. */
|
|
33
|
+
export const MAX_AGE_YEARS = 120;
|
|
34
|
+
/**
|
|
35
|
+
* Whole years completed, by the calendar.
|
|
36
|
+
*
|
|
37
|
+
* ⚠️ NOT `elapsedMs / (365.25 * day)`, which is what the sibling product uses.
|
|
38
|
+
* That approximation reads 17.9986 at the start of somebody's eighteenth
|
|
39
|
+
* birthday and only crosses 18 about halfway through the day, so it refuses
|
|
40
|
+
* them for the half of their birthday they are most likely to be signing up on.
|
|
41
|
+
* Measured: it disagrees with the calendar on all 400 of the next 400 days.
|
|
42
|
+
* An age gate is exactly the place an approximation is not good enough.
|
|
43
|
+
*/
|
|
44
|
+
export function ageInYears(isoDate, now = new Date()) {
|
|
45
|
+
// ⚠️ A STRING, `YYYY-MM-DD`, not a `Date`. This is a wire contract, so it
|
|
46
|
+
// takes the wire type. The backend is the obvious caller and the backend
|
|
47
|
+
// holds a `Date`, so it must format first; passing the Date straight in used
|
|
48
|
+
// to fail with `isoDate.split is not a function`, which says nothing useful.
|
|
49
|
+
// Naming the fix costs one line and saves the next person the detour.
|
|
50
|
+
if (typeof isoDate !== 'string') {
|
|
51
|
+
throw new TypeError('ageInYears expects a YYYY-MM-DD string, not a Date. Format it first, e.g. date.toISOString().slice(0, 10).');
|
|
52
|
+
}
|
|
53
|
+
const [y, m, d] = isoDate.split('-').map(Number);
|
|
54
|
+
let age = now.getUTCFullYear() - y;
|
|
55
|
+
const monthNow = now.getUTCMonth() + 1;
|
|
56
|
+
if (monthNow < m || (monthNow === m && now.getUTCDate() < d))
|
|
57
|
+
age -= 1;
|
|
58
|
+
return age;
|
|
59
|
+
}
|
|
60
|
+
/** A calendar date with no time component, `YYYY-MM-DD`. */
|
|
61
|
+
export const isoDateSchema = z
|
|
62
|
+
.string()
|
|
63
|
+
.trim()
|
|
64
|
+
.regex(/^\d{4}-\d{2}-\d{2}$/, 'Expected a YYYY-MM-DD date');
|
|
65
|
+
/** True only for a date that exists. Rejects 2026-02-30 and 2026-13-01. */
|
|
66
|
+
function isRealDate(iso) {
|
|
67
|
+
const [y, m, d] = iso.split('-').map(Number);
|
|
68
|
+
if (m < 1 || m > 12 || d < 1)
|
|
69
|
+
return false;
|
|
70
|
+
const asDate = new Date(Date.UTC(y, m - 1, d));
|
|
71
|
+
return (asDate.getUTCFullYear() === y && asDate.getUTCMonth() === m - 1 && asDate.getUTCDate() === d);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Date of birth, with the age gate applied per parse.
|
|
75
|
+
*
|
|
76
|
+
* ⚠️ THE CLIENT IS NOT THE GATE. This schema exists so the member sees a useful
|
|
77
|
+
* message, and the server must refuse the same dates at finalize regardless.
|
|
78
|
+
* The sibling dating product validates the identical rule on its client and
|
|
79
|
+
* still shipped a defect where an under-age date of birth produced a live,
|
|
80
|
+
* discoverable profile, because the client was the only thing checking.
|
|
81
|
+
*/
|
|
82
|
+
export const dateOfBirthSchema = isoDateSchema
|
|
83
|
+
.refine(isRealDate, { message: 'That date does not exist' })
|
|
84
|
+
.refine((v) => ageInYears(v) >= MIN_AGE_YEARS, {
|
|
85
|
+
message: `You must be at least ${MIN_AGE_YEARS} to join`,
|
|
86
|
+
})
|
|
87
|
+
.refine((v) => ageInYears(v) <= MAX_AGE_YEARS, { message: 'Check the year of birth' });
|
|
88
|
+
// ─── Business ────────────────────────────────────────────────────────────────
|
|
89
|
+
export const BUSINESS_TYPE_VALUES = ['startup', 'sme', 'enterprise'];
|
|
90
|
+
export const businessTypeSchema = z.enum(BUSINESS_TYPE_VALUES);
|
|
91
|
+
// ─── Topics: the shared vocabulary ───────────────────────────────────────────────────────────────
|
|
92
|
+
/**
|
|
93
|
+
* ONE vocabulary, used TWICE: a member picks these as their `interests`, and a
|
|
94
|
+
* post carries them as its `topics`. That is what makes relevance an index
|
|
95
|
+
* lookup rather than a guess, and it is why this list is not called "interests"
|
|
96
|
+
* - naming it for one of its two jobs would have invited a second list for the
|
|
97
|
+
* other, which is the mistake this package has already made once with goals.
|
|
98
|
+
* (User's ruling via hary, 2026-09-21.)
|
|
99
|
+
*
|
|
100
|
+
* Deliberately DISTINCT from two neighbours that will otherwise collect the
|
|
101
|
+
* same words:
|
|
102
|
+
*
|
|
103
|
+
* industry what your company does
|
|
104
|
+
* skills what you can do
|
|
105
|
+
* interests what you want to read about
|
|
106
|
+
*
|
|
107
|
+
* If the copy does not keep them apart, members fill all three identically and
|
|
108
|
+
* every one of them stops telling us anything.
|
|
109
|
+
*
|
|
110
|
+
* A closed list, consistent with industry, skill and stage, for the reason
|
|
111
|
+
* already written beside those: free text means two people typing "growth" and
|
|
112
|
+
* "Growth Marketing" never match, and matching is the entire point. The cost is
|
|
113
|
+
* that adding an interest needs a publish.
|
|
114
|
+
*
|
|
115
|
+
* Tuned down from the dating product's 246, which is mostly dating material.
|
|
116
|
+
* Values reuse its exact strings wherever one existed, so its Hindi
|
|
117
|
+
* translations can be lifted across.
|
|
118
|
+
*
|
|
119
|
+
* ⚠️ Stored per member. Append only; removing a value orphans data.
|
|
120
|
+
*/
|
|
121
|
+
export const TOPIC_VALUES = [
|
|
122
|
+
// Career growth
|
|
123
|
+
'mentoring',
|
|
124
|
+
'leadership',
|
|
125
|
+
'networking',
|
|
126
|
+
'public_speaking',
|
|
127
|
+
'teaching',
|
|
128
|
+
'hiring_talent',
|
|
129
|
+
'personal_branding',
|
|
130
|
+
'career_transition',
|
|
131
|
+
'remote_work',
|
|
132
|
+
// Innovation and tech
|
|
133
|
+
'ai_ml',
|
|
134
|
+
'blockchain',
|
|
135
|
+
'crypto_web3',
|
|
136
|
+
'robotics',
|
|
137
|
+
'data_science',
|
|
138
|
+
'cybersecurity',
|
|
139
|
+
'game_dev',
|
|
140
|
+
'ux_design',
|
|
141
|
+
'app_development',
|
|
142
|
+
'open_source',
|
|
143
|
+
'electronics',
|
|
144
|
+
'space_exploration',
|
|
145
|
+
'green_tech',
|
|
146
|
+
'deeptech',
|
|
147
|
+
'no_code',
|
|
148
|
+
// Business
|
|
149
|
+
'entrepreneurship',
|
|
150
|
+
'investing',
|
|
151
|
+
'stock_trading',
|
|
152
|
+
'personal_finance',
|
|
153
|
+
'fundraising',
|
|
154
|
+
'marketing',
|
|
155
|
+
'sales',
|
|
156
|
+
'consulting',
|
|
157
|
+
'ecommerce',
|
|
158
|
+
'retail',
|
|
159
|
+
'real_estate',
|
|
160
|
+
'supply_chain',
|
|
161
|
+
'product_management',
|
|
162
|
+
'growth',
|
|
163
|
+
'b2b_saas',
|
|
164
|
+
'd2c',
|
|
165
|
+
// Personal development
|
|
166
|
+
'meditation',
|
|
167
|
+
'journaling',
|
|
168
|
+
'self_improvement',
|
|
169
|
+
'productivity',
|
|
170
|
+
'reading',
|
|
171
|
+
'online_courses',
|
|
172
|
+
'philosophy',
|
|
173
|
+
'psychology',
|
|
174
|
+
'therapy',
|
|
175
|
+
'sleep_optimization',
|
|
176
|
+
'gym',
|
|
177
|
+
'yoga',
|
|
178
|
+
// Creative
|
|
179
|
+
'photography',
|
|
180
|
+
'writing',
|
|
181
|
+
'graphic_design',
|
|
182
|
+
'filmmaking',
|
|
183
|
+
'digital_art',
|
|
184
|
+
'content_creation',
|
|
185
|
+
'blogging',
|
|
186
|
+
'vlogging',
|
|
187
|
+
'podcasts',
|
|
188
|
+
'animation',
|
|
189
|
+
'music_production',
|
|
190
|
+
// Impact and community
|
|
191
|
+
'volunteering',
|
|
192
|
+
'ngo_work',
|
|
193
|
+
'environmental',
|
|
194
|
+
'women_empowerment',
|
|
195
|
+
'community_service',
|
|
196
|
+
'social_enterprise',
|
|
197
|
+
'education_access',
|
|
198
|
+
];
|
|
199
|
+
export const topicSchema = z.enum(TOPIC_VALUES);
|
|
200
|
+
export const TOPIC_CATEGORIES = [
|
|
201
|
+
{
|
|
202
|
+
key: 'career',
|
|
203
|
+
label: 'Career Growth',
|
|
204
|
+
values: [
|
|
205
|
+
'mentoring',
|
|
206
|
+
'leadership',
|
|
207
|
+
'networking',
|
|
208
|
+
'public_speaking',
|
|
209
|
+
'teaching',
|
|
210
|
+
'hiring_talent',
|
|
211
|
+
'personal_branding',
|
|
212
|
+
'career_transition',
|
|
213
|
+
'remote_work',
|
|
214
|
+
],
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: 'tech',
|
|
218
|
+
label: 'Innovation & Tech',
|
|
219
|
+
values: [
|
|
220
|
+
'ai_ml',
|
|
221
|
+
'blockchain',
|
|
222
|
+
'crypto_web3',
|
|
223
|
+
'robotics',
|
|
224
|
+
'data_science',
|
|
225
|
+
'cybersecurity',
|
|
226
|
+
'game_dev',
|
|
227
|
+
'ux_design',
|
|
228
|
+
'app_development',
|
|
229
|
+
'open_source',
|
|
230
|
+
'electronics',
|
|
231
|
+
'space_exploration',
|
|
232
|
+
'green_tech',
|
|
233
|
+
'deeptech',
|
|
234
|
+
'no_code',
|
|
235
|
+
],
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
key: 'business',
|
|
239
|
+
label: 'Business',
|
|
240
|
+
values: [
|
|
241
|
+
'entrepreneurship',
|
|
242
|
+
'investing',
|
|
243
|
+
'stock_trading',
|
|
244
|
+
'personal_finance',
|
|
245
|
+
'fundraising',
|
|
246
|
+
'marketing',
|
|
247
|
+
'sales',
|
|
248
|
+
'consulting',
|
|
249
|
+
'ecommerce',
|
|
250
|
+
'retail',
|
|
251
|
+
'real_estate',
|
|
252
|
+
'supply_chain',
|
|
253
|
+
'product_management',
|
|
254
|
+
'growth',
|
|
255
|
+
'b2b_saas',
|
|
256
|
+
'd2c',
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
key: 'growth',
|
|
261
|
+
label: 'Personal Development',
|
|
262
|
+
values: [
|
|
263
|
+
'meditation',
|
|
264
|
+
'journaling',
|
|
265
|
+
'self_improvement',
|
|
266
|
+
'productivity',
|
|
267
|
+
'reading',
|
|
268
|
+
'online_courses',
|
|
269
|
+
'philosophy',
|
|
270
|
+
'psychology',
|
|
271
|
+
'therapy',
|
|
272
|
+
'sleep_optimization',
|
|
273
|
+
'gym',
|
|
274
|
+
'yoga',
|
|
275
|
+
],
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
key: 'creative',
|
|
279
|
+
label: 'Creative Arts',
|
|
280
|
+
values: [
|
|
281
|
+
'photography',
|
|
282
|
+
'writing',
|
|
283
|
+
'graphic_design',
|
|
284
|
+
'filmmaking',
|
|
285
|
+
'digital_art',
|
|
286
|
+
'content_creation',
|
|
287
|
+
'blogging',
|
|
288
|
+
'vlogging',
|
|
289
|
+
'podcasts',
|
|
290
|
+
'animation',
|
|
291
|
+
'music_production',
|
|
292
|
+
],
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
key: 'impact',
|
|
296
|
+
label: 'Impact & Community',
|
|
297
|
+
values: [
|
|
298
|
+
'volunteering',
|
|
299
|
+
'ngo_work',
|
|
300
|
+
'environmental',
|
|
301
|
+
'women_empowerment',
|
|
302
|
+
'community_service',
|
|
303
|
+
'social_enterprise',
|
|
304
|
+
'education_access',
|
|
305
|
+
],
|
|
306
|
+
},
|
|
307
|
+
];
|
|
308
|
+
/**
|
|
309
|
+
* How many a member must pick. A minimum exists because these drive feed
|
|
310
|
+
* relevance and zero of them means no relevance at all.
|
|
311
|
+
*
|
|
312
|
+
* ⚠️ If 3 turns out to be hard to reach against this list, that is a signal
|
|
313
|
+
* about the LIST, not about the minimum. The sibling product enforces no
|
|
314
|
+
* minimum at all, and its own field config says "pick up to 10" while every
|
|
315
|
+
* screen and schema enforces 20.
|
|
316
|
+
*/
|
|
317
|
+
/**
|
|
318
|
+
* ⚠️ TRANSLATION KEY SHAPE, for whoever builds the picker:
|
|
319
|
+
* `<fieldKey>.<value>` for a value and `<fieldKey>._label` for the field name.
|
|
320
|
+
* The dating product's interests JSON is keyed by CATEGORY instead, so all five
|
|
321
|
+
* of its label lookups miss and every chip renders its raw English in every
|
|
322
|
+
* locale, with a fully translated Hindi file sitting unreachable. Lift the
|
|
323
|
+
* translations, reshape the keys.
|
|
324
|
+
*/
|
|
325
|
+
export const INTERESTS_MIN = 3;
|
|
326
|
+
export const INTERESTS_MAX = 10;
|
|
327
|
+
export const FOUNDER_ONBOARDING_STEPS = [
|
|
328
|
+
{
|
|
329
|
+
key: 'identity',
|
|
330
|
+
title: "We'd love to hear more about you",
|
|
331
|
+
helpText: 'This is what other founders see first.',
|
|
332
|
+
fields: ['displayName', 'gender', 'dateOfBirth', 'headline', 'about', 'avatarUploadKey'],
|
|
333
|
+
required: true,
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
key: 'contact',
|
|
337
|
+
title: 'How can people reach you?',
|
|
338
|
+
helpText: 'Your number is never shown on your profile.',
|
|
339
|
+
fields: ['phone', 'links'],
|
|
340
|
+
required: true,
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
key: 'location',
|
|
344
|
+
title: 'Where are you based?',
|
|
345
|
+
helpText: 'Used to show founders near you. You can skip this.',
|
|
346
|
+
fields: ['location'],
|
|
347
|
+
required: false,
|
|
348
|
+
},
|
|
349
|
+
{
|
|
350
|
+
key: 'goals',
|
|
351
|
+
title: 'Goals & Passion',
|
|
352
|
+
helpText: 'Pick what matters most to you.',
|
|
353
|
+
fields: ['goals'],
|
|
354
|
+
required: true,
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
key: 'interests',
|
|
358
|
+
title: 'What inspires you the most?',
|
|
359
|
+
helpText: 'We use these to shape your feed.',
|
|
360
|
+
fields: ['interests'],
|
|
361
|
+
required: true,
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
key: 'business',
|
|
365
|
+
title: 'Tell us about your business',
|
|
366
|
+
helpText: 'Skip it if you are still figuring things out.',
|
|
367
|
+
fields: ['venture', 'businessType'],
|
|
368
|
+
required: false,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
key: 'guidelines',
|
|
372
|
+
title: 'How we behave here',
|
|
373
|
+
helpText: 'A short read, and the only rule that matters.',
|
|
374
|
+
fields: ['guidelinesVersion'],
|
|
375
|
+
required: true,
|
|
376
|
+
},
|
|
377
|
+
];
|
|
378
|
+
export const ONBOARDING_TOTAL_STEPS = FOUNDER_ONBOARDING_STEPS.length;
|
|
379
|
+
/**
|
|
380
|
+
* Fields gating each step, DERIVED from the list above so the two can never
|
|
381
|
+
* disagree.
|
|
382
|
+
*/
|
|
383
|
+
export const ONBOARDING_STEP_FIELDS = FOUNDER_ONBOARDING_STEPS.map((s) => s.fields);
|
|
384
|
+
// ─── Contact ─────────────────────────────────────────────────────────────────
|
|
385
|
+
/**
|
|
386
|
+
* Indian mobile number, ten digits, stored raw with no country code, matching
|
|
387
|
+
* the standard used across this family. Display adds `+91`.
|
|
388
|
+
*/
|
|
389
|
+
export const contactLinksSchema = z.object({
|
|
390
|
+
linkedin: httpUrlSchema.optional(),
|
|
391
|
+
x: httpUrlSchema.optional(),
|
|
392
|
+
instagram: httpUrlSchema.optional(),
|
|
393
|
+
});
|
|
394
|
+
// ─── The form, mid-flight ────────────────────────────────────────────────────
|
|
395
|
+
/**
|
|
396
|
+
* The wizard's working shape.
|
|
397
|
+
*
|
|
398
|
+
* Every choice is nullable because "not answered yet" is a real state here and
|
|
399
|
+
* is NOT the same as "answered with nothing". Arrays start empty. Nothing in
|
|
400
|
+
* this schema is required, on purpose: it describes a form somebody is halfway
|
|
401
|
+
* through, and the rules that decide whether they may FINISH live in
|
|
402
|
+
* `completeOnboardingBodySchema`.
|
|
403
|
+
*/
|
|
404
|
+
export const onboardingFormSchema = z.object({
|
|
405
|
+
displayName: z.string().trim().max(80),
|
|
406
|
+
gender: genderSchema.nullable(),
|
|
407
|
+
dateOfBirth: z.string().trim(),
|
|
408
|
+
headline: z.string().trim().max(120),
|
|
409
|
+
about: z.string().trim().max(1000),
|
|
410
|
+
avatarUploadKey: z.string().trim().nullable(),
|
|
411
|
+
phone: z.string().trim(),
|
|
412
|
+
links: contactLinksSchema,
|
|
413
|
+
location: z
|
|
414
|
+
.object({
|
|
415
|
+
city: z.string().trim().max(80),
|
|
416
|
+
state: z.string().trim().max(80),
|
|
417
|
+
country: z.string().trim().max(80),
|
|
418
|
+
})
|
|
419
|
+
.nullable(),
|
|
420
|
+
goals: z.array(goalSchema).max(GOAL_VALUES.length),
|
|
421
|
+
interests: z.array(topicSchema).max(INTERESTS_MAX),
|
|
422
|
+
venture: z
|
|
423
|
+
.object({
|
|
424
|
+
name: z.string().trim().max(120),
|
|
425
|
+
tagline: z.string().trim().max(160),
|
|
426
|
+
website: z.string().trim(),
|
|
427
|
+
industry: industrySchema.nullable(),
|
|
428
|
+
stage: startupStageSchema.nullable(),
|
|
429
|
+
teamSize: z.string().trim(),
|
|
430
|
+
})
|
|
431
|
+
.nullable(),
|
|
432
|
+
businessType: businessTypeSchema.nullable(),
|
|
433
|
+
skills: z.array(skillSchema).max(8),
|
|
434
|
+
guidelinesVersion: z.string().trim().nullable(),
|
|
435
|
+
});
|
|
436
|
+
// ─── The finalize body ───────────────────────────────────────────────────────
|
|
437
|
+
/**
|
|
438
|
+
* What the client sends to finish. Completed answers only.
|
|
439
|
+
*
|
|
440
|
+
* ⚠️ EVERY OPTIONAL FIELD HERE MUST BE ABSENT WHEN UNANSWERED, never a default
|
|
441
|
+
* standing in for one. The sibling gym product sent `bloodGroup ?? 'O+'`, so
|
|
442
|
+
* every member who skipped the question was recorded as O-positive: a real
|
|
443
|
+
* answer, indistinguishable from one somebody gave, sitting on a screen staff
|
|
444
|
+
* would read in an emergency. Absent has to stay absent.
|
|
445
|
+
*
|
|
446
|
+
* ⚠️ AND THIS IS NOT THE GATE EITHER. The server re-checks the age and refuses
|
|
447
|
+
* at finalize. `isOnboarded` may be set here and nowhere else: four of the
|
|
448
|
+
* profile's partial indexes are gated on it, so a half-built profile carrying
|
|
449
|
+
* it true appears in Discover half-built.
|
|
450
|
+
*/
|
|
451
|
+
export const completeOnboardingBodySchema = z.object({
|
|
452
|
+
displayName: z.string().trim().min(1, 'Tell us your name').max(80),
|
|
453
|
+
/**
|
|
454
|
+
* ⚠️ NO `username` HERE, and its absence is deliberate.
|
|
455
|
+
*
|
|
456
|
+
* It used to be required, and that was a defect: no onboarding step collects
|
|
457
|
+
* one, the design draws no screen for one, and it was not even on the
|
|
458
|
+
* in-progress form, so a finalize could never have succeeded. Found by
|
|
459
|
+
* checking every finalize-required field against the step list; a test now
|
|
460
|
+
* asserts that relationship in both directions.
|
|
461
|
+
*
|
|
462
|
+
* The handle is DERIVED SERVER-SIDE at finalize and edited later from the Me
|
|
463
|
+
* tab, which is what the product describes. That also makes the collision
|
|
464
|
+
* case kinder: a server deriving a handle can suffix it and move on, whereas
|
|
465
|
+
* a member who picked one six steps ago can only be told, at the end, that
|
|
466
|
+
* step one failed.
|
|
467
|
+
*/
|
|
468
|
+
gender: genderSchema,
|
|
469
|
+
dateOfBirth: dateOfBirthSchema,
|
|
470
|
+
headline: optionalTrimmedString(120),
|
|
471
|
+
about: optionalTrimmedString(1000),
|
|
472
|
+
avatarUploadKey: z.string().trim().min(1).optional(),
|
|
473
|
+
phone: phoneSchema,
|
|
474
|
+
links: contactLinksSchema.optional(),
|
|
475
|
+
/** Absent when the member skipped the step or refused the permission. */
|
|
476
|
+
location: z
|
|
477
|
+
.object({
|
|
478
|
+
city: z.string().trim().min(1).max(80),
|
|
479
|
+
state: optionalTrimmedString(80),
|
|
480
|
+
country: z.string().trim().min(1).max(80),
|
|
481
|
+
})
|
|
482
|
+
.optional(),
|
|
483
|
+
goals: z.array(goalSchema).min(1, 'Pick at least one').max(GOAL_VALUES.length),
|
|
484
|
+
interests: z
|
|
485
|
+
.array(topicSchema)
|
|
486
|
+
.min(INTERESTS_MIN, `Pick at least ${INTERESTS_MIN}`)
|
|
487
|
+
.max(INTERESTS_MAX),
|
|
488
|
+
skills: z.array(skillSchema).max(8).optional(),
|
|
489
|
+
/** Absent when the member skipped the business step. */
|
|
490
|
+
venture: z
|
|
491
|
+
.object({
|
|
492
|
+
name: z.string().trim().min(1).max(120),
|
|
493
|
+
tagline: optionalTrimmedString(160),
|
|
494
|
+
website: httpUrlSchema.optional(),
|
|
495
|
+
industry: industrySchema,
|
|
496
|
+
stage: startupStageSchema,
|
|
497
|
+
teamSize: z.coerce.number().int().min(1).max(100000).optional(),
|
|
498
|
+
})
|
|
499
|
+
.optional(),
|
|
500
|
+
businessType: businessTypeSchema.optional(),
|
|
501
|
+
/**
|
|
502
|
+
* The version of the guidelines this member read.
|
|
503
|
+
*
|
|
504
|
+
* A version and not a boolean, and the server stamps the time. A boolean says
|
|
505
|
+
* somebody agreed; a version and a timestamp say WHAT they agreed to and
|
|
506
|
+
* WHEN, which is the only form worth anything the day the text changes or a
|
|
507
|
+
* moderation decision is disputed.
|
|
508
|
+
*/
|
|
509
|
+
guidelinesVersion: z.string().trim().min(1),
|
|
510
|
+
});
|
|
511
|
+
// ─── The resume cursor ───────────────────────────────────────────────────────
|
|
512
|
+
/**
|
|
513
|
+
* Where the member got to, kept in its own collection, separate from the
|
|
514
|
+
* profile.
|
|
515
|
+
*
|
|
516
|
+
* `step` is a flat index into `FOUNDER_ONBOARDING_STEPS`, not a `(step, sub)`
|
|
517
|
+
* pair: one number is what lets a member start on the web and resume elsewhere,
|
|
518
|
+
* provided both ends agree on the encoding.
|
|
519
|
+
*
|
|
520
|
+
* `draft` is opaque here on purpose. The client owns the form's shape, and it is
|
|
521
|
+
* re-validated strictly at finalize, so this schema has no business describing
|
|
522
|
+
* it.
|
|
523
|
+
*/
|
|
524
|
+
export const onboardingDraftSchema = z.record(z.string(), z.unknown());
|
|
525
|
+
export const onboardingProgressSchema = z.object({
|
|
526
|
+
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS),
|
|
527
|
+
draft: onboardingDraftSchema.optional(),
|
|
528
|
+
});
|
|
529
|
+
/** `PUT` body: any subset of the cursor, merged server-side. No defaults. */
|
|
530
|
+
export const onboardingProgressUpdateSchema = z.object({
|
|
531
|
+
step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS).optional(),
|
|
532
|
+
draft: onboardingDraftSchema.optional(),
|
|
533
|
+
});
|
|
534
|
+
/**
|
|
535
|
+
* What the client reads on return.
|
|
536
|
+
*
|
|
537
|
+
* ⚠️ `onboarded` is the gate and it comes from the profile, not from the
|
|
538
|
+
* cursor: a finished member on a new device must be recognised even when the
|
|
539
|
+
* cursor row is gone, and a cursor that failed to load must never make an
|
|
540
|
+
* unfinished member look finished.
|
|
541
|
+
*/
|
|
542
|
+
export const onboardingStatusSchema = z.object({
|
|
543
|
+
onboarded: z.boolean(),
|
|
544
|
+
step: z.number().int().min(0),
|
|
545
|
+
draft: onboardingDraftSchema.optional(),
|
|
546
|
+
});
|
|
547
|
+
//# sourceMappingURL=onboarding.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onboarding.js","sourceRoot":"","sources":["../src/onboarding.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — Onboarding: steps, enums, and the two shapes of the answers\n *\n * ONE LIST DEFINES THE FLOW. `FOUNDER_ONBOARDING_STEPS` carries the screen\n * title, the help line, the progress denominator AND the fields each step\n * gates on. GymMonk arrived here after keeping two lists in sync by hand, where\n * a step could exist with no title or, worse, be left with no validation gate\n * at all. Dating reached the same place independently. One list makes both\n * impossible.\n *\n * TWO SHAPES, ONE SET OF RULES. A half-filled form legitimately holds `null`\n * for a choice nobody has made yet; the finalize body only ever sees completed\n * answers. So the rules are written once and expressed over both shapes:\n * `onboardingFormSchema` for work in progress and `completeOnboardingBodySchema`\n * for the end. Letting those two drift is how a wizard says a step is optional\n * and then answers 422 at Finish.\n *\n * @module founder-schema/onboarding\n */\n\nimport { z } from 'zod';\nimport {\n GOAL_VALUES,\n genderSchema,\n goalSchema,\n httpUrlSchema,\n industrySchema,\n optionalTrimmedString,\n phoneSchema,\n skillSchema,\n startupStageSchema,\n} from './common.js';\n\n// ─── Age ─────────────────────────────────────────────────────────────────────\n\n/**\n * 18, ruled by the user 2026-09-21. Under-18 would pull in parental-consent\n * duties under the DPDP Act, which is a different and larger problem than a\n * validation rule.\n *\n * ⚠️ The SERVER refuses at finalize. This schema exists so the member sees a\n * useful message; it is not the gate.\n */\nexport const MIN_AGE_YEARS = 18;\n/** A sanity ceiling, not a product rule. Catches a mistyped year. */\nexport const MAX_AGE_YEARS = 120;\n\n/**\n * Whole years completed, by the calendar.\n *\n * ⚠️ NOT `elapsedMs / (365.25 * day)`, which is what the sibling product uses.\n * That approximation reads 17.9986 at the start of somebody's eighteenth\n * birthday and only crosses 18 about halfway through the day, so it refuses\n * them for the half of their birthday they are most likely to be signing up on.\n * Measured: it disagrees with the calendar on all 400 of the next 400 days.\n * An age gate is exactly the place an approximation is not good enough.\n */\nexport function ageInYears(isoDate: string, now: Date = new Date()): number {\n // ⚠️ A STRING, `YYYY-MM-DD`, not a `Date`. This is a wire contract, so it\n // takes the wire type. The backend is the obvious caller and the backend\n // holds a `Date`, so it must format first; passing the Date straight in used\n // to fail with `isoDate.split is not a function`, which says nothing useful.\n // Naming the fix costs one line and saves the next person the detour.\n if (typeof isoDate !== 'string') {\n throw new TypeError(\n 'ageInYears expects a YYYY-MM-DD string, not a Date. Format it first, e.g. date.toISOString().slice(0, 10).',\n );\n }\n const [y, m, d] = isoDate.split('-').map(Number) as [number, number, number];\n let age = now.getUTCFullYear() - y;\n const monthNow = now.getUTCMonth() + 1;\n if (monthNow < m || (monthNow === m && now.getUTCDate() < d)) age -= 1;\n return age;\n}\n\n/** A calendar date with no time component, `YYYY-MM-DD`. */\nexport const isoDateSchema = z\n .string()\n .trim()\n .regex(/^\\d{4}-\\d{2}-\\d{2}$/, 'Expected a YYYY-MM-DD date');\n\n/** True only for a date that exists. Rejects 2026-02-30 and 2026-13-01. */\nfunction isRealDate(iso: string): boolean {\n const [y, m, d] = iso.split('-').map(Number) as [number, number, number];\n if (m < 1 || m > 12 || d < 1) return false;\n const asDate = new Date(Date.UTC(y, m - 1, d));\n return (\n asDate.getUTCFullYear() === y && asDate.getUTCMonth() === m - 1 && asDate.getUTCDate() === d\n );\n}\n\n/**\n * Date of birth, with the age gate applied per parse.\n *\n * ⚠️ THE CLIENT IS NOT THE GATE. This schema exists so the member sees a useful\n * message, and the server must refuse the same dates at finalize regardless.\n * The sibling dating product validates the identical rule on its client and\n * still shipped a defect where an under-age date of birth produced a live,\n * discoverable profile, because the client was the only thing checking.\n */\nexport const dateOfBirthSchema = isoDateSchema\n .refine(isRealDate, { message: 'That date does not exist' })\n .refine((v) => ageInYears(v) >= MIN_AGE_YEARS, {\n message: `You must be at least ${MIN_AGE_YEARS} to join`,\n })\n .refine((v) => ageInYears(v) <= MAX_AGE_YEARS, { message: 'Check the year of birth' });\n\n// ─── Business ────────────────────────────────────────────────────────────────\n\nexport const BUSINESS_TYPE_VALUES = ['startup', 'sme', 'enterprise'] as const;\nexport const businessTypeSchema = z.enum(BUSINESS_TYPE_VALUES);\nexport type BusinessType = z.infer<typeof businessTypeSchema>;\n\n// ─── Topics: the shared vocabulary ───────────────────────────────────────────────────────────────\n\n/**\n * ONE vocabulary, used TWICE: a member picks these as their `interests`, and a\n * post carries them as its `topics`. That is what makes relevance an index\n * lookup rather than a guess, and it is why this list is not called \"interests\"\n * - naming it for one of its two jobs would have invited a second list for the\n * other, which is the mistake this package has already made once with goals.\n * (User's ruling via hary, 2026-09-21.)\n *\n * Deliberately DISTINCT from two neighbours that will otherwise collect the\n * same words:\n *\n * industry what your company does\n * skills what you can do\n * interests what you want to read about\n *\n * If the copy does not keep them apart, members fill all three identically and\n * every one of them stops telling us anything.\n *\n * A closed list, consistent with industry, skill and stage, for the reason\n * already written beside those: free text means two people typing \"growth\" and\n * \"Growth Marketing\" never match, and matching is the entire point. The cost is\n * that adding an interest needs a publish.\n *\n * Tuned down from the dating product's 246, which is mostly dating material.\n * Values reuse its exact strings wherever one existed, so its Hindi\n * translations can be lifted across.\n *\n * ⚠️ Stored per member. Append only; removing a value orphans data.\n */\nexport const TOPIC_VALUES = [\n // Career growth\n 'mentoring',\n 'leadership',\n 'networking',\n 'public_speaking',\n 'teaching',\n 'hiring_talent',\n 'personal_branding',\n 'career_transition',\n 'remote_work',\n // Innovation and tech\n 'ai_ml',\n 'blockchain',\n 'crypto_web3',\n 'robotics',\n 'data_science',\n 'cybersecurity',\n 'game_dev',\n 'ux_design',\n 'app_development',\n 'open_source',\n 'electronics',\n 'space_exploration',\n 'green_tech',\n 'deeptech',\n 'no_code',\n // Business\n 'entrepreneurship',\n 'investing',\n 'stock_trading',\n 'personal_finance',\n 'fundraising',\n 'marketing',\n 'sales',\n 'consulting',\n 'ecommerce',\n 'retail',\n 'real_estate',\n 'supply_chain',\n 'product_management',\n 'growth',\n 'b2b_saas',\n 'd2c',\n // Personal development\n 'meditation',\n 'journaling',\n 'self_improvement',\n 'productivity',\n 'reading',\n 'online_courses',\n 'philosophy',\n 'psychology',\n 'therapy',\n 'sleep_optimization',\n 'gym',\n 'yoga',\n // Creative\n 'photography',\n 'writing',\n 'graphic_design',\n 'filmmaking',\n 'digital_art',\n 'content_creation',\n 'blogging',\n 'vlogging',\n 'podcasts',\n 'animation',\n 'music_production',\n // Impact and community\n 'volunteering',\n 'ngo_work',\n 'environmental',\n 'women_empowerment',\n 'community_service',\n 'social_enterprise',\n 'education_access',\n] as const;\nexport const topicSchema = z.enum(TOPIC_VALUES);\nexport type Topic = z.infer<typeof topicSchema>;\n\n/**\n * The grouping the picker renders.\n *\n * ⚠️ `TOPIC_VALUES` above is the single source of truth and this is typed\n * against it, so a typo here is a compile error rather than a chip that renders\n * and can never be selected. The dating product keeps a categorised list and a\n * flat list side by side and they have already drifted apart by two values;\n * a test in this package asserts these two agree exactly, in both directions.\n */\nexport interface TopicCategory {\n key: string;\n label: string;\n values: readonly Topic[];\n}\n\nexport const TOPIC_CATEGORIES: readonly TopicCategory[] = [\n {\n key: 'career',\n label: 'Career Growth',\n values: [\n 'mentoring',\n 'leadership',\n 'networking',\n 'public_speaking',\n 'teaching',\n 'hiring_talent',\n 'personal_branding',\n 'career_transition',\n 'remote_work',\n ],\n },\n {\n key: 'tech',\n label: 'Innovation & Tech',\n values: [\n 'ai_ml',\n 'blockchain',\n 'crypto_web3',\n 'robotics',\n 'data_science',\n 'cybersecurity',\n 'game_dev',\n 'ux_design',\n 'app_development',\n 'open_source',\n 'electronics',\n 'space_exploration',\n 'green_tech',\n 'deeptech',\n 'no_code',\n ],\n },\n {\n key: 'business',\n label: 'Business',\n values: [\n 'entrepreneurship',\n 'investing',\n 'stock_trading',\n 'personal_finance',\n 'fundraising',\n 'marketing',\n 'sales',\n 'consulting',\n 'ecommerce',\n 'retail',\n 'real_estate',\n 'supply_chain',\n 'product_management',\n 'growth',\n 'b2b_saas',\n 'd2c',\n ],\n },\n {\n key: 'growth',\n label: 'Personal Development',\n values: [\n 'meditation',\n 'journaling',\n 'self_improvement',\n 'productivity',\n 'reading',\n 'online_courses',\n 'philosophy',\n 'psychology',\n 'therapy',\n 'sleep_optimization',\n 'gym',\n 'yoga',\n ],\n },\n {\n key: 'creative',\n label: 'Creative Arts',\n values: [\n 'photography',\n 'writing',\n 'graphic_design',\n 'filmmaking',\n 'digital_art',\n 'content_creation',\n 'blogging',\n 'vlogging',\n 'podcasts',\n 'animation',\n 'music_production',\n ],\n },\n {\n key: 'impact',\n label: 'Impact & Community',\n values: [\n 'volunteering',\n 'ngo_work',\n 'environmental',\n 'women_empowerment',\n 'community_service',\n 'social_enterprise',\n 'education_access',\n ],\n },\n];\n\n/**\n * How many a member must pick. A minimum exists because these drive feed\n * relevance and zero of them means no relevance at all.\n *\n * ⚠️ If 3 turns out to be hard to reach against this list, that is a signal\n * about the LIST, not about the minimum. The sibling product enforces no\n * minimum at all, and its own field config says \"pick up to 10\" while every\n * screen and schema enforces 20.\n */\n/**\n * ⚠️ TRANSLATION KEY SHAPE, for whoever builds the picker:\n * `<fieldKey>.<value>` for a value and `<fieldKey>._label` for the field name.\n * The dating product's interests JSON is keyed by CATEGORY instead, so all five\n * of its label lookups miss and every chip renders its raw English in every\n * locale, with a fully translated Hindi file sitting unreachable. Lift the\n * translations, reshape the keys.\n */\nexport const INTERESTS_MIN = 3;\nexport const INTERESTS_MAX = 10;\n\n// ─── The step list ───────────────────────────────────────────────────────────\n\n/**\n * One step of the wizard.\n *\n * `fields` is what gates leaving this step, and it is why this list and the\n * validation map are the same object: a field that appears in no step is\n * unreachable twice over, because Continue will not surface its error and the\n * final submit's jump-to-the-bad-step finds no step to jump to and leaves the\n * last screen dead.\n */\nexport interface OnboardingStepConfig {\n /** Stable identifier. Safe for analytics and for a resume cursor, unlike an index. */\n key: string;\n title: string;\n helpText: string;\n fields: readonly string[];\n /** False when the whole step may be passed without answering anything. */\n required: boolean;\n}\n\nexport const FOUNDER_ONBOARDING_STEPS: readonly OnboardingStepConfig[] = [\n {\n key: 'identity',\n title: \"We'd love to hear more about you\",\n helpText: 'This is what other founders see first.',\n fields: ['displayName', 'gender', 'dateOfBirth', 'headline', 'about', 'avatarUploadKey'],\n required: true,\n },\n {\n key: 'contact',\n title: 'How can people reach you?',\n helpText: 'Your number is never shown on your profile.',\n fields: ['phone', 'links'],\n required: true,\n },\n {\n key: 'location',\n title: 'Where are you based?',\n helpText: 'Used to show founders near you. You can skip this.',\n fields: ['location'],\n required: false,\n },\n {\n key: 'goals',\n title: 'Goals & Passion',\n helpText: 'Pick what matters most to you.',\n fields: ['goals'],\n required: true,\n },\n {\n key: 'interests',\n title: 'What inspires you the most?',\n helpText: 'We use these to shape your feed.',\n fields: ['interests'],\n required: true,\n },\n {\n key: 'business',\n title: 'Tell us about your business',\n helpText: 'Skip it if you are still figuring things out.',\n fields: ['venture', 'businessType'],\n required: false,\n },\n {\n key: 'guidelines',\n title: 'How we behave here',\n helpText: 'A short read, and the only rule that matters.',\n fields: ['guidelinesVersion'],\n required: true,\n },\n];\n\nexport const ONBOARDING_TOTAL_STEPS = FOUNDER_ONBOARDING_STEPS.length;\n\n/**\n * Fields gating each step, DERIVED from the list above so the two can never\n * disagree.\n */\nexport const ONBOARDING_STEP_FIELDS: readonly (readonly string[])[] = FOUNDER_ONBOARDING_STEPS.map(\n (s) => s.fields,\n);\n\n// ─── Contact ─────────────────────────────────────────────────────────────────\n\n/**\n * Indian mobile number, ten digits, stored raw with no country code, matching\n * the standard used across this family. Display adds `+91`.\n */\nexport const contactLinksSchema = z.object({\n linkedin: httpUrlSchema.optional(),\n x: httpUrlSchema.optional(),\n instagram: httpUrlSchema.optional(),\n});\n\n// ─── The form, mid-flight ────────────────────────────────────────────────────\n\n/**\n * The wizard's working shape.\n *\n * Every choice is nullable because \"not answered yet\" is a real state here and\n * is NOT the same as \"answered with nothing\". Arrays start empty. Nothing in\n * this schema is required, on purpose: it describes a form somebody is halfway\n * through, and the rules that decide whether they may FINISH live in\n * `completeOnboardingBodySchema`.\n */\nexport const onboardingFormSchema = z.object({\n displayName: z.string().trim().max(80),\n gender: genderSchema.nullable(),\n dateOfBirth: z.string().trim(),\n headline: z.string().trim().max(120),\n about: z.string().trim().max(1000),\n avatarUploadKey: z.string().trim().nullable(),\n\n phone: z.string().trim(),\n links: contactLinksSchema,\n\n location: z\n .object({\n city: z.string().trim().max(80),\n state: z.string().trim().max(80),\n country: z.string().trim().max(80),\n })\n .nullable(),\n\n goals: z.array(goalSchema).max(GOAL_VALUES.length),\n interests: z.array(topicSchema).max(INTERESTS_MAX),\n\n venture: z\n .object({\n name: z.string().trim().max(120),\n tagline: z.string().trim().max(160),\n website: z.string().trim(),\n industry: industrySchema.nullable(),\n stage: startupStageSchema.nullable(),\n teamSize: z.string().trim(),\n })\n .nullable(),\n businessType: businessTypeSchema.nullable(),\n skills: z.array(skillSchema).max(8),\n\n guidelinesVersion: z.string().trim().nullable(),\n});\nexport type OnboardingForm = z.infer<typeof onboardingFormSchema>;\n\n// ─── The finalize body ───────────────────────────────────────────────────────\n\n/**\n * What the client sends to finish. Completed answers only.\n *\n * ⚠️ EVERY OPTIONAL FIELD HERE MUST BE ABSENT WHEN UNANSWERED, never a default\n * standing in for one. The sibling gym product sent `bloodGroup ?? 'O+'`, so\n * every member who skipped the question was recorded as O-positive: a real\n * answer, indistinguishable from one somebody gave, sitting on a screen staff\n * would read in an emergency. Absent has to stay absent.\n *\n * ⚠️ AND THIS IS NOT THE GATE EITHER. The server re-checks the age and refuses\n * at finalize. `isOnboarded` may be set here and nowhere else: four of the\n * profile's partial indexes are gated on it, so a half-built profile carrying\n * it true appears in Discover half-built.\n */\nexport const completeOnboardingBodySchema = z.object({\n displayName: z.string().trim().min(1, 'Tell us your name').max(80),\n /**\n * ⚠️ NO `username` HERE, and its absence is deliberate.\n *\n * It used to be required, and that was a defect: no onboarding step collects\n * one, the design draws no screen for one, and it was not even on the\n * in-progress form, so a finalize could never have succeeded. Found by\n * checking every finalize-required field against the step list; a test now\n * asserts that relationship in both directions.\n *\n * The handle is DERIVED SERVER-SIDE at finalize and edited later from the Me\n * tab, which is what the product describes. That also makes the collision\n * case kinder: a server deriving a handle can suffix it and move on, whereas\n * a member who picked one six steps ago can only be told, at the end, that\n * step one failed.\n */\n gender: genderSchema,\n dateOfBirth: dateOfBirthSchema,\n headline: optionalTrimmedString(120),\n about: optionalTrimmedString(1000),\n avatarUploadKey: z.string().trim().min(1).optional(),\n\n phone: phoneSchema,\n links: contactLinksSchema.optional(),\n\n /** Absent when the member skipped the step or refused the permission. */\n location: z\n .object({\n city: z.string().trim().min(1).max(80),\n state: optionalTrimmedString(80),\n country: z.string().trim().min(1).max(80),\n })\n .optional(),\n\n goals: z.array(goalSchema).min(1, 'Pick at least one').max(GOAL_VALUES.length),\n interests: z\n .array(topicSchema)\n .min(INTERESTS_MIN, `Pick at least ${INTERESTS_MIN}`)\n .max(INTERESTS_MAX),\n skills: z.array(skillSchema).max(8).optional(),\n\n /** Absent when the member skipped the business step. */\n venture: z\n .object({\n name: z.string().trim().min(1).max(120),\n tagline: optionalTrimmedString(160),\n website: httpUrlSchema.optional(),\n industry: industrySchema,\n stage: startupStageSchema,\n teamSize: z.coerce.number().int().min(1).max(100000).optional(),\n })\n .optional(),\n businessType: businessTypeSchema.optional(),\n\n /**\n * The version of the guidelines this member read.\n *\n * A version and not a boolean, and the server stamps the time. A boolean says\n * somebody agreed; a version and a timestamp say WHAT they agreed to and\n * WHEN, which is the only form worth anything the day the text changes or a\n * moderation decision is disputed.\n */\n guidelinesVersion: z.string().trim().min(1),\n});\nexport type CompleteOnboardingBody = z.infer<typeof completeOnboardingBodySchema>;\n\n// ─── The resume cursor ───────────────────────────────────────────────────────\n\n/**\n * Where the member got to, kept in its own collection, separate from the\n * profile.\n *\n * `step` is a flat index into `FOUNDER_ONBOARDING_STEPS`, not a `(step, sub)`\n * pair: one number is what lets a member start on the web and resume elsewhere,\n * provided both ends agree on the encoding.\n *\n * `draft` is opaque here on purpose. The client owns the form's shape, and it is\n * re-validated strictly at finalize, so this schema has no business describing\n * it.\n */\nexport const onboardingDraftSchema = z.record(z.string(), z.unknown());\n\nexport const onboardingProgressSchema = z.object({\n step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS),\n draft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingProgress = z.infer<typeof onboardingProgressSchema>;\n\n/** `PUT` body: any subset of the cursor, merged server-side. No defaults. */\nexport const onboardingProgressUpdateSchema = z.object({\n step: z.number().int().min(0).max(ONBOARDING_TOTAL_STEPS).optional(),\n draft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingProgressUpdate = z.infer<typeof onboardingProgressUpdateSchema>;\n\n/**\n * What the client reads on return.\n *\n * ⚠️ `onboarded` is the gate and it comes from the profile, not from the\n * cursor: a finished member on a new device must be recognised even when the\n * cursor row is gone, and a cursor that failed to load must never make an\n * unfinished member look finished.\n */\nexport const onboardingStatusSchema = z.object({\n onboarded: z.boolean(),\n step: z.number().int().min(0),\n draft: onboardingDraftSchema.optional(),\n});\nexport type OnboardingStatus = z.infer<typeof onboardingStatusSchema>;\n"]}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — API response envelope
|
|
3
|
+
*
|
|
4
|
+
* Every Founder Circle endpoint answers in this shape. The clients read the
|
|
5
|
+
* failure reason at `error.message`, so a handler must never return a bare
|
|
6
|
+
* string body.
|
|
7
|
+
*
|
|
8
|
+
* @module founder-schema/responses/common
|
|
9
|
+
*/
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
export declare const apiErrorSchema: z.ZodObject<{
|
|
12
|
+
code: z.ZodString;
|
|
13
|
+
message: z.ZodString;
|
|
14
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type ApiError = z.infer<typeof apiErrorSchema>;
|
|
17
|
+
/** Success envelope. `T` is the payload schema. */
|
|
18
|
+
export declare const successResponseSchema: <T extends z.ZodType>(data: T) => z.ZodObject<{
|
|
19
|
+
success: z.ZodLiteral<true>;
|
|
20
|
+
data: T;
|
|
21
|
+
message: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export declare const errorResponseSchema: z.ZodObject<{
|
|
24
|
+
success: z.ZodLiteral<false>;
|
|
25
|
+
error: z.ZodObject<{
|
|
26
|
+
code: z.ZodString;
|
|
27
|
+
message: z.ZodString;
|
|
28
|
+
details: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString>>>;
|
|
29
|
+
}, z.core.$strip>;
|
|
30
|
+
}, z.core.$strip>;
|
|
31
|
+
export type ErrorResponse = z.infer<typeof errorResponseSchema>;
|
|
32
|
+
/**
|
|
33
|
+
* Cursor-paginated list envelope.
|
|
34
|
+
*
|
|
35
|
+
* `nextCursor` is REQUIRED and is `null` at the end of the list. Omitting the
|
|
36
|
+
* key is not the same thing and does not validate: a handler must decide
|
|
37
|
+
* whether there is another page and say so, because "the key is missing" and
|
|
38
|
+
* "there are no more results" are indistinguishable to a client that has to
|
|
39
|
+
* choose between showing a Load more button and not showing one.
|
|
40
|
+
*
|
|
41
|
+
* An earlier version of this comment said absence and null were equivalent.
|
|
42
|
+
* They never were; the schema has always required the key. Corrected
|
|
43
|
+
* 2026-09-21, and pinned by a test.
|
|
44
|
+
*/
|
|
45
|
+
export declare const paginatedSchema: <T extends z.ZodType>(item: T) => z.ZodObject<{
|
|
46
|
+
items: z.ZodArray<T>;
|
|
47
|
+
nextCursor: z.ZodNullable<z.ZodString>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
//# sourceMappingURL=common.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/responses/common.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,eAAO,MAAM,cAAc;;;;iBAKzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD,mDAAmD;AACnD,eAAO,MAAM,qBAAqB,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC;;;;iBAK9D,CAAC;AAEL,eAAO,MAAM,mBAAmB;;;;;;;iBAG9B,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe,GAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC;;;iBAIxD,CAAC"}
|