founder-schema 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/README.md +125 -17
  2. package/dist/access/access.d.ts +1 -1
  3. package/dist/access/access.d.ts.map +1 -1
  4. package/dist/access/access.js +4 -1
  5. package/dist/access/access.js.map +1 -1
  6. package/dist/access/permissions.d.ts +27 -29
  7. package/dist/access/permissions.d.ts.map +1 -1
  8. package/dist/access/permissions.js +39 -3
  9. package/dist/access/permissions.js.map +1 -1
  10. package/dist/core/common.d.ts +47 -10
  11. package/dist/core/common.d.ts.map +1 -1
  12. package/dist/core/common.js +42 -13
  13. package/dist/core/common.js.map +1 -1
  14. package/dist/core/errors.d.ts +7 -5
  15. package/dist/core/errors.d.ts.map +1 -1
  16. package/dist/core/errors.js +17 -11
  17. package/dist/core/errors.js.map +1 -1
  18. package/dist/core/limits.d.ts +48 -17
  19. package/dist/core/limits.d.ts.map +1 -1
  20. package/dist/core/limits.js +52 -22
  21. package/dist/core/limits.js.map +1 -1
  22. package/dist/core/money.d.ts +33 -0
  23. package/dist/core/money.d.ts.map +1 -0
  24. package/dist/core/money.js +36 -0
  25. package/dist/core/money.js.map +1 -0
  26. package/dist/core/responses.d.ts +14 -4
  27. package/dist/core/responses.d.ts.map +1 -1
  28. package/dist/core/responses.js.map +1 -1
  29. package/dist/core/validation.d.ts +6 -2
  30. package/dist/core/validation.d.ts.map +1 -1
  31. package/dist/core/validation.js +6 -2
  32. package/dist/core/validation.js.map +1 -1
  33. package/dist/domains/billing/billing.d.ts +61 -8
  34. package/dist/domains/billing/billing.d.ts.map +1 -1
  35. package/dist/domains/billing/billing.js +58 -22
  36. package/dist/domains/billing/billing.js.map +1 -1
  37. package/dist/domains/billing/gst.d.ts.map +1 -1
  38. package/dist/domains/billing/gst.js +4 -3
  39. package/dist/domains/billing/gst.js.map +1 -1
  40. package/dist/domains/chat/chat.d.ts +0 -21
  41. package/dist/domains/chat/chat.d.ts.map +1 -1
  42. package/dist/domains/chat/chat.js +0 -20
  43. package/dist/domains/chat/chat.js.map +1 -1
  44. package/dist/domains/communities/communities.d.ts +30 -12
  45. package/dist/domains/communities/communities.d.ts.map +1 -1
  46. package/dist/domains/communities/communities.js +43 -15
  47. package/dist/domains/communities/communities.js.map +1 -1
  48. package/dist/domains/contributions/contributions.d.ts +1 -1
  49. package/dist/domains/contributions/contributions.js +1 -1
  50. package/dist/domains/contributions/contributions.js.map +1 -1
  51. package/dist/domains/dashboard/dashboard.d.ts +3 -1
  52. package/dist/domains/dashboard/dashboard.d.ts.map +1 -1
  53. package/dist/domains/dashboard/dashboard.js +23 -7
  54. package/dist/domains/dashboard/dashboard.js.map +1 -1
  55. package/dist/domains/discover/discover.d.ts +8 -8
  56. package/dist/domains/feed/feed.d.ts +55 -153
  57. package/dist/domains/feed/feed.d.ts.map +1 -1
  58. package/dist/domains/feed/feed.js +53 -58
  59. package/dist/domains/feed/feed.js.map +1 -1
  60. package/dist/domains/location/india.d.ts +14 -4
  61. package/dist/domains/location/india.d.ts.map +1 -1
  62. package/dist/domains/location/india.js +36 -8
  63. package/dist/domains/location/india.js.map +1 -1
  64. package/dist/domains/members/members.d.ts +2 -2
  65. package/dist/domains/membership/membership.d.ts +26 -0
  66. package/dist/domains/membership/membership.d.ts.map +1 -1
  67. package/dist/domains/membership/membership.js +55 -1
  68. package/dist/domains/membership/membership.js.map +1 -1
  69. package/dist/domains/moderation/moderation.d.ts +2 -2
  70. package/dist/domains/moderation/moderation.js +3 -3
  71. package/dist/domains/moderation/moderation.js.map +1 -1
  72. package/dist/domains/notifications/notifications.d.ts +1 -10
  73. package/dist/domains/notifications/notifications.d.ts.map +1 -1
  74. package/dist/domains/notifications/notifications.js +2 -3
  75. package/dist/domains/notifications/notifications.js.map +1 -1
  76. package/dist/domains/onboarding/onboarding-answers.d.ts +33 -2
  77. package/dist/domains/onboarding/onboarding-answers.d.ts.map +1 -1
  78. package/dist/domains/onboarding/onboarding-answers.js +38 -15
  79. package/dist/domains/onboarding/onboarding-answers.js.map +1 -1
  80. package/dist/domains/onboarding/onboarding.d.ts +229 -57
  81. package/dist/domains/onboarding/onboarding.d.ts.map +1 -1
  82. package/dist/domains/onboarding/onboarding.js +34 -41
  83. package/dist/domains/onboarding/onboarding.js.map +1 -1
  84. package/dist/domains/products/products.d.ts +43 -8
  85. package/dist/domains/products/products.d.ts.map +1 -1
  86. package/dist/domains/products/products.js +57 -11
  87. package/dist/domains/products/products.js.map +1 -1
  88. package/dist/domains/profile/founder-profile.d.ts +68 -104
  89. package/dist/domains/profile/founder-profile.d.ts.map +1 -1
  90. package/dist/domains/profile/founder-profile.js +66 -32
  91. package/dist/domains/profile/founder-profile.js.map +1 -1
  92. package/dist/domains/profile/my-profile.d.ts +3 -3
  93. package/dist/domains/reviews/reviews.d.ts +1 -1
  94. package/dist/domains/reviews/reviews.d.ts.map +1 -1
  95. package/dist/domains/reviews/reviews.js +2 -2
  96. package/dist/domains/reviews/reviews.js.map +1 -1
  97. package/dist/domains/support/support.d.ts +22 -3
  98. package/dist/domains/support/support.d.ts.map +1 -1
  99. package/dist/domains/support/support.js +19 -5
  100. package/dist/domains/support/support.js.map +1 -1
  101. package/dist/domains/team/team.d.ts.map +1 -1
  102. package/dist/domains/team/team.js +3 -6
  103. package/dist/domains/team/team.js.map +1 -1
  104. package/dist/domains/uploads/uploads.d.ts +7 -22
  105. package/dist/domains/uploads/uploads.d.ts.map +1 -1
  106. package/dist/domains/uploads/uploads.js +7 -21
  107. package/dist/domains/uploads/uploads.js.map +1 -1
  108. package/dist/env.d.ts +26 -13
  109. package/dist/env.d.ts.map +1 -1
  110. package/dist/env.js +58 -23
  111. package/dist/env.js.map +1 -1
  112. package/dist/index.d.ts +18 -17
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +18 -16
  115. package/dist/index.js.map +1 -1
  116. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"support.js","sourceRoot":"","sources":["../../../src/domains/support/support.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,sBAAsB,EACtB,cAAc,EACd,qBAAqB,GACtB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EACL,oBAAoB,EACpB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,eAAe,EAAE,MAAM,yBAAyB,CAAC;AAE1D,yFAAyF;AACzF,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,SAAS;IACT,SAAS;IACT,MAAM;IACN,MAAM;IACN,QAAQ;IACR,KAAK;IACL,OAAO;IACP,uEAAuE;IACvE,uCAAuC;IACvC,SAAS;IACT,UAAU;IACV,aAAa;CACL,CAAC;AACX,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAC;AAGnE,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,CAAU,CAAC;AAC7E,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;AAG/D,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAAM,EAAE,OAAO,CAAU,CAAC;AAC9D,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC,CAAC;AAG7D,gEAAgE;AAChE,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,CAAU,CAAC;AAClF,MAAM,CAAC,MAAM,0BAA0B,GAAG,CAAC,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;AAGhF,MAAM,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAEzE,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,EAAE,EAAE,cAAc;IAClB,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IACjC,MAAM,EAAE,MAAM;IACd,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;CACrB,CAAC,CAAC;AAGH,0BAA0B;AAC1B,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1C,EAAE,EAAE,cAAc;IAClB,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE;IACnB,QAAQ,EAAE,oBAAoB;IAC9B,MAAM,EAAE,kBAAkB;IAC1B,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC;IACxD,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE;IAC3B,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC3B,aAAa,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC/B,mFAAmF;IACnF,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;CACtC,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,gBAAgB,GAAG,eAAe,CAAC,mBAAmB,CAAC,CAAC;AAGrE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,kBAAkB,GAAG,mBAAmB,CAAC,MAAM,CAAC;IAC3D,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,mBAAmB,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,QAAQ,EAAE;IACvC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC;QACf,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;QACrB,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE;QACrB,UAAU,EAAE,CAAC,CAAC,OAAO,EAAE;QACvB,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE;QACtB,oEAAoE;QACpE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE;KACrB,CAAC;CACH,CAAC,CAAC;AAGH,sCAAsC;AACtC,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,CAAC;AAGzE,8EAA8E;AAC9E,MAAM,CAAC,MAAM,qBAAqB,GAAG,qBAAqB,CAAC,MAAM,CAAC;IAChE,KAAK,EAAE,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,iBAAiB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAC9E,MAAM,EAAE,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,kBAAkB,CAAC,QAAQ,EAAE,CAAC;IAC3E,QAAQ,EAAE,CAAC,CAAC,UAAU,CAAC,sBAAsB,EAAE,0BAA0B,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;CAC1F,CAAC,CAAC;AAIH,MAAM,WAAW,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;AAErE,uEAAuE;AACvE,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC7C,OAAO,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC;IAC1E,QAAQ,EAAE,oBAAoB;IAC9B,IAAI,EAAE,WAAW;CAClB,CAAC,CAAC;AAGH,kDAAkD;AAClD,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;AAGrE,gFAAgF;AAEhF,+FAA+F;AAC/F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,cAAc;IAClB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE;IAChB,MAAM,EAAE,MAAM;IACd,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE;IAC3B,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;CACnE,CAAC,CAAC;AAGH,iFAAiF;AACjF,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC,CAAC;AAGnF,qCAAqC;AACrC,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC,CAAC;AAG5E,+CAA+C;AAC/C,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC3C,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,qBAAqB,CAAC;IAC1D,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC;CACzD,CAAC,CAAC","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 // 0.13.0: payments and refunds (a refund is asked for through Help and\n // support), products, and communities.\n 'billing',\n 'products',\n 'communities',\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"]}
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, and they ARE whose turn it is (0.18.0: a list worded the turn\n * from `lastFrom`, so a reopened ticket read \"Waiting on member\" while it\n * waited on support):\n * open waiting on support: new, reopened, 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 // 0.13.0: payments and refunds (a refund is asked for through Help and\n // support), products, and communities.\n 'billing',\n 'products',\n 'communities',\n] as const;\n/**\n * The category a refund ask is filed under (0.18.0): the backend finds a\n * member's ask by it to decide whether the ask came in time, and Payments\n * presets it. One value, typed, so a renamed category fails to compile rather\n * than turning every ask into \"not in time\".\n */\nexport const REFUND_ASK_CATEGORY =\n 'billing' as const satisfies (typeof TICKET_CATEGORY_VALUES)[number];\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;\n\n/** The three changes a ticket takes, each its own route (`/claim`, `/resolve`, `/reopen`). */\nexport const TICKET_CHANGE_VALUES = ['claim', 'resolve', 'reopen'] as const;\nexport type TicketChange = (typeof TICKET_CHANGE_VALUES)[number];\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/** Which side wrote a message, and so whose turn it is not (0.18.0: the values, for the backend's model). */\nexport const TICKET_SIDE_VALUES = ['member', 'agent'] as const;\nexport const ticketSideSchema = z.enum(TICKET_SIDE_VALUES);\nexport type TicketSide = z.infer<typeof ticketSideSchema>;\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: ticketSideSchema,\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. Whose turn it is comes from `status`, never from this. */\n lastFrom: ticketSideSchema,\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"]}
@@ -1 +1 @@
1
- {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../../src/domains/team/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,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;AAElF;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B;;;;iBAO1C,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAExF,8DAA8D;AAC9D,eAAO,MAAM,mCAAmC;;;;;iBAG9C,CAAC;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC"}
1
+ {"version":3,"file":"team.d.ts","sourceRoot":"","sources":["../../../src/domains/team/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAOxB,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;AAElF;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B;;;;iBAI1C,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,+BAA+B,CAAC,CAAC;AAExF,8DAA8D;AAC9D,eAAO,MAAM,mCAAmC;;;;;iBAG9C,CAAC;AACH,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mCAAmC,CAAC,CAAC"}
@@ -18,7 +18,7 @@
18
18
  import { z } from 'zod';
19
19
  import { CAPABILITY_VALUES, capabilitySchema } from '../../access/capabilities.js';
20
20
  import { PLATFORM_ROLE_VALUES, platformRoleSchema } from '../../access/roles.js';
21
- import { objectIdSchema } from '../../core/common.js';
21
+ import { eachOnce, objectIdSchema } from '../../core/common.js';
22
22
  import { paginatedSchema } from '../../core/responses.js';
23
23
  import { VALIDATION_MESSAGE } from '../../core/validation.js';
24
24
  /** `:memberId` in the paths above. */
@@ -44,7 +44,7 @@ export const setMemberRolesBodySchema = z.object({
44
44
  roles: z
45
45
  .array(platformRoleSchema.exclude(['superadmin']))
46
46
  .max(PLATFORM_ROLE_VALUES.length)
47
- .refine((roles) => new Set(roles).size === roles.length, {
47
+ .refine(eachOnce, {
48
48
  error: VALIDATION_MESSAGE.rolesDuplicate,
49
49
  }),
50
50
  });
@@ -61,10 +61,7 @@ export const setMemberRolesResponseSchema = z.object({
61
61
  * Sending what they already hold changes nothing and writes no audit entry.
62
62
  */
63
63
  export const setMemberCapabilitiesBodySchema = z.object({
64
- capabilities: z
65
- .array(capabilitySchema)
66
- .max(CAPABILITY_VALUES.length)
67
- .refine((list) => new Set(list).size === list.length, {
64
+ capabilities: z.array(capabilitySchema).max(CAPABILITY_VALUES.length).refine(eachOnce, {
68
65
  error: VALIDATION_MESSAGE.capabilitiesDuplicate,
69
66
  }),
70
67
  });
@@ -1 +1 @@
1
- {"version":3,"file":"team.js","sourceRoot":"","sources":["../../../src/domains/team/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACnF,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;AAGH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IACtD,YAAY,EAAE,CAAC;SACZ,KAAK,CAAC,gBAAgB,CAAC;SACvB,GAAG,CAAC,iBAAiB,CAAC,MAAM,CAAC;SAC7B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,CAAC,MAAM,EAAE;QACpD,KAAK,EAAE,kBAAkB,CAAC,qBAAqB;KAChD,CAAC;CACL,CAAC,CAAC;AAGH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1D,EAAE,EAAE,cAAc;IAClB,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC;CACxC,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 * PUT /api/v1/team/:memberId/capabilities set someone's capabilities (0.9.0)\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 { CAPABILITY_VALUES, capabilitySchema } from '../../access/capabilities.js';\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\n/**\n * `PUT /team/:memberId/capabilities` (0.9.0): the capabilities the person\n * should hold, as a whole set. Every one that changes must be one the actor's\n * active role may grant (`CAPABILITY_GRANTED_BY`, else\n * `CAPABILITY_NOT_GRANTABLE`); never one's own (`CANNOT_CHANGE_OWN_CAPABILITIES`).\n * Sending what they already hold changes nothing and writes no audit entry.\n */\nexport const setMemberCapabilitiesBodySchema = z.object({\n capabilities: z\n .array(capabilitySchema)\n .max(CAPABILITY_VALUES.length)\n .refine((list) => new Set(list).size === list.length, {\n error: VALIDATION_MESSAGE.capabilitiesDuplicate,\n }),\n});\nexport type SetMemberCapabilitiesBody = z.infer<typeof setMemberCapabilitiesBodySchema>;\n\n/** What it answers: the capabilities the person now holds. */\nexport const setMemberCapabilitiesResponseSchema = z.object({\n id: objectIdSchema,\n capabilities: z.array(capabilitySchema),\n});\nexport type SetMemberCapabilitiesResponse = z.infer<typeof setMemberCapabilitiesResponseSchema>;\n"]}
1
+ {"version":3,"file":"team.js","sourceRoot":"","sources":["../../../src/domains/team/team.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AACnF,OAAO,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAChE,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,QAAQ,EAAE;QAChB,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;AAGH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,CAAC,MAAM,CAAC;IACtD,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE;QACrF,KAAK,EAAE,kBAAkB,CAAC,qBAAqB;KAChD,CAAC;CACH,CAAC,CAAC;AAGH,8DAA8D;AAC9D,MAAM,CAAC,MAAM,mCAAmC,GAAG,CAAC,CAAC,MAAM,CAAC;IAC1D,EAAE,EAAE,cAAc;IAClB,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,gBAAgB,CAAC;CACxC,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 * PUT /api/v1/team/:memberId/capabilities set someone's capabilities (0.9.0)\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 { CAPABILITY_VALUES, capabilitySchema } from '../../access/capabilities.js';\nimport { PLATFORM_ROLE_VALUES, platformRoleSchema } from '../../access/roles.js';\nimport { eachOnce, 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(eachOnce, {\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\n/**\n * `PUT /team/:memberId/capabilities` (0.9.0): the capabilities the person\n * should hold, as a whole set. Every one that changes must be one the actor's\n * active role may grant (`CAPABILITY_GRANTED_BY`, else\n * `CAPABILITY_NOT_GRANTABLE`); never one's own (`CANNOT_CHANGE_OWN_CAPABILITIES`).\n * Sending what they already hold changes nothing and writes no audit entry.\n */\nexport const setMemberCapabilitiesBodySchema = z.object({\n capabilities: z.array(capabilitySchema).max(CAPABILITY_VALUES.length).refine(eachOnce, {\n error: VALIDATION_MESSAGE.capabilitiesDuplicate,\n }),\n});\nexport type SetMemberCapabilitiesBody = z.infer<typeof setMemberCapabilitiesBodySchema>;\n\n/** What it answers: the capabilities the person now holds. */\nexport const setMemberCapabilitiesResponseSchema = z.object({\n id: objectIdSchema,\n capabilities: z.array(capabilitySchema),\n});\nexport type SetMemberCapabilitiesResponse = z.infer<typeof setMemberCapabilitiesResponseSchema>;\n"]}
@@ -19,16 +19,13 @@ import { z } from 'zod';
19
19
  * For a file input's `accept`, and for a file whose `type` the browser left
20
20
  * empty. Images only: video is shared as a link in the post text, a product
21
21
  * decision of 2026-09-21, not an unfinished uploader.
22
+ *
23
+ * 0.18.0: named for what they are (they were `UPLOAD_KEY_*`, from the 0.4.0
24
+ * storage-key format that no route has taken since 0.5.0, now removed).
22
25
  */
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>;
26
+ export declare const UPLOAD_FILE_EXTENSIONS: readonly ["jpg", "jpeg", "png", "webp", "gif"];
27
+ export type UploadFileExtension = (typeof UPLOAD_FILE_EXTENSIONS)[number];
28
+ /** Each extension's content type: one of `MEDIA_UPLOAD_CONTENT_TYPES`, which a presign accepts. */
32
29
  export declare const UPLOAD_CONTENT_TYPES: {
33
30
  readonly jpg: "image/jpeg";
34
31
  readonly jpeg: "image/jpeg";
@@ -36,18 +33,6 @@ export declare const UPLOAD_CONTENT_TYPES: {
36
33
  readonly webp: "image/webp";
37
34
  readonly gif: "image/gif";
38
35
  };
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
36
  /**
52
37
  * Asking for an upload slot: `POST /api/v1/uploads/presign`.
53
38
  *
@@ -89,7 +74,7 @@ export type UploadPresignBody = z.infer<typeof uploadPresignBodySchema>;
89
74
  * processing the same upload; complete again in a moment.
90
75
  */
91
76
  export declare const uploadPresignResponseSchema: z.ZodObject<{
92
- mediaId: z.ZodString;
77
+ mediaId: z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>;
93
78
  url: z.ZodURL;
94
79
  method: z.ZodLiteral<"PUT">;
95
80
  headers: z.ZodRecord<z.ZodString, z.ZodString>;
@@ -1 +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"}
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;AASxB;;;;;;;;GAQG;AACH,eAAO,MAAM,sBAAsB,gDAAiD,CAAC;AACrF,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,sBAAsB,CAAC,CAAC,MAAM,CAAC,CAAC;AAE1E,mGAAmG;AACnG,eAAO,MAAM,oBAAoB;;;;;;CAMuC,CAAC;AAEzE;;;;;;;;;;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"}
@@ -16,16 +16,18 @@
16
16
  import { z } from 'zod';
17
17
  import { objectIdSchema } from '../../core/common.js';
18
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';
19
+ import { mediaPurposeSchema, mediaUploadContentTypeSchema, } from '../media/media.js';
21
20
  /**
22
21
  * File extensions a picker offers, and the content type each is uploaded as.
23
22
  * For a file input's `accept`, and for a file whose `type` the browser left
24
23
  * empty. Images only: video is shared as a link in the post text, a product
25
24
  * decision of 2026-09-21, not an unfinished uploader.
25
+ *
26
+ * 0.18.0: named for what they are (they were `UPLOAD_KEY_*`, from the 0.4.0
27
+ * storage-key format that no route has taken since 0.5.0, now removed).
26
28
  */
27
- export const UPLOAD_KEY_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
28
- export const uploadKeyExtensionSchema = z.enum(UPLOAD_KEY_EXTENSIONS);
29
+ export const UPLOAD_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
30
+ /** Each extension's content type: one of `MEDIA_UPLOAD_CONTENT_TYPES`, which a presign accepts. */
29
31
  export const UPLOAD_CONTENT_TYPES = {
30
32
  jpg: 'image/jpeg',
31
33
  jpeg: 'image/jpeg',
@@ -33,22 +35,6 @@ export const UPLOAD_CONTENT_TYPES = {
33
35
  webp: 'image/webp',
34
36
  gif: 'image/gif',
35
37
  };
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
38
  /**
53
39
  * Asking for an upload slot: `POST /api/v1/uploads/presign`.
54
40
  *
@@ -78,7 +64,7 @@ export const uploadPresignBodySchema = z.object({
78
64
  * processing the same upload; complete again in a moment.
79
65
  */
80
66
  export const uploadPresignResponseSchema = z.object({
81
- mediaId: z.string(),
67
+ mediaId: objectIdSchema,
82
68
  url: z.url(),
83
69
  method: z.literal('PUT'),
84
70
  headers: z.record(z.string(), z.string()),
@@ -1 +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"]}
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,EAEL,kBAAkB,EAClB,4BAA4B,GAC7B,MAAM,mBAAmB,CAAC;AAE3B;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,CAAU,CAAC;AAGrF,mGAAmG;AACnG,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;CACsD,CAAC;AAEzE;;;;;;;;;;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;IACvB,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 {\n type MediaUploadContentType,\n mediaPurposeSchema,\n mediaUploadContentTypeSchema,\n} 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 *\n * 0.18.0: named for what they are (they were `UPLOAD_KEY_*`, from the 0.4.0\n * storage-key format that no route has taken since 0.5.0, now removed).\n */\nexport const UPLOAD_FILE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'] as const;\nexport type UploadFileExtension = (typeof UPLOAD_FILE_EXTENSIONS)[number];\n\n/** Each extension's content type: one of `MEDIA_UPLOAD_CONTENT_TYPES`, which a presign accepts. */\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<UploadFileExtension, MediaUploadContentType>;\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: objectIdSchema,\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 CHANGED
@@ -13,8 +13,11 @@
13
13
  * );
14
14
  *
15
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.
16
+ * - A BLANK LINE NEVER PASSES FOR A VALUE. `FOO=` is "not given" for an
17
+ * optional field (the feature stays off) and refused for a required one.
18
+ * Where a field has a REAL fallback (a port, localhost, `development`),
19
+ * a blank line is refused too, never quietly given the fallback (0.18.0):
20
+ * only an unset variable takes it.
18
21
  * - A FALLBACK IS FOR LOCAL DEVELOPMENT ONLY. A deployment sets every value
19
22
  * from its own secret store; never "fix" a deployed mismatch by changing a
20
23
  * fallback, which would make the wrong value the default everywhere.
@@ -29,29 +32,39 @@
29
32
  import { z } from 'zod';
30
33
  /** The field builders. Each returns the rule for one variable. */
31
34
  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>;
35
+ /** Required text. With `fallback`, the text used when unset (see `withFallback`). */
36
+ readonly text: (fallback?: string) => z.ZodPreprocess<z.ZodString, unknown>;
37
+ /**
38
+ * Text that may be unset (`undefined`). With `{ refuseBlank: true }`, unset
39
+ * still means "off", but a BLANK line is refused (0.18.0): for a setting
40
+ * whose absence is a legitimate local choice and whose blank line in a
41
+ * deployment would silently switch a feature off (the web's media host: a
42
+ * blank one drops the CDN from the page's image policy).
43
+ */
44
+ readonly optionalText: (options?: {
45
+ refuseBlank?: boolean;
46
+ }) => z.ZodPreprocess<z.ZodOptional<z.ZodString>, unknown>;
47
+ /** A URL. With `fallback`, the URL used when unset (see `withFallback`). */
48
+ readonly url: (fallback?: string) => z.ZodPreprocess<z.ZodURL, unknown>;
38
49
  /** A URL that may be unset (`undefined`). */
39
50
  readonly optionalUrl: () => z.ZodPreprocess<z.ZodOptional<z.ZodURL>, unknown>;
40
51
  /** A MongoDB connection string. Always required: there is no safe default database. */
41
52
  readonly mongoUri: () => z.ZodPreprocess<z.ZodString, unknown>;
42
53
  /** A TCP port. */
43
- readonly port: (fallback: number) => z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
54
+ readonly port: (fallback: number) => z.ZodPreprocess<z.ZodCoercedNumber<unknown>, unknown>;
44
55
  /** A whole number above zero: a count, a duration in milliseconds. */
45
- readonly positiveInt: (fallback: number) => z.ZodPreprocess<z.ZodDefault<z.ZodCoercedNumber<unknown>>, unknown>;
56
+ readonly positiveInt: (fallback: number) => z.ZodPreprocess<z.ZodCoercedNumber<unknown>, unknown>;
46
57
  /**
47
58
  * On or off: `true`/`false`, `1`/`0` or `yes`/`no`, in any case. Anything
48
59
  * else is refused, so a typo cannot quietly turn a safeguard off.
49
60
  */
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>;
61
+ readonly flag: (fallback: boolean) => z.ZodPreprocess<z.ZodPipe<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>, z.ZodTransform<boolean, string | boolean>>, unknown>;
51
62
  /** 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>;
63
+ 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>;
64
+ /** One of a fixed set of words, or unset (`undefined`): a feature it configures stays off. */
65
+ readonly optionalOneOf: <const T extends readonly [string, ...string[]]>(values: T) => z.ZodPreprocess<z.ZodOptional<z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>>, unknown>;
53
66
  /** 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>;
67
+ readonly list: (fallback: string) => z.ZodPreprocess<z.ZodPipe<z.ZodString, z.ZodTransform<string[], string>>, unknown>;
55
68
  /** A required secret of at least `minLength` characters. */
56
69
  readonly secret: (minLength: number) => z.ZodPreprocess<z.ZodString, unknown>;
57
70
  /**
package/dist/env.d.ts.map CHANGED
@@ -1 +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"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAgCxB,kEAAkE;AAClE,eAAO,MAAM,MAAM;IACjB,qFAAqF;+BACrE,MAAM;IAMtB;;;;;;OAMG;sCACmB;QAAE,WAAW,CAAC,EAAE,OAAO,CAAA;KAAE;IAW/C,4EAA4E;8BAC7D,MAAM;IAKrB,6CAA6C;;IAK7C,uFAAuF;;IAWvF,kBAAkB;8BACH,MAAM;IAIrB,sEAAsE;qCAChD,MAAM;IAI5B;;;OAGG;8BACY,OAAO;IActB,mCAAmC;2BACvB,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,MAAM,CAAC;IAKrF,8FAA8F;mCAC1E,CAAC,SAAS,SAAS,CAAC,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC;IAIvE,oFAAoF;8BACrE,MAAM;IAYrB,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"}
package/dist/env.js CHANGED
@@ -13,8 +13,11 @@
13
13
  * );
14
14
  *
15
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.
16
+ * - A BLANK LINE NEVER PASSES FOR A VALUE. `FOO=` is "not given" for an
17
+ * optional field (the feature stays off) and refused for a required one.
18
+ * Where a field has a REAL fallback (a port, localhost, `development`),
19
+ * a blank line is refused too, never quietly given the fallback (0.18.0):
20
+ * only an unset variable takes it.
18
21
  * - A FALLBACK IS FOR LOCAL DEVELOPMENT ONLY. A deployment sets every value
19
22
  * from its own secret store; never "fix" a deployed mismatch by changing a
20
23
  * fallback, which would make the wrong value the default everywhere.
@@ -31,24 +34,57 @@ import { z } from 'zod';
31
34
  const unsetIfEmpty = (v) => (v === '' ? undefined : v);
32
35
  /** Wrap a rule so empty text is treated as absent before it is checked. */
33
36
  const field = (schema) => z.preprocess(unsetIfEmpty, schema);
37
+ const BLANK = 'is blank: give it a value, or leave the line out';
38
+ /**
39
+ * A rule with a FALLBACK (0.18.0). Unset takes the fallback. A blank line takes
40
+ * it only when the fallback is itself blank (`''`, a feature left off); where
41
+ * the fallback is a real value (localhost, a port, `development`, a database
42
+ * name), a blank line is REFUSED: a fallback is for local development, and a
43
+ * deployment's blank line is a mistake to stop at, never a cue to run as
44
+ * development or against localhost. (Plain "empty is unset" let a blank
45
+ * `NODE_ENV=` start a deployment in development mode.)
46
+ */
47
+ const withFallback = (schema, fallback) => z.preprocess((value, ctx) => {
48
+ if (value === undefined)
49
+ return fallback;
50
+ if (value === '' && fallback !== '') {
51
+ ctx.issues.push({ code: 'custom', message: BLANK, input: value });
52
+ return z.NEVER;
53
+ }
54
+ return value;
55
+ }, schema);
34
56
  const TRUE_WORDS = new Set(['true', '1', 'yes']);
35
57
  const FALSE_WORDS = new Set(['false', '0', 'no']);
36
58
  /** The field builders. Each returns the rule for one variable. */
37
59
  export const envVar = {
38
- /** Required text. With `fallback`, the text used when unset. */
60
+ /** Required text. With `fallback`, the text used when unset (see `withFallback`). */
39
61
  text(fallback) {
40
62
  return fallback === undefined
41
63
  ? field(z.string({ error: 'is required' }))
42
- : field(z.string().default(fallback));
64
+ : withFallback(z.string(), fallback);
43
65
  },
44
- /** Text that may be unset (`undefined`). */
45
- optionalText() {
46
- return field(z.string().optional());
66
+ /**
67
+ * Text that may be unset (`undefined`). With `{ refuseBlank: true }`, unset
68
+ * still means "off", but a BLANK line is refused (0.18.0): for a setting
69
+ * whose absence is a legitimate local choice and whose blank line in a
70
+ * deployment would silently switch a feature off (the web's media host: a
71
+ * blank one drops the CDN from the page's image policy).
72
+ */
73
+ optionalText(options = {}) {
74
+ if (!options.refuseBlank)
75
+ return field(z.string().optional());
76
+ return z.preprocess((value, ctx) => {
77
+ if (value === '') {
78
+ ctx.issues.push({ code: 'custom', message: BLANK, input: value });
79
+ return z.NEVER;
80
+ }
81
+ return value;
82
+ }, z.string().optional());
47
83
  },
48
- /** A URL. With `fallback`, the URL used when unset. */
84
+ /** A URL. With `fallback`, the URL used when unset (see `withFallback`). */
49
85
  url(fallback) {
50
86
  const url = z.url({ error: 'must be a URL' });
51
- return fallback === undefined ? field(url) : field(url.default(fallback));
87
+ return fallback === undefined ? field(url) : withFallback(url, fallback);
52
88
  },
53
89
  /** A URL that may be unset (`undefined`). */
54
90
  optionalUrl() {
@@ -64,20 +100,20 @@ export const envVar = {
64
100
  },
65
101
  /** A TCP port. */
66
102
  port(fallback) {
67
- return field(z.coerce.number().int().min(1).max(65535).default(fallback));
103
+ return withFallback(z.coerce.number().int().min(1).max(65535), fallback);
68
104
  },
69
105
  /** A whole number above zero: a count, a duration in milliseconds. */
70
106
  positiveInt(fallback) {
71
- return field(z.coerce.number().int().positive().default(fallback));
107
+ return withFallback(z.coerce.number().int().positive(), fallback);
72
108
  },
73
109
  /**
74
110
  * On or off: `true`/`false`, `1`/`0` or `yes`/`no`, in any case. Anything
75
111
  * else is refused, so a typo cannot quietly turn a safeguard off.
76
112
  */
77
113
  flag(fallback) {
78
- return field(z
79
- .string()
80
- .transform((v, ctx) => {
114
+ return withFallback(z.union([z.boolean(), z.string()]).transform((v, ctx) => {
115
+ if (typeof v === 'boolean')
116
+ return v;
81
117
  const word = v.trim().toLowerCase();
82
118
  if (TRUE_WORDS.has(word))
83
119
  return true;
@@ -85,24 +121,23 @@ export const envVar = {
85
121
  return false;
86
122
  ctx.addIssue({ code: 'custom', message: 'must be true or false' });
87
123
  return z.NEVER;
88
- })
89
- .optional()
90
- .transform((v) => v ?? fallback));
124
+ }), fallback);
91
125
  },
92
126
  /** One of a fixed set of words. */
93
127
  oneOf(values, fallback) {
94
128
  const choice = z.enum(values, { error: `must be one of: ${values.join(', ')}` });
95
- return fallback === undefined ? field(choice) : field(choice.default(fallback));
129
+ return fallback === undefined ? field(choice) : withFallback(choice, fallback);
130
+ },
131
+ /** One of a fixed set of words, or unset (`undefined`): a feature it configures stays off. */
132
+ optionalOneOf(values) {
133
+ return field(z.enum(values, { error: `must be one of: ${values.join(', ')}` }).optional());
96
134
  },
97
135
  /** Comma-separated text, as a list with blanks dropped: `a, b,` is `['a', 'b']`. */
98
136
  list(fallback) {
99
- return field(z
100
- .string()
101
- .default(fallback)
102
- .transform((v) => v
137
+ return withFallback(z.string().transform((v) => v
103
138
  .split(',')
104
139
  .map((item) => item.trim())
105
- .filter(Boolean)));
140
+ .filter(Boolean)), fallback);
106
141
  },
107
142
  /** A required secret of at least `minLength` characters. */
108
143
  secret(minLength) {
package/dist/env.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema/env — how an environment variable is checked.\n *\n * Each Founder Circle app lists ITS OWN variables (configuration belongs to the\n * app: adding one never needs a schema publish, and the web never learns the\n * backend's setting names) and builds each one from the fields below, so the\n * rules live here once and no app writes a validation rule of its own:\n *\n * import { envVar, readEnv } from 'founder-schema/env';\n * export const env = readEnv(\n * { PORT: envVar.port(6010), MONGODB_URI: envVar.mongoUri() },\n * process.env,\n * );\n *\n * The rules every field follows:\n * - EMPTY IS UNSET. `FOO=` in a file or an unset variable both mean \"not\n * given\", so a blank line in an env file cannot pass for a value.\n * - A FALLBACK IS FOR LOCAL DEVELOPMENT ONLY. A deployment sets every value\n * from its own secret store; never \"fix\" a deployed mismatch by changing a\n * fallback, which would make the wrong value the default everywhere.\n * - FAIL FAST AND SAY WHICH. `readEnv` reports every bad variable by NAME,\n * never by value (a value can be a secret), so a misconfigured process\n * stops at start-up rather than three layers down.\n *\n * A separate entry point so a browser bundle never loads it unless it asks.\n *\n * @module founder-schema/env\n */\n\nimport { z } from 'zod';\n\n/** Empty text is unset, like an absent variable. */\nconst unsetIfEmpty = (v: unknown) => (v === '' ? undefined : v);\n\n/** Wrap a rule so empty text is treated as absent before it is checked. */\nconst field = <T extends z.ZodType>(schema: T) => z.preprocess(unsetIfEmpty, schema);\n\nconst TRUE_WORDS = new Set(['true', '1', 'yes']);\nconst FALSE_WORDS = new Set(['false', '0', 'no']);\n\n/** The field builders. Each returns the rule for one variable. */\nexport const envVar = {\n /** Required text. With `fallback`, the text used when unset. */\n text(fallback?: string) {\n return fallback === undefined\n ? field(z.string({ error: 'is required' }))\n : field(z.string().default(fallback));\n },\n\n /** Text that may be unset (`undefined`). */\n optionalText() {\n return field(z.string().optional());\n },\n\n /** A URL. With `fallback`, the URL used when unset. */\n url(fallback?: string) {\n const url = z.url({ error: 'must be a URL' });\n return fallback === undefined ? field(url) : field(url.default(fallback));\n },\n\n /** A URL that may be unset (`undefined`). */\n optionalUrl() {\n return field(z.url({ error: 'must be a URL' }).optional());\n },\n\n /** A MongoDB connection string. Always required: there is no safe default database. */\n mongoUri() {\n return field(\n z\n .string({ error: 'is required' })\n .refine((v) => v.startsWith('mongodb://') || v.startsWith('mongodb+srv://'), {\n error: 'must start with mongodb:// or mongodb+srv://',\n }),\n );\n },\n\n /** A TCP port. */\n port(fallback: number) {\n return field(z.coerce.number().int().min(1).max(65535).default(fallback));\n },\n\n /** A whole number above zero: a count, a duration in milliseconds. */\n positiveInt(fallback: number) {\n return field(z.coerce.number().int().positive().default(fallback));\n },\n\n /**\n * On or off: `true`/`false`, `1`/`0` or `yes`/`no`, in any case. Anything\n * else is refused, so a typo cannot quietly turn a safeguard off.\n */\n flag(fallback: boolean) {\n return field(\n z\n .string()\n .transform((v, ctx) => {\n const word = v.trim().toLowerCase();\n if (TRUE_WORDS.has(word)) return true;\n if (FALSE_WORDS.has(word)) return false;\n ctx.addIssue({ code: 'custom', message: 'must be true or false' });\n return z.NEVER;\n })\n .optional()\n .transform((v) => v ?? fallback),\n );\n },\n\n /** One of a fixed set of words. */\n oneOf<const T extends readonly [string, ...string[]]>(values: T, fallback?: T[number]) {\n const choice = z.enum(values, { error: `must be one of: ${values.join(', ')}` });\n return fallback === undefined ? field(choice) : field(choice.default(fallback));\n },\n\n /** Comma-separated text, as a list with blanks dropped: `a, b,` is `['a', 'b']`. */\n list(fallback: string) {\n return field(\n z\n .string()\n .default(fallback)\n .transform((v) =>\n v\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean),\n ),\n );\n },\n\n /** A required secret of at least `minLength` characters. */\n secret(minLength: number) {\n return field(\n z\n .string({ error: 'is required' })\n .min(minLength, { error: `must be at least ${minLength} characters` }),\n );\n },\n\n /**\n * A secret that may be unset, which then reads as `''` so the feature it\n * guards can check one value and switch itself off. When set it must be at\n * least `minLength` characters: a short secret is a mistake, not a choice.\n */\n optionalSecret(minLength: number) {\n return field(\n z\n .string()\n .min(minLength, { error: `must be at least ${minLength} characters when set` })\n .optional()\n .transform((v) => v ?? ''),\n );\n },\n} as const;\n\n/** One variable that failed, by name. Never carries the value. */\nexport interface EnvProblem {\n variable: string;\n problem: string;\n}\n\n/** Thrown by `readEnv` when any variable is missing or wrong. */\nexport class EnvError extends Error {\n readonly problems: EnvProblem[];\n\n constructor(problems: EnvProblem[]) {\n super(\n `Environment is not valid:\\n${problems.map((p) => ` ${p.variable}: ${p.problem}`).join('\\n')}`,\n );\n this.name = 'EnvError';\n this.problems = problems;\n }\n}\n\n/** What `readEnv` returns: every variable as its field's output. */\nexport type EnvValues<F extends Record<string, z.ZodType>> = { [K in keyof F]: z.output<F[K]> };\n\n/**\n * Check every variable in `fields` against `source` (normally `process.env`)\n * and return them typed. Throws `EnvError` naming EVERY bad variable at once,\n * so a misconfiguration is fixed in one pass, not one restart per variable.\n * Variables in `source` that `fields` does not list are ignored.\n */\nexport function readEnv<F extends Record<string, z.ZodType>>(\n fields: F,\n source: Record<string, string | undefined>,\n): EnvValues<F> {\n const result = z.object(fields).safeParse(source);\n if (result.success) return result.data as EnvValues<F>;\n throw new EnvError(\n result.error.issues.map((issue) => ({\n variable: String(issue.path[0] ?? '(unknown)'),\n problem: issue.message,\n })),\n );\n}\n"]}
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../src/env.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * founder-schema/env — how an environment variable is checked.\n *\n * Each Founder Circle app lists ITS OWN variables (configuration belongs to the\n * app: adding one never needs a schema publish, and the web never learns the\n * backend's setting names) and builds each one from the fields below, so the\n * rules live here once and no app writes a validation rule of its own:\n *\n * import { envVar, readEnv } from 'founder-schema/env';\n * export const env = readEnv(\n * { PORT: envVar.port(6010), MONGODB_URI: envVar.mongoUri() },\n * process.env,\n * );\n *\n * The rules every field follows:\n * - A BLANK LINE NEVER PASSES FOR A VALUE. `FOO=` is \"not given\" for an\n * optional field (the feature stays off) and refused for a required one.\n * Where a field has a REAL fallback (a port, localhost, `development`),\n * a blank line is refused too, never quietly given the fallback (0.18.0):\n * only an unset variable takes it.\n * - A FALLBACK IS FOR LOCAL DEVELOPMENT ONLY. A deployment sets every value\n * from its own secret store; never \"fix\" a deployed mismatch by changing a\n * fallback, which would make the wrong value the default everywhere.\n * - FAIL FAST AND SAY WHICH. `readEnv` reports every bad variable by NAME,\n * never by value (a value can be a secret), so a misconfigured process\n * stops at start-up rather than three layers down.\n *\n * A separate entry point so a browser bundle never loads it unless it asks.\n *\n * @module founder-schema/env\n */\n\nimport { z } from 'zod';\n\n/** Empty text is unset, like an absent variable. */\nconst unsetIfEmpty = (v: unknown) => (v === '' ? undefined : v);\n\n/** Wrap a rule so empty text is treated as absent before it is checked. */\nconst field = <T extends z.ZodType>(schema: T) => z.preprocess(unsetIfEmpty, schema);\n\nconst BLANK = 'is blank: give it a value, or leave the line out';\n\n/**\n * A rule with a FALLBACK (0.18.0). Unset takes the fallback. A blank line takes\n * it only when the fallback is itself blank (`''`, a feature left off); where\n * the fallback is a real value (localhost, a port, `development`, a database\n * name), a blank line is REFUSED: a fallback is for local development, and a\n * deployment's blank line is a mistake to stop at, never a cue to run as\n * development or against localhost. (Plain \"empty is unset\" let a blank\n * `NODE_ENV=` start a deployment in development mode.)\n */\nconst withFallback = <T extends z.ZodType>(schema: T, fallback: string | number | boolean) =>\n z.preprocess((value, ctx) => {\n if (value === undefined) return fallback;\n if (value === '' && fallback !== '') {\n ctx.issues.push({ code: 'custom', message: BLANK, input: value });\n return z.NEVER;\n }\n return value;\n }, schema);\n\nconst TRUE_WORDS = new Set(['true', '1', 'yes']);\nconst FALSE_WORDS = new Set(['false', '0', 'no']);\n\n/** The field builders. Each returns the rule for one variable. */\nexport const envVar = {\n /** Required text. With `fallback`, the text used when unset (see `withFallback`). */\n text(fallback?: string) {\n return fallback === undefined\n ? field(z.string({ error: 'is required' }))\n : withFallback(z.string(), fallback);\n },\n\n /**\n * Text that may be unset (`undefined`). With `{ refuseBlank: true }`, unset\n * still means \"off\", but a BLANK line is refused (0.18.0): for a setting\n * whose absence is a legitimate local choice and whose blank line in a\n * deployment would silently switch a feature off (the web's media host: a\n * blank one drops the CDN from the page's image policy).\n */\n optionalText(options: { refuseBlank?: boolean } = {}) {\n if (!options.refuseBlank) return field(z.string().optional());\n return z.preprocess((value, ctx) => {\n if (value === '') {\n ctx.issues.push({ code: 'custom', message: BLANK, input: value });\n return z.NEVER;\n }\n return value;\n }, z.string().optional());\n },\n\n /** A URL. With `fallback`, the URL used when unset (see `withFallback`). */\n url(fallback?: string) {\n const url = z.url({ error: 'must be a URL' });\n return fallback === undefined ? field(url) : withFallback(url, fallback);\n },\n\n /** A URL that may be unset (`undefined`). */\n optionalUrl() {\n return field(z.url({ error: 'must be a URL' }).optional());\n },\n\n /** A MongoDB connection string. Always required: there is no safe default database. */\n mongoUri() {\n return field(\n z\n .string({ error: 'is required' })\n .refine((v) => v.startsWith('mongodb://') || v.startsWith('mongodb+srv://'), {\n error: 'must start with mongodb:// or mongodb+srv://',\n }),\n );\n },\n\n /** A TCP port. */\n port(fallback: number) {\n return withFallback(z.coerce.number().int().min(1).max(65535), fallback);\n },\n\n /** A whole number above zero: a count, a duration in milliseconds. */\n positiveInt(fallback: number) {\n return withFallback(z.coerce.number().int().positive(), fallback);\n },\n\n /**\n * On or off: `true`/`false`, `1`/`0` or `yes`/`no`, in any case. Anything\n * else is refused, so a typo cannot quietly turn a safeguard off.\n */\n flag(fallback: boolean) {\n return withFallback(\n z.union([z.boolean(), z.string()]).transform((v, ctx) => {\n if (typeof v === 'boolean') return v;\n const word = v.trim().toLowerCase();\n if (TRUE_WORDS.has(word)) return true;\n if (FALSE_WORDS.has(word)) return false;\n ctx.addIssue({ code: 'custom', message: 'must be true or false' });\n return z.NEVER;\n }),\n fallback,\n );\n },\n\n /** One of a fixed set of words. */\n oneOf<const T extends readonly [string, ...string[]]>(values: T, fallback?: T[number]) {\n const choice = z.enum(values, { error: `must be one of: ${values.join(', ')}` });\n return fallback === undefined ? field(choice) : withFallback(choice, fallback);\n },\n\n /** One of a fixed set of words, or unset (`undefined`): a feature it configures stays off. */\n optionalOneOf<const T extends readonly [string, ...string[]]>(values: T) {\n return field(z.enum(values, { error: `must be one of: ${values.join(', ')}` }).optional());\n },\n\n /** Comma-separated text, as a list with blanks dropped: `a, b,` is `['a', 'b']`. */\n list(fallback: string) {\n return withFallback(\n z.string().transform((v) =>\n v\n .split(',')\n .map((item) => item.trim())\n .filter(Boolean),\n ),\n fallback,\n );\n },\n\n /** A required secret of at least `minLength` characters. */\n secret(minLength: number) {\n return field(\n z\n .string({ error: 'is required' })\n .min(minLength, { error: `must be at least ${minLength} characters` }),\n );\n },\n\n /**\n * A secret that may be unset, which then reads as `''` so the feature it\n * guards can check one value and switch itself off. When set it must be at\n * least `minLength` characters: a short secret is a mistake, not a choice.\n */\n optionalSecret(minLength: number) {\n return field(\n z\n .string()\n .min(minLength, { error: `must be at least ${minLength} characters when set` })\n .optional()\n .transform((v) => v ?? ''),\n );\n },\n} as const;\n\n/** One variable that failed, by name. Never carries the value. */\nexport interface EnvProblem {\n variable: string;\n problem: string;\n}\n\n/** Thrown by `readEnv` when any variable is missing or wrong. */\nexport class EnvError extends Error {\n readonly problems: EnvProblem[];\n\n constructor(problems: EnvProblem[]) {\n super(\n `Environment is not valid:\\n${problems.map((p) => ` ${p.variable}: ${p.problem}`).join('\\n')}`,\n );\n this.name = 'EnvError';\n this.problems = problems;\n }\n}\n\n/** What `readEnv` returns: every variable as its field's output. */\nexport type EnvValues<F extends Record<string, z.ZodType>> = { [K in keyof F]: z.output<F[K]> };\n\n/**\n * Check every variable in `fields` against `source` (normally `process.env`)\n * and return them typed. Throws `EnvError` naming EVERY bad variable at once,\n * so a misconfiguration is fixed in one pass, not one restart per variable.\n * Variables in `source` that `fields` does not list are ignored.\n */\nexport function readEnv<F extends Record<string, z.ZodType>>(\n fields: F,\n source: Record<string, string | undefined>,\n): EnvValues<F> {\n const result = z.object(fields).safeParse(source);\n if (result.success) return result.data as EnvValues<F>;\n throw new EnvError(\n result.error.issues.map((issue) => ({\n variable: String(issue.path[0] ?? '(unknown)'),\n problem: issue.message,\n })),\n );\n}\n"]}