@bash-app/bash-common 30.262.0 → 30.265.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 (41) hide show
  1. package/dist/__tests__/requestAuthUser.test.d.ts +2 -0
  2. package/dist/__tests__/requestAuthUser.test.d.ts.map +1 -0
  3. package/dist/__tests__/requestAuthUser.test.js +84 -0
  4. package/dist/__tests__/requestAuthUser.test.js.map +1 -0
  5. package/dist/__tests__/serviceCheckoutPaymentMethods.test.d.ts +2 -0
  6. package/dist/__tests__/serviceCheckoutPaymentMethods.test.d.ts.map +1 -0
  7. package/dist/__tests__/serviceCheckoutPaymentMethods.test.js +41 -0
  8. package/dist/__tests__/serviceCheckoutPaymentMethods.test.js.map +1 -0
  9. package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.d.ts +2 -0
  10. package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.d.ts.map +1 -0
  11. package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.js +30 -0
  12. package/dist/__tests__/ticketBnplPaymentMethods.bankTransfer.test.js.map +1 -0
  13. package/dist/definitions.d.ts +2 -0
  14. package/dist/definitions.d.ts.map +1 -1
  15. package/dist/definitions.js +1 -0
  16. package/dist/definitions.js.map +1 -1
  17. package/dist/extendedSchemas.d.ts +3 -0
  18. package/dist/extendedSchemas.d.ts.map +1 -1
  19. package/dist/extendedSchemas.js.map +1 -1
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +1 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/requestAuthUser.d.ts +36 -0
  25. package/dist/requestAuthUser.d.ts.map +1 -0
  26. package/dist/requestAuthUser.js +74 -0
  27. package/dist/requestAuthUser.js.map +1 -0
  28. package/dist/ticketBnplPaymentMethods.d.ts +30 -1
  29. package/dist/ticketBnplPaymentMethods.d.ts.map +1 -1
  30. package/dist/ticketBnplPaymentMethods.js +41 -0
  31. package/dist/ticketBnplPaymentMethods.js.map +1 -1
  32. package/package.json +1 -1
  33. package/prisma/schema.prisma +22 -8
  34. package/src/__tests__/requestAuthUser.test.ts +120 -0
  35. package/src/__tests__/serviceCheckoutPaymentMethods.test.ts +59 -0
  36. package/src/__tests__/ticketBnplPaymentMethods.bankTransfer.test.ts +42 -0
  37. package/src/definitions.ts +3 -0
  38. package/src/extendedSchemas.ts +3 -0
  39. package/src/index.ts +1 -0
  40. package/src/requestAuthUser.ts +105 -0
  41. package/src/ticketBnplPaymentMethods.ts +99 -1
@@ -0,0 +1,120 @@
1
+ import { UserRole } from "@prisma/client";
2
+ import {
3
+ parseRequestAuthFromJwtPayload,
4
+ parseUserRolesFromJwtClaim,
5
+ userHasSuperUserAccess,
6
+ userHasAdminAccess,
7
+ } from "../requestAuthUser.js";
8
+
9
+ describe("parseUserRolesFromJwtClaim", () => {
10
+ it("returns undefined when claim is absent", () => {
11
+ expect(parseUserRolesFromJwtClaim(undefined)).toBeUndefined();
12
+ });
13
+
14
+ it("defaults empty array to User", () => {
15
+ expect(parseUserRolesFromJwtClaim([])).toEqual([UserRole.User]);
16
+ });
17
+
18
+ it("accepts valid enum values", () => {
19
+ expect(parseUserRolesFromJwtClaim([UserRole.User, UserRole.SuperUser])).toEqual([
20
+ UserRole.User,
21
+ UserRole.SuperUser,
22
+ ]);
23
+ });
24
+
25
+ it("rejects non-array and unknown role strings", () => {
26
+ expect(parseUserRolesFromJwtClaim("User")).toBeNull();
27
+ expect(parseUserRolesFromJwtClaim(["NotARealRole"])).toBeNull();
28
+ });
29
+ });
30
+
31
+ describe("parseRequestAuthFromJwtPayload", () => {
32
+ it("parses session-style payload with roles", () => {
33
+ expect(
34
+ parseRequestAuthFromJwtPayload({
35
+ id: "u-1",
36
+ email: "a@test.com",
37
+ roles: [UserRole.SuperUser],
38
+ isSuperUser: false,
39
+ givenName: "Ada",
40
+ }),
41
+ ).toEqual({
42
+ id: "u-1",
43
+ email: "a@test.com",
44
+ roles: [UserRole.SuperUser],
45
+ isSuperUser: undefined,
46
+ username: undefined,
47
+ givenName: "Ada",
48
+ familyName: undefined,
49
+ });
50
+ });
51
+
52
+ it("returns null when roles claim is absent", () => {
53
+ expect(
54
+ parseRequestAuthFromJwtPayload({
55
+ id: "u-legacy",
56
+ email: "legacy@test.com",
57
+ }),
58
+ ).toBeNull();
59
+ });
60
+
61
+ it("returns null when identity claims are missing or roles are invalid", () => {
62
+ expect(parseRequestAuthFromJwtPayload(null)).toBeNull();
63
+ expect(parseRequestAuthFromJwtPayload({ email: "a@test.com" })).toBeNull();
64
+ expect(
65
+ parseRequestAuthFromJwtPayload({
66
+ id: "u-1",
67
+ email: "a@test.com",
68
+ roles: ["BadRole"],
69
+ }),
70
+ ).toBeNull();
71
+ });
72
+ });
73
+
74
+ describe("userHasSuperUserAccess", () => {
75
+ it("allows SuperUser role or isSuperUser flag", () => {
76
+ expect(
77
+ userHasSuperUserAccess({
78
+ roles: [UserRole.User, UserRole.SuperUser],
79
+ }),
80
+ ).toBe(true);
81
+ expect(
82
+ userHasSuperUserAccess({
83
+ roles: [UserRole.User],
84
+ isSuperUser: true,
85
+ }),
86
+ ).toBe(true);
87
+ expect(
88
+ userHasSuperUserAccess({
89
+ roles: [UserRole.User],
90
+ isSuperUser: false,
91
+ }),
92
+ ).toBe(false);
93
+ });
94
+ });
95
+
96
+ describe("userHasAdminAccess", () => {
97
+ it("allows Admin role", () => {
98
+ expect(
99
+ userHasAdminAccess({ roles: [UserRole.Admin] }),
100
+ ).toBe(true);
101
+ });
102
+
103
+ it("allows SuperUser role", () => {
104
+ expect(
105
+ userHasAdminAccess({ roles: [UserRole.SuperUser] }),
106
+ ).toBe(true);
107
+ });
108
+
109
+ it("allows isSuperUser flag regardless of role", () => {
110
+ expect(
111
+ userHasAdminAccess({ roles: [UserRole.User], isSuperUser: true }),
112
+ ).toBe(true);
113
+ });
114
+
115
+ it("rejects plain User role", () => {
116
+ expect(
117
+ userHasAdminAccess({ roles: [UserRole.User], isSuperUser: false }),
118
+ ).toBe(false);
119
+ });
120
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+
3
+ import {
4
+ resolveServiceCheckoutPaymentMethodTypes,
5
+ serviceCheckoutOffersAchDebit,
6
+ serviceCheckoutOffersBankTransfer,
7
+ } from "../ticketBnplPaymentMethods.js";
8
+
9
+ describe("resolveServiceCheckoutPaymentMethodTypes", () => {
10
+ it("includes customer_balance when bank transfer enabled", () => {
11
+ expect(
12
+ resolveServiceCheckoutPaymentMethodTypes({
13
+ bankTransferEnabled: true,
14
+ totalChargedCents: 250_000,
15
+ currency: "usd",
16
+ })
17
+ ).toContain("customer_balance");
18
+ });
19
+
20
+ it("includes us_bank_account when ACH enabled", () => {
21
+ expect(
22
+ resolveServiceCheckoutPaymentMethodTypes({
23
+ achDebitEnabled: true,
24
+ totalChargedCents: 250_000,
25
+ currency: "usd",
26
+ })
27
+ ).toContain("us_bank_account");
28
+ });
29
+
30
+ it("excludes async methods when globally disabled", () => {
31
+ expect(
32
+ resolveServiceCheckoutPaymentMethodTypes({
33
+ bankTransferEnabled: true,
34
+ achDebitEnabled: true,
35
+ totalChargedCents: 250_000,
36
+ currency: "usd",
37
+ bankTransferGloballyEnabled: false,
38
+ achDebitGloballyEnabled: false,
39
+ })
40
+ ).toEqual(["card", "link"]);
41
+ });
42
+
43
+ it("serviceCheckoutOffers* reflects resolver", () => {
44
+ expect(
45
+ serviceCheckoutOffersBankTransfer({
46
+ bankTransferEnabled: true,
47
+ totalChargedCents: 10_000,
48
+ currency: "usd",
49
+ })
50
+ ).toBe(true);
51
+ expect(
52
+ serviceCheckoutOffersAchDebit({
53
+ achDebitEnabled: true,
54
+ totalChargedCents: 10_000,
55
+ currency: "usd",
56
+ })
57
+ ).toBe(true);
58
+ });
59
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "@jest/globals";
2
+
3
+ import {
4
+ resolveTicketCheckoutPaymentMethodTypes,
5
+ ticketCheckoutOffersBankTransfer,
6
+ } from "../ticketBnplPaymentMethods.js";
7
+
8
+ describe("resolveTicketCheckoutPaymentMethodTypes bank transfer", () => {
9
+ it("includes customer_balance when host toggle and USD order", () => {
10
+ expect(
11
+ resolveTicketCheckoutPaymentMethodTypes({
12
+ installmentsEnabled: false,
13
+ bankTransferEnabled: true,
14
+ totalChargedCents: 50_000,
15
+ currency: "usd",
16
+ })
17
+ ).toContain("customer_balance");
18
+ });
19
+
20
+ it("excludes customer_balance when globally disabled", () => {
21
+ expect(
22
+ resolveTicketCheckoutPaymentMethodTypes({
23
+ installmentsEnabled: false,
24
+ bankTransferEnabled: true,
25
+ totalChargedCents: 50_000,
26
+ currency: "usd",
27
+ bankTransferGloballyEnabled: false,
28
+ })
29
+ ).not.toContain("customer_balance");
30
+ });
31
+
32
+ it("ticketCheckoutOffersBankTransfer reflects resolver", () => {
33
+ expect(
34
+ ticketCheckoutOffersBankTransfer({
35
+ installmentsEnabled: false,
36
+ bankTransferEnabled: true,
37
+ totalChargedCents: 10_000,
38
+ currency: "usd",
39
+ })
40
+ ).toBe(true);
41
+ });
42
+ });
@@ -473,6 +473,8 @@ export const HTTP_CODE_FORBIDDEN = 403 as const;
473
473
  export const HTTP_CODE_NOT_FOUND = 404 as const;
474
474
  export const ERR_UNAUTHORIZED_REQUEST =
475
475
  "Unauthorized to perform requested action. Have you logged in?" as const;
476
+ export const ERR_SESSION_EXPIRED =
477
+ "Your session has expired — please refresh or log in again." as const;
476
478
 
477
479
  export const DEFAULT_PRISMA_TTL_SECONDS = 60 as const;
478
480
  export const PRISMA_MEDIA_TTL_SECONDS = 60 * 5; // 5 hours
@@ -1022,6 +1024,7 @@ export type StripeLinkingStatus = {
1022
1024
  cardPaymentsActive?: boolean;
1023
1025
  klarnaPaymentsActive?: boolean;
1024
1026
  cashappPaymentsActive?: boolean;
1027
+ usBankTransferPaymentsActive?: boolean;
1025
1028
  };
1026
1029
 
1027
1030
  export type StripeBusinessDataPublic = {
@@ -1217,6 +1217,9 @@ export interface InvitationExtraData extends Invitation {
1217
1217
  organizerTitle?: string;
1218
1218
  isOwnershipTransfer?: boolean;
1219
1219
  includeQRCode?: boolean; // Controls whether QR code should be included in the invitation email
1220
+ sendEmail?: boolean; // When false, skip email notification (default true)
1221
+ existingTicketId?: string; // Link invitation to an existing ticket instead of creating a new one
1222
+ revoke?: boolean; // Host action: set revokedDate
1220
1223
  }
1221
1224
 
1222
1225
  export interface AssociatedBashExt extends AssociatedBash {
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./definitions.js";
2
+ export * from "./requestAuthUser.js";
2
3
  export * from "./sms/smsTemplates.js";
3
4
  export * from "./legalTemplates.js";
4
5
  export * from "./utmAttribution.js";
@@ -0,0 +1,105 @@
1
+ import { UserRole } from "@prisma/client";
2
+
3
+ /**
4
+ * Claims available on `req.auth` after JWT verification.
5
+ *
6
+ * This is intentionally smaller than Prisma `User`: route handlers should load
7
+ * full profile data from the DB when they need more than identity + authorization.
8
+ */
9
+ export type RequestAuthUser = {
10
+ id: string;
11
+ email: string;
12
+ roles: UserRole[];
13
+ isSuperUser?: boolean;
14
+ username?: string;
15
+ givenName?: string;
16
+ familyName?: string;
17
+ };
18
+
19
+ const USER_ROLE_VALUES = new Set<string>(Object.values(UserRole));
20
+
21
+ function isRecord(value: unknown): value is Record<string, unknown> {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
24
+
25
+ function optionalString(value: unknown): string | undefined {
26
+ return typeof value === "string" && value.length > 0 ? value : undefined;
27
+ }
28
+
29
+ /**
30
+ * Parse and validate a `roles` JWT claim.
31
+ *
32
+ * - `undefined` → claim absent (invalid for newly issued tokens)
33
+ * - `null` → present but malformed
34
+ */
35
+ export function parseUserRolesFromJwtClaim(value: unknown): UserRole[] | null | undefined {
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ if (!Array.isArray(value)) {
40
+ return null;
41
+ }
42
+ if (value.length === 0) {
43
+ return [UserRole.User];
44
+ }
45
+ const roles: UserRole[] = [];
46
+ for (const item of value) {
47
+ if (typeof item !== "string" || !USER_ROLE_VALUES.has(item)) {
48
+ return null;
49
+ }
50
+ roles.push(item as UserRole);
51
+ }
52
+ return roles;
53
+ }
54
+
55
+ /**
56
+ * Normalize a verified JWT payload into {@link RequestAuthUser}.
57
+ *
58
+ * Returns `null` when required identity claims are missing, when `roles` is
59
+ * absent, or when `roles` is present but invalid. All newly issued session and
60
+ * minimal JWTs must include a `roles` array (see signMinimalJwt / login flows).
61
+ */
62
+ export function parseRequestAuthFromJwtPayload(decoded: unknown): RequestAuthUser | null {
63
+ if (!isRecord(decoded)) {
64
+ return null;
65
+ }
66
+
67
+ const id = optionalString(decoded.id);
68
+ const email = optionalString(decoded.email);
69
+ if (!id || !email) {
70
+ return null;
71
+ }
72
+
73
+ const parsedRoles = parseUserRolesFromJwtClaim(decoded.roles);
74
+ if (parsedRoles === null || parsedRoles === undefined) {
75
+ return null;
76
+ }
77
+
78
+ return {
79
+ id,
80
+ email,
81
+ roles: parsedRoles,
82
+ isSuperUser: decoded.isSuperUser === true ? true : undefined,
83
+ username: optionalString(decoded.username),
84
+ givenName: optionalString(decoded.givenName),
85
+ familyName: optionalString(decoded.familyName),
86
+ };
87
+ }
88
+
89
+ /** SuperUser role or legacy `isSuperUser` flag on the session JWT. */
90
+ export function userHasSuperUserAccess(
91
+ auth: Pick<RequestAuthUser, "roles" | "isSuperUser">,
92
+ ): boolean {
93
+ return auth.isSuperUser === true || auth.roles.includes(UserRole.SuperUser);
94
+ }
95
+
96
+ /** Admin or SuperUser role, or legacy `isSuperUser` flag. */
97
+ export function userHasAdminAccess(
98
+ auth: Pick<RequestAuthUser, "roles" | "isSuperUser">,
99
+ ): boolean {
100
+ return (
101
+ auth.isSuperUser === true ||
102
+ auth.roles.includes(UserRole.SuperUser) ||
103
+ auth.roles.includes(UserRole.Admin)
104
+ );
105
+ }
@@ -10,7 +10,16 @@ export type TicketCheckoutPaymentMethodType =
10
10
  | "link"
11
11
  | "klarna"
12
12
  | "afterpay_clearpay"
13
- | "cashapp";
13
+ | "cashapp"
14
+ | "customer_balance"
15
+ | "us_bank_account";
16
+
17
+ /** Service booking Checkout — card, link, and optional async bank methods. */
18
+ export type ServiceCheckoutPaymentMethodType =
19
+ | "card"
20
+ | "link"
21
+ | "customer_balance"
22
+ | "us_bank_account";
14
23
 
15
24
  /** USD floors/ceilings aligned with typical Stripe BNPL eligibility (conservative). */
16
25
  export const TICKET_BNPL_MIN_CENTS_USD = 3_500; // $35
@@ -21,6 +30,8 @@ export interface TicketBnplPaymentMethodInput {
21
30
  installmentsEnabled: boolean;
22
31
  /** Per-event host toggle (`BashEvent.cashAppEnabled`). */
23
32
  cashAppEnabled?: boolean;
33
+ /** Per-event host toggle (`BashEvent.bankTransferEnabled`). */
34
+ bankTransferEnabled?: boolean;
24
35
  /** Buyer total charged at checkout (tickets + guest-visible fees), in cents. */
25
36
  totalChargedCents: number;
26
37
  /** ISO currency lowercase, e.g. "usd". */
@@ -29,6 +40,8 @@ export interface TicketBnplPaymentMethodInput {
29
40
  bnplGloballyEnabled?: boolean;
30
41
  /** Platform kill switch (API env `TICKET_CASHAPP_ENABLED`). Default true in tests. */
31
42
  cashAppGloballyEnabled?: boolean;
43
+ /** Platform kill switch (API env `TICKET_BANK_TRANSFER_ENABLED`). Default true in tests. */
44
+ bankTransferGloballyEnabled?: boolean;
32
45
  }
33
46
 
34
47
  /**
@@ -43,6 +56,7 @@ export function resolveTicketCheckoutPaymentMethodTypes(
43
56
 
44
57
  const globallyBnplOn = input.bnplGloballyEnabled !== false;
45
58
  const globallyCashAppOn = input.cashAppGloballyEnabled !== false;
59
+ const globallyBankTransferOn = input.bankTransferGloballyEnabled !== false;
46
60
  const isUsd = input.currency.toLowerCase() === "usd";
47
61
  const cents = input.totalChargedCents;
48
62
 
@@ -61,6 +75,15 @@ export function resolveTicketCheckoutPaymentMethodTypes(
61
75
  methods.add("cashapp");
62
76
  }
63
77
 
78
+ if (
79
+ globallyBankTransferOn &&
80
+ input.bankTransferEnabled &&
81
+ isUsd &&
82
+ cents > 0
83
+ ) {
84
+ methods.add("customer_balance");
85
+ }
86
+
64
87
  return [...methods];
65
88
  }
66
89
 
@@ -77,3 +100,78 @@ export function ticketCheckoutOffersCashApp(
77
100
  const methods = resolveTicketCheckoutPaymentMethodTypes(input);
78
101
  return methods.includes("cashapp");
79
102
  }
103
+
104
+ export function ticketCheckoutOffersBankTransfer(
105
+ input: TicketBnplPaymentMethodInput
106
+ ): boolean {
107
+ const methods = resolveTicketCheckoutPaymentMethodTypes(input);
108
+ return methods.includes("customer_balance");
109
+ }
110
+
111
+ export interface ServiceCheckoutPaymentMethodInput {
112
+ /** Per-service host toggle (`Service.bankTransferEnabled`). */
113
+ bankTransferEnabled?: boolean;
114
+ /** Per-service host toggle (`Service.achDebitEnabled`). */
115
+ achDebitEnabled?: boolean;
116
+ /** Buyer total charged at checkout, in cents. */
117
+ totalChargedCents: number;
118
+ /** ISO currency lowercase, e.g. "usd". */
119
+ currency: string;
120
+ /** Platform kill switch (API env `SERVICE_BANK_TRANSFER_ENABLED`). Default true in tests. */
121
+ bankTransferGloballyEnabled?: boolean;
122
+ /** Platform kill switch (API env `SERVICE_ACH_DEBIT_ENABLED`). Default true in tests. */
123
+ achDebitGloballyEnabled?: boolean;
124
+ }
125
+
126
+ /**
127
+ * Resolve `payment_method_types` for Stripe Checkout service booking sessions.
128
+ * Bank transfer and ACH are async — fulfillment waits for `checkout.session.async_payment_succeeded`.
129
+ */
130
+ export function resolveServiceCheckoutPaymentMethodTypes(
131
+ input: ServiceCheckoutPaymentMethodInput
132
+ ): ServiceCheckoutPaymentMethodType[] {
133
+ const methods = new Set<ServiceCheckoutPaymentMethodType>(["card", "link"]);
134
+ const globallyBankTransferOn = input.bankTransferGloballyEnabled !== false;
135
+ const globallyAchOn = input.achDebitGloballyEnabled !== false;
136
+ const isUsd = input.currency.toLowerCase() === "usd";
137
+ const cents = input.totalChargedCents;
138
+
139
+ if (
140
+ globallyBankTransferOn &&
141
+ input.bankTransferEnabled &&
142
+ isUsd &&
143
+ cents > 0
144
+ ) {
145
+ methods.add("customer_balance");
146
+ }
147
+
148
+ if (globallyAchOn && input.achDebitEnabled && isUsd && cents > 0) {
149
+ methods.add("us_bank_account");
150
+ }
151
+
152
+ return [...methods];
153
+ }
154
+
155
+ export function serviceCheckoutOffersBankTransfer(
156
+ input: ServiceCheckoutPaymentMethodInput
157
+ ): boolean {
158
+ return resolveServiceCheckoutPaymentMethodTypes(input).includes(
159
+ "customer_balance"
160
+ );
161
+ }
162
+
163
+ export function serviceCheckoutOffersAchDebit(
164
+ input: ServiceCheckoutPaymentMethodInput
165
+ ): boolean {
166
+ return resolveServiceCheckoutPaymentMethodTypes(input).includes(
167
+ "us_bank_account"
168
+ );
169
+ }
170
+
171
+ export function checkoutUsesAsyncBankPayment(
172
+ methods: ReadonlyArray<
173
+ TicketCheckoutPaymentMethodType | ServiceCheckoutPaymentMethodType
174
+ >
175
+ ): boolean {
176
+ return methods.includes("customer_balance") || methods.includes("us_bank_account");
177
+ }