@mohasinac/appkit 4.30.17 → 4.30.19

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 (163) hide show
  1. package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +5 -2
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -2
  4. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +5 -2
  5. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +11 -3
  6. package/dist/_internal/client/hand-mode/HandModeProvider.js +2 -0
  7. package/dist/_internal/server/features/auctions/actions.js +14 -3
  8. package/dist/_internal/server/features/auctions/service.js +1 -1
  9. package/dist/_internal/server/features/blog/data.js +2 -2
  10. package/dist/_internal/server/features/blog/service.js +3 -3
  11. package/dist/_internal/server/features/brands/data.js +1 -1
  12. package/dist/_internal/server/features/brands/service.js +3 -3
  13. package/dist/_internal/server/features/bundles/actions.js +6 -1
  14. package/dist/_internal/server/features/bundles/data.js +21 -4
  15. package/dist/_internal/server/features/cart/data.js +3 -1
  16. package/dist/_internal/server/features/cart/service.js +13 -2
  17. package/dist/_internal/server/features/categories/data.js +12 -3
  18. package/dist/_internal/server/features/events/actions.js +17 -4
  19. package/dist/_internal/server/features/events/data.js +1 -1
  20. package/dist/_internal/server/features/events/service.js +1 -1
  21. package/dist/_internal/server/features/grouped/data.js +7 -2
  22. package/dist/_internal/server/features/item-requests/data.js +1 -1
  23. package/dist/_internal/server/features/lottery/actions.js +2 -2
  24. package/dist/_internal/server/features/lottery/data.js +1 -1
  25. package/dist/_internal/server/features/orders/actions.js +7 -7
  26. package/dist/_internal/server/features/orders/data.d.ts +1 -1
  27. package/dist/_internal/server/features/orders/data.js +14 -2
  28. package/dist/_internal/server/features/orders/service.js +1 -1
  29. package/dist/_internal/server/features/pre-orders/service.js +1 -1
  30. package/dist/_internal/server/features/prize-draws/actions.js +1 -1
  31. package/dist/_internal/server/features/products/data.js +7 -2
  32. package/dist/_internal/server/features/products/service.js +1 -1
  33. package/dist/_internal/server/features/promotions/actions.js +1 -1
  34. package/dist/_internal/server/features/promotions/data.js +1 -1
  35. package/dist/_internal/server/features/promotions/service.js +1 -1
  36. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  37. package/dist/_internal/server/features/reviews/data.js +6 -1
  38. package/dist/_internal/server/features/reviews/og-data.js +1 -1
  39. package/dist/_internal/server/features/shared/listing-data-factory.js +7 -2
  40. package/dist/_internal/server/features/stores/data.js +1 -1
  41. package/dist/_internal/server/features/sublisting-categories/data.js +1 -1
  42. package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -1
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +7 -15
  44. package/dist/_internal/server/jobs/core/revenueRollup.js +7 -1
  45. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +27 -2
  46. package/dist/client/api/useApiQuery.js +17 -6
  47. package/dist/client.d.ts +1 -0
  48. package/dist/client.js +3 -0
  49. package/dist/contracts/client-realtime.d.ts +6 -0
  50. package/dist/contracts/realtime-session.d.ts +12 -12
  51. package/dist/contracts/realtime-session.js +72 -77
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +1 -1
  54. package/dist/features/about/components/DeveloperView.js +7 -2
  55. package/dist/features/about/components/PolicyPageView.js +9 -1
  56. package/dist/features/about/components/PublicProfileView.js +23 -5
  57. package/dist/features/account/components/AddressForm.js +1 -1
  58. package/dist/features/account/components/NotificationBell.js +4 -3
  59. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  60. package/dist/features/account/components/UserOffersPanel.js +1 -1
  61. package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
  62. package/dist/features/admin/components/AdminMediaView.js +3 -3
  63. package/dist/features/analytics/types.d.ts +1 -1
  64. package/dist/features/analytics/types.js +13 -0
  65. package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
  66. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
  67. package/dist/features/auth/actions/profile-actions.js +4 -3
  68. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  69. package/dist/features/auth/components/LoginForm.js +1 -1
  70. package/dist/features/auth/components/RegisterForm.js +1 -1
  71. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  72. package/dist/features/auth/hooks/useAuth.js +3 -0
  73. package/dist/features/blog/repository/blog.repository.js +3 -1
  74. package/dist/features/cart/actions/cart-actions.js +13 -4
  75. package/dist/features/cart/utils/pending-ops.js +1 -1
  76. package/dist/features/categories/components/BrandDetailPageView.js +28 -12
  77. package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
  78. package/dist/features/categories/repository/categories.repository.js +3 -1
  79. package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
  80. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
  81. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
  82. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
  83. package/dist/features/events/components/EventBanner.js +5 -2
  84. package/dist/features/events/components/EventOfferCard.js +3 -1
  85. package/dist/features/events/components/EventPollWidget.js +1 -1
  86. package/dist/features/events/components/SpinWheelView.js +3 -2
  87. package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
  88. package/dist/features/history/hooks/useHistory.js +29 -27
  89. package/dist/features/homepage/components/AdSlot.js +3 -1
  90. package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
  91. package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
  92. package/dist/features/homepage/components/SocialFeedSection.js +4 -1
  93. package/dist/features/homepage/lib/live-stats.js +8 -1
  94. package/dist/features/homepage/repository/carousel.repository.js +2 -1
  95. package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
  96. package/dist/features/media/finalize.js +3 -1
  97. package/dist/features/media/modals/ImageEditor.js +1 -1
  98. package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
  99. package/dist/features/media/modals/VideoTrimModal.js +5 -2
  100. package/dist/features/media/upload/MediaUploadField.js +2 -2
  101. package/dist/features/messages/hooks/useConversation.js +18 -4
  102. package/dist/features/messages/hooks/useConversations.js +25 -8
  103. package/dist/features/orders/components/RefundRequestView.js +5 -2
  104. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
  105. package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
  106. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
  108. package/dist/features/products/components/ProductDetailPageView.js +8 -2
  109. package/dist/features/products/components/ShareButton.js +3 -1
  110. package/dist/features/products/repository/loadProductFeatures.js +10 -2
  111. package/dist/features/products/repository/products.repository.js +2 -1
  112. package/dist/features/promotions/components/CouponCard.js +3 -1
  113. package/dist/features/reviews/actions/review-actions.js +8 -1
  114. package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
  115. package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
  116. package/dist/features/scams/actions/scam-actions.d.ts +9 -1
  117. package/dist/features/scams/actions/scam-actions.js +52 -12
  118. package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
  119. package/dist/features/scams/components/SellerTrustBadge.js +13 -0
  120. package/dist/features/scams/repository/scammer.repository.js +2 -1
  121. package/dist/features/seller/actions/offer-actions.js +6 -1
  122. package/dist/features/seller/components/FulfillmentView.js +5 -2
  123. package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
  124. package/dist/features/seller/components/PrintCenterView.js +3 -1
  125. package/dist/features/seller/components/SellerBidsView.js +1 -1
  126. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  127. package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
  128. package/dist/features/seller/components/SellerOrdersView.js +3 -3
  129. package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
  130. package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
  131. package/dist/features/seller/components/SellerProductShell.js +15 -5
  132. package/dist/features/seller/components/SellerProductsView.js +24 -10
  133. package/dist/features/seller/components/SellerReviewsView.js +47 -16
  134. package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
  135. package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
  136. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  137. package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
  138. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
  139. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  140. package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
  141. package/dist/index.d.ts +1 -1
  142. package/dist/index.js +5 -2
  143. package/dist/next/api/routeHandler.js +3 -1
  144. package/dist/providers/auth-firebase/session.js +3 -1
  145. package/dist/providers/db-firebase/admin-app-lite.js +8 -8
  146. package/dist/providers/db-firebase/admin.js +12 -12
  147. package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
  148. package/dist/providers/db-firebase/browser-stub.js +6 -11
  149. package/dist/providers/db-firebase/index.d.ts +0 -1
  150. package/dist/providers/db-firebase/index.js +0 -1
  151. package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
  152. package/dist/providers/db-firebase/rtdb-paths.js +15 -6
  153. package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
  154. package/dist/providers/db-firebase/rtdb-url.js +50 -0
  155. package/dist/providers/firebase-client/realtime.d.ts +20 -0
  156. package/dist/providers/firebase-client/realtime.js +34 -1
  157. package/dist/providers.d.ts +1 -1
  158. package/dist/providers.js +1 -1
  159. package/dist/react/hooks/use-action-dispatch.js +8 -2
  160. package/dist/react/hooks/useFormStatePreservation.js +3 -1
  161. package/dist/react/hooks/useRealtimeEvent.js +20 -9
  162. package/dist/ui/components/toolbar-collapse.js +1 -2
  163. package/package.json +1 -1
@@ -3728,7 +3728,22 @@ const rawTesterChecklistItems = [
3728
3728
  href: "/admin/site",
3729
3729
  },
3730
3730
  { key: "admin-dashboard-widgets", label: "Admin dashboard widgets show accurate data", href: "/admin/dashboard" },
3731
- { key: "analytics-admin", label: "Admin analytics dashboard shows accurate data", href: "/admin/analytics" },
3731
+ {
3732
+ key: "analytics-admin",
3733
+ label: "Admin analytics dashboard shows accurate data",
3734
+ href: "/admin/analytics",
3735
+ },
3736
+ {
3737
+ key: "analytics-traffic-card-has-numbers",
3738
+ label: "The \"Traffic today\" card shows a real number, and it MATCHES the Page Views tab beside it",
3739
+ description: "Fixed 2026-08-29. BEFORE: the card showed an em-dash forever — never even a 0. It subscribed to two Realtime Database paths written by a presence hook whose writes had been denied since the day it shipped (the RTDB rules are backend-write-only), and both subscriptions passed no error callback, so a permission denial fired nothing at all and the tiles simply never left their initial state. AFTER: the card reads the same Firestore pageview counter that backs the Page Views tab in the same tab strip, so the two figures must agree — compare them. If the card errors it now SAYS so instead of showing a dash.",
3740
+ },
3741
+ {
3742
+ key: "analytics-no-permission-denied",
3743
+ label: "Opening /admin/analytics logs no PERMISSION_DENIED in the browser console",
3744
+ description: "Fixed 2026-08-29. BEFORE: every admin page load produced two Firebase permission_denied warnings from the dead presence/analytics subscriptions. AFTER: none — those subscriptions are gone entirely. Open devtools, hard-reload the page, and confirm the console is clean of Firebase permission errors.",
3745
+ href: "/admin/analytics",
3746
+ },
3732
3747
  {
3733
3748
  key: "pageviews-report-listing-standard",
3734
3749
  label: "The \"Page Views\" tab in Admin Analytics is a full standard listing — a search box (by entity id or URL), an entity-type filter drawer, a \"Most views\"/\"Fewest views\" sort dropdown, and real pagination — not a bare unsearchable table",
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useApiMutation, useBulkEvent, RTDB_PATHS } from "@mohasinac/appkit/client";
4
4
  import React from "react";
5
5
  import { normalizeError } from "../../../errors/normalize";
6
+ import { toUserMessage } from "../../../errors/error-display-map";
6
7
  const __P = {
7
8
  p3: "p-[var(--appkit-space-3)]",
8
9
  };
@@ -156,9 +157,9 @@ export function SellerWhatsAppSettingsView({ hasCapability }) {
156
157
  }
157
158
  }
158
159
  catch (err) {
159
- void normalizeError(err);
160
+ const e = normalizeError(err);
160
161
  setImporting(false);
161
- showToast(err instanceof Error ? err.message : "Import failed", "error");
162
+ showToast(toUserMessage(e.code, undefined, { fallback: "Failed to start catalog import." }), "error");
162
163
  }
163
164
  };
164
165
  React.useEffect(() => {
@@ -1,6 +1,8 @@
1
1
  "use client";
2
2
  import { useCallback, useState } from "react";
3
3
  import { useToast } from "../../../ui";
4
+ import { normalizeError } from "../../../errors/normalize";
5
+ import { toUserMessage } from "../../../errors/error-display-map";
4
6
  export function useWishlistToggle(productId, initial = false, actions) {
5
7
  const [inWishlist, setInWishlist] = useState(initial);
6
8
  const [isLoading, setIsLoading] = useState(false);
@@ -25,8 +27,13 @@ export function useWishlistToggle(productId, initial = false, actions) {
25
27
  }
26
28
  }
27
29
  catch (err) {
30
+ const e = normalizeError(err);
28
31
  setInWishlist(prev);
29
- showToast(err instanceof Error ? err.message : "Something went wrong.", "error");
32
+ showToast(toUserMessage(e.code, undefined, {
33
+ fallback: prev
34
+ ? "Could not remove this item from your wishlist."
35
+ : "Could not add this item to your wishlist.",
36
+ }), "error");
30
37
  throw err;
31
38
  }
32
39
  finally {
package/dist/index.d.ts CHANGED
@@ -333,6 +333,7 @@ export { RazorpayUnreachableError } from "./errors/index";
333
333
  export { mapToHttpError, HTTP_ERROR_CODES } from "./errors/index";
334
334
  export type { MappedError, HttpErrorCode } from "./errors/index";
335
335
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/index";
336
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/index";
336
337
  export type { ErrorDisplayEntry } from "./errors/index";
337
338
  export { parseJsonBody } from "./next/api/parseJsonBody";
338
339
  export { SEO_CONFIG } from "./seo/index";
@@ -849,7 +850,6 @@ export { requireRoleUser } from "./providers/auth-firebase/index";
849
850
  export { revokeUserTokens } from "./providers/auth-firebase/index";
850
851
  export { verifyIdToken } from "./providers/auth-firebase/index";
851
852
  export { verifySessionCookie } from "./providers/auth-firebase/index";
852
- export { FirebaseRealtimeRepository } from "./providers/db-firebase/index";
853
853
  export { FirebaseRepository } from "./providers/db-firebase/index";
854
854
  export { FirebaseSieveRepository } from "./providers/db-firebase/index";
855
855
  export { RTDB_PATHS } from "./providers/db-firebase/index";
package/dist/index.js CHANGED
@@ -568,6 +568,11 @@ export { mapToHttpError, HTTP_ERROR_CODES } from "./errors/index";
568
568
  // [UTIL]-Pure utility — client-safe lookup of inline-field + i18n key per error code.
569
569
  // ERROR_DISPLAY_MAP - CLAUDE.md Rule #9 source-of-truth for client error rendering.
570
570
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/index";
571
+ // [UTIL]-Pure utility — the ONLY sanctioned way to turn a failure into user copy.
572
+ // toUserMessage - resolves a stable error code through errors.codes.*; terminates
573
+ // in a constant, so a thrown value's own .message can never reach a user
574
+ // (audit-raw-error-text).
575
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/index";
571
576
  // [SERVER-ONLY] parseJsonBody — strict JSON request-body parser for API routes.
572
577
  // Replaces the silent `request.json().catch(() => ({}))` anti-pattern. Throws
573
578
  // ValidationError on malformed JSON → routeHandler wrapper maps to 400.
@@ -1726,8 +1731,6 @@ export { verifyIdToken } from "./providers/auth-firebase/index";
1726
1731
  export { verifySessionCookie } from "./providers/auth-firebase/index";
1727
1732
  // ./providers/db-firebase/index
1728
1733
  // [DB]-Database layer â€" uses firebase-admin or another server-side DB SDK; can only run in a trusted server environment.
1729
- // FirebaseRealtimeRepository - Shared export for firebase realtime repository.
1730
- export { FirebaseRealtimeRepository } from "./providers/db-firebase/index";
1731
1734
  // [DB]-Database layer â€" uses firebase-admin or another server-side DB SDK; can only run in a trusted server environment.
1732
1735
  // FirebaseRepository - Shared export for firebase repository.
1733
1736
  export { FirebaseRepository } from "./providers/db-firebase/index";
@@ -182,10 +182,12 @@ export function createRouteHandler(options) {
182
182
  else if (options.authOptional) {
183
183
  try {
184
184
  user = await verifySession(request);
185
+ // Deliberate: this is the OPTIONAL-auth path. A bad or absent session means
186
+ // anonymous, which is a legitimate outcome here — routes that require a
187
+ // user use `auth: true`, which throws instead.
185
188
  }
186
189
  catch (_err) {
187
190
  void normalizeError(_err);
188
- // anonymous
189
191
  }
190
192
  }
191
193
  // -- Role check --------------------------------------------------------
@@ -71,8 +71,10 @@ export const firebaseSessionProvider = {
71
71
  await getAdminAuthLite().revokeRefreshTokens(decoded.uid);
72
72
  }
73
73
  catch (_err) {
74
+ // Best-effort teardown: an expired/revoked/malformed cookie cannot be
75
+ // verified, and that is the state destroySession is trying to reach —
76
+ // reporting it would alarm on the success case.
74
77
  void normalizeError(_err);
75
- // Best-effort: if cookie is already invalid, treat as already destroyed
76
78
  }
77
79
  },
78
80
  };
@@ -1,4 +1,9 @@
1
1
  import { initializeApp, getApps, cert } from "firebase-admin/app";
2
+ // One resolver, not a second copy — this file duplicated admin.ts's URL
3
+ // logic byte-for-byte, so the region-wrong fallback bug existed twice.
4
+ // Imported from its own dependency-free module, NOT from ./admin, because the
5
+ // whole point of admin-app-lite is not pulling the full admin module in.
6
+ import { resolveRtdbUrl } from "./rtdb-url";
2
7
  function nodePath() { return require("path"); }
3
8
  function nodeFs() { return require("fs"); }
4
9
  // process.cwd() is Node.js-only; access via module.require so the Edge
@@ -26,17 +31,14 @@ export function getAdminAppLite() {
26
31
  let app;
27
32
  if (nodeFs().existsSync(keyPath)) {
28
33
  const sa = JSON.parse(nodeFs().readFileSync(keyPath, "utf8"));
29
- const dbUrl = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
30
- `https://${sa.project_id}-default-rtdb.firebaseio.com`;
34
+ const dbUrl = resolveRtdbUrl();
31
35
  app = initializeApp({ credential: cert(keyPath), databaseURL: dbUrl, ...(storageBucket && { storageBucket }) });
32
36
  }
33
37
  else if (process.env.FIREBASE_ADMIN_PROJECT_ID &&
34
38
  process.env.FIREBASE_ADMIN_CLIENT_EMAIL &&
35
39
  process.env.FIREBASE_ADMIN_PRIVATE_KEY) {
36
40
  const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID.trim();
37
- const dbUrl = process.env.FIREBASE_ADMIN_DATABASE_URL?.trim() ??
38
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL?.trim() ??
39
- `https://${projectId}-default-rtdb.firebaseio.com`;
41
+ const dbUrl = resolveRtdbUrl();
40
42
  app = initializeApp({
41
43
  credential: cert({
42
44
  projectId,
@@ -63,9 +65,7 @@ export function getAdminAppLite() {
63
65
  : {};
64
66
  const projectId = firebaseConfig.projectId ?? process.env.GCLOUD_PROJECT ?? "";
65
67
  const dbUrl = firebaseConfig.databaseURL ??
66
- process.env.FIREBASE_ADMIN_DATABASE_URL ??
67
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
68
- (projectId ? `https://${projectId}-default-rtdb.firebaseio.com` : undefined);
68
+ resolveRtdbUrl();
69
69
  app = initializeApp({
70
70
  ...(dbUrl && { databaseURL: dbUrl }),
71
71
  ...(storageBucket && { storageBucket }),
@@ -1,4 +1,5 @@
1
1
  import { normalizeError } from "../../errors/normalize";
2
+ import { resolveRtdbUrl } from "./rtdb-url";
2
3
  function nodePath() { return require("path"); }
3
4
  function nodeFs() { return require("fs"); }
4
5
  // process.cwd() is Node.js-only; access via module.require so the Edge
@@ -69,17 +70,16 @@ export function getAdminApp() {
69
70
  try {
70
71
  if (nodeFs().existsSync(keyPath)) {
71
72
  const sa = JSON.parse(nodeFs().readFileSync(keyPath, "utf8"));
72
- const dbUrl = process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
73
- `https://${sa.project_id}-default-rtdb.firebaseio.com`;
73
+ // Was NEXT_PUBLIC-only here while the other two branches honoured
74
+ // FIREBASE_ADMIN_DATABASE_URL, so setting it had no effect on this path.
75
+ const dbUrl = resolveRtdbUrl();
74
76
  app = initializeApp({ credential: cert(keyPath), databaseURL: dbUrl, ...(storageBucket && { storageBucket }) });
75
77
  }
76
78
  else if (process.env.FIREBASE_ADMIN_PROJECT_ID &&
77
79
  process.env.FIREBASE_ADMIN_CLIENT_EMAIL &&
78
80
  process.env.FIREBASE_ADMIN_PRIVATE_KEY) {
79
81
  const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID.trim();
80
- const dbUrl = process.env.FIREBASE_ADMIN_DATABASE_URL?.trim() ??
81
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL?.trim() ??
82
- `https://${projectId}-default-rtdb.firebaseio.com`;
82
+ const dbUrl = resolveRtdbUrl();
83
83
  app = initializeApp({
84
84
  credential: cert({
85
85
  projectId,
@@ -105,12 +105,9 @@ export function getAdminApp() {
105
105
  ? JSON.parse(process.env.FIREBASE_CONFIG)
106
106
  : {};
107
107
  const projectId = firebaseConfig.projectId ?? process.env.GCLOUD_PROJECT ?? "";
108
- const dbUrl = firebaseConfig.databaseURL ??
109
- process.env.FIREBASE_ADMIN_DATABASE_URL ??
110
- process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL ??
111
- (projectId
112
- ? `https://${projectId}-default-rtdb.firebaseio.com`
113
- : undefined);
108
+ // FIREBASE_CONFIG is injected by the Functions runtime and is
109
+ // authoritative there; env vars are the fallback for other runtimes.
110
+ const dbUrl = firebaseConfig.databaseURL || resolveRtdbUrl();
114
111
  app = initializeApp({
115
112
  ...(dbUrl && { databaseURL: dbUrl }),
116
113
  ...(storageBucket && { storageBucket }),
@@ -153,8 +150,11 @@ export function getAdminDb() {
153
150
  db.settings({ ignoreUndefinedProperties: true });
154
151
  }
155
152
  catch (_err) {
153
+ // Firestore rejects a second settings() call on an already-initialised
154
+ // instance, which is the only way this throws (HMR reload / duplicate
155
+ // module instance re-entering the memoised bootstrap). The instance is
156
+ // fully usable either way, so there is nothing degraded to report.
156
157
  void normalizeError(_err);
157
- // Already configured — safe to ignore.
158
158
  }
159
159
  set("__mohasinac_firebase_admin_db__", db);
160
160
  return db;
@@ -22,17 +22,7 @@ export declare function prepareForFirestore(data: any): any;
22
22
  export declare function deserializeTimestamps<T>(data: any): T;
23
23
  export declare const FirebaseRepository: any;
24
24
  export declare const FirebaseSieveRepository: any;
25
- export declare const FirebaseRealtimeRepository: any;
26
25
  export declare const firebaseDbProvider: any;
27
- export declare const RTDB_PATHS: {
28
- readonly PRESENCE: "presence";
29
- readonly CHAT: "chat";
30
- readonly NOTIFICATIONS: "notifications";
31
- readonly LIVE_UPDATES: "live_updates";
32
- readonly AUTH_EVENTS: "auth_events";
33
- readonly PAYMENT_EVENTS: "payment_events";
34
- readonly BULK_EVENTS: "bulk_events";
35
- readonly SEED_EVENTS: "seed_events";
36
- };
26
+ export { RTDB_PATHS } from "./rtdb-paths";
37
27
  export { buildFirebaseClientConfig, normalizeFirebaseConfigValue } from './client-config';
38
28
  export type { FirebaseClientConfig } from './client-config';
@@ -40,17 +40,12 @@ export function deserializeTimestamps(data) {
40
40
  }
41
41
  export const FirebaseRepository = null;
42
42
  export const FirebaseSieveRepository = null;
43
- export const FirebaseRealtimeRepository = null;
44
43
  export const firebaseDbProvider = null;
45
- export const RTDB_PATHS = {
46
- PRESENCE: "presence",
47
- CHAT: "chat",
48
- NOTIFICATIONS: "notifications",
49
- LIVE_UPDATES: "live_updates",
50
- AUTH_EVENTS: "auth_events",
51
- PAYMENT_EVENTS: "payment_events",
52
- BULK_EVENTS: "bulk_events",
53
- SEED_EVENTS: "seed_events",
54
- };
44
+ // Re-exported, NOT re-declared. This file is the `browser` resolution target, so
45
+ // the byte-for-byte copy that used to live here meant client code and server
46
+ // code read two independently-maintained definitions of the same constant, with
47
+ // nothing guarding the drift. `rtdb-paths.ts` has no imports, so it is safe to
48
+ // pull into a browser bundle directly.
49
+ export { RTDB_PATHS } from "./rtdb-paths";
55
50
  // Client config builder � safe in all environments
56
51
  export { buildFirebaseClientConfig, normalizeFirebaseConfigValue } from './client-config';
@@ -20,7 +20,6 @@ export type { DocumentSnapshot } from "firebase-admin/firestore";
20
20
  export { FirebaseRepository } from "./base";
21
21
  export { BaseRepository } from "./base.repository";
22
22
  export { FirebaseSieveRepository } from "./sieve";
23
- export { FirebaseRealtimeRepository } from "./realtime";
24
23
  export { RTDB_PATHS } from "./rtdb-paths";
25
24
  import type { IDbProvider } from "../../contracts";
26
25
  export declare const firebaseDbProvider: IDbProvider;
@@ -27,7 +27,6 @@ export { firebaseFieldOps } from "./field-ops";
27
27
  export { FirebaseRepository } from "./base";
28
28
  export { BaseRepository } from "./base.repository";
29
29
  export { FirebaseSieveRepository } from "./sieve";
30
- export { FirebaseRealtimeRepository } from "./realtime";
31
30
  // Re-export RTDB paths
32
31
  export { RTDB_PATHS } from "./rtdb-paths";
33
32
  import { FirebaseRepository as _FirebaseRepository } from "./base";
@@ -1,13 +1,22 @@
1
1
  /**
2
- * Shared Realtime Database path constants for auth/payment/bulk event channels.
2
+ * Shared Realtime Database path constants for the auth/payment/bulk event channels.
3
+ *
4
+ * 🛑 Only paths that are actually referenced through this constant belong here.
5
+ * Five keys were removed on 2026-08-29 — `PRESENCE`, `CHAT`, `NOTIFICATIONS`,
6
+ * `LIVE_UPDATES`, `SEED_EVENTS` — every one with **zero consumers**, because
7
+ * each live call site hardcodes its path string instead. A constant nobody uses
8
+ * is not a source of truth; it is a second place for the value to be wrong, and
9
+ * three of those five named RTDB nodes that no longer exist at all.
10
+ *
11
+ * If you add a key, migrate the call sites to it in the same change.
12
+ *
13
+ * This module has NO imports on purpose — that is what lets `browser-stub.ts`
14
+ * re-export it rather than keep the byte-for-byte duplicate it used to carry.
15
+ * The stub is the `browser` resolution target, so that duplicate meant client
16
+ * code and server code were reading two independently-maintained copies.
3
17
  */
4
18
  export declare const RTDB_PATHS: {
5
- readonly PRESENCE: "presence";
6
- readonly CHAT: "chat";
7
- readonly NOTIFICATIONS: "notifications";
8
- readonly LIVE_UPDATES: "live_updates";
9
19
  readonly AUTH_EVENTS: "auth_events";
10
20
  readonly PAYMENT_EVENTS: "payment_events";
11
21
  readonly BULK_EVENTS: "bulk_events";
12
- readonly SEED_EVENTS: "seed_events";
13
22
  };
@@ -1,13 +1,22 @@
1
1
  /**
2
- * Shared Realtime Database path constants for auth/payment/bulk event channels.
2
+ * Shared Realtime Database path constants for the auth/payment/bulk event channels.
3
+ *
4
+ * 🛑 Only paths that are actually referenced through this constant belong here.
5
+ * Five keys were removed on 2026-08-29 — `PRESENCE`, `CHAT`, `NOTIFICATIONS`,
6
+ * `LIVE_UPDATES`, `SEED_EVENTS` — every one with **zero consumers**, because
7
+ * each live call site hardcodes its path string instead. A constant nobody uses
8
+ * is not a source of truth; it is a second place for the value to be wrong, and
9
+ * three of those five named RTDB nodes that no longer exist at all.
10
+ *
11
+ * If you add a key, migrate the call sites to it in the same change.
12
+ *
13
+ * This module has NO imports on purpose — that is what lets `browser-stub.ts`
14
+ * re-export it rather than keep the byte-for-byte duplicate it used to carry.
15
+ * The stub is the `browser` resolution target, so that duplicate meant client
16
+ * code and server code were reading two independently-maintained copies.
3
17
  */
4
18
  export const RTDB_PATHS = {
5
- PRESENCE: "presence",
6
- CHAT: "chat",
7
- NOTIFICATIONS: "notifications",
8
- LIVE_UPDATES: "live_updates",
9
19
  AUTH_EVENTS: "auth_events",
10
20
  PAYMENT_EVENTS: "payment_events",
11
21
  BULK_EVENTS: "bulk_events",
12
- SEED_EVENTS: "seed_events",
13
22
  };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Resolve the Realtime Database URL from the environment.
3
+ *
4
+ * Its own module, with **no imports**, because both `admin.ts` and
5
+ * `admin-app-lite.ts` need it and `admin-app-lite` exists precisely to avoid
6
+ * pulling in the full admin module. They previously carried byte-for-byte
7
+ * duplicate resolution logic, which meant the bug below existed twice.
8
+ *
9
+ * 🛑 Returns `undefined` rather than guessing.
10
+ *
11
+ * All six copies used to fall back to `https://${projectId}-default-rtdb.firebaseio.com`.
12
+ * That is the **us-central1** URL shape; this project's instance lives at
13
+ * `…-default-rtdb.asia-southeast1.firebasedatabase.app`. So whenever the env var
14
+ * was missing, every server RTDB write silently targeted a database that does
15
+ * not exist — and since all nine RTDB writers wrap their call in a swallowing
16
+ * try/catch, messaging, bids, job status and payment signalling would every one
17
+ * of them degrade to "nothing ever updates", with clean warn-level logs.
18
+ *
19
+ * A guessed URL cannot be correct for a non-default region, so guessing buys
20
+ * nothing and costs the ability to notice. With no `databaseURL`,
21
+ * `getAdminRealtimeDb()` fails loudly at first use instead, which is the
22
+ * outcome you want.
23
+ *
24
+ * `||` not `??`: an env var that is present but empty must fall through, and
25
+ * `??` only falls through on null/undefined.
26
+ */
27
+ export declare function resolveRtdbUrl(): string | undefined;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Resolve the Realtime Database URL from the environment.
3
+ *
4
+ * Its own module, with **no imports**, because both `admin.ts` and
5
+ * `admin-app-lite.ts` need it and `admin-app-lite` exists precisely to avoid
6
+ * pulling in the full admin module. They previously carried byte-for-byte
7
+ * duplicate resolution logic, which meant the bug below existed twice.
8
+ *
9
+ * 🛑 Returns `undefined` rather than guessing.
10
+ *
11
+ * All six copies used to fall back to `https://${projectId}-default-rtdb.firebaseio.com`.
12
+ * That is the **us-central1** URL shape; this project's instance lives at
13
+ * `…-default-rtdb.asia-southeast1.firebasedatabase.app`. So whenever the env var
14
+ * was missing, every server RTDB write silently targeted a database that does
15
+ * not exist — and since all nine RTDB writers wrap their call in a swallowing
16
+ * try/catch, messaging, bids, job status and payment signalling would every one
17
+ * of them degrade to "nothing ever updates", with clean warn-level logs.
18
+ *
19
+ * A guessed URL cannot be correct for a non-default region, so guessing buys
20
+ * nothing and costs the ability to notice. With no `databaseURL`,
21
+ * `getAdminRealtimeDb()` fails loudly at first use instead, which is the
22
+ * outcome you want.
23
+ *
24
+ * `||` not `??`: an env var that is present but empty must fall through, and
25
+ * `??` only falls through on null/undefined.
26
+ */
27
+ export function resolveRtdbUrl() {
28
+ // 🛑 Emulator first.
29
+ //
30
+ // `firebase.json` has declared a database emulator on port 9000 for a long
31
+ // time and NOTHING ever connected to it. Client writes were shielded only
32
+ // because every rule is `.write: false`; server writes use the Admin SDK and
33
+ // bypass rules entirely — so `npm run dev` wrote to PRODUCTION. A local bid
34
+ // overwrote the prod `auction-bids` node that live auction pages subscribe to,
35
+ // and a local message send wrote prod `chats/**`.
36
+ //
37
+ // `FIREBASE_DATABASE_EMULATOR_HOST` is the standard variable the Firebase CLI
38
+ // exports for exactly this, so honouring it costs one branch and makes
39
+ // `firebase emulators:start` do what everyone already assumed it did.
40
+ const emulatorHost = process.env.FIREBASE_DATABASE_EMULATOR_HOST?.trim();
41
+ if (emulatorHost) {
42
+ const ns = process.env.FIREBASE_ADMIN_PROJECT_ID?.trim() ||
43
+ process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID?.trim() ||
44
+ "default";
45
+ return `http://${emulatorHost}?ns=${ns}`;
46
+ }
47
+ return (process.env.FIREBASE_ADMIN_DATABASE_URL?.trim() ||
48
+ process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL?.trim() ||
49
+ undefined);
50
+ }
@@ -11,7 +11,27 @@ export interface FirebaseClientRealtimeConfig {
11
11
  export declare class FirebaseClientRealtimeProvider implements IClientRealtimeProvider {
12
12
  private app;
13
13
  private db;
14
+ private readonly config;
15
+ private readonly appName;
16
+ private readonly scoped;
14
17
  constructor(config: FirebaseClientRealtimeConfig);
18
+ /**
19
+ * A provider bound to its OWN Firebase app for this claim scope.
20
+ *
21
+ * 🛑 This is what makes concurrent channels safe. Firebase Auth state is
22
+ * per-app, so with one shared app every `signInWithToken` REPLACED the claims
23
+ * of every other live subscriber: a bulk job signing in with a
24
+ * `{ bulkJobId }`-only token instantly stripped `conversationIds` from an open
25
+ * messages subscription, and vice versa. Whoever signed in last won.
26
+ *
27
+ * Reference-counting fixed only the sign-OUT half of that (a finishing job no
28
+ * longer ends someone else's session). Sign-IN needed separate apps, because
29
+ * there is nothing to coordinate — two channels legitimately need two
30
+ * different claim sets at the same time.
31
+ *
32
+ * Apps are cached per scope and reused, so a remount does not leak one.
33
+ */
34
+ forScope(scope: string): IClientRealtimeProvider;
15
35
  signInWithToken(token: string): Promise<void>;
16
36
  signOut(): Promise<void>;
17
37
  subscribe(path: string, onData: (snapshot: RealtimeSnapshot) => void, onError?: (error: Error) => void): Unsubscribe;
@@ -7,12 +7,44 @@ import { getAuth, signInWithCustomToken, signOut } from "firebase/auth";
7
7
  */
8
8
  export class FirebaseClientRealtimeProvider {
9
9
  constructor(config) {
10
+ this.scoped = new Map();
10
11
  const appName = config.appName ?? "appkit-realtime";
12
+ this.config = config;
13
+ this.appName = appName;
11
14
  this.app =
12
15
  getApps().find((a) => a.name === appName) ??
13
16
  initializeApp(config.firebaseConfig, appName);
14
17
  this.db = getDatabase(this.app);
15
18
  }
19
+ /**
20
+ * A provider bound to its OWN Firebase app for this claim scope.
21
+ *
22
+ * 🛑 This is what makes concurrent channels safe. Firebase Auth state is
23
+ * per-app, so with one shared app every `signInWithToken` REPLACED the claims
24
+ * of every other live subscriber: a bulk job signing in with a
25
+ * `{ bulkJobId }`-only token instantly stripped `conversationIds` from an open
26
+ * messages subscription, and vice versa. Whoever signed in last won.
27
+ *
28
+ * Reference-counting fixed only the sign-OUT half of that (a finishing job no
29
+ * longer ends someone else's session). Sign-IN needed separate apps, because
30
+ * there is nothing to coordinate — two channels legitimately need two
31
+ * different claim sets at the same time.
32
+ *
33
+ * Apps are cached per scope and reused, so a remount does not leak one.
34
+ */
35
+ forScope(scope) {
36
+ const existing = this.scoped.get(scope);
37
+ if (existing)
38
+ return existing;
39
+ // Firebase app names are freeform, but keep them predictable in devtools.
40
+ const safe = scope.replace(/[^a-zA-Z0-9_-]/g, "-");
41
+ const child = new FirebaseClientRealtimeProvider({
42
+ ...this.config,
43
+ appName: `${this.appName}-${safe}`,
44
+ });
45
+ this.scoped.set(scope, child);
46
+ return child;
47
+ }
16
48
  async signInWithToken(token) {
17
49
  await signInWithCustomToken(getAuth(this.app), token);
18
50
  }
@@ -21,8 +53,9 @@ export class FirebaseClientRealtimeProvider {
21
53
  await signOut(getAuth(this.app));
22
54
  }
23
55
  catch (_err) {
56
+ // A secondary Firebase app that was never signed in has nothing to sign
57
+ // out of; the caller's goal (no credential on this app) already holds.
24
58
  void normalizeError(_err);
25
- // no-op — signing out of a secondary app can fail harmlessly
26
59
  }
27
60
  }
28
61
  subscribe(path, onData, onError) {
@@ -12,7 +12,7 @@
12
12
  export { registerProviders, getProviders } from "./contracts/index";
13
13
  export { configureMarketDefaults } from "./core/baseline-resolver";
14
14
  export { firebaseAuthProvider, firebaseSessionProvider, createSessionCookieFromToken, createSessionCookie, verifyIdToken, verifySessionCookie, createMiddlewareAuthChain, requireAuth, requireRole, requireAuthUser, requireRoleUser, getUserFromRequest, requireAuthFromRequest, requireRoleFromRequest, revokeUserTokens, } from "./providers/auth-firebase/index";
15
- export { firebaseDbProvider, getAdminApp, getAdminAuth, getAdminDb, getAdminStorage, getAdminRealtimeDb, _resetAdminSingletons, FirebaseRepository, BaseRepository, FirebaseSieveRepository, FirebaseRealtimeRepository, RTDB_PATHS, firebaseFieldOps, removeUndefined, prepareForFirestore, deserializeTimestamps, } from "./providers/db-firebase/index";
15
+ export { firebaseDbProvider, getAdminApp, getAdminAuth, getAdminDb, getAdminStorage, getAdminRealtimeDb, _resetAdminSingletons, FirebaseRepository, BaseRepository, FirebaseSieveRepository, RTDB_PATHS, firebaseFieldOps, removeUndefined, prepareForFirestore, deserializeTimestamps, } from "./providers/db-firebase/index";
16
16
  export type { DocumentSnapshot } from "firebase-admin/firestore";
17
17
  export { firebaseStorageProvider } from "./providers/storage-firebase/index";
18
18
  export { createResendProvider } from "./providers/email-resend/index";
package/dist/providers.js CHANGED
@@ -15,7 +15,7 @@ export { configureMarketDefaults } from "./core/baseline-resolver";
15
15
  // Auth (Firebase Admin)
16
16
  export { firebaseAuthProvider, firebaseSessionProvider, createSessionCookieFromToken, createSessionCookie, verifyIdToken, verifySessionCookie, createMiddlewareAuthChain, requireAuth, requireRole, requireAuthUser, requireRoleUser, getUserFromRequest, requireAuthFromRequest, requireRoleFromRequest, revokeUserTokens, } from "./providers/auth-firebase/index";
17
17
  // Database (Firebase Firestore / RTDB)
18
- export { firebaseDbProvider, getAdminApp, getAdminAuth, getAdminDb, getAdminStorage, getAdminRealtimeDb, _resetAdminSingletons, FirebaseRepository, BaseRepository, FirebaseSieveRepository, FirebaseRealtimeRepository, RTDB_PATHS, firebaseFieldOps, removeUndefined, prepareForFirestore, deserializeTimestamps, } from "./providers/db-firebase/index";
18
+ export { firebaseDbProvider, getAdminApp, getAdminAuth, getAdminDb, getAdminStorage, getAdminRealtimeDb, _resetAdminSingletons, FirebaseRepository, BaseRepository, FirebaseSieveRepository, RTDB_PATHS, firebaseFieldOps, removeUndefined, prepareForFirestore, deserializeTimestamps, } from "./providers/db-firebase/index";
19
19
  // Storage (Firebase Admin)
20
20
  export { firebaseStorageProvider } from "./providers/storage-firebase/index";
21
21
  // Email (Resend)
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { normalizeError } from "../../errors/normalize";
3
+ import { toUserMessage } from "../../errors/error-display-map";
3
4
  import { useCallback } from "react";
4
5
  import { useRouter } from "next/navigation";
5
6
  import { useToast } from "../../ui";
@@ -33,8 +34,13 @@ export function useActionDispatch(options) {
33
34
  showToast(action.successMessage, "success");
34
35
  }
35
36
  catch (err) {
36
- void normalizeError(err);
37
- showToast(action.errorMessage ?? (err instanceof Error ? err.message : "Something went wrong."), "error");
37
+ const e = normalizeError(err);
38
+ // The action's own errorMessage still wins — it is the most
39
+ // specific authored copy available. Only the tail changed: it now
40
+ // resolves the error's CODE and ends in a constant, instead of
41
+ // ending in the thrown value's developer-facing text.
42
+ showToast(action.errorMessage ??
43
+ toUserMessage(e.code, undefined, { fallback: "Something went wrong." }), "error");
38
44
  }
39
45
  break;
40
46
  }
@@ -28,10 +28,12 @@ export function useFormStatePreservation({ values, onRestore, stripFields = [],
28
28
  try {
29
29
  const decoded = JSON.parse(atob(encoded));
30
30
  onRestore(decoded);
31
+ // The payload is user-supplied via the URL, so corrupt input is expected
32
+ // rather than exceptional. Discarding it restores the empty form, which is
33
+ // the same state as arriving without one.
31
34
  }
32
35
  catch (_err) {
33
36
  void normalizeError(_err);
34
- // ignore corrupt payloads
35
37
  }
36
38
  }, [paramName, onRestore]);
37
39
  useEffect(() => {