founder-schema 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -16
- 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 +117 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +139 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/limits.d.ts +122 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +132 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/responses.d.ts +207 -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 +146 -0
- package/dist/core/validation.d.ts.map +1 -0
- package/dist/core/validation.js +152 -0
- package/dist/core/validation.js.map +1 -0
- package/dist/domains/account/account.d.ts +42 -0
- package/dist/domains/account/account.d.ts.map +1 -0
- package/dist/domains/account/account.js +41 -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 +1695 -0
- package/dist/domains/feed/feed.d.ts.map +1 -0
- package/dist/domains/feed/feed.js +626 -0
- package/dist/domains/feed/feed.js.map +1 -0
- package/dist/domains/media/media.d.ts +134 -0
- package/dist/domains/media/media.d.ts.map +1 -0
- package/dist/domains/media/media.js +110 -0
- package/dist/domains/media/media.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 +411 -0
- package/dist/domains/profile/founder-profile.d.ts.map +1 -0
- package/dist/domains/profile/founder-profile.js +202 -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 +101 -0
- package/dist/domains/uploads/uploads.d.ts.map +1 -0
- package/dist/domains/uploads/uploads.js +89 -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 +22 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +32 -19
- 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,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema: support
|
|
3
|
+
*
|
|
4
|
+
* A member asks for help; a support agent answers. One conversation per
|
|
5
|
+
* problem, a ticket, with its messages in order.
|
|
6
|
+
*
|
|
7
|
+
* POST /api/v1/support/tickets { subject, category, body }
|
|
8
|
+
* GET /api/v1/support/tickets ?scope&status&assignee&limit&cursor
|
|
9
|
+
* GET /api/v1/support/tickets/:ticketId
|
|
10
|
+
* POST /api/v1/support/tickets/:ticketId/messages { body } reply
|
|
11
|
+
* POST /api/v1/support/tickets/:ticketId/claim an agent takes it
|
|
12
|
+
* POST /api/v1/support/tickets/:ticketId/resolve closed as solved
|
|
13
|
+
* POST /api/v1/support/tickets/:ticketId/reopen the member reopens
|
|
14
|
+
*
|
|
15
|
+
* GET /api/v1/support/saved-replies
|
|
16
|
+
* POST /api/v1/support/saved-replies { title, body }
|
|
17
|
+
* PUT /api/v1/support/saved-replies/:replyId { title, body }
|
|
18
|
+
* DELETE /api/v1/support/saved-replies/:replyId
|
|
19
|
+
*
|
|
20
|
+
* THE STATES
|
|
21
|
+
* open waiting on support: new, or the member wrote last
|
|
22
|
+
* waiting waiting on the member: an agent wrote last
|
|
23
|
+
* resolved closed; the member can reopen it, and writing reopens it
|
|
24
|
+
*
|
|
25
|
+
* `scope=mine` is the tickets the asker raised, whatever role they act as;
|
|
26
|
+
* `scope=queue` is every ticket the active role may read (all of them for a
|
|
27
|
+
* support agent or manager, only their own for a member). An agent replying to
|
|
28
|
+
* an unclaimed ticket claims it.
|
|
29
|
+
*
|
|
30
|
+
* Every message is free text from a person: untrusted, shown as text.
|
|
31
|
+
*
|
|
32
|
+
* @module founder-schema/domains/support
|
|
33
|
+
*/
|
|
34
|
+
import { z } from 'zod';
|
|
35
|
+
import { emptyStringToUndefined, objectIdSchema, paginationQuerySchema, } from '../../core/common.js';
|
|
36
|
+
import { SAVED_REPLY_BODY_MAX, SAVED_REPLY_TITLE_MAX, TICKET_MESSAGE_MAX, TICKET_SUBJECT_MAX, TICKET_SUBJECT_MIN, } from '../../core/limits.js';
|
|
37
|
+
import { paginatedSchema } from '../../core/responses.js';
|
|
38
|
+
/** What the problem is about: routes it, and lets the team see patterns. Append only. */
|
|
39
|
+
export const TICKET_CATEGORY_VALUES = [
|
|
40
|
+
'account',
|
|
41
|
+
'profile',
|
|
42
|
+
'feed',
|
|
43
|
+
'chat',
|
|
44
|
+
'safety',
|
|
45
|
+
'bug',
|
|
46
|
+
'other',
|
|
47
|
+
];
|
|
48
|
+
export const ticketCategorySchema = z.enum(TICKET_CATEGORY_VALUES);
|
|
49
|
+
export const TICKET_STATUS_VALUES = ['open', 'waiting', 'resolved'];
|
|
50
|
+
export const ticketStatusSchema = z.enum(TICKET_STATUS_VALUES);
|
|
51
|
+
export const TICKET_SCOPE_VALUES = ['mine', 'queue'];
|
|
52
|
+
export const ticketScopeSchema = z.enum(TICKET_SCOPE_VALUES);
|
|
53
|
+
/** The queue's assignee filter: mine, nobody's, or anyone's. */
|
|
54
|
+
export const TICKET_ASSIGNEE_FILTER_VALUES = ['me', 'unassigned', 'any'];
|
|
55
|
+
export const ticketAssigneeFilterSchema = z.enum(TICKET_ASSIGNEE_FILTER_VALUES);
|
|
56
|
+
const person = z.object({ id: objectIdSchema, displayName: z.string() });
|
|
57
|
+
/** One message. `from` says which side wrote it; the agent's name is shown to the member. */
|
|
58
|
+
export const ticketMessageSchema = z.object({
|
|
59
|
+
id: objectIdSchema,
|
|
60
|
+
from: z.enum(['member', 'agent']),
|
|
61
|
+
author: person,
|
|
62
|
+
body: z.string(),
|
|
63
|
+
at: z.iso.datetime(),
|
|
64
|
+
});
|
|
65
|
+
/** A ticket in a list. */
|
|
66
|
+
export const ticketSummarySchema = z.object({
|
|
67
|
+
id: objectIdSchema,
|
|
68
|
+
subject: z.string(),
|
|
69
|
+
category: ticketCategorySchema,
|
|
70
|
+
status: ticketStatusSchema,
|
|
71
|
+
member: person.extend({ avatarUrl: z.url().nullable() }),
|
|
72
|
+
assignee: person.nullable(),
|
|
73
|
+
messageCount: z.number().int().min(1),
|
|
74
|
+
createdAt: z.iso.datetime(),
|
|
75
|
+
lastMessageAt: z.iso.datetime(),
|
|
76
|
+
/** Who wrote last, so a list can say "Support replied" or "Waiting on support". */
|
|
77
|
+
lastFrom: z.enum(['member', 'agent']),
|
|
78
|
+
});
|
|
79
|
+
export const ticketPageSchema = paginatedSchema(ticketSummarySchema);
|
|
80
|
+
/** A ticket with its whole conversation and what the asker may do with it. */
|
|
81
|
+
export const ticketDetailSchema = ticketSummarySchema.extend({
|
|
82
|
+
messages: z.array(ticketMessageSchema).min(1),
|
|
83
|
+
resolvedAt: z.iso.datetime().nullable(),
|
|
84
|
+
viewer: z.object({
|
|
85
|
+
canReply: z.boolean(),
|
|
86
|
+
canClaim: z.boolean(),
|
|
87
|
+
canResolve: z.boolean(),
|
|
88
|
+
canReopen: z.boolean(),
|
|
89
|
+
/** Replying as the agent side (the reply is shown as Support's). */
|
|
90
|
+
asAgent: z.boolean(),
|
|
91
|
+
}),
|
|
92
|
+
});
|
|
93
|
+
/** `:ticketId` in the paths above. */
|
|
94
|
+
export const ticketParamsSchema = z.object({ ticketId: objectIdSchema });
|
|
95
|
+
/** `GET /support/tickets`. Mine by default; status and assignee narrow it. */
|
|
96
|
+
export const ticketListQuerySchema = paginationQuerySchema.extend({
|
|
97
|
+
scope: z.preprocess(emptyStringToUndefined, ticketScopeSchema.default('mine')),
|
|
98
|
+
status: z.preprocess(emptyStringToUndefined, ticketStatusSchema.optional()),
|
|
99
|
+
assignee: z.preprocess(emptyStringToUndefined, ticketAssigneeFilterSchema.default('any')),
|
|
100
|
+
});
|
|
101
|
+
const messageBody = z.string().trim().min(1).max(TICKET_MESSAGE_MAX);
|
|
102
|
+
/** `POST /support/tickets`: what went wrong, in the member's words. */
|
|
103
|
+
export const createTicketBodySchema = z.object({
|
|
104
|
+
subject: z.string().trim().min(TICKET_SUBJECT_MIN).max(TICKET_SUBJECT_MAX),
|
|
105
|
+
category: ticketCategorySchema,
|
|
106
|
+
body: messageBody,
|
|
107
|
+
});
|
|
108
|
+
/** `POST /support/tickets/:ticketId/messages`. */
|
|
109
|
+
export const replyTicketBodySchema = z.object({ body: messageBody });
|
|
110
|
+
// ─── Saved replies ───────────────────────────────────────────────────────────
|
|
111
|
+
/** An answer the support team reuses. Inserted into a reply, then edited, never sent as is. */
|
|
112
|
+
export const savedReplySchema = z.object({
|
|
113
|
+
id: objectIdSchema,
|
|
114
|
+
title: z.string(),
|
|
115
|
+
body: z.string(),
|
|
116
|
+
author: person,
|
|
117
|
+
updatedAt: z.iso.datetime(),
|
|
118
|
+
viewer: z.object({ canEdit: z.boolean(), canDelete: z.boolean() }),
|
|
119
|
+
});
|
|
120
|
+
/** Every saved reply, by title. Bounded by `SAVED_REPLIES_MAX`, so not paged. */
|
|
121
|
+
export const savedReplyListSchema = z.object({ items: z.array(savedReplySchema) });
|
|
122
|
+
/** `:replyId` in the paths above. */
|
|
123
|
+
export const savedReplyParamsSchema = z.object({ replyId: objectIdSchema });
|
|
124
|
+
/** `POST` and `PUT /support/saved-replies`. */
|
|
125
|
+
export const savedReplyBodySchema = z.object({
|
|
126
|
+
title: z.string().trim().min(1).max(SAVED_REPLY_TITLE_MAX),
|
|
127
|
+
body: z.string().trim().min(1).max(SAVED_REPLY_BODY_MAX),
|
|
128
|
+
});
|
|
129
|
+
//# sourceMappingURL=support.js.map
|
|
@@ -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,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Uploads: asking for a slot, and finishing one
|
|
3
|
+
*
|
|
4
|
+
* The routes, in order (the whole pipeline is described in `media`):
|
|
5
|
+
*
|
|
6
|
+
* POST /api/v1/uploads/presign -> a media id and a presigned PUT URL
|
|
7
|
+
* PUT <url> -> the bytes, straight to storage
|
|
8
|
+
* POST /api/v1/uploads/:mediaId/complete -> the processed image
|
|
9
|
+
* DELETE /api/v1/uploads/:mediaId -> discard an unattached upload
|
|
10
|
+
*
|
|
11
|
+
* Its own module so that `feed`, `onboarding` and `profile` can all depend on
|
|
12
|
+
* it without a cycle.
|
|
13
|
+
*
|
|
14
|
+
* @module founder-schema/domains/uploads
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
/**
|
|
18
|
+
* File extensions a picker offers, and the content type each is uploaded as.
|
|
19
|
+
* For a file input's `accept`, and for a file whose `type` the browser left
|
|
20
|
+
* empty. Images only: video is shared as a link in the post text, a product
|
|
21
|
+
* decision of 2026-09-21, not an unfinished uploader.
|
|
22
|
+
*/
|
|
23
|
+
export declare const UPLOAD_KEY_EXTENSIONS: readonly ["jpg", "jpeg", "png", "webp", "gif"];
|
|
24
|
+
export declare const uploadKeyExtensionSchema: z.ZodEnum<{
|
|
25
|
+
jpg: "jpg";
|
|
26
|
+
jpeg: "jpeg";
|
|
27
|
+
png: "png";
|
|
28
|
+
webp: "webp";
|
|
29
|
+
gif: "gif";
|
|
30
|
+
}>;
|
|
31
|
+
export type UploadKeyExtension = z.infer<typeof uploadKeyExtensionSchema>;
|
|
32
|
+
export declare const UPLOAD_CONTENT_TYPES: {
|
|
33
|
+
readonly jpg: "image/jpeg";
|
|
34
|
+
readonly jpeg: "image/jpeg";
|
|
35
|
+
readonly png: "image/png";
|
|
36
|
+
readonly webp: "image/webp";
|
|
37
|
+
readonly gif: "image/gif";
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* @deprecated Since 0.5.0 no route takes or returns a storage key: a client
|
|
41
|
+
* carries a media id (`objectIdSchema`). Kept only because this package never
|
|
42
|
+
* removes an export without a ruling (TEAM.md 5a, `public-surface.test.ts`).
|
|
43
|
+
*/
|
|
44
|
+
export declare const UPLOAD_KEY_MAX_CHARS = 300;
|
|
45
|
+
/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. The 0.4.0 key format, `media/<userId>/<uuid>.<ext>`. */
|
|
46
|
+
export declare const UPLOAD_KEY_PATTERN: RegExp;
|
|
47
|
+
/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. */
|
|
48
|
+
export declare const uploadKeySchema: z.ZodString;
|
|
49
|
+
/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. */
|
|
50
|
+
export type UploadKey = z.infer<typeof uploadKeySchema>;
|
|
51
|
+
/**
|
|
52
|
+
* Asking for an upload slot: `POST /api/v1/uploads/presign`.
|
|
53
|
+
*
|
|
54
|
+
* The server chooses where the file goes (a folder named for the caller,
|
|
55
|
+
* which no CDN can read) and how long the URL lives; the client says only what
|
|
56
|
+
* it is for and what it is sending.
|
|
57
|
+
*
|
|
58
|
+
* `contentType` and `contentLength` are SIGNED INTO the URL: storage refuses a
|
|
59
|
+
* PUT with any other type or any other number of bytes. `contentLength` is the
|
|
60
|
+
* exact size of the bytes about to be sent, after any client-side shrinking.
|
|
61
|
+
*/
|
|
62
|
+
export declare const uploadPresignBodySchema: z.ZodObject<{
|
|
63
|
+
purpose: z.ZodEnum<{
|
|
64
|
+
post: "post";
|
|
65
|
+
avatar: "avatar";
|
|
66
|
+
}>;
|
|
67
|
+
contentType: z.ZodEnum<{
|
|
68
|
+
"image/jpeg": "image/jpeg";
|
|
69
|
+
"image/png": "image/png";
|
|
70
|
+
"image/webp": "image/webp";
|
|
71
|
+
"image/gif": "image/gif";
|
|
72
|
+
}>;
|
|
73
|
+
contentLength: z.ZodNumber;
|
|
74
|
+
}, z.core.$strip>;
|
|
75
|
+
export type UploadPresignBody = z.infer<typeof uploadPresignBodySchema>;
|
|
76
|
+
/**
|
|
77
|
+
* The upload slot.
|
|
78
|
+
*
|
|
79
|
+
* Send `method` to `url` with every one of `headers` verbatim and a body of
|
|
80
|
+
* exactly `contentLength` bytes; the headers are part of the signature, not
|
|
81
|
+
* advice, and anything else is a signature mismatch. Then complete `mediaId`.
|
|
82
|
+
* The URL works for `expiresIn` seconds. It is not single-use, which is why the
|
|
83
|
+
* server reads the file exactly once, when it claims the upload to process it.
|
|
84
|
+
*
|
|
85
|
+
* Completing answers 409 `UPLOAD_PROCESSING` if another request is already
|
|
86
|
+
* processing the same upload; complete again in a moment.
|
|
87
|
+
*/
|
|
88
|
+
export declare const uploadPresignResponseSchema: z.ZodObject<{
|
|
89
|
+
mediaId: z.ZodString;
|
|
90
|
+
url: z.ZodURL;
|
|
91
|
+
method: z.ZodLiteral<"PUT">;
|
|
92
|
+
headers: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
93
|
+
expiresIn: z.ZodNumber;
|
|
94
|
+
}, z.core.$strip>;
|
|
95
|
+
export type UploadPresignResponse = z.infer<typeof uploadPresignResponseSchema>;
|
|
96
|
+
/** `:mediaId` on `/uploads/:mediaId/complete` and `DELETE /uploads/:mediaId`. */
|
|
97
|
+
export declare const uploadParamsSchema: z.ZodObject<{
|
|
98
|
+
mediaId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
|
|
99
|
+
}, z.core.$strip>;
|
|
100
|
+
export type UploadParams = z.infer<typeof uploadParamsSchema>;
|
|
101
|
+
//# 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;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB;;;;;GAKG;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,eAAO,MAAM,oBAAoB;;;;;;CAMiC,CAAC;AAEnE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,MAAM,CAAC;AAExC,mGAAmG;AACnG,eAAO,MAAM,kBAAkB,QAE9B,CAAC;AAEF,8CAA8C;AAC9C,eAAO,MAAM,eAAe,aAMuD,CAAC;AACpF,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,eAAe,CAAC,CAAC;AAExD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,uBAAuB;;;;;;;;;;;;iBAIlC,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,uBAAuB,CAAC,CAAC;AAExE;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,2BAA2B;;;;;;iBAMtC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,2BAA2B,CAAC,CAAC;AAEhF,iFAAiF;AACjF,eAAO,MAAM,kBAAkB;;iBAAwC,CAAC;AACxE,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* founder-schema — Uploads: asking for a slot, and finishing one
|
|
3
|
+
*
|
|
4
|
+
* The routes, in order (the whole pipeline is described in `media`):
|
|
5
|
+
*
|
|
6
|
+
* POST /api/v1/uploads/presign -> a media id and a presigned PUT URL
|
|
7
|
+
* PUT <url> -> the bytes, straight to storage
|
|
8
|
+
* POST /api/v1/uploads/:mediaId/complete -> the processed image
|
|
9
|
+
* DELETE /api/v1/uploads/:mediaId -> discard an unattached upload
|
|
10
|
+
*
|
|
11
|
+
* Its own module so that `feed`, `onboarding` and `profile` can all depend on
|
|
12
|
+
* it without a cycle.
|
|
13
|
+
*
|
|
14
|
+
* @module founder-schema/domains/uploads
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { objectIdSchema } from '../../core/common.js';
|
|
18
|
+
import { UPLOAD_MAX_BYTES } from '../../core/limits.js';
|
|
19
|
+
import { VALIDATION_MESSAGE } from '../../core/validation.js';
|
|
20
|
+
import { mediaPurposeSchema, mediaUploadContentTypeSchema } from '../media/media.js';
|
|
21
|
+
/**
|
|
22
|
+
* File extensions a picker offers, and the content type each is uploaded as.
|
|
23
|
+
* For a file input's `accept`, and for a file whose `type` the browser left
|
|
24
|
+
* empty. Images only: video is shared as a link in the post text, a product
|
|
25
|
+
* decision of 2026-09-21, not an unfinished uploader.
|
|
26
|
+
*/
|
|
27
|
+
export const UPLOAD_KEY_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
|
|
28
|
+
export const uploadKeyExtensionSchema = z.enum(UPLOAD_KEY_EXTENSIONS);
|
|
29
|
+
export const UPLOAD_CONTENT_TYPES = {
|
|
30
|
+
jpg: 'image/jpeg',
|
|
31
|
+
jpeg: 'image/jpeg',
|
|
32
|
+
png: 'image/png',
|
|
33
|
+
webp: 'image/webp',
|
|
34
|
+
gif: 'image/gif',
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* @deprecated Since 0.5.0 no route takes or returns a storage key: a client
|
|
38
|
+
* carries a media id (`objectIdSchema`). Kept only because this package never
|
|
39
|
+
* removes an export without a ruling (TEAM.md 5a, `public-surface.test.ts`).
|
|
40
|
+
*/
|
|
41
|
+
export const UPLOAD_KEY_MAX_CHARS = 300;
|
|
42
|
+
/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. The 0.4.0 key format, `media/<userId>/<uuid>.<ext>`. */
|
|
43
|
+
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('|')})$`);
|
|
44
|
+
/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. */
|
|
45
|
+
export const uploadKeySchema = z
|
|
46
|
+
.string()
|
|
47
|
+
.trim()
|
|
48
|
+
.min(1)
|
|
49
|
+
.max(UPLOAD_KEY_MAX_CHARS)
|
|
50
|
+
.regex(UPLOAD_KEY_PATTERN, VALIDATION_MESSAGE.uploadKeyInvalid)
|
|
51
|
+
.refine((k) => !k.includes('..'), { error: VALIDATION_MESSAGE.uploadKeyInvalid });
|
|
52
|
+
/**
|
|
53
|
+
* Asking for an upload slot: `POST /api/v1/uploads/presign`.
|
|
54
|
+
*
|
|
55
|
+
* The server chooses where the file goes (a folder named for the caller,
|
|
56
|
+
* which no CDN can read) and how long the URL lives; the client says only what
|
|
57
|
+
* it is for and what it is sending.
|
|
58
|
+
*
|
|
59
|
+
* `contentType` and `contentLength` are SIGNED INTO the URL: storage refuses a
|
|
60
|
+
* PUT with any other type or any other number of bytes. `contentLength` is the
|
|
61
|
+
* exact size of the bytes about to be sent, after any client-side shrinking.
|
|
62
|
+
*/
|
|
63
|
+
export const uploadPresignBodySchema = z.object({
|
|
64
|
+
purpose: mediaPurposeSchema,
|
|
65
|
+
contentType: mediaUploadContentTypeSchema,
|
|
66
|
+
contentLength: z.number().int().min(1).max(UPLOAD_MAX_BYTES),
|
|
67
|
+
});
|
|
68
|
+
/**
|
|
69
|
+
* The upload slot.
|
|
70
|
+
*
|
|
71
|
+
* Send `method` to `url` with every one of `headers` verbatim and a body of
|
|
72
|
+
* exactly `contentLength` bytes; the headers are part of the signature, not
|
|
73
|
+
* advice, and anything else is a signature mismatch. Then complete `mediaId`.
|
|
74
|
+
* The URL works for `expiresIn` seconds. It is not single-use, which is why the
|
|
75
|
+
* server reads the file exactly once, when it claims the upload to process it.
|
|
76
|
+
*
|
|
77
|
+
* Completing answers 409 `UPLOAD_PROCESSING` if another request is already
|
|
78
|
+
* processing the same upload; complete again in a moment.
|
|
79
|
+
*/
|
|
80
|
+
export const uploadPresignResponseSchema = z.object({
|
|
81
|
+
mediaId: z.string(),
|
|
82
|
+
url: z.url(),
|
|
83
|
+
method: z.literal('PUT'),
|
|
84
|
+
headers: z.record(z.string(), z.string()),
|
|
85
|
+
expiresIn: z.number().int().positive(),
|
|
86
|
+
});
|
|
87
|
+
/** `:mediaId` on `/uploads/:mediaId/complete` and `DELETE /uploads/:mediaId`. */
|
|
88
|
+
export const uploadParamsSchema = z.object({ mediaId: objectIdSchema });
|
|
89
|
+
//# sourceMappingURL=uploads.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uploads.js","sourceRoot":"","sources":["../../../src/domains/uploads/uploads.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,kBAAkB,EAAE,4BAA4B,EAAE,MAAM,mBAAmB,CAAC;AAErF;;;;;GAKG;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,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;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,GAAG,CAAC;AAExC,mGAAmG;AACnG,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,MAAM,CAC1C,uFAAuF,qBAAqB,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAC3H,CAAC;AAEF,8CAA8C;AAC9C,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;AAIpF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC9C,OAAO,EAAE,kBAAkB;IAC3B,WAAW,EAAE,4BAA4B;IACzC,aAAa,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC;CAC7D,CAAC,CAAC;AAGH;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE;IACZ,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACxB,OAAO,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC;IACzC,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE;CACvC,CAAC,CAAC;AAGH,iFAAiF;AACjF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC","sourcesContent":["/**\n * founder-schema — Uploads: asking for a slot, and finishing one\n *\n * The routes, in order (the whole pipeline is described in `media`):\n *\n * POST /api/v1/uploads/presign -> a media id and a presigned PUT URL\n * PUT <url> -> the bytes, straight to storage\n * POST /api/v1/uploads/:mediaId/complete -> the processed image\n * DELETE /api/v1/uploads/:mediaId -> discard an unattached upload\n *\n * Its own module so that `feed`, `onboarding` and `profile` can all depend on\n * it without a cycle.\n *\n * @module founder-schema/domains/uploads\n */\n\nimport { z } from 'zod';\nimport { objectIdSchema } from '../../core/common.js';\nimport { UPLOAD_MAX_BYTES } from '../../core/limits.js';\nimport { VALIDATION_MESSAGE } from '../../core/validation.js';\nimport { mediaPurposeSchema, mediaUploadContentTypeSchema } from '../media/media.js';\n\n/**\n * File extensions a picker offers, and the content type each is uploaded as.\n * For a file input's `accept`, and for a file whose `type` the browser left\n * empty. Images only: video is shared as a link in the post text, a product\n * decision of 2026-09-21, not an unfinished uploader.\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\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\n/**\n * @deprecated Since 0.5.0 no route takes or returns a storage key: a client\n * carries a media id (`objectIdSchema`). Kept only because this package never\n * removes an export without a ruling (TEAM.md 5a, `public-surface.test.ts`).\n */\nexport const UPLOAD_KEY_MAX_CHARS = 300;\n\n/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. The 0.4.0 key format, `media/<userId>/<uuid>.<ext>`. */\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/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. */\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 });\n/** @deprecated See `UPLOAD_KEY_MAX_CHARS`. */\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 where the file goes (a folder named for the caller,\n * which no CDN can read) and how long the URL lives; the client says only what\n * it is for and what it is sending.\n *\n * `contentType` and `contentLength` are SIGNED INTO the URL: storage refuses a\n * PUT with any other type or any other number of bytes. `contentLength` is the\n * exact size of the bytes about to be sent, after any client-side shrinking.\n */\nexport const uploadPresignBodySchema = z.object({\n purpose: mediaPurposeSchema,\n contentType: mediaUploadContentTypeSchema,\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.\n *\n * Send `method` to `url` with every one of `headers` verbatim and a body of\n * exactly `contentLength` bytes; the headers are part of the signature, not\n * advice, and anything else is a signature mismatch. Then complete `mediaId`.\n * The URL works for `expiresIn` seconds. It is not single-use, which is why the\n * server reads the file exactly once, when it claims the upload to process it.\n *\n * Completing answers 409 `UPLOAD_PROCESSING` if another request is already\n * processing the same upload; complete again in a moment.\n */\nexport const uploadPresignResponseSchema = z.object({\n mediaId: z.string(),\n url: z.url(),\n method: z.literal('PUT'),\n headers: z.record(z.string(), z.string()),\n expiresIn: z.number().int().positive(),\n});\nexport type UploadPresignResponse = z.infer<typeof uploadPresignResponseSchema>;\n\n/** `:mediaId` on `/uploads/:mediaId/complete` and `DELETE /uploads/:mediaId`. */\nexport const uploadParamsSchema = z.object({ mediaId: objectIdSchema });\nexport type UploadParams = z.infer<typeof uploadParamsSchema>;\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"}
|