@moneypot/hub 1.20.5 → 1.21.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Dashboard</title>
7
- <script type="module" crossorigin src="/dashboard/assets/index-0G7cFk_0.js"></script>
7
+ <script type="module" crossorigin src="/dashboard/assets/index-n_xD3-qc.js"></script>
8
8
  <link rel="stylesheet" crossorigin href="/dashboard/assets/index-Bl5lLFHa.css">
9
9
  </head>
10
10
  <body>
@@ -1,7 +1,7 @@
1
1
  import { DbPlayerBalance, DbHouseBankroll } from "./db/types.js";
2
2
  import { QueryExecutor } from "./db/util.js";
3
3
  export type AuditLogType = "player-balance" | "house-bankroll" | "both";
4
- export type HubAuditAction = "hub:init" | "hub:deposit" | "hub:take_request:pending" | "hub:take_request:refund" | "hub:outcome_bet";
4
+ export type HubAuditAction = "hub:init" | "hub:deposit" | "hub:take_request:pending" | "hub:take_request:refund" | "hub:outcome_bet" | "hub:bankroll:adjust";
5
5
  export type HubAuditRefType = "hub.deposit" | "hub.outcome_bet" | "hub.take_request";
6
6
  export type AuditAction = HubAuditAction | (string & {});
7
7
  export type AuditRefType = HubAuditRefType | (string & {});
@@ -20,7 +20,7 @@ declare global {
20
20
  export { HubGameConfigPlugin, type OutcomeBetConfigMap, type OutcomeBetConfig, } from "./plugins/hub-game-config-plugin.js";
21
21
  export { type CustomGameConfig, type CustomGameConfigMap, } from "./custom-game-config.js";
22
22
  export { validateRisk, type RiskPolicy, type RiskPolicyArgs, type RiskLimits, } from "./risk-policy.js";
23
- export { type DbLiability, type NewLiability, type LiabilityConfig, type LiabilityHandler, type LiabilityResolved, type DbLockedLiability, LIABILITY_RESOLVED, createLiabilityConfig, } from "./liability/public/index.js";
23
+ export { type DbLiability, type NewLiability, type LiabilityConfig, type LiabilityHandler, type LiabilityResolved, type DbLockedLiability, type HandleDeadlineArgs, type HandleDeadlineFn, LIABILITY_RESOLVED, createLiabilityConfig, } from "./liability/public/index.js";
24
24
  export type PluginContext = Grafast.Context;
25
25
  export * from "./db/types.js";
26
26
  export { defaultPlugins, type PluginIdentity, type UserSessionContext, type HubPlugin, } from "./server/graphile.config.js";
@@ -95,6 +95,7 @@ export async function processOneDeadlinedLiability(pgClient, { liabilityConfig,
95
95
  lockedLiability: dbLockedLiability,
96
96
  lockedPlayerBalance: dbLockedPlayerBalance,
97
97
  lockedHouseBankroll: dbLockedHouseBankroll,
98
+ liabilityConfig: liabilityConfig,
98
99
  });
99
100
  const { resolved_at } = await pgClient
100
101
  .query(`SELECT resolved_at FROM hub.liability WHERE id = $1`, [dbLockedLiability.id])
@@ -13,6 +13,7 @@ export type HandleDeadlineArgs<RefType extends string> = {
13
13
  lockedLiability: DbLockedLiability<RefType>;
14
14
  lockedPlayerBalance: Pick<DbLockedPlayerBalance, "id" | "amount">;
15
15
  lockedHouseBankroll: Pick<DbLockedHouseBankrollWithAvailable, "id" | "grossAmount" | "available">;
16
+ liabilityConfig: LiabilityConfig<RefType>;
16
17
  };
17
18
  export type HandleDeadlineFn<RefType extends string> = (args: HandleDeadlineArgs<RefType>) => Promise<LiabilityResolved>;
18
19
  export type LiabilityRefTypeConfig<RefType extends string> = {
@@ -1,2 +1,2 @@
1
1
  export type { DbLiability, NewLiability } from "../types.js";
2
- export { type LiabilityConfig, type LiabilityRefTypeConfig as LiabilityHandler, type LiabilityResolved, type DbLockedLiability, LIABILITY_RESOLVED, createLiabilityConfig, } from "./config.js";
2
+ export { type LiabilityConfig, type LiabilityRefTypeConfig as LiabilityHandler, type LiabilityResolved, type DbLockedLiability, type HandleDeadlineArgs, type HandleDeadlineFn, LIABILITY_RESOLVED, createLiabilityConfig, } from "./config.js";
@@ -0,0 +1,49 @@
1
+
2
+ drop policy select_chat_message on hub.chat_message;
3
+ create policy select_chat_message on hub.chat_message for select using (
4
+ hub_hidden.is_operator()
5
+
6
+ or (
7
+ hidden_at is null
8
+ and experience_id = hub_hidden.current_experience_id()
9
+ and casino_id = hub_hidden.current_casino_id()
10
+ )
11
+
12
+ or (
13
+ hub_hidden.is_experience_owner()
14
+ and experience_id = hub_hidden.current_experience_id()
15
+ and casino_id = hub_hidden.current_casino_id()
16
+ )
17
+ );
18
+
19
+ drop policy select_chat_mute on hub.chat_mute;
20
+ create policy select_chat_mute on hub.chat_mute for select using (
21
+ hub_hidden.is_operator()
22
+
23
+ or hub_hidden.current_user_id() = hub.chat_mute.user_id
24
+
25
+ or (
26
+ hub_hidden.is_experience_owner()
27
+ and hub.chat_mute.experience_id = hub_hidden.current_experience_id()
28
+ and hub.chat_mute.casino_id = hub_hidden.current_casino_id()
29
+ )
30
+
31
+ or exists (
32
+ select 1
33
+ from hub.chat_mod
34
+ where user_id = hub_hidden.current_user_id()
35
+ and casino_id = hub.chat_mute.casino_id
36
+ and experience_id = hub.chat_mute.experience_id
37
+ )
38
+ );
39
+
40
+ drop policy select_chat_mod on hub.chat_mod;
41
+ create policy select_chat_mod on hub.chat_mod for select using (
42
+ hub_hidden.is_operator()
43
+
44
+ or (
45
+ hub_hidden.is_experience_owner()
46
+ and experience_id = hub_hidden.current_experience_id()
47
+ and casino_id = hub_hidden.current_casino_id()
48
+ )
49
+ );
@@ -2,7 +2,7 @@ import { GraphQLError } from "graphql";
2
2
  import { context, object, sideEffect } from "postgraphile/grafast";
3
3
  import { extendSchema, gql } from "postgraphile/utils";
4
4
  import z from "zod/v4";
5
- import { extractFirstZodErrorMessage } from "../../util.js";
5
+ import { extractFirstZodErrorMessage, uuidEqual } from "../../util.js";
6
6
  import { exactlyOneRow, maybeOneRow, withPgPoolTransaction, } from "../../db/index.js";
7
7
  const InputSchema = z.object({
8
8
  clientId: z.uuid("Invalid client ID"),
@@ -69,6 +69,10 @@ export const HubChatCreateSystemMessagePlugin = extendSchema((build) => {
69
69
  if (!dbExperience) {
70
70
  throw new GraphQLError("Experience not found");
71
71
  }
72
+ if (identity.kind === "user" &&
73
+ !uuidEqual(identity.session.experience_id, dbExperience.id)) {
74
+ throw new GraphQLError("Your session does not match the experienceId");
75
+ }
72
76
  return await withPgPoolTransaction(superuserPool, async (pgClient) => {
73
77
  const dbChatMessage = await pgClient
74
78
  .query({
@@ -85,9 +85,9 @@ export const HubChatModManagementPlugin = extendSchema((build) => {
85
85
  text: `
86
86
  SELECT id
87
87
  FROM hub.user
88
- WHERE id = $1 AND experience_id = $2
88
+ WHERE id = $1 AND casino_id = $2
89
89
  `,
90
- values: [input.userId, input.experienceId],
90
+ values: [input.userId, dbTargetExperience.casino_id],
91
91
  })
92
92
  .then(maybeOneRow);
93
93
  if (!dbTargetUser) {
@@ -167,9 +167,9 @@ export const HubChatModManagementPlugin = extendSchema((build) => {
167
167
  text: `
168
168
  SELECT id
169
169
  FROM hub.user
170
- WHERE id = $1 AND experience_id = $2
170
+ WHERE id = $1 AND casino_id = $2
171
171
  `,
172
- values: [input.userId, input.experienceId],
172
+ values: [input.userId, dbTargetExperience.casino_id],
173
173
  })
174
174
  .then(maybeOneRow);
175
175
  if (!dbTargetUser) {
@@ -56,8 +56,8 @@ export const HubAddCasinoPlugin = makeExtendSchemaPlugin((build) => {
56
56
  throw new GraphQLError("Unauthorized");
57
57
  }
58
58
  assert(is(input));
59
- logger.info(input, "Input");
60
59
  const { name, baseUrl, graphqlUrl, apiKey } = input;
60
+ logger.info({ name, baseUrl, graphqlUrl }, "Input");
61
61
  const graphqlClient = new GraphQLClient(graphqlUrl, {
62
62
  headers: {
63
63
  Authorization: `apikey:${apiKey}`,
@@ -0,0 +1 @@
1
+ export declare const HubAdjustBankrollPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,106 @@
1
+ import { GraphQLError } from "graphql";
2
+ import { context, object, sideEffect } from "postgraphile/grafast";
3
+ import { extendSchema, gql } from "postgraphile/utils";
4
+ import z from "zod/v4";
5
+ import { extractFirstZodErrorMessage } from "../util.js";
6
+ import { maybeOneRow, withPgPoolTransaction, } from "../db/index.js";
7
+ import { insertAuditLog } from "../audit-log.js";
8
+ const InputSchema = z.object({
9
+ bankrollId: z.uuid("Invalid bankroll ID"),
10
+ delta: z
11
+ .number()
12
+ .int("Delta must be a whole number of base units")
13
+ .refine((delta) => delta !== 0, "Delta must not be zero"),
14
+ reason: z
15
+ .string()
16
+ .trim()
17
+ .min(1, "Reason is required")
18
+ .max(200, "Max reason length: 200 chars"),
19
+ });
20
+ export const HubAdjustBankrollPlugin = extendSchema((build) => {
21
+ const bankrollTable = build.input.pgRegistry.pgResources.hub_bankroll;
22
+ return {
23
+ typeDefs: gql `
24
+ input HubAdjustBankrollInput {
25
+ bankrollId: UUID!
26
+ "Amount to add, in base units. Negative takes money out."
27
+ delta: Float!
28
+ reason: String!
29
+ }
30
+
31
+ type HubAdjustBankrollPayload {
32
+ hubBankroll: HubBankroll!
33
+ }
34
+
35
+ extend type Mutation {
36
+ hubAdjustBankroll(
37
+ input: HubAdjustBankrollInput!
38
+ ): HubAdjustBankrollPayload
39
+ }
40
+ `,
41
+ objects: {
42
+ Mutation: {
43
+ plans: {
44
+ hubAdjustBankroll(_, { $input }) {
45
+ const $identity = context().get("identity");
46
+ const $superuserPool = context().get("superuserPool");
47
+ const $bankrollId = sideEffect([$input, $identity, $superuserPool], async ([rawInput, identity, superuserPool]) => {
48
+ if (identity?.kind !== "operator") {
49
+ throw new GraphQLError("Unauthorized");
50
+ }
51
+ let input;
52
+ try {
53
+ input = InputSchema.parse(rawInput);
54
+ }
55
+ catch (e) {
56
+ if (e instanceof z.ZodError) {
57
+ throw new GraphQLError(extractFirstZodErrorMessage(e));
58
+ }
59
+ throw e;
60
+ }
61
+ return withPgPoolTransaction(superuserPool, async (pgClient) => {
62
+ const dbBankroll = await pgClient
63
+ .query({
64
+ text: `
65
+ SELECT id, amount, liability
66
+ FROM hub.bankroll
67
+ WHERE id = $1
68
+ FOR UPDATE
69
+ `,
70
+ values: [input.bankrollId],
71
+ })
72
+ .then(maybeOneRow);
73
+ if (!dbBankroll) {
74
+ throw new GraphQLError("Bankroll not found");
75
+ }
76
+ const newAmount = dbBankroll.amount + input.delta;
77
+ if (newAmount > Number.MAX_SAFE_INTEGER) {
78
+ throw new GraphQLError("Bankroll would be too large");
79
+ }
80
+ if (newAmount < dbBankroll.liability) {
81
+ throw new GraphQLError("Bankroll can't go below what open games may pay out");
82
+ }
83
+ await pgClient.query({
84
+ text: `UPDATE hub.bankroll SET amount = $2 WHERE id = $1`,
85
+ values: [dbBankroll.id, newAmount],
86
+ });
87
+ await insertAuditLog(pgClient, "house-bankroll", {
88
+ action: "hub:bankroll:adjust",
89
+ bankrollId: dbBankroll.id,
90
+ bankrollOld: dbBankroll.amount,
91
+ bankrollNew: newAmount,
92
+ bankrollDelta: input.delta,
93
+ metadata: { reason: input.reason },
94
+ });
95
+ return dbBankroll.id;
96
+ });
97
+ });
98
+ return object({
99
+ hubBankroll: bankrollTable.get({ id: $bankrollId }),
100
+ });
101
+ },
102
+ },
103
+ },
104
+ },
105
+ };
106
+ }, "HubAdjustBankrollPlugin");
@@ -29,9 +29,6 @@ export const AddCasinoSchema = z.strictObject({
29
29
  graphqlUrl: v.graphqlUrl(),
30
30
  apiKey: z.uuid(),
31
31
  });
32
- const UpdateBankrollSchema = z.strictObject({
33
- amount: z.number().int().min(0),
34
- });
35
32
  export const ValidateCasinoFieldsPlugin = wrapPlans(() => ({
36
33
  Mutation: {
37
34
  updateHubCasinoById: (plan, $source, fieldArgs) => {
@@ -44,10 +41,5 @@ export const ValidateCasinoFieldsPlugin = wrapPlans(() => ({
44
41
  sideEffect($input, (input) => validate(input, AddCasinoSchema));
45
42
  return plan();
46
43
  },
47
- updateHubBankrollById: (plan, $source, fieldArgs) => {
48
- const $patch = fieldArgs.getRaw(["input", "hubBankrollPatch"]);
49
- sideEffect($patch, (patch) => validate(patch, UpdateBankrollSchema));
50
- return plan();
51
- },
52
44
  },
53
45
  }));
@@ -5,8 +5,6 @@ import assert from "assert";
5
5
  import { logger } from "../logger.js";
6
6
  import * as config from "../config.js";
7
7
  import * as db from "../db/index.js";
8
- import * as pg from "pg";
9
- import { z } from "zod/v4";
10
8
  const activeCasinos = new Set();
11
9
  export async function startCasinoTransferProcessor({ casinoId, signal, pool, }) {
12
10
  if (activeCasinos.has(casinoId)) {
@@ -44,51 +42,9 @@ export function initializeTransferProcessors({ signal, pool, }) {
44
42
  logger.info(`Starting casino processor for "${casino.name}" at "${casino.graphql_url}"`);
45
43
  startCasinoTransferProcessor({ casinoId: casino.id, signal, pool });
46
44
  }
47
- await listenForNewCasinos({ signal, pool });
48
45
  }
49
46
  catch (e) {
50
47
  logger.error(e, `Error initializing transfer processors`);
51
48
  }
52
49
  })();
53
50
  }
54
- async function listenForNewCasinos({ signal, pool, }) {
55
- const pgClient = new pg.Client(config.SUPERUSER_DATABASE_URL);
56
- await pgClient.connect();
57
- const NewCasinoPayload = z.object({
58
- id: z.string(),
59
- });
60
- pgClient.on("notification", async (msg) => {
61
- logger.debug(msg, `[listenForNewCasinos] received notification`);
62
- switch (msg.channel) {
63
- case "hub:new_casino": {
64
- if (!msg.payload) {
65
- logger.error("hub:new_casino notification has no payload");
66
- return;
67
- }
68
- let json;
69
- try {
70
- json = JSON.parse(msg.payload);
71
- }
72
- catch (error) {
73
- logger.error(error, "Error parsing new casino notification");
74
- }
75
- const result = NewCasinoPayload.safeParse(json);
76
- if (!result.success) {
77
- logger.error(result.error, "Error parsing new casino notification");
78
- return;
79
- }
80
- startCasinoTransferProcessor({
81
- casinoId: result.data.id,
82
- signal,
83
- pool,
84
- });
85
- break;
86
- }
87
- }
88
- });
89
- signal.addEventListener("abort", () => {
90
- pgClient.removeAllListeners("notification");
91
- pgClient.end();
92
- });
93
- await pgClient.query("LISTEN new_casino");
94
- }
@@ -123,35 +123,59 @@ export function startPollingProcessor({ casinoId, signal, pool, }) {
123
123
  logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (3)`);
124
124
  break;
125
125
  }
126
- cursor = await processTransfersUntilEmpty({
127
- afterCursor: cursor,
128
- graphqlClient,
129
- casinoInfo: { ...casino, ...casinoSecret },
130
- signal,
131
- pool,
132
- });
126
+ let anyTaskFailed = false;
127
+ try {
128
+ cursor = await processTransfersUntilEmpty({
129
+ afterCursor: cursor,
130
+ graphqlClient,
131
+ casinoInfo: { ...casino, ...casinoSecret },
132
+ signal,
133
+ pool,
134
+ });
135
+ }
136
+ catch (e) {
137
+ anyTaskFailed = true;
138
+ logger.error(e, `transfer polling failed for casino ${casinoId}`);
139
+ }
133
140
  if (signal.aborted) {
134
141
  logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (4)`);
135
142
  break;
136
143
  }
137
- await processWithdrawals({
138
- abortSignal: signal,
139
- casinoId: casino.id,
140
- graphqlClient,
141
- pool,
142
- });
144
+ try {
145
+ await processWithdrawals({
146
+ abortSignal: signal,
147
+ casinoId: casino.id,
148
+ graphqlClient,
149
+ pool,
150
+ });
151
+ }
152
+ catch (e) {
153
+ anyTaskFailed = true;
154
+ logger.error(e, `withdrawal processing failed for casino ${casinoId}`);
155
+ }
143
156
  if (signal.aborted) {
144
157
  logger.info(`[startTransferProcessor] Aborted by graceful shutdown. (5)`);
145
158
  break;
146
159
  }
147
- await processTakeRequests({
148
- abortSignal: signal,
149
- controllerId: casinoSecret.controller_id,
150
- casinoId: casino.id,
151
- graphqlClient,
152
- pool,
153
- });
154
- processorState.backoffTime = MIN_BACKOFF_TIME;
160
+ try {
161
+ await processTakeRequests({
162
+ abortSignal: signal,
163
+ controllerId: casinoSecret.controller_id,
164
+ casinoId: casino.id,
165
+ graphqlClient,
166
+ pool,
167
+ });
168
+ }
169
+ catch (e) {
170
+ anyTaskFailed = true;
171
+ logger.error(e, `take request processing failed for casino ${casinoId}`);
172
+ }
173
+ if (anyTaskFailed) {
174
+ processorState.backoffTime = Math.min(processorState.backoffTime * 2, MAX_BACKOFF_TIME);
175
+ }
176
+ else {
177
+ processorState.backoffTime = MIN_BACKOFF_TIME;
178
+ }
155
179
  }
156
180
  catch (e) {
157
181
  processorState.backoffTime = Math.min(processorState.backoffTime * 2, MAX_BACKOFF_TIME);
@@ -143,10 +143,6 @@ export async function processTransfer({ casinoId, controllerId, transfer, graphq
143
143
  logger.debug(`I sent ${user.uname} ${transfer.amount} base units of ${currency.id}`);
144
144
  switch (transfer.status) {
145
145
  case TransferStatusKind.Canceled:
146
- await db.settleWithdrawal(pool, {
147
- withdrawalId: transfer.id,
148
- newStatus: "CANCELED",
149
- });
150
146
  return;
151
147
  case TransferStatusKind.Unclaimed:
152
148
  throw new Error(`Unexpected UNCLAIMED withdrawal.`);
@@ -90,7 +90,7 @@ export function startWebsocketProcessor({ casinoId, graphqlUrl, signal, controll
90
90
  return;
91
91
  }
92
92
  assert(mpTransfer.__typename === "ExperienceTransfer", `Expected experience transfer but got ${mpTransfer.__typename}`);
93
- processTransfer({
93
+ await processTransfer({
94
94
  casinoId,
95
95
  controllerId,
96
96
  transfer: mpTransfer,
@@ -15,6 +15,7 @@ import { HubUserBalanceByCurrencyPlugin } from "../plugins/hub-user-balance-by-c
15
15
  import { logger } from "../logger.js";
16
16
  import { ValidateCasinoFieldsPlugin } from "../plugins/validate-fields.js";
17
17
  import { HubAddCasinoPlugin } from "../plugins/hub-add-casino.js";
18
+ import { HubAdjustBankrollPlugin } from "../plugins/hub-adjust-bankroll.js";
18
19
  import { HubBalanceAlertPlugin } from "../plugins/hub-balance-alert.js";
19
20
  import { custom as customPgOmitArchivedPlugin } from "@graphile-contrib/pg-omit-archived";
20
21
  import { HubCurrentXPlugin } from "../plugins/hub-current-x.js";
@@ -44,6 +45,7 @@ export const requiredPlugins = [
44
45
  HubUserBalanceByCurrencyPlugin,
45
46
  HubAddCasinoPlugin,
46
47
  ValidateCasinoFieldsPlugin,
48
+ HubAdjustBankrollPlugin,
47
49
  HubOutcomeInputNonNullFieldsPlugin,
48
50
  HubBadHashChainErrorPlugin,
49
51
  HubCreateHashChainPlugin,
@@ -23,9 +23,14 @@ export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
23
23
  behavior: ["-orderBy"],
24
24
  },
25
25
  },
26
- "hub.bankroll.amount": {
26
+ "hub.user.client_id": {
27
27
  tags: {
28
- behavior: ["+attribute:update"],
28
+ behavior: ["-select", "-filterBy", "-orderBy"],
29
+ },
30
+ },
31
+ "hub.experience.client_id": {
32
+ tags: {
33
+ behavior: ["-select", "-filterBy", "-orderBy"],
29
34
  },
30
35
  },
31
36
  },
@@ -35,11 +40,6 @@ export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
35
40
  behavior: ["-resource:select"],
36
41
  },
37
42
  },
38
- "hub.bankroll": {
39
- tags: {
40
- behavior: ["+resource:update"],
41
- },
42
- },
43
43
  "hub.casino": {
44
44
  tags: {
45
45
  behavior: [