founder-schema 0.10.0 → 0.12.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 +98 -0
- package/dist/access/access.d.ts +10 -6
- package/dist/access/access.d.ts.map +1 -1
- package/dist/access/access.js +8 -1
- package/dist/access/access.js.map +1 -1
- package/dist/access/permissions.d.ts +60 -3
- package/dist/access/permissions.d.ts.map +1 -1
- package/dist/access/permissions.js +24 -0
- package/dist/access/permissions.js.map +1 -1
- package/dist/access/roles.d.ts +3 -3
- package/dist/access/standing.d.ts +59 -0
- package/dist/access/standing.d.ts.map +1 -0
- package/dist/access/standing.js +52 -0
- package/dist/access/standing.js.map +1 -0
- package/dist/core/errors.d.ts +47 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +82 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/limits.d.ts +39 -3
- package/dist/core/limits.d.ts.map +1 -1
- package/dist/core/limits.js +41 -3
- package/dist/core/limits.js.map +1 -1
- package/dist/core/responses.d.ts +55 -0
- package/dist/core/responses.d.ts.map +1 -1
- package/dist/core/responses.js +6 -0
- package/dist/core/responses.js.map +1 -1
- package/dist/core/validation.d.ts +14 -0
- package/dist/core/validation.d.ts.map +1 -1
- package/dist/core/validation.js +14 -0
- package/dist/core/validation.js.map +1 -1
- package/dist/domains/audit/audit.d.ts +28 -7
- package/dist/domains/audit/audit.d.ts.map +1 -1
- package/dist/domains/audit/audit.js +17 -0
- package/dist/domains/audit/audit.js.map +1 -1
- package/dist/domains/billing/billing.d.ts +1179 -0
- package/dist/domains/billing/billing.d.ts.map +1 -0
- package/dist/domains/billing/billing.js +393 -0
- package/dist/domains/billing/billing.js.map +1 -0
- package/dist/domains/billing/gst.d.ts +229 -0
- package/dist/domains/billing/gst.d.ts.map +1 -0
- package/dist/domains/billing/gst.js +117 -0
- package/dist/domains/billing/gst.js.map +1 -0
- package/dist/domains/chat/chat.d.ts +85 -0
- package/dist/domains/chat/chat.d.ts.map +1 -1
- package/dist/domains/chat/chat.js +62 -0
- package/dist/domains/chat/chat.js.map +1 -1
- package/dist/domains/communities/communities.d.ts +11 -11
- package/dist/domains/dashboard/dashboard.d.ts +9 -1
- package/dist/domains/dashboard/dashboard.d.ts.map +1 -1
- package/dist/domains/dashboard/dashboard.js +15 -0
- package/dist/domains/dashboard/dashboard.js.map +1 -1
- package/dist/domains/feed/feed.d.ts.map +1 -1
- package/dist/domains/feed/feed.js +7 -1
- package/dist/domains/feed/feed.js.map +1 -1
- package/dist/domains/members/members.d.ts +30 -5
- package/dist/domains/members/members.d.ts.map +1 -1
- package/dist/domains/members/members.js +23 -0
- package/dist/domains/members/members.js.map +1 -1
- package/dist/domains/membership/membership.d.ts +239 -0
- package/dist/domains/membership/membership.d.ts.map +1 -0
- package/dist/domains/membership/membership.js +153 -0
- package/dist/domains/membership/membership.js.map +1 -0
- package/dist/domains/notifications/notifications.d.ts +43 -1
- package/dist/domains/notifications/notifications.d.ts.map +1 -1
- package/dist/domains/notifications/notifications.js +19 -0
- package/dist/domains/notifications/notifications.js.map +1 -1
- package/dist/domains/products/products.d.ts +44 -12
- package/dist/domains/products/products.d.ts.map +1 -1
- package/dist/domains/products/products.js +33 -3
- package/dist/domains/products/products.js.map +1 -1
- package/dist/domains/profile/founder-profile.d.ts +178 -0
- package/dist/domains/profile/founder-profile.d.ts.map +1 -1
- package/dist/domains/profile/founder-profile.js +77 -0
- package/dist/domains/profile/founder-profile.js.map +1 -1
- package/dist/domains/reviews/reviews.d.ts +3 -0
- package/dist/domains/reviews/reviews.d.ts.map +1 -1
- package/dist/domains/reviews/reviews.js +6 -1
- package/dist/domains/reviews/reviews.js.map +1 -1
- package/dist/domains/team/team.d.ts +4 -4
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,6 +99,104 @@ instance; this package must not bring its own.
|
|
|
99
99
|
|
|
100
100
|
---
|
|
101
101
|
|
|
102
|
+
## Breaking in 0.12.0: membership, plans and payments (2026-10-11)
|
|
103
|
+
|
|
104
|
+
Looking is free; taking part needs a membership (Rs 99 once), and Premium and
|
|
105
|
+
Business add live products and new people to message. Design:
|
|
106
|
+
`founder-circle-backend/docs/membership-and-plans.md`. No export removed.
|
|
107
|
+
|
|
108
|
+
- **Required on answers, so the BACKEND DEPLOYS FIRST.** A 0.12.0 client
|
|
109
|
+
refuses a 0.11.0 server's answer to: `/me/access` (`standing`), a product
|
|
110
|
+
(`viewer.canSetLive`), `/chat/open` (`via`), a staff member's detail
|
|
111
|
+
(`standing`, `membership`, `plan`, `viewer.canGrantMembership`,
|
|
112
|
+
`canRevokeMembership`, `canReadPayments`, `canReadMembership`), the
|
|
113
|
+
Overview (`billing`, with `paymentsNeedingAction`), an order's state
|
|
114
|
+
(`notApplied`), the staff payments list (`needsAction`), a staff payment's
|
|
115
|
+
detail (`refundedOutside`: what dashboard refunds already gave back;
|
|
116
|
+
`strayPayments`: second payments and whether each is refunded) and a
|
|
117
|
+
review's viewer flags (`canRemoveReply`: the team takes its reply back,
|
|
118
|
+
member or not). A comment's `canMarkHelpful` on a MARKED comment now means
|
|
119
|
+
"may take the mark back" (on the team, member or not).
|
|
120
|
+
- **The price list is here:** `MEMBERSHIP_PRICE_PAISE`, `MEMBER_LIMIT_VALUES`,
|
|
121
|
+
`PLAN_SPECS` (each plan's name, prices and limits) and `FOUNDING_PLACES`.
|
|
122
|
+
The backend charges and counts from them and the web's Terms are written
|
|
123
|
+
from them, so a price change is a schema release.
|
|
124
|
+
- **Money taken but not used** is refunded by itself: `orderStateSchema.notApplied`
|
|
125
|
+
says so to the buyer; staff list it with `staffPaymentsQuerySchema.needsAction`
|
|
126
|
+
and see `staffPaymentSummarySchema.needsAction`. `PLAN_CHANGED` (409) is an
|
|
127
|
+
upgrade whose plan moved while paying.
|
|
128
|
+
- **A free coupon needs no billing details:** `createOrderBodySchema.billing` is
|
|
129
|
+
optional; a paid order without it is refused with the validation key
|
|
130
|
+
`billingStateRequired` on `billing.stateCode`.
|
|
131
|
+
- **Why a product is paused:** `productSchema.pausedBy` (`PRODUCT_PAUSED_BY_VALUES`:
|
|
132
|
+
`limit` comes back by itself, `owner` stays down), set with `paused`.
|
|
133
|
+
- **Access:** actions `react`, `vote`, `save`, `join`, `message`, `refund`
|
|
134
|
+
and subjects `Membership`, `Payment`, `Coupon` are appended (never
|
|
135
|
+
reordered: rules are packed by index). `access/standing.ts` adds
|
|
136
|
+
`STANDING_VALUES` (`viewer`, `member`) and `ACCESS_REASON_VALUES`
|
|
137
|
+
(`MEMBERSHIP_REQUIRED`, `PLAN_REQUIRED`, `MESSAGE_NOT_ACCEPTED`): the
|
|
138
|
+
reason a packed rule carries, which the web reads to open the Join sheet or
|
|
139
|
+
the plans instead of drawing a dead control.
|
|
140
|
+
- **Added domains:** `domains/membership` (kinds, tiers, periods, the plan
|
|
141
|
+
catalogue, `meMembershipSchema`), `domains/billing` (quotes, orders,
|
|
142
|
+
checkout, payments, invoices and credit notes, refunds, coupons, the staff
|
|
143
|
+
payment shapes) and `domains/billing/gst` (the 36 GST state codes, the
|
|
144
|
+
GSTIN checksum, `billingDetailsSchema`).
|
|
145
|
+
- **Chat:** `openChatBodySchema.useAllowance` (spend one of this month's new
|
|
146
|
+
people, only when the member agreed), `chatReachSchema` (`GET
|
|
147
|
+
/chat/reach/:userId`), `messagingSettingsSchema` (`/me/messaging`). A staff
|
|
148
|
+
role reaches anyone who can chat: reach `staff`, `openChatResponseSchema.via`
|
|
149
|
+
`staff` (both enums appended).
|
|
150
|
+
- **Products:** card `paused?` (optional), `setProductLiveBodySchema`
|
|
151
|
+
(`PUT /products/:id/live`). `PRODUCTS_OWNED_MAX` is DEPRECATED: the limit is
|
|
152
|
+
per plan now and counts LIVE products (`GET /me/membership`'s `limits`).
|
|
153
|
+
- **Notifications:** kinds `plan_ending`, `plan_ended`, `payment_refunded`
|
|
154
|
+
(from Founder Circle, no actor). **Audit:** `membership.granted`,
|
|
155
|
+
`membership.revoked`, `payment.refunded`, `coupon.created`,
|
|
156
|
+
`coupon.updated`. **Errors:** 23 codes, from `PRODUCT_PAUSED` to
|
|
157
|
+
`MESSAGE_NOT_ACCEPTED`; `apiErrorDetails` gains `limit`, `left`, `resetsAt`.
|
|
158
|
+
- **Limits:** `REFUND_WINDOW_DAYS` 7, `ORDER_TTL_MINUTES` 30,
|
|
159
|
+
`PLAN_AHEAD_MAX_YEARS` 3, `PLAN_REMINDER_DAYS` [7, 1],
|
|
160
|
+
`PRODUCTS_LIVE_SAFETY_MAX` 100, and the coupon and billing text bounds.
|
|
161
|
+
- **Deploy:** backend, then `sync-indexes --apply`, then
|
|
162
|
+
`membership-setup sync-mirror`, `seed-coupon` (and `dev-grant` on dev only),
|
|
163
|
+
then web. The backend design doc's section 16 has the full order.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Changes in 0.11.0: erasure retention, and a member's page (2026-10-11)
|
|
168
|
+
|
|
169
|
+
No export removed. The backend deploys BEFORE the web, as always, and here
|
|
170
|
+
it matters three ways if not: (1) one read gains a REQUIRED field
|
|
171
|
+
(`visibility` on `myPersonalDetailsSchema`), so a 0.11.0 web against a 0.10.0
|
|
172
|
+
server cannot load Me's personal details or their editor; (2) a 0.10.0 server
|
|
173
|
+
has no member page, so every member link answers "not here"; (3) a 0.10.0
|
|
174
|
+
server's `GET /products/by-member/:userId` still lists a member's UNLISTED
|
|
175
|
+
products to everyone, and the 0.11.0 web shows that list on the member page.
|
|
176
|
+
Backend first is safe: a 0.10.0 web ignores `visibility`, `PATCH /profiles/me`
|
|
177
|
+
still takes `location`, and no 0.10.0 web code calls the two new reads.
|
|
178
|
+
|
|
179
|
+
- **Added: `ERASURE_RETENTION_DAYS` (180)**, `core/limits.ts`: how long what a
|
|
180
|
+
deleted account leaves behind, and the text of content its author deleted or
|
|
181
|
+
moderation removed, is kept before the backend's `erasure-purge` job purges
|
|
182
|
+
it (the user's ruling; the IT Rules 2021 floor). The web's Privacy Policy
|
|
183
|
+
states the same number and its test reads this constant.
|
|
184
|
+
- **Added: what a member chooses to show** (the user's ruling): email, phone,
|
|
185
|
+
gender and birthday are private until the member switches each on.
|
|
186
|
+
`CONTACT_VISIBILITY_FIELDS`, `contactVisibilitySchema` (four booleans);
|
|
187
|
+
`myPersonalDetailsSchema.visibility` (the switches, as stored);
|
|
188
|
+
`updateMyPersonalDetailsBodySchema.visibility` (a PARTIAL: only the
|
|
189
|
+
switches that moved) and `.location` (the city moved to the personal card;
|
|
190
|
+
`null` removes it). `PATCH /profiles/me` still takes `location` too.
|
|
191
|
+
- **Added: a member's page**, `GET /founders/by-username/:username`:
|
|
192
|
+
`memberHandleParamsSchema` and `memberProfileSchema`, the public profile
|
|
193
|
+
plus `shown` (only what the member switched on, decided by the server for
|
|
194
|
+
this viewer: email and phone only to signed-in members) and `viewer`
|
|
195
|
+
(`isSelf`, `canMessage`, the server's answers). A shown birthday is
|
|
196
|
+
`publicBirthdaySchema`, a day and a month: the year never leaves the
|
|
197
|
+
server. `memberProfileSchema` is in the "never public at top level" test:
|
|
198
|
+
nothing private sits outside `shown`.
|
|
199
|
+
|
|
102
200
|
## Changes in 0.10.0: the Me hub (2026-10-11)
|
|
103
201
|
|
|
104
202
|
Me becomes a hub: the member's own details, their public profile, and a
|
package/dist/access/access.d.ts
CHANGED
|
@@ -30,32 +30,36 @@ export declare const mpinSchema: z.ZodString;
|
|
|
30
30
|
/** The answer to `GET /me/access`, and to every switch. */
|
|
31
31
|
export declare const meAccessResponseSchema: z.ZodObject<{
|
|
32
32
|
roles: z.ZodArray<z.ZodEnum<{
|
|
33
|
+
moderator: "moderator";
|
|
33
34
|
superadmin: "superadmin";
|
|
35
|
+
member: "member";
|
|
34
36
|
manager: "manager";
|
|
35
|
-
moderator: "moderator";
|
|
36
37
|
support_agent: "support_agent";
|
|
37
|
-
member: "member";
|
|
38
38
|
}>>;
|
|
39
39
|
activeRole: z.ZodEnum<{
|
|
40
|
+
moderator: "moderator";
|
|
40
41
|
superadmin: "superadmin";
|
|
42
|
+
member: "member";
|
|
41
43
|
manager: "manager";
|
|
42
|
-
moderator: "moderator";
|
|
43
44
|
support_agent: "support_agent";
|
|
44
|
-
member: "member";
|
|
45
45
|
}>;
|
|
46
46
|
activeRoleExpiresAt: z.ZodNullable<z.ZodISODateTime>;
|
|
47
47
|
mpinSet: z.ZodBoolean;
|
|
48
|
+
standing: z.ZodEnum<{
|
|
49
|
+
viewer: "viewer";
|
|
50
|
+
member: "member";
|
|
51
|
+
}>;
|
|
48
52
|
rules: z.ZodArray<z.ZodArray<z.ZodUnknown>>;
|
|
49
53
|
}, z.core.$strip>;
|
|
50
54
|
export type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;
|
|
51
55
|
/** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */
|
|
52
56
|
export declare const switchRoleBodySchema: z.ZodObject<{
|
|
53
57
|
role: z.ZodEnum<{
|
|
58
|
+
moderator: "moderator";
|
|
54
59
|
superadmin: "superadmin";
|
|
60
|
+
member: "member";
|
|
55
61
|
manager: "manager";
|
|
56
|
-
moderator: "moderator";
|
|
57
62
|
support_agent: "support_agent";
|
|
58
|
-
member: "member";
|
|
59
63
|
}>;
|
|
60
64
|
mpin: z.ZodOptional<z.ZodString>;
|
|
61
65
|
}, z.core.$strip>;
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,2CAA2C;AAC3C,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,eAAO,MAAM,UAAU,aAA8D,CAAC;AAEtF,2DAA2D;AAC3D,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;iBAiBjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,0FAA0F;AAC1F,eAAO,MAAM,oBAAoB;;;;;;;;;iBAQ7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,oDAAoD;AACpD,eAAO,MAAM,iBAAiB;;;iBAEA,CAAC;AAC/B,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB;;;;iBAEH,CAAC;AAC/B,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
|
package/dist/access/access.js
CHANGED
|
@@ -24,6 +24,7 @@ import { z } from 'zod';
|
|
|
24
24
|
import { VALIDATION_MESSAGE } from '../core/validation.js';
|
|
25
25
|
import { packedRulesSchema } from './permissions.js';
|
|
26
26
|
import { MEMBER_ROLE, roleSchema } from './roles.js';
|
|
27
|
+
import { standingSchema } from './standing.js';
|
|
27
28
|
export const MPIN_LENGTH = 6;
|
|
28
29
|
/** Wrong tries before the MPIN locks. */
|
|
29
30
|
export const MPIN_MAX_ATTEMPTS = 5;
|
|
@@ -40,7 +41,13 @@ export const meAccessResponseSchema = z.object({
|
|
|
40
41
|
activeRoleExpiresAt: z.iso.datetime().nullable(),
|
|
41
42
|
/** Whether an MPIN is set, so the switcher knows to ask for one or to offer setting one. */
|
|
42
43
|
mpinSet: z.boolean(),
|
|
43
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Whether they take part or only look (0.12.0, `access/standing`). The
|
|
46
|
+
* rules already say what follows from it; this says it plainly, for the
|
|
47
|
+
* places that word it ("Become a member to take part").
|
|
48
|
+
*/
|
|
49
|
+
standing: standingSchema,
|
|
50
|
+
/** The active role's permissions, packed (`packRules`), refusals with their reasons. */
|
|
44
51
|
rules: packedRulesSchema,
|
|
45
52
|
});
|
|
46
53
|
/** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,yCAAyC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC,SAAS,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;AAEtF,2DAA2D;AAC3D,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,gEAAgE;IAChE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACjC,6EAA6E;IAC7E,UAAU,EAAE,UAAU;IACtB,uFAAuF;IACvF,mBAAmB,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IAChD,4FAA4F;IAC5F,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;IACpB;;;;OAIG;IACH,QAAQ,EAAE,cAAc;IACxB,wFAAwF;IACxF,KAAK,EAAE,iBAAiB;CACzB,CAAC,CAAC;AAGH,0FAA0F;AAC1F,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC;IACN,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,UAAU,CAAC,QAAQ,EAAE;CAC5B,CAAC;KACD,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,WAAW,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,EAAE;IACtE,IAAI,EAAE,CAAC,MAAM,CAAC;IACd,KAAK,EAAE,kBAAkB,CAAC,YAAY;CACvC,CAAC,CAAC;AAGL,MAAM,SAAS,GAAG,CAAkD,IAAO,EAAE,EAAE,CAC7E,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,WAAW,CAAC;AACjC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,kBAAkB,CAAC,YAAY,EAAE,CAAC;AAEnF,oDAAoD;AACpD,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC;KAC/B,MAAM,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;KACrD,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAG/B,sEAAsE;AACtE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC;KAClC,MAAM,CAAC,EAAE,WAAW,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;KAC9E,MAAM,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the access API\n *\n * GET /api/v1/me/access who I am acting as, what I hold, my rules\n * PUT /api/v1/me/active-role switch role (a staff role needs the MPIN)\n * POST /api/v1/me/mpin set the MPIN, once\n * PUT /api/v1/me/mpin change it, with the current one\n *\n * THE MPIN is a 6-digit code that stands between a signed-in member and their\n * staff roles. Signing in proves who you are; the MPIN proves it is still you\n * at the moment you pick up admin powers, on a phone that may have been left\n * unlocked. Set once by its owner; a forgotten one is cleared by a superadmin\n * (an audited action), after which the owner sets a new one. `MPIN_MAX_ATTEMPTS`\n * wrong tries lock it for `MPIN_LOCK_MS`.\n *\n * THE ACTIVE ROLE is per account (the user's ruling, 2026-10-05): switching on\n * one device switches every device. It lasts `ACTIVE_ROLE_TTL_MS`, and the\n * server re-checks it on EVERY request: a role that was revoked, or that\n * expired, is acting as a member again at once, not at the next sign-in.\n *\n * @module founder-schema/access/access\n */\n\nimport { z } from 'zod';\nimport { VALIDATION_MESSAGE } from '../core/validation.js';\nimport { packedRulesSchema } from './permissions.js';\nimport { MEMBER_ROLE, roleSchema } from './roles.js';\nimport { standingSchema } from './standing.js';\n\nexport const MPIN_LENGTH = 6;\n/** Wrong tries before the MPIN locks. */\nexport const MPIN_MAX_ATTEMPTS = 5;\n/** How long a locked MPIN stays locked. */\nexport const MPIN_LOCK_MS = 15 * 60 * 1000;\n\nexport const mpinSchema = z.string().regex(/^\\d{6}$/, VALIDATION_MESSAGE.mpinInvalid);\n\n/** The answer to `GET /me/access`, and to every switch. */\nexport const meAccessResponseSchema = z.object({\n /** Every role held, member always among them, highest first. */\n roles: z.array(roleSchema).min(1),\n /** The role being acted as now. Member when no staff role is switched on. */\n activeRole: roleSchema,\n /** When the active staff role lapses back to member. Null while acting as a member. */\n activeRoleExpiresAt: z.iso.datetime().nullable(),\n /** Whether an MPIN is set, so the switcher knows to ask for one or to offer setting one. */\n mpinSet: z.boolean(),\n /**\n * Whether they take part or only look (0.12.0, `access/standing`). The\n * rules already say what follows from it; this says it plainly, for the\n * places that word it (\"Become a member to take part\").\n */\n standing: standingSchema,\n /** The active role's permissions, packed (`packRules`), refusals with their reasons. */\n rules: packedRulesSchema,\n});\nexport type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;\n\n/** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */\nexport const switchRoleBodySchema = z\n .object({\n role: roleSchema,\n mpin: mpinSchema.optional(),\n })\n .refine((body) => body.role === MEMBER_ROLE || body.mpin !== undefined, {\n path: ['mpin'],\n error: VALIDATION_MESSAGE.mpinRequired,\n });\nexport type SwitchRoleBody = z.infer<typeof switchRoleBodySchema>;\n\nconst confirmed = <T extends { mpin: string; confirmMpin: string }>(body: T) =>\n body.mpin === body.confirmMpin;\nconst mismatch = { path: ['confirmMpin'], error: VALIDATION_MESSAGE.mpinMismatch };\n\n/** `POST /me/mpin`: the first MPIN, typed twice. */\nexport const setMpinBodySchema = z\n .object({ mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type SetMpinBody = z.infer<typeof setMpinBodySchema>;\n\n/** `PUT /me/mpin`: the current MPIN, then the new one typed twice. */\nexport const changeMpinBodySchema = z\n .object({ currentMpin: mpinSchema, mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type ChangeMpinBody = z.infer<typeof changeMpinBodySchema>;\n"]}
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import type { ForcedSubject, MongoAbility } from '@casl/ability';
|
|
19
19
|
import { z } from 'zod';
|
|
20
|
+
import type { PurchaseKind } from '../domains/billing/billing.js';
|
|
20
21
|
import type { PlatformRole } from './roles.js';
|
|
22
|
+
import type { Standing } from './standing.js';
|
|
21
23
|
/**
|
|
22
24
|
* The verbs.
|
|
23
25
|
*
|
|
@@ -31,10 +33,24 @@ import type { PlatformRole } from './roles.js';
|
|
|
31
33
|
* represent act for a product as one of its team: tag a post with it, reply
|
|
32
34
|
* to its reviews, mark feedback helpful, confirm a tester
|
|
33
35
|
*
|
|
36
|
+
* Taking part (0.12.0, membership): the verbs a viewer is refused and a member
|
|
37
|
+
* holds. Creating content stays `create`; these are the rest.
|
|
38
|
+
*
|
|
39
|
+
* react a reaction on a post, poll or comment; an upvote on a product;
|
|
40
|
+
* marking a review helpful
|
|
41
|
+
* vote vote in a poll
|
|
42
|
+
* save bookmark a post or poll
|
|
43
|
+
* join join a community, ask to, or accept its invitation; accept an
|
|
44
|
+
* invitation to a product's team
|
|
45
|
+
* message start a conversation with a Member
|
|
46
|
+
* refund give a payment's money back (staff)
|
|
47
|
+
*
|
|
34
48
|
* Append only: a stored or packed rule names these.
|
|
35
49
|
*/
|
|
36
|
-
export declare const ACTION_VALUES: readonly ["manage", "create", "read", "update", "delete", "moderate", "assign", "suspend", "resolve", "reset", "represent"];
|
|
50
|
+
export declare const ACTION_VALUES: readonly ["manage", "create", "read", "update", "delete", "moderate", "assign", "suspend", "resolve", "reset", "represent", "react", "vote", "save", "join", "message", "refund"];
|
|
37
51
|
export declare const actionSchema: z.ZodEnum<{
|
|
52
|
+
join: "join";
|
|
53
|
+
message: "message";
|
|
38
54
|
manage: "manage";
|
|
39
55
|
create: "create";
|
|
40
56
|
read: "read";
|
|
@@ -46,6 +62,10 @@ export declare const actionSchema: z.ZodEnum<{
|
|
|
46
62
|
resolve: "resolve";
|
|
47
63
|
reset: "reset";
|
|
48
64
|
represent: "represent";
|
|
65
|
+
react: "react";
|
|
66
|
+
vote: "vote";
|
|
67
|
+
save: "save";
|
|
68
|
+
refund: "refund";
|
|
49
69
|
}>;
|
|
50
70
|
export type Action = z.infer<typeof actionSchema>;
|
|
51
71
|
/**
|
|
@@ -66,8 +86,11 @@ export type Action = z.infer<typeof actionSchema>;
|
|
|
66
86
|
* Review a member's stars and words on a product
|
|
67
87
|
* Community an official community (0.9.0)
|
|
68
88
|
* CommunitySuggestion a member's suggestion for a new community (0.9.0)
|
|
89
|
+
* Membership a person's platform membership: granted, revoked (0.12.0)
|
|
90
|
+
* Payment a purchase and its invoice, and its refund (0.12.0)
|
|
91
|
+
* Coupon a code that gives a membership or a discount (0.12.0)
|
|
69
92
|
*/
|
|
70
|
-
export declare const SUBJECT_VALUES: readonly ["all", "Dashboard", "Member", "FounderProfile", "Post", "Poll", "Comment", "Report", "SupportTicket", "SavedReply", "PlatformRole", "AuditEntry", "PlatformSettings", "Product", "Review", "Community", "CommunitySuggestion"];
|
|
93
|
+
export declare const SUBJECT_VALUES: readonly ["all", "Dashboard", "Member", "FounderProfile", "Post", "Poll", "Comment", "Report", "SupportTicket", "SavedReply", "PlatformRole", "AuditEntry", "PlatformSettings", "Product", "Review", "Community", "CommunitySuggestion", "Membership", "Payment", "Coupon"];
|
|
71
94
|
export declare const subjectSchema: z.ZodEnum<{
|
|
72
95
|
all: "all";
|
|
73
96
|
Dashboard: "Dashboard";
|
|
@@ -86,6 +109,9 @@ export declare const subjectSchema: z.ZodEnum<{
|
|
|
86
109
|
Review: "Review";
|
|
87
110
|
Community: "Community";
|
|
88
111
|
CommunitySuggestion: "CommunitySuggestion";
|
|
112
|
+
Membership: "Membership";
|
|
113
|
+
Payment: "Payment";
|
|
114
|
+
Coupon: "Coupon";
|
|
89
115
|
}>;
|
|
90
116
|
export type Subject = z.infer<typeof subjectSchema>;
|
|
91
117
|
/**
|
|
@@ -100,10 +126,25 @@ export type Subject = z.infer<typeof subjectSchema>;
|
|
|
100
126
|
*/
|
|
101
127
|
export interface SubjectFields {
|
|
102
128
|
Dashboard: Record<never, never>;
|
|
103
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* `appRoles` is the staff roles held, so a rule can protect a role ("never
|
|
131
|
+
* suspend a superadmin").
|
|
132
|
+
*
|
|
133
|
+
* For `message` (0.12.0), what the asker's relation to them is:
|
|
134
|
+
* `contact` says the asker knows them (a teammate, a private community in
|
|
135
|
+
* common, a conversation already); `standing` is theirs, so nobody starts a
|
|
136
|
+
* conversation with a viewer, who cannot chat; `acceptsStrangers` is their
|
|
137
|
+
* own setting (Premium and Business members may start one, or only people
|
|
138
|
+
* they know). Optional because only the messaging check computes them: a
|
|
139
|
+
* rule on a field that is absent never matches, so a check that forgets them
|
|
140
|
+
* is refused, never let through.
|
|
141
|
+
*/
|
|
104
142
|
Member: {
|
|
105
143
|
_id: string;
|
|
106
144
|
appRoles: PlatformRole[];
|
|
145
|
+
contact?: boolean;
|
|
146
|
+
standing?: Standing;
|
|
147
|
+
acceptsStrangers?: boolean;
|
|
107
148
|
};
|
|
108
149
|
FounderProfile: {
|
|
109
150
|
userId: string;
|
|
@@ -166,6 +207,22 @@ export interface SubjectFields {
|
|
|
166
207
|
suggestedBy: string;
|
|
167
208
|
status: string;
|
|
168
209
|
};
|
|
210
|
+
/** Whose membership (0.12.0), so "never your own" is a rule. */
|
|
211
|
+
Membership: {
|
|
212
|
+
userId: string;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Who pays (0.12.0), what for, and where it stands: a viewer may buy only a
|
|
216
|
+
* membership (a plan needs one), only a paid one is refunded, and never by
|
|
217
|
+
* its own payer.
|
|
218
|
+
*/
|
|
219
|
+
Payment: {
|
|
220
|
+
userId: string;
|
|
221
|
+
status: string;
|
|
222
|
+
kind: PurchaseKind;
|
|
223
|
+
};
|
|
224
|
+
/** Coupons are the platform's (0.12.0): no condition is ever written on one. */
|
|
225
|
+
Coupon: Record<never, never>;
|
|
169
226
|
}
|
|
170
227
|
/** A document tagged with its subject type, as CASL's `subject('Post', doc)` makes one. */
|
|
171
228
|
export type SubjectInstance = {
|
|
@@ -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;AACjE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"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"}
|
|
@@ -29,6 +29,18 @@ import { z } from 'zod';
|
|
|
29
29
|
* represent act for a product as one of its team: tag a post with it, reply
|
|
30
30
|
* to its reviews, mark feedback helpful, confirm a tester
|
|
31
31
|
*
|
|
32
|
+
* Taking part (0.12.0, membership): the verbs a viewer is refused and a member
|
|
33
|
+
* holds. Creating content stays `create`; these are the rest.
|
|
34
|
+
*
|
|
35
|
+
* react a reaction on a post, poll or comment; an upvote on a product;
|
|
36
|
+
* marking a review helpful
|
|
37
|
+
* vote vote in a poll
|
|
38
|
+
* save bookmark a post or poll
|
|
39
|
+
* join join a community, ask to, or accept its invitation; accept an
|
|
40
|
+
* invitation to a product's team
|
|
41
|
+
* message start a conversation with a Member
|
|
42
|
+
* refund give a payment's money back (staff)
|
|
43
|
+
*
|
|
32
44
|
* Append only: a stored or packed rule names these.
|
|
33
45
|
*/
|
|
34
46
|
export const ACTION_VALUES = [
|
|
@@ -43,6 +55,12 @@ export const ACTION_VALUES = [
|
|
|
43
55
|
'resolve',
|
|
44
56
|
'reset',
|
|
45
57
|
'represent',
|
|
58
|
+
'react',
|
|
59
|
+
'vote',
|
|
60
|
+
'save',
|
|
61
|
+
'join',
|
|
62
|
+
'message',
|
|
63
|
+
'refund',
|
|
46
64
|
];
|
|
47
65
|
export const actionSchema = z.enum(ACTION_VALUES);
|
|
48
66
|
/**
|
|
@@ -63,6 +81,9 @@ export const actionSchema = z.enum(ACTION_VALUES);
|
|
|
63
81
|
* Review a member's stars and words on a product
|
|
64
82
|
* Community an official community (0.9.0)
|
|
65
83
|
* CommunitySuggestion a member's suggestion for a new community (0.9.0)
|
|
84
|
+
* Membership a person's platform membership: granted, revoked (0.12.0)
|
|
85
|
+
* Payment a purchase and its invoice, and its refund (0.12.0)
|
|
86
|
+
* Coupon a code that gives a membership or a discount (0.12.0)
|
|
66
87
|
*/
|
|
67
88
|
export const SUBJECT_VALUES = [
|
|
68
89
|
'all',
|
|
@@ -82,6 +103,9 @@ export const SUBJECT_VALUES = [
|
|
|
82
103
|
'Review',
|
|
83
104
|
'Community',
|
|
84
105
|
'CommunitySuggestion',
|
|
106
|
+
'Membership',
|
|
107
|
+
'Payment',
|
|
108
|
+
'Coupon',
|
|
85
109
|
];
|
|
86
110
|
export const subjectSchema = z.enum(SUBJECT_VALUES);
|
|
87
111
|
/**
|
|
@@ -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;
|
|
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"]}
|
package/dist/access/roles.d.ts
CHANGED
|
@@ -25,9 +25,9 @@ import { z } from 'zod';
|
|
|
25
25
|
/** The staff roles, highest first. */
|
|
26
26
|
export declare const PLATFORM_ROLE_VALUES: readonly ["superadmin", "manager", "moderator", "support_agent"];
|
|
27
27
|
export declare const platformRoleSchema: z.ZodEnum<{
|
|
28
|
+
moderator: "moderator";
|
|
28
29
|
superadmin: "superadmin";
|
|
29
30
|
manager: "manager";
|
|
30
|
-
moderator: "moderator";
|
|
31
31
|
support_agent: "support_agent";
|
|
32
32
|
}>;
|
|
33
33
|
export type PlatformRole = z.infer<typeof platformRoleSchema>;
|
|
@@ -40,11 +40,11 @@ export declare const MEMBER_ROLE: "member";
|
|
|
40
40
|
*/
|
|
41
41
|
export declare const ROLE_VALUES: readonly ["superadmin", "manager", "moderator", "support_agent", "member"];
|
|
42
42
|
export declare const roleSchema: z.ZodEnum<{
|
|
43
|
+
moderator: "moderator";
|
|
43
44
|
superadmin: "superadmin";
|
|
45
|
+
member: "member";
|
|
44
46
|
manager: "manager";
|
|
45
|
-
moderator: "moderator";
|
|
46
47
|
support_agent: "support_agent";
|
|
47
|
-
member: "member";
|
|
48
48
|
}>;
|
|
49
49
|
export type Role = z.infer<typeof roleSchema>;
|
|
50
50
|
export declare function isPlatformRole(value: unknown): value is PlatformRole;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: standing, whether someone takes part or only looks (0.12.0)
|
|
3
|
+
*
|
|
4
|
+
* The user's ruling (2026-10-11): LOOKING IS FREE, TAKING PART IS PAID. Every
|
|
5
|
+
* signed-in person has a standing:
|
|
6
|
+
*
|
|
7
|
+
* viewer signed in and onboarded, no membership: reads everything a member
|
|
8
|
+
* reads, reports, asks support, manages their own account; takes
|
|
9
|
+
* part in nothing
|
|
10
|
+
* member holds a platform membership (Rs 99 once, a founding coupon, a
|
|
11
|
+
* grant) or any staff role: takes part
|
|
12
|
+
*
|
|
13
|
+
* The server builds it into the CASL ability on every request, from the
|
|
14
|
+
* member's record; the web learns it from `GET /me/access` and from the rules
|
|
15
|
+
* themselves. A refusal for want of it is not a bare "forbidden": the rule
|
|
16
|
+
* carries a REASON (`ACCESS_REASON_VALUES`), packed with the rules, so the web
|
|
17
|
+
* can open the Join sheet or the Upgrade tab instead of drawing a dead button,
|
|
18
|
+
* and the server answers that same reason as the error code.
|
|
19
|
+
*
|
|
20
|
+
* @module founder-schema/access/standing
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
import type { ApiErrorCode } from '../core/errors.js';
|
|
24
|
+
/** ⚠ Read back from packed rules (a condition on `Member.standing`): append only. */
|
|
25
|
+
export declare const STANDING_VALUES: readonly ["viewer", "member"];
|
|
26
|
+
export declare const standingSchema: z.ZodEnum<{
|
|
27
|
+
viewer: "viewer";
|
|
28
|
+
member: "member";
|
|
29
|
+
}>;
|
|
30
|
+
export type Standing = z.infer<typeof standingSchema>;
|
|
31
|
+
/**
|
|
32
|
+
* Why a rule refuses, as CASL's `because()` carries it. Each is also an API
|
|
33
|
+
* error code (checked below), so the server answers the reason as the code
|
|
34
|
+
* and the web maps both to the same sheet.
|
|
35
|
+
*
|
|
36
|
+
* MEMBERSHIP_REQUIRED a viewer taking part: offer the membership
|
|
37
|
+
* PLAN_REQUIRED a member without a plan starting a conversation with
|
|
38
|
+
* someone they do not know: offer Premium or Business
|
|
39
|
+
* MESSAGE_NOT_ACCEPTED starting a conversation with someone who cannot chat
|
|
40
|
+
* (a viewer): nothing to offer
|
|
41
|
+
*
|
|
42
|
+
* ⚠ When several refusals match, CASL reports the LAST one written. The
|
|
43
|
+
* server writes the viewer's refusals after everything else, so a viewer is
|
|
44
|
+
* always told about the membership first.
|
|
45
|
+
*/
|
|
46
|
+
export declare const ACCESS_REASON_VALUES: readonly ["MEMBERSHIP_REQUIRED", "PLAN_REQUIRED", "MESSAGE_NOT_ACCEPTED"];
|
|
47
|
+
export declare const accessReasonSchema: z.ZodEnum<{
|
|
48
|
+
MEMBERSHIP_REQUIRED: "MEMBERSHIP_REQUIRED";
|
|
49
|
+
PLAN_REQUIRED: "PLAN_REQUIRED";
|
|
50
|
+
MESSAGE_NOT_ACCEPTED: "MESSAGE_NOT_ACCEPTED";
|
|
51
|
+
}>;
|
|
52
|
+
export type AccessReason = z.infer<typeof accessReasonSchema>;
|
|
53
|
+
/** Compile-time proof that every reason is an error code the API answers. */
|
|
54
|
+
type Assert<T extends true> = T;
|
|
55
|
+
export type EveryReasonIsAnErrorCode = Assert<AccessReason extends ApiErrorCode ? true : false>;
|
|
56
|
+
/** A reason off a rule (`relevantRuleFor(...)?.reason`), if it is one of ours. */
|
|
57
|
+
export declare function accessReasonOf(reason: unknown): AccessReason | null;
|
|
58
|
+
export {};
|
|
59
|
+
//# sourceMappingURL=standing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standing.d.ts","sourceRoot":"","sources":["../../src/access/standing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,qFAAqF;AACrF,eAAO,MAAM,eAAe,+BAAgC,CAAC;AAC7D,eAAO,MAAM,cAAc;;;EAA0B,CAAC;AACtD,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,oBAAoB,2EAIvB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;EAA+B,CAAC;AAC/D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,6EAA6E;AAC7E,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAChC,MAAM,MAAM,wBAAwB,GAAG,MAAM,CAAC,YAAY,SAAS,YAAY,GAAG,IAAI,GAAG,KAAK,CAAC,CAAC;AAEhG,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,GAAG,YAAY,GAAG,IAAI,CAGnE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: standing, whether someone takes part or only looks (0.12.0)
|
|
3
|
+
*
|
|
4
|
+
* The user's ruling (2026-10-11): LOOKING IS FREE, TAKING PART IS PAID. Every
|
|
5
|
+
* signed-in person has a standing:
|
|
6
|
+
*
|
|
7
|
+
* viewer signed in and onboarded, no membership: reads everything a member
|
|
8
|
+
* reads, reports, asks support, manages their own account; takes
|
|
9
|
+
* part in nothing
|
|
10
|
+
* member holds a platform membership (Rs 99 once, a founding coupon, a
|
|
11
|
+
* grant) or any staff role: takes part
|
|
12
|
+
*
|
|
13
|
+
* The server builds it into the CASL ability on every request, from the
|
|
14
|
+
* member's record; the web learns it from `GET /me/access` and from the rules
|
|
15
|
+
* themselves. A refusal for want of it is not a bare "forbidden": the rule
|
|
16
|
+
* carries a REASON (`ACCESS_REASON_VALUES`), packed with the rules, so the web
|
|
17
|
+
* can open the Join sheet or the Upgrade tab instead of drawing a dead button,
|
|
18
|
+
* and the server answers that same reason as the error code.
|
|
19
|
+
*
|
|
20
|
+
* @module founder-schema/access/standing
|
|
21
|
+
*/
|
|
22
|
+
import { z } from 'zod';
|
|
23
|
+
/** ⚠ Read back from packed rules (a condition on `Member.standing`): append only. */
|
|
24
|
+
export const STANDING_VALUES = ['viewer', 'member'];
|
|
25
|
+
export const standingSchema = z.enum(STANDING_VALUES);
|
|
26
|
+
/**
|
|
27
|
+
* Why a rule refuses, as CASL's `because()` carries it. Each is also an API
|
|
28
|
+
* error code (checked below), so the server answers the reason as the code
|
|
29
|
+
* and the web maps both to the same sheet.
|
|
30
|
+
*
|
|
31
|
+
* MEMBERSHIP_REQUIRED a viewer taking part: offer the membership
|
|
32
|
+
* PLAN_REQUIRED a member without a plan starting a conversation with
|
|
33
|
+
* someone they do not know: offer Premium or Business
|
|
34
|
+
* MESSAGE_NOT_ACCEPTED starting a conversation with someone who cannot chat
|
|
35
|
+
* (a viewer): nothing to offer
|
|
36
|
+
*
|
|
37
|
+
* ⚠ When several refusals match, CASL reports the LAST one written. The
|
|
38
|
+
* server writes the viewer's refusals after everything else, so a viewer is
|
|
39
|
+
* always told about the membership first.
|
|
40
|
+
*/
|
|
41
|
+
export const ACCESS_REASON_VALUES = [
|
|
42
|
+
'MEMBERSHIP_REQUIRED',
|
|
43
|
+
'PLAN_REQUIRED',
|
|
44
|
+
'MESSAGE_NOT_ACCEPTED',
|
|
45
|
+
];
|
|
46
|
+
export const accessReasonSchema = z.enum(ACCESS_REASON_VALUES);
|
|
47
|
+
/** A reason off a rule (`relevantRuleFor(...)?.reason`), if it is one of ours. */
|
|
48
|
+
export function accessReasonOf(reason) {
|
|
49
|
+
const parsed = accessReasonSchema.safeParse(reason);
|
|
50
|
+
return parsed.success ? parsed.data : null;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=standing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standing.js","sourceRoot":"","sources":["../../src/access/standing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,qFAAqF;AACrF,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAU,CAAC;AAC7D,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;AAGtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,qBAAqB;IACrB,eAAe;IACf,sBAAsB;CACd,CAAC;AACX,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAO/D,kFAAkF;AAClF,MAAM,UAAU,cAAc,CAAC,MAAe;IAC5C,MAAM,MAAM,GAAG,kBAAkB,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;IACpD,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;AAC7C,CAAC","sourcesContent":["/**\n * founder-schema: standing, whether someone takes part or only looks (0.12.0)\n *\n * The user's ruling (2026-10-11): LOOKING IS FREE, TAKING PART IS PAID. Every\n * signed-in person has a standing:\n *\n * viewer signed in and onboarded, no membership: reads everything a member\n * reads, reports, asks support, manages their own account; takes\n * part in nothing\n * member holds a platform membership (Rs 99 once, a founding coupon, a\n * grant) or any staff role: takes part\n *\n * The server builds it into the CASL ability on every request, from the\n * member's record; the web learns it from `GET /me/access` and from the rules\n * themselves. A refusal for want of it is not a bare \"forbidden\": the rule\n * carries a REASON (`ACCESS_REASON_VALUES`), packed with the rules, so the web\n * can open the Join sheet or the Upgrade tab instead of drawing a dead button,\n * and the server answers that same reason as the error code.\n *\n * @module founder-schema/access/standing\n */\n\nimport { z } from 'zod';\nimport type { ApiErrorCode } from '../core/errors.js';\n\n/** ⚠ Read back from packed rules (a condition on `Member.standing`): append only. */\nexport const STANDING_VALUES = ['viewer', 'member'] as const;\nexport const standingSchema = z.enum(STANDING_VALUES);\nexport type Standing = z.infer<typeof standingSchema>;\n\n/**\n * Why a rule refuses, as CASL's `because()` carries it. Each is also an API\n * error code (checked below), so the server answers the reason as the code\n * and the web maps both to the same sheet.\n *\n * MEMBERSHIP_REQUIRED a viewer taking part: offer the membership\n * PLAN_REQUIRED a member without a plan starting a conversation with\n * someone they do not know: offer Premium or Business\n * MESSAGE_NOT_ACCEPTED starting a conversation with someone who cannot chat\n * (a viewer): nothing to offer\n *\n * ⚠ When several refusals match, CASL reports the LAST one written. The\n * server writes the viewer's refusals after everything else, so a viewer is\n * always told about the membership first.\n */\nexport const ACCESS_REASON_VALUES = [\n 'MEMBERSHIP_REQUIRED',\n 'PLAN_REQUIRED',\n 'MESSAGE_NOT_ACCEPTED',\n] as const;\nexport const accessReasonSchema = z.enum(ACCESS_REASON_VALUES);\nexport type AccessReason = z.infer<typeof accessReasonSchema>;\n\n/** Compile-time proof that every reason is an error code the API answers. */\ntype Assert<T extends true> = T;\nexport type EveryReasonIsAnErrorCode = Assert<AccessReason extends ApiErrorCode ? true : false>;\n\n/** A reason off a rule (`relevantRuleFor(...)?.reason`), if it is one of ours. */\nexport function accessReasonOf(reason: unknown): AccessReason | null {\n const parsed = accessReasonSchema.safeParse(reason);\n return parsed.success ? parsed.data : null;\n}\n"]}
|