@mohasinac/appkit 4.70.0 → 4.71.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (169) hide show
  1. package/PROVIDERS.md +190 -0
  2. package/dist/.build-stamp.json +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +2 -1
  4. package/dist/_internal/server/features/checkout/actions.js +43 -8
  5. package/dist/_internal/server/features/orders/actions.js +2 -1
  6. package/dist/_internal/server/features/orders/adapters.js +18 -0
  7. package/dist/_internal/server/features/orders/balance.js +2 -1
  8. package/dist/_internal/server/features/orders/cancellation-refund.d.ts +0 -89
  9. package/dist/_internal/server/features/orders/cancellation-refund.js +3 -2
  10. package/dist/_internal/server/features/payouts/actions.js +2 -1
  11. package/dist/_internal/server/features/points/award.js +3 -2
  12. package/dist/_internal/server/features/protection-claims/actions.js +2 -1
  13. package/dist/_internal/server/features/refunds/actions.js +3 -2
  14. package/dist/_internal/server/features/shipping/courier-block.js +5 -2
  15. package/dist/_internal/server/features/shipping/reconcile.js +3 -2
  16. package/dist/_internal/server/features/shipping/reimbursement.js +3 -2
  17. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  18. package/dist/_internal/server/features/stores/adapters.js +12 -2
  19. package/dist/_internal/server/jobs/core/dailyStatusDigest.js +7 -3
  20. package/dist/_internal/server/jobs/core/emiInstallmentReminder.js +2 -1
  21. package/dist/_internal/server/jobs/core/obligationSweep.js +5 -4
  22. package/dist/_internal/server/jobs/core/onScamReportCreate.js +2 -1
  23. package/dist/_internal/server/jobs/core/paymentReconcile.js +5 -4
  24. package/dist/_internal/shared/checkout/rules/auction.rule.js +25 -7
  25. package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +26 -8
  26. package/dist/_internal/shared/config/schema.d.ts +15 -0
  27. package/dist/_internal/shared/content/published-values.d.ts +2 -2
  28. package/dist/_internal/shared/content/published-values.js +31 -12
  29. package/dist/_internal/shared/features/auctions/errors.js +2 -1
  30. package/dist/_internal/shared/features/orders/refund-copy.js +3 -2
  31. package/dist/_internal/shared/features/promotions/errors.js +2 -1
  32. package/dist/constants/field-names.d.ts +23 -0
  33. package/dist/constants/field-names.js +23 -0
  34. package/dist/constants/storage-keys.d.ts +90 -0
  35. package/dist/constants/storage-keys.js +119 -0
  36. package/dist/contracts/index.d.ts +1 -1
  37. package/dist/contracts/payment.d.ts +42 -0
  38. package/dist/contracts/registry.d.ts +8 -0
  39. package/dist/contracts/tax.d.ts +118 -0
  40. package/dist/contracts/tax.js +3 -0
  41. package/dist/core/brand-resolver.d.ts +92 -0
  42. package/dist/core/brand-resolver.js +120 -0
  43. package/dist/core/brand-text.d.ts +15 -0
  44. package/dist/core/brand-text.js +73 -0
  45. package/dist/core/hooks/useNewsletterSubscribe.d.ts +30 -0
  46. package/dist/core/hooks/useNewsletterSubscribe.js +121 -0
  47. package/dist/features/about/components/DeveloperView.js +2 -1
  48. package/dist/features/account/components/BuyerAccountGuideView.js +2 -1
  49. package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -3
  50. package/dist/features/account/components/BuyerOrdersGuideView.js +6 -5
  51. package/dist/features/account/components/BuyerShoppingGuideView.js +3 -2
  52. package/dist/features/account/components/UserOffersPanel.js +2 -1
  53. package/dist/features/account/hooks/useCollapsedSections.js +5 -3
  54. package/dist/features/account/schemas/index.js +1 -1
  55. package/dist/features/admin/actions/notification-actions.js +9 -2
  56. package/dist/features/admin/components/AdminAnalyticsGuideView.js +2 -1
  57. package/dist/features/admin/components/AdminBundlesView.js +2 -1
  58. package/dist/features/admin/components/AdminCatalogGuideView.js +2 -1
  59. package/dist/features/admin/components/AdminContentGuideView.js +2 -1
  60. package/dist/features/admin/components/AdminFaqEditorView.js +2 -1
  61. package/dist/features/admin/components/AdminGatewayEventsView.js +2 -1
  62. package/dist/features/admin/components/AdminGuideHubView.js +2 -1
  63. package/dist/features/admin/components/AdminMoneyCasesView.js +2 -1
  64. package/dist/features/admin/components/AdminOrdersGuideView.js +2 -1
  65. package/dist/features/admin/components/AdminProductsView.js +3 -2
  66. package/dist/features/admin/components/AdminSectionsView.js +3 -2
  67. package/dist/features/admin/components/AdminSiteSettingsView.js +8 -7
  68. package/dist/features/admin/components/AdminStoresGuideView.js +2 -1
  69. package/dist/features/admin/components/AdminSupportTicketDetailView.js +2 -1
  70. package/dist/features/admin/components/AdminTeamGuideView.js +2 -1
  71. package/dist/features/admin/components/AdminTrustGuideView.js +3 -2
  72. package/dist/features/admin/components/AdminUsersGuideView.js +2 -1
  73. package/dist/features/admin/components/sections/adminSectionsTypes.js +1 -1
  74. package/dist/features/auth/actions/user-slug.js +21 -2
  75. package/dist/features/auth/hooks/useAuth.js +11 -4
  76. package/dist/features/cart/utils/pending-ops.js +16 -3
  77. package/dist/features/catalogue/utils/product-from-item.d.ts +18 -2
  78. package/dist/features/catalogue/utils/product-from-item.js +23 -4
  79. package/dist/features/categories/components/BundleItemsPicker.js +2 -1
  80. package/dist/features/categories/components/CategoryDetailPageView.js +2 -1
  81. package/dist/features/categories/components/ContextualTrail.d.ts +9 -2
  82. package/dist/features/categories/components/ContextualTrail.js +11 -3
  83. package/dist/features/categories/components/LastCategoryBeacon.js +2 -2
  84. package/dist/features/checkout/actions/checkout-value-otp-actions.js +9 -1
  85. package/dist/features/contact/email.js +11 -12
  86. package/dist/features/email/notification-templates.js +4 -5
  87. package/dist/features/email/render-static-markup.d.ts +34 -0
  88. package/dist/features/email/render-static-markup.js +4 -0
  89. package/dist/features/events/utils/event-charts.js +2 -1
  90. package/dist/features/homepage/components/AnnouncementBar.js +5 -3
  91. package/dist/features/homepage/components/WelcomeSection.d.ts +3 -1
  92. package/dist/features/homepage/components/WelcomeSection.js +2 -2
  93. package/dist/features/homepage/lib/section-renderer.d.ts +8 -0
  94. package/dist/features/homepage/lib/section-renderer.js +1 -1
  95. package/dist/features/layout/AppLayoutShell.d.ts +3 -1
  96. package/dist/features/layout/AppLayoutShell.js +2 -2
  97. package/dist/features/layout/TitleBarLayout.d.ts +8 -1
  98. package/dist/features/layout/TitleBarLayout.js +2 -2
  99. package/dist/features/media/MediaVideo.js +2 -1
  100. package/dist/features/orders/actions/refund-actions.js +3 -2
  101. package/dist/features/orders/components/RefundRequestView.js +15 -2
  102. package/dist/features/orders/repository/orders.repository.js +2 -1
  103. package/dist/features/orders/types/index.d.ts +14 -0
  104. package/dist/features/orders/utils/payment-proof-message.js +5 -2
  105. package/dist/features/price-index/components/PriceIndexToolView.js +6 -5
  106. package/dist/features/pricing/components/AdminPricingView.js +2 -1
  107. package/dist/features/print/components/labels.js +2 -1
  108. package/dist/features/products/components/CityCollectiblesView.js +2 -1
  109. package/dist/features/products/components/MakeOfferButton.js +2 -1
  110. package/dist/features/products/components/PrizeDrawCollage.js +3 -2
  111. package/dist/features/products/components/PrizeRevealModal.js +2 -1
  112. package/dist/features/products/repository/products.repository.js +2 -1
  113. package/dist/features/promotions/components/CouponCard.js +3 -2
  114. package/dist/features/promotions/components/CouponHelpDetails.js +2 -1
  115. package/dist/features/promotions/constants/coupon-help.d.ts +1 -1
  116. package/dist/features/promotions/constants/coupon-help.js +8 -1
  117. package/dist/features/scams/components/ScamAwarenessModal.js +2 -1
  118. package/dist/features/scams/components/ScamProfileView.js +5 -3
  119. package/dist/features/scams/components/ScamRegistryView.js +2 -1
  120. package/dist/features/scams/constants/scam-types.d.ts +13 -1
  121. package/dist/features/scams/constants/scam-types.js +67 -23
  122. package/dist/features/scams/schemas/firestore.js +1 -1
  123. package/dist/features/search/components/Search.js +5 -1
  124. package/dist/features/seller/actions/offer-actions.js +47 -9
  125. package/dist/features/seller/components/DispatchForm.js +3 -2
  126. package/dist/features/seller/components/PrintCenterView.js +4 -2
  127. package/dist/features/seller/components/SellerBundlesView.js +2 -1
  128. package/dist/features/seller/components/SellerPayoutSettingsView.js +3 -2
  129. package/dist/features/seller/components/SellerProductShell.js +3 -2
  130. package/dist/features/seller/components/SellerProductsView.js +4 -3
  131. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +30 -4
  132. package/dist/features/store-extensions/repository/store-extensions.repositories.js +46 -7
  133. package/dist/features/store-extensions/schemas/firestore.d.ts +22 -3
  134. package/dist/features/store-extensions/schemas/firestore.js +20 -0
  135. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +7 -6
  136. package/dist/features/stores/components/StoreFinanceGuideView.js +6 -5
  137. package/dist/features/stores/components/StoreGuideHubView.js +2 -1
  138. package/dist/features/stores/components/StoreListingsGuideView.js +2 -1
  139. package/dist/features/stores/components/StoreOrdersGuideView.js +5 -4
  140. package/dist/features/stores/components/StoreSettingsGuideView.js +2 -1
  141. package/dist/features/support/schemas/index.d.ts +2 -2
  142. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +1 -1
  143. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  144. package/dist/features/whatsapp-bot/helpers/whatsapp.js +5 -2
  145. package/dist/index.d.ts +0 -1
  146. package/dist/index.js +0 -2
  147. package/dist/providers/payment-manual/index.d.ts +11 -0
  148. package/dist/providers/payment-manual/index.js +13 -0
  149. package/dist/providers/payment-phonepe/index.d.ts +23 -7
  150. package/dist/providers/payment-phonepe/index.js +15 -0
  151. package/dist/providers/payment-razorpay/index.d.ts +13 -0
  152. package/dist/providers/payment-razorpay/index.js +21 -1
  153. package/dist/providers/tax-india-gst/index.d.ts +45 -0
  154. package/dist/providers/tax-india-gst/index.js +78 -0
  155. package/dist/react/hooks/useScrollToTop.d.ts +5 -0
  156. package/dist/react/hooks/useScrollToTop.js +70 -0
  157. package/dist/schemas/registry.d.ts +2 -2
  158. package/dist/seo/json-ld.js +25 -4
  159. package/dist/ui/components/SiteLogo.d.ts +20 -2
  160. package/dist/ui/components/SiteLogo.js +28 -6
  161. package/dist/ui/components/SiteMark.d.ts +1 -1
  162. package/dist/ui/components/SiteMark.js +21 -6
  163. package/dist/utils/date.formatter.d.ts +20 -0
  164. package/dist/utils/date.formatter.js +25 -0
  165. package/dist/utils/number.formatter.d.ts +47 -0
  166. package/dist/utils/number.formatter.js +79 -1
  167. package/dist/validation/phone.validator.d.ts +0 -1
  168. package/dist/validation/phone.validator.js +17 -3
  169. package/package.json +17 -3
@@ -40,6 +40,7 @@ import { orderRepository } from "../../../../features/orders/repository/orders.r
40
40
  import { pointsRepository } from "../../../../features/points/repository/points.repository.js";
41
41
  import { reconcileShipping, isReimbursableShipment, } from "../../../shared/shipping/quote.js";
42
42
  import { serverLogger } from "../../../../monitoring/index.js";
43
+ import { formatMoney } from "../../../../utils/number.formatter.js";
43
44
  /**
44
45
  * The seller submits postage evidence on a self-shipped order.
45
46
  *
@@ -191,8 +192,8 @@ export async function approveShippingReimbursement(input) {
191
192
  * and this string is written once and never re-derived, so it stays true
192
193
  * of the moment it describes.
193
194
  */
194
- description: `Shipping saving on ${input.orderId}: you paid ₹${chargedToBuyer} for postage, ` +
195
- `the approved cost was ₹${spent}.`,
195
+ description: `Shipping saving on ${input.orderId}: you paid ${formatMoney(chargedToBuyer)} for postage, ` +
196
+ `the approved cost was ${formatMoney(spent)}.`,
196
197
  });
197
198
  }
198
199
  await orderRepository.update(input.orderId, {
@@ -27,7 +27,7 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
27
27
  */
28
28
  export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "allowSharedPurchase", "sharedPurchaseHandlingFee", "sharedPurchaseExtraShippingPerDestination", "isVerified", "sellerTier", "standing", "createdAt"];
29
29
  /** Everything else, with the reason it stays server-side. */
30
- export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "whatsappFeedEnabled", "trustedSeller", "acceptsPoints", "searchTxt", "whatsappNotifySubscribed", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "tierLockedByAdmin", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
30
+ export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "whatsappFeedEnabled", "trustedSeller", "acceptsPoints", "searchTxt", "updatedAt", "adminNotes", "statusHistory", "statusHistoryTruncated", "tierLockedByAdmin", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
31
31
  /**
32
32
  * Accepted input: a `StoreDocument` (the common case) or a loosely-typed
33
33
  * Firestore row. The legacy top-level counters below predate `stats` and are
@@ -168,9 +168,19 @@ export const PRIVATE_STORE_FIELDS = [
168
168
  * what, which is commercial terms (same reason `customCommissionRate` is
169
169
  * below), and it has no reader on any public store surface.
170
170
  */
171
- "whatsappNotifySubscribed",
171
+ /*
172
+ * 🛑 `whatsappNotifySubscribed` was listed TWICE in this array, and
173
+ * `whatsappConfig` was listed although `StoreDocument` says of it
174
+ * "🛑 whatsappConfig IS GONE AND MUST NOT COME BACK" — so this list named a
175
+ * field that does not exist and double-counted one that does.
176
+ *
177
+ * Both were invisible to `audit-public-projection-parity`, which checks that
178
+ * every SCHEMA field appears in exactly one list but not the reverse: it
179
+ * never asks whether a listed name is real, or listed once. An exemption
180
+ * list is a claim nobody checks unless something checks it (Root Cause
181
+ * #106) — the duplicate half is enforced now; see that audit's R5.
182
+ */
172
183
  "updatedAt", // bookkeeping; no public reader
173
- "whatsappConfig", // Meta access token + WABA/catalog ids — secret
174
184
  "adminNotes", // schema comment: never shown to the owner or the public
175
185
  // Admin decisions with actor UIDs and suspension reasons. The store OWNER
176
186
  // sees this on their own admin surfaces; an anonymous visitor to /stores has
@@ -14,7 +14,10 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
14
14
  * sync ceiling — it has the Function's much larger budget.
15
15
  */
16
16
  import { Fragment } from "react";
17
- import { renderToStaticMarkup } from "react-dom/server";
17
+ // 🛑 Lazy require, not a top-level `react-dom/server` import — this module is
18
+ // reachable from a Server Component via appkit/server, and Next's static tracer
19
+ // fails the build on it. See render-static-markup.ts.
20
+ import { renderStaticMarkup } from "../../../../features/email/render-static-markup.js";
18
21
  import { getAdminDb } from "../../../../providers/db-firebase/index.js";
19
22
  import { getProviders } from "../../../../contracts/registry.js";
20
23
  import { checkCourierWalletAndAlert } from "../../features/shipping/courier-block.js";
@@ -40,6 +43,7 @@ import { PAYMENT_CASES_COLLECTION, PAYMENT_CASE_ACTIVE_STATUSES, PAYMENT_CASE_FI
40
43
  import { GATEWAY_EVENTS_COLLECTION, GATEWAY_EVENT_FIELDS, } from "../../../../features/gateway-events/schemas/firestore.js";
41
44
  import { EmailBold, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailLink, EmailRow, } from "../../../../features/email/primitives.js";
42
45
  import { normalizeError } from "../../../../errors/normalize.js";
46
+ import { formatMoney } from "../../../../utils/number.formatter.js";
43
47
  const DAY_MS = 24 * 60 * 60 * 1000;
44
48
  /*
45
49
  * `createdAt` and `isRead` on collections with no feature-specific field-name
@@ -62,7 +66,7 @@ const NON_REVENUE_STATUSES = new Set([
62
66
  /** How many contact messages the digest lists in full before summarising the rest. */
63
67
  const CONTACT_PREVIEW_LIMIT = 20;
64
68
  function currency(amount) {
65
- return `₹${amount.toLocaleString("en-IN", { maximumFractionDigits: 2 })}`;
69
+ return formatMoney(amount);
66
70
  }
67
71
  /** One "Label: value" line. Uses only email primitives — email clients need
68
72
  * inline-styled, table-based markup, which the Email* components provide. */
@@ -263,7 +267,7 @@ export async function collectDailyStatusSummary() {
263
267
  function renderDigestHtml(summary, siteName, version) {
264
268
  const statusRows = Object.entries(summary.statusBreakdown).sort(([a], [b]) => a.localeCompare(b));
265
269
  const title = siteName ? `${siteName} — Daily Status` : "Daily Status";
266
- return `<!DOCTYPE html>${renderToStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsxs(EmailContainer, { children: [_jsx(EmailHeader, { brandName: siteName }), _jsxs(EmailRow, { children: [version
270
+ return `<!DOCTYPE html>${renderStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsxs(EmailContainer, { children: [_jsx(EmailHeader, { brandName: siteName }), _jsxs(EmailRow, { children: [version
267
271
  ? `Deployment ${version} is live. Activity for the last 24 hours.`
268
272
  : "Activity for the last 24 hours.", _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Orders placed", value: summary.orderCount }), _jsx(StatLine, { label: "Revenue (excl. cancelled/refunded)", value: currency(summary.revenue) }), _jsx(StatLine, { label: "Active listings", value: summary.activeListingCount }), _jsx(StatLine, { label: "Pending > 24h (needs attention)", value: summary.stuckPendingCount })] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Orders by status" }), _jsx("br", {}), _jsx("br", {}), statusRows.length === 0 ? ("No orders in this window.") : (statusRows.map(([status, count]) => (_jsx(StatLine, { label: status, value: count }, status))))] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Inbox" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "New contact messages", value: summary.contactTotal }), _jsx(StatLine, { label: "New support tickets", value: summary.newTicketCount }), _jsx(StatLine, { label: "Tickets open / in progress / waiting on user", value: `${summary.ticketsOpen} / ${summary.ticketsInProgress} / ${summary.ticketsWaitingOnUser}` }), _jsx(StatLine, { label: "Unread admin notifications", value: summary.unreadAdminNotificationCount })] }), summary.contactMessages.length > 0 ? (_jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Contact messages" }), _jsx("br", {}), _jsx("br", {}), summary.contactMessages.map((m) => (_jsxs(Fragment, { children: [_jsx(EmailBold, { children: m.subject }), _jsx("br", {}), m.name, " \u2014", " ", _jsxs(EmailLink, { href: `mailto:${m.email}?subject=${encodeURIComponent(`Re: ${m.subject}`)}`, children: ["Reply to ", m.email] }), _jsx("br", {}), m.excerpt, _jsx("br", {}), _jsx("br", {})] }, m.id))), summary.contactTotal > summary.contactMessages.length
269
273
  ? `…and ${summary.contactTotal - summary.contactMessages.length} more in /admin/contact.`
@@ -1,6 +1,7 @@
1
1
  import { normalizeError } from "../../../../errors/normalize.js";
2
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
3
3
  import { orderRepository } from "../../../../repositories/index.js";
4
+ import { formatMoney } from "../../../../utils/number.formatter.js";
4
5
  const REMINDER_WINDOW_MS = 3 * 86400000;
5
6
  function toDate(value) {
6
7
  return value instanceof Date ? value : new Date(value);
@@ -46,7 +47,7 @@ export async function runEmiInstallmentReminder(ctx) {
46
47
  title: isOverdue ? "EMI installment overdue" : "EMI installment due soon",
47
48
  message: isOverdue
48
49
  ? `Installment ${inst.index} for order ${id} is overdue. Pay now to avoid delaying your shipment.`
49
- : `Installment ${inst.index} for order ${id} (₹${inst.amount.toFixed(2)}) is due on ${dueDate.toDateString()}.`,
50
+ : `Installment ${inst.index} for order ${id} (${formatMoney(inst.amount)}) is due on ${dueDate.toDateString()}.`,
50
51
  relatedId: id,
51
52
  relatedType: "order",
52
53
  actionUrl: `/user/orders/view/${id}`,
@@ -34,6 +34,7 @@ import { awardPurchasePoints } from "../../features/points/award.js";
34
34
  import { processRefundAction } from "../../features/refunds/actions.js";
35
35
  import { resolveRefundCapacity } from "../../features/refunds/capacity.js";
36
36
  import { MONEY_EPSILON } from "../../../shared/checkout/refundable-base.js";
37
+ import { formatMoney } from "../../../../utils/number.formatter.js";
37
38
  /**
38
39
  * Perform the policy's action.
39
40
  *
@@ -304,14 +305,14 @@ async function performAction(ctx, obligation, order) {
304
305
  kind: "manual_refund_due",
305
306
  subjectId: order.id,
306
307
  severity: "error",
307
- title: `Uninspected return — automatic refund of ₹${capacity.remaining.toFixed(2)} FAILED on ${order.id}`,
308
+ title: `Uninspected return — automatic refund of ${formatMoney(capacity.remaining)} FAILED on ${order.id}`,
308
309
  detail: [
309
310
  "A received return was never inspected, so it auto-resolved in the buyer's favour — and the refund itself failed.",
310
311
  "",
311
312
  `Gateway error: ${result.error ?? "unknown"}`,
312
313
  "",
313
314
  `Order: ${order.id}`,
314
- `Owed: ₹${capacity.remaining.toFixed(2)}`,
315
+ `Owed: ${formatMoney(capacity.remaining)}`,
315
316
  "",
316
317
  "Retrying from the admin order page is idempotent on this order.",
317
318
  ].join("\n"),
@@ -332,7 +333,7 @@ async function performAction(ctx, obligation, order) {
332
333
  kind: "refund_mismatch",
333
334
  subjectId: `seller-charge:${order.id}`,
334
335
  severity: "warning",
335
- title: `Decide whether the seller bears ₹${capacity.remaining.toFixed(2)} — uninspected return on ${order.id}`,
336
+ title: `Decide whether the seller bears ${formatMoney(capacity.remaining)} — uninspected return on ${order.id}`,
336
337
  detail: [
337
338
  "A returned parcel reached the seller and was never inspected, so the buyer has been refunded automatically — the cover is unconditional and the seller had the goods and the full window.",
338
339
  "",
@@ -340,7 +341,7 @@ async function performAction(ctx, obligation, order) {
340
341
  "",
341
342
  `Order: ${order.id}`,
342
343
  `Store: ${order.storeId ?? "—"}`,
343
- `Refunded to the buyer: ₹${capacity.remaining.toFixed(2)}`,
344
+ `Refunded to the buyer: ${formatMoney(capacity.remaining)}`,
344
345
  `Return received: ${order.returnReceivedAt?.toISOString() ?? "—"}`,
345
346
  `Buyer's stated reason: ${order.returnReasonCode ?? "—"}`,
346
347
  "",
@@ -3,6 +3,7 @@ import { adminNotificationsRepository } from "../../../../features/store-extensi
3
3
  import { userRepository } from "../../../../features/auth/repository/user.repository.js";
4
4
  import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
5
5
  import { SCAM_TYPE_LABELS } from "../../../../features/scams/constants/scam-types.js";
6
+ import { formatMoney } from "../../../../utils/number.formatter.js";
6
7
  export async function handleScamReportCreate(input, ctx) {
7
8
  const { scammerId, report } = input;
8
9
  const { reportedBy, displayNames, scamType, scamPlatform, amountLost } = report;
@@ -59,7 +60,7 @@ export async function handleScamReportCreate(input, ctx) {
59
60
  */
60
61
  try {
61
62
  const scamTypeLabel = scamType ? (SCAM_TYPE_LABELS[scamType] ?? scamType) : "Unknown";
62
- const amountStr = amountLost ? ` ₹${amountLost.toLocaleString("en-IN")}` : "";
63
+ const amountStr = amountLost ? ` ${formatMoney(amountLost)}` : "";
63
64
  const platformStr = scamPlatform ? ` via ${scamPlatform}` : "";
64
65
  await adminNotificationsRepository.create({
65
66
  category: "fraud",
@@ -40,6 +40,7 @@ import { REFUND_CLAIMS_COLLECTION, REFUND_CLAIM_FIELDS, ORDER_COLLECTION, } from
40
40
  import { ORDER_FIELDS } from "../../../../constants/field-names.js";
41
41
  import { reconcileDisputes } from "../../features/payment-cases/disputes.js";
42
42
  import { refundableBase, committedRefundTotal, MONEY_EPSILON, } from "../../../shared/checkout/refundable-base.js";
43
+ import { formatMoney } from "../../../../utils/number.formatter.js";
43
44
  /** How far back R2 looks. Two days of overlap, so a run that fails is covered by the next. */
44
45
  const PAYMENT_WINDOW_HOURS = 48;
45
46
  /** A refund claim unsettled for longer than this is a crash between call and record. */
@@ -478,7 +479,7 @@ async function reconcileUnpaidCancellations(ctx) {
478
479
  kind: "manual_refund_due",
479
480
  subjectId: doc.id,
480
481
  severity: "error",
481
- title: `Cancelled order still owes ₹${shortfall.toFixed(2)} — ${doc.id}`,
482
+ title: `Cancelled order still owes ${formatMoney(shortfall)} — ${doc.id}`,
482
483
  detail: [
483
484
  "This order is cancelled and records a refund it has not paid.",
484
485
  "",
@@ -488,9 +489,9 @@ async function reconcileUnpaidCancellations(ctx) {
488
489
  "cover.",
489
490
  "",
490
491
  `Order: ${doc.id}`,
491
- `Recorded as owed: ₹${intended.toFixed(2)}`,
492
- `Actually refunded: ₹${committed.toFixed(2)}`,
493
- `Shortfall: ₹${shortfall.toFixed(2)}`,
492
+ `Recorded as owed: ${formatMoney(intended)}`,
493
+ `Actually refunded: ${formatMoney(committed)}`,
494
+ `Shortfall: ${formatMoney(shortfall)}`,
494
495
  data.paymentMethod ? `Payment method: ${data.paymentMethod}` : null,
495
496
  data.paymentId ? `Gateway reference: ${data.paymentId}` : null,
496
497
  data.userId ? `Buyer: ${data.userId}` : null,
@@ -5,14 +5,32 @@ export const auctionRule = {
5
5
  cartEligible: false, // bid-only; add-to-cart is blocked at the action layer
6
6
  cartIneligibleHint: "Place a bid on the auction page instead.",
7
7
  /*
8
- * 🛑 cod + cash, NOT emi. An auction win is a committed sale of a unique lot:
9
- * a buyer who never pays on delivery costs the seller the item and the
10
- * auction both, and there is no second-highest bidder to fall back to once
11
- * it has been marked sold. EMI is confirmed-at-order financing rather than a
12
- * deferred payment, so it carries none of that risk and stays available.
8
+ * 🛑 THE GATEWAY IS PROHIBITED HERE, and this list was INVERTED until
9
+ * 2026-10-28 — it blocked `cod` and `cash` while leaving `online` open, so
10
+ * every auction win was payable by card.
11
+ *
12
+ * The reason is the acquirer's, not ours. Auction and bidding businesses sit
13
+ * on the prohibited-merchant lists Indian PGs inherit from the card networks,
14
+ * alongside lotteries and games of chance. Settling auction wins through a
15
+ * standard e-commerce MID is a terms breach, and the penalty is not a
16
+ * declined transaction — it is the whole merchant account frozen with the
17
+ * settlement balance held. One category of order can cost every other
18
+ * category its ability to take money at all.
19
+ *
20
+ * So: manual bank/UPI transfer, or cash on pickup. Both land in the owner's
21
+ * own account with no acquirer in the path.
22
+ *
23
+ * 🛑 `cash` is now ALLOWED, reversing the rule below it. The old comment is
24
+ * kept because its reasoning is still sound — a pay-later method on a unique
25
+ * lot has no second-highest bidder to fall back on — but it was weighing
26
+ * seller risk against buyer convenience, and that trade is not what decides
27
+ * this. The storefront is single-vendor and the owner is the seller: they
28
+ * accept the non-payment risk on their own lots, and they do not get to
29
+ * accept the risk of losing the merchant account. `emi` goes for the same
30
+ * reason as `online` — it settles through the same acquirer.
13
31
  */
14
- blockedPaymentMethods: ["cod", "cash"],
15
- blockedPaymentMessage: "An auction win cannot be paid for on delivery or in cash — the lot is one of a kind and is held for you from the moment the auction closes.",
32
+ blockedPaymentMethods: ["online", "cod", "emi"],
33
+ blockedPaymentMessage: "An auction win is settled by direct bank/UPI transfer or cash on pickup — card and online payment are not available for auctions.",
16
34
  canMergeWithSameProduct: false,
17
35
  refundPolicy: { full: true, partial: false, partialGranularity: "none" },
18
36
  splitKey: (item) => `auction:${item.itemId}`,
@@ -2,14 +2,32 @@ import { ValidationError } from "../../../../errors/index.js";
2
2
  import { DEFAULT_LISTING_RULE } from "./_defaults.js";
3
3
  import { PRIZE_DRAW_MAX_REVEALS_PER_ORDER } from "./_limits.js";
4
4
  /**
5
- * Deferred/unconfirmed-payment methods are not allowed for prize-draw
6
- * checkout — instant-mode winner assignment triggers on the order's payment
7
- * actually being confirmed, which cod/cash/emi don't guarantee happens (or
8
- * happens promptly). Only "upi_manual" (bank/UPI transfer, confirmed later by
9
- * seller/admin) and "online" (PhonePe, confirms near-instantly) are allowed;
10
- * "admin_bypass" stays usable for QA regardless.
5
+ * 🛑 THE GATEWAY IS PROHIBITED HERE — `online` joined this list 2026-10-28,
6
+ * and `cash` left it.
7
+ *
8
+ * A prize draw is a game of chance, which is on the prohibited-merchant lists
9
+ * Indian payment gateways inherit from the card networks — the same lists that
10
+ * carry lotteries and auctions. Routing entry payments through a standard
11
+ * e-commerce MID is a terms breach, and the penalty is the merchant account
12
+ * frozen with the settlement balance held, not a declined charge. One category
13
+ * can cost every other category its ability to take money.
14
+ *
15
+ * 🛑 This REPLACES the original reasoning rather than extending it, and the
16
+ * original is worth keeping in view because it is still true and no longer
17
+ * decisive: deferred methods were blocked because instant-mode winner
18
+ * assignment triggers on confirmed payment, which `cod`/`cash`/`emi` do not
19
+ * guarantee happens promptly. By that logic `online` was the SAFEST method
20
+ * available and `cash` the worst.
21
+ *
22
+ * The acquirer's rules invert it. `online` and `emi` both settle through the
23
+ * gateway and are out; `cash` is in, because the owner is the seller and
24
+ * confirms the payment themselves before the draw. The timing concern becomes
25
+ * an operational one — do not run the draw until payment is confirmed — which
26
+ * `upi_manual` already required and the reveal flow already handles.
27
+ *
28
+ * `admin_bypass` stays usable for QA regardless.
11
29
  */
12
- const PRIZE_DRAW_BLOCKED_PAYMENT_METHODS = ["cod", "cash", "emi"];
30
+ const PRIZE_DRAW_BLOCKED_PAYMENT_METHODS = ["online", "cod", "emi"];
13
31
  /**
14
32
  * The refusal, kept VERBATIM when this check moved out of `preflightChecks`
15
33
  * into `assertPaymentMethodAllowed`.
@@ -20,7 +38,7 @@ const PRIZE_DRAW_BLOCKED_PAYMENT_METHODS = ["cod", "cash", "emi"];
20
38
  * reason is the thing a buyer needs in order to pick a different method rather
21
39
  * than abandon the entry.
22
40
  */
23
- const PRIZE_DRAW_BLOCKED_PAYMENT_MESSAGE = "Prize-draw entries can only be paid by manual bank/UPI transfer or PhonePe — cash on delivery and EMI are not available for this listing type.";
41
+ const PRIZE_DRAW_BLOCKED_PAYMENT_MESSAGE = "Prize-draw entries are paid by direct bank/UPI transfer or cash on pickup — card and online payment are not available for prize draws.";
24
42
  export const prizeDrawRule = {
25
43
  ...DEFAULT_LISTING_RULE,
26
44
  orderType: "prize-draw",
@@ -95,6 +95,21 @@ export interface AppkitBrandConfig {
95
95
  * {name}") and this one stands alone, so they are spelled differently.
96
96
  */
97
97
  heroCaption?: string;
98
+ /**
99
+ * TLD badge drawn in a pill after the SVG wordmark, e.g. `"in"`. Absent
100
+ * renders no badge.
101
+ *
102
+ * 🛑 Optional, and appkit renders nothing without it — a library must not
103
+ * assume its consumer has a TLD worth showing, let alone which one. The
104
+ * badge used to be a hardcoded `.in` inside `SiteLogo`, where no prop could
105
+ * reach it (A5).
106
+ *
107
+ * 🛑 NOT a source for the site's domain. `brand.name + "." + tld` happens to
108
+ * give `letitrip.in` here, which is what makes it a trap: any multi-word
109
+ * brand yields `my shop.com`. The host has one owner, `seo.siteUrl`
110
+ * (Root Cause #81), and `BRAND.DOMAIN` derives from that.
111
+ */
112
+ tld?: string;
98
113
  /** One-line brand description used in meta tags and footers */
99
114
  description?: string;
100
115
  /** Tagline shown in footer / about sections */
@@ -44,7 +44,7 @@
44
44
  * sentence with a word missing, which nobody reports, while `{{wrongKey}}` on
45
45
  * a live page gets reported in minutes.
46
46
  */
47
- export type PublishedValueKey = "returnWindowDays" | "sellerAcceptDays" | "sellerDispatchBusinessDays" | "returnRespondHours" | "returnPostDays" | "offerCheckoutHours" | "paymentWindowMinutes" | "deliveryConfirmDays" | "orderCompleteDays" | "shipmentClaimHours" | "saleClaimHours" | "disputeTransitWindowDays" | "disputeNotReachedGraceDays" | "platformFeeMax" | "platformFeePercent" | "shipmentProtectionPercent" | "shipmentProtectionMin" | "saleProtectionPercent" | "saleProtectionMin" | "saleProtectionMaxValue" | "codHandlingPercent" | "codDepositPercent" | "giftWrapFee" | "pointsEarnPercent" | "pointsMaxRedemptionPercent" | "pointsExpiryMonths" | "refundBusinessDaysMin" | "refundBusinessDaysMax" | "siteName";
47
+ export type PublishedValueKey = "returnWindowDays" | "sellerAcceptDays" | "sellerDispatchBusinessDays" | "returnRespondHours" | "returnPostDays" | "offerCheckoutHours" | "paymentWindowMinutes" | "deliveryConfirmDays" | "orderCompleteDays" | "shipmentClaimHours" | "saleClaimHours" | "disputeTransitWindowDays" | "disputeNotReachedGraceDays" | "platformFeeMax" | "platformFeePercent" | "shipmentProtectionPercent" | "shipmentProtectionMin" | "saleProtectionPercent" | "saleProtectionMin" | "saleProtectionMaxValue" | "codHandlingPercent" | "codDepositPercent" | "giftWrapFee" | "pointsEarnPercent" | "pointsMaxRedemptionPercent" | "pointsExpiryMonths" | "refundBusinessDaysMin" | "refundBusinessDaysMax" | "siteName" | "siteDomain";
48
48
  export type PublishedValues = Record<PublishedValueKey, string>;
49
49
  /**
50
50
  * The settings this registry reads, where a value genuinely is configurable.
@@ -58,7 +58,7 @@ export type PublishedValues = Record<PublishedValueKey, string>;
58
58
  export interface PublishedValueSettings {
59
59
  /**
60
60
  * The operator's display name, for the disclaimers that name who is making
61
- * the statement. Falls back to `SITE_CONFIG.brand.name`.
61
+ * the statement. Falls back to the injected brand (`getBrandName()`).
62
62
  */
63
63
  siteName?: string;
64
64
  platformFeeMax?: number;
@@ -40,13 +40,14 @@
40
40
  * somebody has to remember — and it is why there is no literal for the offer
41
41
  * deadline or the claim windows even though both appear in published copy.
42
42
  */
43
- import { SITE_CONFIG } from "../../../core/site-config.js";
43
+ import { getBrandName, getBrandDomain } from "../../../core/brand-resolver.js";
44
44
  import { OBLIGATION_POLICIES } from "../../../features/obligations/constants/policies.js";
45
45
  import { POINTS_EARN_PERCENT, POINTS_EXPIRY_MONTHS, POINTS_MAX_REDEMPTION_PERCENT, } from "../../../features/points/schemas/firestore.js";
46
46
  import { PAYMENT_WINDOW_MINUTES } from "../../../features/orders/constants/payment-window.js";
47
47
  import { SALE_PROTECTION_MAX_VALUE } from "../features/shipping/protections.js";
48
48
  import { LANE_CHECKOUT_WINDOW_MS } from "../checkout/lanes.js";
49
49
  import { DISPUTE_TRANSIT_WINDOW_DAYS, DISPUTE_NOT_REACHED_GRACE_DAYS, } from "../features/orders/dispute-reasons.js";
50
+ import { formatMoney } from "../../../utils/number.formatter.js";
50
51
  /**
51
52
  * The offer checkout deadline, in hours, derived from the constant the lapse
52
53
  * sweep actually enforces.
@@ -173,12 +174,23 @@ export function resolvePublishedValues(settings) {
173
174
  refundBusinessDaysMin: "5",
174
175
  refundBusinessDaysMax: "7",
175
176
  /*
176
- * `SITE_CONFIG.brand.name` is appkit's own neutral default ("App") until a
177
- * consumer overrides it, which is exactly the right failure mode here: a
178
- * fork that forgets to configure its brand publishes a visibly generic
179
- * disclaimer rather than silently attributing its legal statements to us.
177
+ * Admin setting first, then the brand the consumer INJECTED at boot.
178
+ *
179
+ * 🛑 Was `SITE_CONFIG.brand.name`, and the reasoning given for that was
180
+ * right about the failure mode — "a fork that forgets to configure its
181
+ * brand publishes a visibly generic disclaimer rather than silently
182
+ * attributing its legal statements to us" — but wrong about the mechanism:
183
+ * `SITE_CONFIG` is a FROZEN const, and the only thing that can override it
184
+ * (`createSiteConfig`) has **zero callers**. So the override it described
185
+ * did not exist, and a consumer with a configured brand still published
186
+ * "App" in its own legal text whenever `siteSettings.siteName` was unset.
187
+ *
188
+ * `getBrandName()` keeps the same safe default ("Site") and adds the
189
+ * injection path that was missing.
180
190
  */
181
- siteName: settings?.siteName ?? SITE_CONFIG.brand.name,
191
+ siteName: settings?.siteName ?? getBrandName(),
192
+ /** Bare host, for addresses quoted in published copy. */
193
+ siteDomain: getBrandDomain(),
182
194
  };
183
195
  }
184
196
  /**
@@ -196,11 +208,18 @@ export function resolvePublishedCopy(text, values) {
196
208
  return resolved ?? whole;
197
209
  });
198
210
  }
199
- /** `₹10`, `₹1,499.50` — no trailing `.00` on a whole rupee figure. */
211
+ /**
212
+ * `₹10`, `₹1,499.50` — no trailing `.00` on a whole figure.
213
+ *
214
+ * 🛑 A `const whole = Number.isInteger(amount)` sat here unused after the A2
215
+ * conversion, and an unused local is worth opening rather than deleting: in
216
+ * this codebase a discarded value has three times turned out to be a dropped
217
+ * one (a lost delivery snapshot, two blank seller tiles, a full page reload).
218
+ *
219
+ * Checked — this one is genuinely dead. `formatMoney` runs exactly that test
220
+ * internally (`Number.isInteger(amount) ? 0 : 2`), which is what this function
221
+ * used to do by hand before delegating. Nothing is lost by removing it.
222
+ */
200
223
  function formatRupees(amount) {
201
- const whole = Number.isInteger(amount);
202
- return `₹${amount.toLocaleString("en-IN", {
203
- minimumFractionDigits: whole ? 0 : 2,
204
- maximumFractionDigits: 2,
205
- })}`;
224
+ return formatMoney(amount);
206
225
  }
@@ -1,4 +1,5 @@
1
1
  import { NotFoundError, ValidationError, ConflictError, ExpiredError } from "../../errors/index.js";
2
+ import { formatMoney } from "../../../../utils/number.formatter.js";
2
3
  export class AuctionNotFoundError extends NotFoundError {
3
4
  constructor(id) {
4
5
  super("Auction", id);
@@ -13,7 +14,7 @@ export class AuctionEndedError extends ExpiredError {
13
14
  }
14
15
  export class BidTooLowError extends ValidationError {
15
16
  constructor(minBid) {
16
- super(`Bid must be at least ₹${minBid.toLocaleString("en-IN")}`, "amount");
17
+ super(`Bid must be at least ${formatMoney(minBid)}`, "amount");
17
18
  this.name = "BidTooLowError";
18
19
  }
19
20
  }
@@ -1,3 +1,4 @@
1
+ import { formatMoney } from "../../../../utils/number.formatter.js";
1
2
  /**
2
3
  * REFUND_COPY — all user-visible strings for refund + order-sibling UI.
3
4
  *
@@ -79,10 +80,10 @@ export const REFUND_COPY = {
79
80
  cancellation: {
80
81
  message: (outcome) => {
81
82
  if (outcome.moved === "gateway") {
82
- return `Order cancelled. A refund of ₹${outcome.amount.toFixed(2)} has been sent back to your original payment method.`;
83
+ return `Order cancelled. A refund of ${formatMoney(outcome.amount)} has been sent back to your original payment method.`;
83
84
  }
84
85
  if (outcome.moved === "owed") {
85
- return `Order cancelled. We owe you ₹${outcome.amount.toFixed(2)} — it needs to be sent manually, and our team has been notified.`;
86
+ return `Order cancelled. We owe you ${formatMoney(outcome.amount)} — it needs to be sent manually, and our team has been notified.`;
86
87
  }
87
88
  /*
88
89
  * `none` splits two very different situations and must not blur them: an
@@ -1,4 +1,5 @@
1
1
  import { NotFoundError, ConflictError, ValidationError, ExpiredError } from "../../errors/index.js";
2
+ import { formatMoney } from "../../../../utils/number.formatter.js";
2
3
  export class CouponNotFoundError extends NotFoundError {
3
4
  constructor(code) {
4
5
  super("Coupon", code);
@@ -25,7 +26,7 @@ export class CouponPerUserLimitError extends ConflictError {
25
26
  }
26
27
  export class CouponMinPurchaseError extends ValidationError {
27
28
  constructor(minAmount) {
28
- super(`Minimum purchase of ₹${minAmount.toLocaleString("en-IN")} required for this coupon`, "total");
29
+ super(`Minimum purchase of ${formatMoney(minAmount)} required for this coupon`, "total");
29
30
  this.name = "CouponMinPurchaseError";
30
31
  }
31
32
  }
@@ -1058,5 +1058,28 @@ export declare const SCHEMA_DEFAULTS: {
1058
1058
  readonly UNKNOWN_USER: "Unknown User";
1059
1059
  readonly ANONYMOUS_USER: "Anonymous";
1060
1060
  readonly DEFAULT_DISPLAY_NAME: "User";
1061
+ /**
1062
+ * 🛑 NOT A DEFAULT — THIS CONSTANT GRANTS THE ADMIN ROLE, and it is the one
1063
+ * brand literal in appkit that was DELIBERATELY left in place by A5d.
1064
+ *
1065
+ * Three consumer auth routes read it as an authorisation decision:
1066
+ * `email === SCHEMA_DEFAULTS.ADMIN_EMAIL ? "admin" : "user"`
1067
+ * in `api/auth/register`, `api/auth/session` and `api/auth/google/callback`.
1068
+ * So whoever controls this address is auto-promoted to admin at signup or at
1069
+ * first session — in EVERY install of appkit, because the address is baked
1070
+ * into the library rather than configured by the consumer.
1071
+ *
1072
+ * It was not changed with the other 131 brand literals because changing it
1073
+ * changes who becomes an administrator. CLAUDE.md Rule #1 puts "security
1074
+ * boundaries: auth, RBAC" on the escalate-before-acting list, and rewiring an
1075
+ * admin-promotion check as a drive-by inside a branding sweep is exactly the
1076
+ * shape that rule forbids — a wrong move here either locks the owner out or
1077
+ * hands the role to someone else.
1078
+ *
1079
+ * The fix needs an owner decision, and the options differ in blast radius:
1080
+ * move it to `appkit.config.js` and inject it like the brand; replace
1081
+ * email-matching with an explicit role grant; or drop auto-promotion and seed
1082
+ * the first admin. Tracked, not silently chosen.
1083
+ */
1061
1084
  readonly ADMIN_EMAIL: "admin@letitrip.in";
1062
1085
  };
@@ -1285,5 +1285,28 @@ export const SCHEMA_DEFAULTS = {
1285
1285
  UNKNOWN_USER: "Unknown User",
1286
1286
  ANONYMOUS_USER: "Anonymous",
1287
1287
  DEFAULT_DISPLAY_NAME: "User",
1288
+ /**
1289
+ * 🛑 NOT A DEFAULT — THIS CONSTANT GRANTS THE ADMIN ROLE, and it is the one
1290
+ * brand literal in appkit that was DELIBERATELY left in place by A5d.
1291
+ *
1292
+ * Three consumer auth routes read it as an authorisation decision:
1293
+ * `email === SCHEMA_DEFAULTS.ADMIN_EMAIL ? "admin" : "user"`
1294
+ * in `api/auth/register`, `api/auth/session` and `api/auth/google/callback`.
1295
+ * So whoever controls this address is auto-promoted to admin at signup or at
1296
+ * first session — in EVERY install of appkit, because the address is baked
1297
+ * into the library rather than configured by the consumer.
1298
+ *
1299
+ * It was not changed with the other 131 brand literals because changing it
1300
+ * changes who becomes an administrator. CLAUDE.md Rule #1 puts "security
1301
+ * boundaries: auth, RBAC" on the escalate-before-acting list, and rewiring an
1302
+ * admin-promotion check as a drive-by inside a branding sweep is exactly the
1303
+ * shape that rule forbids — a wrong move here either locks the owner out or
1304
+ * hands the role to someone else.
1305
+ *
1306
+ * The fix needs an owner decision, and the options differ in blast radius:
1307
+ * move it to `appkit.config.js` and inject it like the brand; replace
1308
+ * email-matching with an explicit role grant; or drop auto-promotion and seed
1309
+ * the first admin. Tracked, not silently chosen.
1310
+ */
1288
1311
  ADMIN_EMAIL: "admin@letitrip.in",
1289
1312
  };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Browser-storage and postMessage keys — one namespace, configurable.
3
+ *
4
+ * ## Why this exists
5
+ *
6
+ * Seven call sites hardcoded a `letitrip`-prefixed key:
7
+ *
8
+ * localStorage "letitrip:section-state" · "letitrip:last-category"
9
+ * "letitrip:announcement-dismissed" · "letitrip_oauth_redirect"
10
+ * "letitrip_search_type"
11
+ * postMessage "letitrip_auth_close"
12
+ *
13
+ * For the app that shipped them that is harmless. For a LIBRARY it is not:
14
+ * every consumer inherits one marketplace's namespace, two appkit apps on the
15
+ * same origin collide, and the brand is baked into data the user's browser
16
+ * keeps. Tier AK's whole premise is that appkit should not know whose app it
17
+ * is in.
18
+ *
19
+ * ## 🛑 The prefix is read at CALL TIME, not at module load
20
+ *
21
+ * `storageKey()` is a function and the keys below are getters. A consumer sets
22
+ * the namespace from its own bootstrap, which runs after this module is
23
+ * evaluated — so a `const KEY = PREFIX + ":x"` captured at import time would
24
+ * freeze the default and silently ignore the configuration. That is the same
25
+ * trap as a mirror field (Root Cause #42): two things that must agree, where
26
+ * only one is ever written.
27
+ *
28
+ * ## 🛑 Changing the namespace ORPHANS existing values
29
+ *
30
+ * These keys are how a returning visitor's dismissed banner and collapsed
31
+ * sections are remembered. Renaming the prefix does not migrate them — the old
32
+ * entries simply stop being read. That is acceptable for UI state and would NOT
33
+ * be for anything load-bearing, which is why nothing load-bearing belongs here:
34
+ * the auth session is an HTTP-only cookie, not a storage key.
35
+ *
36
+ * ## The separator is deliberately NOT normalised
37
+ *
38
+ * Five keys use `:` and three use `_`. They are kept exactly as they were,
39
+ * because normalising them is a silent data migration for every existing
40
+ * visitor in exchange for tidiness. New keys should use `:`.
41
+ */
42
+ /**
43
+ * Set the prefix every appkit storage key carries. Call once, from the
44
+ * consumer's client bootstrap, before any UI mounts.
45
+ *
46
+ * Idempotent and last-call-wins; there is no unset, because a namespace that
47
+ * changes mid-session would strand whatever was written under the previous one.
48
+ */
49
+ export declare function setStorageNamespace(value: string): void;
50
+ /** The active namespace. Exported for diagnostics, not for building keys. */
51
+ export declare function getStorageNamespace(): string;
52
+ /**
53
+ * Build a namespaced key.
54
+ *
55
+ * @param suffix the part after the separator, e.g. `"last-category"`
56
+ * @param separator `":"` for new keys; `"_"` only where an existing key used it
57
+ */
58
+ export declare function storageKey(suffix: string, separator?: ":" | "_"): string;
59
+ /**
60
+ * The keys appkit itself owns.
61
+ *
62
+ * Getters, not constants — see the call-time note above. Each records the
63
+ * literal it replaced so the migration is auditable and a reader can tell at a
64
+ * glance which separator is historical rather than chosen.
65
+ */
66
+ export declare const STORAGE_KEYS: {
67
+ /** Collapsed account-page sections. Was `"letitrip:section-state"`. */
68
+ readonly sectionState: string;
69
+ /** Last category visited, for the contextual trail. Was `"letitrip:last-category"`. */
70
+ readonly lastCategory: string;
71
+ /** Announcement bar dismissal. Was `"letitrip:announcement-dismissed"`. */
72
+ readonly announcementDismissed: string;
73
+ /** OAuth return URL across the popup round-trip. Was `"letitrip_oauth_redirect"`. */
74
+ readonly oauthRedirect: string;
75
+ /** Remembered search-type tab. Was `"letitrip_search_type"`. */
76
+ readonly searchType: string;
77
+ };
78
+ /**
79
+ * postMessage `type` values.
80
+ *
81
+ * 🛑 Namespaced for the same reason, and with a sharper edge: a `message`
82
+ * listener receives events from ANY window that can reach it, so an
83
+ * unnamespaced type is both a collision and an input-validation question. The
84
+ * listeners that read this must keep checking `event.origin` regardless — a
85
+ * namespace is not an authentication mechanism.
86
+ */
87
+ export declare const MESSAGE_TYPES: {
88
+ /** Auth popup signalling it is done. Was `"letitrip_auth_close"`. */
89
+ readonly authClose: string;
90
+ };