founder-schema 0.2.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/account.d.ts +40 -0
- package/dist/account.d.ts.map +1 -0
- package/dist/account.js +39 -0
- package/dist/account.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 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +30 -17
- 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
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"support.js","sourceRoot":"","sources":["../../../src/domains/support/support.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema: support\n *\n * A member asks for help; a support agent answers. One conversation per\n * problem, a ticket, with its messages in order.\n *\n * POST /api/v1/support/tickets { subject, category, body }\n * GET /api/v1/support/tickets ?scope&status&assignee&limit&cursor\n * GET /api/v1/support/tickets/:ticketId\n * POST /api/v1/support/tickets/:ticketId/messages { body } reply\n * POST /api/v1/support/tickets/:ticketId/claim an agent takes it\n * POST /api/v1/support/tickets/:ticketId/resolve closed as solved\n * POST /api/v1/support/tickets/:ticketId/reopen the member reopens\n *\n * GET /api/v1/support/saved-replies\n * POST /api/v1/support/saved-replies { title, body }\n * PUT /api/v1/support/saved-replies/:replyId { title, body }\n * DELETE /api/v1/support/saved-replies/:replyId\n *\n * THE STATES\n * open waiting on support: new, or the member wrote last\n * waiting waiting on the member: an agent wrote last\n * resolved closed; the member can reopen it, and writing reopens it\n *\n * `scope=mine` is the tickets the asker raised, whatever role they act as;\n * `scope=queue` is every ticket the active role may read (all of them for a\n * support agent or manager, only their own for a member). An agent replying to\n * an unclaimed ticket claims it.\n *\n * Every message is free text from a person: untrusted, shown as text.\n *\n * @module founder-schema/domains/support\n */\n\nimport { z } from 'zod';\nimport {\n emptyStringToUndefined,\n objectIdSchema,\n paginationQuerySchema,\n} from '../../core/common.js';\nimport {\n SAVED_REPLY_BODY_MAX,\n SAVED_REPLY_TITLE_MAX,\n TICKET_MESSAGE_MAX,\n TICKET_SUBJECT_MAX,\n TICKET_SUBJECT_MIN,\n} from '../../core/limits.js';\nimport { paginatedSchema } from '../../core/responses.js';\n\n/** What the problem is about: routes it, and lets the team see patterns. Append only. */\nexport const TICKET_CATEGORY_VALUES = [\n 'account',\n 'profile',\n 'feed',\n 'chat',\n 'safety',\n 'bug',\n 'other',\n] as const;\nexport const ticketCategorySchema = z.enum(TICKET_CATEGORY_VALUES);\nexport type TicketCategory = z.infer<typeof ticketCategorySchema>;\n\nexport const TICKET_STATUS_VALUES = ['open', 'waiting', 'resolved'] as const;\nexport const ticketStatusSchema = z.enum(TICKET_STATUS_VALUES);\nexport type TicketStatus = z.infer<typeof ticketStatusSchema>;\n\nexport const TICKET_SCOPE_VALUES = ['mine', 'queue'] as const;\nexport const ticketScopeSchema = z.enum(TICKET_SCOPE_VALUES);\nexport type TicketScope = z.infer<typeof ticketScopeSchema>;\n\n/** The queue's assignee filter: mine, nobody's, or anyone's. */\nexport const TICKET_ASSIGNEE_FILTER_VALUES = ['me', 'unassigned', 'any'] as const;\nexport const ticketAssigneeFilterSchema = z.enum(TICKET_ASSIGNEE_FILTER_VALUES);\nexport type TicketAssigneeFilter = z.infer<typeof ticketAssigneeFilterSchema>;\n\nconst person = z.object({ id: objectIdSchema, displayName: z.string() });\n\n/** One message. `from` says which side wrote it; the agent's name is shown to the member. */\nexport const ticketMessageSchema = z.object({\n id: objectIdSchema,\n from: z.enum(['member', 'agent']),\n author: person,\n body: z.string(),\n at: z.iso.datetime(),\n});\nexport type TicketMessage = z.infer<typeof ticketMessageSchema>;\n\n/** A ticket in a list. */\nexport const ticketSummarySchema = z.object({\n id: objectIdSchema,\n subject: z.string(),\n category: ticketCategorySchema,\n status: ticketStatusSchema,\n member: person.extend({ avatarUrl: z.url().nullable() }),\n assignee: person.nullable(),\n messageCount: z.number().int().min(1),\n createdAt: z.iso.datetime(),\n lastMessageAt: z.iso.datetime(),\n /** Who wrote last, so a list can say \"Support replied\" or \"Waiting on support\". */\n lastFrom: z.enum(['member', 'agent']),\n});\nexport type TicketSummary = z.infer<typeof ticketSummarySchema>;\n\nexport const ticketPageSchema = paginatedSchema(ticketSummarySchema);\nexport type TicketPage = z.infer<typeof ticketPageSchema>;\n\n/** A ticket with its whole conversation and what the asker may do with it. */\nexport const ticketDetailSchema = ticketSummarySchema.extend({\n messages: z.array(ticketMessageSchema).min(1),\n resolvedAt: z.iso.datetime().nullable(),\n viewer: z.object({\n canReply: z.boolean(),\n canClaim: z.boolean(),\n canResolve: z.boolean(),\n canReopen: z.boolean(),\n /** Replying as the agent side (the reply is shown as Support's). */\n asAgent: z.boolean(),\n }),\n});\nexport type TicketDetail = z.infer<typeof ticketDetailSchema>;\n\n/** `:ticketId` in the paths above. */\nexport const ticketParamsSchema = z.object({ ticketId: objectIdSchema });\nexport type TicketParams = z.infer<typeof ticketParamsSchema>;\n\n/** `GET /support/tickets`. Mine by default; status and assignee narrow it. */\nexport const ticketListQuerySchema = paginationQuerySchema.extend({\n scope: z.preprocess(emptyStringToUndefined, ticketScopeSchema.default('mine')),\n status: z.preprocess(emptyStringToUndefined, ticketStatusSchema.optional()),\n assignee: z.preprocess(emptyStringToUndefined, ticketAssigneeFilterSchema.default('any')),\n});\nexport type TicketListQuery = z.infer<typeof ticketListQuerySchema>;\nexport type TicketListQueryInput = z.input<typeof ticketListQuerySchema>;\n\nconst messageBody = z.string().trim().min(1).max(TICKET_MESSAGE_MAX);\n\n/** `POST /support/tickets`: what went wrong, in the member's words. */\nexport const createTicketBodySchema = z.object({\n subject: z.string().trim().min(TICKET_SUBJECT_MIN).max(TICKET_SUBJECT_MAX),\n category: ticketCategorySchema,\n body: messageBody,\n});\nexport type CreateTicketBody = z.infer<typeof createTicketBodySchema>;\n\n/** `POST /support/tickets/:ticketId/messages`. */\nexport const replyTicketBodySchema = z.object({ body: messageBody });\nexport type ReplyTicketBody = z.infer<typeof replyTicketBodySchema>;\n\n// ─── Saved replies ───────────────────────────────────────────────────────────\n\n/** An answer the support team reuses. Inserted into a reply, then edited, never sent as is. */\nexport const savedReplySchema = z.object({\n id: objectIdSchema,\n title: z.string(),\n body: z.string(),\n author: person,\n updatedAt: z.iso.datetime(),\n viewer: z.object({ canEdit: z.boolean(), canDelete: z.boolean() }),\n});\nexport type SavedReply = z.infer<typeof savedReplySchema>;\n\n/** Every saved reply, by title. Bounded by `SAVED_REPLIES_MAX`, so not paged. */\nexport const savedReplyListSchema = z.object({ items: z.array(savedReplySchema) });\nexport type SavedReplyList = z.infer<typeof savedReplyListSchema>;\n\n/** `:replyId` in the paths above. */\nexport const savedReplyParamsSchema = z.object({ replyId: objectIdSchema });\nexport type SavedReplyParams = z.infer<typeof savedReplyParamsSchema>;\n\n/** `POST` and `PUT /support/saved-replies`. */\nexport const savedReplyBodySchema = z.object({\n title: z.string().trim().min(1).max(SAVED_REPLY_TITLE_MAX),\n body: z.string().trim().min(1).max(SAVED_REPLY_BODY_MAX),\n});\nexport type SavedReplyBody = z.infer<typeof savedReplyBodySchema>;\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: the staff team
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/team the people holding a platform role
|
|
5
|
+
* PUT /api/v1/team/:memberId/roles set someone's platform roles
|
|
6
|
+
* DELETE /api/v1/team/:memberId/mpin clear a forgotten MPIN (superadmin;
|
|
7
|
+
* answers `IdResponse`)
|
|
8
|
+
*
|
|
9
|
+
* Setting roles sends the person's WHOLE set, so the request says what they
|
|
10
|
+
* should end up with rather than a list of changes to replay. Every role that
|
|
11
|
+
* changes must be one the actor's active role may grant (`ROLE_GRANTED_BY`);
|
|
12
|
+
* nobody changes their own; superadmin is never in a request. Each change is
|
|
13
|
+
* written to the audit log.
|
|
14
|
+
*
|
|
15
|
+
* @module founder-schema/domains/team
|
|
16
|
+
*/
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
/** `:memberId` in the paths above. */
|
|
19
|
+
export declare const teamMemberParamsSchema: z.ZodObject<{
|
|
20
|
+
memberId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
export type TeamMemberParams = z.infer<typeof teamMemberParamsSchema>;
|
|
23
|
+
/** One person on the team, as the Team screens list them. */
|
|
24
|
+
export declare const teamMemberSchema: z.ZodObject<{
|
|
25
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
26
|
+
displayName: z.ZodString;
|
|
27
|
+
avatarUrl: z.ZodNullable<z.ZodURL>;
|
|
28
|
+
email: z.ZodNullable<z.ZodString>;
|
|
29
|
+
roles: z.ZodArray<z.ZodEnum<{
|
|
30
|
+
superadmin: "superadmin";
|
|
31
|
+
manager: "manager";
|
|
32
|
+
moderator: "moderator";
|
|
33
|
+
support_agent: "support_agent";
|
|
34
|
+
}>>;
|
|
35
|
+
mpinSet: z.ZodBoolean;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
export type TeamMember = z.infer<typeof teamMemberSchema>;
|
|
38
|
+
export declare const teamPageSchema: z.ZodObject<{
|
|
39
|
+
items: z.ZodArray<z.ZodObject<{
|
|
40
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
41
|
+
displayName: z.ZodString;
|
|
42
|
+
avatarUrl: z.ZodNullable<z.ZodURL>;
|
|
43
|
+
email: z.ZodNullable<z.ZodString>;
|
|
44
|
+
roles: z.ZodArray<z.ZodEnum<{
|
|
45
|
+
superadmin: "superadmin";
|
|
46
|
+
manager: "manager";
|
|
47
|
+
moderator: "moderator";
|
|
48
|
+
support_agent: "support_agent";
|
|
49
|
+
}>>;
|
|
50
|
+
mpinSet: z.ZodBoolean;
|
|
51
|
+
}, z.core.$strip>>;
|
|
52
|
+
nextCursor: z.ZodNullable<z.ZodString>;
|
|
53
|
+
}, z.core.$strip>;
|
|
54
|
+
export type TeamPage = z.infer<typeof teamPageSchema>;
|
|
55
|
+
/**
|
|
56
|
+
* `PUT /team/:memberId/roles`: the platform roles the person should hold.
|
|
57
|
+
* Empty takes every staff role away. No duplicates; no superadmin (a server
|
|
58
|
+
* script grants it, never the product).
|
|
59
|
+
*/
|
|
60
|
+
export declare const setMemberRolesBodySchema: z.ZodObject<{
|
|
61
|
+
roles: z.ZodArray<z.ZodEnum<{
|
|
62
|
+
manager: "manager";
|
|
63
|
+
moderator: "moderator";
|
|
64
|
+
support_agent: "support_agent";
|
|
65
|
+
}>>;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
export type SetMemberRolesBody = z.infer<typeof setMemberRolesBodySchema>;
|
|
68
|
+
/** What `PUT /team/:memberId/roles` answers: the roles the person now holds, highest first. */
|
|
69
|
+
export declare const setMemberRolesResponseSchema: z.ZodObject<{
|
|
70
|
+
id: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
71
|
+
roles: z.ZodArray<z.ZodEnum<{
|
|
72
|
+
superadmin: "superadmin";
|
|
73
|
+
manager: "manager";
|
|
74
|
+
moderator: "moderator";
|
|
75
|
+
support_agent: "support_agent";
|
|
76
|
+
}>>;
|
|
77
|
+
}, z.core.$strip>;
|
|
78
|
+
export type SetMemberRolesResponse = z.infer<typeof setMemberRolesResponseSchema>;
|
|
79
|
+
//# sourceMappingURL=team.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../../src/domains/team/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,sCAAsC;AACtC,eAAO,MAAM,sBAAsB;;iBAAyC,CAAC;AAC7E,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAEtE,6DAA6D;AAC7D,eAAO,MAAM,gBAAgB;;;;;;;;;;;;iBAS3B,CAAC;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;iBAAoC,CAAC;AAChE,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;;;;;iBAOnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E,+FAA+F;AAC/F,eAAO,MAAM,4BAA4B;;;;;;;;iBAGvC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,4BAA4B,CAAC,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: the staff team
|
|
3
|
+
*
|
|
4
|
+
* GET /api/v1/team the people holding a platform role
|
|
5
|
+
* PUT /api/v1/team/:memberId/roles set someone's platform roles
|
|
6
|
+
* DELETE /api/v1/team/:memberId/mpin clear a forgotten MPIN (superadmin;
|
|
7
|
+
* answers `IdResponse`)
|
|
8
|
+
*
|
|
9
|
+
* Setting roles sends the person's WHOLE set, so the request says what they
|
|
10
|
+
* should end up with rather than a list of changes to replay. Every role that
|
|
11
|
+
* changes must be one the actor's active role may grant (`ROLE_GRANTED_BY`);
|
|
12
|
+
* nobody changes their own; superadmin is never in a request. Each change is
|
|
13
|
+
* written to the audit log.
|
|
14
|
+
*
|
|
15
|
+
* @module founder-schema/domains/team
|
|
16
|
+
*/
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { PLATFORM_ROLE_VALUES, platformRoleSchema } from '../../access/roles.js';
|
|
19
|
+
import { objectIdSchema } from '../../core/common.js';
|
|
20
|
+
import { paginatedSchema } from '../../core/responses.js';
|
|
21
|
+
import { VALIDATION_MESSAGE } from '../../core/validation.js';
|
|
22
|
+
/** `:memberId` in the paths above. */
|
|
23
|
+
export const teamMemberParamsSchema = z.object({ memberId: objectIdSchema });
|
|
24
|
+
/** One person on the team, as the Team screens list them. */
|
|
25
|
+
export const teamMemberSchema = z.object({
|
|
26
|
+
id: objectIdSchema,
|
|
27
|
+
displayName: z.string(),
|
|
28
|
+
/** Server-set or from the login provider, like every avatar on the wire: a plain URL check. */
|
|
29
|
+
avatarUrl: z.url().nullable(),
|
|
30
|
+
email: z.string().nullable(),
|
|
31
|
+
/** Their platform roles, highest first. Never empty in this list. */
|
|
32
|
+
roles: z.array(platformRoleSchema).min(1),
|
|
33
|
+
mpinSet: z.boolean(),
|
|
34
|
+
});
|
|
35
|
+
export const teamPageSchema = paginatedSchema(teamMemberSchema);
|
|
36
|
+
/**
|
|
37
|
+
* `PUT /team/:memberId/roles`: the platform roles the person should hold.
|
|
38
|
+
* Empty takes every staff role away. No duplicates; no superadmin (a server
|
|
39
|
+
* script grants it, never the product).
|
|
40
|
+
*/
|
|
41
|
+
export const setMemberRolesBodySchema = z.object({
|
|
42
|
+
roles: z
|
|
43
|
+
.array(platformRoleSchema.exclude(['superadmin']))
|
|
44
|
+
.max(PLATFORM_ROLE_VALUES.length)
|
|
45
|
+
.refine((roles) => new Set(roles).size === roles.length, {
|
|
46
|
+
error: VALIDATION_MESSAGE.rolesDuplicate,
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
/** What `PUT /team/:memberId/roles` answers: the roles the person now holds, highest first. */
|
|
50
|
+
export const setMemberRolesResponseSchema = z.object({
|
|
51
|
+
id: objectIdSchema,
|
|
52
|
+
roles: z.array(platformRoleSchema),
|
|
53
|
+
});
|
|
54
|
+
//# sourceMappingURL=team.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"team.js","sourceRoot":"","sources":["../../../src/domains/team/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D,sCAAsC;AACtC,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC;AAG7E,6DAA6D;AAC7D,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE;IACvB,+FAA+F;IAC/F,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;IAC7B,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC5B,qEAAqE;IACrE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACzC,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;CACrB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,cAAc,GAAG,eAAe,CAAC,gBAAgB,CAAC,CAAC;AAGhE;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/C,KAAK,EAAE,CAAC;SACL,KAAK,CAAC,kBAAkB,CAAC,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC;SACjD,GAAG,CAAC,oBAAoB,CAAC,MAAM,CAAC;SAChC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,MAAM,EAAE;QACvD,KAAK,EAAE,kBAAkB,CAAC,cAAc;KACzC,CAAC;CACL,CAAC,CAAC;AAGH,+FAA+F;AAC/F,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,CAAC,MAAM,CAAC;IACnD,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,kBAAkB,CAAC;CACnC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema: the staff team\n *\n * GET /api/v1/team the people holding a platform role\n * PUT /api/v1/team/:memberId/roles set someone's platform roles\n * DELETE /api/v1/team/:memberId/mpin clear a forgotten MPIN (superadmin;\n * answers `IdResponse`)\n *\n * Setting roles sends the person's WHOLE set, so the request says what they\n * should end up with rather than a list of changes to replay. Every role that\n * changes must be one the actor's active role may grant (`ROLE_GRANTED_BY`);\n * nobody changes their own; superadmin is never in a request. Each change is\n * written to the audit log.\n *\n * @module founder-schema/domains/team\n */\n\nimport { z } from 'zod';\nimport { PLATFORM_ROLE_VALUES, platformRoleSchema } from '../../access/roles.js';\nimport { objectIdSchema } from '../../core/common.js';\nimport { paginatedSchema } from '../../core/responses.js';\nimport { VALIDATION_MESSAGE } from '../../core/validation.js';\n\n/** `:memberId` in the paths above. */\nexport const teamMemberParamsSchema = z.object({ memberId: objectIdSchema });\nexport type TeamMemberParams = z.infer<typeof teamMemberParamsSchema>;\n\n/** One person on the team, as the Team screens list them. */\nexport const teamMemberSchema = z.object({\n id: objectIdSchema,\n displayName: z.string(),\n /** Server-set or from the login provider, like every avatar on the wire: a plain URL check. */\n avatarUrl: z.url().nullable(),\n email: z.string().nullable(),\n /** Their platform roles, highest first. Never empty in this list. */\n roles: z.array(platformRoleSchema).min(1),\n mpinSet: z.boolean(),\n});\nexport type TeamMember = z.infer<typeof teamMemberSchema>;\n\nexport const teamPageSchema = paginatedSchema(teamMemberSchema);\nexport type TeamPage = z.infer<typeof teamPageSchema>;\n\n/**\n * `PUT /team/:memberId/roles`: the platform roles the person should hold.\n * Empty takes every staff role away. No duplicates; no superadmin (a server\n * script grants it, never the product).\n */\nexport const setMemberRolesBodySchema = z.object({\n roles: z\n .array(platformRoleSchema.exclude(['superadmin']))\n .max(PLATFORM_ROLE_VALUES.length)\n .refine((roles) => new Set(roles).size === roles.length, {\n error: VALIDATION_MESSAGE.rolesDuplicate,\n }),\n});\nexport type SetMemberRolesBody = z.infer<typeof setMemberRolesBodySchema>;\n\n/** What `PUT /team/:memberId/roles` answers: the roles the person now holds, highest first. */\nexport const setMemberRolesResponseSchema = z.object({\n id: objectIdSchema,\n roles: z.array(platformRoleSchema),\n});\nexport type SetMemberRolesResponse = z.infer<typeof setMemberRolesResponseSchema>;\n"]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Uploads: the key a client carries, and asking for a slot
|
|
3
|
+
*
|
|
4
|
+
* Its own module so that both `feed` (a post's media) and `onboarding` (the
|
|
5
|
+
* avatar) can use the one key rule. It lived in `feed` until 0.2.0, and
|
|
6
|
+
* `onboarding` could not import it from there without a cycle (`feed` imports
|
|
7
|
+
* `onboarding` for the topic list), so the avatar key was a bare string and
|
|
8
|
+
* `'../../etc/passwd'` passed.
|
|
9
|
+
*
|
|
10
|
+
* Imports only the limits and the message keys.
|
|
11
|
+
*
|
|
12
|
+
* @module founder-schema/domains/uploads
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
/**
|
|
16
|
+
* ⚠ IMAGES ONLY, and that is a product decision rather than an oversight.
|
|
17
|
+
*
|
|
18
|
+
* The asymmetry was real and was raised: `feedVideoSchema` is in the read union
|
|
19
|
+
* and a card can render a video, while nothing could ever upload one. The user
|
|
20
|
+
* settled it on 2026-09-21: members upload images, and share video by putting a
|
|
21
|
+
* link in the post text, which the client renders inline. So the minter stays
|
|
22
|
+
* images-only by intent.
|
|
23
|
+
*
|
|
24
|
+
* `feedVideoSchema` is deliberately KEPT. Widening the write path later is
|
|
25
|
+
* additive; removing video from the read union once a client handles it is not.
|
|
26
|
+
* A test named for refusing a video key holds this visible, so whoever adds
|
|
27
|
+
* video uploads has to delete a test that says out loud why it exists.
|
|
28
|
+
*/
|
|
29
|
+
export declare const UPLOAD_KEY_EXTENSIONS: readonly ["jpg", "jpeg", "png", "webp", "gif"];
|
|
30
|
+
export declare const uploadKeyExtensionSchema: z.ZodEnum<{
|
|
31
|
+
jpg: "jpg";
|
|
32
|
+
jpeg: "jpeg";
|
|
33
|
+
png: "png";
|
|
34
|
+
webp: "webp";
|
|
35
|
+
gif: "gif";
|
|
36
|
+
}>;
|
|
37
|
+
export type UploadKeyExtension = z.infer<typeof uploadKeyExtensionSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* The content type each extension is signed for, and the only types an
|
|
40
|
+
* attached object may turn out to have. The server signs the PUT with this
|
|
41
|
+
* type and refuses to attach an object whose stored type is anything else.
|
|
42
|
+
*/
|
|
43
|
+
export declare const UPLOAD_CONTENT_TYPES: {
|
|
44
|
+
readonly jpg: "image/jpeg";
|
|
45
|
+
readonly jpeg: "image/jpeg";
|
|
46
|
+
readonly png: "image/png";
|
|
47
|
+
readonly webp: "image/webp";
|
|
48
|
+
readonly gif: "image/gif";
|
|
49
|
+
};
|
|
50
|
+
export declare const UPLOAD_KEY_MAX_CHARS = 300;
|
|
51
|
+
/**
|
|
52
|
+
* An attachment on the way IN is a KEY, never a URL. FORMAT, minted
|
|
53
|
+
* server-side and never by a client:
|
|
54
|
+
*
|
|
55
|
+
* media/<userId>/<uuid>.<ext>
|
|
56
|
+
*
|
|
57
|
+
* Built from `UPLOAD_KEY_EXTENSIONS`, so the extension list exists once. The
|
|
58
|
+
* backend reads the owner segment with this same pattern.
|
|
59
|
+
*
|
|
60
|
+
* ⚠ THIS IS THE FIRST OF THREE CHECKS AND THE WEAKEST. It proves the key is
|
|
61
|
+
* well FORMED. It cannot prove the two things that actually matter:
|
|
62
|
+
*
|
|
63
|
+
* 1. OWNERSHIP. The `userId` segment is what makes "is this your object"
|
|
64
|
+
* checkable, and the backend compares it against the authenticated caller.
|
|
65
|
+
* A pattern happily accepts a well-formed key belonging to somebody else.
|
|
66
|
+
* 2. EXISTENCE. The backend reads the object before attaching it, so a key
|
|
67
|
+
* for an object nobody uploaded cannot be committed.
|
|
68
|
+
*/
|
|
69
|
+
export declare const UPLOAD_KEY_PATTERN: RegExp;
|
|
70
|
+
/**
|
|
71
|
+
* The length ceiling and the traversal refusal are redundant against the
|
|
72
|
+
* pattern, which is anchored and admits no dots outside the extension. They are
|
|
73
|
+
* kept on purpose: they are what still holds if the pattern is ever loosened.
|
|
74
|
+
*/
|
|
75
|
+
export declare const uploadKeySchema: z.ZodString;
|
|
76
|
+
export type UploadKey = z.infer<typeof uploadKeySchema>;
|
|
77
|
+
/**
|
|
78
|
+
* Asking for an upload slot: `POST /api/v1/uploads/presign`.
|
|
79
|
+
*
|
|
80
|
+
* The server chooses the key (so it carries the caller's id), the content type
|
|
81
|
+
* and the expiry; a client that could name its own key could aim it at
|
|
82
|
+
* someone else's folder.
|
|
83
|
+
*
|
|
84
|
+
* `contentLength` is the file's exact size in bytes, and it is SIGNED INTO the
|
|
85
|
+
* URL: storage refuses a PUT whose body is any other size. Until 0.2.0 nothing
|
|
86
|
+
* capped the size at upload, so a member could put 40 MB into the bucket and
|
|
87
|
+
* only learn at attach time that it was refused.
|
|
88
|
+
*/
|
|
89
|
+
export declare const uploadPresignBodySchema: z.ZodObject<{
|
|
90
|
+
extension: z.ZodEnum<{
|
|
91
|
+
jpg: "jpg";
|
|
92
|
+
jpeg: "jpeg";
|
|
93
|
+
png: "png";
|
|
94
|
+
webp: "webp";
|
|
95
|
+
gif: "gif";
|
|
96
|
+
}>;
|
|
97
|
+
contentLength: z.ZodNumber;
|
|
98
|
+
}, z.core.$strip>;
|
|
99
|
+
export type UploadPresignBody = z.infer<typeof uploadPresignBodySchema>;
|
|
100
|
+
/**
|
|
101
|
+
* The upload slot. `headers` are part of the signature, not advice: send every
|
|
102
|
+
* one of them verbatim on the PUT, with a body of exactly `contentLength`
|
|
103
|
+
* bytes, or storage answers with a signature mismatch. `key` is what a create
|
|
104
|
+
* body then carries, exactly as given.
|
|
105
|
+
*/
|
|
106
|
+
export declare const uploadPresignResponseSchema: z.ZodObject<{
|
|
107
|
+
key: z.ZodString;
|
|
108
|
+
url: z.ZodURL;
|
|
109
|
+
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
110
|
+
expiresIn: z.ZodNumber;
|
|
111
|
+
}, z.core.$strip>;
|
|
112
|
+
export type UploadPresignResponse = z.infer<typeof uploadPresignResponseSchema>;
|
|
113
|
+
//# sourceMappingURL=uploads.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploads.d.ts","sourceRoot":"","sources":["../../../src/domains/uploads/uploads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,gDAAiD,CAAC;AACpF,eAAO,MAAM,wBAAwB;;;;;;EAAgC,CAAC;AACtE,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;CAMiC,CAAC;AAEnE,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,QAE9B,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,eAAe,aAMuD,CAAC;AACpF,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;iBAGlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B;;;;;iBAMtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Uploads: the key a client carries, and asking for a slot
|
|
3
|
+
*
|
|
4
|
+
* Its own module so that both `feed` (a post's media) and `onboarding` (the
|
|
5
|
+
* avatar) can use the one key rule. It lived in `feed` until 0.2.0, and
|
|
6
|
+
* `onboarding` could not import it from there without a cycle (`feed` imports
|
|
7
|
+
* `onboarding` for the topic list), so the avatar key was a bare string and
|
|
8
|
+
* `'../../etc/passwd'` passed.
|
|
9
|
+
*
|
|
10
|
+
* Imports only the limits and the message keys.
|
|
11
|
+
*
|
|
12
|
+
* @module founder-schema/domains/uploads
|
|
13
|
+
*/
|
|
14
|
+
import { z } from 'zod';
|
|
15
|
+
import { UPLOAD_MAX_BYTES } from '../../core/limits.js';
|
|
16
|
+
import { VALIDATION_MESSAGE } from '../../core/validation.js';
|
|
17
|
+
/**
|
|
18
|
+
* ⚠ IMAGES ONLY, and that is a product decision rather than an oversight.
|
|
19
|
+
*
|
|
20
|
+
* The asymmetry was real and was raised: `feedVideoSchema` is in the read union
|
|
21
|
+
* and a card can render a video, while nothing could ever upload one. The user
|
|
22
|
+
* settled it on 2026-09-21: members upload images, and share video by putting a
|
|
23
|
+
* link in the post text, which the client renders inline. So the minter stays
|
|
24
|
+
* images-only by intent.
|
|
25
|
+
*
|
|
26
|
+
* `feedVideoSchema` is deliberately KEPT. Widening the write path later is
|
|
27
|
+
* additive; removing video from the read union once a client handles it is not.
|
|
28
|
+
* A test named for refusing a video key holds this visible, so whoever adds
|
|
29
|
+
* video uploads has to delete a test that says out loud why it exists.
|
|
30
|
+
*/
|
|
31
|
+
export const UPLOAD_KEY_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
|
32
|
+
export const uploadKeyExtensionSchema = z.enum(UPLOAD_KEY_EXTENSIONS);
|
|
33
|
+
/**
|
|
34
|
+
* The content type each extension is signed for, and the only types an
|
|
35
|
+
* attached object may turn out to have. The server signs the PUT with this
|
|
36
|
+
* type and refuses to attach an object whose stored type is anything else.
|
|
37
|
+
*/
|
|
38
|
+
export const UPLOAD_CONTENT_TYPES = {
|
|
39
|
+
jpg: 'image/jpeg',
|
|
40
|
+
jpeg: 'image/jpeg',
|
|
41
|
+
png: 'image/png',
|
|
42
|
+
webp: 'image/webp',
|
|
43
|
+
gif: 'image/gif',
|
|
44
|
+
};
|
|
45
|
+
export const UPLOAD_KEY_MAX_CHARS = 300;
|
|
46
|
+
/**
|
|
47
|
+
* An attachment on the way IN is a KEY, never a URL. FORMAT, minted
|
|
48
|
+
* server-side and never by a client:
|
|
49
|
+
*
|
|
50
|
+
* media/<userId>/<uuid>.<ext>
|
|
51
|
+
*
|
|
52
|
+
* Built from `UPLOAD_KEY_EXTENSIONS`, so the extension list exists once. The
|
|
53
|
+
* backend reads the owner segment with this same pattern.
|
|
54
|
+
*
|
|
55
|
+
* ⚠ THIS IS THE FIRST OF THREE CHECKS AND THE WEAKEST. It proves the key is
|
|
56
|
+
* well FORMED. It cannot prove the two things that actually matter:
|
|
57
|
+
*
|
|
58
|
+
* 1. OWNERSHIP. The `userId` segment is what makes "is this your object"
|
|
59
|
+
* checkable, and the backend compares it against the authenticated caller.
|
|
60
|
+
* A pattern happily accepts a well-formed key belonging to somebody else.
|
|
61
|
+
* 2. EXISTENCE. The backend reads the object before attaching it, so a key
|
|
62
|
+
* for an object nobody uploaded cannot be committed.
|
|
63
|
+
*/
|
|
64
|
+
export const UPLOAD_KEY_PATTERN = new RegExp(`^media/[0-9a-f]{24}/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.(${UPLOAD_KEY_EXTENSIONS.join('|')})$`);
|
|
65
|
+
/**
|
|
66
|
+
* The length ceiling and the traversal refusal are redundant against the
|
|
67
|
+
* pattern, which is anchored and admits no dots outside the extension. They are
|
|
68
|
+
* kept on purpose: they are what still holds if the pattern is ever loosened.
|
|
69
|
+
*/
|
|
70
|
+
export const uploadKeySchema = z
|
|
71
|
+
.string()
|
|
72
|
+
.trim()
|
|
73
|
+
.min(1)
|
|
74
|
+
.max(UPLOAD_KEY_MAX_CHARS)
|
|
75
|
+
.regex(UPLOAD_KEY_PATTERN, VALIDATION_MESSAGE.uploadKeyInvalid)
|
|
76
|
+
.refine((k) => !k.includes('..'), { error: VALIDATION_MESSAGE.uploadKeyInvalid });
|
|
77
|
+
/**
|
|
78
|
+
* Asking for an upload slot: `POST /api/v1/uploads/presign`.
|
|
79
|
+
*
|
|
80
|
+
* The server chooses the key (so it carries the caller's id), the content type
|
|
81
|
+
* and the expiry; a client that could name its own key could aim it at
|
|
82
|
+
* someone else's folder.
|
|
83
|
+
*
|
|
84
|
+
* `contentLength` is the file's exact size in bytes, and it is SIGNED INTO the
|
|
85
|
+
* URL: storage refuses a PUT whose body is any other size. Until 0.2.0 nothing
|
|
86
|
+
* capped the size at upload, so a member could put 40 MB into the bucket and
|
|
87
|
+
* only learn at attach time that it was refused.
|
|
88
|
+
*/
|
|
89
|
+
export const uploadPresignBodySchema = z.object({
|
|
90
|
+
extension: uploadKeyExtensionSchema,
|
|
91
|
+
contentLength: z.number().int().min(1).max(UPLOAD_MAX_BYTES),
|
|
92
|
+
});
|
|
93
|
+
/**
|
|
94
|
+
* The upload slot. `headers` are part of the signature, not advice: send every
|
|
95
|
+
* one of them verbatim on the PUT, with a body of exactly `contentLength`
|
|
96
|
+
* bytes, or storage answers with a signature mismatch. `key` is what a create
|
|
97
|
+
* body then carries, exactly as given.
|
|
98
|
+
*/
|
|
99
|
+
export const uploadPresignResponseSchema = z.object({
|
|
100
|
+
key: uploadKeySchema,
|
|
101
|
+
url: z.url(),
|
|
102
|
+
headers: z.record(z.string(), z.string()),
|
|
103
|
+
/** Seconds the URL stays valid. */
|
|
104
|
+
expiresIn: z.number().int().positive(),
|
|
105
|
+
});
|
|
106
|
+
//# sourceMappingURL=uploads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploads.js","sourceRoot":"","sources":["../../../src/domains/uploads/uploads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAE9D;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC;AAGtE;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,GAAG,EAAE,YAAY;IACjB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;IAChB,IAAI,EAAE,YAAY;IAClB,GAAG,EAAE,WAAW;CACgD,CAAC;AAEnE,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,MAAM,CAC1C,uFAAuF,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAC3H,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC;KAC7B,MAAM,EAAE;KACR,IAAI,EAAE;KACN,GAAG,CAAC,CAAC,CAAC;KACN,GAAG,CAAC,oBAAoB,CAAC;KACzB,KAAK,CAAC,kBAAkB,EAAE,kBAAkB,CAAC,gBAAgB,CAAC;KAC9D,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,EAAE,kBAAkB,CAAC,gBAAgB,EAAE,CAAC,CAAC;AAGpF;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,SAAS,EAAE,wBAAwB;IACnC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC;CAC7D,CAAC,CAAC;AAGH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,GAAG,EAAE,eAAe;IACpB,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE;IACZ,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;IACzC,mCAAmC;IACnC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Uploads: the key a client carries, and asking for a slot\n *\n * Its own module so that both `feed` (a post's media) and `onboarding` (the\n * avatar) can use the one key rule. It lived in `feed` until 0.2.0, and\n * `onboarding` could not import it from there without a cycle (`feed` imports\n * `onboarding` for the topic list), so the avatar key was a bare string and\n * `'../../etc/passwd'` passed.\n *\n * Imports only the limits and the message keys.\n *\n * @module founder-schema/domains/uploads\n */\n\nimport { z } from 'zod';\nimport { UPLOAD_MAX_BYTES } from '../../core/limits.js';\nimport { VALIDATION_MESSAGE } from '../../core/validation.js';\n\n/**\n * ⚠ IMAGES ONLY, and that is a product decision rather than an oversight.\n *\n * The asymmetry was real and was raised: `feedVideoSchema` is in the read union\n * and a card can render a video, while nothing could ever upload one. The user\n * settled it on 2026-09-21: members upload images, and share video by putting a\n * link in the post text, which the client renders inline. So the minter stays\n * images-only by intent.\n *\n * `feedVideoSchema` is deliberately KEPT. Widening the write path later is\n * additive; removing video from the read union once a client handles it is not.\n * A test named for refusing a video key holds this visible, so whoever adds\n * video uploads has to delete a test that says out loud why it exists.\n */\nexport const UPLOAD_KEY_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] as const;\nexport const uploadKeyExtensionSchema = z.enum(UPLOAD_KEY_EXTENSIONS);\nexport type UploadKeyExtension = z.infer<typeof uploadKeyExtensionSchema>;\n\n/**\n * The content type each extension is signed for, and the only types an\n * attached object may turn out to have. The server signs the PUT with this\n * type and refuses to attach an object whose stored type is anything else.\n */\nexport const UPLOAD_CONTENT_TYPES = {\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n webp: 'image/webp',\n gif: 'image/gif',\n} as const satisfies Record<UploadKeyExtension, `image/${string}`>;\n\nexport const UPLOAD_KEY_MAX_CHARS = 300;\n\n/**\n * An attachment on the way IN is a KEY, never a URL. FORMAT, minted\n * server-side and never by a client:\n *\n * media/<userId>/<uuid>.<ext>\n *\n * Built from `UPLOAD_KEY_EXTENSIONS`, so the extension list exists once. The\n * backend reads the owner segment with this same pattern.\n *\n * ⚠ THIS IS THE FIRST OF THREE CHECKS AND THE WEAKEST. It proves the key is\n * well FORMED. It cannot prove the two things that actually matter:\n *\n * 1. OWNERSHIP. The `userId` segment is what makes \"is this your object\"\n * checkable, and the backend compares it against the authenticated caller.\n * A pattern happily accepts a well-formed key belonging to somebody else.\n * 2. EXISTENCE. The backend reads the object before attaching it, so a key\n * for an object nobody uploaded cannot be committed.\n */\nexport const UPLOAD_KEY_PATTERN = new RegExp(\n `^media/[0-9a-f]{24}/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\\\.(${UPLOAD_KEY_EXTENSIONS.join('|')})$`,\n);\n\n/**\n * The length ceiling and the traversal refusal are redundant against the\n * pattern, which is anchored and admits no dots outside the extension. They are\n * kept on purpose: they are what still holds if the pattern is ever loosened.\n */\nexport const uploadKeySchema = z\n .string()\n .trim()\n .min(1)\n .max(UPLOAD_KEY_MAX_CHARS)\n .regex(UPLOAD_KEY_PATTERN, VALIDATION_MESSAGE.uploadKeyInvalid)\n .refine((k) => !k.includes('..'), { error: VALIDATION_MESSAGE.uploadKeyInvalid });\nexport type UploadKey = z.infer<typeof uploadKeySchema>;\n\n/**\n * Asking for an upload slot: `POST /api/v1/uploads/presign`.\n *\n * The server chooses the key (so it carries the caller's id), the content type\n * and the expiry; a client that could name its own key could aim it at\n * someone else's folder.\n *\n * `contentLength` is the file's exact size in bytes, and it is SIGNED INTO the\n * URL: storage refuses a PUT whose body is any other size. Until 0.2.0 nothing\n * capped the size at upload, so a member could put 40 MB into the bucket and\n * only learn at attach time that it was refused.\n */\nexport const uploadPresignBodySchema = z.object({\n extension: uploadKeyExtensionSchema,\n contentLength: z.number().int().min(1).max(UPLOAD_MAX_BYTES),\n});\nexport type UploadPresignBody = z.infer<typeof uploadPresignBodySchema>;\n\n/**\n * The upload slot. `headers` are part of the signature, not advice: send every\n * one of them verbatim on the PUT, with a body of exactly `contentLength`\n * bytes, or storage answers with a signature mismatch. `key` is what a create\n * body then carries, exactly as given.\n */\nexport const uploadPresignResponseSchema = z.object({\n key: uploadKeySchema,\n url: z.url(),\n headers: z.record(z.string(), z.string()),\n /** Seconds the URL stays valid. */\n expiresIn: z.number().int().positive(),\n});\nexport type UploadPresignResponse = z.infer<typeof uploadPresignResponseSchema>;\n"]}
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema/env — how an environment variable is checked.
|
|
3
|
+
*
|
|
4
|
+
* Each Founder Circle app lists ITS OWN variables (configuration belongs to the
|
|
5
|
+
* app: adding one never needs a schema publish, and the web never learns the
|
|
6
|
+
* backend's setting names) and builds each one from the fields below, so the
|
|
7
|
+
* rules live here once and no app writes a validation rule of its own:
|
|
8
|
+
*
|
|
9
|
+
* import { envVar, readEnv } from 'founder-schema/env';
|
|
10
|
+
* export const env = readEnv(
|
|
11
|
+
* { PORT: envVar.port(6010), MONGODB_URI: envVar.mongoUri() },
|
|
12
|
+
* process.env,
|
|
13
|
+
* );
|
|
14
|
+
*
|
|
15
|
+
* The rules every field follows:
|
|
16
|
+
* - EMPTY IS UNSET. `FOO=` in a file or an unset variable both mean "not
|
|
17
|
+
* given", so a blank line in an env file cannot pass for a value.
|
|
18
|
+
* - A FALLBACK IS FOR LOCAL DEVELOPMENT ONLY. A deployment sets every value
|
|
19
|
+
* from its own secret store; never "fix" a deployed mismatch by changing a
|
|
20
|
+
* fallback, which would make the wrong value the default everywhere.
|
|
21
|
+
* - FAIL FAST AND SAY WHICH. `readEnv` reports every bad variable by NAME,
|
|
22
|
+
* never by value (a value can be a secret), so a misconfigured process
|
|
23
|
+
* stops at start-up rather than three layers down.
|
|
24
|
+
*
|
|
25
|
+
* A separate entry point so a browser bundle never loads it unless it asks.
|
|
26
|
+
*
|
|
27
|
+
* @module founder-schema/env
|
|
28
|
+
*/
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
/** The field builders. Each returns the rule for one variable. */
|
|
31
|
+
export declare const envVar: {
|
|
32
|
+
/** Required text. With `fallback`, the text used when unset. */
|
|
33
|
+
readonly text: (fallback?: string) => z.ZodPreprocess<z.ZodString, unknown> | z.ZodPreprocess<z.ZodDefault<z.ZodString>, unknown>;
|
|
34
|
+
/** Text that may be unset (`undefined`). */
|
|
35
|
+
readonly optionalText: () => z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
|
|
36
|
+
/** A URL. With `fallback`, the URL used when unset. */
|
|
37
|
+
readonly url: (fallback?: string) => z.ZodPreprocess<z.ZodURL, unknown> | z.ZodPreprocess<z.ZodDefault<z.ZodURL>, unknown>;
|
|
38
|
+
/** A URL that may be unset (`undefined`). */
|
|
39
|
+
readonly optionalUrl: () => z.ZodPreprocess<z.ZodOptional<z.ZodURL>, unknown>;
|
|
40
|
+
/** A MongoDB connection string. Always required: there is no safe default database. */
|
|
41
|
+
readonly mongoUri: () => z.ZodPreprocess<z.ZodString, unknown>;
|
|
42
|
+
/** A TCP port. */
|
|
43
|
+
readonly port: (fallback: number) => z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
|
|
44
|
+
/** A whole number above zero: a count, a duration in milliseconds. */
|
|
45
|
+
readonly positiveInt: (fallback: number) => z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* On or off: `true`/`false`, `1`/`0` or `yes`/`no`, in any case. Anything
|
|
48
|
+
* else is refused, so a typo cannot quietly turn a safeguard off.
|
|
49
|
+
*/
|
|
50
|
+
readonly flag: (fallback: boolean) => z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodPipe<z.ZodString, z.ZodTransform<boolean, string>>>, z.ZodTransform<boolean, boolean | undefined>>, unknown>;
|
|
51
|
+
/** One of a fixed set of words. */
|
|
52
|
+
readonly oneOf: <const T extends readonly [string, ...string[]]>(values: T, fallback?: T[number]) => z.ZodPreprocess<z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>, unknown> | z.ZodPreprocess<z.ZodDefault<z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never>>, unknown>;
|
|
53
|
+
/** Comma-separated text, as a list with blanks dropped: `a, b,` is `['a', 'b']`. */
|
|
54
|
+
readonly list: (fallback: string) => z.ZodPreprocess<z.ZodPipe<z.ZodDefault<z.ZodString>, z.ZodTransform<string[], string>>, unknown>;
|
|
55
|
+
/** A required secret of at least `minLength` characters. */
|
|
56
|
+
readonly secret: (minLength: number) => z.ZodPreprocess<z.ZodString, unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* A secret that may be unset, which then reads as `''` so the feature it
|
|
59
|
+
* guards can check one value and switch itself off. When set it must be at
|
|
60
|
+
* least `minLength` characters: a short secret is a mistake, not a choice.
|
|
61
|
+
*/
|
|
62
|
+
readonly optionalSecret: (minLength: number) => z.ZodPreprocess<z.ZodPipe<z.ZodOptional<z.ZodString>, z.ZodTransform<string, string | undefined>>, unknown>;
|
|
63
|
+
};
|
|
64
|
+
/** One variable that failed, by name. Never carries the value. */
|
|
65
|
+
export interface EnvProblem {
|
|
66
|
+
variable: string;
|
|
67
|
+
problem: string;
|
|
68
|
+
}
|
|
69
|
+
/** Thrown by `readEnv` when any variable is missing or wrong. */
|
|
70
|
+
export declare class EnvError extends Error {
|
|
71
|
+
readonly problems: EnvProblem[];
|
|
72
|
+
constructor(problems: EnvProblem[]);
|
|
73
|
+
}
|
|
74
|
+
/** What `readEnv` returns: every variable as its field's output. */
|
|
75
|
+
export type EnvValues<F extends Record<string, z.ZodType>> = {
|
|
76
|
+
[K in keyof F]: z.output<F[K]>;
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* Check every variable in `fields` against `source` (normally `process.env`)
|
|
80
|
+
* and return them typed. Throws `EnvError` naming EVERY bad variable at once,
|
|
81
|
+
* so a misconfiguration is fixed in one pass, not one restart per variable.
|
|
82
|
+
* Variables in `source` that `fields` does not list are ignored.
|
|
83
|
+
*/
|
|
84
|
+
export declare function readEnv<F extends Record<string, z.ZodType>>(fields: F, source: Record<string, string | undefined>): EnvValues<F>;
|
|
85
|
+
//# sourceMappingURL=env.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAWxB,kEAAkE;AAClE,eAAO,MAAM,MAAM;IACjB,gEAAgE;+BAChD,MAAM;IAMtB,4CAA4C;;IAK5C,uDAAuD;8BACxC,MAAM;IAKrB,6CAA6C;;IAK7C,uFAAuF;;IAWvF,kBAAkB;8BACH,MAAM;IAIrB,sEAAsE;qCAChD,MAAM;IAI5B;;;OAGG;8BACY,OAAO;IAgBtB,mCAAmC;2BACvB,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC;IAKrF,oFAAoF;8BACrE,MAAM;IAcrB,4DAA4D;iCAC1C,MAAM;IAQxB;;;;OAIG;yCACuB,MAAM;CASxB,CAAC;AAEX,kEAAkE;AAClE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,iEAAiE;AACjE,qBAAa,QAAS,SAAQ,KAAK;IACjC,QAAQ,CAAC,QAAQ,EAAE,UAAU,EAAE,CAAC;gBAEpB,QAAQ,EAAE,UAAU,EAAE;CAOnC;AAED,oEAAoE;AACpE,MAAM,MAAM,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEhG;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EACzD,MAAM,EAAE,CAAC,EACT,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GACzC,SAAS,CAAC,CAAC,CAAC,CASd"}
|