@mohasinac/appkit 4.41.0 → 4.41.1

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 (166) hide show
  1. package/dist/features/shell/SectionForm.js +44 -5
  2. package/package.json +2 -2
  3. package/dist/_internal/server/features/classified/actions.d.ts +0 -12
  4. package/dist/_internal/server/features/classified/actions.js +0 -33
  5. package/dist/_internal/server/features/digital-code/actions.d.ts +0 -12
  6. package/dist/_internal/server/features/digital-code/actions.js +0 -33
  7. package/dist/_internal/server/features/homepage/data.d.ts +0 -18
  8. package/dist/_internal/server/features/homepage/data.js +0 -70
  9. package/dist/_internal/server/features/live/actions.d.ts +0 -6
  10. package/dist/_internal/server/features/live/actions.js +0 -27
  11. package/dist/_internal/server/features/messages/actions.d.ts +0 -6
  12. package/dist/_internal/server/features/messages/actions.js +0 -6
  13. package/dist/_internal/server/features/messages/data.d.ts +0 -10
  14. package/dist/_internal/server/features/messages/data.js +0 -24
  15. package/dist/_internal/server/features/messages/index.d.ts +0 -2
  16. package/dist/_internal/server/features/messages/index.js +0 -2
  17. package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +0 -12
  18. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +0 -112
  19. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +0 -2
  20. package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +0 -2
  21. package/dist/_internal/shared/features/homepage/section-gate.d.ts +0 -14
  22. package/dist/_internal/shared/features/homepage/section-gate.js +0 -51
  23. package/dist/_internal/shared/features/lottery/config-form.d.ts +0 -58
  24. package/dist/_internal/shared/features/lottery/config-form.js +0 -71
  25. package/dist/client/api/useFeatureFlags.d.ts +0 -22
  26. package/dist/client/api/useFeatureFlags.js +0 -27
  27. package/dist/features/account/address-validation.d.ts +0 -20
  28. package/dist/features/account/address-validation.js +0 -35
  29. package/dist/features/account/components/ChatList.d.ts +0 -14
  30. package/dist/features/account/components/ChatList.js +0 -9
  31. package/dist/features/account/components/ChatWindow.d.ts +0 -17
  32. package/dist/features/account/components/ChatWindow.js +0 -6
  33. package/dist/features/account/components/MessagesView.d.ts +0 -16
  34. package/dist/features/account/components/MessagesView.js +0 -5
  35. package/dist/features/account/components/UserOffersView.d.ts +0 -10
  36. package/dist/features/account/components/UserOffersView.js +0 -5
  37. package/dist/features/account/constants/addresses.d.ts +0 -13
  38. package/dist/features/account/constants/addresses.js +0 -43
  39. package/dist/features/account/hooks/useAddressForm.d.ts +0 -24
  40. package/dist/features/account/hooks/useAddressForm.js +0 -58
  41. package/dist/features/admin/actions/chat-actions.d.ts +0 -29
  42. package/dist/features/admin/actions/chat-actions.js +0 -101
  43. package/dist/features/admin/components/AdminFeatureFlagsView.d.ts +0 -9
  44. package/dist/features/admin/components/AdminFeatureFlagsView.js +0 -109
  45. package/dist/features/admin/components/AdminSiteView.d.ts +0 -10
  46. package/dist/features/admin/components/AdminSiteView.js +0 -52
  47. package/dist/features/admin/hooks/useChat.d.ts +0 -24
  48. package/dist/features/admin/hooks/useChat.js +0 -100
  49. package/dist/features/admin/permission-map.d.ts +0 -3
  50. package/dist/features/admin/permission-map.js +0 -26
  51. package/dist/features/admin/repository/chat.repository.d.ts +0 -50
  52. package/dist/features/admin/repository/chat.repository.js +0 -223
  53. package/dist/features/auth/actions/password-change-otp-actions.d.ts +0 -18
  54. package/dist/features/auth/actions/password-change-otp-actions.js +0 -88
  55. package/dist/features/auth/actions/realtime-token-actions.d.ts +0 -11
  56. package/dist/features/auth/actions/realtime-token-actions.js +0 -38
  57. package/dist/features/auth/password-change-otp.d.ts +0 -34
  58. package/dist/features/auth/password-change-otp.js +0 -56
  59. package/dist/features/classified/components/ClassifiedContactSellerPanel.d.ts +0 -12
  60. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +0 -49
  61. package/dist/features/events/api/route.d.ts +0 -12
  62. package/dist/features/events/api/route.js +0 -90
  63. package/dist/features/faq/api/route.d.ts +0 -11
  64. package/dist/features/faq/api/route.js +0 -52
  65. package/dist/features/faq/hooks/useFAQs.d.ts +0 -22
  66. package/dist/features/faq/hooks/useFAQs.js +0 -41
  67. package/dist/features/media/upload/video-poster.test.d.ts +0 -1
  68. package/dist/features/media/upload/video-poster.test.js +0 -42
  69. package/dist/features/messages/actions/index.d.ts +0 -2
  70. package/dist/features/messages/actions/index.js +0 -2
  71. package/dist/features/messages/actions/messages-actions.d.ts +0 -15
  72. package/dist/features/messages/actions/messages-actions.js +0 -33
  73. package/dist/features/messages/actions/ping-rtdb.d.ts +0 -2
  74. package/dist/features/messages/actions/ping-rtdb.js +0 -26
  75. package/dist/features/messages/hooks/useConversation.d.ts +0 -22
  76. package/dist/features/messages/hooks/useConversation.js +0 -155
  77. package/dist/features/messages/hooks/useConversations.d.ts +0 -15
  78. package/dist/features/messages/hooks/useConversations.js +0 -104
  79. package/dist/features/messages/index.d.ts +0 -6
  80. package/dist/features/messages/index.js +0 -4
  81. package/dist/features/messages/realtime.d.ts +0 -32
  82. package/dist/features/messages/realtime.js +0 -39
  83. package/dist/features/messages/repository/conversations.repository.d.ts +0 -71
  84. package/dist/features/messages/repository/conversations.repository.js +0 -284
  85. package/dist/features/messages/schemas/firestore.d.ts +0 -34
  86. package/dist/features/messages/schemas/firestore.js +0 -16
  87. package/dist/features/messages/schemas/index.d.ts +0 -177
  88. package/dist/features/messages/schemas/index.js +0 -35
  89. package/dist/features/messages/server.d.ts +0 -6
  90. package/dist/features/messages/server.js +0 -6
  91. package/dist/features/products/components/MakeOfferForm.d.ts +0 -20
  92. package/dist/features/products/components/MakeOfferForm.js +0 -9
  93. package/dist/features/reviews/api/route.d.ts +0 -21
  94. package/dist/features/reviews/api/route.js +0 -174
  95. package/dist/features/seller/api/offers/route.d.ts +0 -17
  96. package/dist/features/seller/api/offers/route.js +0 -63
  97. package/dist/features/seller/components/SellerAuctionsView.d.ts +0 -9
  98. package/dist/features/seller/components/SellerAuctionsView.js +0 -113
  99. package/dist/features/seller/components/SellerOffersPanel.d.ts +0 -21
  100. package/dist/features/seller/components/SellerOffersPanel.js +0 -160
  101. package/dist/features/seller/components/SellerPreOrdersView.d.ts +0 -6
  102. package/dist/features/seller/components/SellerPreOrdersView.js +0 -148
  103. package/dist/features/seller/components/SellerPrizeDrawsView.d.ts +0 -6
  104. package/dist/features/seller/components/SellerPrizeDrawsView.js +0 -152
  105. package/dist/features/seller/permission-map.d.ts +0 -5
  106. package/dist/features/seller/permission-map.js +0 -17
  107. package/dist/features/shell/StepForm.d.ts +0 -80
  108. package/dist/features/shell/StepForm.js +0 -136
  109. package/dist/features/stores/api/route.d.ts +0 -12
  110. package/dist/features/stores/api/route.js +0 -110
  111. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.d.ts +0 -2
  112. package/dist/features/tester/seed-data/authored/account-auth__testing-program--admin.js +0 -33
  113. package/dist/features/tester/seed-data/authored/account-auth__testing-program.d.ts +0 -2
  114. package/dist/features/tester/seed-data/authored/account-auth__testing-program.js +0 -138
  115. package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +0 -2
  116. package/dist/features/tester/seed-data/bids-tester-seed-data.js +0 -37
  117. package/dist/features/tester/seed-data/blog-tester-seed-data.d.ts +0 -2
  118. package/dist/features/tester/seed-data/blog-tester-seed-data.js +0 -40
  119. package/dist/features/tester/seed-data/carts-tester-seed-data.d.ts +0 -3
  120. package/dist/features/tester/seed-data/carts-tester-seed-data.js +0 -70
  121. package/dist/features/tester/seed-data/categories-tester-seed-data.d.ts +0 -2
  122. package/dist/features/tester/seed-data/categories-tester-seed-data.js +0 -218
  123. package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +0 -2
  124. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +0 -76
  125. package/dist/features/tester/seed-data/events-tester-seed-data.d.ts +0 -2
  126. package/dist/features/tester/seed-data/events-tester-seed-data.js +0 -193
  127. package/dist/features/tester/seed-data/offers-tester-seed-data.d.ts +0 -2
  128. package/dist/features/tester/seed-data/offers-tester-seed-data.js +0 -191
  129. package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +0 -2
  130. package/dist/features/tester/seed-data/orders-tester-seed-data.js +0 -329
  131. package/dist/features/tester/seed-data/products-tester-seed-data.d.ts +0 -2
  132. package/dist/features/tester/seed-data/products-tester-seed-data.js +0 -815
  133. package/dist/features/tester/seed-data/stores-tester-seed-data.d.ts +0 -2
  134. package/dist/features/tester/seed-data/stores-tester-seed-data.js +0 -79
  135. package/dist/features/wishlist/components/WishlistToggleButton.d.ts +0 -12
  136. package/dist/features/wishlist/components/WishlistToggleButton.js +0 -36
  137. package/dist/monitoring/analytics.d.ts +0 -80
  138. package/dist/monitoring/analytics.js +0 -176
  139. package/dist/next/middleware/pii-redactor.d.ts +0 -32
  140. package/dist/next/middleware/pii-redactor.js +0 -84
  141. package/dist/providers/db-firebase/realtime.d.ts +0 -42
  142. package/dist/providers/db-firebase/realtime.js +0 -138
  143. package/dist/security/rbac/Can.d.ts +0 -31
  144. package/dist/security/rbac/Can.js +0 -22
  145. package/dist/security/rbac/default-roles.d.ts +0 -2
  146. package/dist/security/rbac/default-roles.js +0 -83
  147. package/dist/security/rbac/hook.d.ts +0 -20
  148. package/dist/security/rbac/hook.js +0 -30
  149. package/dist/security/rbac/index.d.ts +0 -8
  150. package/dist/security/rbac/index.js +0 -6
  151. package/dist/security/rbac/middleware.d.ts +0 -51
  152. package/dist/security/rbac/middleware.js +0 -51
  153. package/dist/security/rbac/resolver.d.ts +0 -5
  154. package/dist/security/rbac/resolver.js +0 -38
  155. package/dist/security/rbac/server.d.ts +0 -24
  156. package/dist/security/rbac/server.js +0 -42
  157. package/dist/security/rbac/types.d.ts +0 -22
  158. package/dist/security/rbac/types.js +0 -2
  159. package/dist/seed/conversations-seed-data.d.ts +0 -2
  160. package/dist/seed/conversations-seed-data.js +0 -292
  161. package/dist/utils/filter.helper.d.ts +0 -18
  162. package/dist/utils/filter.helper.js +0 -21
  163. package/dist/utils/pagination.helper.d.ts +0 -23
  164. package/dist/utils/pagination.helper.js +0 -24
  165. package/dist/utils/search-tokens.d.ts +0 -2
  166. package/dist/utils/search-tokens.js +0 -21
@@ -1,88 +0,0 @@
1
- /**
2
- * Password Change OTP Actions.
3
- *
4
- * Server-side gate for POST /api/user/change-password — see
5
- * ../password-change-otp.ts for the full incident writeup. Mirrors
6
- * checkout-value-otp-actions.ts's shape exactly (same crypto primitives,
7
- * separate Firestore namespace and copy).
8
- */
9
- import { timingSafeEqual } from "crypto";
10
- import { ValidationError } from "../../../errors";
11
- import { serverLogger } from "../../../monitoring";
12
- import { sendEmail } from "../../contact/email";
13
- import { getAdminDb } from "../../../providers/db-firebase";
14
- import { resolveDate } from "../../../utils";
15
- import { hashOtp, generateOtpCode, PASSWORD_CHANGE_OTP_EXPIRY_MS, PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, passwordChangeOtpRef, enforcePasswordChangeOtpRateLimit, } from "../password-change-otp";
16
- /** Sends a password-change OTP to the account's registered email. Returns masked email so the UI can confirm which inbox to check. */
17
- export async function sendPasswordChangeOtp(userId, userEmail) {
18
- const db = getAdminDb();
19
- await enforcePasswordChangeOtpRateLimit(db, userId);
20
- const code = generateOtpCode();
21
- const codeHash = hashOtp(code);
22
- const expiresAt = new Date(Date.now() + PASSWORD_CHANGE_OTP_EXPIRY_MS);
23
- await passwordChangeOtpRef(db, userId).set({
24
- codeHash,
25
- expiresAt,
26
- attempts: 0,
27
- verified: false,
28
- createdAt: new Date(),
29
- });
30
- const siteName = process.env.NEXT_PUBLIC_SITE_NAME || "LetItRip";
31
- const [local, domain] = userEmail.split("@");
32
- const maskedEmail = domain
33
- ? `${local.length <= 2 ? "*".repeat(local.length) : local[0] + "*".repeat(local.length - 2) + local[local.length - 1]}@${domain}`
34
- : "***";
35
- await sendEmail({
36
- to: userEmail,
37
- subject: `${siteName}: Verify your password change`,
38
- html: `
39
- <div style="font-family:sans-serif;max-width:480px;margin:0 auto;padding:24px">
40
- <h2 style="margin-bottom:8px">Confirm Password Change</h2>
41
- <p style="color:#555">Someone requested to change this account's password. Enter this code to confirm it was you:</p>
42
- <div style="font-size:36px;font-weight:bold;letter-spacing:8px;text-align:center;margin:24px 0;padding:16px;background:#f3f4f6;border-radius:8px">${code}</div>
43
- <p style="color:#888;font-size:12px">This code expires in 10 minutes. If you did not request this, your password has NOT been changed — but consider securing your account.</p>
44
- </div>
45
- `,
46
- });
47
- serverLogger.info(`Password change OTP sent: uid=${userId}`);
48
- return { maskedEmail };
49
- }
50
- /** Verifies the 6-digit password-change OTP and marks the Firestore record verified. */
51
- export async function verifyPasswordChangeOtp(userId, code) {
52
- const db = getAdminDb();
53
- const ref = passwordChangeOtpRef(db, userId);
54
- const snap = await ref.get();
55
- if (!snap.exists) {
56
- throw new ValidationError("No verification code found. Please request a new code.");
57
- }
58
- const otpDoc = snap.data();
59
- if (otpDoc.verified)
60
- return;
61
- if (otpDoc.attempts >= PASSWORD_CHANGE_OTP_MAX_ATTEMPTS) {
62
- throw new ValidationError("Too many failed attempts. Please request a new code.");
63
- }
64
- if (Date.now() > (resolveDate(otpDoc.expiresAt)?.getTime() ?? 0)) {
65
- throw new ValidationError("Code expired. Please request a new one.");
66
- }
67
- const inputHash = hashOtp(code);
68
- if (!timingSafeEqual(Buffer.from(inputHash, "hex"), Buffer.from(otpDoc.codeHash, "hex"))) {
69
- await ref.update({ attempts: otpDoc.attempts + 1 });
70
- throw new ValidationError("Invalid code. Please check and try again.");
71
- }
72
- await ref.update({ verified: true, verifiedAt: new Date() });
73
- serverLogger.info(`Password change OTP verified: uid=${userId}`);
74
- }
75
- /** Whether the account has a currently-verified password-change OTP on file. */
76
- export async function isPasswordChangeOtpVerified(userId) {
77
- const db = getAdminDb();
78
- const snap = await passwordChangeOtpRef(db, userId).get();
79
- if (!snap.exists)
80
- return false;
81
- const otpDoc = snap.data();
82
- return otpDoc.verified === true;
83
- }
84
- /** Consumes the verified OTP so it can't be reused for a future password change. */
85
- export async function consumePasswordChangeOtp(userId) {
86
- const db = getAdminDb();
87
- await passwordChangeOtpRef(db, userId).delete();
88
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * Realtime Token Domain Actions — appkit
3
- *
4
- * Issues a Firebase custom auth token for Realtime Database subscriptions.
5
- * Called by letitrip thin wrapper which handles auth + rate limiting.
6
- */
7
- export interface RealtimeTokenResult {
8
- customToken: string;
9
- expiresAt: number;
10
- }
11
- export declare function issueRealtimeToken(userId: string, userRole: string): Promise<RealtimeTokenResult>;
@@ -1,38 +0,0 @@
1
- /**
2
- * Realtime Token Domain Actions — appkit
3
- *
4
- * Issues a Firebase custom auth token for Realtime Database subscriptions.
5
- * Called by letitrip thin wrapper which handles auth + rate limiting.
6
- */
7
- import { getAdminAuth } from "../../../providers/db-firebase/index";
8
- import { conversationsRepository } from "../../messages/repository/conversations.repository";
9
- import { storeRepository } from "../../stores/repository/store.repository";
10
- import { serverLogger } from "../../../monitoring/index";
11
- import { normalizeError } from "../../../errors/normalize";
12
- export async function issueRealtimeToken(userId, userRole) {
13
- const conversationIds = {};
14
- try {
15
- const buyerConvs = await conversationsRepository.listByBuyer(userId);
16
- buyerConvs.forEach((c) => { conversationIds[c.id] = true; });
17
- if (userRole === "seller") {
18
- const store = await storeRepository.findByOwnerId(userId);
19
- if (store?.id) {
20
- const sellerConvs = await conversationsRepository.listByStore(store.id);
21
- sellerConvs.forEach((c) => { conversationIds[c.id] = true; });
22
- }
23
- }
24
- }
25
- catch (err) {
26
- void normalizeError(err);
27
- serverLogger.warn("Could not resolve conversationIds for realtime token", { userId, err });
28
- }
29
- const customToken = await getAdminAuth().createCustomToken(userId, {
30
- role: userRole,
31
- conversationIds,
32
- });
33
- serverLogger.info("issueRealtimeToken: token issued", {
34
- userId,
35
- conversationCount: Object.keys(conversationIds).length,
36
- });
37
- return { customToken, expiresAt: Date.now() + 3600000 };
38
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * Password Change OTP.
3
- *
4
- * Root-caused 2026-08-20: POST /api/user/change-password validated the
5
- * *shape* of `currentPassword` (Zod: non-empty, differs from newPassword)
6
- * but the handler never read or verified it — `getAdminAuth().updateUser()`
7
- * overwrote the password unconditionally for whatever `uid` the session
8
- * cookie carried. The only real current-password check
9
- * (`reauthenticateWithCredential` in the Firebase client SDK) happens in
10
- * the browser and is trivially bypassed by calling the API directly with a
11
- * stolen session cookie — a session hijack (XSS, log leak, etc.) was
12
- * enough to take over the account's password with zero server-enforced
13
- * identity check. This module gates the actual server-side write behind an
14
- * email OTP, verified server-side, mirroring the Tier PP checkout-value-OTP
15
- * pattern exactly (same crypto primitives, reused not duplicated) so a
16
- * session-cookie thief also needs access to the account's inbox.
17
- */
18
- import { hashOtp, generateOtpCode, CHECKOUT_VALUE_OTP_EXPIRY_MS as PASSWORD_CHANGE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_EXPIRY_MINUTES as PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, CHECKOUT_VALUE_OTP_COOLDOWN_MS as PASSWORD_CHANGE_OTP_COOLDOWN_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS as PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, CHECKOUT_VALUE_OTP_VERIFY_RATE_LIMIT as PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT } from "./checkout-value-otp";
19
- export { hashOtp, generateOtpCode, PASSWORD_CHANGE_OTP_EXPIRY_MS, PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, PASSWORD_CHANGE_OTP_COOLDOWN_MS, PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT, };
20
- type Db = FirebaseFirestore.Firestore;
21
- /** Single-slot doc per user — a user can only be mid-password-change once. */
22
- export declare function passwordChangeOtpRef(db: Db, uid: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
23
- export declare function passwordChangeOtpRateLimitRef(db: Db, uid: string): FirebaseFirestore.DocumentReference<FirebaseFirestore.DocumentData, FirebaseFirestore.DocumentData>;
24
- export interface PasswordChangeOtpDoc {
25
- codeHash: string;
26
- expiresAt: FirebaseFirestore.Timestamp;
27
- attempts: number;
28
- verified: boolean;
29
- }
30
- /**
31
- * Enforce the per-user send rate-limit.
32
- * Throws AuthorizationError("passwordChangeOtpRateLimit") when throttled.
33
- */
34
- export declare function enforcePasswordChangeOtpRateLimit(db: Db, uid: string): Promise<void>;
@@ -1,56 +0,0 @@
1
- /**
2
- * Password Change OTP.
3
- *
4
- * Root-caused 2026-08-20: POST /api/user/change-password validated the
5
- * *shape* of `currentPassword` (Zod: non-empty, differs from newPassword)
6
- * but the handler never read or verified it — `getAdminAuth().updateUser()`
7
- * overwrote the password unconditionally for whatever `uid` the session
8
- * cookie carried. The only real current-password check
9
- * (`reauthenticateWithCredential` in the Firebase client SDK) happens in
10
- * the browser and is trivially bypassed by calling the API directly with a
11
- * stolen session cookie — a session hijack (XSS, log leak, etc.) was
12
- * enough to take over the account's password with zero server-enforced
13
- * identity check. This module gates the actual server-side write behind an
14
- * email OTP, verified server-side, mirroring the Tier PP checkout-value-OTP
15
- * pattern exactly (same crypto primitives, reused not duplicated) so a
16
- * session-cookie thief also needs access to the account's inbox.
17
- */
18
- import { AuthorizationError } from "../../errors";
19
- import { resolveDate } from "../../utils";
20
- import { USER_COLLECTION } from "./schemas";
21
- import { hashOtp, generateOtpCode, CHECKOUT_VALUE_OTP_EXPIRY_MS as PASSWORD_CHANGE_OTP_EXPIRY_MS, CHECKOUT_VALUE_OTP_EXPIRY_MINUTES as PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, CHECKOUT_VALUE_OTP_COOLDOWN_MS as PASSWORD_CHANGE_OTP_COOLDOWN_MS, CHECKOUT_VALUE_OTP_MAX_ATTEMPTS as PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, CHECKOUT_VALUE_OTP_VERIFY_RATE_LIMIT as PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT, } from "./checkout-value-otp";
22
- export { hashOtp, generateOtpCode, PASSWORD_CHANGE_OTP_EXPIRY_MS, PASSWORD_CHANGE_OTP_EXPIRY_MINUTES, PASSWORD_CHANGE_OTP_COOLDOWN_MS, PASSWORD_CHANGE_OTP_MAX_ATTEMPTS, PASSWORD_CHANGE_OTP_VERIFY_RATE_LIMIT, };
23
- /** Single-slot doc per user — a user can only be mid-password-change once. */
24
- export function passwordChangeOtpRef(db, uid) {
25
- return db
26
- .collection(USER_COLLECTION)
27
- .doc(uid)
28
- .collection("passwordChangeOtps")
29
- .doc("current");
30
- }
31
- export function passwordChangeOtpRateLimitRef(db, uid) {
32
- return db
33
- .collection(USER_COLLECTION)
34
- .doc(uid)
35
- .collection("passwordChangeOtpRateLimit")
36
- .doc("meta");
37
- }
38
- /**
39
- * Enforce the per-user send rate-limit.
40
- * Throws AuthorizationError("passwordChangeOtpRateLimit") when throttled.
41
- */
42
- export async function enforcePasswordChangeOtpRateLimit(db, uid) {
43
- const metaRef = passwordChangeOtpRateLimitRef(db, uid);
44
- await db.runTransaction(async (tx) => {
45
- const metaSnap = await tx.get(metaRef);
46
- const meta = metaSnap.exists
47
- ? metaSnap.data()
48
- : null;
49
- const lastSentMs = resolveDate(meta?.lastSentAt)?.getTime() ?? 0;
50
- const elapsed = Date.now() - lastSentMs;
51
- if (elapsed < PASSWORD_CHANGE_OTP_COOLDOWN_MS) {
52
- throw new AuthorizationError("passwordChangeOtpRateLimit");
53
- }
54
- tx.set(metaRef, { lastSentAt: new Date() }, { merge: true });
55
- });
56
- }
@@ -1,12 +0,0 @@
1
- import React from "react";
2
- import type { startClassifiedConversationAction } from "../../../_internal/server/features/classified/actions";
3
- export interface ClassifiedContactSellerPanelProps {
4
- productId: string;
5
- onContactSeller: typeof startClassifiedConversationAction;
6
- }
7
- /**
8
- * The only purchase-path CTA on a classified listing — no cart, no checkout.
9
- * Extracted from the old ClassifiedDetailView so the richer, full-parity
10
- * ClassifiedDetailPageView can slot this into a `renderBuyBar` prop.
11
- */
12
- export declare function ClassifiedContactSellerPanel({ productId, onContactSeller }: ClassifiedContactSellerPanelProps): React.JSX.Element;
@@ -1,49 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { normalizeError } from "../../../errors/normalize";
4
- import { useState } from "react";
5
- import Link from "next/link";
6
- import { Button, Div, Stack, Text } from "../../../ui";
7
- import { ROUTES } from "../../../next/routing/route-map";
8
- const __P = {
9
- p4: "p-[var(--appkit-space-4)]",
10
- };
11
- /**
12
- * The only purchase-path CTA on a classified listing — no cart, no checkout.
13
- * Extracted from the old ClassifiedDetailView so the richer, full-parity
14
- * ClassifiedDetailPageView can slot this into a `renderBuyBar` prop.
15
- */
16
- export function ClassifiedContactSellerPanel({ productId, onContactSeller }) {
17
- const [pending, setPending] = useState(false);
18
- const [conversation, setConversation] = useState(null);
19
- const [error, setError] = useState(null);
20
- async function handleContactSeller() {
21
- setPending(true);
22
- setError(null);
23
- try {
24
- const result = await onContactSeller({ productId });
25
- if (result && typeof result === "object" && "ok" in result) {
26
- if (result.ok) {
27
- setConversation(result.data);
28
- }
29
- else {
30
- setError(result.error);
31
- }
32
- }
33
- else {
34
- setConversation(result);
35
- }
36
- }
37
- catch (e) {
38
- void normalizeError(e);
39
- setError("Something went wrong");
40
- }
41
- finally {
42
- setPending(false);
43
- }
44
- }
45
- if (conversation) {
46
- return (_jsxs(Div, { className: `border border-border bg-muted/40 ${__P.p4}`, rounded: "lg", children: [_jsx(Text, { className: "mb-2", weight: "medium", children: "Conversation started!" }), _jsx(Link, { href: ROUTES.USER.MESSAGES, className: "text-primary underline underline-offset-2", children: "Go to your messages \u2192" })] }));
47
- }
48
- return (_jsxs(Stack, { gap: "sm", children: [error && (_jsx(Text, { className: "text-destructive", size: "sm", children: error })), _jsx(Button, { type: "button", variant: "primary", size: "lg", isLoading: pending, disabled: pending, onClick: handleContactSeller, className: "w-full", children: "Contact Seller" })] }));
49
- }
@@ -1,12 +0,0 @@
1
- /**
2
- * feat-events — Next.js App Router API handler (GET /api/events)
3
- *
4
- * Consuming projects can create a 2-line stub:
5
- *
6
- * ```ts
7
- * // app/api/events/route.ts
8
- * export { GET } from "@mohasinac/feat-events";
9
- * ```
10
- */
11
- import { NextResponse } from "next/server.js";
12
- export declare function GET(request: Request): Promise<NextResponse>;
@@ -1,90 +0,0 @@
1
- /**
2
- * feat-events — Next.js App Router API handler (GET /api/events)
3
- *
4
- * Consuming projects can create a 2-line stub:
5
- *
6
- * ```ts
7
- * // app/api/events/route.ts
8
- * export { GET } from "@mohasinac/feat-events";
9
- * ```
10
- */
11
- import { NextResponse } from "next/server.js";
12
- import { getProviders } from "../../../contracts";
13
- import { normalizeError } from "../../../errors/normalize";
14
- function param(url, key) {
15
- return url.searchParams.get(key);
16
- }
17
- function numParam(url, key, fallback) {
18
- const v = url.searchParams.get(key);
19
- const n = v !== null ? Number(v) : NaN;
20
- return Number.isFinite(n) ? n : fallback;
21
- }
22
- const SAFE_EVENT_FILTER_FIELDS = new Set([
23
- "status",
24
- "title",
25
- "type",
26
- "startsAt",
27
- "endsAt",
28
- "createdAt",
29
- ]);
30
- function validateSieveFilters(raw, allowedFields) {
31
- return raw
32
- .split(",")
33
- .map((c) => c.trim())
34
- .filter((c) => {
35
- const m = c.match(/^([^<>=!@]+)\s*(?:==|!=|<=|>=|<|>|@=\*?)/);
36
- return m ? allowedFields.has(m[1].trim()) : false;
37
- })
38
- .join(",");
39
- }
40
- // ——— GET /api/events ————————————————————————————————
41
- export async function GET(request) {
42
- try {
43
- const url = new URL(request.url);
44
- const page = numParam(url, "page", 1);
45
- const pageSize = numParam(url, "pageSize", 24);
46
- const sort = param(url, "sorts") ?? param(url, "sort") ?? "-startsAt";
47
- const q = param(url, "q");
48
- const raw = param(url, "filters");
49
- const safe = raw ? validateSieveFilters(raw, SAFE_EVENT_FILTER_FIELDS) : "";
50
- // Only default to active events when no explicit status filter is provided.
51
- const hasStatusFilter = safe.includes("status==");
52
- const parts = hasStatusFilter ? [] : ["status==active"];
53
- if (q)
54
- parts.push(`title@=*${q}`);
55
- if (safe)
56
- parts.push(safe);
57
- const filters = parts.join(",");
58
- const { db } = getProviders();
59
- if (!db) {
60
- return NextResponse.json({ success: false, error: "Database provider not registered" }, { status: 503 });
61
- }
62
- const repo = db.getRepository("events");
63
- const result = await repo.findAll({
64
- filters,
65
- sort,
66
- page,
67
- perPage: pageSize,
68
- });
69
- const totalPages = Math.max(1, Math.ceil(result.total / pageSize));
70
- // Strip internal fields (createdBy) before returning
71
- const items = result.data.map((event) => {
72
- const { createdBy: _createdBy, ...publicEvent } = event;
73
- return publicEvent;
74
- });
75
- const body = {
76
- items: items,
77
- total: result.total,
78
- page: result.page,
79
- pageSize,
80
- totalPages,
81
- hasMore: result.page < totalPages,
82
- };
83
- return NextResponse.json({ success: true, data: body });
84
- }
85
- catch (error) {
86
- void normalizeError(error);
87
- console.error("[feat-events] GET /api/events failed", error);
88
- return NextResponse.json({ success: false, error: "Failed to fetch events" }, { status: 500 });
89
- }
90
- }
@@ -1,11 +0,0 @@
1
- /**
2
- * feat-faq — Next.js App Router API handler (GET /api/faq)
3
- *
4
- * 2-line stub:
5
- * ```ts
6
- * // app/api/faq/route.ts
7
- * export { GET } from "@mohasinac/feat-faq";
8
- * ```
9
- */
10
- import { NextResponse } from "next/server.js";
11
- export declare function GET(request: Request): Promise<NextResponse>;
@@ -1,52 +0,0 @@
1
- /**
2
- * feat-faq — Next.js App Router API handler (GET /api/faq)
3
- *
4
- * 2-line stub:
5
- * ```ts
6
- * // app/api/faq/route.ts
7
- * export { GET } from "@mohasinac/feat-faq";
8
- * ```
9
- */
10
- import { NextResponse } from "next/server.js";
11
- import { getProviders } from "../../../contracts";
12
- import { normalizeError } from "../../../errors/normalize";
13
- function param(url, key) {
14
- return url.searchParams.get(key);
15
- }
16
- export async function GET(request) {
17
- try {
18
- const url = new URL(request.url);
19
- const parts = ["isActive==true"];
20
- const category = param(url, "category");
21
- if (category)
22
- parts.push(`category==${category}`);
23
- const homepage = param(url, "homepage");
24
- if (homepage === "true")
25
- parts.push("showOnHomepage==true");
26
- const q = param(url, "q");
27
- if (q)
28
- parts.push(`question@=*${q}`);
29
- const filters = parts.join(",");
30
- const { db } = getProviders();
31
- if (!db) {
32
- return NextResponse.json({ success: false, error: "Database provider not registered" }, { status: 503 });
33
- }
34
- const repo = db.getRepository("faqs");
35
- const result = await repo.findAll({
36
- filters,
37
- sort: "order",
38
- order: "asc",
39
- perPage: 100,
40
- });
41
- const body = {
42
- items: result.data,
43
- total: result.total,
44
- };
45
- return NextResponse.json({ success: true, data: body });
46
- }
47
- catch (error) {
48
- void normalizeError(error);
49
- console.error("[feat-faq] GET /api/faq failed", error);
50
- return NextResponse.json({ success: false, error: "Failed to fetch FAQs" }, { status: 500 });
51
- }
52
- }
@@ -1,22 +0,0 @@
1
- import type { FAQ, FAQListResponse, FAQListParams } from "../types";
2
- interface UseFAQsOptions {
3
- initialData?: FAQListResponse;
4
- enabled?: boolean;
5
- endpoint?: string;
6
- }
7
- export declare function useFAQs(params?: FAQListParams, opts?: UseFAQsOptions): {
8
- faqs: FAQ[];
9
- total: number;
10
- isLoading: boolean;
11
- error: Error | null;
12
- };
13
- export declare function useFAQ(id: string, opts?: {
14
- initialData?: FAQ;
15
- enabled?: boolean;
16
- endpoint?: string;
17
- }): {
18
- faq: FAQ | undefined;
19
- isLoading: boolean;
20
- error: Error | null;
21
- };
22
- export {};
@@ -1,41 +0,0 @@
1
- import { useQuery } from "@tanstack/react-query";
2
- import { apiClient } from "../../../http";
3
- import { FAQ_ENDPOINTS } from "../../../constants/api-endpoints";
4
- export function useFAQs(params = {}, opts) {
5
- const sp = new URLSearchParams();
6
- if (params.category)
7
- sp.set("category", params.category);
8
- if (params.homepage !== undefined)
9
- sp.set("homepage", String(params.homepage));
10
- if (params.q)
11
- sp.set("q", params.q);
12
- if (params.page)
13
- sp.set("page", String(params.page));
14
- if (params.perPage)
15
- sp.set("perPage", String(params.perPage));
16
- const qs = sp.toString();
17
- const endpoint = opts?.endpoint ?? `${FAQ_ENDPOINTS.LIST}${qs ? `?${qs}` : ""}`;
18
- const query = useQuery({
19
- queryKey: ["faqs", qs],
20
- queryFn: () => apiClient.get(endpoint),
21
- initialData: opts?.initialData,
22
- enabled: opts?.enabled,
23
- staleTime: 10 * 60 * 1000,
24
- });
25
- return {
26
- faqs: query.data?.items ?? [],
27
- total: query.data?.total ?? 0,
28
- isLoading: query.isLoading,
29
- error: query.error,
30
- };
31
- }
32
- export function useFAQ(id, opts) {
33
- const endpoint = opts?.endpoint ?? FAQ_ENDPOINTS.BY_ID(id);
34
- const query = useQuery({
35
- queryKey: ["faqs", id],
36
- queryFn: () => apiClient.get(endpoint),
37
- initialData: opts?.initialData,
38
- enabled: opts?.enabled !== false && !!id,
39
- });
40
- return { faq: query.data, isLoading: query.isLoading, error: query.error };
41
- }
@@ -1 +0,0 @@
1
- export {};
@@ -1,42 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { buildPosterFilename, extractVideoPosterFrame, posterBlobAsFile, } from "./video-poster";
3
- describe("buildPosterFilename", () => {
4
- it("strips the extension and appends .poster.jpg", () => {
5
- expect(buildPosterFilename("vid-123.mp4")).toBe("vid-123.poster.jpg");
6
- expect(buildPosterFilename("hero.webm")).toBe("hero.poster.jpg");
7
- expect(buildPosterFilename("Sunset.MOV")).toBe("Sunset.poster.jpg");
8
- });
9
- it("handles names with multiple dots — only the last segment is the extension", () => {
10
- expect(buildPosterFilename("clip.v2.mp4")).toBe("clip.v2.poster.jpg");
11
- expect(buildPosterFilename("2026.01.01-recap.webm")).toBe("2026.01.01-recap.poster.jpg");
12
- });
13
- it("works when the source has no extension", () => {
14
- expect(buildPosterFilename("untitled")).toBe("untitled.poster.jpg");
15
- });
16
- });
17
- describe("posterBlobAsFile", () => {
18
- it("wraps the blob as an image/jpeg File with the derived filename", () => {
19
- const blob = new Blob([new Uint8Array([0xff, 0xd8, 0xff])], {
20
- type: "image/jpeg",
21
- });
22
- const file = posterBlobAsFile({ blob, width: 320, height: 180, duration: 12.5 }, "promo-reel.mp4");
23
- expect(file).toBeInstanceOf(File);
24
- expect(file.name).toBe("promo-reel.poster.jpg");
25
- expect(file.type).toBe("image/jpeg");
26
- expect(file.size).toBe(3);
27
- });
28
- });
29
- describe("extractVideoPosterFrame — early-return paths", () => {
30
- it("returns null for a non-video File without touching the DOM", async () => {
31
- const file = new File([new Uint8Array([1, 2, 3])], "photo.jpg", {
32
- type: "image/jpeg",
33
- });
34
- const result = await extractVideoPosterFrame(file);
35
- expect(result).toBeNull();
36
- });
37
- it("returns null when the File has an empty MIME type", async () => {
38
- const file = new File([new Uint8Array([1])], "mystery.bin", { type: "" });
39
- const result = await extractVideoPosterFrame(file);
40
- expect(result).toBeNull();
41
- });
42
- });
@@ -1,2 +0,0 @@
1
- export * from "./messages-actions";
2
- export * from "./ping-rtdb";
@@ -1,2 +0,0 @@
1
- export * from "./messages-actions";
2
- export * from "./ping-rtdb";
@@ -1,15 +0,0 @@
1
- import { type ConversationsRepository } from "../repository/conversations.repository";
2
- import type { ConversationDocument, ConversationMessage } from "../schemas/firestore";
3
- export declare const MESSAGE_MAX_LENGTH = 2000;
4
- export declare function listConversationsForBuyer(buyerId: string): Promise<ConversationDocument[]>;
5
- export declare function listConversationsForStore(storeId: string): Promise<ConversationDocument[]>;
6
- export declare function getConversation(conversationId: string): Promise<ConversationDocument | null>;
7
- export interface SendMessageInput {
8
- conversationId: string;
9
- senderId: string;
10
- senderRole: "buyer" | "seller";
11
- body: string;
12
- }
13
- export declare function sendMessage(input: SendMessageInput): Promise<ConversationDocument>;
14
- export declare function markConversationRead(conversationId: string, role: "buyer" | "seller"): Promise<void>;
15
- export type { ConversationsRepository, ConversationDocument, ConversationMessage };
@@ -1,33 +0,0 @@
1
- /**
2
- * Messages domain actions — pure business functions; auth/rate-limit is the
3
- * caller's responsibility.
4
- */
5
- import { ValidationError } from "../../../errors";
6
- import { ERROR_MESSAGES } from "../../../errors/messages";
7
- import { conversationsRepository, } from "../repository/conversations.repository";
8
- export const MESSAGE_MAX_LENGTH = 2000;
9
- export async function listConversationsForBuyer(buyerId) {
10
- return conversationsRepository.listByBuyer(buyerId);
11
- }
12
- export async function listConversationsForStore(storeId) {
13
- return conversationsRepository.listByStore(storeId);
14
- }
15
- export async function getConversation(conversationId) {
16
- return conversationsRepository.findById(conversationId);
17
- }
18
- export async function sendMessage(input) {
19
- const body = input.body?.trim();
20
- if (!body)
21
- throw new ValidationError(ERROR_MESSAGES.CONVERSATIONS.BODY_REQUIRED);
22
- if (body.length > MESSAGE_MAX_LENGTH) {
23
- throw new ValidationError(`${ERROR_MESSAGES.CONVERSATIONS.BODY_TOO_LONG} (max ${MESSAGE_MAX_LENGTH} characters)`);
24
- }
25
- return conversationsRepository.appendMessage(input.conversationId, {
26
- senderId: input.senderId,
27
- senderRole: input.senderRole,
28
- body,
29
- });
30
- }
31
- export async function markConversationRead(conversationId, role) {
32
- await conversationsRepository.markRead(conversationId, role);
33
- }
@@ -1,2 +0,0 @@
1
- import { type ConversationPingTargets } from "../realtime";
2
- export declare function pingConversationRtdb(targets: ConversationPingTargets): Promise<void>;