@cravery/core 0.0.5 → 0.0.7

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 (265) hide show
  1. package/dist/config/index.d.ts +1 -0
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/index.js +1 -0
  4. package/dist/config/index.js.map +1 -1
  5. package/dist/config/subscription.d.ts +2 -0
  6. package/dist/config/subscription.d.ts.map +1 -0
  7. package/dist/config/subscription.js +5 -0
  8. package/dist/config/subscription.js.map +1 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/lib/ai/cost.d.ts.map +1 -1
  14. package/dist/lib/ai/cost.js +10 -1
  15. package/dist/lib/ai/cost.js.map +1 -1
  16. package/dist/lib/ai/embedding.d.ts.map +1 -1
  17. package/dist/lib/ai/embedding.js +6 -1
  18. package/dist/lib/ai/embedding.js.map +1 -1
  19. package/dist/lib/errors/index.d.ts +31 -0
  20. package/dist/lib/errors/index.d.ts.map +1 -0
  21. package/dist/lib/errors/index.js +70 -0
  22. package/dist/lib/errors/index.js.map +1 -0
  23. package/dist/lib/index.d.ts +2 -0
  24. package/dist/lib/index.d.ts.map +1 -1
  25. package/dist/lib/index.js +2 -0
  26. package/dist/lib/index.js.map +1 -1
  27. package/dist/lib/repository/factory.d.ts +9 -0
  28. package/dist/lib/repository/factory.d.ts.map +1 -0
  29. package/dist/lib/repository/factory.js +16 -0
  30. package/dist/lib/repository/factory.js.map +1 -0
  31. package/dist/lib/repository/index.d.ts +1 -0
  32. package/dist/lib/repository/index.d.ts.map +1 -1
  33. package/dist/lib/repository/index.js +1 -0
  34. package/dist/lib/repository/index.js.map +1 -1
  35. package/dist/lib/repository/profile.repository.d.ts +1 -1
  36. package/dist/lib/repository/profile.repository.d.ts.map +1 -1
  37. package/dist/lib/repository/profile.repository.js +1 -1
  38. package/dist/lib/repository/profile.repository.js.map +1 -1
  39. package/dist/lib/repository/user.repository.d.ts +1 -1
  40. package/dist/lib/repository/user.repository.d.ts.map +1 -1
  41. package/dist/lib/repository/user.repository.js +1 -1
  42. package/dist/lib/repository/user.repository.js.map +1 -1
  43. package/dist/lib/storage.d.ts.map +1 -1
  44. package/dist/lib/storage.js +16 -0
  45. package/dist/lib/storage.js.map +1 -1
  46. package/dist/lib/utils/index.d.ts +2 -0
  47. package/dist/lib/utils/index.d.ts.map +1 -0
  48. package/dist/lib/utils/index.js +18 -0
  49. package/dist/lib/utils/index.js.map +1 -0
  50. package/dist/lib/utils/sanitize.d.ts +20 -0
  51. package/dist/lib/utils/sanitize.d.ts.map +1 -0
  52. package/dist/lib/utils/sanitize.js +40 -0
  53. package/dist/lib/utils/sanitize.js.map +1 -0
  54. package/dist/types/ai/filters.js +1 -1
  55. package/dist/types/ai/filters.js.map +1 -1
  56. package/dist/types/ai/recipe.js +5 -5
  57. package/dist/types/ai/recipe.js.map +1 -1
  58. package/dist/types/ai/translation.js +3 -3
  59. package/dist/types/ai/translation.js.map +1 -1
  60. package/dist/types/enums/allergen.d.ts +19 -0
  61. package/dist/types/enums/allergen.d.ts.map +1 -0
  62. package/dist/types/enums/allergen.js +21 -0
  63. package/dist/types/enums/allergen.js.map +1 -0
  64. package/dist/types/enums/cuisine.d.ts +39 -0
  65. package/dist/types/enums/cuisine.d.ts.map +1 -0
  66. package/dist/types/enums/cuisine.js +41 -0
  67. package/dist/types/enums/cuisine.js.map +1 -0
  68. package/dist/types/enums/dietary_tag.d.ts +21 -0
  69. package/dist/types/enums/dietary_tag.d.ts.map +1 -0
  70. package/dist/types/enums/dietary_tag.js +23 -0
  71. package/dist/types/enums/dietary_tag.js.map +1 -0
  72. package/dist/types/enums/difficulty.d.ts +10 -0
  73. package/dist/types/enums/difficulty.d.ts.map +1 -0
  74. package/dist/types/enums/difficulty.js +12 -0
  75. package/dist/types/enums/difficulty.js.map +1 -0
  76. package/dist/types/enums/image_type.d.ts +8 -0
  77. package/dist/types/enums/image_type.d.ts.map +1 -0
  78. package/dist/types/enums/image_type.js +7 -0
  79. package/dist/types/enums/image_type.js.map +1 -0
  80. package/dist/types/enums/index.d.ts +22 -0
  81. package/dist/types/enums/index.d.ts.map +1 -0
  82. package/dist/types/enums/index.js +38 -0
  83. package/dist/types/enums/index.js.map +1 -0
  84. package/dist/types/enums/locale.d.ts +11 -0
  85. package/dist/types/enums/locale.d.ts.map +1 -0
  86. package/dist/types/enums/locale.js +7 -0
  87. package/dist/types/enums/locale.js.map +1 -0
  88. package/dist/types/enums/meal_type.d.ts +14 -0
  89. package/dist/types/enums/meal_type.d.ts.map +1 -0
  90. package/dist/types/enums/meal_type.js +16 -0
  91. package/dist/types/enums/meal_type.js.map +1 -0
  92. package/dist/types/enums/moderation_status.d.ts +9 -0
  93. package/dist/types/enums/moderation_status.d.ts.map +1 -0
  94. package/dist/types/enums/moderation_status.js +11 -0
  95. package/dist/types/enums/moderation_status.js.map +1 -0
  96. package/dist/types/enums/priority.d.ts +9 -0
  97. package/dist/types/enums/priority.d.ts.map +1 -0
  98. package/dist/types/enums/priority.js +7 -0
  99. package/dist/types/enums/priority.js.map +1 -0
  100. package/dist/types/enums/profile_status.d.ts +10 -0
  101. package/dist/types/enums/profile_status.d.ts.map +1 -0
  102. package/dist/types/enums/profile_status.js +12 -0
  103. package/dist/types/enums/profile_status.js.map +1 -0
  104. package/dist/types/enums/recipe_source.d.ts +10 -0
  105. package/dist/types/enums/recipe_source.d.ts.map +1 -0
  106. package/dist/types/enums/recipe_source.js +7 -0
  107. package/dist/types/enums/recipe_source.js.map +1 -0
  108. package/dist/types/enums/recipe_status.d.ts +13 -0
  109. package/dist/types/enums/recipe_status.d.ts.map +1 -0
  110. package/dist/types/enums/recipe_status.js +15 -0
  111. package/dist/types/enums/recipe_status.js.map +1 -0
  112. package/dist/types/enums/role.d.ts +11 -0
  113. package/dist/types/enums/role.d.ts.map +1 -0
  114. package/dist/types/enums/role.js +13 -0
  115. package/dist/types/enums/role.js.map +1 -0
  116. package/dist/types/enums/severity.d.ts +9 -0
  117. package/dist/types/enums/severity.d.ts.map +1 -0
  118. package/dist/types/enums/severity.js +7 -0
  119. package/dist/types/enums/severity.js.map +1 -0
  120. package/dist/types/enums/spiciness.d.ts +10 -0
  121. package/dist/types/enums/spiciness.d.ts.map +1 -0
  122. package/dist/types/enums/spiciness.js +7 -0
  123. package/dist/types/enums/spiciness.js.map +1 -0
  124. package/dist/types/enums/status.d.ts +11 -0
  125. package/dist/types/enums/status.d.ts.map +1 -0
  126. package/dist/types/enums/status.js +13 -0
  127. package/dist/types/enums/status.js.map +1 -0
  128. package/dist/types/enums/subscription_tier.d.ts +8 -0
  129. package/dist/types/enums/subscription_tier.d.ts.map +1 -0
  130. package/dist/types/enums/subscription_tier.js +6 -0
  131. package/dist/types/enums/subscription_tier.js.map +1 -0
  132. package/dist/types/enums/suggestion_category.d.ts +11 -0
  133. package/dist/types/enums/suggestion_category.d.ts.map +1 -0
  134. package/dist/types/enums/suggestion_category.js +13 -0
  135. package/dist/types/enums/suggestion_category.js.map +1 -0
  136. package/dist/types/enums/temperature_unit.d.ts +8 -0
  137. package/dist/types/enums/temperature_unit.d.ts.map +1 -0
  138. package/dist/types/enums/temperature_unit.js +7 -0
  139. package/dist/types/enums/temperature_unit.js.map +1 -0
  140. package/dist/types/enums/unit.d.ts +43 -0
  141. package/dist/types/enums/unit.d.ts.map +1 -0
  142. package/dist/types/enums/unit.js +53 -0
  143. package/dist/types/enums/unit.js.map +1 -0
  144. package/dist/types/enums/url_type.d.ts +13 -0
  145. package/dist/types/enums/url_type.d.ts.map +1 -0
  146. package/dist/types/enums/url_type.js +15 -0
  147. package/dist/types/enums/url_type.js.map +1 -0
  148. package/dist/types/enums/user_status.d.ts +10 -0
  149. package/dist/types/enums/user_status.d.ts.map +1 -0
  150. package/dist/types/enums/user_status.js +12 -0
  151. package/dist/types/enums/user_status.js.map +1 -0
  152. package/dist/types/iam/index.d.ts +4 -0
  153. package/dist/types/iam/index.d.ts.map +1 -0
  154. package/dist/types/iam/index.js +20 -0
  155. package/dist/types/iam/index.js.map +1 -0
  156. package/dist/types/iam/profile.d.ts +30 -0
  157. package/dist/types/iam/profile.d.ts.map +1 -0
  158. package/dist/types/iam/profile.js +26 -0
  159. package/dist/types/iam/profile.js.map +1 -0
  160. package/dist/types/iam/subscription.d.ts +43 -0
  161. package/dist/types/iam/subscription.d.ts.map +1 -0
  162. package/dist/types/iam/subscription.js +22 -0
  163. package/dist/types/iam/subscription.js.map +1 -0
  164. package/dist/types/iam/user.d.ts +27 -0
  165. package/dist/types/iam/user.d.ts.map +1 -0
  166. package/dist/types/iam/user.js +18 -0
  167. package/dist/types/iam/user.js.map +1 -0
  168. package/dist/types/index.d.ts +2 -8
  169. package/dist/types/index.d.ts.map +1 -1
  170. package/dist/types/index.js +2 -8
  171. package/dist/types/index.js.map +1 -1
  172. package/dist/types/moderation.js +1 -1
  173. package/dist/types/moderation.js.map +1 -1
  174. package/dist/types/recipe/equipment.d.ts +20 -0
  175. package/dist/types/recipe/equipment.d.ts.map +1 -0
  176. package/dist/types/recipe/equipment.js +16 -0
  177. package/dist/types/recipe/equipment.js.map +1 -0
  178. package/dist/types/recipe/index.d.ts +7 -0
  179. package/dist/types/recipe/index.d.ts.map +1 -0
  180. package/dist/types/recipe/index.js +23 -0
  181. package/dist/types/recipe/index.js.map +1 -0
  182. package/dist/types/recipe/ingredient.d.ts +209 -0
  183. package/dist/types/recipe/ingredient.d.ts.map +1 -0
  184. package/dist/types/recipe/ingredient.js +33 -0
  185. package/dist/types/recipe/ingredient.js.map +1 -0
  186. package/dist/types/recipe/instruction.d.ts +32 -0
  187. package/dist/types/recipe/instruction.d.ts.map +1 -0
  188. package/dist/types/recipe/instruction.js +16 -0
  189. package/dist/types/recipe/instruction.js.map +1 -0
  190. package/dist/types/recipe/nutrition.d.ts +18 -0
  191. package/dist/types/recipe/nutrition.d.ts.map +1 -0
  192. package/dist/types/recipe/nutrition.js +20 -0
  193. package/dist/types/recipe/nutrition.js.map +1 -0
  194. package/dist/types/recipe/recipe.d.ts +453 -0
  195. package/dist/types/recipe/recipe.d.ts.map +1 -0
  196. package/dist/types/recipe/recipe.js +82 -0
  197. package/dist/types/recipe/recipe.js.map +1 -0
  198. package/dist/types/recipe/temperature.d.ts +10 -0
  199. package/dist/types/recipe/temperature.d.ts.map +1 -0
  200. package/dist/types/recipe/temperature.js +10 -0
  201. package/dist/types/recipe/temperature.js.map +1 -0
  202. package/dist/types/response.d.ts +16 -0
  203. package/dist/types/response.d.ts.map +1 -0
  204. package/dist/types/response.js +14 -0
  205. package/dist/types/response.js.map +1 -0
  206. package/dist/types/settings.js +6 -6
  207. package/dist/types/settings.js.map +1 -1
  208. package/package.json +28 -1
  209. package/src/config/index.ts +1 -0
  210. package/src/config/subscription.ts +1 -0
  211. package/src/index.ts +1 -1
  212. package/src/lib/ai/cost.ts +10 -1
  213. package/src/lib/ai/embedding.ts +6 -1
  214. package/src/lib/errors/index.ts +79 -0
  215. package/src/lib/iam.ts +1 -1
  216. package/src/lib/index.ts +2 -0
  217. package/src/lib/repository/factory.ts +14 -0
  218. package/src/lib/repository/index.ts +1 -0
  219. package/src/lib/repository/profile.repository.ts +2 -2
  220. package/src/lib/repository/user.repository.ts +2 -2
  221. package/src/lib/storage.ts +18 -0
  222. package/src/lib/utils/index.ts +1 -0
  223. package/src/lib/utils/sanitize.ts +38 -0
  224. package/src/types/ai/filters.ts +1 -1
  225. package/src/types/ai/recipe.ts +5 -5
  226. package/src/types/ai/translation.ts +3 -3
  227. package/src/{enums → types/enums}/recipe_status.ts +3 -0
  228. package/src/types/iam/index.ts +3 -0
  229. package/src/types/{profile.ts → iam/profile.ts} +1 -1
  230. package/src/types/iam/subscription.ts +23 -0
  231. package/src/types/{user.ts → iam/user.ts} +2 -1
  232. package/src/types/index.ts +2 -8
  233. package/src/types/moderation.ts +2 -2
  234. package/src/types/{equipment.ts → recipe/equipment.ts} +1 -1
  235. package/src/types/recipe/index.ts +6 -0
  236. package/src/types/{ingredient.ts → recipe/ingredient.ts} +1 -1
  237. package/src/types/{recipe.ts → recipe/recipe.ts} +7 -7
  238. package/src/types/response.ts +30 -0
  239. package/src/types/settings.ts +7 -7
  240. package/src/types/subscription.ts +0 -13
  241. /package/src/{enums → types/enums}/allergen.ts +0 -0
  242. /package/src/{enums → types/enums}/cuisine.ts +0 -0
  243. /package/src/{enums → types/enums}/dietary_tag.ts +0 -0
  244. /package/src/{enums → types/enums}/difficulty.ts +0 -0
  245. /package/src/{enums → types/enums}/image_type.ts +0 -0
  246. /package/src/{enums → types/enums}/index.ts +0 -0
  247. /package/src/{enums → types/enums}/locale.ts +0 -0
  248. /package/src/{enums → types/enums}/meal_type.ts +0 -0
  249. /package/src/{enums → types/enums}/moderation_status.ts +0 -0
  250. /package/src/{enums → types/enums}/priority.ts +0 -0
  251. /package/src/{enums → types/enums}/profile_status.ts +0 -0
  252. /package/src/{enums → types/enums}/recipe_source.ts +0 -0
  253. /package/src/{enums → types/enums}/role.ts +0 -0
  254. /package/src/{enums → types/enums}/severity.ts +0 -0
  255. /package/src/{enums → types/enums}/spiciness.ts +0 -0
  256. /package/src/{enums → types/enums}/status.ts +0 -0
  257. /package/src/{enums → types/enums}/subscription_tier.ts +0 -0
  258. /package/src/{enums → types/enums}/suggestion_category.ts +0 -0
  259. /package/src/{enums → types/enums}/temperature_unit.ts +0 -0
  260. /package/src/{enums → types/enums}/unit.ts +0 -0
  261. /package/src/{enums → types/enums}/url_type.ts +0 -0
  262. /package/src/{enums → types/enums}/user_status.ts +0 -0
  263. /package/src/types/{instruction.ts → recipe/instruction.ts} +0 -0
  264. /package/src/types/{nutrition.ts → recipe/nutrition.ts} +0 -0
  265. /package/src/types/{temperature.ts → recipe/temperature.ts} +0 -0
package/package.json CHANGED
@@ -1,9 +1,36 @@
1
1
  {
2
2
  "name": "@cravery/core",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Clean architecture foundation for Cravery",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./types": {
13
+ "types": "./dist/types/index.d.ts",
14
+ "default": "./dist/types/index.js"
15
+ },
16
+ "./config": {
17
+ "types": "./dist/config/index.d.ts",
18
+ "default": "./dist/config/index.js"
19
+ }
20
+ },
21
+ "typesVersions": {
22
+ "*": {
23
+ "types": [
24
+ "dist/types/index.d.ts"
25
+ ],
26
+ "enums": [
27
+ "dist/enums/index.d.ts"
28
+ ],
29
+ "config": [
30
+ "dist/config/index.d.ts"
31
+ ]
32
+ }
33
+ },
7
34
  "scripts": {
8
35
  "build": "tsc",
9
36
  "build:watch": "tsc --watch",
@@ -1,3 +1,4 @@
1
1
  export * from "./ai";
2
2
  export * from "./collections";
3
3
  export * from "./common";
4
+ export * from "./subscription";
@@ -0,0 +1 @@
1
+ export const MaxMembersPerSubscription = 3;
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from "./config";
2
- export * from "./enums";
2
+ export * from "./types/enums";
3
3
  export * from "./lib";
4
4
  export * from "./types";
@@ -66,7 +66,16 @@ export const logAIUsage = (usage: AIUsage): number => {
66
66
  updates[`${base}/models/${model.id}/${key}`] = ServerValue.increment(value);
67
67
  }
68
68
 
69
- db.ref().update(updates).catch(console.error);
69
+ db.ref()
70
+ .update(updates)
71
+ .catch((error) => {
72
+ console.error("[AI Usage] Failed to log usage:", {
73
+ flowName,
74
+ model: model.id,
75
+ cost,
76
+ error: error instanceof Error ? error.message : String(error),
77
+ });
78
+ });
70
79
 
71
80
  return cost;
72
81
  };
@@ -4,10 +4,15 @@ import { firestore } from "../firebase";
4
4
  import { ai, geminiEmbedding001 } from "./genkit";
5
5
  import { logAIUsage } from "./cost";
6
6
  import { AI_MODELS } from "../../config/ai";
7
+ import { ValidationError } from "../errors";
7
8
 
8
9
  export const generateEmbedding = async (text: string, flowName: string) => {
9
10
  if (text.length < 1 || text.length > 7500) {
10
- throw new Error("Validation error: Text out of range");
11
+ throw new ValidationError("Text must be between 1 and 7500 characters", {
12
+ length: text.length,
13
+ minLength: 1,
14
+ maxLength: 7500,
15
+ });
11
16
  }
12
17
  const response = await ai.embed({
13
18
  embedder: geminiEmbedding001,
@@ -0,0 +1,79 @@
1
+ export const APP_ERROR_CODE_VALUES = [
2
+ "BAD_REQUEST",
3
+ "UNAUTHORIZED",
4
+ "FORBIDDEN",
5
+ "NOT_FOUND",
6
+ "CONFLICT",
7
+ "VALIDATION_ERROR",
8
+ "INTERNAL_ERROR",
9
+ ] as const;
10
+
11
+ export type AppErrorCode = (typeof APP_ERROR_CODE_VALUES)[number];
12
+
13
+ export class AppError extends Error {
14
+ public readonly code: AppErrorCode;
15
+ public readonly statusCode: number;
16
+ public readonly context?: Record<string, unknown>;
17
+
18
+ constructor(
19
+ code: AppErrorCode,
20
+ message: string,
21
+ statusCode: number = 500,
22
+ context?: Record<string, unknown>,
23
+ ) {
24
+ super(message);
25
+ this.name = "AppError";
26
+ this.code = code;
27
+ this.statusCode = statusCode;
28
+ this.context = context;
29
+
30
+ if (Error.captureStackTrace) {
31
+ Error.captureStackTrace(this, AppError);
32
+ }
33
+ }
34
+
35
+ toJSON() {
36
+ return {
37
+ name: this.name,
38
+ code: this.code,
39
+ message: this.message,
40
+ statusCode: this.statusCode,
41
+ context: this.context,
42
+ };
43
+ }
44
+ }
45
+
46
+ export class ValidationError extends AppError {
47
+ constructor(message: string, context?: Record<string, unknown>) {
48
+ super("VALIDATION_ERROR", message, 400, context);
49
+ this.name = "ValidationError";
50
+ }
51
+ }
52
+
53
+ export class NotFoundError extends AppError {
54
+ constructor(message: string, context?: Record<string, unknown>) {
55
+ super("NOT_FOUND", message, 404, context);
56
+ this.name = "NotFoundError";
57
+ }
58
+ }
59
+
60
+ export class UnauthorizedError extends AppError {
61
+ constructor(message: string, context?: Record<string, unknown>) {
62
+ super("UNAUTHORIZED", message, 401, context);
63
+ this.name = "UnauthorizedError";
64
+ }
65
+ }
66
+
67
+ export class ForbiddenError extends AppError {
68
+ constructor(message: string, context?: Record<string, unknown>) {
69
+ super("FORBIDDEN", message, 403, context);
70
+ this.name = "ForbiddenError";
71
+ }
72
+ }
73
+
74
+ export class ConflictError extends AppError {
75
+ constructor(message: string, context?: Record<string, unknown>) {
76
+ super("CONFLICT", message, 409, context);
77
+ this.name = "ConflictError";
78
+ }
79
+ }
package/src/lib/iam.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as admin from "firebase-admin";
2
- import { Role } from "../enums";
2
+ import { Role } from "../types/enums";
3
3
 
4
4
  const getUserRole = async (uid: string): Promise<Role> => {
5
5
  let role: Role = "guest";
package/src/lib/index.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export * from "./ai";
2
2
  export * from "./api";
3
+ export * from "./errors";
3
4
  export * from "./firebase";
4
5
  export * from "./iam";
5
6
  export * from "./repository";
6
7
  export * from "./storage";
8
+ export * from "./utils";
@@ -0,0 +1,14 @@
1
+ import { firestore } from "../firebase";
2
+ import { UserRepository } from "./user.repository";
3
+ import { ProfileRepository } from "./profile.repository";
4
+ import { SettingsRepository } from "./settings.repository";
5
+
6
+ let userRepo: UserRepository | null = null;
7
+ let profileRepo: ProfileRepository | null = null;
8
+ let settingsRepo: SettingsRepository | null = null;
9
+
10
+ export const repositories = {
11
+ user: () => (userRepo ??= new UserRepository(firestore)),
12
+ profile: () => (profileRepo ??= new ProfileRepository(firestore)),
13
+ settings: () => (settingsRepo ??= new SettingsRepository(firestore)),
14
+ };
@@ -1,4 +1,5 @@
1
1
  export * from "./errors";
2
+ export * from "./factory";
2
3
  export * from "./firestore.repository";
3
4
  export * from "./rtdb.repository";
4
5
  export * from "./user.repository";
@@ -1,7 +1,7 @@
1
1
  import { Firestore } from "firebase-admin/firestore";
2
2
  import { FirestoreRepository } from "./firestore.repository";
3
- import type { Profile } from "../../types/profile";
4
- import { ProfileSchema } from "../../types/profile";
3
+ import type { Profile } from "../../types/iam/profile";
4
+ import { ProfileSchema } from "../../types/iam/profile";
5
5
  import { Collections } from "../../config/collections";
6
6
  import { RepositoryError, RepositoryErrorCode } from "./errors";
7
7
 
@@ -1,7 +1,7 @@
1
1
  import { Firestore } from "firebase-admin/firestore";
2
2
  import { FirestoreRepository } from "./firestore.repository";
3
- import type { User } from "../../types/user";
4
- import { UserSchema } from "../../types/user";
3
+ import type { User } from "../../types/iam/user";
4
+ import { UserSchema } from "../../types/iam/user";
5
5
  import { Collections } from "../../config/collections";
6
6
  import { RepositoryError, RepositoryErrorCode } from "./errors";
7
7
 
@@ -1,15 +1,33 @@
1
1
  import { getStorage } from "firebase-admin/storage";
2
+ import { ValidationError } from "./errors";
2
3
 
3
4
  export interface UploadImageResult {
4
5
  url: string;
5
6
  path: string;
6
7
  }
7
8
 
9
+ function validateStoragePath(path: string): void {
10
+ if (!path || path.length === 0) {
11
+ throw new ValidationError("Storage path cannot be empty");
12
+ }
13
+ if (path.includes("..")) {
14
+ throw new ValidationError("Storage path cannot contain '..'", { path });
15
+ }
16
+ if (path.startsWith("/")) {
17
+ throw new ValidationError("Storage path cannot start with '/'", { path });
18
+ }
19
+ if (path.includes("//")) {
20
+ throw new ValidationError("Storage path cannot contain '//'", { path });
21
+ }
22
+ }
23
+
8
24
  export const uploadImageToStorage = async (
9
25
  base64Data: string,
10
26
  path: string,
11
27
  contentType: string = "image/png",
12
28
  ): Promise<UploadImageResult> => {
29
+ validateStoragePath(path);
30
+
13
31
  try {
14
32
  const bucket = getStorage().bucket();
15
33
  const buffer = Buffer.from(base64Data, "base64");
@@ -0,0 +1 @@
1
+ export * from "./sanitize";
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Removes HTML tags and script content from a string.
5
+ * Use for text fields that should not contain HTML (descriptions, bios, etc.)
6
+ */
7
+ export function sanitizeText(text: string): string {
8
+ return text
9
+ .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "")
10
+ .replace(/<[^>]*>/g, "")
11
+ .trim();
12
+ }
13
+
14
+ /**
15
+ * Creates a Zod string schema that sanitizes HTML on transformation.
16
+ * Use in schemas where text input needs to be cleaned.
17
+ *
18
+ * @example
19
+ * const BioSchema = sanitizedString(z.string().max(280));
20
+ */
21
+ export function sanitizedString<T extends z.ZodString>(schema: T) {
22
+ return schema.transform(sanitizeText);
23
+ }
24
+
25
+ /**
26
+ * Escapes HTML special characters to prevent XSS.
27
+ * Use when HTML needs to be displayed as text.
28
+ */
29
+ export function escapeHtml(text: string): string {
30
+ const htmlEscapes: Record<string, string> = {
31
+ "&": "&amp;",
32
+ "<": "&lt;",
33
+ ">": "&gt;",
34
+ '"': "&quot;",
35
+ "'": "&#39;",
36
+ };
37
+ return text.replace(/[&<>"']/g, (char) => htmlEscapes[char]);
38
+ }
@@ -5,7 +5,7 @@ import {
5
5
  DifficultySchema,
6
6
  MealTypeSchema,
7
7
  SpicinessSchema,
8
- } from "../../enums";
8
+ } from "../enums";
9
9
 
10
10
  export const RecipeFiltersSchema = z.object({
11
11
  cuisines: z.array(CuisineSchema).optional(),
@@ -6,11 +6,11 @@ import {
6
6
  DifficultySchema,
7
7
  MealTypeSchema,
8
8
  SpicinessSchema,
9
- } from "../../enums";
10
- import { EquipmentSchema } from "../equipment";
11
- import { IngredientSectionSchema } from "../ingredient";
12
- import { InstructionSchema } from "../instruction";
13
- import { NutritionSchema } from "../nutrition";
9
+ } from "../enums";
10
+ import { EquipmentSchema } from "../recipe/equipment";
11
+ import { IngredientSectionSchema } from "../recipe/ingredient";
12
+ import { InstructionSchema } from "../recipe/instruction";
13
+ import { NutritionSchema } from "../recipe/nutrition";
14
14
 
15
15
  export const AIRecipeSchema = z.object({
16
16
  allergens: z.array(AllergenSchema),
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
- import { EquipmentContentSchema } from "../equipment";
3
- import { IngredientSectionContentSchema } from "../ingredient";
4
- import { InstructionContentSchema } from "../instruction";
2
+ import { EquipmentContentSchema } from "../recipe/equipment";
3
+ import { IngredientSectionContentSchema } from "../recipe/ingredient";
4
+ import { InstructionContentSchema } from "../recipe/instruction";
5
5
 
6
6
  export const AIRecipeTranslationSchema = z.object({
7
7
  description: z.string().min(10).max(2000),
@@ -4,7 +4,10 @@ export const RECIPE_STATUS_VALUES = [
4
4
  "active",
5
5
  "archived",
6
6
  "deleted",
7
+ "draft",
8
+ "flagged",
7
9
  "pending",
10
+ "rejected",
8
11
  ] as const;
9
12
  export const RecipeStatusSchema = z.enum(RECIPE_STATUS_VALUES);
10
13
  export type RecipeStatus = (typeof RECIPE_STATUS_VALUES)[number];
@@ -0,0 +1,3 @@
1
+ export * from "./profile";
2
+ export * from "./subscription";
3
+ export * from "./user";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { Timestamp } from "firebase-admin/firestore";
2
+ import type { Timestamp } from "firebase-admin/firestore";
3
3
  import { ProfileStatusSchema } from "../enums";
4
4
 
5
5
  export const ProfileSchema = z.object({
@@ -0,0 +1,23 @@
1
+ import { z } from "zod";
2
+ import type { Timestamp } from "firebase-admin/firestore";
3
+ import { SubscriptionTierSchema } from "../enums";
4
+ import { MaxMembersPerSubscription } from "../../config";
5
+
6
+ export const MemberSchema = z.object({
7
+ userId: z.string(),
8
+ role: z.enum(["owner", "member"]),
9
+ status: z.enum(["pending", "active", "removed"]),
10
+ invitedAt: z.custom<Timestamp>(),
11
+ joinedAt: z.custom<Timestamp>().optional(),
12
+ });
13
+ export type Member = z.infer<typeof MemberSchema>;
14
+
15
+ export const SubscriptionSchema = z.object({
16
+ expiresAt: z.custom<Timestamp>(),
17
+ id: z.string(),
18
+ members: z.array(MemberSchema).max(MaxMembersPerSubscription).optional(),
19
+ revenuecatId: z.string(),
20
+ tier: SubscriptionTierSchema,
21
+ updatedAt: z.custom<Timestamp>(),
22
+ });
23
+ export type Subscription = z.infer<typeof SubscriptionSchema>;
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { Timestamp } from "firebase-admin/firestore";
2
+ import type { Timestamp } from "firebase-admin/firestore";
3
3
  import { RoleSchema, UserStatusSchema } from "../enums";
4
4
 
5
5
  export const UserSchema = z.object({
@@ -12,6 +12,7 @@ export const UserSchema = z.object({
12
12
  status: UserStatusSchema,
13
13
  subscriptionId: z.string().optional(),
14
14
  updatedAt: z.custom<Timestamp>(),
15
+ verified: z.boolean(),
15
16
  });
16
17
  export type User = z.infer<typeof UserSchema>;
17
18
 
@@ -1,13 +1,7 @@
1
1
  export * from "./ai";
2
- export * from "./equipment";
3
- export * from "./ingredient";
4
- export * from "./instruction";
2
+ export * from "./iam";
5
3
  export * from "./moderation";
6
- export * from "./nutrition";
7
- export * from "./profile";
8
4
  export * from "./recipe";
9
5
  export * from "./repository";
6
+ export * from "./response";
10
7
  export * from "./settings";
11
- export * from "./subscription";
12
- export * from "./temperature";
13
- export * from "./user";
@@ -1,11 +1,11 @@
1
1
  import { z } from "zod";
2
- import { Timestamp } from "firebase-admin/firestore";
2
+ import type { Timestamp } from "firebase-admin/firestore";
3
3
  import {
4
4
  ModerationStatusSchema,
5
5
  PrioritySchema,
6
6
  SeveritySchema,
7
7
  SuggestionCategorySchema,
8
- } from "../enums";
8
+ } from "./enums";
9
9
 
10
10
  export const ModerationSuggestionSchema = z.object({
11
11
  category: SuggestionCategorySchema,
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { SlugRegex } from "../config";
2
+ import { SlugRegex } from "../../config";
3
3
 
4
4
  export const EquipmentMetaSchema = z.object({
5
5
  required: z.boolean().optional(),
@@ -0,0 +1,6 @@
1
+ export * from "./equipment";
2
+ export * from "./ingredient";
3
+ export * from "./instruction";
4
+ export * from "./nutrition";
5
+ export * from "./recipe";
6
+ export * from "./temperature";
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { SlugRegex } from "../config";
2
+ import { SlugRegex } from "../../config";
3
3
  import { UnitSchema } from "../enums";
4
4
 
5
5
  export const IngredientMetaSchema = z.object({
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { Timestamp } from "firebase-admin/firestore";
2
+ import type { Timestamp } from "firebase-admin/firestore";
3
3
  import {
4
4
  AllergenSchema,
5
5
  CuisineSchema,
@@ -37,11 +37,11 @@ export const RecipeMetaSchema = z.object({
37
37
  deletedAt: z.custom<Timestamp>().optional(),
38
38
  dietaryTags: z.array(DietaryTagSchema),
39
39
  difficulty: DifficultySchema,
40
- equipment: z.array(EquipmentMetaSchema).optional(),
40
+ equipment: z.array(EquipmentMetaSchema).max(30).optional(),
41
41
  id: z.string(),
42
42
  imageUrl: z.string().optional(),
43
- ingredientSections: z.array(IngredientSectionMetaSchema).min(1),
44
- instructions: z.array(InstructionMetaSchema).min(1),
43
+ ingredientSections: z.array(IngredientSectionMetaSchema).min(1).max(20),
44
+ instructions: z.array(InstructionMetaSchema).min(1).max(100),
45
45
  mealTypes: z.array(MealTypeSchema).min(1),
46
46
  nutrition: NutritionSchema.optional(),
47
47
  originalLocale: LocaleSchema,
@@ -58,10 +58,10 @@ export type RecipeMeta = z.infer<typeof RecipeMetaSchema>;
58
58
  export const RecipeContentSchema = z.object({
59
59
  description: z.string().min(10).max(2000),
60
60
  equipment: z.array(EquipmentContentSchema).optional(),
61
- ingredientSections: z.array(IngredientSectionContentSchema).min(1),
62
- instructions: z.array(InstructionContentSchema).min(1),
61
+ ingredientSections: z.array(IngredientSectionContentSchema).min(1).max(20),
62
+ instructions: z.array(InstructionContentSchema).min(1).max(100),
63
63
  locale: LocaleSchema,
64
- tips: z.array(z.string().max(500)).optional(),
64
+ tips: z.array(z.string().max(500)).max(20).optional(),
65
65
  title: z.string().min(3).max(200),
66
66
  });
67
67
  export type RecipeContent = z.infer<typeof RecipeContentSchema>;
@@ -0,0 +1,30 @@
1
+ export type ApiSuccessResponse<T> = {
2
+ success: true;
3
+ data: T;
4
+ };
5
+
6
+ export type ApiErrorResponse = {
7
+ success: false;
8
+ error: {
9
+ code: string;
10
+ message: string;
11
+ details?: unknown;
12
+ };
13
+ };
14
+
15
+ export type ApiResponse<T> = ApiSuccessResponse<T> | ApiErrorResponse;
16
+
17
+ export function success<T>(data: T): ApiSuccessResponse<T> {
18
+ return { success: true, data };
19
+ }
20
+
21
+ export function failure(
22
+ code: string,
23
+ message: string,
24
+ details?: unknown,
25
+ ): ApiErrorResponse {
26
+ return {
27
+ success: false,
28
+ error: { code, message, details },
29
+ };
30
+ }
@@ -1,11 +1,11 @@
1
1
  import { z } from "zod";
2
- import { Timestamp } from "firebase-admin/firestore";
3
- import { AllergenSchema } from "../enums/allergen";
4
- import { DietaryTagSchema } from "../enums/dietary_tag";
5
- import { CuisineSchema } from "../enums/cuisine";
6
- import { SpicinessSchema } from "../enums/spiciness";
7
- import { LocaleSchema } from "../enums/locale";
8
- import { TemperatureUnitSchema } from "../enums/temperature_unit";
2
+ import type { Timestamp } from "firebase-admin/firestore";
3
+ import { AllergenSchema } from "./enums/allergen";
4
+ import { DietaryTagSchema } from "./enums/dietary_tag";
5
+ import { CuisineSchema } from "./enums/cuisine";
6
+ import { SpicinessSchema } from "./enums/spiciness";
7
+ import { LocaleSchema } from "./enums/locale";
8
+ import { TemperatureUnitSchema } from "./enums/temperature_unit";
9
9
 
10
10
  export const MEASUREMENT_SYSTEM_VALUES = ["metric", "imperial"] as const;
11
11
  export const MeasurementSystemSchema = z.enum(MEASUREMENT_SYSTEM_VALUES);
@@ -1,13 +0,0 @@
1
- import { z } from "zod";
2
- import { Timestamp } from "firebase-admin/firestore";
3
- import { SubscriptionTierSchema } from "../enums";
4
-
5
- export const SubscriptionSchema = z.object({
6
- expiresAt: z.custom<Timestamp>(),
7
- familyMembers: z.array(z.string()).max(3).optional(),
8
- id: z.string(),
9
- revenuecatId: z.string(),
10
- tier: SubscriptionTierSchema,
11
- updatedAt: z.custom<Timestamp>(),
12
- });
13
- export type Subscription = z.infer<typeof SubscriptionSchema>;
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes