@mohasinac/appkit 4.14.0 → 4.15.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 (157) hide show
  1. package/dist/_internal/server/features/auctions/actions.js +2 -2
  2. package/dist/_internal/server/features/cart/service.d.ts +3 -3
  3. package/dist/_internal/server/features/checkout/actions.js +19 -0
  4. package/dist/_internal/server/features/checkout/source-context.d.ts +5 -0
  5. package/dist/_internal/server/features/checkout/source-context.js +157 -0
  6. package/dist/_internal/server/features/offers/adapters.d.ts +103 -0
  7. package/dist/_internal/server/features/offers/adapters.js +88 -0
  8. package/dist/_internal/server/features/offers/index.d.ts +2 -0
  9. package/dist/_internal/server/features/offers/index.js +1 -0
  10. package/dist/_internal/server/features/orders/adapters.js +24 -0
  11. package/dist/_internal/server/jobs/core/offerExpiry.js +10 -10
  12. package/dist/_internal/shared/actions/action-registry.js +3 -1
  13. package/dist/_internal/shared/features/auctions/schema.d.ts +14 -2
  14. package/dist/_internal/shared/features/auctions/schema.js +13 -1
  15. package/dist/_internal/shared/features/cart/schema.d.ts +14 -9
  16. package/dist/_internal/shared/features/cart/schema.js +15 -0
  17. package/dist/_internal/shared/features/products/schema.d.ts +2 -2
  18. package/dist/_internal/shared/history/history.d.ts +22 -0
  19. package/dist/_internal/shared/history/history.js +41 -1
  20. package/dist/client.d.ts +13 -2
  21. package/dist/client.js +20 -2
  22. package/dist/constants/field-names.d.ts +8 -6
  23. package/dist/constants/field-names.js +48 -6
  24. package/dist/features/account/components/UserBidsView.js +12 -4
  25. package/dist/features/account/components/UserReturnsView.d.ts +3 -0
  26. package/dist/features/account/components/UserReturnsView.js +17 -1
  27. package/dist/features/admin/components/AdminAuditLogView.js +1 -0
  28. package/dist/features/admin/components/AdminBidsView.js +15 -2
  29. package/dist/features/admin/components/AdminBlogEditorView.js +65 -33
  30. package/dist/features/admin/components/AdminFeatureEditorView.js +35 -6
  31. package/dist/features/admin/components/AdminNavEditorView.js +15 -5
  32. package/dist/features/admin/components/AdminNewsletterView.js +18 -2
  33. package/dist/features/admin/components/AdminOffersView.js +62 -18
  34. package/dist/features/admin/components/AdminPaymentMethodsView.js +24 -4
  35. package/dist/features/admin/components/AdminScammerEditorView.d.ts +2 -1
  36. package/dist/features/admin/components/AdminScammerEditorView.js +3 -13
  37. package/dist/features/admin/components/AdminScammersView.js +3 -8
  38. package/dist/features/admin/components/AdminSectionsView.js +32 -23
  39. package/dist/features/admin/components/AdminSessionsView.js +17 -2
  40. package/dist/features/admin/components/ViewAuditLogEntryModal.js +1 -0
  41. package/dist/features/admin/components/sections/adminSectionsTypes.d.ts +4 -2
  42. package/dist/features/admin/components/sections/adminSectionsTypes.js +45 -27
  43. package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
  44. package/dist/features/admin/constants/filter-tabs.js +1 -0
  45. package/dist/features/admin/schemas/homepage-section-form.d.ts +22 -0
  46. package/dist/features/admin/schemas/homepage-section-form.js +68 -0
  47. package/dist/features/admin/schemas/nav-item-form.d.ts +74 -0
  48. package/dist/features/admin/schemas/nav-item-form.js +72 -0
  49. package/dist/features/auctions/components/AuctionBidsTable.d.ts +10 -1
  50. package/dist/features/auctions/components/AuctionBidsTable.js +22 -4
  51. package/dist/features/auctions/components/bid-detail-fields.d.ts +29 -0
  52. package/dist/features/auctions/components/bid-detail-fields.js +103 -0
  53. package/dist/features/audit-log/schemas/firestore.d.ts +14 -0
  54. package/dist/features/audit-log/schemas/firestore.js +7 -0
  55. package/dist/features/blog/schemas/blog-form.d.ts +80 -0
  56. package/dist/features/blog/schemas/blog-form.js +100 -0
  57. package/dist/features/events/components/AdminEventEditorView.js +32 -54
  58. package/dist/features/events/components/AdminEventEntriesView.js +22 -3
  59. package/dist/features/events/schemas/event-form.d.ts +113 -0
  60. package/dist/features/events/schemas/event-form.js +82 -0
  61. package/dist/features/grouped/schemas/grouped-listing-form.d.ts +101 -0
  62. package/dist/features/grouped/schemas/grouped-listing-form.js +89 -0
  63. package/dist/features/homepage/actions/homepage-section-actions.d.ts +11 -3
  64. package/dist/features/homepage/actions/homepage-section-actions.js +33 -1
  65. package/dist/features/homepage/schemas/index.d.ts +31 -0
  66. package/dist/features/homepage/schemas/index.js +24 -0
  67. package/dist/features/orders/components/OrderStatusTimeline.d.ts +5 -1
  68. package/dist/features/orders/components/OrderStatusTimeline.js +39 -20
  69. package/dist/features/orders/repository/orders.repository.js +5 -0
  70. package/dist/features/orders/schemas/firestore.d.ts +36 -1
  71. package/dist/features/orders/types/index.d.ts +35 -7
  72. package/dist/features/products/components/GroupSettingsPanel.js +5 -1
  73. package/dist/features/products/schemas/index.d.ts +2 -2
  74. package/dist/features/products/schemas/product-features.validators.d.ts +73 -0
  75. package/dist/features/products/schemas/product-features.validators.js +63 -0
  76. package/dist/features/scams/constants/scammer-status-display.d.ts +28 -0
  77. package/dist/features/scams/constants/scammer-status-display.js +79 -0
  78. package/dist/features/seller/actions/offer-actions.js +32 -2
  79. package/dist/features/seller/components/OfferPhaseTimeline.d.ts +59 -0
  80. package/dist/features/seller/components/OfferPhaseTimeline.js +107 -0
  81. package/dist/features/seller/components/SellerBidsView.js +11 -4
  82. package/dist/features/seller/components/SellerCouponEditorView.js +17 -17
  83. package/dist/features/seller/components/SellerOffersView.js +2 -1
  84. package/dist/features/seller/components/SellerPayoutSettingsView.js +38 -14
  85. package/dist/features/seller/repository/offer.repository.d.ts +69 -8
  86. package/dist/features/seller/repository/offer.repository.js +127 -23
  87. package/dist/features/seller/schemas/coupon-form.d.ts +72 -0
  88. package/dist/features/seller/schemas/coupon-form.js +116 -0
  89. package/dist/features/seller/schemas/firestore.d.ts +56 -2
  90. package/dist/features/seller/schemas/firestore.js +27 -0
  91. package/dist/features/seller/schemas/offer-forms.d.ts +21 -0
  92. package/dist/features/seller/schemas/offer-forms.js +21 -0
  93. package/dist/features/shell/build-sections.d.ts +63 -0
  94. package/dist/features/shell/build-sections.js +207 -0
  95. package/dist/features/shell/field-ui-meta.d.ts +115 -0
  96. package/dist/features/shell/field-ui-meta.js +324 -0
  97. package/dist/features/shell/index.d.ts +4 -0
  98. package/dist/features/shell/index.js +2 -0
  99. package/dist/features/status-history/components/StatusTimeline.d.ts +74 -0
  100. package/dist/features/status-history/components/StatusTimeline.js +46 -0
  101. package/dist/features/status-history/index.d.ts +11 -0
  102. package/dist/features/status-history/index.js +10 -0
  103. package/dist/features/store-extensions/components/ReviewDecisionModal.d.ts +27 -0
  104. package/dist/features/store-extensions/components/ReviewDecisionModal.js +82 -0
  105. package/dist/features/store-extensions/index.d.ts +12 -0
  106. package/dist/features/store-extensions/index.js +12 -0
  107. package/dist/features/store-extensions/schemas/admin-notification-form.d.ts +36 -0
  108. package/dist/features/store-extensions/schemas/admin-notification-form.js +114 -0
  109. package/dist/features/store-extensions/schemas/analytics-forms.d.ts +57 -0
  110. package/dist/features/store-extensions/schemas/analytics-forms.js +158 -0
  111. package/dist/features/store-extensions/schemas/custom-role-form.d.ts +85 -0
  112. package/dist/features/store-extensions/schemas/custom-role-form.js +118 -0
  113. package/dist/features/store-extensions/schemas/firestore.d.ts +21 -1
  114. package/dist/features/store-extensions/schemas/firestore.js +60 -0
  115. package/dist/features/store-extensions/schemas/google-config-form.d.ts +53 -0
  116. package/dist/features/store-extensions/schemas/google-config-form.js +105 -0
  117. package/dist/features/store-extensions/schemas/item-request-create-form.d.ts +28 -0
  118. package/dist/features/store-extensions/schemas/item-request-create-form.js +80 -0
  119. package/dist/features/store-extensions/schemas/listing-template-form.d.ts +78 -0
  120. package/dist/features/store-extensions/schemas/listing-template-form.js +87 -0
  121. package/dist/features/store-extensions/schemas/moderation-review-form.d.ts +49 -0
  122. package/dist/features/store-extensions/schemas/moderation-review-form.js +101 -0
  123. package/dist/features/store-extensions/schemas/payout-method-form.d.ts +153 -0
  124. package/dist/features/store-extensions/schemas/payout-method-form.js +155 -0
  125. package/dist/features/store-extensions/schemas/report-create-form.d.ts +25 -0
  126. package/dist/features/store-extensions/schemas/report-create-form.js +79 -0
  127. package/dist/features/store-extensions/schemas/report-review-form.d.ts +47 -0
  128. package/dist/features/store-extensions/schemas/report-review-form.js +111 -0
  129. package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +148 -0
  130. package/dist/features/store-extensions/schemas/shipping-config-form.js +104 -0
  131. package/dist/features/store-extensions/schemas/store-category-form.d.ts +54 -0
  132. package/dist/features/store-extensions/schemas/store-category-form.js +73 -0
  133. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +326 -0
  134. package/dist/index.d.ts +12 -2
  135. package/dist/index.js +12 -2
  136. package/dist/next/routing/route-map.d.ts +16 -2
  137. package/dist/next/routing/route-map.js +9 -2
  138. package/dist/schemas/firestore-value.d.ts +8 -0
  139. package/dist/schemas/firestore-value.js +44 -0
  140. package/dist/schemas/index.d.ts +1 -1
  141. package/dist/schemas/index.js +1 -1
  142. package/dist/schemas/registry.d.ts +634 -1
  143. package/dist/schemas/registry.js +64 -1
  144. package/dist/seed/bids-seed-data.js +30 -0
  145. package/dist/seed/offers-seed-data.js +212 -7
  146. package/dist/seed/orders-seed-data.js +336 -1
  147. package/dist/seed/products-auctions-seed-data.js +41 -0
  148. package/dist/server.d.ts +2 -0
  149. package/dist/server.js +5 -0
  150. package/dist/styles.css +1 -1
  151. package/dist/tailwind-utilities.css +1 -1
  152. package/dist/ui/components/RecordDetailModal.d.ts +10 -1
  153. package/dist/ui/components/RecordDetailModal.js +2 -2
  154. package/package.json +2 -1
  155. package/scripts/node-esm-loader.mjs +65 -0
  156. package/scripts/seed-cli-loader.mjs +6 -49
  157. package/scripts/seed-cli.mjs +1 -1
@@ -0,0 +1,85 @@
1
+ import { z } from "zod";
2
+ /** Whether a string is a permission this system can actually grant. */
3
+ export declare function isKnownPermission(value: string): boolean;
4
+ /**
5
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
6
+ * by schema instance and every zod wrapper returns a new one.
7
+ */
8
+ export declare const customRoleFormSchema: z.ZodObject<{
9
+ name: z.ZodString;
10
+ slug: z.ZodString;
11
+ description: z.ZodOptional<z.ZodString>;
12
+ scope: z.ZodEnum<["global", "store"]>;
13
+ permissions: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
14
+ inheritsFrom: z.ZodOptional<z.ZodString>;
15
+ isActive: z.ZodBoolean;
16
+ }, "strip", z.ZodTypeAny, {
17
+ scope: "store" | "global";
18
+ name: string;
19
+ slug: string;
20
+ isActive: boolean;
21
+ permissions: string[];
22
+ description?: string | undefined;
23
+ inheritsFrom?: string | undefined;
24
+ }, {
25
+ scope: "store" | "global";
26
+ name: string;
27
+ slug: string;
28
+ isActive: boolean;
29
+ permissions: string[];
30
+ description?: string | undefined;
31
+ inheritsFrom?: string | undefined;
32
+ }>;
33
+ export type CustomRoleFormValues = z.infer<typeof customRoleFormSchema>;
34
+ /** Create contract. `createdBy` is absent — the route sets it from the session. */
35
+ export declare const customRoleCreateSchema: z.ZodObject<{
36
+ name: z.ZodString;
37
+ slug: z.ZodString;
38
+ description: z.ZodOptional<z.ZodString>;
39
+ scope: z.ZodEnum<["global", "store"]>;
40
+ permissions: z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">;
41
+ inheritsFrom: z.ZodOptional<z.ZodString>;
42
+ isActive: z.ZodBoolean;
43
+ }, "strip", z.ZodTypeAny, {
44
+ scope: "store" | "global";
45
+ name: string;
46
+ slug: string;
47
+ isActive: boolean;
48
+ permissions: string[];
49
+ description?: string | undefined;
50
+ inheritsFrom?: string | undefined;
51
+ }, {
52
+ scope: "store" | "global";
53
+ name: string;
54
+ slug: string;
55
+ isActive: boolean;
56
+ permissions: string[];
57
+ description?: string | undefined;
58
+ inheritsFrom?: string | undefined;
59
+ }>;
60
+ /**
61
+ * Update contract — partial and `.strict()`, so an unknown key is a 400 rather
62
+ * than a silent write into a document the permission system reads.
63
+ */
64
+ export declare const customRoleUpdateSchema: z.ZodObject<{
65
+ name: z.ZodOptional<z.ZodString>;
66
+ description: z.ZodOptional<z.ZodString>;
67
+ scope: z.ZodOptional<z.ZodEnum<["global", "store"]>>;
68
+ permissions: z.ZodOptional<z.ZodArray<z.ZodEffects<z.ZodString, string, string>, "many">>;
69
+ inheritsFrom: z.ZodOptional<z.ZodString>;
70
+ isActive: z.ZodOptional<z.ZodBoolean>;
71
+ }, "strict", z.ZodTypeAny, {
72
+ scope?: "store" | "global" | undefined;
73
+ name?: string | undefined;
74
+ description?: string | undefined;
75
+ isActive?: boolean | undefined;
76
+ permissions?: string[] | undefined;
77
+ inheritsFrom?: string | undefined;
78
+ }, {
79
+ scope?: "store" | "global" | undefined;
80
+ name?: string | undefined;
81
+ description?: string | undefined;
82
+ isActive?: boolean | undefined;
83
+ permissions?: string[] | undefined;
84
+ inheritsFrom?: string | undefined;
85
+ }>;
@@ -0,0 +1,118 @@
1
+ /*
2
+ * WHY: Custom roles GRANT PERMISSIONS, and neither creating nor updating one
3
+ * validated anything. Measured 2026-08-24:
4
+ * · `POST /api/admin/roles` — `parseJsonBody` then `...body` spread
5
+ * into `.create()`.
6
+ * · `PATCH /api/admin/roles/[id]` — the raw body handed straight to
7
+ * `.update()`.
8
+ * · Both pages (`admin/roles/new`, `.../[id]/edit`) are raw controls
9
+ * with no Zod anywhere.
10
+ * So a role could be written with no name, a `permissions` array holding
11
+ * arbitrary strings, a `scope` outside its union, or any invented key —
12
+ * all persisted verbatim into the document the permission system reads.
13
+ * WHAT: One schema for both pages AND both routes.
14
+ *
15
+ * ## Permissions are checked against the real catalogue
16
+ *
17
+ * `Permission` is a closed union of ~85 `admin:resource:action` strings, and
18
+ * `PERMISSION_GROUPS` is its runtime form. A permission string that is not in
19
+ * it grants nothing — it silently never matches — so a role built from typos
20
+ * looks configured and does nothing, which is worse than being rejected.
21
+ *
22
+ * `"*"` is accepted deliberately: `security/rbac/types.ts` defines it as the
23
+ * wildcard, and rejecting it here would break a legitimate super-role.
24
+ *
25
+ * ## What is NOT accepted from the body
26
+ *
27
+ * `createdBy` is set from the session by the route. `id`/`createdAt`/
28
+ * `updatedAt` are the repository's. A caller able to set `createdBy` could
29
+ * attribute a privilege grant to somebody else, which is precisely the field
30
+ * an audit trail depends on.
31
+ *
32
+ * EXPORTS:
33
+ * customRoleFormSchema, customRoleCreateSchema, customRoleUpdateSchema,
34
+ * isKnownPermission, type CustomRoleFormValues
35
+ *
36
+ * @tag domain:store-extensions,rbac
37
+ * @tag layer:schema
38
+ * @tag pattern:none
39
+ * @tag access:isomorphic
40
+ * @tag consumers:admin/roles pages,/api/admin/roles
41
+ * @tag sideEffects:none
42
+ */
43
+ import { z } from "zod";
44
+ import { annotate } from "../../shell/field-ui-meta";
45
+ import { PERMISSION_GROUPS } from "../../auth/permissions/constants";
46
+ /**
47
+ * Every permission the catalogue actually defines, plus the wildcard.
48
+ *
49
+ * Derived from `PERMISSION_GROUPS` rather than restated — a hand-written copy
50
+ * would be the second list of ~85 strings to keep in sync, and the first one
51
+ * to drift (Root Cause #61).
52
+ */
53
+ const KNOWN_PERMISSIONS = new Set([
54
+ "*",
55
+ ...Object.values(PERMISSION_GROUPS).flat(),
56
+ ]);
57
+ /** Whether a string is a permission this system can actually grant. */
58
+ export function isKnownPermission(value) {
59
+ return KNOWN_PERMISSIONS.has(value);
60
+ }
61
+ const permissionSchema = z.string().refine(isKnownPermission, {
62
+ message: "Not a permission this system defines — it would grant nothing.",
63
+ });
64
+ /**
65
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
66
+ * by schema instance and every zod wrapper returns a new one.
67
+ */
68
+ export const customRoleFormSchema = z.object({
69
+ name: annotate(z.string().min(1, "A role needs a name.").max(80, "Keep the name under 80 characters."), {
70
+ section: "basics", sectionLabel: "Role", sectionRequired: true,
71
+ quick: true, order: 1, row: "pair",
72
+ }),
73
+ slug: annotate(z
74
+ .string()
75
+ .min(1, "A slug is required.")
76
+ .max(80)
77
+ .regex(/^[a-z0-9-]+$/, "Use lowercase letters, numbers and hyphens only."), {
78
+ section: "basics", quick: true, order: 2, row: "pair",
79
+ tier: "t1-derive",
80
+ help: "Generated from the name when left blank.",
81
+ }),
82
+ description: annotate(z.string().max(300, "Keep the description under 300 characters.").optional(), {
83
+ section: "basics", order: 3, row: "full",
84
+ }),
85
+ scope: annotate(z.enum(["global", "store"]), {
86
+ section: "basics", quick: true, order: 4, row: "pair",
87
+ }),
88
+ permissions: annotate(z.array(permissionSchema), {
89
+ section: "permissions", sectionLabel: "Permissions",
90
+ order: 1, row: "full", kind: "list",
91
+ }),
92
+ inheritsFrom: annotate(z.string().max(80).optional(), {
93
+ section: "permissions", order: 2, row: "pair",
94
+ help: "Another role whose permissions this one starts from.",
95
+ }),
96
+ isActive: annotate(z.boolean(), {
97
+ section: "visibility", sectionLabel: "Visibility", order: 1, row: "quarter",
98
+ }),
99
+ });
100
+ /** Create contract. `createdBy` is absent — the route sets it from the session. */
101
+ export const customRoleCreateSchema = customRoleFormSchema;
102
+ /**
103
+ * Update contract — partial and `.strict()`, so an unknown key is a 400 rather
104
+ * than a silent write into a document the permission system reads.
105
+ */
106
+ export const customRoleUpdateSchema = z
107
+ .object({
108
+ name: z.string().min(1).max(80).optional(),
109
+ description: z.string().max(300).optional(),
110
+ scope: z.enum(["global", "store"]).optional(),
111
+ permissions: z.array(permissionSchema).optional(),
112
+ inheritsFrom: z.string().max(80).optional(),
113
+ isActive: z.boolean().optional(),
114
+ // `slug` is deliberately absent: it is the role's stable identifier and
115
+ // renaming it would orphan every user already assigned the role. Same
116
+ // create-only rule every other slug in this codebase follows.
117
+ })
118
+ .strict();
@@ -100,7 +100,25 @@ export interface StoreCategoryDocument extends BaseDocument {
100
100
  export declare const STORE_CATEGORIES_COLLECTION: "storeCategories";
101
101
  export declare const STORE_CATEGORY_INDEXED_FIELDS: readonly ["storeId", "slug", "isActive", "displayOrder", "createdAt"];
102
102
  export declare const DEFAULT_STORE_CATEGORY_DATA: Partial<StoreCategoryDocument>;
103
- export type ListingTemplateType = "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | "classified" | "digital-code" | "live";
103
+ /**
104
+ * The listing kinds a seller can save a template for.
105
+ *
106
+ * A runtime array, with the type DERIVED from it — so the Zod enum in
107
+ * `listing-template-form.ts` reads this rather than restating it. That was the
108
+ * eleventh hand-written copy of the listing-type union in this codebase, and
109
+ * the tenth had already drifted (Root Cause #61).
110
+ *
111
+ * Two deliberate differences from `ListingType`:
112
+ * - `bundle` is KEPT even though SB-UNI-D stopped it being a listingType. A
113
+ * seeded template stores it (`store-extensions-seed-data.ts:202`), so
114
+ * dropping it here would make that template fail validation and become
115
+ * unsaveable — the same defect as the blog `slug` regex no stored row
116
+ * satisfied.
117
+ * - `art` and `stickers` are ADDED. They are real listing types a seller can
118
+ * create, and their absence meant a template could never be saved for them.
119
+ */
120
+ export declare const LISTING_TEMPLATE_TYPES: readonly ["standard", "auction", "pre-order", "prize-draw", "bundle", "classified", "digital-code", "live", "art", "stickers"];
121
+ export type ListingTemplateType = (typeof LISTING_TEMPLATE_TYPES)[number];
104
122
  export interface ListingTemplateDocument extends BaseDocument {
105
123
  storeId: string;
106
124
  ownerId: string;
@@ -151,6 +169,8 @@ export interface ReportDocument extends BaseDocument {
151
169
  resolution?: string;
152
170
  resolvedAt?: Date;
153
171
  }
172
+ export declare const REPORT_ENTITY_TYPES: [ReportEntityType, ...ReportEntityType[]];
173
+ export declare const REPORT_REASONS: [ReportReason, ...ReportReason[]];
154
174
  export declare const REPORTS_COLLECTION: "reports";
155
175
  export declare const REPORT_INDEXED_FIELDS: readonly ["entityType", "entityId", "reporterId", "reason", "status", "createdAt"];
156
176
  export declare const DEFAULT_REPORT_DATA: Partial<ReportDocument>;
@@ -63,6 +63,36 @@ export const DEFAULT_STORE_CATEGORY_DATA = {
63
63
  displayOrder: 0,
64
64
  productIds: [],
65
65
  };
66
+ // 6. ───── listingTemplates ───────────────────────────────────────────────
67
+ /**
68
+ * The listing kinds a seller can save a template for.
69
+ *
70
+ * A runtime array, with the type DERIVED from it — so the Zod enum in
71
+ * `listing-template-form.ts` reads this rather than restating it. That was the
72
+ * eleventh hand-written copy of the listing-type union in this codebase, and
73
+ * the tenth had already drifted (Root Cause #61).
74
+ *
75
+ * Two deliberate differences from `ListingType`:
76
+ * - `bundle` is KEPT even though SB-UNI-D stopped it being a listingType. A
77
+ * seeded template stores it (`store-extensions-seed-data.ts:202`), so
78
+ * dropping it here would make that template fail validation and become
79
+ * unsaveable — the same defect as the blog `slug` regex no stored row
80
+ * satisfied.
81
+ * - `art` and `stickers` are ADDED. They are real listing types a seller can
82
+ * create, and their absence meant a template could never be saved for them.
83
+ */
84
+ export const LISTING_TEMPLATE_TYPES = [
85
+ "standard",
86
+ "auction",
87
+ "pre-order",
88
+ "prize-draw",
89
+ "bundle",
90
+ "classified",
91
+ "digital-code",
92
+ "live",
93
+ "art",
94
+ "stickers",
95
+ ];
66
96
  export const LISTING_TEMPLATES_COLLECTION = "listingTemplates";
67
97
  export const LISTING_TEMPLATE_INDEXED_FIELDS = [
68
98
  "storeId",
@@ -92,6 +122,36 @@ export const MODERATION_QUEUE_INDEXED_FIELDS = [
92
122
  export const DEFAULT_MODERATION_QUEUE_DATA = {
93
123
  status: "pending",
94
124
  };
125
+ /*
126
+ * Runtime companions for the three report unions above.
127
+ *
128
+ * Keyed `Record<Union, true>` rather than written as arrays, because that is
129
+ * the only shape where BOTH mistakes are compile errors: a member added to the
130
+ * union and forgotten here, AND a value here that is not a real member. An
131
+ * array literal catches neither, which is how ten independent enumerations of
132
+ * `ListingType` came to disagree (Recurrent Root Cause #61).
133
+ */
134
+ const REPORT_ENTITY_TYPE_MAP = {
135
+ product: true,
136
+ store: true,
137
+ review: true,
138
+ event: true,
139
+ user: true,
140
+ blog: true,
141
+ comment: true,
142
+ };
143
+ export const REPORT_ENTITY_TYPES = Object.keys(REPORT_ENTITY_TYPE_MAP);
144
+ const REPORT_REASON_MAP = {
145
+ scam: true,
146
+ counterfeit: true,
147
+ prohibited: true,
148
+ inappropriate: true,
149
+ harassment: true,
150
+ spam: true,
151
+ "ip-violation": true,
152
+ other: true,
153
+ };
154
+ export const REPORT_REASONS = Object.keys(REPORT_REASON_MAP);
95
155
  export const REPORTS_COLLECTION = "reports";
96
156
  export const REPORT_INDEXED_FIELDS = [
97
157
  "entityType",
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import type { StoreGoogleConfigDocument } from "./firestore";
3
+ /**
4
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
5
+ * by schema instance and every zod wrapper returns a new one.
6
+ */
7
+ export declare const storeGoogleConfigUpdateSchema: z.ZodObject<{
8
+ isConnected: z.ZodOptional<z.ZodBoolean>;
9
+ placeId: z.ZodOptional<z.ZodString>;
10
+ businessName: z.ZodOptional<z.ZodString>;
11
+ }, "strict", z.ZodTypeAny, {
12
+ placeId?: string | undefined;
13
+ isConnected?: boolean | undefined;
14
+ businessName?: string | undefined;
15
+ }, {
16
+ placeId?: string | undefined;
17
+ isConnected?: boolean | undefined;
18
+ businessName?: string | undefined;
19
+ }>;
20
+ export type StoreGoogleConfigFormValues = z.infer<typeof storeGoogleConfigUpdateSchema>;
21
+ /**
22
+ * What the seller's own settings page may see.
23
+ *
24
+ * An ALLOW-list, built as a new object — never the document with keys deleted.
25
+ * A deny-list publishes every field nobody thought to remove, including ones
26
+ * the interface does not declare, which is exactly how a decrypted API token
27
+ * and an unmasked ad-credentials blob reached anonymous callers before
28
+ * (Recurrent Root Cause #70).
29
+ *
30
+ * PUBLIC (to the store owner):
31
+ * storeId, isConnected — what the page renders
32
+ * placeId, businessName — what the page lets them edit
33
+ * averageRating, totalReviews, lastSyncedAt — sync output they need to see
34
+ * id, createdAt, updatedAt — ordinary record metadata
35
+ *
36
+ * PRIVATE:
37
+ * oauthRefreshToken — a long-lived Google credential with no reader and no
38
+ * writer anywhere in either tree. It was being sent to the browser on every
39
+ * load simply because the route returned the document whole.
40
+ */
41
+ export declare function toSellerGoogleConfig(doc: StoreGoogleConfigDocument): {
42
+ id: string;
43
+ storeId: string;
44
+ isConnected: boolean;
45
+ placeId: string | undefined;
46
+ businessName: string | undefined;
47
+ averageRating: number | undefined;
48
+ totalReviews: number | undefined;
49
+ lastSyncedAt: Date | undefined;
50
+ createdAt: Date;
51
+ updatedAt: Date;
52
+ };
53
+ export type SellerGoogleConfig = ReturnType<typeof toSellerGoogleConfig>;
@@ -0,0 +1,105 @@
1
+ /*
2
+ * WHY: `PUT /api/store/google-reviews` spread the raw request body into
3
+ * Firestore on both the create and the update branch, with `storeId`
4
+ * pinned and nothing else checked.
5
+ *
6
+ * ## 🛑 The field that makes this worth doing carefully
7
+ *
8
+ * `StoreGoogleConfigDocument` declares `oauthRefreshToken` — a long-lived
9
+ * Google credential. The raw spread meant a request body could set it, and the
10
+ * sibling `GET` returns the document WHOLE, so it was also being sent to the
11
+ * browser on every load of the seller's Google Reviews settings page.
12
+ *
13
+ * It is scoped to the caller's own store, so this is not a cross-tenant leak.
14
+ * It is still a secret in a page payload for no reason: a repo-wide grep finds
15
+ * **no reader and no writer** for that field anywhere — nothing sets it and
16
+ * nothing consumes it. Per the public-projection rule, a field nobody can name
17
+ * a reader for is private by default.
18
+ *
19
+ * So this schema deliberately does NOT declare it, and the route projects it
20
+ * out of the response. The field itself is left on the document rather than
21
+ * deleted — removing a schema field is a data-model change and deserves its
22
+ * own decision, not a drive-by inside a validation fix.
23
+ *
24
+ * ## `averageRating` / `totalReviews` / `lastSyncedAt` are absent too
25
+ *
26
+ * Those are SYNC OUTPUT — what Google last told us. A seller typing their own
27
+ * review count in is not configuration, it is fabricating the number the
28
+ * storefront displays.
29
+ *
30
+ * EXPORTS:
31
+ * storeGoogleConfigUpdateSchema, toSellerGoogleConfig,
32
+ * type StoreGoogleConfigFormValues, type SellerGoogleConfig
33
+ *
34
+ * @tag domain:store-extensions,google-reviews
35
+ * @tag layer:schema
36
+ * @tag pattern:none
37
+ * @tag access:isomorphic
38
+ * @tag consumers:/api/store/google-reviews
39
+ * @tag sideEffects:none
40
+ */
41
+ import { z } from "zod";
42
+ import { annotate } from "../../shell/field-ui-meta";
43
+ /**
44
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
45
+ * by schema instance and every zod wrapper returns a new one.
46
+ */
47
+ export const storeGoogleConfigUpdateSchema = z
48
+ .object({
49
+ isConnected: annotate(z.boolean().optional(), {
50
+ section: "google",
51
+ sectionLabel: "Google Reviews",
52
+ sectionRequired: true,
53
+ quick: true,
54
+ order: 1,
55
+ row: "quarter",
56
+ }),
57
+ placeId: annotate(z.string().max(256).optional(), {
58
+ section: "google",
59
+ quick: true,
60
+ order: 2,
61
+ row: "pair",
62
+ help: "The Google Place ID for this store's listing.",
63
+ }),
64
+ businessName: annotate(z.string().max(200).optional(), {
65
+ section: "google",
66
+ quick: true,
67
+ order: 3,
68
+ row: "pair",
69
+ }),
70
+ })
71
+ .strict();
72
+ /**
73
+ * What the seller's own settings page may see.
74
+ *
75
+ * An ALLOW-list, built as a new object — never the document with keys deleted.
76
+ * A deny-list publishes every field nobody thought to remove, including ones
77
+ * the interface does not declare, which is exactly how a decrypted API token
78
+ * and an unmasked ad-credentials blob reached anonymous callers before
79
+ * (Recurrent Root Cause #70).
80
+ *
81
+ * PUBLIC (to the store owner):
82
+ * storeId, isConnected — what the page renders
83
+ * placeId, businessName — what the page lets them edit
84
+ * averageRating, totalReviews, lastSyncedAt — sync output they need to see
85
+ * id, createdAt, updatedAt — ordinary record metadata
86
+ *
87
+ * PRIVATE:
88
+ * oauthRefreshToken — a long-lived Google credential with no reader and no
89
+ * writer anywhere in either tree. It was being sent to the browser on every
90
+ * load simply because the route returned the document whole.
91
+ */
92
+ export function toSellerGoogleConfig(doc) {
93
+ return {
94
+ id: doc.id,
95
+ storeId: doc.storeId,
96
+ isConnected: doc.isConnected,
97
+ placeId: doc.placeId,
98
+ businessName: doc.businessName,
99
+ averageRating: doc.averageRating,
100
+ totalReviews: doc.totalReviews,
101
+ lastSyncedAt: doc.lastSyncedAt,
102
+ createdAt: doc.createdAt,
103
+ updatedAt: doc.updatedAt,
104
+ };
105
+ }
@@ -0,0 +1,28 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
4
+ * by schema instance and every zod wrapper returns a new one.
5
+ */
6
+ export declare const itemRequestCreateSchema: z.ZodObject<{
7
+ title: z.ZodString;
8
+ description: z.ZodString;
9
+ category: z.ZodOptional<z.ZodString>;
10
+ brand: z.ZodOptional<z.ZodString>;
11
+ maxBudget: z.ZodOptional<z.ZodNumber>;
12
+ imageUrls: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ }, "strict", z.ZodTypeAny, {
14
+ title: string;
15
+ description: string;
16
+ brand?: string | undefined;
17
+ category?: string | undefined;
18
+ imageUrls?: string[] | undefined;
19
+ maxBudget?: number | undefined;
20
+ }, {
21
+ title: string;
22
+ description: string;
23
+ brand?: string | undefined;
24
+ category?: string | undefined;
25
+ imageUrls?: string[] | undefined;
26
+ maxBudget?: number | undefined;
27
+ }>;
28
+ export type ItemRequestCreateValues = z.infer<typeof itemRequestCreateSchema>;
@@ -0,0 +1,80 @@
1
+ /*
2
+ * WHY: `POST /api/item-requests` spread the raw body into Firestore, and this
3
+ * one is READ BY THE PUBLIC — `GET /api/item-requests` is `auth: false`
4
+ * and returns the open list to anyone.
5
+ *
6
+ * const body = await parseJsonBody<FirestoreDocument>(request);
7
+ * await itemRequestsRepository.create({ ...body, opUserId, ..., replies: [], replyCount: 0 });
8
+ *
9
+ * So an unvalidated write here is an unvalidated PUBLISH. A request could
10
+ * be created with no title and no description, and a caller could set
11
+ * `approvedAt` / `approvedBy` — the fields that record a moderator having
12
+ * cleared it.
13
+ *
14
+ * `status`, `replies` and `replyCount` were pinned server-side, so the
15
+ * approval GATE itself held; what a caller could forge was the RECORD of
16
+ * approval, which is the part a human reads when deciding whether it has
17
+ * already been looked at.
18
+ *
19
+ * WHAT: The create contract — title and description required, budget bounded,
20
+ * and nothing a moderator owns.
21
+ *
22
+ * ## `maxBudget` is coerced and bounded
23
+ *
24
+ * It arrives from a number input as a string. Uncoerced it was written to
25
+ * Firestore as a string, and a later numeric comparison against it would sort
26
+ * lexically — the quiet half of the same class as the `resolvedAt` string that
27
+ * was being written into a Date field on the reports route.
28
+ *
29
+ * EXPORTS: itemRequestCreateSchema, type ItemRequestCreateValues
30
+ *
31
+ * @tag domain:store-extensions,item-requests
32
+ * @tag layer:schema
33
+ * @tag pattern:none
34
+ * @tag access:isomorphic
35
+ * @tag consumers:/api/item-requests
36
+ * @tag sideEffects:none
37
+ */
38
+ import { z } from "zod";
39
+ import { annotate } from "../../shell/field-ui-meta";
40
+ /**
41
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
42
+ * by schema instance and every zod wrapper returns a new one.
43
+ */
44
+ export const itemRequestCreateSchema = z
45
+ .object({
46
+ title: annotate(z
47
+ .string()
48
+ .min(3, "Give the request a title of at least 3 characters.")
49
+ .max(200, "Keep the title under 200 characters."), {
50
+ section: "basics",
51
+ sectionLabel: "What are you looking for?",
52
+ sectionRequired: true,
53
+ quick: true,
54
+ order: 1,
55
+ row: "full",
56
+ }),
57
+ description: annotate(z
58
+ .string()
59
+ .min(10, "Describe what you're after in at least 10 characters.")
60
+ .max(5000, "Keep the description under 5000 characters."), { section: "basics", quick: true, order: 2, row: "full", kind: "textarea" }),
61
+ category: annotate(z.string().max(120).optional(), {
62
+ section: "basics",
63
+ order: 3,
64
+ row: "pair",
65
+ }),
66
+ brand: annotate(z.string().max(120).optional(), {
67
+ section: "basics",
68
+ order: 4,
69
+ row: "pair",
70
+ }),
71
+ // Coerced — see the header. Bounded so a typo cannot record a budget that
72
+ // reads as real money.
73
+ maxBudget: annotate(z.coerce
74
+ .number()
75
+ .min(0, "A budget cannot be negative.")
76
+ .max(10000000, "That budget looks like a typo.")
77
+ .optional(), { section: "basics", order: 5, row: "pair", kind: "number" }),
78
+ imageUrls: annotate(z.array(z.string().url("Each image must be a URL.")).max(10).optional(), { section: "media", sectionLabel: "Photos", order: 1, row: "full", kind: "media" }),
79
+ })
80
+ .strict();
@@ -0,0 +1,78 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
4
+ * by schema instance and every zod wrapper returns a new one.
5
+ */
6
+ export declare const listingTemplateFormSchema: z.ZodObject<{
7
+ name: z.ZodString;
8
+ description: z.ZodOptional<z.ZodString>;
9
+ listingType: z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "bundle", "classified", "digital-code", "live", "art", "stickers"]>;
10
+ defaults: z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>;
11
+ isShared: z.ZodBoolean;
12
+ isActive: z.ZodBoolean;
13
+ }, "strip", z.ZodTypeAny, {
14
+ name: string;
15
+ listingType: "art" | "stickers" | "classified" | "standard" | "auction" | "pre-order" | "prize-draw" | "digital-code" | "live" | "bundle";
16
+ isActive: boolean;
17
+ isShared: boolean;
18
+ defaults: Record<string, import("../../..").FirestoreValue>;
19
+ description?: string | undefined;
20
+ }, {
21
+ name: string;
22
+ listingType: "art" | "stickers" | "classified" | "standard" | "auction" | "pre-order" | "prize-draw" | "digital-code" | "live" | "bundle";
23
+ isActive: boolean;
24
+ isShared: boolean;
25
+ defaults: Record<string, import("../../..").FirestoreValue>;
26
+ description?: string | undefined;
27
+ }>;
28
+ export type ListingTemplateFormValues = z.infer<typeof listingTemplateFormSchema>;
29
+ /**
30
+ * Create contract. `storeId`/`ownerId` are deliberately absent: the route sets
31
+ * both from the session, so a caller cannot create a template inside someone
32
+ * else's store by adding a field to the body.
33
+ */
34
+ export declare const listingTemplateCreateSchema: z.ZodObject<{
35
+ name: z.ZodString;
36
+ description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
37
+ listingType: z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "bundle", "classified", "digital-code", "live", "art", "stickers"]>;
38
+ isActive: z.ZodOptional<z.ZodBoolean>;
39
+ isShared: z.ZodOptional<z.ZodBoolean>;
40
+ defaults: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
41
+ }, "strip", z.ZodTypeAny, {
42
+ name: string;
43
+ listingType: "art" | "stickers" | "classified" | "standard" | "auction" | "pre-order" | "prize-draw" | "digital-code" | "live" | "bundle";
44
+ description?: string | undefined;
45
+ isActive?: boolean | undefined;
46
+ isShared?: boolean | undefined;
47
+ defaults?: Record<string, import("../../..").FirestoreValue> | undefined;
48
+ }, {
49
+ name: string;
50
+ listingType: "art" | "stickers" | "classified" | "standard" | "auction" | "pre-order" | "prize-draw" | "digital-code" | "live" | "bundle";
51
+ description?: string | undefined;
52
+ isActive?: boolean | undefined;
53
+ isShared?: boolean | undefined;
54
+ defaults?: Record<string, import("../../..").FirestoreValue> | undefined;
55
+ }>;
56
+ /** Update contract — every field optional, and the same ownership fields absent. */
57
+ export declare const listingTemplateUpdateSchema: z.ZodObject<{
58
+ name: z.ZodOptional<z.ZodString>;
59
+ description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
60
+ listingType: z.ZodOptional<z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "bundle", "classified", "digital-code", "live", "art", "stickers"]>>;
61
+ defaults: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<import("../../..").FirestoreValue, z.ZodTypeDef, import("../../..").FirestoreValue>>>;
62
+ isShared: z.ZodOptional<z.ZodBoolean>;
63
+ isActive: z.ZodOptional<z.ZodBoolean>;
64
+ }, "strip", z.ZodTypeAny, {
65
+ name?: string | undefined;
66
+ description?: string | undefined;
67
+ listingType?: "art" | "stickers" | "classified" | "standard" | "auction" | "pre-order" | "prize-draw" | "digital-code" | "live" | "bundle" | undefined;
68
+ isActive?: boolean | undefined;
69
+ isShared?: boolean | undefined;
70
+ defaults?: Record<string, import("../../..").FirestoreValue> | undefined;
71
+ }, {
72
+ name?: string | undefined;
73
+ description?: string | undefined;
74
+ listingType?: "art" | "stickers" | "classified" | "standard" | "auction" | "pre-order" | "prize-draw" | "digital-code" | "live" | "bundle" | undefined;
75
+ isActive?: boolean | undefined;
76
+ isShared?: boolean | undefined;
77
+ defaults?: Record<string, import("../../..").FirestoreValue> | undefined;
78
+ }>;