founder-schema 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -15
- package/dist/access/access.d.ts +76 -0
- package/dist/access/access.d.ts.map +1 -0
- package/dist/access/access.js +66 -0
- package/dist/access/access.js.map +1 -0
- package/dist/access/permissions.d.ts +153 -0
- package/dist/access/permissions.d.ts.map +1 -0
- package/dist/access/permissions.js +80 -0
- package/dist/access/permissions.js.map +1 -0
- package/dist/access/roles.d.ts +71 -0
- package/dist/access/roles.d.ts.map +1 -0
- package/dist/access/roles.js +72 -0
- package/dist/access/roles.js.map +1 -0
- package/dist/chat.d.ts +4 -4
- package/dist/chat.d.ts.map +1 -1
- package/dist/chat.js +7 -2
- package/dist/chat.js.map +1 -1
- package/dist/core/common.d.ts +304 -0
- package/dist/core/common.d.ts.map +1 -0
- package/dist/core/common.js +340 -0
- package/dist/core/common.js.map +1 -0
- package/dist/core/errors.d.ts +105 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +115 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/limits.d.ts +99 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +109 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/responses.d.ts +195 -0
- package/dist/core/responses.d.ts.map +1 -0
- package/dist/core/responses.js +73 -0
- package/dist/core/responses.js.map +1 -0
- package/dist/core/validation.d.ts +144 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +150 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/domains/account/account.d.ts +40 -0
- package/dist/domains/account/account.d.ts.map +1 -0
- package/dist/domains/account/account.js +39 -0
- package/dist/domains/account/account.js.map +1 -0
- package/dist/domains/audit/audit.d.ts +169 -0
- package/dist/domains/audit/audit.d.ts.map +1 -0
- package/dist/domains/audit/audit.js +84 -0
- package/dist/domains/audit/audit.js.map +1 -0
- package/dist/domains/chat/chat.d.ts +187 -0
- package/dist/domains/chat/chat.d.ts.map +1 -0
- package/dist/domains/chat/chat.js +189 -0
- package/dist/domains/chat/chat.js.map +1 -0
- package/dist/domains/dashboard/dashboard.d.ts +48 -0
- package/dist/domains/dashboard/dashboard.d.ts.map +1 -0
- package/dist/domains/dashboard/dashboard.js +58 -0
- package/dist/domains/dashboard/dashboard.js.map +1 -0
- package/dist/domains/discover/discover.d.ts +245 -0
- package/dist/domains/discover/discover.d.ts.map +1 -0
- package/dist/domains/discover/discover.js +68 -0
- package/dist/domains/discover/discover.js.map +1 -0
- package/dist/domains/feed/feed.d.ts +1658 -0
- package/dist/domains/feed/feed.d.ts.map +1 -0
- package/dist/domains/feed/feed.js +623 -0
- package/dist/domains/feed/feed.js.map +1 -0
- package/dist/domains/members/members.d.ts +175 -0
- package/dist/domains/members/members.d.ts.map +1 -0
- package/dist/domains/members/members.js +106 -0
- package/dist/domains/members/members.js.map +1 -0
- package/dist/domains/moderation/moderation.d.ts +184 -0
- package/dist/domains/moderation/moderation.d.ts.map +1 -0
- package/dist/domains/moderation/moderation.js +90 -0
- package/dist/domains/moderation/moderation.js.map +1 -0
- package/dist/domains/onboarding/onboarding.d.ts +1018 -0
- package/dist/domains/onboarding/onboarding.d.ts.map +1 -0
- package/dist/domains/onboarding/onboarding.js +717 -0
- package/dist/domains/onboarding/onboarding.js.map +1 -0
- package/dist/domains/profile/founder-profile.d.ts +387 -0
- package/dist/domains/profile/founder-profile.d.ts.map +1 -0
- package/dist/domains/profile/founder-profile.js +180 -0
- package/dist/domains/profile/founder-profile.js.map +1 -0
- package/dist/domains/support/support.d.ts +305 -0
- package/dist/domains/support/support.d.ts.map +1 -0
- package/dist/domains/support/support.js +129 -0
- package/dist/domains/support/support.js.map +1 -0
- package/dist/domains/team/team.d.ts +79 -0
- package/dist/domains/team/team.d.ts.map +1 -0
- package/dist/domains/team/team.js +54 -0
- package/dist/domains/team/team.js.map +1 -0
- package/dist/domains/uploads/uploads.d.ts +113 -0
- package/dist/domains/uploads/uploads.d.ts.map +1 -0
- package/dist/domains/uploads/uploads.js +106 -0
- package/dist/domains/uploads/uploads.js.map +1 -0
- package/dist/env.d.ts +85 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +150 -0
- package/dist/env.js.map +1 -0
- package/dist/index.d.ts +21 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -18
- package/dist/index.js.map +1 -1
- package/dist/onboarding.d.ts +58 -32
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +155 -32
- package/dist/onboarding.js.map +1 -1
- 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
|
|
@@ -152,24 +152,27 @@ Two things that follow from it:
|
|
|
152
152
|
|
|
153
153
|
```
|
|
154
154
|
src/
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
155
|
+
index.ts the public surface. Everything a consumer imports.
|
|
156
|
+
env.ts env field builders, its own entry: `founder-schema/env`
|
|
157
|
+
core/ what every domain builds on (imports no domain)
|
|
158
|
+
limits.ts every length, size and count limit, by name (imports nothing)
|
|
159
|
+
validation.ts message keys, the wire shape of an issue, toValidationIssues
|
|
160
|
+
errors.ts every API error code (API_ERROR_CODE_VALUES)
|
|
161
|
+
common.ts usernames (and the reserved list), the enums, URLs, phone,
|
|
162
|
+
location, coordinates, pagination
|
|
163
|
+
responses.ts success / error / paginated / { id } envelopes
|
|
164
|
+
access/ who may do what (CASL's words, shared by server and web)
|
|
165
|
+
roles.ts the roles, who may grant which, the active-role window
|
|
166
|
+
permissions.ts actions, subjects, the fields a rule may name, AppAbility
|
|
167
|
+
access.ts GET /me/access, switching role, the MPIN
|
|
168
|
+
domains/<domain>/ one folder per product area: its bodies, params and answers
|
|
169
|
+
account/ audit/ chat/ discover/ feed/ onboarding/ profile/ team/ uploads/
|
|
168
170
|
tests/
|
|
169
171
|
ssot.test.ts derived not copied, no body coercion, URLs, uploads, codes
|
|
170
172
|
validation.test.ts keys not sentences, no input leaves, limits applied
|
|
171
173
|
onboarding-steps.test.ts steps, step schemas and the finish body agree
|
|
172
174
|
responses.test.ts the envelopes, pinned
|
|
175
|
+
access.test.ts roles, the grant table, the MPIN, rules across the wire
|
|
173
176
|
public-surface.test.ts export-removal tripwire + the values the database is pinned to
|
|
174
177
|
```
|
|
175
178
|
|
|
@@ -252,7 +255,7 @@ bearing, so append at the end and never insert.
|
|
|
252
255
|
## Settled since the scaffold
|
|
253
256
|
|
|
254
257
|
- **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`.
|
|
258
|
+
- **The onboarding steps:** seven, ruled 2026-09-22; see `src/domains/onboarding/onboarding.ts`.
|
|
256
259
|
|
|
257
260
|
## Contract decisions, 2026-09-21
|
|
258
261
|
|
|
@@ -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":"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;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,gHAWhB,CAAC;AACX,eAAO,MAAM,YAAY;;;;;;;;;;;EAAwB,CAAC;AAClD,MAAM,MAAM,MAAM,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAElD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,cAAc,iLAcjB,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;CACxC;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"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,72 @@
|
|
|
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 const PLATFORM_ROLE_VALUES = [
|
|
27
|
+
'superadmin',
|
|
28
|
+
'manager',
|
|
29
|
+
'moderator',
|
|
30
|
+
'support_agent',
|
|
31
|
+
];
|
|
32
|
+
export const platformRoleSchema = z.enum(PLATFORM_ROLE_VALUES);
|
|
33
|
+
/** Everyone holds it. It is never stored and never granted. */
|
|
34
|
+
export const MEMBER_ROLE = 'member';
|
|
35
|
+
/**
|
|
36
|
+
* Every role, highest first: the switcher's order. Nobody LANDS in a staff
|
|
37
|
+
* role: everyone acts as a member until they switch with their MPIN (see
|
|
38
|
+
* `access.ts`), so a borrowed phone never opens on a moderation console.
|
|
39
|
+
*/
|
|
40
|
+
export const ROLE_VALUES = [...PLATFORM_ROLE_VALUES, MEMBER_ROLE];
|
|
41
|
+
export const roleSchema = z.enum(ROLE_VALUES);
|
|
42
|
+
export function isPlatformRole(value) {
|
|
43
|
+
return platformRoleSchema.safeParse(value).success;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Who may grant or take away each platform role, keyed by the role in
|
|
47
|
+
* question, listing the roles ACTED AS that may do it. The user's ruling,
|
|
48
|
+
* 2026-10-05:
|
|
49
|
+
*
|
|
50
|
+
* - only a superadmin makes (or unmakes) a manager;
|
|
51
|
+
* - a superadmin or a manager makes a moderator or a support agent;
|
|
52
|
+
* - superadmin is never granted through the product: a server script does
|
|
53
|
+
* it, so no compromised account can mint another superadmin;
|
|
54
|
+
* - nobody changes their own roles (enforced where the change is made).
|
|
55
|
+
*/
|
|
56
|
+
export const ROLE_GRANTED_BY = {
|
|
57
|
+
superadmin: [],
|
|
58
|
+
manager: ['superadmin'],
|
|
59
|
+
moderator: ['superadmin', 'manager'],
|
|
60
|
+
support_agent: ['superadmin', 'manager'],
|
|
61
|
+
};
|
|
62
|
+
/** May someone acting as `actingAs` grant or revoke `role`? */
|
|
63
|
+
export function canGrantRole(actingAs, role) {
|
|
64
|
+
return isPlatformRole(actingAs) && ROLE_GRANTED_BY[role].includes(actingAs);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* How long a staff role stays switched on. After it, the person is back to
|
|
68
|
+
* acting as a member until they switch again, with their MPIN. A phone left
|
|
69
|
+
* unlocked on a desk does not stay a moderation console for days.
|
|
70
|
+
*/
|
|
71
|
+
export const ACTIVE_ROLE_TTL_MS = 12 * 60 * 60 * 1000;
|
|
72
|
+
//# sourceMappingURL=roles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"roles.js","sourceRoot":"","sources":["../../src/access/roles.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,sCAAsC;AACtC,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,YAAY;IACZ,SAAS;IACT,WAAW;IACX,eAAe;CACP,CAAC;AACX,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAG/D,+DAA+D;AAC/D,MAAM,CAAC,MAAM,WAAW,GAAG,QAAiB,CAAC;AAE7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAG,oBAAoB,EAAE,WAAW,CAAU,CAAC;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;AAG9C,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,kBAAkB,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC;AACrD,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,eAAe,GAA4D;IACtF,UAAU,EAAE,EAAE;IACd,OAAO,EAAE,CAAC,YAAY,CAAC;IACvB,SAAS,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC;IACpC,aAAa,EAAE,CAAC,YAAY,EAAE,SAAS,CAAC;CACzC,CAAC;AAEF,+DAA+D;AAC/D,MAAM,UAAU,YAAY,CAAC,QAAc,EAAE,IAAkB;IAC7D,OAAO,cAAc,CAAC,QAAQ,CAAC,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC9E,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC","sourcesContent":["/**\n * founder-schema: who someone can be in Founder Circle\n *\n * Every person is a MEMBER. Some also hold PLATFORM ROLES, the staff who run\n * the product. A person acts as ONE role at a time (the \"active role\"), chosen\n * with the role switcher; what they may do is that role's permissions plus the\n * member's own, never the union of everything they hold. A superadmin reading\n * the feed as a member sees what a member sees.\n *\n * superadmin owns the platform: governance, the staff team, the audit log\n * manager runs the community day to day: members, content, the team\n * of moderators and support agents\n * moderator keeps the feed safe: reports, hiding and removing content\n * support_agent helps members with their account and their problems\n * member everyone, the founder using the product\n *\n * THE ONE LIST. The backend's policy, the web's tabs and switcher, and every\n * request and response that names a role read these values, so a role cannot\n * exist on one side and not the other. GymMonk and Jansathi each kept their\n * role list in three or four places, and the copies drifted.\n *\n * @module founder-schema/access/roles\n */\n\nimport { z } from 'zod';\n\n/** The staff roles, highest first. */\nexport const PLATFORM_ROLE_VALUES = [\n 'superadmin',\n 'manager',\n 'moderator',\n 'support_agent',\n] as const;\nexport const platformRoleSchema = z.enum(PLATFORM_ROLE_VALUES);\nexport type PlatformRole = z.infer<typeof platformRoleSchema>;\n\n/** Everyone holds it. It is never stored and never granted. */\nexport const MEMBER_ROLE = 'member' as const;\n\n/**\n * Every role, highest first: the switcher's order. Nobody LANDS in a staff\n * role: everyone acts as a member until they switch with their MPIN (see\n * `access.ts`), so a borrowed phone never opens on a moderation console.\n */\nexport const ROLE_VALUES = [...PLATFORM_ROLE_VALUES, MEMBER_ROLE] as const;\nexport const roleSchema = z.enum(ROLE_VALUES);\nexport type Role = z.infer<typeof roleSchema>;\n\nexport function isPlatformRole(value: unknown): value is PlatformRole {\n return platformRoleSchema.safeParse(value).success;\n}\n\n/**\n * Who may grant or take away each platform role, keyed by the role in\n * question, listing the roles ACTED AS that may do it. The user's ruling,\n * 2026-10-05:\n *\n * - only a superadmin makes (or unmakes) a manager;\n * - a superadmin or a manager makes a moderator or a support agent;\n * - superadmin is never granted through the product: a server script does\n * it, so no compromised account can mint another superadmin;\n * - nobody changes their own roles (enforced where the change is made).\n */\nexport const ROLE_GRANTED_BY: Readonly<Record<PlatformRole, readonly PlatformRole[]>> = {\n superadmin: [],\n manager: ['superadmin'],\n moderator: ['superadmin', 'manager'],\n support_agent: ['superadmin', 'manager'],\n};\n\n/** May someone acting as `actingAs` grant or revoke `role`? */\nexport function canGrantRole(actingAs: Role, role: PlatformRole): boolean {\n return isPlatformRole(actingAs) && ROLE_GRANTED_BY[role].includes(actingAs);\n}\n\n/**\n * How long a staff role stays switched on. After it, the person is back to\n * acting as a member until they switch again, with their MPIN. A phone left\n * unlocked on a desk does not stay a moderation console for days.\n */\nexport const ACTIVE_ROLE_TTL_MS = 12 * 60 * 60 * 1000;\n"]}
|