@mohasinac/appkit 4.41.4 โ†’ 4.41.6

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.
@@ -0,0 +1,46 @@
1
+ import type { LotteryConfig, ClientLotteryConfig } from "../../../../features/lottery/types";
2
+ /** A spin prize as a public visitor may see it: what it is, not what it is worth. */
3
+ export interface ClientSpinPrize {
4
+ id: string;
5
+ label: string;
6
+ isActive: boolean;
7
+ }
8
+ /** Allow-list, not a spread: `weight` and `couponId` are deliberately absent. */
9
+ export declare function toClientSpinPrize(prize: {
10
+ id: string;
11
+ label: string;
12
+ isActive?: boolean;
13
+ }): ClientSpinPrize;
14
+ /**
15
+ * The three fields this projection touches; every other field passes through.
16
+ *
17
+ * ๐Ÿ›‘ `lotteryConfig` accepts EITHER shape, and that is not laxness โ€” it is the
18
+ * reason this leak survived review. `EventItem.lotteryConfig` is declared as
19
+ * `ClientLotteryConfig`, i.e. the already-projected type, while the repository
20
+ * hands back the STORED document with `price`, `weight` and `bookedByUserId`
21
+ * still on every slot. The type asserted the projection had happened; nothing
22
+ * had performed it. Accepting both means the adapter can be applied to a value
23
+ * whose type is lying, which is precisely where it is needed.
24
+ */
25
+ export interface PublicEventInput {
26
+ createdBy?: string;
27
+ lotteryConfig?: LotteryConfig | ClientLotteryConfig;
28
+ spinPrizes?: {
29
+ id: string;
30
+ label: string;
31
+ isActive?: boolean;
32
+ }[];
33
+ }
34
+ /** What a caller gets back: `createdBy` gone, the two nested shapes narrowed. */
35
+ export type PublicEvent<T extends PublicEventInput> = Omit<T, "createdBy" | "lotteryConfig" | "spinPrizes"> & {
36
+ lotteryConfig?: ClientLotteryConfig;
37
+ spinPrizes?: ClientSpinPrize[];
38
+ };
39
+ /**
40
+ * Project one stored event into its public shape.
41
+ *
42
+ * The return TYPE is narrowed too, not just the runtime value โ€” so a caller
43
+ * that goes looking for `slot.weight` on the result gets a compile error rather
44
+ * than `undefined` at runtime, and the next projection bug is caught by tsc.
45
+ */
46
+ export declare function toPublicEvent<T extends PublicEventInput>(event: T): PublicEvent<T>;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Events adapters โ€” the public projection for an event document.
3
+ *
4
+ * ๐Ÿ›‘ An event is published by two separate routes (appkit's own `eventIdGET`
5
+ * and the consumer's `/api/events` list), and both previously built their
6
+ * payload by SPREADING the stored document and deleting `createdBy`. A
7
+ * deny-list keyed on a handful of remembered field names publishes everything
8
+ * nobody thought to delete โ€” see ยง Public Data Projections and Root Cause #70.
9
+ *
10
+ * Two nested structures on `EventDocument` carry fields that decide outcomes
11
+ * and must never leave the server:
12
+ *
13
+ * - `lotteryConfig.slots[]` โ€” per-slot `price` and `weight` (the weighting IS
14
+ * the odds), plus the internal `bookedByUserId`.
15
+ * - `spinPrizes[]` โ€” per-prize `weight` (again, the odds) and
16
+ * `couponId` (an internal coupon document id).
17
+ *
18
+ * `SpinWheelView` reads `id`, `label` and `isActive` and nothing else, so
19
+ * dropping the other two costs the client nothing. The prize a spin actually
20
+ * wins is resolved server-side in `assignSpinPrize` and returned as a coupon
21
+ * CODE on the entry, never as the coupon's document id.
22
+ *
23
+ * Both routes call `toPublicEvent` so the two can no longer disagree
24
+ * (Root Cause #75 โ€” a rule reachable by copy-paste gets copied, and the copy
25
+ * never learns about the branch added later).
26
+ */
27
+ import { toClientLotteryConfig } from "../lottery/adapters";
28
+ /** Allow-list, not a spread: `weight` and `couponId` are deliberately absent. */
29
+ export function toClientSpinPrize(prize) {
30
+ return {
31
+ id: prize.id,
32
+ label: prize.label,
33
+ isActive: prize.isActive !== false,
34
+ };
35
+ }
36
+ /**
37
+ * Project one stored event into its public shape.
38
+ *
39
+ * The return TYPE is narrowed too, not just the runtime value โ€” so a caller
40
+ * that goes looking for `slot.weight` on the result gets a compile error rather
41
+ * than `undefined` at runtime, and the next projection bug is caught by tsc.
42
+ */
43
+ export function toPublicEvent(event) {
44
+ const { createdBy: _createdBy, lotteryConfig, spinPrizes, ...rest } = event;
45
+ const out = rest;
46
+ if (lotteryConfig)
47
+ out.lotteryConfig = toClientLotteryConfig(lotteryConfig);
48
+ if (Array.isArray(spinPrizes))
49
+ out.spinPrizes = spinPrizes.map(toClientSpinPrize);
50
+ return out;
51
+ }
@@ -3,7 +3,13 @@
3
3
  * before any slot/config data leaves the server.
4
4
  */
5
5
  import type { LotteryConfig, LotterySlot, ClientLotteryConfig, ClientLotterySlot } from "../../../../features/lottery/types";
6
- /** Strip price and weight from a single slot for client consumption. */
7
- export declare function toClientLotterySlot(slot: LotterySlot): ClientLotterySlot;
6
+ /**
7
+ * Strip price and weight from a single slot for client consumption.
8
+ *
9
+ * Takes the loose shape rather than `LotterySlot` so it can be applied to a
10
+ * value already TYPED as projected but not actually projected โ€” which is the
11
+ * state `EventItem.lotteryConfig` is in when it comes off the repository.
12
+ */
13
+ export declare function toClientLotterySlot(slot: LotterySlot | ClientLotterySlot): ClientLotterySlot;
8
14
  /** Strip price/weight from the entire config for client consumption. */
9
- export declare function toClientLotteryConfig(config: LotteryConfig): ClientLotteryConfig;
15
+ export declare function toClientLotteryConfig(config: LotteryConfig | ClientLotteryConfig): ClientLotteryConfig;
@@ -1,4 +1,10 @@
1
- /** Strip price and weight from a single slot for client consumption. */
1
+ /**
2
+ * Strip price and weight from a single slot for client consumption.
3
+ *
4
+ * Takes the loose shape rather than `LotterySlot` so it can be applied to a
5
+ * value already TYPED as projected but not actually projected โ€” which is the
6
+ * state `EventItem.lotteryConfig` is in when it comes off the repository.
7
+ */
2
8
  export function toClientLotterySlot(slot) {
3
9
  // Allow-list, not a spread: every field named here is a deliberate public
4
10
  // exposure with a known reader (ยง Public Data Projections). `image` is read
@@ -18,6 +18,7 @@ import { NextResponse } from "next/server.js";
18
18
  import { getProviders } from "../../../../contracts";
19
19
  import { EVENT_FIELDS } from "../../schemas";
20
20
  import { normalizeError } from "../../../../errors/normalize";
21
+ import { toPublicEvent } from "../../../../_internal/server/features/events/adapters";
21
22
  // --- GET /api/events/[id] -----------------------------------------------------
22
23
  export async function GET(_request, context) {
23
24
  try {
@@ -33,8 +34,12 @@ export async function GET(_request, context) {
33
34
  event.status === EVENT_FIELDS.STATUS_VALUES.PAUSED) {
34
35
  return NextResponse.json({ success: false, error: "Event not found" }, { status: 404 });
35
36
  }
36
- // Strip internal field
37
- const { createdBy: _createdBy, ...publicEvent } = event;
37
+ // ๐Ÿ›‘ Do not hand-roll this projection here. `toPublicEvent` drops
38
+ // `createdBy` AND allow-lists the two nested structures that decide
39
+ // outcomes โ€” `lotteryConfig.slots[]` (price / weight / bookedByUserId) and
40
+ // `spinPrizes[]` (weight / couponId). Both were published to anonymous
41
+ // callers by the deny-list spread that used to live here.
42
+ const publicEvent = toPublicEvent(event);
38
43
  const entriesRepo = db.getRepository("eventEntries");
39
44
  // For poll events with resultsVisibility='always': compute live vote tallies
40
45
  let pollResults = [];
package/dist/server.d.ts CHANGED
@@ -566,6 +566,8 @@ export { renderLiveItemOg, renderLiveItemOgImage, renderLiveItemOgFromDoc, type
566
566
  export { getLotteryEventCached, listLotteryEvents, getLotteryEntriesForAdmin, getLotteryEntriesForUser } from "./_internal/server/features/lottery/data";
567
567
  export type { LotteryEventClient } from "./_internal/server/features/lottery/data";
568
568
  export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/features/lottery/adapters";
569
+ export { toPublicEvent, toClientSpinPrize } from "./_internal/server/features/events/adapters";
570
+ export type { ClientSpinPrize } from "./_internal/server/features/events/adapters";
569
571
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
570
572
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
571
573
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
package/dist/server.js CHANGED
@@ -1464,6 +1464,7 @@ export { renderLiveItemOg, renderLiveItemOgImage, renderLiveItemOgFromDoc } from
1464
1464
  // โ”€โ”€ Lottery feature โ€” server data + adapters + actions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
1465
1465
  export { getLotteryEventCached, listLotteryEvents, getLotteryEntriesForAdmin, getLotteryEntriesForUser } from "./_internal/server/features/lottery/data";
1466
1466
  export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/features/lottery/adapters";
1467
+ export { toPublicEvent, toClientSpinPrize } from "./_internal/server/features/events/adapters";
1467
1468
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
1468
1469
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
1469
1470
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mohasinac/appkit",
3
- "version": "4.41.4",
3
+ "version": "4.41.6",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"