founder-schema 0.18.0 → 0.20.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 (156) hide show
  1. package/README.md +110 -3
  2. package/dist/access/permissions.d.ts +3 -3
  3. package/dist/access/permissions.d.ts.map +1 -1
  4. package/dist/access/permissions.js.map +1 -1
  5. package/dist/core/common.d.ts +49 -28
  6. package/dist/core/common.d.ts.map +1 -1
  7. package/dist/core/common.js +65 -26
  8. package/dist/core/common.js.map +1 -1
  9. package/dist/core/deletion.d.ts +40 -0
  10. package/dist/core/deletion.d.ts.map +1 -0
  11. package/dist/core/deletion.js +39 -0
  12. package/dist/core/deletion.js.map +1 -0
  13. package/dist/core/limits.d.ts +25 -0
  14. package/dist/core/limits.d.ts.map +1 -1
  15. package/dist/core/limits.js +25 -0
  16. package/dist/core/limits.js.map +1 -1
  17. package/dist/core/validation.d.ts +13 -0
  18. package/dist/core/validation.d.ts.map +1 -1
  19. package/dist/core/validation.js +13 -0
  20. package/dist/core/validation.js.map +1 -1
  21. package/dist/domains/audit/audit.d.ts +36 -12
  22. package/dist/domains/audit/audit.d.ts.map +1 -1
  23. package/dist/domains/audit/audit.js +9 -10
  24. package/dist/domains/audit/audit.js.map +1 -1
  25. package/dist/domains/billing/billing.d.ts +29 -13
  26. package/dist/domains/billing/billing.d.ts.map +1 -1
  27. package/dist/domains/billing/billing.js +2 -3
  28. package/dist/domains/billing/billing.js.map +1 -1
  29. package/dist/domains/chat/chat.d.ts +19 -5
  30. package/dist/domains/chat/chat.d.ts.map +1 -1
  31. package/dist/domains/chat/chat.js +10 -10
  32. package/dist/domains/chat/chat.js.map +1 -1
  33. package/dist/domains/communities/communities.d.ts +95 -57
  34. package/dist/domains/communities/communities.d.ts.map +1 -1
  35. package/dist/domains/contributions/contributions.d.ts +28 -12
  36. package/dist/domains/contributions/contributions.d.ts.map +1 -1
  37. package/dist/domains/contributions/contributions.js +3 -0
  38. package/dist/domains/contributions/contributions.js.map +1 -1
  39. package/dist/domains/discover/discover.d.ts +2 -2
  40. package/dist/domains/feed/feed.d.ts +111 -63
  41. package/dist/domains/feed/feed.d.ts.map +1 -1
  42. package/dist/domains/feed/feed.js +3 -0
  43. package/dist/domains/feed/feed.js.map +1 -1
  44. package/dist/domains/location/areas.d.ts +338 -0
  45. package/dist/domains/location/areas.d.ts.map +1 -0
  46. package/dist/domains/location/areas.js +241 -0
  47. package/dist/domains/location/areas.js.map +1 -0
  48. package/dist/domains/location/india.d.ts +31 -123
  49. package/dist/domains/location/india.d.ts.map +1 -1
  50. package/dist/domains/location/india.js +98 -64
  51. package/dist/domains/location/india.js.map +1 -1
  52. package/dist/domains/media/media.d.ts +22 -0
  53. package/dist/domains/media/media.d.ts.map +1 -1
  54. package/dist/domains/media/media.js +20 -0
  55. package/dist/domains/media/media.js.map +1 -1
  56. package/dist/domains/members/members.d.ts +26 -9
  57. package/dist/domains/members/members.d.ts.map +1 -1
  58. package/dist/domains/members/members.js +9 -10
  59. package/dist/domains/members/members.js.map +1 -1
  60. package/dist/domains/membership/membership.d.ts +19 -0
  61. package/dist/domains/membership/membership.d.ts.map +1 -1
  62. package/dist/domains/membership/membership.js +10 -0
  63. package/dist/domains/membership/membership.js.map +1 -1
  64. package/dist/domains/moderation/moderation.d.ts +8 -2
  65. package/dist/domains/moderation/moderation.d.ts.map +1 -1
  66. package/dist/domains/moderation/moderation.js +3 -6
  67. package/dist/domains/moderation/moderation.js.map +1 -1
  68. package/dist/domains/notifications/notifications.d.ts +10 -6
  69. package/dist/domains/notifications/notifications.d.ts.map +1 -1
  70. package/dist/domains/onboarding/onboarding-answers.d.ts +9 -12
  71. package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -1
  72. package/dist/domains/onboarding/onboarding-answers.js +12 -19
  73. package/dist/domains/onboarding/onboarding-answers.js.map +1 -1
  74. package/dist/domains/onboarding/onboarding.d.ts +121 -98
  75. package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
  76. package/dist/domains/onboarding/onboarding.js +30 -28
  77. package/dist/domains/onboarding/onboarding.js.map +1 -1
  78. package/dist/domains/people/person.d.ts +54 -0
  79. package/dist/domains/people/person.d.ts.map +1 -0
  80. package/dist/domains/people/person.js +73 -0
  81. package/dist/domains/people/person.js.map +1 -0
  82. package/dist/domains/products/products.d.ts +62 -33
  83. package/dist/domains/products/products.d.ts.map +1 -1
  84. package/dist/domains/products/products.js +3 -0
  85. package/dist/domains/products/products.js.map +1 -1
  86. package/dist/domains/profile/founder-profile.d.ts +93 -60
  87. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  88. package/dist/domains/profile/founder-profile.js +55 -25
  89. package/dist/domains/profile/founder-profile.js.map +1 -1
  90. package/dist/domains/reviews/reviews.d.ts +25 -15
  91. package/dist/domains/reviews/reviews.d.ts.map +1 -1
  92. package/dist/domains/support/support.d.ts +47 -10
  93. package/dist/domains/support/support.d.ts.map +1 -1
  94. package/dist/domains/support/support.js +4 -2
  95. package/dist/domains/support/support.js.map +1 -1
  96. package/dist/domains/team/team.d.ts +9 -3
  97. package/dist/domains/team/team.d.ts.map +1 -1
  98. package/dist/domains/team/team.js +3 -6
  99. package/dist/domains/team/team.js.map +1 -1
  100. package/dist/index.d.ts +13 -10
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +17 -11
  103. package/dist/index.js.map +1 -1
  104. package/package.json +3 -2
  105. package/dist/account.d.ts +0 -40
  106. package/dist/account.d.ts.map +0 -1
  107. package/dist/account.js +0 -39
  108. package/dist/account.js.map +0 -1
  109. package/dist/chat.d.ts +0 -187
  110. package/dist/chat.d.ts.map +0 -1
  111. package/dist/chat.js +0 -189
  112. package/dist/chat.js.map +0 -1
  113. package/dist/common.d.ts +0 -304
  114. package/dist/common.d.ts.map +0 -1
  115. package/dist/common.js +0 -340
  116. package/dist/common.js.map +0 -1
  117. package/dist/discover.d.ts +0 -245
  118. package/dist/discover.d.ts.map +0 -1
  119. package/dist/discover.js +0 -68
  120. package/dist/discover.js.map +0 -1
  121. package/dist/errors.d.ts +0 -85
  122. package/dist/errors.d.ts.map +0 -1
  123. package/dist/errors.js +0 -89
  124. package/dist/errors.js.map +0 -1
  125. package/dist/feed.d.ts +0 -1658
  126. package/dist/feed.d.ts.map +0 -1
  127. package/dist/feed.js +0 -623
  128. package/dist/feed.js.map +0 -1
  129. package/dist/founder-profile.d.ts +0 -387
  130. package/dist/founder-profile.d.ts.map +0 -1
  131. package/dist/founder-profile.js +0 -180
  132. package/dist/founder-profile.js.map +0 -1
  133. package/dist/limits.d.ts +0 -76
  134. package/dist/limits.d.ts.map +0 -1
  135. package/dist/limits.js +0 -84
  136. package/dist/limits.js.map +0 -1
  137. package/dist/onboarding.d.ts +0 -1018
  138. package/dist/onboarding.d.ts.map +0 -1
  139. package/dist/onboarding.js +0 -717
  140. package/dist/onboarding.js.map +0 -1
  141. package/dist/responses/common.d.ts +0 -167
  142. package/dist/responses/common.d.ts.map +0 -1
  143. package/dist/responses/common.js +0 -67
  144. package/dist/responses/common.js.map +0 -1
  145. package/dist/responses/index.d.ts +0 -7
  146. package/dist/responses/index.d.ts.map +0 -1
  147. package/dist/responses/index.js +0 -7
  148. package/dist/responses/index.js.map +0 -1
  149. package/dist/uploads.d.ts +0 -113
  150. package/dist/uploads.d.ts.map +0 -1
  151. package/dist/uploads.js +0 -106
  152. package/dist/uploads.js.map +0 -1
  153. package/dist/validation.d.ts +0 -136
  154. package/dist/validation.d.ts.map +0 -1
  155. package/dist/validation.js +0 -142
  156. package/dist/validation.js.map +0 -1
package/README.md CHANGED
@@ -95,7 +95,7 @@ SAME schema object, so the two can never disagree about what is allowed.
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
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
98
+ one `locationPickSchema` to WRITE one (area ids, 0.19.0), one `founderLinksSchema`, one
99
99
  `ventureSchema`, one `mediaImageSchema`; the other places pick, omit or reuse
100
100
  them, and a test checks by object identity.
101
101
 
@@ -104,6 +104,109 @@ instance; this package must not bring its own.
104
104
 
105
105
  ---
106
106
 
107
+ ## Breaking in 0.20.0: one person everywhere, names as first + last, soft delete (2026-10-22)
108
+
109
+ The data model redesign the user approved on 2026-10-22 (`founder-circle/DATA-MODEL-PLAN-2026-10-22.md`).
110
+
111
+ **One person on every surface: `personSchema`.**
112
+ - Shape: `{ userId, username | null, displayName, firstName, avatarUrl | null, former }`.
113
+ - It replaces seven variants:
114
+ - `memberBylineSchema` is now the person plus a `headline`;
115
+ - `memberSummarySchema`, `teamMemberSchema`, `chatParticipantSchema`, the moderation `author`, every
116
+ support person, `payerSchema`, the audit actor and `suspension.by` are all built on it.
117
+ - Where these had `id`, they now have `userId`.
118
+ - **Former members.** A deleted account is `former: true`, with no name, handle or picture (`formerPerson`).
119
+ Clients word it ("Former member") and show a neutral icon. A nullable person still means "nobody" (the
120
+ system did it).
121
+ - `initialsOf` gives the avatar fallback: two letters (first name and last word), empty for a former
122
+ member.
123
+ - A chat participant is answered for every id asked; a deleted account comes back as a former person.
124
+
125
+ **The name.** The user's ruling: one "Full name" box, the display name derived from first + last.
126
+ - `displayNameSchema` is gone. What a member types is `fullNameSchema`, under the key `fullName`:
127
+ - in the onboarding form, draft and finish body (the basics step);
128
+ - in `PATCH /profiles/me/personal`.
129
+ - The server splits it at the first space (`splitFullName`) and stores `firstName` and `lastName`. The
130
+ display name is them joined (`joinFullName`), which is the tidied name as typed.
131
+ - A sign-in name is seeded the same way (`nameFromSignIn`), cut to `DISPLAY_NAME_MAX` without splitting a
132
+ character (`graphemesOf`).
133
+ - `readOnboardingDraft` still reads a draft saved with the old `displayName` key.
134
+ - `founderProfileSchema` (owner) and `myPersonalDetailsSchema` carry `firstName` and `lastName`.
135
+
136
+ **The photo.** `founderProfileSchema` carries `useProviderPhoto` and `hasProviderPhoto`. `PATCH
137
+ /profiles/me/photo-settings` (`updatePhotoSettingsBodySchema`) turns the sign-in provider's picture off or
138
+ on. Off, a member with no upload shows their initials everywhere.
139
+
140
+ **Removed from `founderProfileSchema`:** `isOnboarded` (a profile exists only once onboarding is finished)
141
+ and `isActive` (replaced by soft delete).
142
+
143
+ **Soft delete.** `DELETED_REASON_VALUES` / `deletedReasonSchema` name why a row was deleted. The backend
144
+ stamps them beside `deletedAt`, and one purge removes rows `ERASURE_RETENTION_DAYS` after.
145
+
146
+ **Release:** publish, pin EXACTLY in both consumers, deploy backend and web together, then run the dev
147
+ migrations in the plan's order.
148
+
149
+ ## Breaking in 0.19.0: location is Jansathi's model (2026-10-22)
150
+
151
+ The user, 2026-10-21/22: Jansathi's location drawer (state, then district, then
152
+ locality, from `master_db.areas`) and its whole location model ("just copy
153
+ paste things from jansathi"). Plan, Jansathi analysis and rulings:
154
+ `founder-circle/LOCATION-PLAN-2026-10-21.md`. Contract:
155
+ `src/domains/location/areas.ts`.
156
+
157
+ **Removed** (listed in `public-surface.test.ts`, `REMOVED_IN_0_19`):
158
+ `indiaLocationSchema` (a typed city and state is no longer a write),
159
+ `suggestedLocationSchema` and `coordinatesQuerySchema` (`GET
160
+ /location/areas/reverse` is gone; GPS goes to `POST /me/geolocation`).
161
+
162
+ **Changed on the wire:**
163
+ - A location is WRITTEN as area ids, `locationPickSchema`
164
+ `{ stateId, districtId, localityId | null }` (`null`: "my locality is not
165
+ listed", the whole district): onboarding's finish and
166
+ `PATCH /profiles/me/personal`. The server names the place, the public city
167
+ (`publicCityOf`: the town the member is in, else their district, cleaned by
168
+ `townName` and spelled as the state's list spells it; Discover's city filter
169
+ is unchanged) and its coordinates from `master_db`. Refusals:
170
+ `stateRequired`, `districtRequired`, `localityRequired`, `areaNotListed`,
171
+ `stateUnknown`, each on its id.
172
+ - The onboarding draft's `location` is a `Place` (names and ids). A draft saved
173
+ before 0.19.0 holds a typed place, which names no area: it reads as not
174
+ answered.
175
+ - The owner's profile (`GET /profiles/me`) carries `place`; no card, member page
176
+ or staff view does. Other members still read `location` (city, state,
177
+ country), unchanged.
178
+ - New: `GET /location/areas/states|districts|localities` (`areaListSchema`,
179
+ `areaPageSchema`, `LOCALITY_PAGE_SIZE`), and `POST /me/geolocation`
180
+ (`geolocationFixSchema` -> `geolocationSyncResultSchema` `{ synced, match }`).
181
+ The GPS axis (fix, area, the last `LOCATION_HISTORY_MAX` of each) is kept as
182
+ Jansathi keeps it but NEVER becomes the declared location (the user, "No,
183
+ member saves") and is never shown to another member. History is kept at most
184
+ `LOCATION_HISTORY_MAX_DAYS` (365) days.
185
+ - The member's own control (the user, 2026-10-22, "Add stop + clear"):
186
+ `GET`/`PATCH /me/location-settings` (`locationSettingsSchema`,
187
+ `updateLocationSettingsBodySchema`; `dailyLocation` OFF keeps no position)
188
+ and `DELETE /me/location-history` (`clearLocationHistoryResultSchema`; the
189
+ chosen place stays).
190
+ - `queryInt(min, max, fallback)` is the query integer helper (core/common.ts).
191
+ - `areaOptionSchema.within` (a ward's town, a village's tehsil or block) tells
192
+ same-named places apart; `areaWords` reads a numbered ward as "Ward 25";
193
+ `placeWords` includes `within`. `AREA_NAME_MAX` is 300: the all-India tree has
194
+ names up to 250 characters.
195
+ - The public city is `publicCityOf`; `townName` cleans a council's name. Over
196
+ all of India (4,995 public cities, 2026-10-22) none is rewritten by
197
+ Discover's state-blind `canonicalCityName` (Bijapur is listed for
198
+ Chhattisgarh too), none keeps council words or brackets, the longest is 35.
199
+ A twin town is one hyphen however the tree spaced it (Murwara-Katni), the
200
+ list matches hyphen and space alike (the tree's "Pimpri Chinchwad" is
201
+ Pimpri-Chinchwad), and a name in capitals reads title-case (MAUGANJ is
202
+ Mauganj): over every town and district (5,745), no city is written two ways
203
+ in one state. It always fits `CITY_MAX` (a too-long town is shown as its
204
+ district, a too-long district is cut at a word), so a save never fails on it.
205
+
206
+ **Release:** publish, pin EXACTLY in both consumers, backend before web (a 0.18
207
+ web sends a typed place, which a 0.19 backend refuses). The drawer needs real
208
+ data in `master_db.areas`: AWS dev's was synthetic until the all-India load.
209
+
107
210
  ## Breaking in 0.18.0: the SSOT audit (2026-10-20)
108
211
 
109
212
  The user, 2026-10-20: "make sure it is the SSOT for data types, validation,
@@ -757,6 +860,10 @@ Two things that follow from it:
757
860
  - Removing or renaming an export is the only change here that can break a consumer
758
861
  that already compiles. `tests/public-surface.test.ts` fails on any removal, which
759
862
  is the cheap version of the publish-time no-loss check.
863
+ - `npm run build` empties `dist` first (0.20.0, `npm run clean`). Up to 0.19.0 the
864
+ publishing checkout kept files from an old layout (`dist/account.js`,
865
+ `dist/common.js` and others), and they were published with every release.
866
+ Nothing imported them, but a package must hold exactly what its source builds.
760
867
 
761
868
  ---
762
869
 
@@ -836,8 +943,8 @@ member actually changed.
836
943
 
837
944
  **A default inside a nested object is the same trap one level down.** A write
838
945
  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.
946
+ since it was removed, and a place is WRITTEN through `locationPickSchema` (area
947
+ ids, 0.19.0), whole: a locality's id, or `null` for the whole district.
841
948
 
842
949
  **A patch field the member can clear has three states, not two.** Absent means
843
950
  do not touch. `null` means clear, and the backend maps it to `$unset`. An empty
@@ -110,8 +110,8 @@ export type Subject = z.infer<typeof subjectSchema>;
110
110
  export interface SubjectFields {
111
111
  Dashboard: Record<never, never>;
112
112
  /**
113
- * `appRoles` is the staff roles held, so a rule can protect a role ("never
114
- * suspend a superadmin").
113
+ * `roles` is the staff roles held (0.20.0: from their access grant; `appRoles`
114
+ * until 0.19.0), so a rule can protect a role ("never suspend a superadmin").
115
115
  *
116
116
  * For `message` (0.12.0), what the asker's relation to them is:
117
117
  * `contact` says the asker knows them (a teammate, a private community in
@@ -124,7 +124,7 @@ export interface SubjectFields {
124
124
  */
125
125
  Member: {
126
126
  _id: string;
127
- appRoles: PlatformRole[];
127
+ roles: PlatformRole[];
128
128
  contact?: boolean;
129
129
  standing?: Standing;
130
130
  acceptsStrangers?: boolean;
@@ -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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC5E,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,uCAAuC,CAAC;AACvF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,aAAa,mLAkBhB,CAAC;AACX,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,cAAc,6QAqBjB,CAAC;AACX,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;EAAyB,CAAC;AACpD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAChC;;;;;;;;;;;;OAYG;IACH,MAAM,EAAE;QACN,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,YAAY,EAAE,CAAC;QACtB,OAAO,CAAC,EAAE,OAAO,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,CAAC;QACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,CAAC;IACF,cAAc,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,CAAC;IACrD,aAAa,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/D,UAAU,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,sFAAsF;IACtF,YAAY,EAAE;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,4DAA4D;IAC5D,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACvC,gBAAgB,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACvC;;;;OAIG;IACH,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACxE,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B;;;;OAIG;IACH,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACvE,qFAAqF;IACrF,mBAAmB,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,yBAAyB,CAAA;KAAE,CAAC;IAChF,gEAAgE;IAChE,UAAU,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/B;;;;OAIG;IACH,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAC;QAAC,IAAI,EAAE,YAAY,CAAA;KAAE,CAAC;IACrE,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;CAC9B;AAED,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG;KAC3B,CAAC,IAAI,MAAM,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;CAChE,CAAC,MAAM,aAAa,CAAC,CAAC;AAEvB;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC;AAE3E;;;GAGG;AACH,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;AAC7E,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAChC,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,aAAa,EAAE,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAW/F;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,ugBAa3B,CAAC;AACH,eAAO,MAAM,iBAAiB,mhBAA4B,CAAC;AAC3D,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,0EAA0E;AAC1E,MAAM,MAAM,UAAU,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC;AAO/C;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CAC7B,GAAG,IAAI,IAAI,UAAU,CAErB"}
@@ -1 +1 @@
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"]}
1
+ {"version":3,"file":"permissions.js","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB;;;;;;;;;;;;;;;;;;;;;;;;;;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;AAGX;;;;;;;;;;;;;;;;;;;;;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;;;;;;;GAOG;AACH,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxD;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC;IACtC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/C,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5D,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IAC1E,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACnF,CAAC,CAAC,KAAK,CAAC;QACN,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACjB,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;QACjB,CAAC,CAAC,OAAO,EAAE;QACX,SAAS;QACT,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;QACnC,CAAC,CAAC,MAAM,EAAE;KACX,CAAC;CACH,CAAC,CAAC;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC;AAM3D,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;AAC/D,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAC,cAAc,CAAC,CAAC;AACjE,MAAM,OAAO,GAAG,CAAC,KAAoC,EAAE,KAA0B,EAAE,EAAE,CACnF,KAAK,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;AAE7F;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,IAG5B;IACC,OAAO,OAAO,CAAC,IAAI,CAAC,MAAM,EAAE,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;AAChF,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, 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 * `roles` is the staff roles held (0.20.0: from their access grant; `appRoles`\n * until 0.19.0), so a rule can protect a role (\"never 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 roles: 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"]}
@@ -372,38 +372,65 @@ export declare const genderSchema: z.ZodEnum<{
372
372
  export type Gender = z.infer<typeof genderSchema>;
373
373
  /**
374
374
  * A full name as it is sent and stored (0.17.0, moved here from the web):
375
- * trimmed, and every run of spaces made one. `displayNameSchema` applies it,
375
+ * trimmed, and every run of spaces made one. `fullNameSchema` applies it,
376
376
  * so the server stores it from every client; a client applies it to show the
377
377
  * member what will be kept. What `splitFullName` reads.
378
378
  */
379
379
  export declare const tidyFullName: (fullName: string) => string;
380
380
  /**
381
- * The name a member is shown by. ONE rule for every body that writes it:
382
- * onboarding's finish, the profile patch and the personal-details patch, so a
383
- * name one accepts the others can never refuse, and each stores it TIDIED
384
- * (`tidyFullName`, 0.17.0: "Asha Rao" from the Me editor was stored as
385
- * typed, while onboarding sent it tidy). Plain bounds, no custom message:
381
+ * A member's full name as they type it (0.20.0; `displayNameSchema` until
382
+ * 0.19.0): onboarding's "Full name" box and Me's. ONE rule for every body that
383
+ * takes it, stored TIDIED (`tidyFullName`). Plain bounds, no custom message:
386
384
  * `too_small` with minimum 1 already says "required", and the client words it.
385
+ *
386
+ * It is never stored as typed: the server splits it (`splitFullName`) and
387
+ * stores the first and last name; the display name is those two joined
388
+ * (`joinFullName`), which is this same tidied string, so nothing is lost.
387
389
  */
388
- export declare const displayNameSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
390
+ export declare const fullNameSchema: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
389
391
  /**
390
- * A full name's first and last name, DEDUCED, never asked (the user,
391
- * 2026-10-18: onboarding asks one "Full name" and works the two out from the
392
- * space). The first word is the first name and everything after the first
393
- * space is the last name, kept as written ("Asha Rao Kulkarni" is Asha and
394
- * Rao Kulkarni).
392
+ * A full name's first and last name (the user, 2026-10-18 and 2026-10-22:
393
+ * one "Full name" box, split at the space). The first word is the first name
394
+ * and everything after the first space is the last name, kept as written
395
+ * ("Asha Rao Kulkarni" is Asha and Rao Kulkarni).
395
396
  *
396
397
  * ⚠ A ONE-WORD NAME IS A WHOLE NAME. Many people in India go by a single name,
397
398
  * so `lastName` is then `''`, and nothing may refuse it for that.
398
399
  *
399
- * Derived wherever it is needed, never stored as a second copy of the name:
400
- * `displayName` stays the one owner of a member's name, so a rename can never
401
- * leave a stale first name behind.
400
+ * STORED since 0.20.0 (the user, 2026-10-22: "the display name should be a
401
+ * combination of first name and last name, auto-calculated"): the account
402
+ * keeps `firstName` and `lastName`, and the display name is derived from them
403
+ * (`joinFullName`). So a first name never holds a space, and
404
+ * `joinFullName(splitFullName(x))` is `tidyFullName(x)`.
402
405
  */
403
406
  export declare function splitFullName(fullName: string): {
404
407
  firstName: string;
405
408
  lastName: string;
406
409
  };
410
+ /**
411
+ * The display name (0.20.0): the first and last name joined by one space,
412
+ * tidied. The ONLY way a display name is made: the account's model hook calls
413
+ * it, and nothing writes a display name directly.
414
+ */
415
+ export declare function joinFullName(firstName: string, lastName?: string | null): string;
416
+ /**
417
+ * A string's user-perceived characters: an emoji, a flag or a letter with its
418
+ * accents is ONE, never split (falls back to code points without `Intl.Segmenter`).
419
+ */
420
+ export declare function graphemesOf(text: string): string[];
421
+ /**
422
+ * The first and last name to seed an account with, from the sign-in service
423
+ * (0.20.0): its `firstName` and `lastName`, joined, cut to `DISPLAY_NAME_MAX`
424
+ * without splitting a character, then split at the first space exactly as the
425
+ * "Full name" box is. So a seeded name keeps the same rule as a typed one: a
426
+ * Google given name "Mary Ann" with family name "Rao" is first name Mary, last
427
+ * name Ann Rao. Empty parts are `''`; the sign-in service always sends a first
428
+ * name, and a blank one stays blank for the caller to handle.
429
+ */
430
+ export declare function nameFromSignIn(firstName?: string | null, lastName?: string | null): {
431
+ firstName: string;
432
+ lastName: string;
433
+ };
407
434
  /** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */
408
435
  export declare const PHONE_DIGITS = 10;
409
436
  /**
@@ -438,8 +465,9 @@ export declare function normalizePhone(raw: string): string;
438
465
  * location has a country, because the model requires one, so the read side
439
466
  * loses nothing.
440
467
  *
441
- * This is a DECLARED location, not a GPS reading. Jansathi's rule applies
442
- * here too: never write a declared location from a GPS capture.
468
+ * This is a DECLARED location, not a GPS reading (Jansathi's rule): a GPS
469
+ * fix only ever fills the member's field; the member saves it (0.19.0,
470
+ * `domains/location/areas.ts`).
443
471
  */
444
472
  export declare const locationSchema: z.ZodObject<{
445
473
  city: z.ZodString;
@@ -448,18 +476,11 @@ export declare const locationSchema: z.ZodObject<{
448
476
  }, z.core.$strip>;
449
477
  export type FounderLocation = z.infer<typeof locationSchema>;
450
478
  /**
451
- * `GET /api/v1/location/areas/reverse?lat=&lng=`: plain decimal degrees.
452
- *
453
- * A QUERY, so coercion is right here: everything in a URL is a string. An
454
- * emptied parameter is "missing", never 0, because 0,0 is a real place in the
455
- * Gulf of Guinea and would be looked up rather than refused.
479
+ * A whole number in a QUERY (everything in a URL is text, so coercion is right
480
+ * here and only here). An emptied parameter is "not provided": the fallback,
481
+ * never 0 by accident.
456
482
  */
457
- export declare const coordinatesQuerySchema: z.ZodObject<{
458
- lat: z.ZodPreprocess<z.ZodCoercedNumber<unknown>, unknown>;
459
- lng: z.ZodPreprocess<z.ZodCoercedNumber<unknown>, unknown>;
460
- }, z.core.$strip>;
461
- export type CoordinatesQuery = z.infer<typeof coordinatesQuerySchema>;
462
- export type CoordinatesQueryInput = z.input<typeof coordinatesQuerySchema>;
483
+ export declare const queryInt: (min: number, max: number, fallback: number) => z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
463
484
  /**
464
485
  * Cursor pagination query. Feed and Discover both use it.
465
486
  *
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../src/core/common.ts"],"names":[],"mappings":"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"}
@@ -411,38 +411,41 @@ export const genderSchema = z.enum(GENDER_VALUES);
411
411
  // ─── Display name ────────────────────────────────────────────────────────────
412
412
  /**
413
413
  * A full name as it is sent and stored (0.17.0, moved here from the web):
414
- * trimmed, and every run of spaces made one. `displayNameSchema` applies it,
414
+ * trimmed, and every run of spaces made one. `fullNameSchema` applies it,
415
415
  * so the server stores it from every client; a client applies it to show the
416
416
  * member what will be kept. What `splitFullName` reads.
417
417
  */
418
418
  export const tidyFullName = (fullName) => fullName.trim().replace(/\s+/g, ' ');
419
419
  /**
420
- * The name a member is shown by. ONE rule for every body that writes it:
421
- * onboarding's finish, the profile patch and the personal-details patch, so a
422
- * name one accepts the others can never refuse, and each stores it TIDIED
423
- * (`tidyFullName`, 0.17.0: "Asha Rao" from the Me editor was stored as
424
- * typed, while onboarding sent it tidy). Plain bounds, no custom message:
420
+ * A member's full name as they type it (0.20.0; `displayNameSchema` until
421
+ * 0.19.0): onboarding's "Full name" box and Me's. ONE rule for every body that
422
+ * takes it, stored TIDIED (`tidyFullName`). Plain bounds, no custom message:
425
423
  * `too_small` with minimum 1 already says "required", and the client words it.
424
+ *
425
+ * It is never stored as typed: the server splits it (`splitFullName`) and
426
+ * stores the first and last name; the display name is those two joined
427
+ * (`joinFullName`), which is this same tidied string, so nothing is lost.
426
428
  */
427
- export const displayNameSchema = z
429
+ export const fullNameSchema = z
428
430
  .string()
429
431
  .trim()
430
432
  .min(1)
431
433
  .max(DISPLAY_NAME_MAX)
432
434
  .transform(tidyFullName);
433
435
  /**
434
- * A full name's first and last name, DEDUCED, never asked (the user,
435
- * 2026-10-18: onboarding asks one "Full name" and works the two out from the
436
- * space). The first word is the first name and everything after the first
437
- * space is the last name, kept as written ("Asha Rao Kulkarni" is Asha and
438
- * Rao Kulkarni).
436
+ * A full name's first and last name (the user, 2026-10-18 and 2026-10-22:
437
+ * one "Full name" box, split at the space). The first word is the first name
438
+ * and everything after the first space is the last name, kept as written
439
+ * ("Asha Rao Kulkarni" is Asha and Rao Kulkarni).
439
440
  *
440
441
  * ⚠ A ONE-WORD NAME IS A WHOLE NAME. Many people in India go by a single name,
441
442
  * so `lastName` is then `''`, and nothing may refuse it for that.
442
443
  *
443
- * Derived wherever it is needed, never stored as a second copy of the name:
444
- * `displayName` stays the one owner of a member's name, so a rename can never
445
- * leave a stale first name behind.
444
+ * STORED since 0.20.0 (the user, 2026-10-22: "the display name should be a
445
+ * combination of first name and last name, auto-calculated"): the account
446
+ * keeps `firstName` and `lastName`, and the display name is derived from them
447
+ * (`joinFullName`). So a first name never holds a space, and
448
+ * `joinFullName(splitFullName(x))` is `tidyFullName(x)`.
446
449
  */
447
450
  export function splitFullName(fullName) {
448
451
  const name = tidyFullName(fullName);
@@ -451,6 +454,46 @@ export function splitFullName(fullName) {
451
454
  ? { firstName: name, lastName: '' }
452
455
  : { firstName: name.slice(0, space), lastName: name.slice(space + 1) };
453
456
  }
457
+ /**
458
+ * The display name (0.20.0): the first and last name joined by one space,
459
+ * tidied. The ONLY way a display name is made: the account's model hook calls
460
+ * it, and nothing writes a display name directly.
461
+ */
462
+ export function joinFullName(firstName, lastName) {
463
+ return tidyFullName(`${firstName} ${lastName ?? ''}`);
464
+ }
465
+ const graphemeSegmenter = typeof Intl !== 'undefined' && 'Segmenter' in Intl
466
+ ? new Intl.Segmenter(undefined, { granularity: 'grapheme' })
467
+ : null;
468
+ /**
469
+ * A string's user-perceived characters: an emoji, a flag or a letter with its
470
+ * accents is ONE, never split (falls back to code points without `Intl.Segmenter`).
471
+ */
472
+ export function graphemesOf(text) {
473
+ return graphemeSegmenter
474
+ ? Array.from(graphemeSegmenter.segment(text), (s) => s.segment)
475
+ : Array.from(text);
476
+ }
477
+ /**
478
+ * The first and last name to seed an account with, from the sign-in service
479
+ * (0.20.0): its `firstName` and `lastName`, joined, cut to `DISPLAY_NAME_MAX`
480
+ * without splitting a character, then split at the first space exactly as the
481
+ * "Full name" box is. So a seeded name keeps the same rule as a typed one: a
482
+ * Google given name "Mary Ann" with family name "Rao" is first name Mary, last
483
+ * name Ann Rao. Empty parts are `''`; the sign-in service always sends a first
484
+ * name, and a blank one stays blank for the caller to handle.
485
+ */
486
+ export function nameFromSignIn(firstName, lastName) {
487
+ const full = graphemesOf(joinFullName(firstName ?? '', lastName))
488
+ .slice(0, DISPLAY_NAME_MAX)
489
+ .join('');
490
+ // A cut can land on a character that is longer than one UTF-16 unit; the
491
+ // model's `maxlength` counts units, so trim whole characters until it fits.
492
+ let fitted = full;
493
+ while (fitted.length > DISPLAY_NAME_MAX)
494
+ fitted = graphemesOf(fitted).slice(0, -1).join('');
495
+ return splitFullName(fitted);
496
+ }
454
497
  // ─── Phone ───────────────────────────────────────────────────────────────────
455
498
  /** The digits an Indian mobile number has: what `phoneSchema` takes and the field keeps. */
456
499
  export const PHONE_DIGITS = 10;
@@ -498,8 +541,9 @@ export function normalizePhone(raw) {
498
541
  * location has a country, because the model requires one, so the read side
499
542
  * loses nothing.
500
543
  *
501
- * This is a DECLARED location, not a GPS reading. Jansathi's rule applies
502
- * here too: never write a declared location from a GPS capture.
544
+ * This is a DECLARED location, not a GPS reading (Jansathi's rule): a GPS
545
+ * fix only ever fills the member's field; the member saves it (0.19.0,
546
+ * `domains/location/areas.ts`).
503
547
  */
504
548
  export const locationSchema = z.object({
505
549
  city: z.string().trim().min(1).max(CITY_MAX),
@@ -507,16 +551,11 @@ export const locationSchema = z.object({
507
551
  country: z.string().trim().min(1).max(COUNTRY_MAX),
508
552
  });
509
553
  /**
510
- * `GET /api/v1/location/areas/reverse?lat=&lng=`: plain decimal degrees.
511
- *
512
- * A QUERY, so coercion is right here: everything in a URL is a string. An
513
- * emptied parameter is "missing", never 0, because 0,0 is a real place in the
514
- * Gulf of Guinea and would be looked up rather than refused.
554
+ * A whole number in a QUERY (everything in a URL is text, so coercion is right
555
+ * here and only here). An emptied parameter is "not provided": the fallback,
556
+ * never 0 by accident.
515
557
  */
516
- export const coordinatesQuerySchema = z.object({
517
- lat: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-90).max(90)),
518
- lng: z.preprocess(emptyStringToUndefined, z.coerce.number().min(-180).max(180)),
519
- });
558
+ export const queryInt = (min, max, fallback) => z.preprocess(emptyStringToUndefined, z.coerce.number().int().min(min).max(max).default(fallback));
520
559
  // ─── Pagination ──────────────────────────────────────────────────────────────
521
560
  /**
522
561
  * Cursor pagination query. Feed and Discover both use it.