@powerhousedao/reactor-api 1.13.0 → 1.14.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.
@@ -0,0 +1,149 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
2
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-call */
5
+ /* eslint-disable @typescript-eslint/no-unsafe-argument */
6
+ import { GraphQLError } from "graphql";
7
+ import ms from "ms";
8
+ import { SiweMessage } from "siwe";
9
+ import { JWT_EXPIRATION_PERIOD } from "../env";
10
+ import { Session } from "../types";
11
+ import {
12
+ generateTokenAndSession,
13
+ validateOriginAgainstAllowed,
14
+ verifyToken,
15
+ } from "./helpers";
16
+ import { Db } from "../../../types";
17
+ import { Context } from "src/subgraphs/types";
18
+
19
+ export const createAuthenticationSession = async (
20
+ db: Db,
21
+ userId: string,
22
+ allowedOrigins = ["*"],
23
+ ) => {
24
+ return generateTokenAndSession(
25
+ db,
26
+ {
27
+ expiresAt: new Date(
28
+ new Date().getTime() + ms(JWT_EXPIRATION_PERIOD),
29
+ ).toISOString(),
30
+ name: "Sign in/Sign up",
31
+ allowedOrigins,
32
+ },
33
+ userId,
34
+ true,
35
+ );
36
+ };
37
+
38
+ export const createCustomSession = async (
39
+ db: Db,
40
+ userId: string,
41
+ session: {
42
+ expiryDurationSeconds?: number | null;
43
+ name: string;
44
+ allowedOrigins: string[];
45
+ },
46
+ isUserCreated = false,
47
+ ) => {
48
+ return generateTokenAndSession(db, session, userId, isUserCreated);
49
+ };
50
+
51
+ export const listSessions = async (db: Db, userId: string) => {
52
+ return db<Session>("Session").select().where("createdBy", userId);
53
+ };
54
+
55
+ export const revoke = async (db: Db, sessionId: string, userId: string) => {
56
+ const [session] = await db<Session>("Session").select().where({
57
+ id: sessionId,
58
+ userId,
59
+ });
60
+
61
+ if (!session) {
62
+ throw new GraphQLError("Session not found", {
63
+ extensions: { code: "SESSION_NOT_FOUND" },
64
+ });
65
+ }
66
+ if (session.revokedAt !== null) {
67
+ throw new GraphQLError("Session already revoked", {
68
+ extensions: { code: "SESSION_ALREADY_REVOKED" },
69
+ });
70
+ }
71
+ await db<Session>("Session")
72
+ .update({
73
+ revokedAt: new Date().toISOString(),
74
+ })
75
+ .where({
76
+ id: sessionId,
77
+ userId,
78
+ });
79
+ };
80
+
81
+ export const authenticate = async (context: Context) => {
82
+ const authorization = context.headers.authorization;
83
+ const db = context.db as Db;
84
+ if (!authorization) {
85
+ throw new GraphQLError("Not authenticated", {
86
+ extensions: { code: "NOT_AUTHENTICATED" },
87
+ });
88
+ }
89
+ const token = authorization.replace("Bearer ", "");
90
+ const origin = context.headers.origin;
91
+ const session = await getSessionByToken(db, origin, token);
92
+ return session;
93
+ };
94
+
95
+ export const getSessionByToken = async (
96
+ db: Db,
97
+ origin?: string,
98
+ token?: string,
99
+ ) => {
100
+ if (!token) {
101
+ throw new GraphQLError("Not authenticated", {
102
+ extensions: { code: "NOT_AUTHENTICATED" },
103
+ });
104
+ }
105
+ const verificationTokenResult = verifyToken(token);
106
+ if (!verificationTokenResult) {
107
+ throw new GraphQLError("Invalid token", {
108
+ extensions: { code: "INVALID_TOKEN" },
109
+ });
110
+ }
111
+ const { sessionId } = verificationTokenResult;
112
+ const [session] = await db<Session>("Session").select().where({
113
+ id: sessionId,
114
+ });
115
+ if (!session) {
116
+ throw new GraphQLError("Session not found", {
117
+ extensions: { code: "SESSION_NOT_FOUND" },
118
+ });
119
+ }
120
+ if (session.revokedAt) {
121
+ throw new GraphQLError("Session expired", {
122
+ extensions: { code: "SESSION_EXPIRED" },
123
+ });
124
+ }
125
+ if (
126
+ origin &&
127
+ (!session.allowedOrigins ||
128
+ session.allowedOrigins === "*" ||
129
+ session.allowedOrigins.includes(origin))
130
+ ) {
131
+ validateOriginAgainstAllowed(session.allowedOrigins, origin);
132
+ }
133
+ return session;
134
+ };
135
+
136
+ export const verifySignature = async (
137
+ parsedMessage: SiweMessage,
138
+ signature: string,
139
+ ) => {
140
+ try {
141
+ const response = await parsedMessage.verify({
142
+ time: new Date().toISOString(),
143
+ signature,
144
+ });
145
+ return response;
146
+ } catch (error) {
147
+ throw new GraphQLError("Invalid signature");
148
+ }
149
+ };
@@ -0,0 +1,40 @@
1
+ import { GraphQLError } from "graphql";
2
+ import { Db } from "../../../utils/db";
3
+
4
+ interface User {
5
+ address: string;
6
+ createdAt?: string;
7
+ updatedAt?: string;
8
+ networkId: string;
9
+ chainId: number;
10
+ }
11
+ export const upsertUser = async (db: Db, user: User) => {
12
+ const { AUTH_SIGNUP_DISABLED } = process.env;
13
+ if (AUTH_SIGNUP_DISABLED) {
14
+ throw new GraphQLError("Sign up is disabled");
15
+ }
16
+
17
+ const [existingUser] = await db<User>("User")
18
+ .select()
19
+ .where("address", user.address);
20
+
21
+ if (existingUser) {
22
+ return existingUser;
23
+ }
24
+
25
+ const date = new Date().toISOString();
26
+ const [newUser] = await db<User>("User")
27
+ .insert({
28
+ address: user.address,
29
+ updatedAt: date,
30
+ createdAt: date,
31
+ })
32
+ .returning("*");
33
+
34
+ return newUser;
35
+ };
36
+
37
+ export const getUser = async (db: Db, address: string) => {
38
+ const [user] = await db<User>("User").select().where("address", address);
39
+ return user;
40
+ };
@@ -6,10 +6,11 @@ import { GraphQLResolverMap } from "@apollo/subgraph/dist/schema-helper";
6
6
  import { gql } from "graphql-tag";
7
7
  import { Context } from "../types";
8
8
  import { Db } from "src/types";
9
+ import { SubgraphManager } from "../manager";
9
10
 
10
11
  export class Subgraph implements ISubgraph {
11
12
  name = "example";
12
- resolvers: GraphQLResolverMap<Context> = {
13
+ resolvers: Record<string, any> = {
13
14
  Query: {
14
15
  hello: () => this.name,
15
16
  },
@@ -20,9 +21,11 @@ export class Subgraph implements ISubgraph {
20
21
  }
21
22
  `;
22
23
  reactor: IDocumentDriveServer;
24
+ subgraphManager: SubgraphManager;
23
25
  operationalStore: Db;
24
26
  constructor(args: SubgraphArgs) {
25
27
  this.reactor = args.reactor;
28
+ this.subgraphManager = args.subgraphManager;
26
29
  this.operationalStore = args.operationalStore;
27
30
  }
28
31
  async onSetup() {
@@ -3,6 +3,7 @@ import { Subgraph } from "./base";
3
3
  export * as analyticsSubgraph from "./analytics";
4
4
  export * as driveSubgraph from "./drive";
5
5
  export * as systemSubgraph from "./system";
6
+ export * as authSubgraph from "./auth";
6
7
  export * from "./types";
7
8
  export { Subgraph } from "./base";
8
9
 
@@ -7,12 +7,13 @@ import cors from "cors";
7
7
  import { IDocumentDriveServer } from "document-drive";
8
8
  import express, { IRouter, Router } from "express";
9
9
  import { Db } from "src/types";
10
- import { Context, SubgraphArgs, SubgraphClass } from ".";
10
+ import { authSubgraph, Context, SubgraphArgs, SubgraphClass } from ".";
11
11
  import { createSchema } from "../utils/create-schema";
12
12
  import { AnalyticsSubgraph } from "./analytics";
13
13
  import { Subgraph } from "./base";
14
14
  import { DriveSubgraph } from "./drive";
15
15
  import { SystemSubgraph } from "./system";
16
+ import { AuthSubgraph } from "./auth";
16
17
 
17
18
  export class SubgraphManager {
18
19
  private reactorRouter: IRouter = Router();
@@ -27,6 +28,7 @@ export class SubgraphManager {
27
28
  private readonly analyticsStore: IAnalyticsStore,
28
29
  ) {
29
30
  // Setup Default subgraphs
31
+ this.registerSubgraph(AuthSubgraph);
30
32
  this.registerSubgraph(SystemSubgraph);
31
33
  this.registerSubgraph(DriveSubgraph);
32
34
  this.registerSubgraph(AnalyticsSubgraph);
@@ -106,7 +108,7 @@ export class SubgraphManager {
106
108
  });
107
109
  await subgraphInstance.onSetup();
108
110
  this.subgraphs.unshift(subgraphInstance);
109
- console.log(`> Registered ${subgraphInstance.name} subgraph.`);
111
+ console.log(`> Registered ${this.path.slice(-1) === "/" ? this.path : this.path+ "/"}${subgraphInstance.name} subgraph.`);
110
112
  await this.updateRouter();
111
113
  }
112
114
 
@@ -0,0 +1,7 @@
1
+ export const getAdminUsers = (): string[] => {
2
+ return (
3
+ process.env.ADMIN_USERS?.split(",").map((user) =>
4
+ user.trim().toLocaleLowerCase(),
5
+ ) || []
6
+ );
7
+ };
@@ -0,0 +1,6 @@
1
+ import dotenv from "dotenv";
2
+ import { getAdminUsers } from "./getters";
3
+
4
+ dotenv.config();
5
+
6
+ export const ADMIN_USERS = getAdminUsers();
@@ -1,9 +1,13 @@
1
1
  import { DriveInput } from "document-drive";
2
+ import { GraphQLError } from "graphql";
2
3
  import { gql } from "graphql-tag";
3
4
  import { Subgraph } from "../base";
5
+ import { ADMIN_USERS } from "./env";
6
+ import { SystemContext } from "./types";
4
7
 
5
8
  export class SystemSubgraph extends Subgraph {
6
9
  name = "system";
10
+
7
11
  typeDefs = gql`
8
12
  type Query {
9
13
  drives: [String!]!
@@ -32,8 +36,16 @@ export class SystemSubgraph extends Subgraph {
32
36
  },
33
37
  },
34
38
  Mutation: {
35
- addDrive: async (parent: unknown, args: DriveInput) => {
39
+ addDrive: async (
40
+ parent: unknown,
41
+ args: DriveInput,
42
+ ctx: SystemContext,
43
+ ) => {
36
44
  try {
45
+ const isAdmin = ctx.isAdmin(ctx);
46
+ if (!isAdmin) {
47
+ throw new GraphQLError("Unauthorized");
48
+ }
37
49
  const drive = await this.reactor.addDrive(args);
38
50
  return drive.state.global;
39
51
  } catch (e) {
@@ -43,4 +55,17 @@ export class SystemSubgraph extends Subgraph {
43
55
  },
44
56
  },
45
57
  };
58
+
59
+ async onSetup() {
60
+ await super.onSetup();
61
+ this.subgraphManager.setAdditionalContextFields({
62
+ isAdmin: (ctx: SystemContext) => {
63
+ return (
64
+ ADMIN_USERS.length === 0 ||
65
+ (ctx.session.address &&
66
+ ADMIN_USERS.includes(ctx.session.address.toLocaleLowerCase()))
67
+ );
68
+ },
69
+ });
70
+ }
46
71
  }
@@ -0,0 +1,5 @@
1
+ import { AuthContext } from "../auth/types";
2
+
3
+ export type SystemContext = AuthContext & {
4
+ isAdmin: (ctx: AuthContext) => boolean;
5
+ };
package/src/utils/db.ts CHANGED
@@ -16,10 +16,11 @@ export function getDbClient(
16
16
  ): Db {
17
17
  const isPg = connectionString && isPG(connectionString);
18
18
  const client = isPg ? "pg" : (ClientPgLite as typeof knex.Client);
19
-
19
+ const connection = isPg
20
+ ? { connectionString }
21
+ : { pglite: new PGlite(connectionString) };
20
22
  return knex({
21
23
  client,
22
- // @ts-expect-error
23
- connection: { pglite: new PGlite(connectionString) },
24
+ connection,
24
25
  });
25
26
  }
package/tsconfig.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "module": "ESNext",
4
4
  "moduleResolution": "Bundler",
5
5
  "target": "esnext",
6
- "types": ["node", "./types.d.ts"],
6
+ "types": ["@types/node", "./types.d.ts"],
7
7
  "declaration": true,
8
8
  "outDir": "./dist",
9
9
  "esModuleInterop": true,