@moneypot/hub 0.0.1

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 (106) hide show
  1. package/README.md +108 -0
  2. package/dist/cli/add-casino.d.ts +2 -0
  3. package/dist/cli/add-casino.js +116 -0
  4. package/dist/dashboard/assets/index-BtrbrisP.js +360 -0
  5. package/dist/dashboard/assets/index-tK7EUtyc.css +5 -0
  6. package/dist/dashboard/index.html +13 -0
  7. package/dist/src/GraphQLError.d.ts +8 -0
  8. package/dist/src/GraphQLError.js +79 -0
  9. package/dist/src/__generated__/fragment-masking.d.ts +19 -0
  10. package/dist/src/__generated__/fragment-masking.js +16 -0
  11. package/dist/src/__generated__/gql.d.ts +26 -0
  12. package/dist/src/__generated__/gql.js +15 -0
  13. package/dist/src/__generated__/graphql.d.ts +3129 -0
  14. package/dist/src/__generated__/graphql.js +454 -0
  15. package/dist/src/__generated__/index.d.ts +2 -0
  16. package/dist/src/__generated__/index.js +2 -0
  17. package/dist/src/config.d.ts +14 -0
  18. package/dist/src/config.js +57 -0
  19. package/dist/src/db/index.d.ts +89 -0
  20. package/dist/src/db/index.js +339 -0
  21. package/dist/src/db/internal.d.ts +7 -0
  22. package/dist/src/db/internal.js +33 -0
  23. package/dist/src/db/public.d.ts +7 -0
  24. package/dist/src/db/public.js +20 -0
  25. package/dist/src/db/types.d.ts +80 -0
  26. package/dist/src/db/types.js +1 -0
  27. package/dist/src/db/util.d.ts +6 -0
  28. package/dist/src/db/util.js +9 -0
  29. package/dist/src/express.d.ts +13 -0
  30. package/dist/src/express.js +1 -0
  31. package/dist/src/grafast.d.ts +1 -0
  32. package/dist/src/grafast.js +1 -0
  33. package/dist/src/graphile.d.ts +1 -0
  34. package/dist/src/graphile.js +1 -0
  35. package/dist/src/graphql-client.d.ts +6 -0
  36. package/dist/src/graphql-client.js +8 -0
  37. package/dist/src/graphql-queries.d.ts +18 -0
  38. package/dist/src/graphql-queries.js +123 -0
  39. package/dist/src/graphql.d.ts +1 -0
  40. package/dist/src/graphql.js +1 -0
  41. package/dist/src/index.d.ts +15 -0
  42. package/dist/src/index.js +65 -0
  43. package/dist/src/logger.d.ts +9 -0
  44. package/dist/src/logger.js +21 -0
  45. package/dist/src/pg-versions/001-schema.sql +456 -0
  46. package/dist/src/plugins/caas-add-casino.d.ts +1 -0
  47. package/dist/src/plugins/caas-add-casino.js +150 -0
  48. package/dist/src/plugins/caas-authenticate.d.ts +1 -0
  49. package/dist/src/plugins/caas-authenticate.js +175 -0
  50. package/dist/src/plugins/caas-balance-alert.d.ts +1 -0
  51. package/dist/src/plugins/caas-balance-alert.js +43 -0
  52. package/dist/src/plugins/caas-claim-faucet.d.ts +1 -0
  53. package/dist/src/plugins/caas-claim-faucet.js +85 -0
  54. package/dist/src/plugins/caas-current-x.d.ts +1 -0
  55. package/dist/src/plugins/caas-current-x.js +62 -0
  56. package/dist/src/plugins/caas-schema-prefix.d.ts +1 -0
  57. package/dist/src/plugins/caas-schema-prefix.js +25 -0
  58. package/dist/src/plugins/caas-user-balance-by-currency.d.ts +1 -0
  59. package/dist/src/plugins/caas-user-balance-by-currency.js +55 -0
  60. package/dist/src/plugins/caas-withdraw.d.ts +1 -0
  61. package/dist/src/plugins/caas-withdraw.js +133 -0
  62. package/dist/src/plugins/debug.d.ts +1 -0
  63. package/dist/src/plugins/debug.js +14 -0
  64. package/dist/src/plugins/hub-add-casino.d.ts +1 -0
  65. package/dist/src/plugins/hub-add-casino.js +150 -0
  66. package/dist/src/plugins/hub-authenticate.d.ts +1 -0
  67. package/dist/src/plugins/hub-authenticate.js +175 -0
  68. package/dist/src/plugins/hub-balance-alert.d.ts +1 -0
  69. package/dist/src/plugins/hub-balance-alert.js +43 -0
  70. package/dist/src/plugins/hub-claim-faucet.d.ts +1 -0
  71. package/dist/src/plugins/hub-claim-faucet.js +85 -0
  72. package/dist/src/plugins/hub-current-x.d.ts +1 -0
  73. package/dist/src/plugins/hub-current-x.js +62 -0
  74. package/dist/src/plugins/hub-schema-prefix.d.ts +1 -0
  75. package/dist/src/plugins/hub-schema-prefix.js +25 -0
  76. package/dist/src/plugins/hub-user-balance-by-currency.d.ts +1 -0
  77. package/dist/src/plugins/hub-user-balance-by-currency.js +55 -0
  78. package/dist/src/plugins/hub-withdraw.d.ts +1 -0
  79. package/dist/src/plugins/hub-withdraw.js +133 -0
  80. package/dist/src/plugins/id-to-node-id.d.ts +1 -0
  81. package/dist/src/plugins/id-to-node-id.js +31 -0
  82. package/dist/src/plugins/validate-fields.d.ts +1 -0
  83. package/dist/src/plugins/validate-fields.js +61 -0
  84. package/dist/src/process-transfers.d.ts +7 -0
  85. package/dist/src/process-transfers.js +413 -0
  86. package/dist/src/process-withdrawal-request.d.ts +5 -0
  87. package/dist/src/process-withdrawal-request.js +129 -0
  88. package/dist/src/server/graphile.config.d.ts +33 -0
  89. package/dist/src/server/graphile.config.js +166 -0
  90. package/dist/src/server/handle-errors.d.ts +10 -0
  91. package/dist/src/server/handle-errors.js +88 -0
  92. package/dist/src/server/index.d.ts +2 -0
  93. package/dist/src/server/index.js +69 -0
  94. package/dist/src/server/middleware/authentication.d.ts +4 -0
  95. package/dist/src/server/middleware/authentication.js +55 -0
  96. package/dist/src/server/middleware/cors.d.ts +3 -0
  97. package/dist/src/server/middleware/cors.js +14 -0
  98. package/dist/src/services/jwt-service.d.ts +13 -0
  99. package/dist/src/services/jwt-service.js +131 -0
  100. package/dist/src/smart-tags.d.ts +1 -0
  101. package/dist/src/smart-tags.js +55 -0
  102. package/dist/src/util.d.ts +12 -0
  103. package/dist/src/util.js +4 -0
  104. package/dist/src/validate.d.ts +9 -0
  105. package/dist/src/validate.js +91 -0
  106. package/package.json +69 -0
@@ -0,0 +1,166 @@
1
+ import "graphile-config";
2
+ import "postgraphile";
3
+ import { makePgService } from "postgraphile/adaptors/pg";
4
+ import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
5
+ import config from "../config.js";
6
+ import { maskError } from "./handle-errors.js";
7
+ import * as db from "../db/index.js";
8
+ import { SmartTagsPlugin } from "../smart-tags.js";
9
+ import { HubAuthenticatePlugin } from "../plugins/hub-authenticate.js";
10
+ import { IdToNodeIdPlugin } from "../plugins/id-to-node-id.js";
11
+ import { HubClaimFaucetPlugin } from "../plugins/hub-claim-faucet.js";
12
+ import { DebugPlugin } from "../plugins/debug.js";
13
+ import { HubWithdrawPlugin } from "../plugins/hub-withdraw.js";
14
+ import { HubPrefixPlugin } from "../plugins/hub-schema-prefix.js";
15
+ import { isUuid } from "../util.js";
16
+ import { HubUserBalanceByCurrencyPlugin } from "../plugins/hub-user-balance-by-currency.js";
17
+ import { logger } from "../logger.js";
18
+ import { ValidateCasinoFieldsPlugin } from "../plugins/validate-fields.js";
19
+ import { HubAddCasinoPlugin } from "../plugins/hub-add-casino.js";
20
+ import { HubBalanceAlertPlugin } from "../plugins/hub-balance-alert.js";
21
+ import { custom as customPgOmitArchivedPlugin } from "@graphile-contrib/pg-omit-archived";
22
+ import { HubCurrentXPlugin } from "../plugins/hub-current-x.js";
23
+ export const requiredPlugins = [
24
+ SmartTagsPlugin,
25
+ IdToNodeIdPlugin,
26
+ HubPrefixPlugin,
27
+ HubAuthenticatePlugin,
28
+ HubCurrentXPlugin,
29
+ HubWithdrawPlugin,
30
+ HubBalanceAlertPlugin,
31
+ HubUserBalanceByCurrencyPlugin,
32
+ HubAddCasinoPlugin,
33
+ ValidateCasinoFieldsPlugin,
34
+ ];
35
+ export const defaultPlugins = [
36
+ ...(config.NODE_ENV === "development" ? [DebugPlugin] : []),
37
+ ...requiredPlugins,
38
+ HubClaimFaucetPlugin,
39
+ customPgOmitArchivedPlugin("deleted"),
40
+ ];
41
+ export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, }) {
42
+ if (!exportSchemaSDLPath.startsWith("/")) {
43
+ throw new Error("exportSchemaSDLPath must be an absolute path");
44
+ }
45
+ if (!exportSchemaSDLPath.endsWith(".graphql")) {
46
+ throw new Error("exportSchemaSDLPath must end with .graphql");
47
+ }
48
+ const mutablePlugins = [...plugins];
49
+ for (const requiredPlugin of requiredPlugins) {
50
+ if (!plugins.some((plugin) => plugin === requiredPlugin)) {
51
+ logger.warn(`Adding required plugin "${requiredPlugin.name}"`);
52
+ mutablePlugins.unshift(requiredPlugin);
53
+ }
54
+ }
55
+ logger.info("Will save generated graphql schema to:", exportSchemaSDLPath);
56
+ const preset = {
57
+ extends: [PostGraphileAmberPreset],
58
+ disablePlugins: ["NodePlugin"],
59
+ pgServices: [
60
+ makePgService({
61
+ connectionString: config.DATABASE_URL,
62
+ schemas: [...extraPgSchemas, "hub"],
63
+ }),
64
+ ],
65
+ schema: {
66
+ dontSwallowErrors: true,
67
+ exportSchemaSDLPath,
68
+ sortExport: true,
69
+ defaultBehavior: [
70
+ "-resource:update",
71
+ "-resource:delete",
72
+ "-resource:insert",
73
+ "-query:resource:list",
74
+ "-query:resource:connection",
75
+ ].join(" "),
76
+ pgDeletedColumnName: "deleted_at",
77
+ },
78
+ grafserv: {
79
+ dangerouslyAllowAllCORSRequests: true,
80
+ websockets: true,
81
+ maskError(error) {
82
+ return maskError(error);
83
+ },
84
+ },
85
+ grafast: {
86
+ context(ctx, _args) {
87
+ if (ctx.ws) {
88
+ return handleWebsocketContext(ctx) || {};
89
+ }
90
+ const expressReq = ctx.expressv4.req;
91
+ const reqIdentity = expressReq.identity;
92
+ let pluginIdentity = undefined;
93
+ if (reqIdentity?.kind === "operator") {
94
+ pluginIdentity = {
95
+ kind: "operator",
96
+ };
97
+ }
98
+ else if (reqIdentity?.kind === "user") {
99
+ pluginIdentity = {
100
+ kind: "user",
101
+ session: {
102
+ user_id: reqIdentity.user.id,
103
+ mp_user_id: reqIdentity.user.mp_user_id,
104
+ casino_id: reqIdentity.user.casino_id,
105
+ experience_id: reqIdentity.user.experience_id,
106
+ session_id: reqIdentity.sessionId,
107
+ },
108
+ };
109
+ }
110
+ const pgSettings = {};
111
+ if (reqIdentity?.kind === "user") {
112
+ pgSettings["session.user_id"] = reqIdentity.user.id;
113
+ pgSettings["session.experience_id"] = reqIdentity.user.experience_id;
114
+ pgSettings["session.casino_id"] = reqIdentity.user.casino_id;
115
+ pgSettings["session.session_id"] = reqIdentity.sessionId;
116
+ }
117
+ else if (reqIdentity?.kind === "operator") {
118
+ pgSettings["operator.api_key"] = reqIdentity.apiKey;
119
+ }
120
+ return {
121
+ pgSettings,
122
+ identity: pluginIdentity,
123
+ };
124
+ },
125
+ },
126
+ plugins: mutablePlugins,
127
+ };
128
+ return preset;
129
+ }
130
+ function getSessionIdFromWebsocketCtx(ctx) {
131
+ const value = Object.entries(ctx.ws.connectionParams).find(([key]) => key.toLowerCase() === "authorization")?.[1];
132
+ if (typeof value !== "string") {
133
+ return "";
134
+ }
135
+ const uuid = value.slice("session:".length);
136
+ return isUuid(uuid) ? uuid : "";
137
+ }
138
+ async function handleWebsocketContext(ctx) {
139
+ const sessionId = getSessionIdFromWebsocketCtx(ctx);
140
+ if (!sessionId) {
141
+ throw new Error("Unauthorized");
142
+ }
143
+ const result = await db.userFromActiveSessionKey(db.superuserPool, sessionId);
144
+ if (!result) {
145
+ throw new Error("Unauthorized");
146
+ }
147
+ const session = {
148
+ user_id: result.user.id,
149
+ mp_user_id: result.user.mp_user_id,
150
+ casino_id: result.user.casino_id,
151
+ experience_id: result.user.experience_id,
152
+ session_id: result.sessionId,
153
+ };
154
+ return {
155
+ pgSettings: {
156
+ "session.user_id": result.user.id,
157
+ "session.experience_id": result.user.experience_id,
158
+ "session.casino_id": result.user.casino_id,
159
+ "session.session_id": result.sessionId,
160
+ },
161
+ identity: {
162
+ kind: "user",
163
+ session,
164
+ },
165
+ };
166
+ }
@@ -0,0 +1,10 @@
1
+ import { GraphQLError } from "postgraphile/graphql";
2
+ declare const pluck: (err: any) => {
3
+ [key: string]: any;
4
+ };
5
+ export declare const ERROR_MESSAGE_OVERRIDES: {
6
+ [code: string]: typeof pluck;
7
+ };
8
+ export declare function maskError(error: GraphQLError): GraphQLError;
9
+ export default function handleErrors(errors: readonly GraphQLError[]): GraphQLError[];
10
+ export {};
@@ -0,0 +1,88 @@
1
+ import { GraphQLError } from "postgraphile/graphql";
2
+ import config from "../config.js";
3
+ const isDev = config.NODE_ENV === "development";
4
+ const isTest = config.NODE_ENV === "test";
5
+ const camelCase = (s) => s.toLowerCase().replace(/(_\\w)/g, (m) => m[1].toUpperCase());
6
+ const ERROR_PROPERTIES_TO_EXPOSE = isDev || isTest
7
+ ? [
8
+ "code",
9
+ "severity",
10
+ "detail",
11
+ "hint",
12
+ "position",
13
+ "internalPosition",
14
+ "internalQuery",
15
+ "where",
16
+ "schema",
17
+ "table",
18
+ "column",
19
+ "dataType",
20
+ "constraint",
21
+ ]
22
+ : ["code"];
23
+ const pluck = (err) => {
24
+ return ERROR_PROPERTIES_TO_EXPOSE.reduce((memo, key) => {
25
+ const value = key === "code"
26
+ ?
27
+ err.code || err.errcode
28
+ : err[key];
29
+ if (value != null) {
30
+ memo[key] = value;
31
+ }
32
+ return memo;
33
+ }, Object.create(null));
34
+ };
35
+ export const ERROR_MESSAGE_OVERRIDES = {
36
+ "42501": (err) => ({
37
+ ...pluck(err),
38
+ message: "Permission denied (by RLS)",
39
+ }),
40
+ "23505": (err) => ({
41
+ ...pluck(err),
42
+ message: "Conflict occurred",
43
+ fields: conflictFieldsFromError(err),
44
+ code: "NUNIQ",
45
+ }),
46
+ "23503": (err) => ({
47
+ ...pluck(err),
48
+ message: "Invalid reference",
49
+ fields: conflictFieldsFromError(err),
50
+ code: "BADFK",
51
+ }),
52
+ };
53
+ function conflictFieldsFromError(err) {
54
+ const { table, constraint } = err;
55
+ if (constraint && table) {
56
+ const PREFIX = `${table}_`;
57
+ const SUFFIX_LIST = [`_key`, `_fkey`];
58
+ if (constraint.startsWith(PREFIX)) {
59
+ const matchingSuffix = SUFFIX_LIST.find((SUFFIX) => constraint.endsWith(SUFFIX));
60
+ if (matchingSuffix) {
61
+ const maybeColumnNames = constraint.substr(PREFIX.length, constraint.length - PREFIX.length - matchingSuffix.length);
62
+ return [camelCase(maybeColumnNames)];
63
+ }
64
+ }
65
+ }
66
+ return undefined;
67
+ }
68
+ export function maskError(error) {
69
+ const { message: rawMessage, originalError } = error;
70
+ const code = originalError ? originalError["code"] : null;
71
+ const localPluck = ERROR_MESSAGE_OVERRIDES[code] || pluck;
72
+ const exception = localPluck(originalError || error);
73
+ const options = {
74
+ nodes: error.nodes,
75
+ source: error.source,
76
+ positions: error.positions,
77
+ path: error.path,
78
+ originalError: error.originalError,
79
+ extensions: {
80
+ exception,
81
+ code: code || exception.code || error.extensions.code,
82
+ },
83
+ };
84
+ return new GraphQLError(exception.message || rawMessage, options);
85
+ }
86
+ export default function handleErrors(errors) {
87
+ return errors.map(maskError);
88
+ }
@@ -0,0 +1,2 @@
1
+ import { ServerOptions } from "../index.js";
2
+ export declare function listen({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, }: Pick<ServerOptions, "plugins" | "exportSchemaSDLPath" | "extraPgSchemas" | "configureApp">): Promise<void>;
@@ -0,0 +1,69 @@
1
+ import { createServer } from "node:http";
2
+ import { grafserv } from "grafserv/express/v4";
3
+ import postgraphile from "postgraphile";
4
+ import { createPreset, defaultPlugins } from "./graphile.config.js";
5
+ import express from "express";
6
+ import config from "../config.js";
7
+ import { logger } from "../logger.js";
8
+ import cors from "./middleware/cors.js";
9
+ import authentication from "./middleware/authentication.js";
10
+ import path, { dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import fs from "node:fs";
13
+ const __filename = fileURLToPath(import.meta.url);
14
+ const __dirname = dirname(__filename);
15
+ function findDistDirectory(startPath) {
16
+ let currentPath = startPath;
17
+ while (currentPath !== "/") {
18
+ console.log(currentPath);
19
+ const distPath = path.join(currentPath, "dist");
20
+ if (fs.existsSync(distPath)) {
21
+ return distPath;
22
+ }
23
+ currentPath = path.dirname(currentPath);
24
+ }
25
+ return null;
26
+ }
27
+ const distDir = findDistDirectory(__dirname);
28
+ if (!distDir) {
29
+ throw new Error("Could not find dist directory");
30
+ }
31
+ const dashboardDir = path.join(distDir, "dashboard");
32
+ if (!fs.existsSync(dashboardDir)) {
33
+ throw new Error(`Could not find dashboard directory. Expected it to be at "${dashboardDir}"`);
34
+ }
35
+ function createExpressServer() {
36
+ const app = express();
37
+ app.disable("x-powered-by");
38
+ app.use(cors());
39
+ app.use(authentication());
40
+ app.use("/dashboard", express.static(dashboardDir));
41
+ app.use("/dashboard/*splat", (_, res) => {
42
+ res.sendFile(path.join(dashboardDir, "index.html"));
43
+ });
44
+ return app;
45
+ }
46
+ export async function listen({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, }) {
47
+ const expressServer = createExpressServer();
48
+ const preset = createPreset({
49
+ plugins: plugins ?? defaultPlugins,
50
+ exportSchemaSDLPath,
51
+ extraPgSchemas: extraPgSchemas ?? [],
52
+ });
53
+ const pgl = postgraphile.default(preset);
54
+ const serv = pgl.createServ(grafserv);
55
+ if (configureApp) {
56
+ configureApp(expressServer);
57
+ }
58
+ const server = createServer(expressServer);
59
+ server.on("error", (e) => {
60
+ logger.error(e);
61
+ });
62
+ serv.addTo(expressServer, server).catch((e) => {
63
+ logger.error(e);
64
+ process.exit(1);
65
+ });
66
+ return new Promise((resolve) => {
67
+ server.listen(config.PORT, resolve);
68
+ });
69
+ }
@@ -0,0 +1,4 @@
1
+ import { Response, NextFunction } from "express";
2
+ import { HubRequest } from "../../express.js";
3
+ declare const authentication: () => (req: HubRequest, _: Response, next: NextFunction) => Promise<void>;
4
+ export default authentication;
@@ -0,0 +1,55 @@
1
+ import { isUuid } from "../../util.js";
2
+ import * as db from "../../db/index.js";
3
+ async function checkAndUpdateApiKey(key) {
4
+ const row = await db.superuserPool
5
+ .query(`
6
+ UPDATE hub.api_key
7
+ SET last_used_at = now()
8
+ WHERE key = $1::uuid
9
+ AND revoked_at IS NULL
10
+ RETURNING id`, [key])
11
+ .then(db.maybeOneRow);
12
+ return !!row;
13
+ }
14
+ const authentication = () => {
15
+ return async (req, _, next) => {
16
+ const header = req.get("authorization");
17
+ if (!header) {
18
+ return next();
19
+ }
20
+ const match = header.match(/^(session|apikey):(.+)$/);
21
+ if (!match) {
22
+ return next();
23
+ }
24
+ const tokenType = match[1];
25
+ const token = match[2];
26
+ if (!isUuid(token)) {
27
+ return next();
28
+ }
29
+ if (tokenType === "session") {
30
+ const sessionKey = token;
31
+ const result = await db.userFromActiveSessionKey(db.superuserPool, sessionKey);
32
+ if (result) {
33
+ req.identity = {
34
+ kind: "user",
35
+ user: result.user,
36
+ sessionId: result.sessionId,
37
+ };
38
+ }
39
+ return next();
40
+ }
41
+ else if (tokenType === "apikey") {
42
+ const apiKey = token;
43
+ const isValid = await checkAndUpdateApiKey(apiKey);
44
+ if (isValid) {
45
+ req.identity = {
46
+ kind: "operator",
47
+ apiKey,
48
+ };
49
+ }
50
+ return next();
51
+ }
52
+ return next();
53
+ };
54
+ };
55
+ export default authentication;
@@ -0,0 +1,3 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ declare const cors: () => (req: Request, res: Response, next: NextFunction) => Promise<void>;
3
+ export default cors;
@@ -0,0 +1,14 @@
1
+ const cors = () => {
2
+ return async (req, res, next) => {
3
+ res.header("Access-Control-Allow-Origin", "*");
4
+ res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
5
+ if (req.method === "OPTIONS") {
6
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH");
7
+ res.header("Access-Control-Max-Age", "86400");
8
+ res.status(204).end();
9
+ return;
10
+ }
11
+ next();
12
+ };
13
+ };
14
+ export default cors;
@@ -0,0 +1,13 @@
1
+ import { PoolClient } from "pg";
2
+ import { GraphQLClient } from "graphql-request";
3
+ import { QueryExecutor, Result } from "../util.js";
4
+ export declare function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient: PoolClient, { casinoId, jwt, }: {
5
+ casinoId: string;
6
+ jwt: string;
7
+ }): Promise<Result<{
8
+ userToken: string;
9
+ }, string>>;
10
+ export declare function refreshCasinoJwksTask(pgClient: QueryExecutor, { casinoId, graphqlClient, }: {
11
+ casinoId: string;
12
+ graphqlClient: GraphQLClient;
13
+ }): Promise<void>;
@@ -0,0 +1,131 @@
1
+ import { z } from "zod";
2
+ import * as jose from "jose";
3
+ import { gql } from "../__generated__/gql.js";
4
+ import { maybeOneRow } from "../db/util.js";
5
+ import { logger } from "../logger.js";
6
+ const CACHE_TTL = "5 minutes";
7
+ const GET_CASINO_JWKS = gql(`
8
+ query GetCasinoJWKS {
9
+ jwks
10
+ }
11
+ `);
12
+ const JSONWebKeySetSchema = z.object({
13
+ keys: z
14
+ .array(z.object({
15
+ kty: z.literal("OKP"),
16
+ kid: z.string().uuid(),
17
+ use: z.literal("sig"),
18
+ alg: z.literal("EdDSA"),
19
+ crv: z.literal("Ed25519"),
20
+ x: z.string(),
21
+ }))
22
+ .min(1),
23
+ });
24
+ async function dbInsertCasinoJWKS(pgClient, { casinoId, jwks: unvalidatedJwks, }) {
25
+ logger.debug(`[dbInsertCasinoJWKS] Inserting JWKS for casino ${casinoId}`, unvalidatedJwks);
26
+ const result = JSONWebKeySetSchema.safeParse(unvalidatedJwks);
27
+ if (!result.success) {
28
+ throw new Error(`Will not insert invalid JWKS into database: ${result.error}`);
29
+ }
30
+ await pgClient.query(`
31
+ WITH old_value AS (
32
+ SELECT jwks FROM hub.jwk_set WHERE casino_id = $1
33
+ ),
34
+ updated AS (
35
+ INSERT INTO hub.jwk_set(casino_id, jwks)
36
+ VALUES($1, $2)
37
+ ON CONFLICT (casino_id) DO UPDATE
38
+ SET jwks = EXCLUDED.jwks,
39
+ updated_at = NOW()
40
+ RETURNING casino_id, jwks,
41
+ (
42
+ NOT EXISTS (SELECT 1 FROM old_value) -- new insert
43
+ OR
44
+ (SELECT jwks::jsonb FROM old_value) IS DISTINCT FROM $2::jsonb -- content changed
45
+ ) as changed
46
+ )
47
+ INSERT INTO hub.jwk_set_snapshot(casino_id, jwks)
48
+ SELECT casino_id, jwks FROM updated
49
+ WHERE changed = true
50
+ `, [casinoId, result.data]);
51
+ }
52
+ async function dbGetCasinoJWKS(pgClient, { casinoId }) {
53
+ const jwks = await pgClient
54
+ .query(`
55
+ SELECT *
56
+ FROM hub.jwk_set
57
+ WHERE casino_id = $1
58
+ `, [casinoId])
59
+ .then(maybeOneRow);
60
+ return jwks ?? null;
61
+ }
62
+ async function mpFetchCasinoJWKS(graphqlClient) {
63
+ console.log("sending request to casino");
64
+ const response = await graphqlClient.request(GET_CASINO_JWKS);
65
+ console.log("response", response);
66
+ const jwks = response.jwks;
67
+ if (jwks.keys.length === 0) {
68
+ throw new Error("No JWKS keys found in response");
69
+ }
70
+ const parseResult = JSONWebKeySetSchema.safeParse(jwks);
71
+ if (!parseResult.success) {
72
+ throw new Error("Invalid JWKS from casino", parseResult.error);
73
+ }
74
+ return parseResult.data;
75
+ }
76
+ export async function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient, { casinoId, jwt, }) {
77
+ const jwksRow = await dbGetCasinoJWKS(pgClient, { casinoId });
78
+ if (!jwksRow) {
79
+ throw new Error("No JWKS found in database");
80
+ }
81
+ const joseJwks = await jose.createLocalJWKSet(jwksRow.jwks);
82
+ let result;
83
+ try {
84
+ result = await jose.jwtVerify(jwt, joseJwks);
85
+ }
86
+ catch (e) {
87
+ if (e instanceof jose.errors.JWTExpired) {
88
+ return { ok: false, error: "User token expired" };
89
+ }
90
+ return { ok: false, error: "Invalid user token" };
91
+ }
92
+ const { userToken } = result.payload;
93
+ if (typeof userToken !== "string") {
94
+ return { ok: false, error: "No user token found in JWT" };
95
+ }
96
+ const alreadyUsed = await dbAlreadyUsedUserToken(pgClient, { userToken });
97
+ if (alreadyUsed) {
98
+ return { ok: false, error: "User token already used" };
99
+ }
100
+ return { ok: true, value: { userToken } };
101
+ }
102
+ async function dbAlreadyUsedUserToken(pgClient, { userToken }) {
103
+ const result = await pgClient
104
+ .query(`
105
+ SELECT 1
106
+ FROM hub.session
107
+ WHERE user_token = $1
108
+
109
+ FOR UPDATE SKIP LOCKED
110
+ `, [userToken])
111
+ .then(maybeOneRow);
112
+ return !!result;
113
+ }
114
+ export async function refreshCasinoJwksTask(pgClient, { casinoId, graphqlClient, }) {
115
+ const cacheHit = await pgClient
116
+ .query(`
117
+ SELECT 1
118
+ FROM hub.jwk_set
119
+ WHERE casino_id = $1
120
+ AND updated_at > NOW() - $2::interval
121
+ `, [casinoId, CACHE_TTL])
122
+ .then((res) => res.rows.length > 0);
123
+ if (cacheHit) {
124
+ return;
125
+ }
126
+ const jwks = await mpFetchCasinoJWKS(graphqlClient);
127
+ await dbInsertCasinoJWKS(pgClient, {
128
+ casinoId,
129
+ jwks,
130
+ });
131
+ }
@@ -0,0 +1 @@
1
+ export declare const SmartTagsPlugin: GraphileConfig.Plugin;
@@ -0,0 +1,55 @@
1
+ import { makeJSONPgSmartTagsPlugin } from "postgraphile/utils";
2
+ export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
3
+ version: 1,
4
+ config: {
5
+ attribute: {
6
+ "hub.casino.id": {
7
+ tags: {
8
+ behavior: ["-attribute:update"],
9
+ },
10
+ },
11
+ "hub.session.user_token": {
12
+ tags: {
13
+ behavior: ["-orderBy"],
14
+ },
15
+ },
16
+ "hub.session.key": {
17
+ tags: {
18
+ behavior: ["-orderBy"],
19
+ },
20
+ },
21
+ "hub.bankroll.amount": {
22
+ tags: {
23
+ behavior: ["+attribute:update"],
24
+ },
25
+ },
26
+ },
27
+ class: {
28
+ "hub.balance": {
29
+ tags: {
30
+ behavior: ["-resource:select"],
31
+ },
32
+ },
33
+ "hub.bankroll": {
34
+ tags: {
35
+ behavior: ["+resource:update"],
36
+ },
37
+ },
38
+ "hub.casino": {
39
+ tags: {
40
+ behavior: [
41
+ "+query:resource:connection",
42
+ "+resource:update",
43
+ ],
44
+ },
45
+ },
46
+ "hub.api_key": {
47
+ tags: {
48
+ behavior: [
49
+ "+query:resource:connection",
50
+ ],
51
+ },
52
+ },
53
+ },
54
+ },
55
+ });
@@ -0,0 +1,12 @@
1
+ import { QueryResult, QueryResultRow } from "pg";
2
+ export declare function isUuid(input: any): boolean;
3
+ export interface QueryExecutor {
4
+ query<T extends QueryResultRow = any>(queryText: string, values?: any[]): Promise<QueryResult<T>>;
5
+ }
6
+ export type Result<V, E> = {
7
+ ok: true;
8
+ value: V;
9
+ } | {
10
+ ok: false;
11
+ error: E;
12
+ };
@@ -0,0 +1,4 @@
1
+ const UUID_REGEX = /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/;
2
+ export function isUuid(input) {
3
+ return typeof input === "string" && UUID_REGEX.test(input);
4
+ }
@@ -0,0 +1,9 @@
1
+ import * as Yup from "yup";
2
+ export declare const isUuid: {
3
+ name: string;
4
+ message: string;
5
+ test(value: string | undefined): boolean;
6
+ };
7
+ export declare const graphqlUrl: () => Yup.StringSchema<string | undefined, Yup.AnyObject, undefined, "">;
8
+ export declare const baseUrl: () => Yup.StringSchema<string | undefined, Yup.AnyObject, undefined, "">;
9
+ export declare const uuid: () => Yup.StringSchema<string | undefined, Yup.AnyObject, undefined, "">;