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.
Files changed (84) hide show
  1. package/README.md +98 -0
  2. package/dist/access/access.d.ts +10 -6
  3. package/dist/access/access.d.ts.map +1 -1
  4. package/dist/access/access.js +8 -1
  5. package/dist/access/access.js.map +1 -1
  6. package/dist/access/permissions.d.ts +60 -3
  7. package/dist/access/permissions.d.ts.map +1 -1
  8. package/dist/access/permissions.js +24 -0
  9. package/dist/access/permissions.js.map +1 -1
  10. package/dist/access/roles.d.ts +3 -3
  11. package/dist/access/standing.d.ts +59 -0
  12. package/dist/access/standing.d.ts.map +1 -0
  13. package/dist/access/standing.js +52 -0
  14. package/dist/access/standing.js.map +1 -0
  15. package/dist/core/errors.d.ts +47 -1
  16. package/dist/core/errors.d.ts.map +1 -1
  17. package/dist/core/errors.js +82 -1
  18. package/dist/core/errors.js.map +1 -1
  19. package/dist/core/limits.d.ts +39 -3
  20. package/dist/core/limits.d.ts.map +1 -1
  21. package/dist/core/limits.js +41 -3
  22. package/dist/core/limits.js.map +1 -1
  23. package/dist/core/responses.d.ts +55 -0
  24. package/dist/core/responses.d.ts.map +1 -1
  25. package/dist/core/responses.js +6 -0
  26. package/dist/core/responses.js.map +1 -1
  27. package/dist/core/validation.d.ts +14 -0
  28. package/dist/core/validation.d.ts.map +1 -1
  29. package/dist/core/validation.js +14 -0
  30. package/dist/core/validation.js.map +1 -1
  31. package/dist/domains/audit/audit.d.ts +28 -7
  32. package/dist/domains/audit/audit.d.ts.map +1 -1
  33. package/dist/domains/audit/audit.js +17 -0
  34. package/dist/domains/audit/audit.js.map +1 -1
  35. package/dist/domains/billing/billing.d.ts +1179 -0
  36. package/dist/domains/billing/billing.d.ts.map +1 -0
  37. package/dist/domains/billing/billing.js +393 -0
  38. package/dist/domains/billing/billing.js.map +1 -0
  39. package/dist/domains/billing/gst.d.ts +229 -0
  40. package/dist/domains/billing/gst.d.ts.map +1 -0
  41. package/dist/domains/billing/gst.js +117 -0
  42. package/dist/domains/billing/gst.js.map +1 -0
  43. package/dist/domains/chat/chat.d.ts +85 -0
  44. package/dist/domains/chat/chat.d.ts.map +1 -1
  45. package/dist/domains/chat/chat.js +62 -0
  46. package/dist/domains/chat/chat.js.map +1 -1
  47. package/dist/domains/communities/communities.d.ts +11 -11
  48. package/dist/domains/dashboard/dashboard.d.ts +9 -1
  49. package/dist/domains/dashboard/dashboard.d.ts.map +1 -1
  50. package/dist/domains/dashboard/dashboard.js +15 -0
  51. package/dist/domains/dashboard/dashboard.js.map +1 -1
  52. package/dist/domains/feed/feed.d.ts.map +1 -1
  53. package/dist/domains/feed/feed.js +7 -1
  54. package/dist/domains/feed/feed.js.map +1 -1
  55. package/dist/domains/members/members.d.ts +30 -5
  56. package/dist/domains/members/members.d.ts.map +1 -1
  57. package/dist/domains/members/members.js +23 -0
  58. package/dist/domains/members/members.js.map +1 -1
  59. package/dist/domains/membership/membership.d.ts +239 -0
  60. package/dist/domains/membership/membership.d.ts.map +1 -0
  61. package/dist/domains/membership/membership.js +153 -0
  62. package/dist/domains/membership/membership.js.map +1 -0
  63. package/dist/domains/notifications/notifications.d.ts +43 -1
  64. package/dist/domains/notifications/notifications.d.ts.map +1 -1
  65. package/dist/domains/notifications/notifications.js +19 -0
  66. package/dist/domains/notifications/notifications.js.map +1 -1
  67. package/dist/domains/products/products.d.ts +44 -12
  68. package/dist/domains/products/products.d.ts.map +1 -1
  69. package/dist/domains/products/products.js +33 -3
  70. package/dist/domains/products/products.js.map +1 -1
  71. package/dist/domains/profile/founder-profile.d.ts +178 -0
  72. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  73. package/dist/domains/profile/founder-profile.js +77 -0
  74. package/dist/domains/profile/founder-profile.js.map +1 -1
  75. package/dist/domains/reviews/reviews.d.ts +3 -0
  76. package/dist/domains/reviews/reviews.d.ts.map +1 -1
  77. package/dist/domains/reviews/reviews.js +6 -1
  78. package/dist/domains/reviews/reviews.js.map +1 -1
  79. package/dist/domains/team/team.d.ts +4 -4
  80. package/dist/index.d.ts +7 -4
  81. package/dist/index.d.ts.map +1 -1
  82. package/dist/index.js +9 -4
  83. package/dist/index.js.map +1 -1
  84. 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
@@ -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;AAKxB,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;;;;;;;;;;;;;;;;;;iBAWjC,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,0FAA0F;AAC1F,eAAO,MAAM,oBAAoB;;;;;;;;;iBAQ7B,CAAC;AACL,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAMlE,oDAAoD;AACpD,eAAO,MAAM,iBAAiB;;;iBAEA,CAAC;AAC/B,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAE5D,sEAAsE;AACtE,eAAO,MAAM,oBAAoB;;;;iBAEH,CAAC;AAC/B,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC"}
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,WAAW,IAAI,CAAC;AAC7B,yCAAyC;AACzC,eAAO,MAAM,iBAAiB,IAAI,CAAC;AACnC,2CAA2C;AAC3C,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,eAAO,MAAM,UAAU,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"}
@@ -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
- /** The active role's permissions, packed (`packRules`). */
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;AAErD,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,2DAA2D;IAC3D,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';\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 /** The active role's permissions, packed (`packRules`). */\n rules: packedRulesSchema,\n});\nexport type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;\n\n/** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */\nexport const switchRoleBodySchema = z\n .object({\n role: roleSchema,\n mpin: mpinSchema.optional(),\n })\n .refine((body) => body.role === MEMBER_ROLE || body.mpin !== undefined, {\n path: ['mpin'],\n error: VALIDATION_MESSAGE.mpinRequired,\n });\nexport type SwitchRoleBody = z.infer<typeof switchRoleBodySchema>;\n\nconst confirmed = <T extends { mpin: string; confirmMpin: string }>(body: T) =>\n body.mpin === body.confirmMpin;\nconst mismatch = { path: ['confirmMpin'], error: VALIDATION_MESSAGE.mpinMismatch };\n\n/** `POST /me/mpin`: the first MPIN, typed twice. */\nexport const setMpinBodySchema = z\n .object({ mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type SetMpinBody = z.infer<typeof setMpinBodySchema>;\n\n/** `PUT /me/mpin`: the current MPIN, then the new one typed twice. */\nexport const changeMpinBodySchema = z\n .object({ currentMpin: mpinSchema, mpin: mpinSchema, confirmMpin: mpinSchema })\n .refine(confirmed, mismatch);\nexport type ChangeMpinBody = z.infer<typeof changeMpinBodySchema>;\n"]}
1
+ {"version":3,"file":"access.js","sourceRoot":"","sources":["../../src/access/access.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAC;AAC7B,yCAAyC;AACzC,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AACnC,2CAA2C;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAE3C,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,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
- /** `appRoles` is the staff roles held, so a rule can protect a role ("never suspend a superadmin"). */
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;AAE/C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,aAAa,6HAYhB,CAAC;AACX,eAAO,MAAM,YAAY;;;;;;;;;;;;EAAwB,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,cAAc,0OAkBjB,CAAC;AACX,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;EAAyB,CAAC;AACpD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAEpD;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IAChC,uGAAuG;IACvG,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;KAAE,CAAC;IAClD,cAAc,EAAE;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACnC,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC3B,OAAO,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/C,aAAa,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAC/D,UAAU,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACjC,sFAAsF;IACtF,YAAY,EAAE;QAAE,IAAI,EAAE,YAAY,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACvD,4DAA4D;IAC5D,UAAU,EAAE;QAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IACvC,gBAAgB,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;IACvC;;;;OAIG;IACH,OAAO,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACxE,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7B;;;;OAIG;IACH,SAAS,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;QAAC,YAAY,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IACvE,qFAAqF;IACrF,mBAAmB,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CAC9D;AAED,2FAA2F;AAC3F,MAAM,MAAM,eAAe,GAAG;KAC3B,CAAC,IAAI,MAAM,aAAa,GAAG,aAAa,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC;CAChE,CAAC,MAAM,aAAa,CAAC,CAAC;AAEvB;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG,YAAY,CAAC,CAAC,MAAM,EAAE,OAAO,GAAG,eAAe,CAAC,CAAC,CAAC;AAE3E;;;GAGG;AACH,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG,KAAK,CAAC;AAC7E,KAAK,MAAM,CAAC,CAAC,SAAS,IAAI,IAAI,CAAC,CAAC;AAChC,MAAM,MAAM,qBAAqB,GAAG,MAAM,CAAC,IAAI,CAAC,MAAM,aAAa,EAAE,OAAO,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;AAE/F;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,sCAAgC,CAAC;AAC/D,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,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;AAGxB;;;;;;;;;;;;;;GAcG;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;CACH,CAAC;AACX,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAGlD;;;;;;;;;;;;;;;;;;GAkBG;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;CACb,CAAC;AACX,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAiEpD;;;;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 { PlatformRole } from './roles.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 * 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] 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 */\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] 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 /** `appRoles` is the staff roles held, so a rule can protect a role (\"never suspend a superadmin\"). */\n Member: { _id: string; appRoles: PlatformRole[] };\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}\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":"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"]}
@@ -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"]}