founder-schema 0.17.0 → 0.19.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 (120) hide show
  1. package/README.md +186 -17
  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 +54 -23
  11. package/dist/core/common.d.ts.map +1 -1
  12. package/dist/core/common.js +49 -24
  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 +73 -17
  19. package/dist/core/limits.d.ts.map +1 -1
  20. package/dist/core/limits.js +77 -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 +19 -2
  30. package/dist/core/validation.d.ts.map +1 -1
  31. package/dist/core/validation.js +19 -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/areas.d.ts +331 -0
  61. package/dist/domains/location/areas.d.ts.map +1 -0
  62. package/dist/domains/location/areas.js +238 -0
  63. package/dist/domains/location/areas.js.map +1 -0
  64. package/dist/domains/location/india.d.ts +41 -123
  65. package/dist/domains/location/india.d.ts.map +1 -1
  66. package/dist/domains/location/india.js +121 -59
  67. package/dist/domains/location/india.js.map +1 -1
  68. package/dist/domains/members/members.d.ts +2 -2
  69. package/dist/domains/membership/membership.d.ts +26 -0
  70. package/dist/domains/membership/membership.d.ts.map +1 -1
  71. package/dist/domains/membership/membership.js +55 -1
  72. package/dist/domains/membership/membership.js.map +1 -1
  73. package/dist/domains/moderation/moderation.d.ts +2 -2
  74. package/dist/domains/moderation/moderation.js +3 -3
  75. package/dist/domains/moderation/moderation.js.map +1 -1
  76. package/dist/domains/notifications/notifications.d.ts +1 -10
  77. package/dist/domains/notifications/notifications.d.ts.map +1 -1
  78. package/dist/domains/notifications/notifications.js +2 -3
  79. package/dist/domains/notifications/notifications.js.map +1 -1
  80. package/dist/domains/onboarding/onboarding-answers.d.ts +42 -14
  81. package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -1
  82. package/dist/domains/onboarding/onboarding-answers.js +49 -33
  83. package/dist/domains/onboarding/onboarding-answers.js.map +1 -1
  84. package/dist/domains/onboarding/onboarding.d.ts +332 -138
  85. package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
  86. package/dist/domains/onboarding/onboarding.js +47 -59
  87. package/dist/domains/onboarding/onboarding.js.map +1 -1
  88. package/dist/domains/products/products.d.ts +43 -8
  89. package/dist/domains/products/products.d.ts.map +1 -1
  90. package/dist/domains/products/products.js +57 -11
  91. package/dist/domains/products/products.js.map +1 -1
  92. package/dist/domains/profile/founder-profile.d.ts +123 -143
  93. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  94. package/dist/domains/profile/founder-profile.js +77 -35
  95. package/dist/domains/profile/founder-profile.js.map +1 -1
  96. package/dist/domains/profile/my-profile.d.ts +3 -3
  97. package/dist/domains/reviews/reviews.d.ts +1 -1
  98. package/dist/domains/reviews/reviews.d.ts.map +1 -1
  99. package/dist/domains/reviews/reviews.js +2 -2
  100. package/dist/domains/reviews/reviews.js.map +1 -1
  101. package/dist/domains/support/support.d.ts +22 -3
  102. package/dist/domains/support/support.d.ts.map +1 -1
  103. package/dist/domains/support/support.js +19 -5
  104. package/dist/domains/support/support.js.map +1 -1
  105. package/dist/domains/team/team.d.ts.map +1 -1
  106. package/dist/domains/team/team.js +3 -6
  107. package/dist/domains/team/team.js.map +1 -1
  108. package/dist/domains/uploads/uploads.d.ts +7 -22
  109. package/dist/domains/uploads/uploads.d.ts.map +1 -1
  110. package/dist/domains/uploads/uploads.js +7 -21
  111. package/dist/domains/uploads/uploads.js.map +1 -1
  112. package/dist/env.d.ts +26 -13
  113. package/dist/env.d.ts.map +1 -1
  114. package/dist/env.js +58 -23
  115. package/dist/env.js.map +1 -1
  116. package/dist/index.d.ts +19 -17
  117. package/dist/index.d.ts.map +1 -1
  118. package/dist/index.js +21 -17
  119. package/dist/index.js.map +1 -1
  120. 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
@@ -94,15 +94,172 @@ SAME schema object, so the two can never disagree about what is allowed.
94
94
  nothing reads them yet.
95
95
  - **Bodies are typed JSON; only queries coerce.** `teamSize: "5"` is refused in a
96
96
  body, and `?limit=5` is read as 5.
97
- - **Shapes are derived, never copied.** One `locationSchema`, one
98
- `founderLinksSchema`, one `ventureSchema`, one `mediaImageSchema`; the other
99
- 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 `locationPickSchema` to WRITE one (area ids, 0.19.0), one `founderLinksSchema`, one
99
+ `ventureSchema`, one `mediaImageSchema`; the other places pick, omit or reuse
100
+ them, and a test checks by object identity.
100
101
 
101
102
  `zod` is a **peer** dependency (`^4.4.0`), on purpose. The consumer owns the Zod
102
103
  instance; this package must not bring its own.
103
104
 
104
105
  ---
105
106
 
107
+ ## Breaking in 0.19.0: location is Jansathi's model (2026-10-22)
108
+
109
+ The user, 2026-10-21/22: Jansathi's location drawer (state, then district, then
110
+ locality, from `master_db.areas`) and its whole location model ("just copy
111
+ paste things from jansathi"). Plan, Jansathi analysis and rulings:
112
+ `founder-circle/LOCATION-PLAN-2026-10-21.md`. Contract:
113
+ `src/domains/location/areas.ts`.
114
+
115
+ **Removed** (listed in `public-surface.test.ts`, `REMOVED_IN_0_19`):
116
+ `indiaLocationSchema` (a typed city and state is no longer a write),
117
+ `suggestedLocationSchema` and `coordinatesQuerySchema` (`GET
118
+ /location/areas/reverse` is gone; GPS goes to `POST /me/geolocation`).
119
+
120
+ **Changed on the wire:**
121
+ - A location is WRITTEN as area ids, `locationPickSchema`
122
+ `{ stateId, districtId, localityId | null }` (`null`: "my locality is not
123
+ listed", the whole district): onboarding's finish and
124
+ `PATCH /profiles/me/personal`. The server names the place, the public city
125
+ (`publicCityOf`: the town the member is in, else their district, cleaned by
126
+ `townName` and spelled as the state's list spells it; Discover's city filter
127
+ is unchanged) and its coordinates from `master_db`. Refusals:
128
+ `stateRequired`, `districtRequired`, `localityRequired`, `areaNotListed`,
129
+ `stateUnknown`, each on its id.
130
+ - The onboarding draft's `location` is a `Place` (names and ids). A draft saved
131
+ before 0.19.0 holds a typed place, which names no area: it reads as not
132
+ answered.
133
+ - The owner's profile (`GET /profiles/me`) carries `place`; no card, member page
134
+ or staff view does. Other members still read `location` (city, state,
135
+ country), unchanged.
136
+ - New: `GET /location/areas/states|districts|localities` (`areaListSchema`,
137
+ `areaPageSchema`, `LOCALITY_PAGE_SIZE`), and `POST /me/geolocation`
138
+ (`geolocationFixSchema` -> `geolocationSyncResultSchema` `{ synced, match }`).
139
+ The GPS axis (fix, area, the last `LOCATION_HISTORY_MAX` of each) is kept as
140
+ Jansathi keeps it but NEVER becomes the declared location (the user, "No,
141
+ member saves") and is never shown to another member. History is kept at most
142
+ `LOCATION_HISTORY_MAX_DAYS` (365) days.
143
+ - The member's own control (the user, 2026-10-22, "Add stop + clear"):
144
+ `GET`/`PATCH /me/location-settings` (`locationSettingsSchema`,
145
+ `updateLocationSettingsBodySchema`; `dailyLocation` OFF keeps no position)
146
+ and `DELETE /me/location-history` (`clearLocationHistoryResultSchema`; the
147
+ chosen place stays).
148
+ - `queryInt(min, max, fallback)` is the query integer helper (core/common.ts).
149
+ - `areaOptionSchema.within` (a ward's town, a village's tehsil or block) tells
150
+ same-named places apart; `areaWords` reads a numbered ward as "Ward 25";
151
+ `placeWords` includes `within`. `AREA_NAME_MAX` is 300: the all-India tree has
152
+ names up to 250 characters.
153
+ - The public city is `publicCityOf`; `townName` cleans a council's name. Over
154
+ all of India (4,995 public cities, 2026-10-22) none is rewritten by
155
+ Discover's state-blind `canonicalCityName` (Bijapur is listed for
156
+ Chhattisgarh too), none keeps council words or brackets, the longest is 35.
157
+ A twin town is one hyphen however the tree spaced it (Murwara-Katni), the
158
+ list matches hyphen and space alike (the tree's "Pimpri Chinchwad" is
159
+ Pimpri-Chinchwad), and a name in capitals reads title-case (MAUGANJ is
160
+ Mauganj): over every town and district (5,745), no city is written two ways
161
+ in one state. It always fits `CITY_MAX` (a too-long town is shown as its
162
+ district, a too-long district is cut at a word), so a save never fails on it.
163
+
164
+ **Release:** publish, pin EXACTLY in both consumers, backend before web (a 0.18
165
+ web sends a typed place, which a 0.19 backend refuses). The drawer needs real
166
+ data in `master_db.areas`: AWS dev's was synthetic until the all-India load.
167
+
168
+ ## Breaking in 0.18.0: the SSOT audit (2026-10-20)
169
+
170
+ The user, 2026-10-20: "make sure it is the SSOT for data types, validation,
171
+ etc., for front end and back end, implemented thoroughly end to end, no
172
+ patchwork or lazy workaround, and a proper cleanup". Ten read-only reviewers,
173
+ every high finding verified in code. Each rule below lived in two places, or in
174
+ one consumer only, or as a hand-written literal; it is one export now, and both
175
+ consumers use it (`tests/ssot-0-18.test.ts`, one test per rule).
176
+
177
+ **Removed** (no consumer imports any; listed in `public-surface.test.ts`):
178
+ `chatInvitation*`, the 0.4.0 upload-key code (`uploadKey*`, `UPLOAD_KEY_*`,
179
+ `uploadKeyInvalid`; the picker's extensions are `UPLOAD_FILE_EXTENSIONS` now),
180
+ `PRODUCTS_OWNED_MAX`, `REVIEWS_REWARDED_PER_DAY`, the never-sent codes
181
+ `ORDER_EXPIRED` and `COUPON_FOUNDING_EXISTS`, the unused zod wrappers
182
+ `actionSchema`, `purchaseKindSchema`, `notificationPreferenceSchema`,
183
+ `onboardingProgressSchema`, the uploaded-video shape (`feedVideoSchema`: post
184
+ media is images only) and the link preview no server writes (`linkPreview*`,
185
+ `LINK_PREVIEW_*`).
186
+
187
+ **Stricter or changed on the wire:**
188
+ - `PATCH /onboarding` takes the draft in the FORM's shape
189
+ (`onboardingFormSchema.partial().strict()`): typed and bounded, half-answered
190
+ still fine. It took any JSON before.
191
+ - `PATCH /profiles/me` takes only its own fields: `headline`, `about`, `skills`,
192
+ `goals`, `links`. The name and the city are the personal details', the venture
193
+ the business editor's. `goals` keeps at least one (`requiredGoalListSchema`,
194
+ the user), as onboarding's finish does.
195
+ - `PATCH /feed/:id` may clear the text; the server refuses only a post left with
196
+ nothing (`postHasContent`, the create body's rule too).
197
+ - A vote names each option once (`pollVoteDuplicate`). An emptied `?cursor=`
198
+ means none; a cursor is at most `CURSOR_MAX`.
199
+ - A blank ban reason means no reason. The MPIN rule reads `MPIN_LENGTH`.
200
+ - Responses: `grantFailed` is a `GRANT_FAILURE_VALUES`; the dashboard's support
201
+ block adds `waitingTickets` and `openTickets` is status `open` only; product
202
+ links are READ leniently (`productLinksReadSchema`), so an older link cannot
203
+ break a page; ids and usernames are typed where they were strings; excerpts
204
+ are bounded; packed CASL rules are checked (action and subject).
205
+ - New codes for refusals that were a bare `FORBIDDEN`:
206
+ `COMMUNITY_NOT_ON_YOURSELF`, `HIDDEN_BY_STAFF`, `TICKET_MEMBER_ONLY`; body
207
+ messages `bodyTooLarge`, `bodyNotJson`.
208
+ - A community's last admin, alone in it, may leave: that ARCHIVES it (on the
209
+ audit record, as a staff archive is). A plain member last in an adminless
210
+ community leaves it as it was.
211
+ - Every request body types what a caller SENDS: no body field is a
212
+ `z.preprocess` (whose input type is `unknown`), so a web typing a body by
213
+ `z.input<typeof xBodySchema>` cannot send the wrong type and compile.
214
+ `optionalTrimmedString` and `indiaLocationSchema.state` were the two; a test
215
+ walks every exported body and fails on any preprocess. Same runtime rules.
216
+ - A model refusing what the schema accepted answers `500 INTERNAL_ERROR` (our
217
+ mismatch), never `400`; body-parse refusals carry message KEYS.
218
+ - `PATCH /profiles/me` is STRICT: a key that belongs to another editor is a
219
+ 400 naming it, never dropped without a word.
220
+ - `grantFailed` adds `GRANT_REFUSED`, any other refusal of a grant: still
221
+ recorded and refunded, never a capture rolled back for a reason not listed.
222
+ - `changesDiscoverCard` ignores points (`FOUNDER_CARD_EDIT_FIELDS`): other
223
+ people's acts move them, and they never lift a member up Discover.
224
+ - New: `keptPaise` (what is kept of a payment: the Overview's revenue) and
225
+ `refundLeftPaise` (what a refund returns: never zeroed by a credit note, which
226
+ a failed refund carries), `CONTRIBUTION_PAIR_WINDOW_DAYS` (the pair cap's own
227
+ window), `RAZORPAY_EVENT_RETENTION_DAYS`, `communityTypeChangeAllowed` (an
228
+ archived community narrows, never opens), `communityViewerSchema.leaveArchives`
229
+ (the server says what leaving does), `envVar.optionalOneOf`;
230
+ `onboardingDraftForStep` answers the form's own type, so a step save needs no
231
+ cast.
232
+ - `founder-schema/env`: a BLANK line never takes a real fallback (a port,
233
+ localhost, `development`, a database name): it is refused at start-up and
234
+ named. Unset still takes it; a blank fallback (`''`, a feature off) still
235
+ takes a blank line. `optionalText({ refuseBlank: true })` is for a setting
236
+ whose absence is a legitimate "off" but whose blank line in a deployment
237
+ would switch a feature off silently (the web's media host).
238
+ - `indiaLocationSchema.state`: missing, blank and unknown all refuse as the
239
+ choice they are (`invalid_value`, as 0.17 did), so a form words "Choose your
240
+ state".
241
+
242
+ **One rule each, moved or named here:** the upgrade price and India's calendar
243
+ months (`upgradePrice`, `addPlanPeriod`, `UPGRADE_PER_*`: the Terms state it
244
+ from these), `rupees`, `INDIA_UTC_OFFSET_MS` / `DAY_MS`, `eachOnce`,
245
+ `missingForListing`, `productIsOnDiscover`, `changesDiscoverCard`,
246
+ `CONTACT_VISIBILITY_AUDIENCE` (the user: members only, all four),
247
+ `canonicalCityName`, the one YouTube rule (posts and products: `/v/`, and the
248
+ music and www.youtu.be hosts), `ventureBodyFrom` / `foundedYearDigits`,
249
+ `QueryArgs<S>` (type a list's arguments by the PARSED shape: a query field's
250
+ input is `unknown`), and the values and limits the backend copied by hand:
251
+ `TICKET_SIDE_VALUES`, `INVOICE_KIND_VALUES`, `COUPON_KIND_VALUES`,
252
+ `REPORT_STATUS_VALUES`, `COMMUNITY_SUGGESTION_DECISION_VALUES`,
253
+ `TICKET_CHANGE_VALUES`, `REFUND_ASK_CATEGORY`, `COUPON_PERCENT_*`,
254
+ `COUPON_LABEL_MIN`, `BILLING_NAME_MIN` / `BILLING_ADDRESS_MIN`, `GSTIN_LENGTH`,
255
+ `INVOICE_NUMBER_MAX`, `RAZORPAY_*_MAX`, `RATING_MIN` / `RATING_MAX`,
256
+ `DASHBOARD_RECENT_DAYS`, `PHONE_INPUT_MAX` / `DATE_INPUT_MAX` /
257
+ `GUIDELINES_VERSION_MAX`. The permission fields type their statuses.
258
+
259
+ **Deploy order: the backend, then the web.** The web reads the new response
260
+ fields; a 0.17 web on a 0.18 backend reads the dashboard and payments as before
261
+ (unknown keys are dropped).
262
+
106
263
  ## Breaking in 0.17.0: the onboarding photo is saved, and onboarding's single source of truth (2026-10-19)
107
264
 
108
265
  Two pieces of the user's work on onboarding, 2026-10-19.
@@ -640,8 +797,8 @@ correction of something the backend, the web or the data already disagreed on.
640
797
  | `discoverFacetsSchema` options | `{ value, label, count }` | `{ value, count }` |
641
798
  | `updateUsernameBodySchema` | format only | format, and not reserved |
642
799
 
643
- Deprecated, removed in 0.3.0: `chatInvitationBodySchema` (use
644
- `openChatBodySchema`) and `chatInvitationSchema` (describes nothing that
800
+ Deprecated in 0.2.0, kept until 0.18.0 removed them: `chatInvitationBodySchema`
801
+ (use `openChatBodySchema`) and `chatInvitationSchema` (describes nothing that
645
802
  exists).
646
803
 
647
804
  ---
@@ -671,25 +828,36 @@ src/
671
828
  index.ts the public surface. Everything a consumer imports.
672
829
  env.ts env field builders, its own entry: `founder-schema/env`
673
830
  core/ what every domain builds on (imports no domain)
674
- limits.ts every length, size and count limit, by name (imports nothing)
831
+ limits.ts every length, size and count limit, by name, and India's
832
+ offset and DAY_MS (imports nothing)
675
833
  validation.ts message keys, the wire shape of an issue, toValidationIssues
676
834
  errors.ts every API error code (API_ERROR_CODE_VALUES)
677
835
  common.ts usernames (and the reserved list), the enums, URLs, phone,
678
- location, coordinates, pagination
679
- responses.ts success / error / paginated / { id } envelopes
836
+ location, coordinates, pagination, eachOnce, YouTube links
837
+ responses.ts success / error / paginated / { id } envelopes, QueryArgs
838
+ money.ts rupees(): paise as people read them
680
839
  access/ who may do what (CASL's words, shared by server and web)
681
840
  roles.ts the roles, who may grant which, the active-role window
682
841
  permissions.ts actions, subjects, the fields a rule may name, AppAbility
683
842
  access.ts GET /me/access, switching role, the MPIN
684
- domains/<domain>/ one folder per product area: its bodies, params and answers
685
- account/ audit/ chat/ discover/ feed/ onboarding/ profile/ team/ uploads/
843
+ standing.ts viewer / member (staff count as members), the refusal reasons
844
+ capabilities.ts powers given to selected members (create_communities), who grants them
845
+ domains/<domain>/ one folder per product area: its bodies, params, answers
846
+ and the rules both sides apply
847
+ account/ audit/ billing/ chat/ communities/ contributions/ dashboard/
848
+ discover/ feed/ location/ media/ members/ membership/ moderation/
849
+ notifications/ onboarding/ products/ profile/ reviews/ support/ team/
850
+ uploads/ waitlist/
686
851
  tests/
852
+ the domains' own tests, plus:
687
853
  ssot.test.ts derived not copied, no body coercion, URLs, uploads, codes
854
+ ssot-0-18.test.ts the 0.18.0 audit: each rule moved here, on the rule
688
855
  validation.test.ts keys not sentences, no input leaves, limits applied
689
856
  onboarding-steps.test.ts steps, step schemas and the finish body agree
690
857
  responses.test.ts the envelopes, pinned
691
858
  access.test.ts roles, the grant table, the MPIN, rules across the wire
692
- public-surface.test.ts export-removal tripwire + the values the database is pinned to
859
+ public-surface.test.ts export-removal tripwire, the 0.18.0 removals, and the
860
+ values the database is pinned to
693
861
  ```
694
862
 
695
863
  A consumer imports from the package root and never from a deep path:
@@ -727,9 +895,10 @@ cannot see it and the request looks correct in the network tab. Build a write bo
727
895
  as its own object with no defaults, and have the client send only the keys the
728
896
  member actually changed.
729
897
 
730
- **A default inside a nested object is the same trap one level down.** A patch
731
- carrying `{ location: { city: 'Berlin' } }` picks up `country: 'India'` from
732
- `locationSchema`.
898
+ **A default inside a nested object is the same trap one level down.** A write
899
+ body never fills a nested field in: `locationSchema` has no default country
900
+ since it was removed, and a place is WRITTEN through `locationPickSchema` (area
901
+ ids, 0.19.0), whole: a locality's id, or `null` for the whole district.
733
902
 
734
903
  **A patch field the member can clear has three states, not two.** Absent means
735
904
  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
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAKxB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG;IAC3B,QAAQ;IACR,QAAQ;IACR,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,QAAQ;IACR,SAAS;IACT,SAAS;IACT,OAAO;IACP,WAAW;IACX,OAAO;IACP,MAAM;IACN,MAAM;IACN,MAAM;IACN,SAAS;IACT,QAAQ;CACA,CAAC;AACX,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAGlD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG;IAC5B,KAAK;IACL,WAAW;IACX,QAAQ;IACR,gBAAgB;IAChB,MAAM;IACN,MAAM;IACN,SAAS;IACT,QAAQ;IACR,eAAe;IACf,YAAY;IACZ,cAAc;IACd,YAAY;IACZ,kBAAkB;IAClB,SAAS;IACT,QAAQ;IACR,WAAW;IACX,qBAAqB;IACrB,YAAY;IACZ,SAAS;IACT,QAAQ;CACA,CAAC;AACX,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AA6FpD;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: what a role may do, in CASL's terms\n *\n * Permissions are ACTION on SUBJECT, optionally narrowed by conditions on the\n * subject's fields (\"update a Post whose authorId is mine\"). The backend writes\n * one small policy per role from these words; the web receives the resulting\n * rules and asks the same questions to decide what to draw. Because both sides\n * spell actions and subjects from this file, a permission check with a typo is\n * a compile error, not a button that silently never shows.\n *\n * The SERVER decides; the web only mirrors. Every rule the web holds came from\n * the server for the role being acted as, and every request is checked again\n * on the server, so a tampered client can show itself a button and nothing\n * more.\n *\n * @module founder-schema/access/permissions\n */\n\nimport type { ForcedSubject, MongoAbility } from '@casl/ability';\nimport { z } from 'zod';\nimport type { PurchaseKind } from '../domains/billing/billing.js';\nimport type { PlatformRole } from './roles.js';\nimport type { Standing } from './standing.js';\n\n/**\n * The verbs.\n *\n * manage every action (CASL's wildcard). Only superadmin holds it on `all`.\n * create, read, update, delete\n * moderate hide, restore or remove someone else's content, act on a report\n * assign grant or revoke (a platform role); take or hand over (a ticket)\n * suspend stop a member signing in to the product, and lift it\n * resolve close a support ticket or a report\n * reset clear a credential its owner forgot (a staff member's MPIN)\n * represent act for a product as one of its team: tag a post with it, reply\n * to its reviews, mark feedback helpful, confirm a tester\n *\n * Taking part (0.12.0, membership): the verbs a viewer is refused and a member\n * holds. Creating content stays `create`; these are the rest.\n *\n * react a reaction on a post, poll or comment; an upvote on a product;\n * marking a review helpful\n * vote vote in a poll\n * save bookmark a post or poll\n * join join a community, ask to, or accept its invitation; accept an\n * invitation to a product's team\n * message start a conversation with a Member\n * refund give a payment's money back (staff)\n *\n * Append only: a stored or packed rule names these.\n */\nexport const ACTION_VALUES = [\n 'manage',\n 'create',\n 'read',\n 'update',\n 'delete',\n 'moderate',\n 'assign',\n 'suspend',\n 'resolve',\n 'reset',\n 'represent',\n 'react',\n 'vote',\n 'save',\n 'join',\n 'message',\n 'refund',\n] as const;\nexport const actionSchema = z.enum(ACTION_VALUES);\nexport type Action = z.infer<typeof actionSchema>;\n\n/**\n * The things.\n *\n * all CASL's wildcard subject\n * Dashboard the staff overview: counts and trends\n * Member a person's account in this product\n * FounderProfile a member's public profile\n * Post, Poll, Comment\n * Report a member's report of a post, poll or comment\n * SupportTicket a member's request for help\n * SavedReply a support agent's reusable answer\n * PlatformRole a staff role, as something granted and revoked\n * AuditEntry the record of a staff action\n * PlatformSettings product-wide configuration\n * Product something a founder builds, with its team\n * Review a member's stars and words on a product\n * Community an official community (0.9.0)\n * CommunitySuggestion a member's suggestion for a new community (0.9.0)\n * Membership a person's platform membership: granted, revoked (0.12.0)\n * Payment a purchase and its invoice, and its refund (0.12.0)\n * Coupon a code that gives a membership or a discount (0.12.0)\n */\nexport const SUBJECT_VALUES = [\n 'all',\n 'Dashboard',\n 'Member',\n 'FounderProfile',\n 'Post',\n 'Poll',\n 'Comment',\n 'Report',\n 'SupportTicket',\n 'SavedReply',\n 'PlatformRole',\n 'AuditEntry',\n 'PlatformSettings',\n 'Product',\n 'Review',\n 'Community',\n 'CommunitySuggestion',\n 'Membership',\n 'Payment',\n 'Coupon',\n] as const;\nexport const subjectSchema = z.enum(SUBJECT_VALUES);\nexport type Subject = z.infer<typeof subjectSchema>;\n\n/**\n * The fields a rule may set CONDITIONS on, per subject: the server's stored\n * names, ids as strings. Declared here so a condition on a field the subject\n * does not have (`{ ownerId: me }` on a Post) is a compile error, on both sides.\n *\n * A web screen does not evaluate these against an API response: a response\n * names things differently (`author.userId`, not `authorId`). Row-level answers\n * come from the server on the item itself (`viewer.canEdit`); the web's own\n * checks are type-level (\"may I moderate Reports at all\").\n */\nexport interface SubjectFields {\n Dashboard: Record<never, never>;\n /**\n * `appRoles` is the staff roles held, so a rule can protect a role (\"never\n * suspend a superadmin\").\n *\n * For `message` (0.12.0), what the asker's relation to them is:\n * `contact` says the asker knows them (a teammate, a private community in\n * common, a conversation already); `standing` is theirs, so nobody starts a\n * conversation with a viewer, who cannot chat; `acceptsStrangers` is their\n * own setting (Premium and Business members may start one, or only people\n * they know). Optional because only the messaging check computes them: a\n * rule on a field that is absent never matches, so a check that forgets them\n * is refused, never let through.\n */\n Member: {\n _id: string;\n appRoles: PlatformRole[];\n contact?: boolean;\n standing?: Standing;\n acceptsStrangers?: boolean;\n };\n FounderProfile: { userId: string };\n Post: { authorId: string };\n Poll: { authorId: string };\n Comment: { authorId: string };\n Report: { reporterId: string; status: string };\n SupportTicket: { memberId: string; assigneeId: string | null };\n SavedReply: { authorId: string };\n /** One role on one person: `memberId` is whose, so \"never your own\" is a rule too. */\n PlatformRole: { role: PlatformRole; memberId: string };\n /** `actorId` is null for an entry a server script wrote. */\n AuditEntry: { actorId: string | null };\n PlatformSettings: Record<never, never>;\n /**\n * The team, as three lists of member ids, so a rule reads \"a product whose\n * managers include me\" (`{ managerIds: me }`). `managerIds` is the owner and\n * the co-founders; `memberIds` is everyone on the team, owner included.\n */\n Product: { ownerId: string; managerIds: string[]; memberIds: string[] };\n Review: { authorId: string };\n /**\n * Its admins and moderators as id lists (0.9.0), so a rule reads \"a\n * community whose admins include me\" (`{ adminIds: me }`). Both short by\n * nature; ordinary members are never listed here (a community has no cap).\n */\n Community: { _id: string; adminIds: string[]; moderatorIds: string[] };\n /** `suggestedBy` so \"your own\" is a rule; `status` so only open ones are decided. */\n CommunitySuggestion: { suggestedBy: string; status: string };\n /** Whose membership (0.12.0), so \"never your own\" is a rule. */\n Membership: { userId: string };\n /**\n * Who pays (0.12.0), what for, and where it stands: a viewer may buy only a\n * membership (a plan needs one), only a paid one is refunded, and never by\n * its own payer.\n */\n Payment: { userId: string; status: string; kind: PurchaseKind };\n /** Coupons are the platform's (0.12.0): no condition is ever written on one. */\n Coupon: Record<never, never>;\n}\n\n/** A document tagged with its subject type, as CASL's `subject('Post', doc)` makes one. */\nexport type SubjectInstance = {\n [K in keyof SubjectFields]: SubjectFields[K] & ForcedSubject<K>;\n}[keyof SubjectFields];\n\n/**\n * The ability both sides build: the backend from its policy, the web from the\n * rules the backend sends. Mongo-style conditions, typed by `SubjectFields`.\n */\nexport type AppAbility = MongoAbility<[Action, Subject | SubjectInstance]>;\n\n/**\n * Compile-time proof that `SubjectFields` names exactly the subjects, no more\n * and no fewer: adding a subject without its fields fails the build here.\n */\ntype Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;\ntype Assert<T extends true> = T;\nexport type EverySubjectHasFields = Assert<Same<keyof SubjectFields, Exclude<Subject, 'all'>>>;\n\n/**\n * Rules as they cross the wire: CASL's `packRules` output, an array of\n * compact arrays. The web unpacks them with `unpackRules` before building its\n * ability; their inner shape is CASL's, so it is not re-described here.\n */\nexport const packedRulesSchema = z.array(z.array(z.unknown()));\nexport type PackedRules = z.infer<typeof packedRulesSchema>;\n"]}
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema: what a role may do, in CASL's terms\n *\n * Permissions are ACTION on SUBJECT, optionally narrowed by conditions on the\n * subject's fields (\"update a Post whose authorId is mine\"). The backend writes\n * one small policy per role from these words; the web receives the resulting\n * rules and asks the same questions to decide what to draw. Because both sides\n * spell actions and subjects from this file, a permission check with a typo is\n * a compile error, not a button that silently never shows.\n *\n * The SERVER decides; the web only mirrors. Every rule the web holds came from\n * the server for the role being acted as, and every request is checked again\n * on the server, so a tampered client can show itself a button and nothing\n * more.\n *\n * @module founder-schema/access/permissions\n */\n\nimport type { ForcedSubject, MongoAbility, RawRuleOf } from '@casl/ability';\nimport { z } from 'zod';\nimport type { OrderStatus, PurchaseKind } from '../domains/billing/billing.js';\nimport type { CommunitySuggestionStatus } from '../domains/communities/communities.js';\nimport type { ReportStatus } from '../domains/feed/feed.js';\nimport type { PlatformRole } from './roles.js';\nimport type { Standing } from './standing.js';\n\n/**\n * The verbs.\n *\n * manage every action (CASL's wildcard). Only superadmin holds it on `all`.\n * create, read, update, delete\n * moderate hide, restore or remove someone else's content, act on a report\n * assign grant or revoke (a platform role); take or hand over (a ticket)\n * suspend stop a member signing in to the product, and lift it\n * resolve close a support ticket or a report\n * reset clear a credential its owner forgot (a staff member's MPIN)\n * represent act for a product as one of its team: tag a post with it, reply\n * to its reviews, mark feedback helpful, confirm a tester\n *\n * Taking part (0.12.0, membership): the verbs a viewer is refused and a member\n * holds. Creating content stays `create`; these are the rest.\n *\n * react a reaction on a post, poll or comment; an upvote on a product;\n * marking a review helpful\n * vote vote in a poll\n * save bookmark a post or poll\n * join join a community, ask to, or accept its invitation; accept an\n * invitation to a product's team\n * message start a conversation with a Member\n * refund give a payment's money back (staff)\n *\n * Append only: a stored or packed rule names these.\n */\nexport const ACTION_VALUES = [\n 'manage',\n 'create',\n 'read',\n 'update',\n 'delete',\n 'moderate',\n 'assign',\n 'suspend',\n 'resolve',\n 'reset',\n 'represent',\n 'react',\n 'vote',\n 'save',\n 'join',\n 'message',\n 'refund',\n] as const;\nexport type Action = (typeof ACTION_VALUES)[number];\n\n/**\n * The things.\n *\n * all CASL's wildcard subject\n * Dashboard the staff overview: counts and trends\n * Member a person's account in this product\n * FounderProfile a member's public profile\n * Post, Poll, Comment\n * Report a member's report of a post, poll or comment\n * SupportTicket a member's request for help\n * SavedReply a support agent's reusable answer\n * PlatformRole a staff role, as something granted and revoked\n * AuditEntry the record of a staff action\n * PlatformSettings product-wide configuration\n * Product something a founder builds, with its team\n * Review a member's stars and words on a product\n * Community an official community (0.9.0)\n * CommunitySuggestion a member's suggestion for a new community (0.9.0)\n * Membership a person's platform membership: granted, revoked (0.12.0)\n * Payment a purchase and its invoice, and its refund (0.12.0)\n * Coupon a code that gives a membership or a discount (0.12.0)\n */\nexport const SUBJECT_VALUES = [\n 'all',\n 'Dashboard',\n 'Member',\n 'FounderProfile',\n 'Post',\n 'Poll',\n 'Comment',\n 'Report',\n 'SupportTicket',\n 'SavedReply',\n 'PlatformRole',\n 'AuditEntry',\n 'PlatformSettings',\n 'Product',\n 'Review',\n 'Community',\n 'CommunitySuggestion',\n 'Membership',\n 'Payment',\n 'Coupon',\n] as const;\nexport const subjectSchema = z.enum(SUBJECT_VALUES);\nexport type Subject = z.infer<typeof subjectSchema>;\n\n/**\n * The fields a rule may set CONDITIONS on, per subject: the server's stored\n * names, ids as strings. Declared here so a condition on a field the subject\n * does not have (`{ ownerId: me }` on a Post) is a compile error, on both sides.\n *\n * A web screen does not evaluate these against an API response: a response\n * names things differently (`author.userId`, not `authorId`). Row-level answers\n * come from the server on the item itself (`viewer.canEdit`); the web's own\n * checks are type-level (\"may I moderate Reports at all\").\n */\nexport interface SubjectFields {\n Dashboard: Record<never, never>;\n /**\n * `appRoles` is the staff roles held, so a rule can protect a role (\"never\n * suspend a superadmin\").\n *\n * For `message` (0.12.0), what the asker's relation to them is:\n * `contact` says the asker knows them (a teammate, a private community in\n * common, a conversation already); `standing` is theirs, so nobody starts a\n * conversation with a viewer, who cannot chat; `acceptsStrangers` is their\n * own setting (Premium and Business members may start one, or only people\n * they know). Optional because only the messaging check computes them: a\n * rule on a field that is absent never matches, so a check that forgets them\n * is refused, never let through.\n */\n Member: {\n _id: string;\n appRoles: PlatformRole[];\n contact?: boolean;\n standing?: Standing;\n acceptsStrangers?: boolean;\n };\n FounderProfile: { userId: string };\n Post: { authorId: string };\n Poll: { authorId: string };\n Comment: { authorId: string };\n Report: { reporterId: string; status: ReportStatus };\n SupportTicket: { memberId: string; assigneeId: string | null };\n SavedReply: { authorId: string };\n /** One role on one person: `memberId` is whose, so \"never your own\" is a rule too. */\n PlatformRole: { role: PlatformRole; memberId: string };\n /** `actorId` is null for an entry a server script wrote. */\n AuditEntry: { actorId: string | null };\n PlatformSettings: Record<never, never>;\n /**\n * The team, as three lists of member ids, so a rule reads \"a product whose\n * managers include me\" (`{ managerIds: me }`). `managerIds` is the owner and\n * the co-founders; `memberIds` is everyone on the team, owner included.\n */\n Product: { ownerId: string; managerIds: string[]; memberIds: string[] };\n Review: { authorId: string };\n /**\n * Its admins and moderators as id lists (0.9.0), so a rule reads \"a\n * community whose admins include me\" (`{ adminIds: me }`). Both short by\n * nature; ordinary members are never listed here (a community has no cap).\n */\n Community: { _id: string; adminIds: string[]; moderatorIds: string[] };\n /** `suggestedBy` so \"your own\" is a rule; `status` so only open ones are decided. */\n CommunitySuggestion: { suggestedBy: string; status: CommunitySuggestionStatus };\n /** Whose membership (0.12.0), so \"never your own\" is a rule. */\n Membership: { userId: string };\n /**\n * Who pays (0.12.0), what for, and where it stands: a viewer may buy only a\n * membership (a plan needs one), only a paid one is refunded, and never by\n * its own payer.\n */\n Payment: { userId: string; status: OrderStatus; kind: PurchaseKind };\n /** Coupons are the platform's (0.12.0): no condition is ever written on one. */\n Coupon: Record<never, never>;\n}\n\n/** A document tagged with its subject type, as CASL's `subject('Post', doc)` makes one. */\nexport type SubjectInstance = {\n [K in keyof SubjectFields]: SubjectFields[K] & ForcedSubject<K>;\n}[keyof SubjectFields];\n\n/**\n * The ability both sides build: the backend from its policy, the web from the\n * rules the backend sends. Mongo-style conditions, typed by `SubjectFields`.\n */\nexport type AppAbility = MongoAbility<[Action, Subject | SubjectInstance]>;\n\n/**\n * Compile-time proof that `SubjectFields` names exactly the subjects, no more\n * and no fewer: adding a subject without its fields fails the build here.\n */\ntype Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;\ntype Assert<T extends true> = T;\nexport type EverySubjectHasFields = Assert<Same<keyof SubjectFields, Exclude<Subject, 'all'>>>;\n\n/**\n * Rules as they cross the wire: CASL's `packRules` output, an array of\n * compact arrays. The web unpacks them with `unpackRules` before building its\n * ability. Each starts with its action(s) and subject(s), comma-joined\n * strings (checked, 0.18.0: an unchecked rule list could silently turn staff\n * routing off); what follows (conditions, inverted, fields, reason) is CASL's\n * own compact form.\n */\nconst zeroOrOne = z.union([z.literal(0), z.literal(1)]);\n/**\n * One packed rule, in exactly the shapes CASL's `PackRule` takes (a union of\n * tuples, not a rest: `unpackRules` needs one of these), with `conditions` as\n * `unknown`, which `isAppRawRule` and the ability itself then judge.\n */\nexport const packedRuleSchema = z.union([\n z.tuple([z.string().min(1), z.string().min(1)]),\n z.tuple([z.string().min(1), z.string().min(1), z.unknown()]),\n z.tuple([z.string().min(1), z.string().min(1), z.unknown(), z.literal(1)]),\n z.tuple([z.string().min(1), z.string().min(1), z.unknown(), zeroOrOne, z.string()]),\n z.tuple([\n z.string().min(1),\n z.string().min(1),\n z.unknown(),\n zeroOrOne,\n z.union([z.string(), z.literal(0)]),\n z.string(),\n ]),\n]);\nexport const packedRulesSchema = z.array(packedRuleSchema);\nexport type PackedRules = z.infer<typeof packedRulesSchema>;\n\n/** One rule as CASL's `unpackRules` gives it back, typed for this app. */\nexport type AppRawRule = RawRuleOf<AppAbility>;\n\nconst ACTION_SET: ReadonlySet<string> = new Set(ACTION_VALUES);\nconst SUBJECT_SET: ReadonlySet<string> = new Set(SUBJECT_VALUES);\nconst everyIn = (value: string | string[] | undefined, known: ReadonlySet<string>) =>\n value !== undefined && (Array.isArray(value) ? value : [value]).every((v) => known.has(v));\n\n/**\n * Whether an UNPACKED rule names only this app's actions and subjects\n * (0.18.0). Packed rules cross the wire as strings (`packRules` joins several\n * actions or subjects with commas, so the wire shape cannot be an enum); this\n * is the check that turns them back into `AppRawRule`s, with no cast:\n * `rules.every(isAppRawRule)` narrows the list, and a list that fails is a\n * contract mismatch, never an ability built from it.\n */\nexport function isAppRawRule(rule: {\n action: string | string[];\n subject?: string | string[];\n}): rule is AppRawRule {\n return everyIn(rule.action, ACTION_SET) && everyIn(rule.subject, SUBJECT_SET);\n}\n"]}