founder-schema 0.3.0 → 0.5.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 (106) hide show
  1. package/README.md +44 -16
  2. package/dist/access/access.d.ts +76 -0
  3. package/dist/access/access.d.ts.map +1 -0
  4. package/dist/access/access.js +66 -0
  5. package/dist/access/access.js.map +1 -0
  6. package/dist/access/permissions.d.ts +153 -0
  7. package/dist/access/permissions.d.ts.map +1 -0
  8. package/dist/access/permissions.js +80 -0
  9. package/dist/access/permissions.js.map +1 -0
  10. package/dist/access/roles.d.ts +71 -0
  11. package/dist/access/roles.d.ts.map +1 -0
  12. package/dist/access/roles.js +72 -0
  13. package/dist/access/roles.js.map +1 -0
  14. package/dist/chat.d.ts +4 -4
  15. package/dist/chat.d.ts.map +1 -1
  16. package/dist/chat.js +7 -2
  17. package/dist/chat.js.map +1 -1
  18. package/dist/core/common.d.ts +304 -0
  19. package/dist/core/common.d.ts.map +1 -0
  20. package/dist/core/common.js +340 -0
  21. package/dist/core/common.js.map +1 -0
  22. package/dist/core/errors.d.ts +117 -0
  23. package/dist/core/errors.d.ts.map +1 -0
  24. package/dist/core/errors.js +139 -0
  25. package/dist/core/errors.js.map +1 -0
  26. package/dist/core/limits.d.ts +122 -0
  27. package/dist/core/limits.d.ts.map +1 -0
  28. package/dist/core/limits.js +132 -0
  29. package/dist/core/limits.js.map +1 -0
  30. package/dist/core/responses.d.ts +207 -0
  31. package/dist/core/responses.d.ts.map +1 -0
  32. package/dist/core/responses.js +73 -0
  33. package/dist/core/responses.js.map +1 -0
  34. package/dist/core/validation.d.ts +146 -0
  35. package/dist/core/validation.d.ts.map +1 -0
  36. package/dist/core/validation.js +152 -0
  37. package/dist/core/validation.js.map +1 -0
  38. package/dist/domains/account/account.d.ts +42 -0
  39. package/dist/domains/account/account.d.ts.map +1 -0
  40. package/dist/domains/account/account.js +41 -0
  41. package/dist/domains/account/account.js.map +1 -0
  42. package/dist/domains/audit/audit.d.ts +169 -0
  43. package/dist/domains/audit/audit.d.ts.map +1 -0
  44. package/dist/domains/audit/audit.js +84 -0
  45. package/dist/domains/audit/audit.js.map +1 -0
  46. package/dist/domains/chat/chat.d.ts +187 -0
  47. package/dist/domains/chat/chat.d.ts.map +1 -0
  48. package/dist/domains/chat/chat.js +189 -0
  49. package/dist/domains/chat/chat.js.map +1 -0
  50. package/dist/domains/dashboard/dashboard.d.ts +48 -0
  51. package/dist/domains/dashboard/dashboard.d.ts.map +1 -0
  52. package/dist/domains/dashboard/dashboard.js +58 -0
  53. package/dist/domains/dashboard/dashboard.js.map +1 -0
  54. package/dist/domains/discover/discover.d.ts +245 -0
  55. package/dist/domains/discover/discover.d.ts.map +1 -0
  56. package/dist/domains/discover/discover.js +68 -0
  57. package/dist/domains/discover/discover.js.map +1 -0
  58. package/dist/domains/feed/feed.d.ts +1695 -0
  59. package/dist/domains/feed/feed.d.ts.map +1 -0
  60. package/dist/domains/feed/feed.js +626 -0
  61. package/dist/domains/feed/feed.js.map +1 -0
  62. package/dist/domains/media/media.d.ts +134 -0
  63. package/dist/domains/media/media.d.ts.map +1 -0
  64. package/dist/domains/media/media.js +110 -0
  65. package/dist/domains/media/media.js.map +1 -0
  66. package/dist/domains/members/members.d.ts +175 -0
  67. package/dist/domains/members/members.d.ts.map +1 -0
  68. package/dist/domains/members/members.js +106 -0
  69. package/dist/domains/members/members.js.map +1 -0
  70. package/dist/domains/moderation/moderation.d.ts +184 -0
  71. package/dist/domains/moderation/moderation.d.ts.map +1 -0
  72. package/dist/domains/moderation/moderation.js +90 -0
  73. package/dist/domains/moderation/moderation.js.map +1 -0
  74. package/dist/domains/onboarding/onboarding.d.ts +1018 -0
  75. package/dist/domains/onboarding/onboarding.d.ts.map +1 -0
  76. package/dist/domains/onboarding/onboarding.js +717 -0
  77. package/dist/domains/onboarding/onboarding.js.map +1 -0
  78. package/dist/domains/profile/founder-profile.d.ts +411 -0
  79. package/dist/domains/profile/founder-profile.d.ts.map +1 -0
  80. package/dist/domains/profile/founder-profile.js +202 -0
  81. package/dist/domains/profile/founder-profile.js.map +1 -0
  82. package/dist/domains/support/support.d.ts +305 -0
  83. package/dist/domains/support/support.d.ts.map +1 -0
  84. package/dist/domains/support/support.js +129 -0
  85. package/dist/domains/support/support.js.map +1 -0
  86. package/dist/domains/team/team.d.ts +79 -0
  87. package/dist/domains/team/team.d.ts.map +1 -0
  88. package/dist/domains/team/team.js +54 -0
  89. package/dist/domains/team/team.js.map +1 -0
  90. package/dist/domains/uploads/uploads.d.ts +101 -0
  91. package/dist/domains/uploads/uploads.d.ts.map +1 -0
  92. package/dist/domains/uploads/uploads.js +89 -0
  93. package/dist/domains/uploads/uploads.js.map +1 -0
  94. package/dist/env.d.ts +85 -0
  95. package/dist/env.d.ts.map +1 -0
  96. package/dist/env.js +150 -0
  97. package/dist/env.js.map +1 -0
  98. package/dist/index.d.ts +22 -12
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +32 -19
  101. package/dist/index.js.map +1 -1
  102. package/dist/onboarding.d.ts +58 -32
  103. package/dist/onboarding.d.ts.map +1 -1
  104. package/dist/onboarding.js +155 -32
  105. package/dist/onboarding.js.map +1 -1
  106. package/package.json +14 -2
package/README.md CHANGED
@@ -57,7 +57,7 @@ until it is on npm.
57
57
  The backend validates a request and the web client validates a form with the
58
58
  SAME schema object, so the two can never disagree about what is allowed.
59
59
 
60
- - **Every limit has a name** (`src/limits.ts`: `DISPLAY_NAME_MAX`,
60
+ - **Every limit has a name** (`src/core/limits.ts`: `DISPLAY_NAME_MAX`,
61
61
  `LINK_URL_MAX`, `UPLOAD_MAX_BYTES`...). A schema uses the constant; a consumer
62
62
  imports the same constant for its counters or its Mongoose `maxlength`. Nobody
63
63
  types the digits again. Three copies of one number is how image alt text came
@@ -91,7 +91,7 @@ SAME schema object, so the two can never disagree about what is allowed.
91
91
  - **Bodies are typed JSON; only queries coerce.** `teamSize: "5"` is refused in a
92
92
  body, and `?limit=5` is read as 5.
93
93
  - **Shapes are derived, never copied.** One `locationSchema`, one
94
- `founderLinksSchema`, one `ventureSchema`, one `uploadKeySchema`; the other
94
+ `founderLinksSchema`, one `ventureSchema`, one `mediaImageSchema`; the other
95
95
  places pick, omit or reuse them, and a test checks by object identity.
96
96
 
97
97
  `zod` is a **peer** dependency (`^4.4.0`), on purpose. The consumer owns the Zod
@@ -99,6 +99,31 @@ instance; this package must not bring its own.
99
99
 
100
100
  ---
101
101
 
102
+ ## Breaking in 0.5.0: the media pipeline
103
+
104
+ No export was removed (268 -> 282). Images are now processed on the server:
105
+ uploaded, then COMPLETED (decoded, metadata stripped, re-encoded as WebP sizes),
106
+ and referenced by a media id. The whole flow is in `domains/media/media.ts` and
107
+ in founder-circle-backend `docs/media.md`.
108
+
109
+ | What | Before | Now |
110
+ |---|---|---|
111
+ | `uploadPresignBodySchema` | `{ extension, contentLength }` | `{ purpose, contentType, contentLength }` |
112
+ | `uploadPresignResponseSchema` | `{ key, url, headers, expiresIn }` | `{ mediaId, url, method: 'PUT', headers, expiresIn }` |
113
+ | `attachedMediaSchema` (post media) | `{ uploadKey, alt? }` | `{ mediaId, alt? }`; no id twice in one post |
114
+ | `completeOnboardingBodySchema` | `avatarUploadKey` | `avatarMediaId` (an upload with purpose `avatar`) |
115
+ | `feedImageSchema` | `{ kind, url, width, height, thumbnailUrl?, alt? }` | `mediaImageSchema` (`id`, `variants`, `placeholder`) + `kind`, `thumbnailUrl`, `alt` |
116
+ | `founderProfileSchema` | `avatarUrl` | `avatarUrl` (upload's 192 px size, else the sign-in picture) and `avatar` (the uploaded set) |
117
+ | Error codes | `UPLOAD_NOT_ATTACHABLE`, `_TOO_LARGE`, `_WRONG_TYPE` | plus `UPLOAD_NOT_RECEIVED`, `_PROCESSING`, `_UNREADABLE`, `_TOO_MANY_PIXELS`, `_LIMIT_REACHED`, `_IN_USE` |
118
+
119
+ New: `POST /uploads/:mediaId/complete`, `DELETE /uploads/:mediaId`,
120
+ `GET /profiles/me`, `PUT|DELETE /profiles/me/avatar`.
121
+
122
+ Deprecated, unused by any route: `uploadKeySchema`, `UPLOAD_KEY_PATTERN`,
123
+ `UPLOAD_KEY_MAX_CHARS`, `UploadKey` (the 0.4.0 storage key).
124
+
125
+ ---
126
+
102
127
  ## Breaking in 0.2.0
103
128
 
104
129
  No export was removed; these change BEHAVIOUR or a field's shape. Every one is a
@@ -152,24 +177,27 @@ Two things that follow from it:
152
177
 
153
178
  ```
154
179
  src/
155
- limits.ts every length, size and count limit, by name (imports nothing)
156
- validation.ts message keys, the wire shape of an issue, toValidationIssues
157
- errors.ts every API error code (API_ERROR_CODE_VALUES)
158
- uploads.ts the upload key, its extensions and content types, presign
159
- common.ts usernames (and the reserved list), the enums, URLs, phone,
160
- location, coordinates, pagination
161
- founder-profile.ts the profile, links, venture, the card, the write bodies
162
- onboarding.ts the steps, their per-step schemas, the draft and the finish
163
- feed.ts posts, polls, comments, reactions, reports, params, pages
164
- discover.ts the Discover query, its page and its facets
165
- chat.ts config, token, opening a conversation, participants
166
- responses/common.ts success / error / paginated / { id } envelopes
167
- index.ts the public surface. Everything a consumer imports.
180
+ index.ts the public surface. Everything a consumer imports.
181
+ env.ts env field builders, its own entry: `founder-schema/env`
182
+ core/ what every domain builds on (imports no domain)
183
+ limits.ts every length, size and count limit, by name (imports nothing)
184
+ validation.ts message keys, the wire shape of an issue, toValidationIssues
185
+ errors.ts every API error code (API_ERROR_CODE_VALUES)
186
+ common.ts usernames (and the reserved list), the enums, URLs, phone,
187
+ location, coordinates, pagination
188
+ responses.ts success / error / paginated / { id } envelopes
189
+ access/ who may do what (CASL's words, shared by server and web)
190
+ roles.ts the roles, who may grant which, the active-role window
191
+ permissions.ts actions, subjects, the fields a rule may name, AppAbility
192
+ access.ts GET /me/access, switching role, the MPIN
193
+ domains/<domain>/ one folder per product area: its bodies, params and answers
194
+ account/ audit/ chat/ discover/ feed/ onboarding/ profile/ team/ uploads/
168
195
  tests/
169
196
  ssot.test.ts derived not copied, no body coercion, URLs, uploads, codes
170
197
  validation.test.ts keys not sentences, no input leaves, limits applied
171
198
  onboarding-steps.test.ts steps, step schemas and the finish body agree
172
199
  responses.test.ts the envelopes, pinned
200
+ access.test.ts roles, the grant table, the MPIN, rules across the wire
173
201
  public-surface.test.ts export-removal tripwire + the values the database is pinned to
174
202
  ```
175
203
 
@@ -252,7 +280,7 @@ bearing, so append at the end and never insert.
252
280
  ## Settled since the scaffold
253
281
 
254
282
  - **The npm name and the first publish:** `founder-schema`, 0.1.0, by the user.
255
- - **The onboarding steps:** seven, ruled 2026-09-22; see `src/onboarding.ts`.
283
+ - **The onboarding steps:** seven, ruled 2026-09-22; see `src/domains/onboarding/onboarding.ts`.
256
284
 
257
285
  ## Contract decisions, 2026-09-21
258
286
 
@@ -0,0 +1,76 @@
1
+ /**
2
+ * founder-schema: the access API
3
+ *
4
+ * GET /api/v1/me/access who I am acting as, what I hold, my rules
5
+ * PUT /api/v1/me/active-role switch role (a staff role needs the MPIN)
6
+ * POST /api/v1/me/mpin set the MPIN, once
7
+ * PUT /api/v1/me/mpin change it, with the current one
8
+ *
9
+ * THE MPIN is a 6-digit code that stands between a signed-in member and their
10
+ * staff roles. Signing in proves who you are; the MPIN proves it is still you
11
+ * at the moment you pick up admin powers, on a phone that may have been left
12
+ * unlocked. Set once by its owner; a forgotten one is cleared by a superadmin
13
+ * (an audited action), after which the owner sets a new one. `MPIN_MAX_ATTEMPTS`
14
+ * wrong tries lock it for `MPIN_LOCK_MS`.
15
+ *
16
+ * THE ACTIVE ROLE is per account (the user's ruling, 2026-10-05): switching on
17
+ * one device switches every device. It lasts `ACTIVE_ROLE_TTL_MS`, and the
18
+ * server re-checks it on EVERY request: a role that was revoked, or that
19
+ * expired, is acting as a member again at once, not at the next sign-in.
20
+ *
21
+ * @module founder-schema/access/access
22
+ */
23
+ import { z } from 'zod';
24
+ export declare const MPIN_LENGTH = 6;
25
+ /** Wrong tries before the MPIN locks. */
26
+ export declare const MPIN_MAX_ATTEMPTS = 5;
27
+ /** How long a locked MPIN stays locked. */
28
+ export declare const MPIN_LOCK_MS: number;
29
+ export declare const mpinSchema: z.ZodString;
30
+ /** The answer to `GET /me/access`, and to every switch. */
31
+ export declare const meAccessResponseSchema: z.ZodObject<{
32
+ roles: z.ZodArray<z.ZodEnum<{
33
+ superadmin: "superadmin";
34
+ manager: "manager";
35
+ moderator: "moderator";
36
+ support_agent: "support_agent";
37
+ member: "member";
38
+ }>>;
39
+ activeRole: z.ZodEnum<{
40
+ superadmin: "superadmin";
41
+ manager: "manager";
42
+ moderator: "moderator";
43
+ support_agent: "support_agent";
44
+ member: "member";
45
+ }>;
46
+ activeRoleExpiresAt: z.ZodNullable<z.ZodISODateTime>;
47
+ mpinSet: z.ZodBoolean;
48
+ rules: z.ZodArray<z.ZodArray<z.ZodUnknown>>;
49
+ }, z.core.$strip>;
50
+ export type MeAccessResponse = z.infer<typeof meAccessResponseSchema>;
51
+ /** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */
52
+ export declare const switchRoleBodySchema: z.ZodObject<{
53
+ role: z.ZodEnum<{
54
+ superadmin: "superadmin";
55
+ manager: "manager";
56
+ moderator: "moderator";
57
+ support_agent: "support_agent";
58
+ member: "member";
59
+ }>;
60
+ mpin: z.ZodOptional<z.ZodString>;
61
+ }, z.core.$strip>;
62
+ export type SwitchRoleBody = z.infer<typeof switchRoleBodySchema>;
63
+ /** `POST /me/mpin`: the first MPIN, typed twice. */
64
+ export declare const setMpinBodySchema: z.ZodObject<{
65
+ mpin: z.ZodString;
66
+ confirmMpin: z.ZodString;
67
+ }, z.core.$strip>;
68
+ export type SetMpinBody = z.infer<typeof setMpinBodySchema>;
69
+ /** `PUT /me/mpin`: the current MPIN, then the new one typed twice. */
70
+ export declare const changeMpinBodySchema: z.ZodObject<{
71
+ currentMpin: z.ZodString;
72
+ mpin: z.ZodString;
73
+ confirmMpin: z.ZodString;
74
+ }, z.core.$strip>;
75
+ export type ChangeMpinBody = z.infer<typeof changeMpinBodySchema>;
76
+ //# sourceMappingURL=access.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,66 @@
1
+ /**
2
+ * founder-schema: the access API
3
+ *
4
+ * GET /api/v1/me/access who I am acting as, what I hold, my rules
5
+ * PUT /api/v1/me/active-role switch role (a staff role needs the MPIN)
6
+ * POST /api/v1/me/mpin set the MPIN, once
7
+ * PUT /api/v1/me/mpin change it, with the current one
8
+ *
9
+ * THE MPIN is a 6-digit code that stands between a signed-in member and their
10
+ * staff roles. Signing in proves who you are; the MPIN proves it is still you
11
+ * at the moment you pick up admin powers, on a phone that may have been left
12
+ * unlocked. Set once by its owner; a forgotten one is cleared by a superadmin
13
+ * (an audited action), after which the owner sets a new one. `MPIN_MAX_ATTEMPTS`
14
+ * wrong tries lock it for `MPIN_LOCK_MS`.
15
+ *
16
+ * THE ACTIVE ROLE is per account (the user's ruling, 2026-10-05): switching on
17
+ * one device switches every device. It lasts `ACTIVE_ROLE_TTL_MS`, and the
18
+ * server re-checks it on EVERY request: a role that was revoked, or that
19
+ * expired, is acting as a member again at once, not at the next sign-in.
20
+ *
21
+ * @module founder-schema/access/access
22
+ */
23
+ import { z } from 'zod';
24
+ import { VALIDATION_MESSAGE } from '../core/validation.js';
25
+ import { packedRulesSchema } from './permissions.js';
26
+ import { MEMBER_ROLE, roleSchema } from './roles.js';
27
+ export const MPIN_LENGTH = 6;
28
+ /** Wrong tries before the MPIN locks. */
29
+ export const MPIN_MAX_ATTEMPTS = 5;
30
+ /** How long a locked MPIN stays locked. */
31
+ export const MPIN_LOCK_MS = 15 * 60 * 1000;
32
+ export const mpinSchema = z.string().regex(/^\d{6}$/, VALIDATION_MESSAGE.mpinInvalid);
33
+ /** The answer to `GET /me/access`, and to every switch. */
34
+ export const meAccessResponseSchema = z.object({
35
+ /** Every role held, member always among them, highest first. */
36
+ roles: z.array(roleSchema).min(1),
37
+ /** The role being acted as now. Member when no staff role is switched on. */
38
+ activeRole: roleSchema,
39
+ /** When the active staff role lapses back to member. Null while acting as a member. */
40
+ activeRoleExpiresAt: z.iso.datetime().nullable(),
41
+ /** Whether an MPIN is set, so the switcher knows to ask for one or to offer setting one. */
42
+ mpinSet: z.boolean(),
43
+ /** The active role's permissions, packed (`packRules`). */
44
+ rules: packedRulesSchema,
45
+ });
46
+ /** `PUT /me/active-role`. Back to member needs nothing; any staff role needs the MPIN. */
47
+ export const switchRoleBodySchema = z
48
+ .object({
49
+ role: roleSchema,
50
+ mpin: mpinSchema.optional(),
51
+ })
52
+ .refine((body) => body.role === MEMBER_ROLE || body.mpin !== undefined, {
53
+ path: ['mpin'],
54
+ error: VALIDATION_MESSAGE.mpinRequired,
55
+ });
56
+ const confirmed = (body) => body.mpin === body.confirmMpin;
57
+ const mismatch = { path: ['confirmMpin'], error: VALIDATION_MESSAGE.mpinMismatch };
58
+ /** `POST /me/mpin`: the first MPIN, typed twice. */
59
+ export const setMpinBodySchema = z
60
+ .object({ mpin: mpinSchema, confirmMpin: mpinSchema })
61
+ .refine(confirmed, mismatch);
62
+ /** `PUT /me/mpin`: the current MPIN, then the new one typed twice. */
63
+ export const changeMpinBodySchema = z
64
+ .object({ currentMpin: mpinSchema, mpin: mpinSchema, confirmMpin: mpinSchema })
65
+ .refine(confirmed, mismatch);
66
+ //# sourceMappingURL=access.js.map
@@ -0,0 +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"]}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * founder-schema: what a role may do, in CASL's terms
3
+ *
4
+ * Permissions are ACTION on SUBJECT, optionally narrowed by conditions on the
5
+ * subject's fields ("update a Post whose authorId is mine"). The backend writes
6
+ * one small policy per role from these words; the web receives the resulting
7
+ * rules and asks the same questions to decide what to draw. Because both sides
8
+ * spell actions and subjects from this file, a permission check with a typo is
9
+ * a compile error, not a button that silently never shows.
10
+ *
11
+ * The SERVER decides; the web only mirrors. Every rule the web holds came from
12
+ * the server for the role being acted as, and every request is checked again
13
+ * on the server, so a tampered client can show itself a button and nothing
14
+ * more.
15
+ *
16
+ * @module founder-schema/access/permissions
17
+ */
18
+ import type { ForcedSubject, MongoAbility } from '@casl/ability';
19
+ import { z } from 'zod';
20
+ import type { PlatformRole } from './roles.js';
21
+ /**
22
+ * The verbs.
23
+ *
24
+ * manage every action (CASL's wildcard). Only superadmin holds it on `all`.
25
+ * create, read, update, delete
26
+ * moderate hide, restore or remove someone else's content, act on a report
27
+ * assign grant or revoke (a platform role); take or hand over (a ticket)
28
+ * suspend stop a member signing in to the product, and lift it
29
+ * resolve close a support ticket or a report
30
+ * reset clear a credential its owner forgot (a staff member's MPIN)
31
+ */
32
+ export declare const ACTION_VALUES: readonly ["manage", "create", "read", "update", "delete", "moderate", "assign", "suspend", "resolve", "reset"];
33
+ export declare const actionSchema: z.ZodEnum<{
34
+ manage: "manage";
35
+ create: "create";
36
+ read: "read";
37
+ update: "update";
38
+ delete: "delete";
39
+ moderate: "moderate";
40
+ assign: "assign";
41
+ suspend: "suspend";
42
+ resolve: "resolve";
43
+ reset: "reset";
44
+ }>;
45
+ export type Action = z.infer<typeof actionSchema>;
46
+ /**
47
+ * The things.
48
+ *
49
+ * all CASL's wildcard subject
50
+ * Dashboard the staff overview: counts and trends
51
+ * Member a person's account in this product
52
+ * FounderProfile a member's public profile
53
+ * Post, Poll, Comment
54
+ * Report a member's report of a post, poll or comment
55
+ * SupportTicket a member's request for help
56
+ * SavedReply a support agent's reusable answer
57
+ * PlatformRole a staff role, as something granted and revoked
58
+ * AuditEntry the record of a staff action
59
+ * PlatformSettings product-wide configuration
60
+ */
61
+ export declare const SUBJECT_VALUES: readonly ["all", "Dashboard", "Member", "FounderProfile", "Post", "Poll", "Comment", "Report", "SupportTicket", "SavedReply", "PlatformRole", "AuditEntry", "PlatformSettings"];
62
+ export declare const subjectSchema: z.ZodEnum<{
63
+ all: "all";
64
+ Dashboard: "Dashboard";
65
+ Member: "Member";
66
+ FounderProfile: "FounderProfile";
67
+ Post: "Post";
68
+ Poll: "Poll";
69
+ Comment: "Comment";
70
+ Report: "Report";
71
+ SupportTicket: "SupportTicket";
72
+ SavedReply: "SavedReply";
73
+ PlatformRole: "PlatformRole";
74
+ AuditEntry: "AuditEntry";
75
+ PlatformSettings: "PlatformSettings";
76
+ }>;
77
+ export type Subject = z.infer<typeof subjectSchema>;
78
+ /**
79
+ * The fields a rule may set CONDITIONS on, per subject: the server's stored
80
+ * names, ids as strings. Declared here so a condition on a field the subject
81
+ * does not have (`{ ownerId: me }` on a Post) is a compile error, on both sides.
82
+ *
83
+ * A web screen does not evaluate these against an API response: a response
84
+ * names things differently (`author.userId`, not `authorId`). Row-level answers
85
+ * come from the server on the item itself (`viewer.canEdit`); the web's own
86
+ * checks are type-level ("may I moderate Reports at all").
87
+ */
88
+ export interface SubjectFields {
89
+ Dashboard: Record<never, never>;
90
+ /** `appRoles` is the staff roles held, so a rule can protect a role ("never suspend a superadmin"). */
91
+ Member: {
92
+ _id: string;
93
+ appRoles: PlatformRole[];
94
+ };
95
+ FounderProfile: {
96
+ userId: string;
97
+ };
98
+ Post: {
99
+ authorId: string;
100
+ };
101
+ Poll: {
102
+ authorId: string;
103
+ };
104
+ Comment: {
105
+ authorId: string;
106
+ };
107
+ Report: {
108
+ reporterId: string;
109
+ status: string;
110
+ };
111
+ SupportTicket: {
112
+ memberId: string;
113
+ assigneeId: string | null;
114
+ };
115
+ SavedReply: {
116
+ authorId: string;
117
+ };
118
+ /** One role on one person: `memberId` is whose, so "never your own" is a rule too. */
119
+ PlatformRole: {
120
+ role: PlatformRole;
121
+ memberId: string;
122
+ };
123
+ /** `actorId` is null for an entry a server script wrote. */
124
+ AuditEntry: {
125
+ actorId: string | null;
126
+ };
127
+ PlatformSettings: Record<never, never>;
128
+ }
129
+ /** A document tagged with its subject type, as CASL's `subject('Post', doc)` makes one. */
130
+ export type SubjectInstance = {
131
+ [K in keyof SubjectFields]: SubjectFields[K] & ForcedSubject<K>;
132
+ }[keyof SubjectFields];
133
+ /**
134
+ * The ability both sides build: the backend from its policy, the web from the
135
+ * rules the backend sends. Mongo-style conditions, typed by `SubjectFields`.
136
+ */
137
+ export type AppAbility = MongoAbility<[Action, Subject | SubjectInstance]>;
138
+ /**
139
+ * Compile-time proof that `SubjectFields` names exactly the subjects, no more
140
+ * and no fewer: adding a subject without its fields fails the build here.
141
+ */
142
+ type Same<A, B> = [A] extends [B] ? ([B] extends [A] ? true : false) : false;
143
+ type Assert<T extends true> = T;
144
+ export type EverySubjectHasFields = Assert<Same<keyof SubjectFields, Exclude<Subject, 'all'>>>;
145
+ /**
146
+ * Rules as they cross the wire: CASL's `packRules` output, an array of
147
+ * compact arrays. The web unpacks them with `unpackRules` before building its
148
+ * ability; their inner shape is CASL's, so it is not re-described here.
149
+ */
150
+ export declare const packedRulesSchema: z.ZodArray<z.ZodArray<z.ZodUnknown>>;
151
+ export type PackedRules = z.infer<typeof packedRulesSchema>;
152
+ export {};
153
+ //# sourceMappingURL=permissions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/access/permissions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * founder-schema: what a role may do, in CASL's terms
3
+ *
4
+ * Permissions are ACTION on SUBJECT, optionally narrowed by conditions on the
5
+ * subject's fields ("update a Post whose authorId is mine"). The backend writes
6
+ * one small policy per role from these words; the web receives the resulting
7
+ * rules and asks the same questions to decide what to draw. Because both sides
8
+ * spell actions and subjects from this file, a permission check with a typo is
9
+ * a compile error, not a button that silently never shows.
10
+ *
11
+ * The SERVER decides; the web only mirrors. Every rule the web holds came from
12
+ * the server for the role being acted as, and every request is checked again
13
+ * on the server, so a tampered client can show itself a button and nothing
14
+ * more.
15
+ *
16
+ * @module founder-schema/access/permissions
17
+ */
18
+ import { z } from 'zod';
19
+ /**
20
+ * The verbs.
21
+ *
22
+ * manage every action (CASL's wildcard). Only superadmin holds it on `all`.
23
+ * create, read, update, delete
24
+ * moderate hide, restore or remove someone else's content, act on a report
25
+ * assign grant or revoke (a platform role); take or hand over (a ticket)
26
+ * suspend stop a member signing in to the product, and lift it
27
+ * resolve close a support ticket or a report
28
+ * reset clear a credential its owner forgot (a staff member's MPIN)
29
+ */
30
+ export const ACTION_VALUES = [
31
+ 'manage',
32
+ 'create',
33
+ 'read',
34
+ 'update',
35
+ 'delete',
36
+ 'moderate',
37
+ 'assign',
38
+ 'suspend',
39
+ 'resolve',
40
+ 'reset',
41
+ ];
42
+ export const actionSchema = z.enum(ACTION_VALUES);
43
+ /**
44
+ * The things.
45
+ *
46
+ * all CASL's wildcard subject
47
+ * Dashboard the staff overview: counts and trends
48
+ * Member a person's account in this product
49
+ * FounderProfile a member's public profile
50
+ * Post, Poll, Comment
51
+ * Report a member's report of a post, poll or comment
52
+ * SupportTicket a member's request for help
53
+ * SavedReply a support agent's reusable answer
54
+ * PlatformRole a staff role, as something granted and revoked
55
+ * AuditEntry the record of a staff action
56
+ * PlatformSettings product-wide configuration
57
+ */
58
+ export const SUBJECT_VALUES = [
59
+ 'all',
60
+ 'Dashboard',
61
+ 'Member',
62
+ 'FounderProfile',
63
+ 'Post',
64
+ 'Poll',
65
+ 'Comment',
66
+ 'Report',
67
+ 'SupportTicket',
68
+ 'SavedReply',
69
+ 'PlatformRole',
70
+ 'AuditEntry',
71
+ 'PlatformSettings',
72
+ ];
73
+ export const subjectSchema = z.enum(SUBJECT_VALUES);
74
+ /**
75
+ * Rules as they cross the wire: CASL's `packRules` output, an array of
76
+ * compact arrays. The web unpacks them with `unpackRules` before building its
77
+ * ability; their inner shape is CASL's, so it is not re-described here.
78
+ */
79
+ export const packedRulesSchema = z.array(z.array(z.unknown()));
80
+ //# sourceMappingURL=permissions.js.map
@@ -0,0 +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;;;;;;;;;;GAUG;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;CACC,CAAC;AACX,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;AAGlD;;;;;;;;;;;;;;GAcG;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;CACV,CAAC;AACX,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;AAkDpD;;;;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 */\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] 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 */\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] 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\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"]}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * founder-schema: who someone can be in Founder Circle
3
+ *
4
+ * Every person is a MEMBER. Some also hold PLATFORM ROLES, the staff who run
5
+ * the product. A person acts as ONE role at a time (the "active role"), chosen
6
+ * with the role switcher; what they may do is that role's permissions plus the
7
+ * member's own, never the union of everything they hold. A superadmin reading
8
+ * the feed as a member sees what a member sees.
9
+ *
10
+ * superadmin owns the platform: governance, the staff team, the audit log
11
+ * manager runs the community day to day: members, content, the team
12
+ * of moderators and support agents
13
+ * moderator keeps the feed safe: reports, hiding and removing content
14
+ * support_agent helps members with their account and their problems
15
+ * member everyone, the founder using the product
16
+ *
17
+ * THE ONE LIST. The backend's policy, the web's tabs and switcher, and every
18
+ * request and response that names a role read these values, so a role cannot
19
+ * exist on one side and not the other. GymMonk and Jansathi each kept their
20
+ * role list in three or four places, and the copies drifted.
21
+ *
22
+ * @module founder-schema/access/roles
23
+ */
24
+ import { z } from 'zod';
25
+ /** The staff roles, highest first. */
26
+ export declare const PLATFORM_ROLE_VALUES: readonly ["superadmin", "manager", "moderator", "support_agent"];
27
+ export declare const platformRoleSchema: z.ZodEnum<{
28
+ superadmin: "superadmin";
29
+ manager: "manager";
30
+ moderator: "moderator";
31
+ support_agent: "support_agent";
32
+ }>;
33
+ export type PlatformRole = z.infer<typeof platformRoleSchema>;
34
+ /** Everyone holds it. It is never stored and never granted. */
35
+ export declare const MEMBER_ROLE: "member";
36
+ /**
37
+ * Every role, highest first: the switcher's order. Nobody LANDS in a staff
38
+ * role: everyone acts as a member until they switch with their MPIN (see
39
+ * `access.ts`), so a borrowed phone never opens on a moderation console.
40
+ */
41
+ export declare const ROLE_VALUES: readonly ["superadmin", "manager", "moderator", "support_agent", "member"];
42
+ export declare const roleSchema: z.ZodEnum<{
43
+ superadmin: "superadmin";
44
+ manager: "manager";
45
+ moderator: "moderator";
46
+ support_agent: "support_agent";
47
+ member: "member";
48
+ }>;
49
+ export type Role = z.infer<typeof roleSchema>;
50
+ export declare function isPlatformRole(value: unknown): value is PlatformRole;
51
+ /**
52
+ * Who may grant or take away each platform role, keyed by the role in
53
+ * question, listing the roles ACTED AS that may do it. The user's ruling,
54
+ * 2026-10-05:
55
+ *
56
+ * - only a superadmin makes (or unmakes) a manager;
57
+ * - a superadmin or a manager makes a moderator or a support agent;
58
+ * - superadmin is never granted through the product: a server script does
59
+ * it, so no compromised account can mint another superadmin;
60
+ * - nobody changes their own roles (enforced where the change is made).
61
+ */
62
+ export declare const ROLE_GRANTED_BY: Readonly<Record<PlatformRole, readonly PlatformRole[]>>;
63
+ /** May someone acting as `actingAs` grant or revoke `role`? */
64
+ export declare function canGrantRole(actingAs: Role, role: PlatformRole): boolean;
65
+ /**
66
+ * How long a staff role stays switched on. After it, the person is back to
67
+ * acting as a member until they switch again, with their MPIN. A phone left
68
+ * unlocked on a desk does not stay a moderation console for days.
69
+ */
70
+ export declare const ACTIVE_ROLE_TTL_MS: number;
71
+ //# sourceMappingURL=roles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.d.ts","sourceRoot":"","sources":["../../src/access/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,sCAAsC;AACtC,eAAO,MAAM,oBAAoB,kEAKvB,CAAC;AACX,eAAO,MAAM,kBAAkB;;;;;EAA+B,CAAC;AAC/D,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,+DAA+D;AAC/D,eAAO,MAAM,WAAW,EAAG,QAAiB,CAAC;AAE7C;;;;GAIG;AACH,eAAO,MAAM,WAAW,4EAAkD,CAAC;AAC3E,eAAO,MAAM,UAAU;;;;;;EAAsB,CAAC;AAC9C,MAAM,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,UAAU,CAAC,CAAC;AAE9C,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAEpE;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,YAAY,EAAE,SAAS,YAAY,EAAE,CAAC,CAKnF,CAAC;AAEF,+DAA+D;AAC/D,wBAAgB,YAAY,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAExE;AAED;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,QAAsB,CAAC"}