founder-schema 0.16.0 → 0.18.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 (116) hide show
  1. package/README.md +263 -19
  2. package/dist/access/access.d.ts +1 -1
  3. package/dist/access/access.d.ts.map +1 -1
  4. package/dist/access/access.js +4 -1
  5. package/dist/access/access.js.map +1 -1
  6. package/dist/access/permissions.d.ts +27 -29
  7. package/dist/access/permissions.d.ts.map +1 -1
  8. package/dist/access/permissions.js +39 -3
  9. package/dist/access/permissions.js.map +1 -1
  10. package/dist/core/common.d.ts +117 -23
  11. package/dist/core/common.d.ts.map +1 -1
  12. package/dist/core/common.js +104 -21
  13. package/dist/core/common.js.map +1 -1
  14. package/dist/core/errors.d.ts +7 -5
  15. package/dist/core/errors.d.ts.map +1 -1
  16. package/dist/core/errors.js +17 -11
  17. package/dist/core/errors.js.map +1 -1
  18. package/dist/core/limits.d.ts +55 -17
  19. package/dist/core/limits.d.ts.map +1 -1
  20. package/dist/core/limits.js +59 -22
  21. package/dist/core/limits.js.map +1 -1
  22. package/dist/core/money.d.ts +33 -0
  23. package/dist/core/money.d.ts.map +1 -0
  24. package/dist/core/money.js +36 -0
  25. package/dist/core/money.js.map +1 -0
  26. package/dist/core/responses.d.ts +14 -4
  27. package/dist/core/responses.d.ts.map +1 -1
  28. package/dist/core/responses.js.map +1 -1
  29. package/dist/core/validation.d.ts +32 -2
  30. package/dist/core/validation.d.ts.map +1 -1
  31. package/dist/core/validation.js +32 -2
  32. package/dist/core/validation.js.map +1 -1
  33. package/dist/domains/billing/billing.d.ts +61 -8
  34. package/dist/domains/billing/billing.d.ts.map +1 -1
  35. package/dist/domains/billing/billing.js +58 -22
  36. package/dist/domains/billing/billing.js.map +1 -1
  37. package/dist/domains/billing/gst.d.ts.map +1 -1
  38. package/dist/domains/billing/gst.js +4 -3
  39. package/dist/domains/billing/gst.js.map +1 -1
  40. package/dist/domains/chat/chat.d.ts +0 -21
  41. package/dist/domains/chat/chat.d.ts.map +1 -1
  42. package/dist/domains/chat/chat.js +0 -20
  43. package/dist/domains/chat/chat.js.map +1 -1
  44. package/dist/domains/communities/communities.d.ts +30 -12
  45. package/dist/domains/communities/communities.d.ts.map +1 -1
  46. package/dist/domains/communities/communities.js +43 -15
  47. package/dist/domains/communities/communities.js.map +1 -1
  48. package/dist/domains/contributions/contributions.d.ts +1 -1
  49. package/dist/domains/contributions/contributions.js +1 -1
  50. package/dist/domains/contributions/contributions.js.map +1 -1
  51. package/dist/domains/dashboard/dashboard.d.ts +3 -1
  52. package/dist/domains/dashboard/dashboard.d.ts.map +1 -1
  53. package/dist/domains/dashboard/dashboard.js +23 -7
  54. package/dist/domains/dashboard/dashboard.js.map +1 -1
  55. package/dist/domains/discover/discover.d.ts +8 -8
  56. package/dist/domains/feed/feed.d.ts +55 -153
  57. package/dist/domains/feed/feed.d.ts.map +1 -1
  58. package/dist/domains/feed/feed.js +53 -58
  59. package/dist/domains/feed/feed.js.map +1 -1
  60. package/dist/domains/location/india.d.ts +215 -0
  61. package/dist/domains/location/india.d.ts.map +1 -0
  62. package/dist/domains/location/india.js +1203 -0
  63. package/dist/domains/location/india.js.map +1 -0
  64. package/dist/domains/members/members.d.ts +2 -2
  65. package/dist/domains/membership/membership.d.ts +26 -0
  66. package/dist/domains/membership/membership.d.ts.map +1 -1
  67. package/dist/domains/membership/membership.js +55 -1
  68. package/dist/domains/membership/membership.js.map +1 -1
  69. package/dist/domains/moderation/moderation.d.ts +2 -2
  70. package/dist/domains/moderation/moderation.js +3 -3
  71. package/dist/domains/moderation/moderation.js.map +1 -1
  72. package/dist/domains/notifications/notifications.d.ts +1 -10
  73. package/dist/domains/notifications/notifications.d.ts.map +1 -1
  74. package/dist/domains/notifications/notifications.js +2 -3
  75. package/dist/domains/notifications/notifications.js.map +1 -1
  76. package/dist/domains/onboarding/onboarding-answers.d.ts +112 -0
  77. package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -0
  78. package/dist/domains/onboarding/onboarding-answers.js +164 -0
  79. package/dist/domains/onboarding/onboarding-answers.js.map +1 -0
  80. package/dist/domains/onboarding/onboarding.d.ts +398 -62
  81. package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
  82. package/dist/domains/onboarding/onboarding.js +229 -68
  83. package/dist/domains/onboarding/onboarding.js.map +1 -1
  84. package/dist/domains/products/products.d.ts +43 -8
  85. package/dist/domains/products/products.d.ts.map +1 -1
  86. package/dist/domains/products/products.js +57 -11
  87. package/dist/domains/products/products.js.map +1 -1
  88. package/dist/domains/profile/founder-profile.d.ts +120 -68
  89. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  90. package/dist/domains/profile/founder-profile.js +80 -29
  91. package/dist/domains/profile/founder-profile.js.map +1 -1
  92. package/dist/domains/profile/my-profile.d.ts +3 -3
  93. package/dist/domains/reviews/reviews.d.ts +1 -1
  94. package/dist/domains/reviews/reviews.d.ts.map +1 -1
  95. package/dist/domains/reviews/reviews.js +2 -2
  96. package/dist/domains/reviews/reviews.js.map +1 -1
  97. package/dist/domains/support/support.d.ts +22 -3
  98. package/dist/domains/support/support.d.ts.map +1 -1
  99. package/dist/domains/support/support.js +19 -5
  100. package/dist/domains/support/support.js.map +1 -1
  101. package/dist/domains/team/team.d.ts.map +1 -1
  102. package/dist/domains/team/team.js +3 -6
  103. package/dist/domains/team/team.js.map +1 -1
  104. package/dist/domains/uploads/uploads.d.ts +7 -22
  105. package/dist/domains/uploads/uploads.d.ts.map +1 -1
  106. package/dist/domains/uploads/uploads.js +7 -21
  107. package/dist/domains/uploads/uploads.js.map +1 -1
  108. package/dist/env.d.ts +26 -13
  109. package/dist/env.d.ts.map +1 -1
  110. package/dist/env.js +58 -23
  111. package/dist/env.js.map +1 -1
  112. package/dist/index.d.ts +18 -15
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +19 -14
  115. package/dist/index.js.map +1 -1
  116. package/package.json +1 -1
package/README.md CHANGED
@@ -29,11 +29,11 @@ Consumed by:
29
29
 
30
30
  ## Installing it
31
31
 
32
- On npm since 0.1.0. Pin the EXACT version, with `-E` so npm writes `0.2.0` and
33
- not `^0.2.0` (a caret on `0.x` pins the minor; see Publishing):
32
+ On npm since 0.1.0. Pin the EXACT version, with `-E` so npm writes `0.18.0` and
33
+ not `^0.18.0` (a caret on `0.x` pins the minor; see Publishing):
34
34
 
35
35
  ```sh
36
- npm install -E founder-schema@0.2.0
36
+ npm install -E founder-schema@0.18.0
37
37
  ```
38
38
 
39
39
  **Working against a change that is not published yet** is the path TEAM.md
@@ -86,19 +86,251 @@ SAME schema object, so the two can never disagree about what is allowed.
86
86
  (`ONBOARDING_STEP_SCHEMAS`), so a step the wizard lets through is a step the
87
87
  finish accepts. The step list (`FOUNDER_ONBOARDING_STEPS`) is built from those
88
88
  slices, including whether a step may be skipped.
89
- - **No copy.** Step titles, help lines and topic categories are translation keys
90
- (`titleKey`, `helpKey`, `labelKey`); the words live in each client.
89
+ - **No copy.** A step's label and title, a topic category and a goal category
90
+ are translation keys or plain keys (`labelKey`, `titleKey`, `key`); the words
91
+ live in each client. The web keys its category words by `key`, type-checked
92
+ against it, and its step words by the step's own key (`OnboardingStepKey`);
93
+ `labelKey` / `titleKey` are there for a translated client (i18n), and
94
+ nothing reads them yet.
91
95
  - **Bodies are typed JSON; only queries coerce.** `teamSize: "5"` is refused in a
92
96
  body, and `?limit=5` is read as 5.
93
- - **Shapes are derived, never copied.** One `locationSchema`, one
94
- `founderLinksSchema`, one `ventureSchema`, one `mediaImageSchema`; the other
95
- places pick, omit or reuse them, and a test checks by object identity.
97
+ - **Shapes are derived, never copied.** One `locationSchema` to READ a place and
98
+ one `indiaLocationSchema` to WRITE one (0.17.0), one `founderLinksSchema`, one
99
+ `ventureSchema`, one `mediaImageSchema`; the other places pick, omit or reuse
100
+ them, and a test checks by object identity.
96
101
 
97
102
  `zod` is a **peer** dependency (`^4.4.0`), on purpose. The consumer owns the Zod
98
103
  instance; this package must not bring its own.
99
104
 
100
105
  ---
101
106
 
107
+ ## Breaking in 0.18.0: the SSOT audit (2026-10-20)
108
+
109
+ The user, 2026-10-20: "make sure it is the SSOT for data types, validation,
110
+ etc., for front end and back end, implemented thoroughly end to end, no
111
+ patchwork or lazy workaround, and a proper cleanup". Ten read-only reviewers,
112
+ every high finding verified in code. Each rule below lived in two places, or in
113
+ one consumer only, or as a hand-written literal; it is one export now, and both
114
+ consumers use it (`tests/ssot-0-18.test.ts`, one test per rule).
115
+
116
+ **Removed** (no consumer imports any; listed in `public-surface.test.ts`):
117
+ `chatInvitation*`, the 0.4.0 upload-key code (`uploadKey*`, `UPLOAD_KEY_*`,
118
+ `uploadKeyInvalid`; the picker's extensions are `UPLOAD_FILE_EXTENSIONS` now),
119
+ `PRODUCTS_OWNED_MAX`, `REVIEWS_REWARDED_PER_DAY`, the never-sent codes
120
+ `ORDER_EXPIRED` and `COUPON_FOUNDING_EXISTS`, the unused zod wrappers
121
+ `actionSchema`, `purchaseKindSchema`, `notificationPreferenceSchema`,
122
+ `onboardingProgressSchema`, the uploaded-video shape (`feedVideoSchema`: post
123
+ media is images only) and the link preview no server writes (`linkPreview*`,
124
+ `LINK_PREVIEW_*`).
125
+
126
+ **Stricter or changed on the wire:**
127
+ - `PATCH /onboarding` takes the draft in the FORM's shape
128
+ (`onboardingFormSchema.partial().strict()`): typed and bounded, half-answered
129
+ still fine. It took any JSON before.
130
+ - `PATCH /profiles/me` takes only its own fields: `headline`, `about`, `skills`,
131
+ `goals`, `links`. The name and the city are the personal details', the venture
132
+ the business editor's. `goals` keeps at least one (`requiredGoalListSchema`,
133
+ the user), as onboarding's finish does.
134
+ - `PATCH /feed/:id` may clear the text; the server refuses only a post left with
135
+ nothing (`postHasContent`, the create body's rule too).
136
+ - A vote names each option once (`pollVoteDuplicate`). An emptied `?cursor=`
137
+ means none; a cursor is at most `CURSOR_MAX`.
138
+ - A blank ban reason means no reason. The MPIN rule reads `MPIN_LENGTH`.
139
+ - Responses: `grantFailed` is a `GRANT_FAILURE_VALUES`; the dashboard's support
140
+ block adds `waitingTickets` and `openTickets` is status `open` only; product
141
+ links are READ leniently (`productLinksReadSchema`), so an older link cannot
142
+ break a page; ids and usernames are typed where they were strings; excerpts
143
+ are bounded; packed CASL rules are checked (action and subject).
144
+ - New codes for refusals that were a bare `FORBIDDEN`:
145
+ `COMMUNITY_NOT_ON_YOURSELF`, `HIDDEN_BY_STAFF`, `TICKET_MEMBER_ONLY`; body
146
+ messages `bodyTooLarge`, `bodyNotJson`.
147
+ - A community's last admin, alone in it, may leave: that ARCHIVES it (on the
148
+ audit record, as a staff archive is). A plain member last in an adminless
149
+ community leaves it as it was.
150
+ - Every request body types what a caller SENDS: no body field is a
151
+ `z.preprocess` (whose input type is `unknown`), so a web typing a body by
152
+ `z.input<typeof xBodySchema>` cannot send the wrong type and compile.
153
+ `optionalTrimmedString` and `indiaLocationSchema.state` were the two; a test
154
+ walks every exported body and fails on any preprocess. Same runtime rules.
155
+ - A model refusing what the schema accepted answers `500 INTERNAL_ERROR` (our
156
+ mismatch), never `400`; body-parse refusals carry message KEYS.
157
+ - `PATCH /profiles/me` is STRICT: a key that belongs to another editor is a
158
+ 400 naming it, never dropped without a word.
159
+ - `grantFailed` adds `GRANT_REFUSED`, any other refusal of a grant: still
160
+ recorded and refunded, never a capture rolled back for a reason not listed.
161
+ - `changesDiscoverCard` ignores points (`FOUNDER_CARD_EDIT_FIELDS`): other
162
+ people's acts move them, and they never lift a member up Discover.
163
+ - New: `keptPaise` (what is kept of a payment: the Overview's revenue) and
164
+ `refundLeftPaise` (what a refund returns: never zeroed by a credit note, which
165
+ a failed refund carries), `CONTRIBUTION_PAIR_WINDOW_DAYS` (the pair cap's own
166
+ window), `RAZORPAY_EVENT_RETENTION_DAYS`, `communityTypeChangeAllowed` (an
167
+ archived community narrows, never opens), `communityViewerSchema.leaveArchives`
168
+ (the server says what leaving does), `envVar.optionalOneOf`;
169
+ `onboardingDraftForStep` answers the form's own type, so a step save needs no
170
+ cast.
171
+ - `founder-schema/env`: a BLANK line never takes a real fallback (a port,
172
+ localhost, `development`, a database name): it is refused at start-up and
173
+ named. Unset still takes it; a blank fallback (`''`, a feature off) still
174
+ takes a blank line. `optionalText({ refuseBlank: true })` is for a setting
175
+ whose absence is a legitimate "off" but whose blank line in a deployment
176
+ would switch a feature off silently (the web's media host).
177
+ - `indiaLocationSchema.state`: missing, blank and unknown all refuse as the
178
+ choice they are (`invalid_value`, as 0.17 did), so a form words "Choose your
179
+ state".
180
+
181
+ **One rule each, moved or named here:** the upgrade price and India's calendar
182
+ months (`upgradePrice`, `addPlanPeriod`, `UPGRADE_PER_*`: the Terms state it
183
+ from these), `rupees`, `INDIA_UTC_OFFSET_MS` / `DAY_MS`, `eachOnce`,
184
+ `missingForListing`, `productIsOnDiscover`, `changesDiscoverCard`,
185
+ `CONTACT_VISIBILITY_AUDIENCE` (the user: members only, all four),
186
+ `canonicalCityName`, the one YouTube rule (posts and products: `/v/`, and the
187
+ music and www.youtu.be hosts), `ventureBodyFrom` / `foundedYearDigits`,
188
+ `QueryArgs<S>` (type a list's arguments by the PARSED shape: a query field's
189
+ input is `unknown`), and the values and limits the backend copied by hand:
190
+ `TICKET_SIDE_VALUES`, `INVOICE_KIND_VALUES`, `COUPON_KIND_VALUES`,
191
+ `REPORT_STATUS_VALUES`, `COMMUNITY_SUGGESTION_DECISION_VALUES`,
192
+ `TICKET_CHANGE_VALUES`, `REFUND_ASK_CATEGORY`, `COUPON_PERCENT_*`,
193
+ `COUPON_LABEL_MIN`, `BILLING_NAME_MIN` / `BILLING_ADDRESS_MIN`, `GSTIN_LENGTH`,
194
+ `INVOICE_NUMBER_MAX`, `RAZORPAY_*_MAX`, `RATING_MIN` / `RATING_MAX`,
195
+ `DASHBOARD_RECENT_DAYS`, `PHONE_INPUT_MAX` / `DATE_INPUT_MAX` /
196
+ `GUIDELINES_VERSION_MAX`. The permission fields type their statuses.
197
+
198
+ **Deploy order: the backend, then the web.** The web reads the new response
199
+ fields; a 0.17 web on a 0.18 backend reads the dashboard and payments as before
200
+ (unknown keys are dropped).
201
+
202
+ ## Breaking in 0.17.0: the onboarding photo is saved, and onboarding's single source of truth (2026-10-19)
203
+
204
+ Two pieces of the user's work on onboarding, 2026-10-19.
205
+
206
+ **The photo.** Going back to the photo step showed no photo, though the member
207
+ had pressed Save and continue on it. Every other answer came back from the
208
+ draft; the photo was held for the visit only, so any reload or a resume the
209
+ next day lost it.
210
+
211
+ - **New:** `onboardingStatusSchema.photo` (optional, a `mediaImageSchema`): the
212
+ photo the member saved, as it will appear. Absent when there is none.
213
+ - **Changed meaning:** a step save carrying `draft.avatarMediaId` (an upload id,
214
+ or `null`) is the photo step SAVING its photo. The server holds that upload
215
+ for the draft, so the 24 hour unattached sweep never deletes it, and lets go
216
+ of the one it replaces; `null` lets go of it. It is not kept in the draft:
217
+ `readOnboardingDraft` still gives `null` for it, and the photo comes back only
218
+ as `photo`, from what the server holds.
219
+ - **Stricter:** `onboardingProgressUpdateSchema` refuses a `draft.avatarMediaId`
220
+ that is neither an upload id nor `null` (`invalidId`, at
221
+ `draft.avatarMediaId`). No client sent the key before 0.17.0.
222
+ - **Backend:** a held photo is a media row attached to the member's onboarding
223
+ progress row (attachment kind `onboarding`, backend only). Finish moves it to
224
+ the new profile in the same transaction; a scheduled job lets go of a held
225
+ photo whose progress row the 90 day TTL deleted (Privacy: sign-up answers go
226
+ 90 days after they were last changed). New partial index on `media` for that
227
+ job; run `sync-indexes --apply` after deploy.
228
+ - **Deploy order: the backend, then the web.** The web reads the status with
229
+ `z.object`, which drops a key it does not know, so a 0.16 web on a 0.17
230
+ backend simply shows no photo on Back, as today.
231
+ - **Rollback floor:** a backend below 0.17 does not know the `onboarding`
232
+ attachment, so photos held by then stay attached and are never swept until
233
+ 0.17 runs again, and a member who finishes on the old build is told the photo
234
+ expired and asked to add it again (the old finish takes only an unattached
235
+ upload). A rollback that is KEPT therefore needs a one-off release of every
236
+ `media` row attached as kind `onboarding`, or those photos outlive the
237
+ Privacy's 90 days (account deletion still erases them).
238
+
239
+ **One source of truth** (the user: "all the data for all the onboarding steps
240
+ and all the errors and validation types ... should come from the founder
241
+ circle schema"). What a client or the backend kept its own copy of now lives
242
+ here, and each consumer reads it:
243
+
244
+ - **New, data:** `GOAL_CATEGORIES` / `GOAL_CATEGORY_KEYS` (the goals step's
245
+ three groups, from the web); `domains/location/india`: `INDIA`,
246
+ `INDIA_DIALING_CODE`, `INDIA_STATES` (each state's cities and other names,
247
+ from the web) with names typed against `GST_STATES` (ONE list of state
248
+ names), `INDIA_STATE_NAMES`, `STATE_ALIASES`, `CITY_ALIASES`,
249
+ `findIndiaState`, `listedCityName`, `canonicalIndiaLocation`;
250
+ `ONBOARDING_DRAFT_RETENTION_DAYS` (90, from the backend's TTL);
251
+ `PHONE_DIGITS`.
252
+ - **New, rules every side applies the same way:** `normalizePhone`,
253
+ `normalizeLinkUrl`, `tidyFullName`, `teamSizeDigits` (from the web);
254
+ `isRealIsoDate`, `todayInIndia`, `dateOfBirthBounds` (a picker's window,
255
+ exact at both ends), `isUnderAgeDateOfBirth` (the draft's age rule, from the
256
+ backend, now with the real-date check), `isoDateToDate` / `dateToIsoDate`;
257
+ `goalListSchema` / `skillListSchema` (one list rule for the finish and Me).
258
+ - **New, from the answers to what is sent** (`domains/onboarding/
259
+ onboarding-answers`, from the web): `emptyOnboardingForm`, `ventureStarted`,
260
+ `onboardingDraftForStep` (exactly a step's own fields), and
261
+ `completeOnboardingBodyFrom` (the finish body: every unanswered optional
262
+ answer absent, each answer cleaned as above), with the
263
+ `CompleteOnboardingBodyCandidate` type; `locationStarted` (a place opened
264
+ but nothing chosen is "no location").
265
+ - **New, the keys of shapes** (so no consumer lists them by hand):
266
+ `FOUNDER_LINK_KEYS` / `FounderLinkKey`, `VENTURE_KEYS` /
267
+ `VENTURE_OPTIONAL_KEYS` / `VentureKey`; types `IndiaLocation`, `IndiaState`,
268
+ `IndiaStateName`, `GoalCategory`.
269
+ - **New message keys:** `guidelinesRequired`, `photoUnavailable`,
270
+ `ventureRequired`, `stateRequired`, `stateUnknown`, `goalsDuplicate`,
271
+ `skillsDuplicate`. Each client words them in the same change (a client
272
+ holding its words as `Record<ValidationMessageKey, string>` fails to build
273
+ until it does).
274
+ - **Stricter:**
275
+ - every location WRITE (`completeOnboardingBodySchema`,
276
+ `updateFounderProfileBodySchema`, `updateMyPersonalDetailsBodySchema`, and
277
+ the server's `suggestedLocationSchema`) is `indiaLocationSchema`: country
278
+ `India`, a state from the list (required: `stateRequired`; not on it:
279
+ `stateUnknown`; read in any common spelling, "KARNATAKA", "Orissa",
280
+ "Jammu & Kashmir", and kept as the list's name), a city. READS keep
281
+ `locationSchema`, so a place stored before this is read as it is;
282
+ - `businessType` / `legalStructure` without a `venture` are each refused ON
283
+ THEIR OWN FIELD (`ventureRequired`), on the finish body and the business
284
+ step; the backend used to drop them without a word, and the web dropped a
285
+ lone registration (hary's ruling: no silent drop). `ventureStarted` no
286
+ longer counts the type: it describes a business, it does not start one;
287
+ - `displayNameSchema` stores the name tidied (`tidyFullName`): every body
288
+ that writes a name, the Me patches included;
289
+ - goals and skills each once (`goalsDuplicate`, `skillsDuplicate`), on the
290
+ finish and the Me patch;
291
+ - the guidelines: a box left unticked is `guidelinesRequired` (it was
292
+ `guidelinesOutdated`, the words for an older text).
293
+ - **Breaking:** `completeOnboardingBodySchema` and
294
+ `ONBOARDING_STEP_SCHEMAS.business` carry the venture rule (refinements), so
295
+ Zod 4.6 throws on `.pick`, `.omit`, `.partial` and `.merge` of them, and on
296
+ an `.extend` that overwrites a key. Read a field with `.shape.<field>`, and a
297
+ step's fields with `ONBOARDING_STEP_SCHEMAS.<step>` (picked before the rule;
298
+ the business slice carries it again). The web picked from the finish body in
299
+ three places (the `/dummy` showcase, at module scope, and two tests); they
300
+ read `.shape` now.
301
+ - **Changed type:** `displayNameSchema` is a pipe (`.transform(tidyFullName)`),
302
+ no longer a `ZodString`, so `.min` / `.max` / `.regex` cannot be chained on
303
+ it. Nothing did; a length is read through the pipe.
304
+ - **Breaking (no consumer reads it):** `FOUNDER_ONBOARDING_STEPS[i].helpKey`
305
+ is gone (there is no help line any more; the user, 2026-10-19), and
306
+ `labelKey` names the step's short label.
307
+ - **Moved, same export name:** `suggestedLocationSchema` / `SuggestedLocation`
308
+ now come from `domains/location/india`.
309
+ - **Deploy order: still the backend first.** A 0.16 web is refused a little
310
+ that it used to send, on its field: a place with no state (an old draft's,
311
+ or the 0.16 Me editor saving an OLD stateless place, which it sends without
312
+ one) at `location.state`. (It never sends a lone business type: its
313
+ `ventureStarted` counted the type, so a type on its own went with an empty
314
+ venture, refused at `venture.name` / `industry` / `stage` as on 0.16.) A
315
+ 0.17 web needs the 0.17 backend for the photo. It reads the location
316
+ suggestion with the strict write shape, which takes a state in any common
317
+ spelling ("KARNATAKA"), so a 0.16 backend's answer is read unless it has no
318
+ state, or one the list does not know: then it is no suggestion.
319
+ - **Rollback floor (also):** the 0.17 web on a backend rolled below 0.17 saves
320
+ no photo, and shows no suggestion where the old backend answered without a
321
+ state the list knows.
322
+ - **Known, follow-up (hary, 2026-10-19), not in 0.17.0:**
323
+ - a step save racing a finish when the member has NO progress row (the TTL
324
+ deleted it while a tab stayed open) can leave a photo held for a finished
325
+ member, until the TTL takes that new row: the finish's update of a row
326
+ that is not there writes nothing, so the two transactions never conflict.
327
+ Fix: the finish upserts its completion stamp;
328
+ - finish's retry loop sets its result inside the transaction callback, so a
329
+ commit that fails and a retry that then meets a username clash could
330
+ answer success for a profile never committed. Fix: reset the result on
331
+ every attempt. Do both together: an 11000 on that upsert would otherwise
332
+ read as a username clash.
333
+
102
334
  ## Breaking in 0.16.0: onboarding in nine steps (2026-10-18)
103
335
 
104
336
  The user's rework of onboarding. The finish body takes the same answers plus one.
@@ -504,8 +736,8 @@ correction of something the backend, the web or the data already disagreed on.
504
736
  | `discoverFacetsSchema` options | `{ value, label, count }` | `{ value, count }` |
505
737
  | `updateUsernameBodySchema` | format only | format, and not reserved |
506
738
 
507
- Deprecated, removed in 0.3.0: `chatInvitationBodySchema` (use
508
- `openChatBodySchema`) and `chatInvitationSchema` (describes nothing that
739
+ Deprecated in 0.2.0, kept until 0.18.0 removed them: `chatInvitationBodySchema`
740
+ (use `openChatBodySchema`) and `chatInvitationSchema` (describes nothing that
509
741
  exists).
510
742
 
511
743
  ---
@@ -535,25 +767,36 @@ src/
535
767
  index.ts the public surface. Everything a consumer imports.
536
768
  env.ts env field builders, its own entry: `founder-schema/env`
537
769
  core/ what every domain builds on (imports no domain)
538
- limits.ts every length, size and count limit, by name (imports nothing)
770
+ limits.ts every length, size and count limit, by name, and India's
771
+ offset and DAY_MS (imports nothing)
539
772
  validation.ts message keys, the wire shape of an issue, toValidationIssues
540
773
  errors.ts every API error code (API_ERROR_CODE_VALUES)
541
774
  common.ts usernames (and the reserved list), the enums, URLs, phone,
542
- location, coordinates, pagination
543
- responses.ts success / error / paginated / { id } envelopes
775
+ location, coordinates, pagination, eachOnce, YouTube links
776
+ responses.ts success / error / paginated / { id } envelopes, QueryArgs
777
+ money.ts rupees(): paise as people read them
544
778
  access/ who may do what (CASL's words, shared by server and web)
545
779
  roles.ts the roles, who may grant which, the active-role window
546
780
  permissions.ts actions, subjects, the fields a rule may name, AppAbility
547
781
  access.ts GET /me/access, switching role, the MPIN
548
- domains/<domain>/ one folder per product area: its bodies, params and answers
549
- account/ audit/ chat/ discover/ feed/ onboarding/ profile/ team/ uploads/
782
+ standing.ts viewer / member (staff count as members), the refusal reasons
783
+ capabilities.ts powers given to selected members (create_communities), who grants them
784
+ domains/<domain>/ one folder per product area: its bodies, params, answers
785
+ and the rules both sides apply
786
+ account/ audit/ billing/ chat/ communities/ contributions/ dashboard/
787
+ discover/ feed/ location/ media/ members/ membership/ moderation/
788
+ notifications/ onboarding/ products/ profile/ reviews/ support/ team/
789
+ uploads/ waitlist/
550
790
  tests/
791
+ the domains' own tests, plus:
551
792
  ssot.test.ts derived not copied, no body coercion, URLs, uploads, codes
793
+ ssot-0-18.test.ts the 0.18.0 audit: each rule moved here, on the rule
552
794
  validation.test.ts keys not sentences, no input leaves, limits applied
553
795
  onboarding-steps.test.ts steps, step schemas and the finish body agree
554
796
  responses.test.ts the envelopes, pinned
555
797
  access.test.ts roles, the grant table, the MPIN, rules across the wire
556
- public-surface.test.ts export-removal tripwire + the values the database is pinned to
798
+ public-surface.test.ts export-removal tripwire, the 0.18.0 removals, and the
799
+ values the database is pinned to
557
800
  ```
558
801
 
559
802
  A consumer imports from the package root and never from a deep path:
@@ -591,9 +834,10 @@ cannot see it and the request looks correct in the network tab. Build a write bo
591
834
  as its own object with no defaults, and have the client send only the keys the
592
835
  member actually changed.
593
836
 
594
- **A default inside a nested object is the same trap one level down.** A patch
595
- carrying `{ location: { city: 'Berlin' } }` picks up `country: 'India'` from
596
- `locationSchema`.
837
+ **A default inside a nested object is the same trap one level down.** A write
838
+ body never fills a nested field in: `locationSchema` has no default country
839
+ since it was removed, and a place is WRITTEN through `indiaLocationSchema`,
840
+ whole.
597
841
 
598
842
  **A patch field the member can clear has three states, not two.** Absent means
599
843
  do not touch. `null` means clear, and the backend maps it to `$unset`. An empty
@@ -49,7 +49,7 @@ export declare const meAccessResponseSchema: z.ZodObject<{
49
49
  viewer: "viewer";
50
50
  member: "member";
51
51
  }>;
52
- rules: z.ZodArray<z.ZodArray<z.ZodUnknown>>;
52
+ rules: z.ZodArray<z.ZodUnion<readonly [z.ZodTuple<[z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodLiteral<1>], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>, z.ZodString], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>, z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<0>]>, z.ZodString], null>]>>;
53
53
  }, z.core.$strip>;
54
54
  export type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;
55
55
  /** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */
@@ -1 +1 @@
1
- {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,2CAA2C;AAC3C,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,eAAO,MAAM,UAAU,aAA8D,CAAC;AAEtF,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;iBAiBjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,0FAA0F;AAC1F,eAAO,MAAM,oBAAoB;;;;;;;;;iBAQ7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,oDAAoD;AACpD,eAAO,MAAM,iBAAiB;;;iBAEA,CAAC;AAC/B,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB;;;;iBAEH,CAAC;AAC/B,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,2CAA2C;AAC3C,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,eAAO,MAAM,UAAU,aAG0B,CAAC;AAElD,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;iBAiBjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,0FAA0F;AAC1F,eAAO,MAAM,oBAAoB;;;;;;;;;iBAQ7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,oDAAoD;AACpD,eAAO,MAAM,iBAAiB;;;iBAEA,CAAC;AAC/B,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB;;;;iBAEH,CAAC;AAC/B,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
@@ -30,7 +30,10 @@ export const MPIN_LENGTH = 6;
30
30
  export const MPIN_MAX_ATTEMPTS = 5;
31
31
  /** How long a locked MPIN stays locked. */
32
32
  export const MPIN_LOCK_MS = 15 * 60 * 1000;
33
- export const mpinSchema = z.string().regex(/^\d{6}$/, VALIDATION_MESSAGE.mpinInvalid);
33
+ export const mpinSchema = z
34
+ .string()
35
+ .length(MPIN_LENGTH, VALIDATION_MESSAGE.mpinInvalid)
36
+ .regex(/^\d+$/, VALIDATION_MESSAGE.mpinInvalid);
34
37
  /** The answer to `GET /me/access`, and to every switch. */
35
38
  export const meAccessResponseSchema = z.object({
36
39
  /** Every role held, member always among them, highest first. */
@@ -1 +1 @@
1
- {"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,yCAAyC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AAEtF,2DAA2D;AAC3D,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,gEAAgE;IAChE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,6EAA6E;IAC7E,UAAU,EAAE,UAAU;IACtB,uFAAuF;IACvF,mBAAmB,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAChD,4FAA4F;IAC5F,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB;;;;OAIG;IACH,QAAQ,EAAE,cAAc;IACxB,wFAAwF;IACxF,KAAK,EAAE,iBAAiB;CACzB,CAAC,CAAC;AAGH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE;CAC5B,CAAC;KACD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE;IACtE,IAAI,EAAE,CAAC,MAAM,CAAC;IACd,KAAK,EAAE,kBAAkB,CAAC,YAAY;CACvC,CAAC,CAAC;AAGL,MAAM,SAAS,GAAG,CAAkD,IAAO,EAAE,EAAE,CAC7E,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,WAAW,CAAC;AACjC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,YAAY,EAAE,CAAC;AAEnF,oDAAoD;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;KACrD,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAG/B,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;KAC9E,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the access API\n *\n * GET /api/v1/me/access who I am acting as, what I hold, my rules\n * PUT /api/v1/me/active-role switch role (a staff role needs the MPIN)\n * POST /api/v1/me/mpin set the MPIN, once\n * PUT /api/v1/me/mpin change it, with the current one\n *\n * THE MPIN is a 6-digit code that stands between a signed-in member and their\n * staff roles. Signing in proves who you are; the MPIN proves it is still you\n * at the moment you pick up admin powers, on a phone that may have been left\n * unlocked. Set once by its owner; a forgotten one is cleared by a superadmin\n * (an audited action), after which the owner sets a new one. `MPIN_MAX_ATTEMPTS`\n * wrong tries lock it for `MPIN_LOCK_MS`.\n *\n * THE ACTIVE ROLE is per account (the user's ruling, 2026-10-05): switching on\n * one device switches every device. It lasts `ACTIVE_ROLE_TTL_MS`, and the\n * server re-checks it on EVERY request: a role that was revoked, or that\n * expired, is acting as a member again at once, not at the next sign-in.\n *\n * @module founder-schema/access/access\n */\n\nimport { z } from 'zod';\nimport { VALIDATION_MESSAGE } from '../core/validation.js';\nimport { packedRulesSchema } from './permissions.js';\nimport { MEMBER_ROLE, roleSchema } from './roles.js';\nimport { standingSchema } from './standing.js';\n\nexport const MPIN_LENGTH = 6;\n/** Wrong tries before the MPIN locks. */\nexport const MPIN_MAX_ATTEMPTS = 5;\n/** How long a locked MPIN stays locked. */\nexport const MPIN_LOCK_MS = 15 * 60 * 1000;\n\nexport const mpinSchema = z.string().regex(/^\\d{6}$/, VALIDATION_MESSAGE.mpinInvalid);\n\n/** The answer to `GET /me/access`, and to every switch. */\nexport const meAccessResponseSchema = z.object({\n /** Every role held, member always among them, highest first. */\n roles: z.array(roleSchema).min(1),\n /** The role being acted as now. Member when no staff role is switched on. */\n activeRole: roleSchema,\n /** When the active staff role lapses back to member. Null while acting as a member. */\n activeRoleExpiresAt: z.iso.datetime().nullable(),\n /** Whether an MPIN is set, so the switcher knows to ask for one or to offer setting one. */\n mpinSet: z.boolean(),\n /**\n * Whether they take part or only look (0.12.0, `access/standing`). The\n * rules already say what follows from it; this says it plainly, for the\n * places that word it (\"Become a member to take part\").\n */\n standing: standingSchema,\n /** The active role's permissions, packed (`packRules`), refusals with their reasons. */\n rules: packedRulesSchema,\n});\nexport type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;\n\n/** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */\nexport const switchRoleBodySchema = z\n .object({\n role: roleSchema,\n mpin: mpinSchema.optional(),\n })\n .refine((body) => body.role === MEMBER_ROLE || body.mpin !== undefined, {\n path: ['mpin'],\n error: VALIDATION_MESSAGE.mpinRequired,\n });\nexport type SwitchRoleBody = z.infer<typeof switchRoleBodySchema>;\n\nconst confirmed = <T extends { mpin: string; confirmMpin: string }>(body: T) =>\n body.mpin === body.confirmMpin;\nconst mismatch = { path: ['confirmMpin'], error: VALIDATION_MESSAGE.mpinMismatch };\n\n/** `POST /me/mpin`: the first MPIN, typed twice. */\nexport const setMpinBodySchema = z\n .object({ mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type SetMpinBody = z.infer<typeof setMpinBodySchema>;\n\n/** `PUT /me/mpin`: the current MPIN, then the new one typed twice. */\nexport const changeMpinBodySchema = z\n .object({ currentMpin: mpinSchema, mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type ChangeMpinBody = z.infer<typeof changeMpinBodySchema>;\n"]}
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,yCAAyC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC;KACxB,MAAM,EAAE;KACR,MAAM,CAAC,WAAW,EAAE,kBAAkB,CAAC,WAAW,CAAC;KACnD,KAAK,CAAC,OAAO,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AAElD,2DAA2D;AAC3D,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,gEAAgE;IAChE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,6EAA6E;IAC7E,UAAU,EAAE,UAAU;IACtB,uFAAuF;IACvF,mBAAmB,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAChD,4FAA4F;IAC5F,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB;;;;OAIG;IACH,QAAQ,EAAE,cAAc;IACxB,wFAAwF;IACxF,KAAK,EAAE,iBAAiB;CACzB,CAAC,CAAC;AAGH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE;CAC5B,CAAC;KACD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE;IACtE,IAAI,EAAE,CAAC,MAAM,CAAC;IACd,KAAK,EAAE,kBAAkB,CAAC,YAAY;CACvC,CAAC,CAAC;AAGL,MAAM,SAAS,GAAG,CAAkD,IAAO,EAAE,EAAE,CAC7E,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,WAAW,CAAC;AACjC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,YAAY,EAAE,CAAC;AAEnF,oDAAoD;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;KACrD,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAG/B,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;KAC9E,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the access API\n *\n * GET /api/v1/me/access who I am acting as, what I hold, my rules\n * PUT /api/v1/me/active-role switch role (a staff role needs the MPIN)\n * POST /api/v1/me/mpin set the MPIN, once\n * PUT /api/v1/me/mpin change it, with the current one\n *\n * THE MPIN is a 6-digit code that stands between a signed-in member and their\n * staff roles. Signing in proves who you are; the MPIN proves it is still you\n * at the moment you pick up admin powers, on a phone that may have been left\n * unlocked. Set once by its owner; a forgotten one is cleared by a superadmin\n * (an audited action), after which the owner sets a new one. `MPIN_MAX_ATTEMPTS`\n * wrong tries lock it for `MPIN_LOCK_MS`.\n *\n * THE ACTIVE ROLE is per account (the user's ruling, 2026-10-05): switching on\n * one device switches every device. It lasts `ACTIVE_ROLE_TTL_MS`, and the\n * server re-checks it on EVERY request: a role that was revoked, or that\n * expired, is acting as a member again at once, not at the next sign-in.\n *\n * @module founder-schema/access/access\n */\n\nimport { z } from 'zod';\nimport { VALIDATION_MESSAGE } from '../core/validation.js';\nimport { packedRulesSchema } from './permissions.js';\nimport { MEMBER_ROLE, roleSchema } from './roles.js';\nimport { standingSchema } from './standing.js';\n\nexport const MPIN_LENGTH = 6;\n/** Wrong tries before the MPIN locks. */\nexport const MPIN_MAX_ATTEMPTS = 5;\n/** How long a locked MPIN stays locked. */\nexport const MPIN_LOCK_MS = 15 * 60 * 1000;\n\nexport const mpinSchema = z\n .string()\n .length(MPIN_LENGTH, VALIDATION_MESSAGE.mpinInvalid)\n .regex(/^\\d+$/, VALIDATION_MESSAGE.mpinInvalid);\n\n/** The answer to `GET /me/access`, and to every switch. */\nexport const meAccessResponseSchema = z.object({\n /** Every role held, member always among them, highest first. */\n roles: z.array(roleSchema).min(1),\n /** The role being acted as now. Member when no staff role is switched on. */\n activeRole: roleSchema,\n /** When the active staff role lapses back to member. Null while acting as a member. */\n activeRoleExpiresAt: z.iso.datetime().nullable(),\n /** Whether an MPIN is set, so the switcher knows to ask for one or to offer setting one. */\n mpinSet: z.boolean(),\n /**\n * Whether they take part or only look (0.12.0, `access/standing`). The\n * rules already say what follows from it; this says it plainly, for the\n * places that word it (\"Become a member to take part\").\n */\n standing: standingSchema,\n /** The active role's permissions, packed (`packRules`), refusals with their reasons. */\n rules: packedRulesSchema,\n});\nexport type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;\n\n/** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */\nexport const switchRoleBodySchema = z\n .object({\n role: roleSchema,\n mpin: mpinSchema.optional(),\n })\n .refine((body) => body.role === MEMBER_ROLE || body.mpin !== undefined, {\n path: ['mpin'],\n error: VALIDATION_MESSAGE.mpinRequired,\n });\nexport type SwitchRoleBody = z.infer<typeof switchRoleBodySchema>;\n\nconst confirmed = <T extends { mpin: string; confirmMpin: string }>(body: T) =>\n body.mpin === body.confirmMpin;\nconst mismatch = { path: ['confirmMpin'], error: VALIDATION_MESSAGE.mpinMismatch };\n\n/** `POST /me/mpin`: the first MPIN, typed twice. */\nexport const setMpinBodySchema = z\n .object({ mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type SetMpinBody = z.infer<typeof setMpinBodySchema>;\n\n/** `PUT /me/mpin`: the current MPIN, then the new one typed twice. */\nexport const changeMpinBodySchema = z\n .object({ currentMpin: mpinSchema, mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type ChangeMpinBody = z.infer<typeof changeMpinBodySchema>;\n"]}
@@ -15,9 +15,11 @@
15
15
  *
16
16
  * @module founder-schema/access/permissions
17
17
  */
18
- import type { ForcedSubject, MongoAbility } from '@casl/ability';
18
+ import type { ForcedSubject, MongoAbility, RawRuleOf } from '@casl/ability';
19
19
  import { z } from 'zod';
20
- import type { PurchaseKind } from '../domains/billing/billing.js';
20
+ import type { OrderStatus, PurchaseKind } from '../domains/billing/billing.js';
21
+ import type { CommunitySuggestionStatus } from '../domains/communities/communities.js';
22
+ import type { ReportStatus } from '../domains/feed/feed.js';
21
23
  import type { PlatformRole } from './roles.js';
22
24
  import type { Standing } from './standing.js';
23
25
  /**
@@ -48,26 +50,7 @@ import type { Standing } from './standing.js';
48
50
  * Append only: a stored or packed rule names these.
49
51
  */
50
52
  export declare const ACTION_VALUES: readonly ["manage", "create", "read", "update", "delete", "moderate", "assign", "suspend", "resolve", "reset", "represent", "react", "vote", "save", "join", "message", "refund"];
51
- export declare const actionSchema: z.ZodEnum<{
52
- join: "join";
53
- message: "message";
54
- manage: "manage";
55
- create: "create";
56
- read: "read";
57
- update: "update";
58
- delete: "delete";
59
- moderate: "moderate";
60
- assign: "assign";
61
- suspend: "suspend";
62
- resolve: "resolve";
63
- reset: "reset";
64
- represent: "represent";
65
- react: "react";
66
- vote: "vote";
67
- save: "save";
68
- refund: "refund";
69
- }>;
70
- export type Action = z.infer<typeof actionSchema>;
53
+ export type Action = (typeof ACTION_VALUES)[number];
71
54
  /**
72
55
  * The things.
73
56
  *
@@ -160,7 +143,7 @@ export interface SubjectFields {
160
143
  };
161
144
  Report: {
162
145
  reporterId: string;
163
- status: string;
146
+ status: ReportStatus;
164
147
  };
165
148
  SupportTicket: {
166
149
  memberId: string;
@@ -205,7 +188,7 @@ export interface SubjectFields {
205
188
  /** `suggestedBy` so "your own" is a rule; `status` so only open ones are decided. */
206
189
  CommunitySuggestion: {
207
190
  suggestedBy: string;
208
- status: string;
191
+ status: CommunitySuggestionStatus;
209
192
  };
210
193
  /** Whose membership (0.12.0), so "never your own" is a rule. */
211
194
  Membership: {
@@ -218,7 +201,7 @@ export interface SubjectFields {
218
201
  */
219
202
  Payment: {
220
203
  userId: string;
221
- status: string;
204
+ status: OrderStatus;
222
205
  kind: PurchaseKind;
223
206
  };
224
207
  /** Coupons are the platform's (0.12.0): no condition is ever written on one. */
@@ -241,11 +224,26 @@ type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
241
224
  type Assert<T extends true> = T;
242
225
  export type EverySubjectHasFields = Assert<Same<keyof SubjectFields, Exclude<Subject, 'all'>>>;
243
226
  /**
244
- * Rules as they cross the wire: CASL's `packRules` output, an array of
245
- * compact arrays. The web unpacks them with `unpackRules` before building its
246
- * ability; their inner shape is CASL's, so it is not re-described here.
227
+ * One packed rule, in exactly the shapes CASL's `PackRule` takes (a union of
228
+ * tuples, not a rest: `unpackRules` needs one of these), with `conditions` as
229
+ * `unknown`, which `isAppRawRule` and the ability itself then judge.
247
230
  */
248
- export declare const packedRulesSchema: z.ZodArray<z.ZodArray<z.ZodUnknown>>;
231
+ export declare const packedRuleSchema: z.ZodUnion<readonly [z.ZodTuple<[z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodLiteral<1>], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>, z.ZodString], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>, z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<0>]>, z.ZodString], null>]>;
232
+ export declare const packedRulesSchema: z.ZodArray<z.ZodUnion<readonly [z.ZodTuple<[z.ZodString, z.ZodString], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodLiteral<1>], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>, z.ZodString], null>, z.ZodTuple<[z.ZodString, z.ZodString, z.ZodUnknown, z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<1>]>, z.ZodUnion<readonly [z.ZodString, z.ZodLiteral<0>]>, z.ZodString], null>]>>;
249
233
  export type PackedRules = z.infer<typeof packedRulesSchema>;
234
+ /** One rule as CASL's `unpackRules` gives it back, typed for this app. */
235
+ export type AppRawRule = RawRuleOf<AppAbility>;
236
+ /**
237
+ * Whether an UNPACKED rule names only this app's actions and subjects
238
+ * (0.18.0). Packed rules cross the wire as strings (`packRules` joins several
239
+ * actions or subjects with commas, so the wire shape cannot be an enum); this
240
+ * is the check that turns them back into `AppRawRule`s, with no cast:
241
+ * `rules.every(isAppRawRule)` narrows the list, and a list that fails is a
242
+ * contract mismatch, never an ability built from it.
243
+ */
244
+ export declare function isAppRawRule(rule: {
245
+ action: string | string[];
246
+ subject?: string | string[];
247
+ }): rule is AppRawRule;
250
248
  export {};
251
249
  //# sourceMappingURL=permissions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"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"}
@@ -62,7 +62,6 @@ export const ACTION_VALUES = [
62
62
  'message',
63
63
  'refund',
64
64
  ];
65
- export const actionSchema = z.enum(ACTION_VALUES);
66
65
  /**
67
66
  * The things.
68
67
  *
@@ -111,7 +110,44 @@ export const subjectSchema = z.enum(SUBJECT_VALUES);
111
110
  /**
112
111
  * Rules as they cross the wire: CASL's `packRules` output, an array of
113
112
  * compact arrays. The web unpacks them with `unpackRules` before building its
114
- * ability; their inner shape is CASL's, so it is not re-described here.
113
+ * ability. Each starts with its action(s) and subject(s), comma-joined
114
+ * strings (checked, 0.18.0: an unchecked rule list could silently turn staff
115
+ * routing off); what follows (conditions, inverted, fields, reason) is CASL's
116
+ * own compact form.
115
117
  */
116
- export const packedRulesSchema = z.array(z.array(z.unknown()));
118
+ const zeroOrOne = z.union([z.literal(0), z.literal(1)]);
119
+ /**
120
+ * One packed rule, in exactly the shapes CASL's `PackRule` takes (a union of
121
+ * tuples, not a rest: `unpackRules` needs one of these), with `conditions` as
122
+ * `unknown`, which `isAppRawRule` and the ability itself then judge.
123
+ */
124
+ export const packedRuleSchema = z.union([
125
+ z.tuple([z.string().min(1), z.string().min(1)]),
126
+ z.tuple([z.string().min(1), z.string().min(1), z.unknown()]),
127
+ z.tuple([z.string().min(1), z.string().min(1), z.unknown(), z.literal(1)]),
128
+ z.tuple([z.string().min(1), z.string().min(1), z.unknown(), zeroOrOne, z.string()]),
129
+ z.tuple([
130
+ z.string().min(1),
131
+ z.string().min(1),
132
+ z.unknown(),
133
+ zeroOrOne,
134
+ z.union([z.string(), z.literal(0)]),
135
+ z.string(),
136
+ ]),
137
+ ]);
138
+ export const packedRulesSchema = z.array(packedRuleSchema);
139
+ const ACTION_SET = new Set(ACTION_VALUES);
140
+ const SUBJECT_SET = new Set(SUBJECT_VALUES);
141
+ const everyIn = (value, known) => value !== undefined && (Array.isArray(value) ? value : [value]).every((v) => known.has(v));
142
+ /**
143
+ * Whether an UNPACKED rule names only this app's actions and subjects
144
+ * (0.18.0). Packed rules cross the wire as strings (`packRules` joins several
145
+ * actions or subjects with commas, so the wire shape cannot be an enum); this
146
+ * is the check that turns them back into `AppRawRule`s, with no cast:
147
+ * `rules.every(isAppRawRule)` narrows the list, and a list that fails is a
148
+ * contract mismatch, never an ability built from it.
149
+ */
150
+ export function isAppRawRule(rule) {
151
+ return everyIn(rule.action, ACTION_SET) && everyIn(rule.subject, SUBJECT_SET);
152
+ }
117
153
  //# sourceMappingURL=permissions.js.map