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.
- package/README.md +186 -17
- package/dist/access/access.d.ts +1 -1
- package/dist/access/access.d.ts.map +1 -1
- package/dist/access/access.js +4 -1
- package/dist/access/access.js.map +1 -1
- package/dist/access/permissions.d.ts +27 -29
- package/dist/access/permissions.d.ts.map +1 -1
- package/dist/access/permissions.js +39 -3
- package/dist/access/permissions.js.map +1 -1
- package/dist/core/common.d.ts +54 -23
- package/dist/core/common.d.ts.map +1 -1
- package/dist/core/common.js +49 -24
- package/dist/core/common.js.map +1 -1
- package/dist/core/errors.d.ts +7 -5
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +17 -11
- package/dist/core/errors.js.map +1 -1
- package/dist/core/limits.d.ts +73 -17
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +77 -22
- package/dist/core/limits.js.map +1 -1
- package/dist/core/money.d.ts +33 -0
- package/dist/core/money.d.ts.map +1 -0
- package/dist/core/money.js +36 -0
- package/dist/core/money.js.map +1 -0
- package/dist/core/responses.d.ts +14 -4
- package/dist/core/responses.d.ts.map +1 -1
- package/dist/core/responses.js.map +1 -1
- package/dist/core/validation.d.ts +19 -2
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +19 -2
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/billing/billing.d.ts +61 -8
- package/dist/domains/billing/billing.d.ts.map +1 -1
- package/dist/domains/billing/billing.js +58 -22
- package/dist/domains/billing/billing.js.map +1 -1
- package/dist/domains/billing/gst.d.ts.map +1 -1
- package/dist/domains/billing/gst.js +4 -3
- package/dist/domains/billing/gst.js.map +1 -1
- package/dist/domains/chat/chat.d.ts +0 -21
- package/dist/domains/chat/chat.d.ts.map +1 -1
- package/dist/domains/chat/chat.js +0 -20
- package/dist/domains/chat/chat.js.map +1 -1
- package/dist/domains/communities/communities.d.ts +30 -12
- package/dist/domains/communities/communities.d.ts.map +1 -1
- package/dist/domains/communities/communities.js +43 -15
- package/dist/domains/communities/communities.js.map +1 -1
- package/dist/domains/contributions/contributions.d.ts +1 -1
- package/dist/domains/contributions/contributions.js +1 -1
- package/dist/domains/contributions/contributions.js.map +1 -1
- package/dist/domains/dashboard/dashboard.d.ts +3 -1
- package/dist/domains/dashboard/dashboard.d.ts.map +1 -1
- package/dist/domains/dashboard/dashboard.js +23 -7
- package/dist/domains/dashboard/dashboard.js.map +1 -1
- package/dist/domains/discover/discover.d.ts +8 -8
- package/dist/domains/feed/feed.d.ts +55 -153
- package/dist/domains/feed/feed.d.ts.map +1 -1
- package/dist/domains/feed/feed.js +53 -58
- package/dist/domains/feed/feed.js.map +1 -1
- package/dist/domains/location/areas.d.ts +331 -0
- package/dist/domains/location/areas.d.ts.map +1 -0
- package/dist/domains/location/areas.js +238 -0
- package/dist/domains/location/areas.js.map +1 -0
- package/dist/domains/location/india.d.ts +41 -123
- package/dist/domains/location/india.d.ts.map +1 -1
- package/dist/domains/location/india.js +121 -59
- package/dist/domains/location/india.js.map +1 -1
- package/dist/domains/members/members.d.ts +2 -2
- package/dist/domains/membership/membership.d.ts +26 -0
- package/dist/domains/membership/membership.d.ts.map +1 -1
- package/dist/domains/membership/membership.js +55 -1
- package/dist/domains/membership/membership.js.map +1 -1
- package/dist/domains/moderation/moderation.d.ts +2 -2
- package/dist/domains/moderation/moderation.js +3 -3
- package/dist/domains/moderation/moderation.js.map +1 -1
- package/dist/domains/notifications/notifications.d.ts +1 -10
- package/dist/domains/notifications/notifications.d.ts.map +1 -1
- package/dist/domains/notifications/notifications.js +2 -3
- package/dist/domains/notifications/notifications.js.map +1 -1
- package/dist/domains/onboarding/onboarding-answers.d.ts +42 -14
- package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding-answers.js +49 -33
- package/dist/domains/onboarding/onboarding-answers.js.map +1 -1
- package/dist/domains/onboarding/onboarding.d.ts +332 -138
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
- package/dist/domains/onboarding/onboarding.js +47 -59
- package/dist/domains/onboarding/onboarding.js.map +1 -1
- package/dist/domains/products/products.d.ts +43 -8
- package/dist/domains/products/products.d.ts.map +1 -1
- package/dist/domains/products/products.js +57 -11
- package/dist/domains/products/products.js.map +1 -1
- package/dist/domains/profile/founder-profile.d.ts +123 -143
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +77 -35
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/domains/profile/my-profile.d.ts +3 -3
- package/dist/domains/reviews/reviews.d.ts +1 -1
- package/dist/domains/reviews/reviews.d.ts.map +1 -1
- package/dist/domains/reviews/reviews.js +2 -2
- package/dist/domains/reviews/reviews.js.map +1 -1
- package/dist/domains/support/support.d.ts +22 -3
- package/dist/domains/support/support.d.ts.map +1 -1
- package/dist/domains/support/support.js +19 -5
- package/dist/domains/support/support.js.map +1 -1
- package/dist/domains/team/team.d.ts.map +1 -1
- package/dist/domains/team/team.js +3 -6
- package/dist/domains/team/team.js.map +1 -1
- package/dist/domains/uploads/uploads.d.ts +7 -22
- package/dist/domains/uploads/uploads.d.ts.map +1 -1
- package/dist/domains/uploads/uploads.js +7 -21
- package/dist/domains/uploads/uploads.js.map +1 -1
- package/dist/env.d.ts +26 -13
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +58 -23
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +19 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -17
- package/dist/index.js.map +1 -1
- 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.
|
|
33
|
-
not `^0.
|
|
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.
|
|
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
|
|
98
|
-
`
|
|
99
|
-
places pick, omit or reuse
|
|
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,
|
|
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
|
|
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
|
-
|
|
685
|
-
|
|
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
|
|
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
|
|
731
|
-
|
|
732
|
-
`
|
|
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
|
package/dist/access/access.d.ts
CHANGED
|
@@ -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.
|
|
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,
|
|
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"}
|
package/dist/access/access.js
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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
|
|
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":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;
|
|
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
|
|
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
|
-
|
|
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"]}
|