founder-schema 0.16.0 → 0.17.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 +138 -2
- package/dist/core/common.d.ts +73 -16
- package/dist/core/common.d.ts.map +1 -1
- package/dist/core/common.js +68 -14
- package/dist/core/common.js.map +1 -1
- package/dist/core/limits.d.ts +7 -0
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +7 -0
- package/dist/core/limits.js.map +1 -1
- package/dist/core/validation.d.ts +26 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +26 -0
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/location/india.d.ts +205 -0
- package/dist/domains/location/india.d.ts.map +1 -0
- package/dist/domains/location/india.js +1175 -0
- package/dist/domains/location/india.js.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts +81 -0
- package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -0
- package/dist/domains/onboarding/onboarding-answers.js +141 -0
- package/dist/domains/onboarding/onboarding-answers.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +178 -14
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding.js +215 -47
- package/dist/domains/onboarding/onboarding.js.map +1 -1
- package/dist/domains/profile/founder-profile.d.ts +94 -6
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +26 -9
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -86,8 +86,12 @@ SAME schema object, so the two can never disagree about what is allowed.
|
|
|
86
86
|
(`ONBOARDING_STEP_SCHEMAS`), so a step the wizard lets through is a step the
|
|
87
87
|
finish accepts. The step list (`FOUNDER_ONBOARDING_STEPS`) is built from those
|
|
88
88
|
slices, including whether a step may be skipped.
|
|
89
|
-
- **No copy.**
|
|
90
|
-
(`
|
|
89
|
+
- **No copy.** A step's label and title, a topic category and a goal category
|
|
90
|
+
are translation keys or plain keys (`labelKey`, `titleKey`, `key`); the words
|
|
91
|
+
live in each client. The web keys its category words by `key`, type-checked
|
|
92
|
+
against it, and its step words by the step's own key (`OnboardingStepKey`);
|
|
93
|
+
`labelKey` / `titleKey` are there for a translated client (i18n), and
|
|
94
|
+
nothing reads them yet.
|
|
91
95
|
- **Bodies are typed JSON; only queries coerce.** `teamSize: "5"` is refused in a
|
|
92
96
|
body, and `?limit=5` is read as 5.
|
|
93
97
|
- **Shapes are derived, never copied.** One `locationSchema`, one
|
|
@@ -99,6 +103,138 @@ instance; this package must not bring its own.
|
|
|
99
103
|
|
|
100
104
|
---
|
|
101
105
|
|
|
106
|
+
## Breaking in 0.17.0: the onboarding photo is saved, and onboarding's single source of truth (2026-10-19)
|
|
107
|
+
|
|
108
|
+
Two pieces of the user's work on onboarding, 2026-10-19.
|
|
109
|
+
|
|
110
|
+
**The photo.** Going back to the photo step showed no photo, though the member
|
|
111
|
+
had pressed Save and continue on it. Every other answer came back from the
|
|
112
|
+
draft; the photo was held for the visit only, so any reload or a resume the
|
|
113
|
+
next day lost it.
|
|
114
|
+
|
|
115
|
+
- **New:** `onboardingStatusSchema.photo` (optional, a `mediaImageSchema`): the
|
|
116
|
+
photo the member saved, as it will appear. Absent when there is none.
|
|
117
|
+
- **Changed meaning:** a step save carrying `draft.avatarMediaId` (an upload id,
|
|
118
|
+
or `null`) is the photo step SAVING its photo. The server holds that upload
|
|
119
|
+
for the draft, so the 24 hour unattached sweep never deletes it, and lets go
|
|
120
|
+
of the one it replaces; `null` lets go of it. It is not kept in the draft:
|
|
121
|
+
`readOnboardingDraft` still gives `null` for it, and the photo comes back only
|
|
122
|
+
as `photo`, from what the server holds.
|
|
123
|
+
- **Stricter:** `onboardingProgressUpdateSchema` refuses a `draft.avatarMediaId`
|
|
124
|
+
that is neither an upload id nor `null` (`invalidId`, at
|
|
125
|
+
`draft.avatarMediaId`). No client sent the key before 0.17.0.
|
|
126
|
+
- **Backend:** a held photo is a media row attached to the member's onboarding
|
|
127
|
+
progress row (attachment kind `onboarding`, backend only). Finish moves it to
|
|
128
|
+
the new profile in the same transaction; a scheduled job lets go of a held
|
|
129
|
+
photo whose progress row the 90 day TTL deleted (Privacy: sign-up answers go
|
|
130
|
+
90 days after they were last changed). New partial index on `media` for that
|
|
131
|
+
job; run `sync-indexes --apply` after deploy.
|
|
132
|
+
- **Deploy order: the backend, then the web.** The web reads the status with
|
|
133
|
+
`z.object`, which drops a key it does not know, so a 0.16 web on a 0.17
|
|
134
|
+
backend simply shows no photo on Back, as today.
|
|
135
|
+
- **Rollback floor:** a backend below 0.17 does not know the `onboarding`
|
|
136
|
+
attachment, so photos held by then stay attached and are never swept until
|
|
137
|
+
0.17 runs again, and a member who finishes on the old build is told the photo
|
|
138
|
+
expired and asked to add it again (the old finish takes only an unattached
|
|
139
|
+
upload). A rollback that is KEPT therefore needs a one-off release of every
|
|
140
|
+
`media` row attached as kind `onboarding`, or those photos outlive the
|
|
141
|
+
Privacy's 90 days (account deletion still erases them).
|
|
142
|
+
|
|
143
|
+
**One source of truth** (the user: "all the data for all the onboarding steps
|
|
144
|
+
and all the errors and validation types ... should come from the founder
|
|
145
|
+
circle schema"). What a client or the backend kept its own copy of now lives
|
|
146
|
+
here, and each consumer reads it:
|
|
147
|
+
|
|
148
|
+
- **New, data:** `GOAL_CATEGORIES` / `GOAL_CATEGORY_KEYS` (the goals step's
|
|
149
|
+
three groups, from the web); `domains/location/india`: `INDIA`,
|
|
150
|
+
`INDIA_DIALING_CODE`, `INDIA_STATES` (each state's cities and other names,
|
|
151
|
+
from the web) with names typed against `GST_STATES` (ONE list of state
|
|
152
|
+
names), `INDIA_STATE_NAMES`, `STATE_ALIASES`, `CITY_ALIASES`,
|
|
153
|
+
`findIndiaState`, `listedCityName`, `canonicalIndiaLocation`;
|
|
154
|
+
`ONBOARDING_DRAFT_RETENTION_DAYS` (90, from the backend's TTL);
|
|
155
|
+
`PHONE_DIGITS`.
|
|
156
|
+
- **New, rules every side applies the same way:** `normalizePhone`,
|
|
157
|
+
`normalizeLinkUrl`, `tidyFullName`, `teamSizeDigits` (from the web);
|
|
158
|
+
`isRealIsoDate`, `todayInIndia`, `dateOfBirthBounds` (a picker's window,
|
|
159
|
+
exact at both ends), `isUnderAgeDateOfBirth` (the draft's age rule, from the
|
|
160
|
+
backend, now with the real-date check), `isoDateToDate` / `dateToIsoDate`;
|
|
161
|
+
`goalListSchema` / `skillListSchema` (one list rule for the finish and Me).
|
|
162
|
+
- **New, from the answers to what is sent** (`domains/onboarding/
|
|
163
|
+
onboarding-answers`, from the web): `emptyOnboardingForm`, `ventureStarted`,
|
|
164
|
+
`onboardingDraftForStep` (exactly a step's own fields), and
|
|
165
|
+
`completeOnboardingBodyFrom` (the finish body: every unanswered optional
|
|
166
|
+
answer absent, each answer cleaned as above), with the
|
|
167
|
+
`CompleteOnboardingBodyCandidate` type; `locationStarted` (a place opened
|
|
168
|
+
but nothing chosen is "no location").
|
|
169
|
+
- **New, the keys of shapes** (so no consumer lists them by hand):
|
|
170
|
+
`FOUNDER_LINK_KEYS` / `FounderLinkKey`, `VENTURE_KEYS` /
|
|
171
|
+
`VENTURE_OPTIONAL_KEYS` / `VentureKey`; types `IndiaLocation`, `IndiaState`,
|
|
172
|
+
`IndiaStateName`, `GoalCategory`.
|
|
173
|
+
- **New message keys:** `guidelinesRequired`, `photoUnavailable`,
|
|
174
|
+
`ventureRequired`, `stateRequired`, `stateUnknown`, `goalsDuplicate`,
|
|
175
|
+
`skillsDuplicate`. Each client words them in the same change (a client
|
|
176
|
+
holding its words as `Record<ValidationMessageKey, string>` fails to build
|
|
177
|
+
until it does).
|
|
178
|
+
- **Stricter:**
|
|
179
|
+
- every location WRITE (`completeOnboardingBodySchema`,
|
|
180
|
+
`updateFounderProfileBodySchema`, `updateMyPersonalDetailsBodySchema`, and
|
|
181
|
+
the server's `suggestedLocationSchema`) is `indiaLocationSchema`: country
|
|
182
|
+
`India`, a state from the list (required: `stateRequired`; not on it:
|
|
183
|
+
`stateUnknown`; read in any common spelling, "KARNATAKA", "Orissa",
|
|
184
|
+
"Jammu & Kashmir", and kept as the list's name), a city. READS keep
|
|
185
|
+
`locationSchema`, so a place stored before this is read as it is;
|
|
186
|
+
- `businessType` / `legalStructure` without a `venture` are each refused ON
|
|
187
|
+
THEIR OWN FIELD (`ventureRequired`), on the finish body and the business
|
|
188
|
+
step; the backend used to drop them without a word, and the web dropped a
|
|
189
|
+
lone registration (hary's ruling: no silent drop). `ventureStarted` no
|
|
190
|
+
longer counts the type: it describes a business, it does not start one;
|
|
191
|
+
- `displayNameSchema` stores the name tidied (`tidyFullName`): every body
|
|
192
|
+
that writes a name, the Me patches included;
|
|
193
|
+
- goals and skills each once (`goalsDuplicate`, `skillsDuplicate`), on the
|
|
194
|
+
finish and the Me patch;
|
|
195
|
+
- the guidelines: a box left unticked is `guidelinesRequired` (it was
|
|
196
|
+
`guidelinesOutdated`, the words for an older text).
|
|
197
|
+
- **Breaking:** `completeOnboardingBodySchema` and
|
|
198
|
+
`ONBOARDING_STEP_SCHEMAS.business` carry the venture rule (refinements), so
|
|
199
|
+
Zod 4.6 throws on `.pick`, `.omit`, `.partial` and `.merge` of them, and on
|
|
200
|
+
an `.extend` that overwrites a key. Read a field with `.shape.<field>`, and a
|
|
201
|
+
step's fields with `ONBOARDING_STEP_SCHEMAS.<step>` (picked before the rule;
|
|
202
|
+
the business slice carries it again). The web picked from the finish body in
|
|
203
|
+
three places (the `/dummy` showcase, at module scope, and two tests); they
|
|
204
|
+
read `.shape` now.
|
|
205
|
+
- **Changed type:** `displayNameSchema` is a pipe (`.transform(tidyFullName)`),
|
|
206
|
+
no longer a `ZodString`, so `.min` / `.max` / `.regex` cannot be chained on
|
|
207
|
+
it. Nothing did; a length is read through the pipe.
|
|
208
|
+
- **Breaking (no consumer reads it):** `FOUNDER_ONBOARDING_STEPS[i].helpKey`
|
|
209
|
+
is gone (there is no help line any more; the user, 2026-10-19), and
|
|
210
|
+
`labelKey` names the step's short label.
|
|
211
|
+
- **Moved, same export name:** `suggestedLocationSchema` / `SuggestedLocation`
|
|
212
|
+
now come from `domains/location/india`.
|
|
213
|
+
- **Deploy order: still the backend first.** A 0.16 web is refused a little
|
|
214
|
+
that it used to send, on its field: a place with no state (an old draft's,
|
|
215
|
+
or the 0.16 Me editor saving an OLD stateless place, which it sends without
|
|
216
|
+
one) at `location.state`. (It never sends a lone business type: its
|
|
217
|
+
`ventureStarted` counted the type, so a type on its own went with an empty
|
|
218
|
+
venture, refused at `venture.name` / `industry` / `stage` as on 0.16.) A
|
|
219
|
+
0.17 web needs the 0.17 backend for the photo. It reads the location
|
|
220
|
+
suggestion with the strict write shape, which takes a state in any common
|
|
221
|
+
spelling ("KARNATAKA"), so a 0.16 backend's answer is read unless it has no
|
|
222
|
+
state, or one the list does not know: then it is no suggestion.
|
|
223
|
+
- **Rollback floor (also):** the 0.17 web on a backend rolled below 0.17 saves
|
|
224
|
+
no photo, and shows no suggestion where the old backend answered without a
|
|
225
|
+
state the list knows.
|
|
226
|
+
- **Known, follow-up (hary, 2026-10-19), not in 0.17.0:**
|
|
227
|
+
- a step save racing a finish when the member has NO progress row (the TTL
|
|
228
|
+
deleted it while a tab stayed open) can leave a photo held for a finished
|
|
229
|
+
member, until the TTL takes that new row: the finish's update of a row
|
|
230
|
+
that is not there writes nothing, so the two transactions never conflict.
|
|
231
|
+
Fix: the finish upserts its completion stamp;
|
|
232
|
+
- finish's retry loop sets its result inside the transaction callback, so a
|
|
233
|
+
commit that fails and a retry that then meets a username clash could
|
|
234
|
+
answer success for a profile never committed. Fix: reset the result on
|
|
235
|
+
every attempt. Do both together: an 11000 on that upsert would otherwise
|
|
236
|
+
read as a username clash.
|
|
237
|
+
|
|
102
238
|
## Breaking in 0.16.0: onboarding in nine steps (2026-10-18)
|
|
103
239
|
|
|
104
240
|
The user's rework of onboarding. The finish body takes the same answers plus one.
|
package/dist/core/common.d.ts
CHANGED
|
@@ -78,6 +78,14 @@ export type SafeUrl = z.infer<typeof safeUrlSchema>;
|
|
|
78
78
|
*/
|
|
79
79
|
export declare const httpUrlSchema: z.ZodURL;
|
|
80
80
|
export type HttpUrl = z.infer<typeof httpUrlSchema>;
|
|
81
|
+
/**
|
|
82
|
+
* A link as people paste it, made into what `httpUrlSchema` checks (0.17.0,
|
|
83
|
+
* moved here from the web so every client cleans a link the same way):
|
|
84
|
+
* `linkedin.com/in/asha` gains `https://`; blank stays blank. Something that
|
|
85
|
+
* already names a scheme keeps it, so `javascript:` is never quietly "fixed"
|
|
86
|
+
* into a link: the schema then refuses it, as it only takes http and https.
|
|
87
|
+
*/
|
|
88
|
+
export declare function normalizeLinkUrl(raw: string): string;
|
|
81
89
|
/**
|
|
82
90
|
* True when `url` parses and its hostname is EXACTLY one of `hosts`
|
|
83
91
|
* (lowercase). The one host check in this package, written the way the warning
|
|
@@ -265,6 +273,50 @@ export declare const goalSchema: z.ZodEnum<{
|
|
|
265
273
|
attend_events: "attend_events";
|
|
266
274
|
}>;
|
|
267
275
|
export type Goal = z.infer<typeof goalSchema>;
|
|
276
|
+
/**
|
|
277
|
+
* A member's goals as a list (0.17.0): each goal once, at most every goal.
|
|
278
|
+
* ONE rule for onboarding's finish (which adds its minimum, `GOALS_MIN`) and
|
|
279
|
+
* the Me tab's editor: two copies of one goal met the minimum while meaning
|
|
280
|
+
* one, and the profile would show it twice.
|
|
281
|
+
*/
|
|
282
|
+
export declare const goalListSchema: z.ZodArray<z.ZodEnum<{
|
|
283
|
+
find_cofounder: "find_cofounder";
|
|
284
|
+
find_investors: "find_investors";
|
|
285
|
+
find_customers: "find_customers";
|
|
286
|
+
hiring: "hiring";
|
|
287
|
+
partnerships: "partnerships";
|
|
288
|
+
mentorship: "mentorship";
|
|
289
|
+
grow_network: "grow_network";
|
|
290
|
+
business_ideas: "business_ideas";
|
|
291
|
+
market_insights: "market_insights";
|
|
292
|
+
learn_startup: "learn_startup";
|
|
293
|
+
start_company: "start_company";
|
|
294
|
+
get_feedback: "get_feedback";
|
|
295
|
+
find_testers: "find_testers";
|
|
296
|
+
mentor_others: "mentor_others";
|
|
297
|
+
invest_in_startups: "invest_in_startups";
|
|
298
|
+
join_startup: "join_startup";
|
|
299
|
+
find_service_providers: "find_service_providers";
|
|
300
|
+
attend_events: "attend_events";
|
|
301
|
+
}>>;
|
|
302
|
+
/** A member's skills (0.17.0): each skill once, at most `SKILLS_MAX`. ONE rule for onboarding and Me. */
|
|
303
|
+
export declare const skillListSchema: z.ZodArray<z.ZodEnum<{
|
|
304
|
+
engineering: "engineering";
|
|
305
|
+
product: "product";
|
|
306
|
+
design: "design";
|
|
307
|
+
data: "data";
|
|
308
|
+
growth: "growth";
|
|
309
|
+
sales: "sales";
|
|
310
|
+
marketing: "marketing";
|
|
311
|
+
operations: "operations";
|
|
312
|
+
finance: "finance";
|
|
313
|
+
legal: "legal";
|
|
314
|
+
hr_people: "hr_people";
|
|
315
|
+
supply_chain: "supply_chain";
|
|
316
|
+
research: "research";
|
|
317
|
+
fundraising: "fundraising";
|
|
318
|
+
community: "community";
|
|
319
|
+
}>>;
|
|
268
320
|
/**
|
|
269
321
|
* Demographic only. Nothing here computes anything from it, unlike the sibling
|
|
270
322
|
* gym product where gender feeds body-metric maths.
|
|
@@ -281,14 +333,22 @@ export declare const genderSchema: z.ZodEnum<{
|
|
|
281
333
|
prefer_not_to_say: "prefer_not_to_say";
|
|
282
334
|
}>;
|
|
283
335
|
export type Gender = z.infer<typeof genderSchema>;
|
|
336
|
+
/**
|
|
337
|
+
* A full name as it is sent and stored (0.17.0, moved here from the web):
|
|
338
|
+
* trimmed, and every run of spaces made one. `displayNameSchema` applies it,
|
|
339
|
+
* so the server stores it from every client; a client applies it to show the
|
|
340
|
+
* member what will be kept. What `splitFullName` reads.
|
|
341
|
+
*/
|
|
342
|
+
export declare const tidyFullName: (fullName: string) => string;
|
|
284
343
|
/**
|
|
285
344
|
* The name a member is shown by. ONE rule for every body that writes it:
|
|
286
345
|
* onboarding's finish, the profile patch and the personal-details patch, so a
|
|
287
|
-
* name one accepts the others can never refuse
|
|
288
|
-
*
|
|
289
|
-
*
|
|
346
|
+
* name one accepts the others can never refuse, and each stores it TIDIED
|
|
347
|
+
* (`tidyFullName`, 0.17.0: "Asha Rao" from the Me editor was stored as
|
|
348
|
+
* typed, while onboarding sent it tidy). Plain bounds, no custom message:
|
|
349
|
+
* `too_small` with minimum 1 already says "required", and the client words it.
|
|
290
350
|
*/
|
|
291
|
-
export declare const displayNameSchema: z.ZodString
|
|
351
|
+
export declare const displayNameSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
292
352
|
/**
|
|
293
353
|
* A full name's first and last name, DEDUCED, never asked (the user,
|
|
294
354
|
* 2026-10-18: onboarding asks one "Full name" and works the two out from the
|
|
@@ -307,6 +367,8 @@ export declare function splitFullName(fullName: string): {
|
|
|
307
367
|
firstName: string;
|
|
308
368
|
lastName: string;
|
|
309
369
|
};
|
|
370
|
+
/** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */
|
|
371
|
+
export declare const PHONE_DIGITS = 10;
|
|
310
372
|
/**
|
|
311
373
|
* Indian mobile number: ten raw digits, no country code, matching the standard
|
|
312
374
|
* used across this family. Display adds the `+91`.
|
|
@@ -318,6 +380,13 @@ export declare function splitFullName(fullName: string): {
|
|
|
318
380
|
*/
|
|
319
381
|
export declare const phoneSchema: z.ZodString;
|
|
320
382
|
export type Phone = z.infer<typeof phoneSchema>;
|
|
383
|
+
/**
|
|
384
|
+
* An Indian mobile number as `phoneSchema`'s ten digits, from however it was
|
|
385
|
+
* typed (0.17.0, moved here from the web): `98765 43210`, `+91 98765-43210`,
|
|
386
|
+
* `091 9876543210`. Anything else comes back digits only and fails the schema
|
|
387
|
+
* with `phoneInvalid`, rather than being guessed at.
|
|
388
|
+
*/
|
|
389
|
+
export declare function normalizePhone(raw: string): string;
|
|
321
390
|
/**
|
|
322
391
|
* Where the founder is based. City and country are what Discover filters on.
|
|
323
392
|
*
|
|
@@ -354,18 +423,6 @@ export declare const coordinatesQuerySchema: z.ZodObject<{
|
|
|
354
423
|
}, z.core.$strip>;
|
|
355
424
|
export type CoordinatesQuery = z.infer<typeof coordinatesQuerySchema>;
|
|
356
425
|
export type CoordinatesQueryInput = z.input<typeof coordinatesQuerySchema>;
|
|
357
|
-
/**
|
|
358
|
-
* The city these coordinates look like, for the member to CONFIRM, or `null`
|
|
359
|
-
* when the lookup could not place them confidently (or they are outside India,
|
|
360
|
-
* whose area data is all there is). A suggestion, never a write: only what the
|
|
361
|
-
* member then submits becomes their location.
|
|
362
|
-
*/
|
|
363
|
-
export declare const suggestedLocationSchema: z.ZodNullable<z.ZodObject<{
|
|
364
|
-
city: z.ZodString;
|
|
365
|
-
state: z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
366
|
-
country: z.ZodString;
|
|
367
|
-
}, z.core.$strip>>;
|
|
368
|
-
export type SuggestedLocation = z.infer<typeof suggestedLocationSchema>;
|
|
369
426
|
/**
|
|
370
427
|
* Cursor pagination query. Feed and Discover both use it.
|
|
371
428
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;
|
|
1
|
+
{"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"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"}
|
package/dist/core/common.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module founder-schema/core/common
|
|
8
8
|
*/
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
-
import { CITY_MAX, COUNTRY_MAX, DISPLAY_NAME_MAX, LINK_URL_MAX, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, STATE_MAX, } from './limits.js';
|
|
10
|
+
import { CITY_MAX, COUNTRY_MAX, DISPLAY_NAME_MAX, LINK_URL_MAX, PAGE_SIZE_DEFAULT, PAGE_SIZE_MAX, SKILLS_MAX, STATE_MAX, } from './limits.js';
|
|
11
11
|
import { VALIDATION_MESSAGE } from './validation.js';
|
|
12
12
|
// ─── String helpers ──────────────────────────────────────────────────────────
|
|
13
13
|
/**
|
|
@@ -94,6 +94,19 @@ export const safeUrlSchema = z.url({
|
|
|
94
94
|
* `imageUrl` would have failed a whole feed page.
|
|
95
95
|
*/
|
|
96
96
|
export const httpUrlSchema = safeUrlSchema.max(LINK_URL_MAX);
|
|
97
|
+
/**
|
|
98
|
+
* A link as people paste it, made into what `httpUrlSchema` checks (0.17.0,
|
|
99
|
+
* moved here from the web so every client cleans a link the same way):
|
|
100
|
+
* `linkedin.com/in/asha` gains `https://`; blank stays blank. Something that
|
|
101
|
+
* already names a scheme keeps it, so `javascript:` is never quietly "fixed"
|
|
102
|
+
* into a link: the schema then refuses it, as it only takes http and https.
|
|
103
|
+
*/
|
|
104
|
+
export function normalizeLinkUrl(raw) {
|
|
105
|
+
const trimmed = raw.trim();
|
|
106
|
+
if (!trimmed)
|
|
107
|
+
return '';
|
|
108
|
+
return /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
109
|
+
}
|
|
97
110
|
/**
|
|
98
111
|
* True when `url` parses and its hostname is EXACTLY one of `hosts`
|
|
99
112
|
* (lowercase). The one host check in this package, written the way the warning
|
|
@@ -343,6 +356,23 @@ export const GOAL_VALUES = [
|
|
|
343
356
|
'attend_events',
|
|
344
357
|
];
|
|
345
358
|
export const goalSchema = z.enum(GOAL_VALUES);
|
|
359
|
+
/** True when no value is in the list twice. */
|
|
360
|
+
const eachOnce = (items) => new Set(items).size === items.length;
|
|
361
|
+
/**
|
|
362
|
+
* A member's goals as a list (0.17.0): each goal once, at most every goal.
|
|
363
|
+
* ONE rule for onboarding's finish (which adds its minimum, `GOALS_MIN`) and
|
|
364
|
+
* the Me tab's editor: two copies of one goal met the minimum while meaning
|
|
365
|
+
* one, and the profile would show it twice.
|
|
366
|
+
*/
|
|
367
|
+
export const goalListSchema = z
|
|
368
|
+
.array(goalSchema)
|
|
369
|
+
.max(GOAL_VALUES.length)
|
|
370
|
+
.refine(eachOnce, { error: VALIDATION_MESSAGE.goalsDuplicate });
|
|
371
|
+
/** A member's skills (0.17.0): each skill once, at most `SKILLS_MAX`. ONE rule for onboarding and Me. */
|
|
372
|
+
export const skillListSchema = z
|
|
373
|
+
.array(skillSchema)
|
|
374
|
+
.max(SKILLS_MAX)
|
|
375
|
+
.refine(eachOnce, { error: VALIDATION_MESSAGE.skillsDuplicate });
|
|
346
376
|
// ─── Gender ──────────────────────────────────────────────────────────────────
|
|
347
377
|
/**
|
|
348
378
|
* Demographic only. Nothing here computes anything from it, unlike the sibling
|
|
@@ -355,14 +385,27 @@ export const goalSchema = z.enum(GOAL_VALUES);
|
|
|
355
385
|
export const GENDER_VALUES = ['male', 'female', 'other', 'prefer_not_to_say'];
|
|
356
386
|
export const genderSchema = z.enum(GENDER_VALUES);
|
|
357
387
|
// ─── Display name ────────────────────────────────────────────────────────────
|
|
388
|
+
/**
|
|
389
|
+
* A full name as it is sent and stored (0.17.0, moved here from the web):
|
|
390
|
+
* trimmed, and every run of spaces made one. `displayNameSchema` applies it,
|
|
391
|
+
* so the server stores it from every client; a client applies it to show the
|
|
392
|
+
* member what will be kept. What `splitFullName` reads.
|
|
393
|
+
*/
|
|
394
|
+
export const tidyFullName = (fullName) => fullName.trim().replace(/\s+/g, ' ');
|
|
358
395
|
/**
|
|
359
396
|
* The name a member is shown by. ONE rule for every body that writes it:
|
|
360
397
|
* onboarding's finish, the profile patch and the personal-details patch, so a
|
|
361
|
-
* name one accepts the others can never refuse
|
|
362
|
-
*
|
|
363
|
-
*
|
|
398
|
+
* name one accepts the others can never refuse, and each stores it TIDIED
|
|
399
|
+
* (`tidyFullName`, 0.17.0: "Asha Rao" from the Me editor was stored as
|
|
400
|
+
* typed, while onboarding sent it tidy). Plain bounds, no custom message:
|
|
401
|
+
* `too_small` with minimum 1 already says "required", and the client words it.
|
|
364
402
|
*/
|
|
365
|
-
export const displayNameSchema = z
|
|
403
|
+
export const displayNameSchema = z
|
|
404
|
+
.string()
|
|
405
|
+
.trim()
|
|
406
|
+
.min(1)
|
|
407
|
+
.max(DISPLAY_NAME_MAX)
|
|
408
|
+
.transform(tidyFullName);
|
|
366
409
|
/**
|
|
367
410
|
* A full name's first and last name, DEDUCED, never asked (the user,
|
|
368
411
|
* 2026-10-18: onboarding asks one "Full name" and works the two out from the
|
|
@@ -378,13 +421,15 @@ export const displayNameSchema = z.string().trim().min(1).max(DISPLAY_NAME_MAX);
|
|
|
378
421
|
* leave a stale first name behind.
|
|
379
422
|
*/
|
|
380
423
|
export function splitFullName(fullName) {
|
|
381
|
-
const name = fullName
|
|
424
|
+
const name = tidyFullName(fullName);
|
|
382
425
|
const space = name.indexOf(' ');
|
|
383
426
|
return space === -1
|
|
384
427
|
? { firstName: name, lastName: '' }
|
|
385
428
|
: { firstName: name.slice(0, space), lastName: name.slice(space + 1) };
|
|
386
429
|
}
|
|
387
430
|
// ─── Phone ───────────────────────────────────────────────────────────────────
|
|
431
|
+
/** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */
|
|
432
|
+
export const PHONE_DIGITS = 10;
|
|
388
433
|
/**
|
|
389
434
|
* Indian mobile number: ten raw digits, no country code, matching the standard
|
|
390
435
|
* used across this family. Display adds the `+91`.
|
|
@@ -397,7 +442,23 @@ export function splitFullName(fullName) {
|
|
|
397
442
|
export const phoneSchema = z
|
|
398
443
|
.string()
|
|
399
444
|
.trim()
|
|
400
|
-
.regex(
|
|
445
|
+
.regex(new RegExp(`^[6-9]\\d{${PHONE_DIGITS - 1}}$`), VALIDATION_MESSAGE.phoneInvalid);
|
|
446
|
+
/**
|
|
447
|
+
* An Indian mobile number as `phoneSchema`'s ten digits, from however it was
|
|
448
|
+
* typed (0.17.0, moved here from the web): `98765 43210`, `+91 98765-43210`,
|
|
449
|
+
* `091 9876543210`. Anything else comes back digits only and fails the schema
|
|
450
|
+
* with `phoneInvalid`, rather than being guessed at.
|
|
451
|
+
*/
|
|
452
|
+
export function normalizePhone(raw) {
|
|
453
|
+
let digits = raw.replace(/\D/g, '');
|
|
454
|
+
if (digits.length === PHONE_DIGITS + 2 && digits.startsWith('91'))
|
|
455
|
+
digits = digits.slice(2);
|
|
456
|
+
else if (digits.length === PHONE_DIGITS + 1 && digits.startsWith('0'))
|
|
457
|
+
digits = digits.slice(1);
|
|
458
|
+
else if (digits.length === PHONE_DIGITS + 3 && digits.startsWith('091'))
|
|
459
|
+
digits = digits.slice(3);
|
|
460
|
+
return digits;
|
|
461
|
+
}
|
|
401
462
|
// ─── Location ────────────────────────────────────────────────────────────────
|
|
402
463
|
/**
|
|
403
464
|
* Where the founder is based. City and country are what Discover filters on.
|
|
@@ -432,13 +493,6 @@ export const coordinatesQuerySchema = z.object({
|
|
|
432
493
|
lat: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-90).max(90)),
|
|
433
494
|
lng: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-180).max(180)),
|
|
434
495
|
});
|
|
435
|
-
/**
|
|
436
|
-
* The city these coordinates look like, for the member to CONFIRM, or `null`
|
|
437
|
-
* when the lookup could not place them confidently (or they are outside India,
|
|
438
|
-
* whose area data is all there is). A suggestion, never a write: only what the
|
|
439
|
-
* member then submits becomes their location.
|
|
440
|
-
*/
|
|
441
|
-
export const suggestedLocationSchema = locationSchema.nullable();
|
|
442
496
|
// ─── Pagination ──────────────────────────────────────────────────────────────
|
|
443
497
|
/**
|
|
444
498
|
* Cursor pagination query. Feed and Discover both use it.
|
package/dist/core/common.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/core/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/core/common\n */\n\nimport { z } from 'zod';\nimport {\n CITY_MAX,\n COUNTRY_MAX,\n DISPLAY_NAME_MAX,\n LINK_URL_MAX,\n PAGE_SIZE_DEFAULT,\n PAGE_SIZE_MAX,\n STATE_MAX,\n} from './limits.js';\nimport { VALIDATION_MESSAGE } from './validation.js';\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/**\n * A repeated query param (`?skill=growth&skill=sales`) or a single value,\n * always as an array, at most `max` values.\n *\n * Express gives `string` for one occurrence and `string[]` for several, so\n * without this the same URL shape parses two different ways. EMPTY values are\n * dropped (`?industry=` is a select the member just reset): asking for fewer\n * filters is never a 400.\n */\nexport const multiValueQuery = <T extends z.ZodType>(inner: T, max: number) =>\n z.preprocess((value) => {\n if (value === undefined || value === null) return undefined;\n const list = (Array.isArray(value) ? value : [value]).filter((v) => v !== '');\n return list.length > 0 ? list : undefined;\n }, z.array(inner).max(max).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, VALIDATION_MESSAGE.invalidId)\n .transform((v) => v.toLowerCase());\n/** Named for what it is on the wire, so it never collides with Mongoose's `ObjectId`. */\nexport type ObjectIdString = z.infer<typeof objectIdSchema>;\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 safeUrlSchema = z.url({\n protocol: /^https?$/,\n hostname: z.regexes.domain,\n error: VALIDATION_MESSAGE.urlInvalid,\n});\nexport type SafeUrl = z.infer<typeof safeUrlSchema>;\n\n/**\n * A URL a member TYPES: `safeUrlSchema` plus the `LINK_URL_MAX` ceiling, so\n * every member URL carries the same cap and no consumer has to remember it.\n *\n * Two schemas, not one, because the cap is about what a person types and is\n * wrong for what the server reads from elsewhere. Until 0.2.0 there was only\n * this one, and it capped a link preview's CDN image URL too: one 314-character\n * `imageUrl` would have failed a whole feed page.\n */\nexport const httpUrlSchema = safeUrlSchema.max(LINK_URL_MAX);\nexport type HttpUrl = z.infer<typeof httpUrlSchema>;\n\n/**\n * True when `url` parses and its hostname is EXACTLY one of `hosts`\n * (lowercase). The one host check in this package, written the way the warning\n * on `safeUrlSchema` demands: parsed, compared for equality, never matched as\n * text. `youtube.com.evil.example` and `evil.example/?u=youtube.com` are refused.\n */\nexport function isUrlOnHost(url: string, hosts: readonly string[]): boolean {\n try {\n return hosts.includes(new URL(url).hostname.toLowerCase());\n } catch {\n return false;\n }\n}\n\n/** A typed URL (`httpUrlSchema`) that must also live on one of `hosts`. */\nexport const httpUrlOnHosts = (hosts: readonly string[], message: string) =>\n httpUrlSchema.refine((url) => isUrlOnHost(url, hosts), { error: message });\n\n/**\n * What a product needs before it can be listed in Discover (0.6.0). Here, not\n * in the products domain, because an error's details carry it\n * (`PRODUCT_NOT_LISTABLE` -> `details.missing`) and core never imports a domain.\n */\nexport const PRODUCT_LISTING_REQUIREMENT_VALUES = ['logo', 'tagline', 'screenshot'] as const;\nexport const productListingRequirementSchema = z.enum(PRODUCT_LISTING_REQUIREMENT_VALUES);\nexport type ProductListingRequirement = z.infer<typeof productListingRequirementSchema>;\n\n/**\n * The hosts a shared video may come from. The user's ruling (2026-09-21): video\n * is shared as a link and played in the card, never uploaded. The web plays it\n * through youtube-nocookie.com, the one video frame its CSP admits.\n */\nexport const YOUTUBE_HOSTS = [\n 'youtube.com',\n 'www.youtube.com',\n 'm.youtube.com',\n 'youtu.be',\n // The privacy-mode player's own links (0.7.0).\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n] as const;\n\n/**\n * The 11-character id of a YouTube VIDEO link, or null for anything else (a\n * channel, a playlist, the home page, another site): what a page can embed.\n * Reads `youtu.be/<id>`, `/watch?v=<id>`, and `/shorts/`, `/embed/`, `/live/<id>`\n * on a `YOUTUBE_HOSTS` host (0.7.0).\n */\nexport function youtubeVideoId(url: string): string | null {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n return null;\n }\n const host = u.hostname.toLowerCase();\n if (!(YOUTUBE_HOSTS as readonly string[]).includes(host)) return null;\n const id =\n host === 'youtu.be'\n ? (u.pathname.split('/')[1] ?? null)\n : u.pathname === '/watch'\n ? u.searchParams.get('v')\n : (/^\\/(?:shorts|embed|live)\\/([^/]+)/.exec(u.pathname)?.[1] ?? null);\n return id && /^[A-Za-z0-9_-]{11}$/.test(id) ? id : null;\n}\n\n// ─── Username ────────────────────────────────────────────────────────────────\n\n/**\n * Public handle. Derived by the server at onboarding, changeable 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.\n *\n * This is the FORMAT, and it is what every read shape uses. A handle being\n * CHOSEN must also not be reserved: that is `chosenUsernameSchema`.\n */\nexport const USERNAME_MIN = 3;\nexport const USERNAME_MAX = 30;\n\nexport const usernameSchema = z\n .string()\n .trim()\n .toLowerCase()\n // Plain bounds: the client words them from the issue's own minimum/maximum.\n .min(USERNAME_MIN)\n .max(USERNAME_MAX)\n .regex(/^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)?$/, VALIDATION_MESSAGE.usernameInvalid);\nexport type Username = z.infer<typeof usernameSchema>;\n\n/**\n * Handles this product keeps for itself: route names a handle would shadow,\n * and words that would let a member pass as staff.\n *\n * ⚠ HERE, IN THE CONTRACT, since 0.2.0. It used to live in the backend so it\n * could grow without a publish, and nothing ever read it: the server derived\n * handles without checking it, so a member called \"Support\" was handed\n * `support`. One list, read by the server when it derives a handle and by any\n * form that lets a member choose one, is worth a publish per addition.\n *\n * Matched exactly, after lowercasing.\n */\nexport const RESERVED_USERNAMES = [\n 'admin',\n 'about',\n 'api',\n 'chat',\n 'discover',\n 'feed',\n 'founder',\n 'foundercircle',\n 'help',\n 'home',\n 'me',\n 'meetup',\n 'moderator',\n 'onboarding',\n 'settings',\n 'staff',\n 'superadmin',\n 'support',\n 'system',\n 'team',\n] as const;\n\nconst RESERVED = new Set<string>(RESERVED_USERNAMES);\n\nexport function isReservedUsername(handle: string): boolean {\n return RESERVED.has(handle.trim().toLowerCase());\n}\n\n/**\n * A handle a member is CHOOSING: the format, and not reserved.\n *\n * ⚠ Not used on read shapes, deliberately. A handle stored before the reserved\n * check existed must still READ, or one member's old handle would fail every\n * page that shows their name.\n */\nexport const chosenUsernameSchema = usernameSchema.refine((u) => !isReservedUsername(u), {\n error: VALIDATION_MESSAGE.usernameReserved,\n});\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. The others were all asks of other\n * people and this one an ask of the product, which is the whole of the oddness.\n * (hary, 2026-09-21.) Since 0.16.0 the list also holds offers (`mentor_others`,\n * `invest_in_startups`) and a second ask of the product (`attend_events`).\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 // 0.16.0 (the user, 2026-10-18: \"a few more ... cover most of the goals\"):\n // the journey's own asks (feedback on an idea, early testers), the other\n // side of mentorship and of fundraising (mentoring, investing), and three\n // more a founder network is asked for: joining a startup, finding\n // service providers, and events. Appended, per the rule above.\n 'get_feedback',\n 'find_testers',\n 'mentor_others',\n 'invest_in_startups',\n 'join_startup',\n 'find_service_providers',\n 'attend_events',\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// ─── Display name ────────────────────────────────────────────────────────────\n\n/**\n * The name a member is shown by. ONE rule for every body that writes it:\n * onboarding's finish, the profile patch and the personal-details patch, so a\n * name one accepts the others can never refuse. Plain bounds, no custom\n * message: `too_small` with minimum 1 already says \"required\", and the client\n * words it.\n */\nexport const displayNameSchema = z.string().trim().min(1).max(DISPLAY_NAME_MAX);\n\n/**\n * A full name's first and last name, DEDUCED, never asked (the user,\n * 2026-10-18: onboarding asks one \"Full name\" and works the two out from the\n * space). The first word is the first name and everything after the first\n * space is the last name, kept as written (\"Asha Rao Kulkarni\" is Asha and\n * Rao Kulkarni).\n *\n * ⚠ A ONE-WORD NAME IS A WHOLE NAME. Many people in India go by a single name,\n * so `lastName` is then `''`, and nothing may refuse it for that.\n *\n * Derived wherever it is needed, never stored as a second copy of the name:\n * `displayName` stays the one owner of a member's name, so a rename can never\n * leave a stale first name behind.\n */\nexport function splitFullName(fullName: string): { firstName: string; lastName: string } {\n const name = fullName.trim().replace(/\\s+/g, ' ');\n const space = name.indexOf(' ');\n return space === -1\n ? { firstName: name, lastName: '' }\n : { firstName: name.slice(0, space), lastName: name.slice(space + 1) };\n}\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}$/, VALIDATION_MESSAGE.phoneInvalid);\nexport type Phone = z.infer<typeof phoneSchema>;\n\n// ─── Location ────────────────────────────────────────────────────────────────\n\n/**\n * Where the founder is based. City and country are what Discover filters on.\n *\n * ONE SHAPE for the profile, the Me tab's patch, onboarding's finish and the\n * location suggestion. It was written out three times until 0.2.0, the same\n * kind of copy that let the profile model lose Instagram.\n *\n * ⚠ `country` is REQUIRED and has NO default. A default here (it used to be\n * `'India'`) is destructive on every write path: a founder in Berlin who\n * corrects only their city would be moved to India by a value they never saw.\n * A body that carries a location carries the whole location. Every stored\n * location has a country, because the model requires one, so the read side\n * loses nothing.\n *\n * This is a DECLARED location, not a GPS reading. Jansathi's rule applies\n * 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(CITY_MAX),\n state: optionalTrimmedString(STATE_MAX),\n country: z.string().trim().min(1).max(COUNTRY_MAX),\n});\nexport type FounderLocation = z.infer<typeof locationSchema>;\n\n/**\n * `GET /api/v1/location/areas/reverse?lat=&lng=`: plain decimal degrees.\n *\n * A QUERY, so coercion is right here: everything in a URL is a string. An\n * emptied parameter is \"missing\", never 0, because 0,0 is a real place in the\n * Gulf of Guinea and would be looked up rather than refused.\n */\nexport const coordinatesQuerySchema = z.object({\n lat: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-90).max(90)),\n lng: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-180).max(180)),\n});\nexport type CoordinatesQuery = z.infer<typeof coordinatesQuerySchema>;\nexport type CoordinatesQueryInput = z.input<typeof coordinatesQuerySchema>;\n\n/**\n * The city these coordinates look like, for the member to CONFIRM, or `null`\n * when the lookup could not place them confidently (or they are outside India,\n * whose area data is all there is). A suggestion, never a write: only what the\n * member then submits becomes their location.\n */\nexport const suggestedLocationSchema = locationSchema.nullable();\nexport type SuggestedLocation = z.infer<typeof suggestedLocationSchema>;\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(\n emptyStringToUndefined,\n z.coerce.number().int().min(1).max(PAGE_SIZE_MAX).default(PAGE_SIZE_DEFAULT),\n ),\n cursor: z.string().trim().min(1).optional(),\n});\nexport type PaginationQuery = z.infer<typeof paginationQuerySchema>;\n/** What a client may put IN the URL: every field optional, numbers as strings. */\nexport type PaginationQueryInput = z.input<typeof paginationQuerySchema>;\n"]}
|
|
1
|
+
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../src/core/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/core/common\n */\n\nimport { z } from 'zod';\nimport {\n CITY_MAX,\n COUNTRY_MAX,\n DISPLAY_NAME_MAX,\n LINK_URL_MAX,\n PAGE_SIZE_DEFAULT,\n PAGE_SIZE_MAX,\n SKILLS_MAX,\n STATE_MAX,\n} from './limits.js';\nimport { VALIDATION_MESSAGE } from './validation.js';\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/**\n * A repeated query param (`?skill=growth&skill=sales`) or a single value,\n * always as an array, at most `max` values.\n *\n * Express gives `string` for one occurrence and `string[]` for several, so\n * without this the same URL shape parses two different ways. EMPTY values are\n * dropped (`?industry=` is a select the member just reset): asking for fewer\n * filters is never a 400.\n */\nexport const multiValueQuery = <T extends z.ZodType>(inner: T, max: number) =>\n z.preprocess((value) => {\n if (value === undefined || value === null) return undefined;\n const list = (Array.isArray(value) ? value : [value]).filter((v) => v !== '');\n return list.length > 0 ? list : undefined;\n }, z.array(inner).max(max).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, VALIDATION_MESSAGE.invalidId)\n .transform((v) => v.toLowerCase());\n/** Named for what it is on the wire, so it never collides with Mongoose's `ObjectId`. */\nexport type ObjectIdString = z.infer<typeof objectIdSchema>;\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 safeUrlSchema = z.url({\n protocol: /^https?$/,\n hostname: z.regexes.domain,\n error: VALIDATION_MESSAGE.urlInvalid,\n});\nexport type SafeUrl = z.infer<typeof safeUrlSchema>;\n\n/**\n * A URL a member TYPES: `safeUrlSchema` plus the `LINK_URL_MAX` ceiling, so\n * every member URL carries the same cap and no consumer has to remember it.\n *\n * Two schemas, not one, because the cap is about what a person types and is\n * wrong for what the server reads from elsewhere. Until 0.2.0 there was only\n * this one, and it capped a link preview's CDN image URL too: one 314-character\n * `imageUrl` would have failed a whole feed page.\n */\nexport const httpUrlSchema = safeUrlSchema.max(LINK_URL_MAX);\nexport type HttpUrl = z.infer<typeof httpUrlSchema>;\n\n/**\n * A link as people paste it, made into what `httpUrlSchema` checks (0.17.0,\n * moved here from the web so every client cleans a link the same way):\n * `linkedin.com/in/asha` gains `https://`; blank stays blank. Something that\n * already names a scheme keeps it, so `javascript:` is never quietly \"fixed\"\n * into a link: the schema then refuses it, as it only takes http and https.\n */\nexport function normalizeLinkUrl(raw: string): string {\n const trimmed = raw.trim();\n if (!trimmed) return '';\n return /^[a-z][a-z0-9+.-]*:/i.test(trimmed) ? trimmed : `https://${trimmed}`;\n}\n\n/**\n * True when `url` parses and its hostname is EXACTLY one of `hosts`\n * (lowercase). The one host check in this package, written the way the warning\n * on `safeUrlSchema` demands: parsed, compared for equality, never matched as\n * text. `youtube.com.evil.example` and `evil.example/?u=youtube.com` are refused.\n */\nexport function isUrlOnHost(url: string, hosts: readonly string[]): boolean {\n try {\n return hosts.includes(new URL(url).hostname.toLowerCase());\n } catch {\n return false;\n }\n}\n\n/** A typed URL (`httpUrlSchema`) that must also live on one of `hosts`. */\nexport const httpUrlOnHosts = (hosts: readonly string[], message: string) =>\n httpUrlSchema.refine((url) => isUrlOnHost(url, hosts), { error: message });\n\n/**\n * What a product needs before it can be listed in Discover (0.6.0). Here, not\n * in the products domain, because an error's details carry it\n * (`PRODUCT_NOT_LISTABLE` -> `details.missing`) and core never imports a domain.\n */\nexport const PRODUCT_LISTING_REQUIREMENT_VALUES = ['logo', 'tagline', 'screenshot'] as const;\nexport const productListingRequirementSchema = z.enum(PRODUCT_LISTING_REQUIREMENT_VALUES);\nexport type ProductListingRequirement = z.infer<typeof productListingRequirementSchema>;\n\n/**\n * The hosts a shared video may come from. The user's ruling (2026-09-21): video\n * is shared as a link and played in the card, never uploaded. The web plays it\n * through youtube-nocookie.com, the one video frame its CSP admits.\n */\nexport const YOUTUBE_HOSTS = [\n 'youtube.com',\n 'www.youtube.com',\n 'm.youtube.com',\n 'youtu.be',\n // The privacy-mode player's own links (0.7.0).\n 'youtube-nocookie.com',\n 'www.youtube-nocookie.com',\n] as const;\n\n/**\n * The 11-character id of a YouTube VIDEO link, or null for anything else (a\n * channel, a playlist, the home page, another site): what a page can embed.\n * Reads `youtu.be/<id>`, `/watch?v=<id>`, and `/shorts/`, `/embed/`, `/live/<id>`\n * on a `YOUTUBE_HOSTS` host (0.7.0).\n */\nexport function youtubeVideoId(url: string): string | null {\n let u: URL;\n try {\n u = new URL(url);\n } catch {\n return null;\n }\n const host = u.hostname.toLowerCase();\n if (!(YOUTUBE_HOSTS as readonly string[]).includes(host)) return null;\n const id =\n host === 'youtu.be'\n ? (u.pathname.split('/')[1] ?? null)\n : u.pathname === '/watch'\n ? u.searchParams.get('v')\n : (/^\\/(?:shorts|embed|live)\\/([^/]+)/.exec(u.pathname)?.[1] ?? null);\n return id && /^[A-Za-z0-9_-]{11}$/.test(id) ? id : null;\n}\n\n// ─── Username ────────────────────────────────────────────────────────────────\n\n/**\n * Public handle. Derived by the server at onboarding, changeable 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.\n *\n * This is the FORMAT, and it is what every read shape uses. A handle being\n * CHOSEN must also not be reserved: that is `chosenUsernameSchema`.\n */\nexport const USERNAME_MIN = 3;\nexport const USERNAME_MAX = 30;\n\nexport const usernameSchema = z\n .string()\n .trim()\n .toLowerCase()\n // Plain bounds: the client words them from the issue's own minimum/maximum.\n .min(USERNAME_MIN)\n .max(USERNAME_MAX)\n .regex(/^[a-z][a-z0-9_]*(\\.[a-z0-9_]+)?$/, VALIDATION_MESSAGE.usernameInvalid);\nexport type Username = z.infer<typeof usernameSchema>;\n\n/**\n * Handles this product keeps for itself: route names a handle would shadow,\n * and words that would let a member pass as staff.\n *\n * ⚠ HERE, IN THE CONTRACT, since 0.2.0. It used to live in the backend so it\n * could grow without a publish, and nothing ever read it: the server derived\n * handles without checking it, so a member called \"Support\" was handed\n * `support`. One list, read by the server when it derives a handle and by any\n * form that lets a member choose one, is worth a publish per addition.\n *\n * Matched exactly, after lowercasing.\n */\nexport const RESERVED_USERNAMES = [\n 'admin',\n 'about',\n 'api',\n 'chat',\n 'discover',\n 'feed',\n 'founder',\n 'foundercircle',\n 'help',\n 'home',\n 'me',\n 'meetup',\n 'moderator',\n 'onboarding',\n 'settings',\n 'staff',\n 'superadmin',\n 'support',\n 'system',\n 'team',\n] as const;\n\nconst RESERVED = new Set<string>(RESERVED_USERNAMES);\n\nexport function isReservedUsername(handle: string): boolean {\n return RESERVED.has(handle.trim().toLowerCase());\n}\n\n/**\n * A handle a member is CHOOSING: the format, and not reserved.\n *\n * ⚠ Not used on read shapes, deliberately. A handle stored before the reserved\n * check existed must still READ, or one member's old handle would fail every\n * page that shows their name.\n */\nexport const chosenUsernameSchema = usernameSchema.refine((u) => !isReservedUsername(u), {\n error: VALIDATION_MESSAGE.usernameReserved,\n});\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. The others were all asks of other\n * people and this one an ask of the product, which is the whole of the oddness.\n * (hary, 2026-09-21.) Since 0.16.0 the list also holds offers (`mentor_others`,\n * `invest_in_startups`) and a second ask of the product (`attend_events`).\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 // 0.16.0 (the user, 2026-10-18: \"a few more ... cover most of the goals\"):\n // the journey's own asks (feedback on an idea, early testers), the other\n // side of mentorship and of fundraising (mentoring, investing), and three\n // more a founder network is asked for: joining a startup, finding\n // service providers, and events. Appended, per the rule above.\n 'get_feedback',\n 'find_testers',\n 'mentor_others',\n 'invest_in_startups',\n 'join_startup',\n 'find_service_providers',\n 'attend_events',\n] as const;\nexport const goalSchema = z.enum(GOAL_VALUES);\nexport type Goal = z.infer<typeof goalSchema>;\n\n/** True when no value is in the list twice. */\nconst eachOnce = (items: readonly unknown[]): boolean => new Set(items).size === items.length;\n\n/**\n * A member's goals as a list (0.17.0): each goal once, at most every goal.\n * ONE rule for onboarding's finish (which adds its minimum, `GOALS_MIN`) and\n * the Me tab's editor: two copies of one goal met the minimum while meaning\n * one, and the profile would show it twice.\n */\nexport const goalListSchema = z\n .array(goalSchema)\n .max(GOAL_VALUES.length)\n .refine(eachOnce, { error: VALIDATION_MESSAGE.goalsDuplicate });\n\n/** A member's skills (0.17.0): each skill once, at most `SKILLS_MAX`. ONE rule for onboarding and Me. */\nexport const skillListSchema = z\n .array(skillSchema)\n .max(SKILLS_MAX)\n .refine(eachOnce, { error: VALIDATION_MESSAGE.skillsDuplicate });\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// ─── Display name ────────────────────────────────────────────────────────────\n\n/**\n * A full name as it is sent and stored (0.17.0, moved here from the web):\n * trimmed, and every run of spaces made one. `displayNameSchema` applies it,\n * so the server stores it from every client; a client applies it to show the\n * member what will be kept. What `splitFullName` reads.\n */\nexport const tidyFullName = (fullName: string): string => fullName.trim().replace(/\\s+/g, ' ');\n\n/**\n * The name a member is shown by. ONE rule for every body that writes it:\n * onboarding's finish, the profile patch and the personal-details patch, so a\n * name one accepts the others can never refuse, and each stores it TIDIED\n * (`tidyFullName`, 0.17.0: \"Asha Rao\" from the Me editor was stored as\n * typed, while onboarding sent it tidy). Plain bounds, no custom message:\n * `too_small` with minimum 1 already says \"required\", and the client words it.\n */\nexport const displayNameSchema = z\n .string()\n .trim()\n .min(1)\n .max(DISPLAY_NAME_MAX)\n .transform(tidyFullName);\n\n/**\n * A full name's first and last name, DEDUCED, never asked (the user,\n * 2026-10-18: onboarding asks one \"Full name\" and works the two out from the\n * space). The first word is the first name and everything after the first\n * space is the last name, kept as written (\"Asha Rao Kulkarni\" is Asha and\n * Rao Kulkarni).\n *\n * ⚠ A ONE-WORD NAME IS A WHOLE NAME. Many people in India go by a single name,\n * so `lastName` is then `''`, and nothing may refuse it for that.\n *\n * Derived wherever it is needed, never stored as a second copy of the name:\n * `displayName` stays the one owner of a member's name, so a rename can never\n * leave a stale first name behind.\n */\nexport function splitFullName(fullName: string): { firstName: string; lastName: string } {\n const name = tidyFullName(fullName);\n const space = name.indexOf(' ');\n return space === -1\n ? { firstName: name, lastName: '' }\n : { firstName: name.slice(0, space), lastName: name.slice(space + 1) };\n}\n\n// ─── Phone ───────────────────────────────────────────────────────────────────\n\n/** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */\nexport const PHONE_DIGITS = 10;\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(new RegExp(`^[6-9]\\\\d{${PHONE_DIGITS - 1}}$`), VALIDATION_MESSAGE.phoneInvalid);\nexport type Phone = z.infer<typeof phoneSchema>;\n\n/**\n * An Indian mobile number as `phoneSchema`'s ten digits, from however it was\n * typed (0.17.0, moved here from the web): `98765 43210`, `+91 98765-43210`,\n * `091 9876543210`. Anything else comes back digits only and fails the schema\n * with `phoneInvalid`, rather than being guessed at.\n */\nexport function normalizePhone(raw: string): string {\n let digits = raw.replace(/\\D/g, '');\n if (digits.length === PHONE_DIGITS + 2 && digits.startsWith('91')) digits = digits.slice(2);\n else if (digits.length === PHONE_DIGITS + 1 && digits.startsWith('0')) digits = digits.slice(1);\n else if (digits.length === PHONE_DIGITS + 3 && digits.startsWith('091')) digits = digits.slice(3);\n return digits;\n}\n\n// ─── Location ────────────────────────────────────────────────────────────────\n\n/**\n * Where the founder is based. City and country are what Discover filters on.\n *\n * ONE SHAPE for the profile, the Me tab's patch, onboarding's finish and the\n * location suggestion. It was written out three times until 0.2.0, the same\n * kind of copy that let the profile model lose Instagram.\n *\n * ⚠ `country` is REQUIRED and has NO default. A default here (it used to be\n * `'India'`) is destructive on every write path: a founder in Berlin who\n * corrects only their city would be moved to India by a value they never saw.\n * A body that carries a location carries the whole location. Every stored\n * location has a country, because the model requires one, so the read side\n * loses nothing.\n *\n * This is a DECLARED location, not a GPS reading. Jansathi's rule applies\n * 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(CITY_MAX),\n state: optionalTrimmedString(STATE_MAX),\n country: z.string().trim().min(1).max(COUNTRY_MAX),\n});\nexport type FounderLocation = z.infer<typeof locationSchema>;\n\n/**\n * `GET /api/v1/location/areas/reverse?lat=&lng=`: plain decimal degrees.\n *\n * A QUERY, so coercion is right here: everything in a URL is a string. An\n * emptied parameter is \"missing\", never 0, because 0,0 is a real place in the\n * Gulf of Guinea and would be looked up rather than refused.\n */\nexport const coordinatesQuerySchema = z.object({\n lat: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-90).max(90)),\n lng: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-180).max(180)),\n});\nexport type CoordinatesQuery = z.infer<typeof coordinatesQuerySchema>;\nexport type CoordinatesQueryInput = z.input<typeof coordinatesQuerySchema>;\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(\n emptyStringToUndefined,\n z.coerce.number().int().min(1).max(PAGE_SIZE_MAX).default(PAGE_SIZE_DEFAULT),\n ),\n cursor: z.string().trim().min(1).optional(),\n});\nexport type PaginationQuery = z.infer<typeof paginationQuerySchema>;\n/** What a client may put IN the URL: every field optional, numbers as strings. */\nexport type PaginationQueryInput = z.input<typeof paginationQuerySchema>;\n"]}
|
package/dist/core/limits.d.ts
CHANGED
|
@@ -99,6 +99,13 @@ export declare const MEDIA_MAX_INPUT_PIXELS = 50000000;
|
|
|
99
99
|
* that the server deletes it: the composer was abandoned, or the tab closed.
|
|
100
100
|
*/
|
|
101
101
|
export declare const MEDIA_UNATTACHED_TTL_HOURS = 24;
|
|
102
|
+
/**
|
|
103
|
+
* Days an unfinished sign-up's answers are kept after the member last changed
|
|
104
|
+
* them, finished or not (0.17.0, moved here from the backend's model): the
|
|
105
|
+
* draft row's TTL, the photo saved with it (released by the backend's job once
|
|
106
|
+
* the row goes), and the number Privacy states. One number, three readers.
|
|
107
|
+
*/
|
|
108
|
+
export declare const ONBOARDING_DRAFT_RETENTION_DAYS = 90;
|
|
102
109
|
/**
|
|
103
110
|
* Uploads one member may have waiting (requested or processed, not yet
|
|
104
111
|
* attached) at once. A full post is `POST_MAX_MEDIA` (10); this leaves room for
|