founder-schema 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +29 -0
  2. package/dist/core/common.d.ts +8 -1
  3. package/dist/core/common.d.ts.map +1 -1
  4. package/dist/core/common.js +27 -0
  5. package/dist/core/common.js.map +1 -1
  6. package/dist/core/errors.d.ts +7 -1
  7. package/dist/core/errors.d.ts.map +1 -1
  8. package/dist/core/errors.js +11 -1
  9. package/dist/core/errors.js.map +1 -1
  10. package/dist/core/limits.d.ts +5 -1
  11. package/dist/core/limits.d.ts.map +1 -1
  12. package/dist/core/limits.js +5 -1
  13. package/dist/core/limits.js.map +1 -1
  14. package/dist/core/responses.d.ts +6 -0
  15. package/dist/core/responses.d.ts.map +1 -1
  16. package/dist/domains/contributions/contributions.d.ts +9 -2
  17. package/dist/domains/contributions/contributions.d.ts.map +1 -1
  18. package/dist/domains/contributions/contributions.js +14 -4
  19. package/dist/domains/contributions/contributions.js.map +1 -1
  20. package/dist/domains/feed/feed.d.ts +478 -6
  21. package/dist/domains/feed/feed.d.ts.map +1 -1
  22. package/dist/domains/feed/feed.js +152 -9
  23. package/dist/domains/feed/feed.js.map +1 -1
  24. package/dist/domains/moderation/moderation.d.ts +2 -0
  25. package/dist/domains/moderation/moderation.d.ts.map +1 -1
  26. package/dist/domains/moderation/moderation.js +9 -2
  27. package/dist/domains/moderation/moderation.js.map +1 -1
  28. package/dist/domains/notifications/notifications.d.ts +53 -4
  29. package/dist/domains/notifications/notifications.d.ts.map +1 -1
  30. package/dist/domains/notifications/notifications.js +40 -4
  31. package/dist/domains/notifications/notifications.js.map +1 -1
  32. package/dist/domains/products/products.d.ts +21 -6
  33. package/dist/domains/products/products.d.ts.map +1 -1
  34. package/dist/domains/products/products.js +31 -10
  35. package/dist/domains/products/products.js.map +1 -1
  36. package/dist/domains/profile/founder-profile.d.ts +2 -1
  37. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  38. package/dist/domains/profile/founder-profile.js +2 -1
  39. package/dist/domains/profile/founder-profile.js.map +1 -1
  40. package/dist/index.d.ts +3 -3
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +3 -3
  43. package/dist/index.js.map +1 -1
  44. package/package.json +1 -1
package/README.md CHANGED
@@ -99,6 +99,35 @@ instance; this package must not bring its own.
99
99
 
100
100
  ---
101
101
 
102
+ ## Changes in 0.7.0: the products double check (2026-10-07)
103
+
104
+ No export removed; two added (`youtubeVideoId`, `CONTRIBUTION_AWARDED_KIND_VALUES`).
105
+ Nothing a 0.6.0 client parses changes shape.
106
+
107
+ - **Added, optional:** product cards carry `hiddenByStaff` (only on cards the
108
+ viewer may see while hidden) and `viewer.canRepresent` (on its team, active,
109
+ not hidden: the rule for a "post about which product" picker).
110
+ - **Added:** `CONTRIBUTION_AWARDED_KIND_VALUES`, every contribution kind that is
111
+ actually awarded (all but `review_written`).
112
+
113
+ - **Link checks loosened:** `appStore` also takes `itunes.apple.com`, `github`
114
+ also `www.github.com`, and `YOUTUBE_HOSTS` gains the privacy-mode player's
115
+ `youtube-nocookie.com` hosts.
116
+ - **Link check tightened:** a product's `videoUrl` must be ONE video
117
+ (`youtubeVideoId(url)` is not null): a channel, playlist or home-page link is
118
+ refused, because the page cannot embed it. The web can use `youtubeVideoId`
119
+ to build its embed.
120
+ - **Never produced any more:** the `review_written` contribution kind (writing a
121
+ review earns nothing by itself), and so `REVIEWS_REWARDED_PER_DAY`
122
+ (`@deprecated`). Both stay exported so nothing breaks.
123
+ - **Doc comments corrected:** Discover's `new` order (first listed), "top rated"
124
+ (a neutral prior), `myProductsSchema`, the pending invitations' order, the
125
+ notification `count` (events, not people), `PRODUCT_ARCHIVED` (archived
126
+ freezes everything), the contribution actor (null for `review_helpful`), and
127
+ the profile's `contribution` summary.
128
+
129
+ ---
130
+
102
131
  ## Breaking in 0.6.0: products
103
132
 
104
133
  No export was removed (282 -> 403, checked against npm 0.5.0 on 2026-10-06).
@@ -104,7 +104,14 @@ export type ProductListingRequirement = z.infer<typeof productListingRequirement
104
104
  * is shared as a link and played in the card, never uploaded. The web plays it
105
105
  * through youtube-nocookie.com, the one video frame its CSP admits.
106
106
  */
107
- export declare const YOUTUBE_HOSTS: readonly ["youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be"];
107
+ export declare const YOUTUBE_HOSTS: readonly ["youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be", "youtube-nocookie.com", "www.youtube-nocookie.com"];
108
+ /**
109
+ * The 11-character id of a YouTube VIDEO link, or null for anything else (a
110
+ * channel, a playlist, the home page, another site): what a page can embed.
111
+ * Reads `youtu.be/<id>`, `/watch?v=<id>`, and `/shorts/`, `/embed/`, `/live/<id>`
112
+ * on a `YOUTUBE_HOSTS` host (0.7.0).
113
+ */
114
+ export declare function youtubeVideoId(url: string): string | null;
108
115
  /**
109
116
  * Public handle. Derived by the server at onboarding, changeable later.
110
117
  *
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"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"}
@@ -127,7 +127,34 @@ export const YOUTUBE_HOSTS = [
127
127
  'www.youtube.com',
128
128
  'm.youtube.com',
129
129
  'youtu.be',
130
+ // The privacy-mode player's own links (0.7.0).
131
+ 'youtube-nocookie.com',
132
+ 'www.youtube-nocookie.com',
130
133
  ];
134
+ /**
135
+ * The 11-character id of a YouTube VIDEO link, or null for anything else (a
136
+ * channel, a playlist, the home page, another site): what a page can embed.
137
+ * Reads `youtu.be/<id>`, `/watch?v=<id>`, and `/shorts/`, `/embed/`, `/live/<id>`
138
+ * on a `YOUTUBE_HOSTS` host (0.7.0).
139
+ */
140
+ export function youtubeVideoId(url) {
141
+ let u;
142
+ try {
143
+ u = new URL(url);
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ const host = u.hostname.toLowerCase();
149
+ if (!YOUTUBE_HOSTS.includes(host))
150
+ return null;
151
+ const id = host === 'youtu.be'
152
+ ? (u.pathname.split('/')[1] ?? null)
153
+ : u.pathname === '/watch'
154
+ ? u.searchParams.get('v')
155
+ : (/^\/(?:shorts|embed|live)\/([^/]+)/.exec(u.pathname)?.[1] ?? null);
156
+ return id && /^[A-Za-z0-9_-]{11}$/.test(id) ? id : null;
157
+ }
131
158
  // ─── Username ────────────────────────────────────────────────────────────────
132
159
  /**
133
160
  * Public handle. Derived by the server at onboarding, changeable later.
@@ -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 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] as const;\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. Every other value is an ask of\n * other people and this one is an ask of the product, which is the whole of the\n * oddness. (hary, 2026-09-21.)\n */\nexport const GOAL_VALUES = [\n 'find_cofounder',\n 'find_investors',\n 'find_customers',\n 'hiring',\n 'partnerships',\n 'mentorship',\n 'grow_network',\n 'business_ideas',\n 'market_insights',\n 'learn_startup',\n 'start_company',\n] as const;\nexport const goalSchema = z.enum(GOAL_VALUES);\nexport type Goal = z.infer<typeof goalSchema>;\n\n// ─── Gender ──────────────────────────────────────────────────────────────────\n\n/**\n * Demographic only. Nothing here computes anything from it, unlike the sibling\n * gym product where gender feeds body-metric maths.\n *\n * Takes the family's three values and ADDS one rather than renaming any: a\n * network for founders should let somebody decline the question without having\n * to describe themselves as \"other\".\n */\nexport const GENDER_VALUES = ['male', 'female', 'other', 'prefer_not_to_say'] as const;\nexport const genderSchema = z.enum(GENDER_VALUES);\nexport type Gender = z.infer<typeof genderSchema>;\n\n// ─── Phone ───────────────────────────────────────────────────────────────────\n\n/**\n * Indian mobile number: ten raw digits, no country code, matching the standard\n * used across this family. Display adds the `+91`.\n *\n * ⚠️ WHERE IT IS STORED IS NOT A SCHEMA QUESTION, and it has been ruled: the\n * phone lives on the account mirror, never as a second copy on the profile.\n * Two stored copies drift the first time somebody edits one, and the profile's\n * would be the copy nobody keeps in sync.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(/^[6-9]\\d{9}$/, 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":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,QAAQ,EACR,WAAW,EACX,YAAY,EACZ,iBAAiB,EACjB,aAAa,EACb,SAAS,GACV,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAc,EAAW,EAAE;IAChE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,SAAS,CAAC;IACvE,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,yEAAyE;AACzE,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,SAAS,GAAG,GAAG,EAAE,EAAE,CACvD,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEpF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAsB,KAAQ,EAAE,GAAW,EAAE,EAAE,CAC5E,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,KAAK,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5D,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC9E,OAAO,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;AAC5C,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,CAAC,CAAC;AAEzC,4EAA4E;AAC5E,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,MAAM,EAAE;KACR,KAAK,CAAC,iBAAiB,EAAE,kBAAkB,CAAC,SAAS,CAAC;KACtD,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;AAIrC,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,GAAG,CAAC;IACjC,QAAQ,EAAE,UAAU;IACpB,QAAQ,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM;IAC1B,KAAK,EAAE,kBAAkB,CAAC,UAAU;CACrC,CAAC,CAAC;AAGH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,aAAa,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;AAG7D;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAC,GAAW,EAAE,KAAwB;IAC/D,IAAI,CAAC;QACH,OAAO,KAAK,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,KAAwB,EAAE,OAAe,EAAE,EAAE,CAC1E,aAAa,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;AAE7E;;;;GAIG;AACH,MAAM,CAAC,MAAM,kCAAkC,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,CAAU,CAAC;AAC7F,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAC;AAG1F;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,aAAa;IACb,iBAAiB;IACjB,eAAe;IACf,UAAU;IACV,+CAA+C;IAC/C,sBAAsB;IACtB,0BAA0B;CAClB,CAAC;AAEX;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAM,CAAC;IACX,IAAI,CAAC;QACH,CAAC,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC;IACnB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC;IACtC,IAAI,CAAE,aAAmC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACtE,MAAM,EAAE,GACN,IAAI,KAAK,UAAU;QACjB,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;QACpC,CAAC,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ;YACvB,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;YACzB,CAAC,CAAC,CAAC,mCAAmC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC;IAC5E,OAAO,EAAE,IAAI,qBAAqB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAC1D,CAAC;AAED,gFAAgF;AAEhF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC;AAC9B,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,CAAC;AAE/B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC;KAC5B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,WAAW,EAAE;IACd,4EAA4E;KAC3E,GAAG,CAAC,YAAY,CAAC;KACjB,GAAG,CAAC,YAAY,CAAC;KACjB,KAAK,CAAC,kCAAkC,EAAE,kBAAkB,CAAC,eAAe,CAAC,CAAC;AAGjF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG;IAChC,OAAO;IACP,OAAO;IACP,KAAK;IACL,MAAM;IACN,UAAU;IACV,MAAM;IACN,SAAS;IACT,eAAe;IACf,MAAM;IACN,MAAM;IACN,IAAI;IACJ,QAAQ;IACR,WAAW;IACX,YAAY;IACZ,UAAU;IACV,OAAO;IACP,YAAY;IACZ,SAAS;IACT,QAAQ;IACR,MAAM;CACE,CAAC;AAEX,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAS,kBAAkB,CAAC,CAAC;AAErD,MAAM,UAAU,kBAAkB,CAAC,MAAc;IAC/C,OAAO,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AACnD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,kBAAkB,CAAC,CAAC,CAAC,EAAE;IACvF,KAAK,EAAE,kBAAkB,CAAC,gBAAgB;CAC3C,CAAC,CAAC;AAEH,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,MAAM;IACN,YAAY;IACZ,KAAK;IACL,UAAU;IACV,MAAM;IACN,eAAe;IACf,YAAY;IACZ,QAAQ;CACA,CAAC;AACX,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAG/D;;;GAGG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG;IAC7B,UAAU;IACV,OAAO;IACP,UAAU;IACV,UAAU;IACV,SAAS;IACT,KAAK;IACL,UAAU;IACV,QAAQ;IACR,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,WAAW;IACX,eAAe;IACf,aAAa;IACb,OAAO;IACP,UAAU;IACV,UAAU;IACV,UAAU;IACV,UAAU;IACV,OAAO;IACP,OAAO;CACC,CAAC;AACX,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;AAGtD;;;;GAIG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,aAAa;IACb,SAAS;IACT,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,OAAO;IACP,WAAW;IACX,YAAY;IACZ,SAAS;IACT,OAAO;IACP,WAAW;IACX,cAAc;IACd,UAAU;IACV,aAAa;IACb,WAAW;CACH,CAAC;AACX,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;AAGhD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,gBAAgB;IAChB,gBAAgB;IAChB,gBAAgB;IAChB,QAAQ;IACR,cAAc;IACd,YAAY;IACZ,cAAc;IACd,gBAAgB;IAChB,iBAAiB;IACjB,eAAe;IACf,eAAe;CACP,CAAC;AACX,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAG9C,gFAAgF;AAEhF;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,mBAAmB,CAAU,CAAC;AACvF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAGlD,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC;KACzB,MAAM,EAAE;KACR,IAAI,EAAE;KACN,KAAK,CAAC,cAAc,EAAE,kBAAkB,CAAC,YAAY,CAAC,CAAC;AAG1D,gFAAgF;AAEhF;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC;IAC5C,KAAK,EAAE,qBAAqB,CAAC,SAAS,CAAC;IACvC,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC;CACnD,CAAC,CAAC;AAGH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,GAAG,EAAE,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;IAC7E,GAAG,EAAE,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;CAChF,CAAC,CAAC;AAIH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,cAAc,CAAC,QAAQ,EAAE,CAAC;AAGjE,gFAAgF;AAEhF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C;;;;;OAKG;IACH,KAAK,EAAE,CAAC,CAAC,UAAU,CACjB,sBAAsB,EACtB,CAAC,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAC7E;IACD,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE;CAC5C,CAAC,CAAC","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 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. Every other value is an ask of\n * other people and this one is an ask of the product, which is the whole of the\n * oddness. (hary, 2026-09-21.)\n */\nexport const GOAL_VALUES = [\n 'find_cofounder',\n 'find_investors',\n 'find_customers',\n 'hiring',\n 'partnerships',\n 'mentorship',\n 'grow_network',\n 'business_ideas',\n 'market_insights',\n 'learn_startup',\n 'start_company',\n] as const;\nexport const goalSchema = z.enum(GOAL_VALUES);\nexport type Goal = z.infer<typeof goalSchema>;\n\n// ─── Gender ──────────────────────────────────────────────────────────────────\n\n/**\n * Demographic only. Nothing here computes anything from it, unlike the sibling\n * gym product where gender feeds body-metric maths.\n *\n * Takes the family's three values and ADDS one rather than renaming any: a\n * network for founders should let somebody decline the question without having\n * to describe themselves as \"other\".\n */\nexport const GENDER_VALUES = ['male', 'female', 'other', 'prefer_not_to_say'] as const;\nexport const genderSchema = z.enum(GENDER_VALUES);\nexport type Gender = z.infer<typeof genderSchema>;\n\n// ─── Phone ───────────────────────────────────────────────────────────────────\n\n/**\n * Indian mobile number: ten raw digits, no country code, matching the standard\n * used across this family. Display adds the `+91`.\n *\n * ⚠️ WHERE IT IS STORED IS NOT A SCHEMA QUESTION, and it has been ruled: the\n * phone lives on the account mirror, never as a second copy on the profile.\n * Two stored copies drift the first time somebody edits one, and the profile's\n * would be the copy nobody keeps in sync.\n */\nexport const phoneSchema = z\n .string()\n .trim()\n .regex(/^[6-9]\\d{9}$/, 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"]}
@@ -27,7 +27,7 @@ import { z } from 'zod';
27
27
  * As a tuple, the way `GENDER_VALUES` is, so a consumer can iterate it (a
28
28
  * translation-coverage test does exactly that) and TypeScript keeps the union.
29
29
  */
30
- export declare const API_ERROR_CODE_VALUES: readonly ["INTERNAL_ERROR", "VALIDATION_ERROR", "NOT_FOUND", "CONFLICT", "RATE_LIMITED", "SERVICE_UNAVAILABLE", "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED", "SESSION_INVALIDATED", "FORBIDDEN", "ONBOARDING_INCOMPLETE", "ALREADY_ONBOARDED", "UNDER_AGE", "UPLOAD_NOT_ATTACHABLE", "UPLOAD_TOO_LARGE", "UPLOAD_WRONG_TYPE", "UPLOAD_NOT_RECEIVED", "UPLOAD_PROCESSING", "UPLOAD_UNREADABLE", "UPLOAD_TOO_MANY_PIXELS", "UPLOAD_LIMIT_REACHED", "UPLOAD_IN_USE", "EDIT_WINDOW_CLOSED", "POLL_NOT_EDITABLE", "POLL_CLOSED", "POLL_CHOICE_INVALID", "REPLY_TOO_DEEP", "CANNOT_REPORT_OWN", "FEEDBACK_NOT_MARKABLE", "PRODUCT_LIMIT_REACHED", "PRODUCT_NOT_LISTABLE", "PRODUCT_ARCHIVED", "PRODUCT_NOT_YOURS", "PRODUCT_ALREADY_MEMBER", "PRODUCT_MEMBER_NOT_INVITABLE", "PRODUCT_NOT_ON_TEAM", "PRODUCT_OWNER_MUST_TRANSFER", "PRODUCT_OWN_TEAM", "TESTER_NOT_ELIGIBLE", "REVIEW_EXISTS", "REVIEW_OWN_VOTE", "MODERATION_ACTION_INVALID", "CHAT_WITH_SELF", "ACCOUNT_SUSPENDED", "ROLE_NOT_HELD", "MPIN_NOT_SET", "MPIN_ALREADY_SET", "MPIN_INCORRECT", "MPIN_LOCKED", "CANNOT_CHANGE_OWN_ROLES", "ROLE_NOT_GRANTABLE", "TICKET_FULL", "SAVED_REPLIES_FULL"];
30
+ export declare const API_ERROR_CODE_VALUES: readonly ["INTERNAL_ERROR", "VALIDATION_ERROR", "NOT_FOUND", "CONFLICT", "RATE_LIMITED", "SERVICE_UNAVAILABLE", "UNAUTHORIZED", "INVALID_TOKEN", "TOKEN_EXPIRED", "SESSION_INVALIDATED", "FORBIDDEN", "ONBOARDING_INCOMPLETE", "ALREADY_ONBOARDED", "UNDER_AGE", "UPLOAD_NOT_ATTACHABLE", "UPLOAD_TOO_LARGE", "UPLOAD_WRONG_TYPE", "UPLOAD_NOT_RECEIVED", "UPLOAD_PROCESSING", "UPLOAD_UNREADABLE", "UPLOAD_TOO_MANY_PIXELS", "UPLOAD_LIMIT_REACHED", "UPLOAD_IN_USE", "EDIT_WINDOW_CLOSED", "POLL_NOT_EDITABLE", "POLL_CLOSED", "POLL_CHOICE_INVALID", "REPLY_TOO_DEEP", "CANNOT_REPORT_OWN", "FEEDBACK_NOT_MARKABLE", "CANNOT_REPOST_OWN", "CANNOT_REPOST_A_REPOST", "ALREADY_REPOSTED", "PRODUCT_LIMIT_REACHED", "PRODUCT_NOT_LISTABLE", "PRODUCT_ARCHIVED", "PRODUCT_NOT_YOURS", "PRODUCT_ALREADY_MEMBER", "PRODUCT_MEMBER_NOT_INVITABLE", "PRODUCT_NOT_ON_TEAM", "PRODUCT_OWNER_MUST_TRANSFER", "PRODUCT_OWN_TEAM", "TESTER_NOT_ELIGIBLE", "REVIEW_EXISTS", "REVIEW_OWN_VOTE", "MODERATION_ACTION_INVALID", "CHAT_WITH_SELF", "ACCOUNT_SUSPENDED", "ROLE_NOT_HELD", "MPIN_NOT_SET", "MPIN_ALREADY_SET", "MPIN_INCORRECT", "MPIN_LOCKED", "CANNOT_CHANGE_OWN_ROLES", "ROLE_NOT_GRANTABLE", "TICKET_FULL", "SAVED_REPLIES_FULL"];
31
31
  export declare const apiErrorCodeSchema: z.ZodEnum<{
32
32
  INTERNAL_ERROR: "INTERNAL_ERROR";
33
33
  VALIDATION_ERROR: "VALIDATION_ERROR";
@@ -59,6 +59,9 @@ export declare const apiErrorCodeSchema: z.ZodEnum<{
59
59
  REPLY_TOO_DEEP: "REPLY_TOO_DEEP";
60
60
  CANNOT_REPORT_OWN: "CANNOT_REPORT_OWN";
61
61
  FEEDBACK_NOT_MARKABLE: "FEEDBACK_NOT_MARKABLE";
62
+ CANNOT_REPOST_OWN: "CANNOT_REPOST_OWN";
63
+ CANNOT_REPOST_A_REPOST: "CANNOT_REPOST_A_REPOST";
64
+ ALREADY_REPOSTED: "ALREADY_REPOSTED";
62
65
  PRODUCT_LIMIT_REACHED: "PRODUCT_LIMIT_REACHED";
63
66
  PRODUCT_NOT_LISTABLE: "PRODUCT_NOT_LISTABLE";
64
67
  PRODUCT_ARCHIVED: "PRODUCT_ARCHIVED";
@@ -117,6 +120,9 @@ export declare const API_ERROR_CODE: {
117
120
  REPLY_TOO_DEEP: "REPLY_TOO_DEEP";
118
121
  CANNOT_REPORT_OWN: "CANNOT_REPORT_OWN";
119
122
  FEEDBACK_NOT_MARKABLE: "FEEDBACK_NOT_MARKABLE";
123
+ CANNOT_REPOST_OWN: "CANNOT_REPOST_OWN";
124
+ CANNOT_REPOST_A_REPOST: "CANNOT_REPOST_A_REPOST";
125
+ ALREADY_REPOSTED: "ALREADY_REPOSTED";
120
126
  PRODUCT_LIMIT_REACHED: "PRODUCT_LIMIT_REACHED";
121
127
  PRODUCT_NOT_LISTABLE: "PRODUCT_NOT_LISTABLE";
122
128
  PRODUCT_ARCHIVED: "PRODUCT_ARCHIVED";
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,kmCAmKxB,CAAC;AAEX,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAgC,CAAC;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,4DAA4D;AAC5D,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA0B,CAAC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,qBAAqB,qqCA6KxB,CAAC;AAEX,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAAgC,CAAC;AAChE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,4DAA4D;AAC5D,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAA0B,CAAC"}
@@ -109,6 +109,12 @@ export const API_ERROR_CODE_VALUES = [
109
109
  * team's.
110
110
  */
111
111
  'FEEDBACK_NOT_MARKABLE',
112
+ /** Reposting your own post or poll (0.8.0). */
113
+ 'CANNOT_REPOST_OWN',
114
+ /** Reposting a repost (0.8.0): share the original instead. */
115
+ 'CANNOT_REPOST_A_REPOST',
116
+ /** You already have an active repost of this item (0.8.0). Undo it first. 409. */
117
+ 'ALREADY_REPOSTED',
112
118
  // ── Products ──
113
119
  /** Founders own at most `PRODUCTS_OWNED_MAX` products. Delete one to add another. */
114
120
  'PRODUCT_LIMIT_REACHED',
@@ -117,7 +123,11 @@ export const API_ERROR_CODE_VALUES = [
117
123
  * listed one ready. `details.missing`: some of `logo`, `tagline`, `screenshot`.
118
124
  */
119
125
  'PRODUCT_NOT_LISTABLE',
120
- /** The product is archived: no new posts, reviews or upvotes until it is restored. */
126
+ /**
127
+ * The product is archived and frozen until its owner restores it: no new
128
+ * posts about it, reviews, upvotes, replies, helpful marks, testers or
129
+ * invitations.
130
+ */
121
131
  'PRODUCT_ARCHIVED',
122
132
  /** Tagging a post with, or acting for, a product you are not on the team of. */
123
133
  'PRODUCT_NOT_YOURS',
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,iBAAiB;IACjB,gFAAgF;IAChF,gBAAgB;IAChB,yEAAyE;IACzE,kBAAkB;IAClB,2EAA2E;IAC3E,WAAW;IACX,wEAAwE;IACxE,UAAU;IACV,uEAAuE;IACvE,cAAc;IACd,2EAA2E;IAC3E,qBAAqB;IAErB,oBAAoB;IACpB,4BAA4B;IAC5B,cAAc;IACd,sDAAsD;IACtD,eAAe;IACf,qEAAqE;IACrE,eAAe;IACf,oEAAoE;IACpE,qBAAqB;IACrB,6CAA6C;IAC7C,WAAW;IAEX,mBAAmB;IACnB,mFAAmF;IACnF,uBAAuB;IACvB,gFAAgF;IAChF,mBAAmB;IACnB,uEAAuE;IACvE,WAAW;IAEX,kDAAkD;IAClD;;;;;OAKG;IACH,uBAAuB;IACvB,sCAAsC;IACtC,kBAAkB;IAClB;;;OAGG;IACH,mBAAmB;IACnB,2EAA2E;IAC3E,qBAAqB;IACrB;;;;OAIG;IACH,mBAAmB;IACnB,0FAA0F;IAC1F,mBAAmB;IACnB,mEAAmE;IACnE,wBAAwB;IACxB,sEAAsE;IACtE,sBAAsB;IACtB,wFAAwF;IACxF,eAAe;IAEf,iBAAiB;IACjB,+CAA+C;IAC/C,oBAAoB;IACpB,iEAAiE;IACjE,mBAAmB;IACnB,kDAAkD;IAClD,aAAa;IACb,oFAAoF;IACpF,qBAAqB;IACrB,qDAAqD;IACrD,gBAAgB;IAChB,8FAA8F;IAC9F,mBAAmB;IACnB;;;;OAIG;IACH,uBAAuB;IAEvB,iBAAiB;IACjB,qFAAqF;IACrF,uBAAuB;IACvB;;;OAGG;IACH,sBAAsB;IACtB,sFAAsF;IACtF,kBAAkB;IAClB,gFAAgF;IAChF,mBAAmB;IACnB,sEAAsE;IACtE,wBAAwB;IACxB;;;OAGG;IACH,8BAA8B;IAC9B,8FAA8F;IAC9F,qBAAqB;IACrB;;;OAGG;IACH,6BAA6B;IAC7B,yFAAyF;IACzF,kBAAkB;IAClB,6EAA6E;IAC7E,qBAAqB;IAErB,gBAAgB;IAChB,6DAA6D;IAC7D,eAAe;IACf,sCAAsC;IACtC,iBAAiB;IAEjB,mBAAmB;IACnB;;;OAGG;IACH,2BAA2B;IAE3B,aAAa;IACb,4CAA4C;IAC5C,gBAAgB;IAEhB,oBAAoB;IACpB,qFAAqF;IACrF,mBAAmB;IAEnB,2BAA2B;IAC3B;;;OAGG;IACH,eAAe;IACf,4EAA4E;IAC5E,cAAc;IACd,8DAA8D;IAC9D,kBAAkB;IAClB,4EAA4E;IAC5E,gBAAgB;IAChB,0EAA0E;IAC1E,aAAa;IACb,oDAAoD;IACpD,yBAAyB;IACzB,qEAAqE;IACrE,oBAAoB;IAEpB,gBAAgB;IAChB,uEAAuE;IACvE,aAAa;IACb,0EAA0E;IAC1E,oBAAoB;CACZ,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;AAGhE,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAAG,kBAAkB,CAAC,IAAI,CAAC","sourcesContent":["/**\n * founder-schema — Every error code the API answers with\n *\n * THE CODE IS WHAT A CLIENT ACTS ON; the `message` beside it is English for\n * logs and for a client too old to know the code. A client words each code in\n * the member's language, and a test on the web side asserts every value here\n * has a message in every locale it ships.\n *\n * Before 0.2.0 the backend kept this list to itself, and every domain refusal\n * (a closed poll, a reply to a reply, the edit window) went out as\n * `VALIDATION_ERROR` with an English sentence, so the only way for a client to\n * tell them apart was to match on the wording.\n *\n * WHEN TO USE WHICH:\n * - A request that does not match its schema: `VALIDATION_ERROR`, with every\n * problem in `error.details.issues`.\n * - A well-formed request the current STATE of things refuses: its own code\n * below. These carry no issues; the code says it all.\n *\n * ⚠ Append freely; remove deliberately. A code removed here that an older\n * server still sends fails the client's parse of the error envelope.\n *\n * @module founder-schema/core/errors\n */\n\nimport { z } from 'zod';\n\n/**\n * As a tuple, the way `GENDER_VALUES` is, so a consumer can iterate it (a\n * translation-coverage test does exactly that) and TypeScript keeps the union.\n */\nexport const API_ERROR_CODE_VALUES = [\n // ── Anything ──\n /** A bug or an outage on our side. The message says nothing more on purpose. */\n 'INTERNAL_ERROR',\n /** The request did not match its schema. `details.issues` says where. */\n 'VALIDATION_ERROR',\n /** No such thing, or not one you may see. Deliberately the same answer. */\n 'NOT_FOUND',\n /** Two writes raced for something unique; retrying usually succeeds. */\n 'CONFLICT',\n /** Too many requests from this member or address. Wait, then retry. */\n 'RATE_LIMITED',\n /** Something we depend on (the chat server) is down or not switched on. */\n 'SERVICE_UNAVAILABLE',\n\n // ── Who you are ──\n /** No credential at all. */\n 'UNAUTHORIZED',\n /** A credential that is not ours, or not readable. */\n 'INVALID_TOKEN',\n /** A credential that was ours and has expired. Refresh and retry. */\n 'TOKEN_EXPIRED',\n /** Signed out everywhere, or the account changed. Sign in again. */\n 'SESSION_INVALIDATED',\n /** Signed in, and not allowed to do this. */\n 'FORBIDDEN',\n\n // ── Onboarding ──\n /** Finish onboarding first. The client routes here instead of showing an error. */\n 'ONBOARDING_INCOMPLETE',\n /** Onboarding is already finished; there is nothing to save or finish again. */\n 'ALREADY_ONBOARDED',\n /** Under `MIN_AGE_YEARS`. Refused at the draft and again at finish. */\n 'UNDER_AGE',\n\n // ── Uploads: completing one, and attaching it ──\n /**\n * Not an upload of yours, not processed yet, already attached somewhere, or\n * for the other purpose (an avatar upload on a post). One code for all of\n * them on purpose: telling \"someone else's\" apart from \"does not exist\" would\n * let a caller probe other members' media ids.\n */\n 'UPLOAD_NOT_ATTACHABLE',\n /** Larger than `UPLOAD_MAX_BYTES`. */\n 'UPLOAD_TOO_LARGE',\n /**\n * Not one of the image types this product accepts (by the file's own bytes,\n * whatever its name or declared type), or not an image at all.\n */\n 'UPLOAD_WRONG_TYPE',\n /** Completed before the file reached storage: upload it, then complete. */\n 'UPLOAD_NOT_RECEIVED',\n /**\n * Another request is processing this upload right now (a retry or a double\n * tap). Nothing is wrong: complete again in a moment and it answers with\n * the image. 409.\n */\n 'UPLOAD_PROCESSING',\n /** A JPEG, PNG, WebP or GIF by its bytes that cannot be decoded: truncated or corrupt. */\n 'UPLOAD_UNREADABLE',\n /** More than `MEDIA_MAX_INPUT_PIXELS`; refused before decoding. */\n 'UPLOAD_TOO_MANY_PIXELS',\n /** `MEDIA_PENDING_MAX` uploads are already waiting to be attached. */\n 'UPLOAD_LIMIT_REACHED',\n /** Discarding an upload that is attached: remove it from its post or avatar instead. */\n 'UPLOAD_IN_USE',\n\n // ── The feed ──\n /** The post is older than `EDIT_WINDOW_MS`. */\n 'EDIT_WINDOW_CLOSED',\n /** A poll is never editable: people have already voted on it. */\n 'POLL_NOT_EDITABLE',\n /** The poll has closed, by the server's clock. */\n 'POLL_CLOSED',\n /** No option, several on a single-choice poll, or an option that does not exist. */\n 'POLL_CHOICE_INVALID',\n /** A reply to a reply. One level of nesting only. */\n 'REPLY_TOO_DEEP',\n /** Reporting your own post, poll or comment (or, since 0.6.0, your own product or review). */\n 'CANNOT_REPORT_OWN',\n /**\n * Marking a comment \"this helped\" that is not feedback on your product: the\n * post is not tagged with a product you are on, or the comment is your own\n * team's.\n */\n 'FEEDBACK_NOT_MARKABLE',\n\n // ── Products ──\n /** Founders own at most `PRODUCTS_OWNED_MAX` products. Delete one to add another. */\n 'PRODUCT_LIMIT_REACHED',\n /**\n * Listing a product that is not ready for Discover, or removing what keeps a\n * listed one ready. `details.missing`: some of `logo`, `tagline`, `screenshot`.\n */\n 'PRODUCT_NOT_LISTABLE',\n /** The product is archived: no new posts, reviews or upvotes until it is restored. */\n 'PRODUCT_ARCHIVED',\n /** Tagging a post with, or acting for, a product you are not on the team of. */\n 'PRODUCT_NOT_YOURS',\n /** Inviting someone who is already on the team or already invited. */\n 'PRODUCT_ALREADY_MEMBER',\n /**\n * Inviting someone who cannot join a team: not finished onboarding, paused,\n * suspended or deleted. One code for all four, so it reveals none of them.\n */\n 'PRODUCT_MEMBER_NOT_INVITABLE',\n /** Handing over to, or re-roling, someone who is not on the team (or invited, for a role). */\n 'PRODUCT_NOT_ON_TEAM',\n /**\n * The owner leaving, being removed, or having their role changed. Ownership\n * moves only by handing the product to someone first.\n */\n 'PRODUCT_OWNER_MUST_TRANSFER',\n /** Upvoting, reviewing or confirming yourself as a tester of your own team's product. */\n 'PRODUCT_OWN_TEAM',\n /** Confirming as a tester someone who has given this product no feedback. */\n 'TESTER_NOT_ELIGIBLE',\n\n // ── Reviews ──\n /** One review per member per product. Edit yours instead. */\n 'REVIEW_EXISTS',\n /** Voting your own review helpful. */\n 'REVIEW_OWN_VOTE',\n\n // ── Moderation ──\n /**\n * A decision that does not apply to that kind of thing: removing a product\n * (staff hide one, never delete it). `MODERATION_ACTIONS_BY_TARGET_KIND`.\n */\n 'MODERATION_ACTION_INVALID',\n\n // ── Chat ──\n /** Opening a conversation with yourself. */\n 'CHAT_WITH_SELF',\n\n // ── The account ──\n /** Staff suspended this account: nothing in the product works until it is lifted. */\n 'ACCOUNT_SUSPENDED',\n\n // ── Roles and the MPIN ──\n /**\n * Switching to a role this person does not hold (or no longer holds), or\n * setting an MPIN while holding no staff role at all.\n */\n 'ROLE_NOT_HELD',\n /** A staff role needs an MPIN, and none has been set yet. Set one first. */\n 'MPIN_NOT_SET',\n /** An MPIN is set once; changing it needs the current one. */\n 'MPIN_ALREADY_SET',\n /** The MPIN is wrong. `details.attemptsLeft` says how many tries remain. */\n 'MPIN_INCORRECT',\n /** Too many wrong MPINs. `details.lockedUntil` says when to try again. */\n 'MPIN_LOCKED',\n /** Changing your own roles. Someone else has to. */\n 'CANNOT_CHANGE_OWN_ROLES',\n /** Granting or revoking a role the role you act as may not touch. */\n 'ROLE_NOT_GRANTABLE',\n\n // ── Support ──\n /** The ticket holds `TICKET_MESSAGES_MAX` messages. Open a new one. */\n 'TICKET_FULL',\n /** The team keeps `SAVED_REPLIES_MAX` saved replies. Delete one first. */\n 'SAVED_REPLIES_FULL',\n] as const;\n\nexport const apiErrorCodeSchema = z.enum(API_ERROR_CODE_VALUES);\nexport type ApiErrorCode = z.infer<typeof apiErrorCodeSchema>;\n\n/** The same codes by name: `API_ERROR_CODE.POLL_CLOSED`. */\nexport const API_ERROR_CODE = apiErrorCodeSchema.enum;\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,iBAAiB;IACjB,gFAAgF;IAChF,gBAAgB;IAChB,yEAAyE;IACzE,kBAAkB;IAClB,2EAA2E;IAC3E,WAAW;IACX,wEAAwE;IACxE,UAAU;IACV,uEAAuE;IACvE,cAAc;IACd,2EAA2E;IAC3E,qBAAqB;IAErB,oBAAoB;IACpB,4BAA4B;IAC5B,cAAc;IACd,sDAAsD;IACtD,eAAe;IACf,qEAAqE;IACrE,eAAe;IACf,oEAAoE;IACpE,qBAAqB;IACrB,6CAA6C;IAC7C,WAAW;IAEX,mBAAmB;IACnB,mFAAmF;IACnF,uBAAuB;IACvB,gFAAgF;IAChF,mBAAmB;IACnB,uEAAuE;IACvE,WAAW;IAEX,kDAAkD;IAClD;;;;;OAKG;IACH,uBAAuB;IACvB,sCAAsC;IACtC,kBAAkB;IAClB;;;OAGG;IACH,mBAAmB;IACnB,2EAA2E;IAC3E,qBAAqB;IACrB;;;;OAIG;IACH,mBAAmB;IACnB,0FAA0F;IAC1F,mBAAmB;IACnB,mEAAmE;IACnE,wBAAwB;IACxB,sEAAsE;IACtE,sBAAsB;IACtB,wFAAwF;IACxF,eAAe;IAEf,iBAAiB;IACjB,+CAA+C;IAC/C,oBAAoB;IACpB,iEAAiE;IACjE,mBAAmB;IACnB,kDAAkD;IAClD,aAAa;IACb,oFAAoF;IACpF,qBAAqB;IACrB,qDAAqD;IACrD,gBAAgB;IAChB,8FAA8F;IAC9F,mBAAmB;IACnB;;;;OAIG;IACH,uBAAuB;IACvB,+CAA+C;IAC/C,mBAAmB;IACnB,8DAA8D;IAC9D,wBAAwB;IACxB,kFAAkF;IAClF,kBAAkB;IAElB,iBAAiB;IACjB,qFAAqF;IACrF,uBAAuB;IACvB;;;OAGG;IACH,sBAAsB;IACtB;;;;OAIG;IACH,kBAAkB;IAClB,gFAAgF;IAChF,mBAAmB;IACnB,sEAAsE;IACtE,wBAAwB;IACxB;;;OAGG;IACH,8BAA8B;IAC9B,8FAA8F;IAC9F,qBAAqB;IACrB;;;OAGG;IACH,6BAA6B;IAC7B,yFAAyF;IACzF,kBAAkB;IAClB,6EAA6E;IAC7E,qBAAqB;IAErB,gBAAgB;IAChB,6DAA6D;IAC7D,eAAe;IACf,sCAAsC;IACtC,iBAAiB;IAEjB,mBAAmB;IACnB;;;OAGG;IACH,2BAA2B;IAE3B,aAAa;IACb,4CAA4C;IAC5C,gBAAgB;IAEhB,oBAAoB;IACpB,qFAAqF;IACrF,mBAAmB;IAEnB,2BAA2B;IAC3B;;;OAGG;IACH,eAAe;IACf,4EAA4E;IAC5E,cAAc;IACd,8DAA8D;IAC9D,kBAAkB;IAClB,4EAA4E;IAC5E,gBAAgB;IAChB,0EAA0E;IAC1E,aAAa;IACb,oDAAoD;IACpD,yBAAyB;IACzB,qEAAqE;IACrE,oBAAoB;IAEpB,gBAAgB;IAChB,uEAAuE;IACvE,aAAa;IACb,0EAA0E;IAC1E,oBAAoB;CACZ,CAAC;AAEX,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;AAGhE,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAAG,kBAAkB,CAAC,IAAI,CAAC","sourcesContent":["/**\n * founder-schema — Every error code the API answers with\n *\n * THE CODE IS WHAT A CLIENT ACTS ON; the `message` beside it is English for\n * logs and for a client too old to know the code. A client words each code in\n * the member's language, and a test on the web side asserts every value here\n * has a message in every locale it ships.\n *\n * Before 0.2.0 the backend kept this list to itself, and every domain refusal\n * (a closed poll, a reply to a reply, the edit window) went out as\n * `VALIDATION_ERROR` with an English sentence, so the only way for a client to\n * tell them apart was to match on the wording.\n *\n * WHEN TO USE WHICH:\n * - A request that does not match its schema: `VALIDATION_ERROR`, with every\n * problem in `error.details.issues`.\n * - A well-formed request the current STATE of things refuses: its own code\n * below. These carry no issues; the code says it all.\n *\n * ⚠ Append freely; remove deliberately. A code removed here that an older\n * server still sends fails the client's parse of the error envelope.\n *\n * @module founder-schema/core/errors\n */\n\nimport { z } from 'zod';\n\n/**\n * As a tuple, the way `GENDER_VALUES` is, so a consumer can iterate it (a\n * translation-coverage test does exactly that) and TypeScript keeps the union.\n */\nexport const API_ERROR_CODE_VALUES = [\n // ── Anything ──\n /** A bug or an outage on our side. The message says nothing more on purpose. */\n 'INTERNAL_ERROR',\n /** The request did not match its schema. `details.issues` says where. */\n 'VALIDATION_ERROR',\n /** No such thing, or not one you may see. Deliberately the same answer. */\n 'NOT_FOUND',\n /** Two writes raced for something unique; retrying usually succeeds. */\n 'CONFLICT',\n /** Too many requests from this member or address. Wait, then retry. */\n 'RATE_LIMITED',\n /** Something we depend on (the chat server) is down or not switched on. */\n 'SERVICE_UNAVAILABLE',\n\n // ── Who you are ──\n /** No credential at all. */\n 'UNAUTHORIZED',\n /** A credential that is not ours, or not readable. */\n 'INVALID_TOKEN',\n /** A credential that was ours and has expired. Refresh and retry. */\n 'TOKEN_EXPIRED',\n /** Signed out everywhere, or the account changed. Sign in again. */\n 'SESSION_INVALIDATED',\n /** Signed in, and not allowed to do this. */\n 'FORBIDDEN',\n\n // ── Onboarding ──\n /** Finish onboarding first. The client routes here instead of showing an error. */\n 'ONBOARDING_INCOMPLETE',\n /** Onboarding is already finished; there is nothing to save or finish again. */\n 'ALREADY_ONBOARDED',\n /** Under `MIN_AGE_YEARS`. Refused at the draft and again at finish. */\n 'UNDER_AGE',\n\n // ── Uploads: completing one, and attaching it ──\n /**\n * Not an upload of yours, not processed yet, already attached somewhere, or\n * for the other purpose (an avatar upload on a post). One code for all of\n * them on purpose: telling \"someone else's\" apart from \"does not exist\" would\n * let a caller probe other members' media ids.\n */\n 'UPLOAD_NOT_ATTACHABLE',\n /** Larger than `UPLOAD_MAX_BYTES`. */\n 'UPLOAD_TOO_LARGE',\n /**\n * Not one of the image types this product accepts (by the file's own bytes,\n * whatever its name or declared type), or not an image at all.\n */\n 'UPLOAD_WRONG_TYPE',\n /** Completed before the file reached storage: upload it, then complete. */\n 'UPLOAD_NOT_RECEIVED',\n /**\n * Another request is processing this upload right now (a retry or a double\n * tap). Nothing is wrong: complete again in a moment and it answers with\n * the image. 409.\n */\n 'UPLOAD_PROCESSING',\n /** A JPEG, PNG, WebP or GIF by its bytes that cannot be decoded: truncated or corrupt. */\n 'UPLOAD_UNREADABLE',\n /** More than `MEDIA_MAX_INPUT_PIXELS`; refused before decoding. */\n 'UPLOAD_TOO_MANY_PIXELS',\n /** `MEDIA_PENDING_MAX` uploads are already waiting to be attached. */\n 'UPLOAD_LIMIT_REACHED',\n /** Discarding an upload that is attached: remove it from its post or avatar instead. */\n 'UPLOAD_IN_USE',\n\n // ── The feed ──\n /** The post is older than `EDIT_WINDOW_MS`. */\n 'EDIT_WINDOW_CLOSED',\n /** A poll is never editable: people have already voted on it. */\n 'POLL_NOT_EDITABLE',\n /** The poll has closed, by the server's clock. */\n 'POLL_CLOSED',\n /** No option, several on a single-choice poll, or an option that does not exist. */\n 'POLL_CHOICE_INVALID',\n /** A reply to a reply. One level of nesting only. */\n 'REPLY_TOO_DEEP',\n /** Reporting your own post, poll or comment (or, since 0.6.0, your own product or review). */\n 'CANNOT_REPORT_OWN',\n /**\n * Marking a comment \"this helped\" that is not feedback on your product: the\n * post is not tagged with a product you are on, or the comment is your own\n * team's.\n */\n 'FEEDBACK_NOT_MARKABLE',\n /** Reposting your own post or poll (0.8.0). */\n 'CANNOT_REPOST_OWN',\n /** Reposting a repost (0.8.0): share the original instead. */\n 'CANNOT_REPOST_A_REPOST',\n /** You already have an active repost of this item (0.8.0). Undo it first. 409. */\n 'ALREADY_REPOSTED',\n\n // ── Products ──\n /** Founders own at most `PRODUCTS_OWNED_MAX` products. Delete one to add another. */\n 'PRODUCT_LIMIT_REACHED',\n /**\n * Listing a product that is not ready for Discover, or removing what keeps a\n * listed one ready. `details.missing`: some of `logo`, `tagline`, `screenshot`.\n */\n 'PRODUCT_NOT_LISTABLE',\n /**\n * The product is archived and frozen until its owner restores it: no new\n * posts about it, reviews, upvotes, replies, helpful marks, testers or\n * invitations.\n */\n 'PRODUCT_ARCHIVED',\n /** Tagging a post with, or acting for, a product you are not on the team of. */\n 'PRODUCT_NOT_YOURS',\n /** Inviting someone who is already on the team or already invited. */\n 'PRODUCT_ALREADY_MEMBER',\n /**\n * Inviting someone who cannot join a team: not finished onboarding, paused,\n * suspended or deleted. One code for all four, so it reveals none of them.\n */\n 'PRODUCT_MEMBER_NOT_INVITABLE',\n /** Handing over to, or re-roling, someone who is not on the team (or invited, for a role). */\n 'PRODUCT_NOT_ON_TEAM',\n /**\n * The owner leaving, being removed, or having their role changed. Ownership\n * moves only by handing the product to someone first.\n */\n 'PRODUCT_OWNER_MUST_TRANSFER',\n /** Upvoting, reviewing or confirming yourself as a tester of your own team's product. */\n 'PRODUCT_OWN_TEAM',\n /** Confirming as a tester someone who has given this product no feedback. */\n 'TESTER_NOT_ELIGIBLE',\n\n // ── Reviews ──\n /** One review per member per product. Edit yours instead. */\n 'REVIEW_EXISTS',\n /** Voting your own review helpful. */\n 'REVIEW_OWN_VOTE',\n\n // ── Moderation ──\n /**\n * A decision that does not apply to that kind of thing: removing a product\n * (staff hide one, never delete it). `MODERATION_ACTIONS_BY_TARGET_KIND`.\n */\n 'MODERATION_ACTION_INVALID',\n\n // ── Chat ──\n /** Opening a conversation with yourself. */\n 'CHAT_WITH_SELF',\n\n // ── The account ──\n /** Staff suspended this account: nothing in the product works until it is lifted. */\n 'ACCOUNT_SUSPENDED',\n\n // ── Roles and the MPIN ──\n /**\n * Switching to a role this person does not hold (or no longer holds), or\n * setting an MPIN while holding no staff role at all.\n */\n 'ROLE_NOT_HELD',\n /** A staff role needs an MPIN, and none has been set yet. Set one first. */\n 'MPIN_NOT_SET',\n /** An MPIN is set once; changing it needs the current one. */\n 'MPIN_ALREADY_SET',\n /** The MPIN is wrong. `details.attemptsLeft` says how many tries remain. */\n 'MPIN_INCORRECT',\n /** Too many wrong MPINs. `details.lockedUntil` says when to try again. */\n 'MPIN_LOCKED',\n /** Changing your own roles. Someone else has to. */\n 'CANNOT_CHANGE_OWN_ROLES',\n /** Granting or revoking a role the role you act as may not touch. */\n 'ROLE_NOT_GRANTABLE',\n\n // ── Support ──\n /** The ticket holds `TICKET_MESSAGES_MAX` messages. Open a new one. */\n 'TICKET_FULL',\n /** The team keeps `SAVED_REPLIES_MAX` saved replies. Delete one first. */\n 'SAVED_REPLIES_FULL',\n] as const;\n\nexport const apiErrorCodeSchema = z.enum(API_ERROR_CODE_VALUES);\nexport type ApiErrorCode = z.infer<typeof apiErrorCodeSchema>;\n\n/** The same codes by name: `API_ERROR_CODE.POLL_CLOSED`. */\nexport const API_ERROR_CODE = apiErrorCodeSchema.enum;\n"]}
@@ -153,7 +153,11 @@ export declare const REVIEW_REPLY_MAX = 1000;
153
153
  export declare const REVIEW_AVERAGE_MIN_COUNT = 3;
154
154
  /** Points one review can earn its writer through other members' Helpful votes. */
155
155
  export declare const REVIEW_HELPFUL_POINTS_MAX = 10;
156
- /** Reviews a day that earn points for writing them. More are welcome; they earn nothing. */
156
+ /**
157
+ * @deprecated 0.7.0: writing a review earns no points (the score counts only
158
+ * what OTHER members recognise), so nothing reads this. Kept so no export is
159
+ * removed.
160
+ */
157
161
  export declare const REVIEWS_REWARDED_PER_DAY = 3;
158
162
  /**
159
163
  * Points one member can earn from ONE other member over any 30 days, all kinds
@@ -1 +1 @@
1
- {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/core/limits.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"limits.d.ts","sourceRoot":"","sources":["../../src/core/limits.ts"],"names":[],"mappings":"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"}
@@ -166,7 +166,11 @@ export const REVIEW_AVERAGE_MIN_COUNT = 3;
166
166
  // ─── Contribution ────────────────────────────────────────────────────────────
167
167
  /** Points one review can earn its writer through other members' Helpful votes. */
168
168
  export const REVIEW_HELPFUL_POINTS_MAX = 10;
169
- /** Reviews a day that earn points for writing them. More are welcome; they earn nothing. */
169
+ /**
170
+ * @deprecated 0.7.0: writing a review earns no points (the score counts only
171
+ * what OTHER members recognise), so nothing reads this. Kept so no export is
172
+ * removed.
173
+ */
170
174
  export const REVIEWS_REWARDED_PER_DAY = 3;
171
175
  /**
172
176
  * Points one member can earn from ONE other member over any 30 days, all kinds
@@ -1 +1 @@
1
- {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/core/limits.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — Every length, size and count limit, by name\n *\n * ONE PLACE FOR EVERY NUMBER a client and the server must agree on. Before this,\n * the limits lived inline (`.max(80)`, `.max(1000)`) where nothing else could\n * read them, so the web client re-typed each one for its counters and the\n * backend re-typed them as Mongoose `maxlength`. Three copies of one number is\n * how the image alt text came to be 280 here and 500 in the backend.\n *\n * The rule from 0.2.0: a schema uses one of these constants, a consumer imports\n * the same constant, and nobody writes the digits again.\n *\n * Imports nothing, so any module here can use it without a cycle. Limits that\n * already had a name in their own module (POST_MAX_BODY_CHARS, POLL_*,\n * USERNAME_*, INTERESTS_*, MIN_AGE_YEARS...) stay where they are and keep their\n * names; this file holds the ones that had none.\n *\n * ⚠️ Lowering a limit is a breaking change for stored data: a value that was\n * valid when written fails the next edit. Raise freely; lower deliberately.\n *\n * @module founder-schema/core/limits\n */\n\n// ─── Profile ─────────────────────────────────────────────────────────────────\n\nexport const DISPLAY_NAME_MAX = 80;\nexport const HEADLINE_MAX = 120;\nexport const ABOUT_MAX = 1000;\n/** Skills a member may list. Discover filters on them. */\nexport const SKILLS_MAX = 8;\n/** Goals a member must pick at onboarding. The list itself caps the maximum. */\nexport const GOALS_MIN = 1;\n\n// ─── Location ────────────────────────────────────────────────────────────────\n\nexport const CITY_MAX = 80;\nexport const STATE_MAX = 80;\nexport const COUNTRY_MAX = 80;\n\n// ─── Venture ─────────────────────────────────────────────────────────────────\n\nexport const VENTURE_NAME_MAX = 120;\nexport const VENTURE_TAGLINE_MAX = 160;\n/** Team size including the founder. */\nexport const TEAM_SIZE_MIN = 1;\nexport const TEAM_SIZE_MAX = 100_000;\n/** Earliest founding year accepted. The ceiling is \"this year\", read per parse. */\nexport const FOUNDED_YEAR_MIN = 1900;\n\n// ─── Links ───────────────────────────────────────────────────────────────────\n\n/**\n * Longest URL a member may TYPE. Applied by `httpUrlSchema` itself, so every\n * member URL carries it: profile links, a venture website.\n *\n * ⚠ NOT applied to URLs the server reads from elsewhere (a link preview's\n * page or image), which use `safeUrlSchema`: a 314-character CDN image URL is\n * ordinary, and one of them would otherwise fail a whole feed page.\n */\nexport const LINK_URL_MAX = 300;\n\n// ─── Feed and moderation ─────────────────────────────────────────────────────\n\n/** Author-supplied image description. The backend's stored copy uses this too. */\nexport const MEDIA_ALT_MAX = 280;\nexport const LINK_PREVIEW_TITLE_MAX = 200;\nexport const LINK_PREVIEW_DESCRIPTION_MAX = 500;\n/** The free-text note on a report. */\nexport const REPORT_NOTE_MAX = 1000;\n/** A moderator's note on a decision (why it was hidden, removed or dismissed). */\nexport const MODERATION_NOTE_MAX = 500;\n/** What a post, poll or comment is shown as in a staff list: its first words. */\nexport const MODERATION_EXCERPT_MAX = 160;\n\n// ─── Staff ───────────────────────────────────────────────────────────────────\n\n/** Why a member was suspended. Shown to staff, and on record in the audit log. */\nexport const SUSPENSION_REASON_MIN = 3;\nexport const SUSPENSION_REASON_MAX = 500;\n/** The staff member directory's search box (a name, a handle or an email). */\nexport const MEMBER_SEARCH_MAX = 120;\n\n// ─── Support ─────────────────────────────────────────────────────────────────\n\nexport const TICKET_SUBJECT_MIN = 3;\nexport const TICKET_SUBJECT_MAX = 120;\n/** One message in a ticket, the member's or an agent's. */\nexport const TICKET_MESSAGE_MAX = 4000;\n/**\n * Messages one ticket may hold. A conversation that long is a new problem, so\n * the member is asked to open a new ticket rather than the thread growing\n * without bound.\n */\nexport const TICKET_MESSAGES_MAX = 200;\nexport const SAVED_REPLY_TITLE_MAX = 80;\nexport const SAVED_REPLY_BODY_MAX = 4000;\n/** Saved replies kept in all: a team's library, not an archive. */\nexport const SAVED_REPLIES_MAX = 200;\n\n// ─── Uploads ─────────────────────────────────────────────────────────────────\n\n/**\n * Largest file accepted for upload: 10 MB.\n *\n * Enforced three times. The upload-slot body refuses a bigger `contentLength`;\n * the size is SIGNED INTO the upload URL, so storage refuses a body of any other\n * size; and the server checks the stored object again before processing it.\n * The client checks first only to fail fast: it shrinks a photo well below this\n * before uploading (see `MEDIA_SIZES`), so a member rarely meets the limit.\n */\nexport const UPLOAD_MAX_BYTES = 10 * 1024 * 1024;\n\n/**\n * Largest image, in pixels, the server will decode: 50 megapixels, one frame.\n *\n * Bytes do not bound memory. A 50,000 x 50,000 PNG of one colour compresses to\n * a few kilobytes and decodes to ten gigabytes. The server reads the dimensions\n * first and refuses anything above this before decoding (`UPLOAD_TOO_MANY_PIXELS`).\n * 50 MP admits every phone camera's full-resolution photo up to 48 MP.\n */\nexport const MEDIA_MAX_INPUT_PIXELS = 50_000_000;\n\n/**\n * Hours an upload may wait to be attached to a post or set as an avatar. After\n * that the server deletes it: the composer was abandoned, or the tab closed.\n */\nexport const MEDIA_UNATTACHED_TTL_HOURS = 24;\n\n/**\n * Uploads one member may have waiting (requested or processed, not yet\n * attached) at once. A full post is `POST_MAX_MEDIA` (10); this leaves room for\n * a retry or two without letting one account fill storage.\n */\nexport const MEDIA_PENDING_MAX = 30;\n\n/** Longest placeholder string: a base64 ThumbHash is about 30 characters. */\nexport const MEDIA_PLACEHOLDER_MAX = 100;\n\n// ─── Lists and search ────────────────────────────────────────────────────────\n\n/** Values one Discover multi-select filter may carry (`?skill=a&skill=b`). */\nexport const DISCOVER_FILTER_VALUES_MAX = 10;\n/** Discover's free-text inputs: the name search, city and country. */\nexport const DISCOVER_TEXT_MAX = 80;\n/** Page size bounds for every cursor-paged list. */\nexport const PAGE_SIZE_MAX = 50;\nexport const PAGE_SIZE_DEFAULT = 20;\n\n// ─── Chat ────────────────────────────────────────────────────────────────────\n\n/**\n * Members one participants lookup may name. Bounded so the lookup cannot be\n * used to page through the directory.\n */\nexport const CHAT_PARTICIPANTS_MAX = 100;\n\n// ─── Products ────────────────────────────────────────────────────────────────\n\nexport const PRODUCT_NAME_MAX = 60;\n/** One line on a Discover card: short enough to never wrap twice on a phone. */\nexport const PRODUCT_TAGLINE_MAX = 80;\nexport const PRODUCT_DESCRIPTION_MAX = 3000;\n/** Screenshots on one product page. The first is its cover on Discover. */\nexport const PRODUCT_SCREENSHOTS_MAX = 8;\nexport const PRODUCT_CAPTION_MAX = 140;\n/**\n * Products one founder may OWN at once (archived ones count; deleting frees a\n * place). Being on someone else's team is not counted: the user ruled there is\n * no limit on a product's team, and so none on how many teams one joins.\n */\nexport const PRODUCTS_OWNED_MAX = 5;\n/** Team members a product page shows; the rest are one tap away (`/team`). */\nexport const PRODUCT_TEAM_SHOWN = 12;\n/**\n * Days a post's ask keeps its product on the \"asking for testers\" (or feedback,\n * launch, ideas) shelf. Each new post with that ask restarts the clock.\n */\nexport const PRODUCT_ASK_OPEN_DAYS = 14;\n\n// ─── Reviews ─────────────────────────────────────────────────────────────────\n\n/**\n * A review says WHY. A star with no reason does not help the founder, which is\n * the whole point of reviewing here, so the text is required and has a floor.\n */\nexport const REVIEW_TEXT_MIN = 30;\nexport const REVIEW_TEXT_MAX = 2000;\nexport const REVIEW_REPLY_MAX = 1000;\n/**\n * Reviews before a product shows an average (and enters \"Top rated\"). One or two\n * ratings say more about who wrote them than about the product.\n */\nexport const REVIEW_AVERAGE_MIN_COUNT = 3;\n\n// ─── Contribution ────────────────────────────────────────────────────────────\n\n/** Points one review can earn its writer through other members' Helpful votes. */\nexport const REVIEW_HELPFUL_POINTS_MAX = 10;\n/** Reviews a day that earn points for writing them. More are welcome; they earn nothing. */\nexport const REVIEWS_REWARDED_PER_DAY = 3;\n/**\n * Points one member can earn from ONE other member over any 30 days, all kinds\n * together, so two founders cannot farm each other by trading Helpful marks.\n */\nexport const CONTRIBUTION_PAIR_POINTS_MAX = 30;\n/** Window of the \"this month\" score and the top-helpers list. */\nexport const CONTRIBUTION_RECENT_DAYS = 30;\n/** Rows on the top-helpers list. */\nexport const CONTRIBUTION_LEADERBOARD_SIZE = 20;\n\n// ─── Notifications ───────────────────────────────────────────────────────────\n\n/** Days a notification is kept. Neither Jansathi nor GymMonk ever deletes one. */\nexport const NOTIFICATION_RETENTION_DAYS = 90;\n/** The first words of a comment or review a notification shows. */\nexport const NOTIFICATION_EXCERPT_MAX = 140;\n"]}
1
+ {"version":3,"file":"limits.js","sourceRoot":"","sources":["../../src/core/limits.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema — Every length, size and count limit, by name\n *\n * ONE PLACE FOR EVERY NUMBER a client and the server must agree on. Before this,\n * the limits lived inline (`.max(80)`, `.max(1000)`) where nothing else could\n * read them, so the web client re-typed each one for its counters and the\n * backend re-typed them as Mongoose `maxlength`. Three copies of one number is\n * how the image alt text came to be 280 here and 500 in the backend.\n *\n * The rule from 0.2.0: a schema uses one of these constants, a consumer imports\n * the same constant, and nobody writes the digits again.\n *\n * Imports nothing, so any module here can use it without a cycle. Limits that\n * already had a name in their own module (POST_MAX_BODY_CHARS, POLL_*,\n * USERNAME_*, INTERESTS_*, MIN_AGE_YEARS...) stay where they are and keep their\n * names; this file holds the ones that had none.\n *\n * ⚠️ Lowering a limit is a breaking change for stored data: a value that was\n * valid when written fails the next edit. Raise freely; lower deliberately.\n *\n * @module founder-schema/core/limits\n */\n\n// ─── Profile ─────────────────────────────────────────────────────────────────\n\nexport const DISPLAY_NAME_MAX = 80;\nexport const HEADLINE_MAX = 120;\nexport const ABOUT_MAX = 1000;\n/** Skills a member may list. Discover filters on them. */\nexport const SKILLS_MAX = 8;\n/** Goals a member must pick at onboarding. The list itself caps the maximum. */\nexport const GOALS_MIN = 1;\n\n// ─── Location ────────────────────────────────────────────────────────────────\n\nexport const CITY_MAX = 80;\nexport const STATE_MAX = 80;\nexport const COUNTRY_MAX = 80;\n\n// ─── Venture ─────────────────────────────────────────────────────────────────\n\nexport const VENTURE_NAME_MAX = 120;\nexport const VENTURE_TAGLINE_MAX = 160;\n/** Team size including the founder. */\nexport const TEAM_SIZE_MIN = 1;\nexport const TEAM_SIZE_MAX = 100_000;\n/** Earliest founding year accepted. The ceiling is \"this year\", read per parse. */\nexport const FOUNDED_YEAR_MIN = 1900;\n\n// ─── Links ───────────────────────────────────────────────────────────────────\n\n/**\n * Longest URL a member may TYPE. Applied by `httpUrlSchema` itself, so every\n * member URL carries it: profile links, a venture website.\n *\n * ⚠ NOT applied to URLs the server reads from elsewhere (a link preview's\n * page or image), which use `safeUrlSchema`: a 314-character CDN image URL is\n * ordinary, and one of them would otherwise fail a whole feed page.\n */\nexport const LINK_URL_MAX = 300;\n\n// ─── Feed and moderation ─────────────────────────────────────────────────────\n\n/** Author-supplied image description. The backend's stored copy uses this too. */\nexport const MEDIA_ALT_MAX = 280;\nexport const LINK_PREVIEW_TITLE_MAX = 200;\nexport const LINK_PREVIEW_DESCRIPTION_MAX = 500;\n/** The free-text note on a report. */\nexport const REPORT_NOTE_MAX = 1000;\n/** A moderator's note on a decision (why it was hidden, removed or dismissed). */\nexport const MODERATION_NOTE_MAX = 500;\n/** What a post, poll or comment is shown as in a staff list: its first words. */\nexport const MODERATION_EXCERPT_MAX = 160;\n\n// ─── Staff ───────────────────────────────────────────────────────────────────\n\n/** Why a member was suspended. Shown to staff, and on record in the audit log. */\nexport const SUSPENSION_REASON_MIN = 3;\nexport const SUSPENSION_REASON_MAX = 500;\n/** The staff member directory's search box (a name, a handle or an email). */\nexport const MEMBER_SEARCH_MAX = 120;\n\n// ─── Support ─────────────────────────────────────────────────────────────────\n\nexport const TICKET_SUBJECT_MIN = 3;\nexport const TICKET_SUBJECT_MAX = 120;\n/** One message in a ticket, the member's or an agent's. */\nexport const TICKET_MESSAGE_MAX = 4000;\n/**\n * Messages one ticket may hold. A conversation that long is a new problem, so\n * the member is asked to open a new ticket rather than the thread growing\n * without bound.\n */\nexport const TICKET_MESSAGES_MAX = 200;\nexport const SAVED_REPLY_TITLE_MAX = 80;\nexport const SAVED_REPLY_BODY_MAX = 4000;\n/** Saved replies kept in all: a team's library, not an archive. */\nexport const SAVED_REPLIES_MAX = 200;\n\n// ─── Uploads ─────────────────────────────────────────────────────────────────\n\n/**\n * Largest file accepted for upload: 10 MB.\n *\n * Enforced three times. The upload-slot body refuses a bigger `contentLength`;\n * the size is SIGNED INTO the upload URL, so storage refuses a body of any other\n * size; and the server checks the stored object again before processing it.\n * The client checks first only to fail fast: it shrinks a photo well below this\n * before uploading (see `MEDIA_SIZES`), so a member rarely meets the limit.\n */\nexport const UPLOAD_MAX_BYTES = 10 * 1024 * 1024;\n\n/**\n * Largest image, in pixels, the server will decode: 50 megapixels, one frame.\n *\n * Bytes do not bound memory. A 50,000 x 50,000 PNG of one colour compresses to\n * a few kilobytes and decodes to ten gigabytes. The server reads the dimensions\n * first and refuses anything above this before decoding (`UPLOAD_TOO_MANY_PIXELS`).\n * 50 MP admits every phone camera's full-resolution photo up to 48 MP.\n */\nexport const MEDIA_MAX_INPUT_PIXELS = 50_000_000;\n\n/**\n * Hours an upload may wait to be attached to a post or set as an avatar. After\n * that the server deletes it: the composer was abandoned, or the tab closed.\n */\nexport const MEDIA_UNATTACHED_TTL_HOURS = 24;\n\n/**\n * Uploads one member may have waiting (requested or processed, not yet\n * attached) at once. A full post is `POST_MAX_MEDIA` (10); this leaves room for\n * a retry or two without letting one account fill storage.\n */\nexport const MEDIA_PENDING_MAX = 30;\n\n/** Longest placeholder string: a base64 ThumbHash is about 30 characters. */\nexport const MEDIA_PLACEHOLDER_MAX = 100;\n\n// ─── Lists and search ────────────────────────────────────────────────────────\n\n/** Values one Discover multi-select filter may carry (`?skill=a&skill=b`). */\nexport const DISCOVER_FILTER_VALUES_MAX = 10;\n/** Discover's free-text inputs: the name search, city and country. */\nexport const DISCOVER_TEXT_MAX = 80;\n/** Page size bounds for every cursor-paged list. */\nexport const PAGE_SIZE_MAX = 50;\nexport const PAGE_SIZE_DEFAULT = 20;\n\n// ─── Chat ────────────────────────────────────────────────────────────────────\n\n/**\n * Members one participants lookup may name. Bounded so the lookup cannot be\n * used to page through the directory.\n */\nexport const CHAT_PARTICIPANTS_MAX = 100;\n\n// ─── Products ────────────────────────────────────────────────────────────────\n\nexport const PRODUCT_NAME_MAX = 60;\n/** One line on a Discover card: short enough to never wrap twice on a phone. */\nexport const PRODUCT_TAGLINE_MAX = 80;\nexport const PRODUCT_DESCRIPTION_MAX = 3000;\n/** Screenshots on one product page. The first is its cover on Discover. */\nexport const PRODUCT_SCREENSHOTS_MAX = 8;\nexport const PRODUCT_CAPTION_MAX = 140;\n/**\n * Products one founder may OWN at once (archived ones count; deleting frees a\n * place). Being on someone else's team is not counted: the user ruled there is\n * no limit on a product's team, and so none on how many teams one joins.\n */\nexport const PRODUCTS_OWNED_MAX = 5;\n/** Team members a product page shows; the rest are one tap away (`/team`). */\nexport const PRODUCT_TEAM_SHOWN = 12;\n/**\n * Days a post's ask keeps its product on the \"asking for testers\" (or feedback,\n * launch, ideas) shelf. Each new post with that ask restarts the clock.\n */\nexport const PRODUCT_ASK_OPEN_DAYS = 14;\n\n// ─── Reviews ─────────────────────────────────────────────────────────────────\n\n/**\n * A review says WHY. A star with no reason does not help the founder, which is\n * the whole point of reviewing here, so the text is required and has a floor.\n */\nexport const REVIEW_TEXT_MIN = 30;\nexport const REVIEW_TEXT_MAX = 2000;\nexport const REVIEW_REPLY_MAX = 1000;\n/**\n * Reviews before a product shows an average (and enters \"Top rated\"). One or two\n * ratings say more about who wrote them than about the product.\n */\nexport const REVIEW_AVERAGE_MIN_COUNT = 3;\n\n// ─── Contribution ────────────────────────────────────────────────────────────\n\n/** Points one review can earn its writer through other members' Helpful votes. */\nexport const REVIEW_HELPFUL_POINTS_MAX = 10;\n/**\n * @deprecated 0.7.0: writing a review earns no points (the score counts only\n * what OTHER members recognise), so nothing reads this. Kept so no export is\n * removed.\n */\nexport const REVIEWS_REWARDED_PER_DAY = 3;\n/**\n * Points one member can earn from ONE other member over any 30 days, all kinds\n * together, so two founders cannot farm each other by trading Helpful marks.\n */\nexport const CONTRIBUTION_PAIR_POINTS_MAX = 30;\n/** Window of the \"this month\" score and the top-helpers list. */\nexport const CONTRIBUTION_RECENT_DAYS = 30;\n/** Rows on the top-helpers list. */\nexport const CONTRIBUTION_LEADERBOARD_SIZE = 20;\n\n// ─── Notifications ───────────────────────────────────────────────────────────\n\n/** Days a notification is kept. Neither Jansathi nor GymMonk ever deletes one. */\nexport const NOTIFICATION_RETENTION_DAYS = 90;\n/** The first words of a comment or review a notification shows. */\nexport const NOTIFICATION_EXCERPT_MAX = 140;\n"]}
@@ -78,6 +78,9 @@ export declare const apiErrorSchema: z.ZodObject<{
78
78
  REPLY_TOO_DEEP: "REPLY_TOO_DEEP";
79
79
  CANNOT_REPORT_OWN: "CANNOT_REPORT_OWN";
80
80
  FEEDBACK_NOT_MARKABLE: "FEEDBACK_NOT_MARKABLE";
81
+ CANNOT_REPOST_OWN: "CANNOT_REPOST_OWN";
82
+ CANNOT_REPOST_A_REPOST: "CANNOT_REPOST_A_REPOST";
83
+ ALREADY_REPOSTED: "ALREADY_REPOSTED";
81
84
  PRODUCT_LIMIT_REACHED: "PRODUCT_LIMIT_REACHED";
82
85
  PRODUCT_NOT_LISTABLE: "PRODUCT_NOT_LISTABLE";
83
86
  PRODUCT_ARCHIVED: "PRODUCT_ARCHIVED";
@@ -169,6 +172,9 @@ export declare const errorResponseSchema: z.ZodObject<{
169
172
  REPLY_TOO_DEEP: "REPLY_TOO_DEEP";
170
173
  CANNOT_REPORT_OWN: "CANNOT_REPORT_OWN";
171
174
  FEEDBACK_NOT_MARKABLE: "FEEDBACK_NOT_MARKABLE";
175
+ CANNOT_REPOST_OWN: "CANNOT_REPOST_OWN";
176
+ CANNOT_REPOST_A_REPOST: "CANNOT_REPOST_A_REPOST";
177
+ ALREADY_REPOSTED: "ALREADY_REPOSTED";
172
178
  PRODUCT_LIMIT_REACHED: "PRODUCT_LIMIT_REACHED";
173
179
  PRODUCT_NOT_LISTABLE: "PRODUCT_NOT_LISTABLE";
174
180
  PRODUCT_ARCHIVED: "PRODUCT_ARCHIVED";
@@ -1 +1 @@
1
- {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/core/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;iBAQhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAIzB,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;AAEL;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;iBAAmC,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"responses.d.ts","sourceRoot":"","sources":["../../src/core/responses.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;iBAQhC,CAAC;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;GAGG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAIzB,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;AAEL;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;iBAAmC,CAAC;AACjE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
@@ -7,8 +7,10 @@
7
7
  * tester_confirmed a product's team confirms you tested their product +15
8
8
  * review_helpful another member finds your review helpful +2
9
9
  * (at most `REVIEW_HELPFUL_POINTS_MAX` per review)
10
- * review_written you review a product (at most `REVIEWS_REWARDED_PER_DAY`
11
- * a day earn points) +3
10
+ *
11
+ * `review_written` is still listed so older rows and clients parse, but it is
12
+ * NEVER awarded (0.7.0): writing a review is your own act, and the score
13
+ * counts only what others recognise. A review earns through Helpful votes.
12
14
  *
13
15
  * Nothing a member does for their OWN product counts, and nothing their own
14
16
  * product receives, so the score cannot be bought with a popular product.
@@ -40,6 +42,11 @@ export declare const contributionKindSchema: z.ZodEnum<{
40
42
  review_helpful: "review_helpful";
41
43
  review_written: "review_written";
42
44
  }>;
45
+ /**
46
+ * The kinds that are actually awarded (0.7.0): every kind but `review_written`,
47
+ * which is never awarded any more. "How points are earned" lists these.
48
+ */
49
+ export declare const CONTRIBUTION_AWARDED_KIND_VALUES: ("feedback_helpful" | "tester_confirmed" | "review_helpful")[];
43
50
  export type ContributionKind = z.infer<typeof contributionKindSchema>;
44
51
  /**
45
52
  * Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;
@@ -1 +1 @@
1
- {"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/domains/contributions/contributions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,4CAA4C;AAC5C,eAAO,MAAM,wBAAwB,uFAK3B,CAAC;AACX,eAAO,MAAM,sBAAsB;;;;;EAAmC,CAAC;AACvE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;CAKqB,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAiBlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA2C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B;;;;;;;;;;iBAKxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
1
+ {"version":3,"file":"contributions.d.ts","sourceRoot":"","sources":["../../../src/domains/contributions/contributions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,4CAA4C;AAC5C,eAAO,MAAM,wBAAwB,uFAK3B,CAAC;AACX,eAAO,MAAM,sBAAsB;;;;;EAAmC,CAAC;AACvE;;;GAGG;AACH,eAAO,MAAM,gCAAgC,gEAG5C,CAAC;AACF,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE;;;GAGG;AACH,eAAO,MAAM,mBAAmB;;;;;CAKqB,CAAC;AAEtD,4CAA4C;AAC5C,eAAO,MAAM,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAoBlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAA2C,CAAC;AAC/E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B;;;;;;;;;;iBAKxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC;AAEpF;;;GAGG;AACH,eAAO,MAAM,6BAA6B;;;;;;;;;;;;;iBAGxC,CAAC;AACH,MAAM,MAAM,uBAAuB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,6BAA6B,CAAC,CAAC"}
@@ -7,8 +7,10 @@
7
7
  * tester_confirmed a product's team confirms you tested their product +15
8
8
  * review_helpful another member finds your review helpful +2
9
9
  * (at most `REVIEW_HELPFUL_POINTS_MAX` per review)
10
- * review_written you review a product (at most `REVIEWS_REWARDED_PER_DAY`
11
- * a day earn points) +3
10
+ *
11
+ * `review_written` is still listed so older rows and clients parse, but it is
12
+ * NEVER awarded (0.7.0): writing a review is your own act, and the score
13
+ * counts only what others recognise. A review earns through Helpful votes.
12
14
  *
13
15
  * Nothing a member does for their OWN product counts, and nothing their own
14
16
  * product receives, so the score cannot be bought with a popular product.
@@ -45,6 +47,11 @@ export const CONTRIBUTION_KIND_VALUES = [
45
47
  'review_written',
46
48
  ];
47
49
  export const contributionKindSchema = z.enum(CONTRIBUTION_KIND_VALUES);
50
+ /**
51
+ * The kinds that are actually awarded (0.7.0): every kind but `review_written`,
52
+ * which is never awarded any more. "How points are earned" lists these.
53
+ */
54
+ export const CONTRIBUTION_AWARDED_KIND_VALUES = CONTRIBUTION_KIND_VALUES.filter((k) => k !== 'review_written');
48
55
  /**
49
56
  * Points per kind. Exhaustive by type. Changing a number changes FUTURE rows;
50
57
  * the hourly recount sums what each row was actually awarded.
@@ -61,13 +68,16 @@ export const contributionEntrySchema = z.object({
61
68
  kind: contributionKindSchema,
62
69
  /** What it was actually awarded: below the kind's points when a cap applied. */
63
70
  points: z.number().int().positive(),
64
- /** Who recognised it; null for a deleted account, and for `review_written`. */
71
+ /**
72
+ * Who recognised it; null for a deleted account, and for `review_helpful`
73
+ * (a Helpful vote is anonymous: the review shows only a count).
74
+ */
65
75
  actor: memberBylineSchema.nullable(),
66
76
  /** The product it was about, while it exists. */
67
77
  product: productChipSchema.optional(),
68
78
  /**
69
79
  * What earned it, so the row opens it: the post or poll and the comment for
70
- * `feedback_helpful`, the review for `review_helpful` and `review_written`.
80
+ * `feedback_helpful`, the review for `review_helpful`.
71
81
  */
72
82
  content: z.object({ kind: contentKindSchema, id: objectIdSchema }).optional(),
73
83
  commentId: objectIdSchema.optional(),