mirror of
https://github.com/graphql-hive/console
synced 2026-04-21 14:37:17 +00:00
feat: supertokens at home (#7699)
This commit is contained in:
parent
fb0bea7bb3
commit
5f88ce8bd8
38 changed files with 3732 additions and 124 deletions
76
.changeset/light-lions-wear.md
Normal file
76
.changeset/light-lions-wear.md
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
'hive': minor
|
||||
---
|
||||
|
||||
Add experimental support for running without `supertokens` service.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Adjust your docker compose file like the following:
|
||||
- Remove `services.supertokens` from your `docker-compose.community.yml` file
|
||||
- Remove the following environment variables from the `services.server.environment`
|
||||
- `SUPERTOKENS_CONNECTION_URI=`
|
||||
- `SUPERTOKENS_API_KEY=`
|
||||
- Set the following environment variables for `services.server.environment`
|
||||
- `SUPERTOKENS_AT_HOME=1`
|
||||
- `SUPERTOKENS_REFRESH_TOKEN_KEY=`
|
||||
- `SUPERTOKENS_ACCESS_TOKEN_KEY=`
|
||||
- Set the following environment variables for `services.migrations.environment`
|
||||
- `SUPERTOKENS_AT_HOME=1`
|
||||
|
||||
### Set the refresh token key
|
||||
|
||||
#### Extract from existing `supertokens` deployment
|
||||
|
||||
This method works if you use supertokens before and want to have existing user sessions to continue working.
|
||||
If you want to avoid messing with the database, you can also create a new refresh token key from scratch, the drawback is that users are forced to login again.
|
||||
|
||||
Extract the refresh token key from the supertokens database
|
||||
```sql
|
||||
SELECT "value" FROM "supertokens_key_value" WHERE "name" = 'refresh_token_key';
|
||||
```
|
||||
|
||||
The key should look similar to this: `1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0`
|
||||
|
||||
Update the docker compose `services.server.environment.SUPERTOKENS_REFRESH_TOKEN_KEY` environment variable value to this string.
|
||||
|
||||
#### Create from scratch
|
||||
|
||||
Run the following command to create a new refresh key from scratch:
|
||||
|
||||
```sh
|
||||
echo "1000:$(openssl rand -hex 64):$(openssl rand -hex 64)"
|
||||
```
|
||||
|
||||
Update the docker compose `services.server.environment.SUPERTOKENS_REFRESH_TOKEN_KEY` environment variable value to this string.
|
||||
|
||||
### Set the access token key
|
||||
|
||||
Generate a new access token key using the following instructions:
|
||||
|
||||
```sh
|
||||
# 1. Generate a unique key name. 'uuidgen' is great for this.
|
||||
# You can replace this with any string you like, e.g., KEY_NAME="my-app-key-1"
|
||||
KEY_NAME=$(uuidgen)
|
||||
# 2. Generate a 2048-bit RSA private key in PEM format, held in memory.
|
||||
PRIVATE_KEY_PEM=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048)
|
||||
# 3. Extract the corresponding public key from the private key, also held in memory.
|
||||
PUBLIC_KEY_PEM=$(echo "$PRIVATE_KEY_PEM" | openssl rsa -pubout)
|
||||
# 4. Strip the headers/footers and newlines from the private key PEM
|
||||
# to get just the raw Base64 data.
|
||||
PRIVATE_KEY_DATA=$(echo "$PRIVATE_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n')
|
||||
# 5. Do the same for the public key PEM.
|
||||
PUBLIC_KEY_DATA=$(echo "$PUBLIC_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n')
|
||||
# 6. Echo the final formatted string to the console.
|
||||
echo "${KEY_NAME}|${PUBLIC_KEY_DATA}|${PRIVATE_KEY_DATA}"
|
||||
```
|
||||
|
||||
Update the docker compose `services.server.environment.SUPERTOKENS_ACCESS_TOKEN_KEY` environment variable value to the formatted string output.
|
||||
|
||||
## Conclusion
|
||||
|
||||
After performing this updates you can run Hive Console without the need for the `supertokens` service. All the relevant authentication logic resides within the `server` container instead.
|
||||
|
||||
Existing users in the supertokens system will continue to exist when running without the `supertokens` service.
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import 'reflect-metadata';
|
||||
import * as fs from 'node:fs';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies -- cypress SHOULD be a dev dependency
|
||||
import { defineConfig } from 'cypress';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,12 @@
|
|||
"compilerOptions": {
|
||||
"target": "es2021",
|
||||
"lib": ["es2021", "dom"],
|
||||
"types": ["node", "cypress"]
|
||||
"types": ["node", "cypress"],
|
||||
"baseUrl": "..",
|
||||
"paths": {
|
||||
"@hive/api/*": ["./packages/services/api/src/*"],
|
||||
"@hive/server/*": ["./packages/services/server/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "../integration-tests/testkit/**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,5 +37,9 @@ services:
|
|||
ports:
|
||||
- '3567:3567'
|
||||
|
||||
db:
|
||||
ports:
|
||||
- '5432:5432'
|
||||
|
||||
networks:
|
||||
stack: {}
|
||||
|
|
|
|||
|
|
@ -21,3 +21,6 @@ CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE=1000
|
|||
EXTERNAL_COMPOSITION_SECRET=secretsecret
|
||||
LIMIT_CACHE_UPDATE_INTERVAL_MS=2000
|
||||
NODE_OPTIONS=--enable-source-maps
|
||||
SUPERTOKENS_AT_HOME=0
|
||||
SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0
|
||||
SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdpx/LNIAPKDCNgszZIzz2tepThK9/3mCWTrOjAJY8KI8YgMo5Gf+IwYvJ2VcpKYa36UJ70bD283P2O/yE/IjtXI6S9cJyd5LrYs+QDENc8au63iDy2iAiFpR1kO7cWQPDKK3OrD+hc5ZEHA3LN82Kb4ZnEA6tAulPfULVDU+RJfSWZOCE+LnkSZ8obvJjiMeknhNSSJko6V3WVuL5ToYfRIiOnueoTywB+3O3Mtp6lBj1j2rpQfO/qvLdRYLpDmLaoaScAyymWfeBp0hpwxd5Jm4vexyHgit2sK0S+tFl0pmh37iVGsRqPJEPISpEwQBcHOhKRj0uW+t/feK6U0WQIDAQAB|MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN2nH8s0gA8oMI2CzNkjPPa16lOEr3/eYJZOs6MAljwojxiAyjkZ/4jBi8nZVykphrfpQnvRsPbzc/Y7/IT8iO1cjpL1wnJ3kutiz5AMQ1zxq7reIPLaICIWlHWQ7txZA8Morc6sP6FzlkQcDcs3zYpvhmcQDq0C6U99QtUNT5El9JZk4IT4ueRJnyhu8mOIx6SeE1JImSjpXdZW4vlOhh9EiI6e56hPLAH7c7cy2nqUGPWPaulB87+q8t1FgukOYtqhpJwDLKZZ94GnSGnDF3kmbi97HIeCK3awrRL60WXSmaHfuJUaxGo8kQ8hKkTBAFwc6EpGPS5b63994rpTRZAgMBAAECggEBAIl96/IFS4svg/Z0oah3RySKa2g1EeUhAXClkqIJoXBCRD3nomiAY8+i6u8Wxp4QnQ/D1pJV5v6ky6XzZxYezsQzTtNGBkolJn4yMZEAPy3wmXbD6VLQ5jCudb6kAaZRUaYnTxUlr+Kd1BDq8qZ4ik/sNuQEL+Fo+12EgPGTYXov7lWwWjNbzuzMQMm7b1BDU7D/8s/lGg4wimJffVSd4C++buN4Jxm1n1hWWREl7jkJC0sp2J50cpt9IhIIhi8DOnGAcJ4aTtABEJdZyXlO0QllN/D5FEbZBC0Jkbl3lmaIo1WVEYdDpcbSLZxGeYD0CkH4CF/BzUpeHq7FU0HkqOkCgYEA5tgLRFC4CS/FtR3JQ1YifHN4J2bI3BEyje6eiI/n9jmm5zUN/WjCQ6ui2fPzbKAC3yD60lcCgrP7YAn4KFmOv9RS7ApamUH+AX5UBfHlhvwzi4U9eenu7UHH8XrxEHlAwUC9mQbaqzoR/A7jEg8qqincMDUCkk1kjP4nNgQSBFcCgYEAnU/KSQf79m57nXMv+ge9QWkAggnHO7baRox5qlMM6l1gTZB5yELaLNXeik9D2mhVwcpezcQo2Uf+B4MviVeqpoTzYwgdxYg+ebYPUzhd3Ya8ANRDwKCB7SSoRULEDpWebV6ngOc+ruv9ii3ZbVEi7ribtHo6w7rVVJc2bMEKns8CgYB9ssp/yoxLxE2dz7hWCEMDDUUx/1AENQEYNATzS5j9hGsTntodULvnaUBl+eZlEcQ+h5DMlEBzt1l79DHClvGaFx2IFiM7LKoJWiaajhtzo0TWBhlxlyZY3ubm4RD+7WeLU5tqBkdv0VEVtW2D2epbeivBvDvIOog0Ffh3+0NsRQKBgGPA8w84hugPy0dega/VNIfD49SSCsqs+uD9tzDwlSIQsD6/PNpmuh7wR7wA45Ad1TOb9l4Y46ZU5psw7vXyp34MlKHZxbc63BMmBbXJ6ovNIm6MK6J8pacRNbslyVlOOzYzbZhqCu+1KgNza4rMhpBGdEYPtC/ly91mPdbc2rU1AoGBALl6eZqBMahE0S19X5SO/xykGe4ALf74UWCsfKrVEO4Zd3IcELnir0uEPABWvd5C/EAaGoi/a2xgdwuKG32GMpinjoXJywzsquQC6N8CcFIzDiQXaL4j4lFztjgowqNs/YwpOGbm1Dyr3Av072jDPajQGP/xX4fFxBZFnyk1vnXT
|
||||
|
|
|
|||
|
|
@ -24,4 +24,7 @@ applyEnv({
|
|||
CLICKHOUSE_USER: serverEnvVars.CLICKHOUSE_USERNAME,
|
||||
CLICKHOUSE_PASSWORD: serverEnvVars.CLICKHOUSE_PASSWORD,
|
||||
HIVE_ENCRYPTION_SECRET: serverEnvVars.HIVE_ENCRYPTION_SECRET,
|
||||
SUPERTOKENS_AT_HOME: serverEnvVars.SUPERTOKENS_AT_HOME,
|
||||
SUPERTOKENS_REFRESH_TOKEN_KEY: serverEnvVars.SUPERTOKENS_REFRESH_TOKEN_KEY,
|
||||
SUPERTOKENS_ACCESS_TOKEN_KEY: serverEnvVars.SUPERTOKENS_ACCESS_TOKEN_KEY,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import { DatabasePool } from 'slonik';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AccessTokenKeyContainer,
|
||||
hashPassword,
|
||||
} from '@hive/api/modules/auth/lib/supertokens-at-home/crypto';
|
||||
import { SuperTokensStore } from '@hive/api/modules/auth/providers/supertokens-store';
|
||||
import { NoopLogger } from '@hive/api/modules/shared/providers/logger';
|
||||
import type { InternalApi } from '@hive/server';
|
||||
import { createNewSession } from '@hive/server/supertokens-at-home/shared';
|
||||
import { createTRPCProxyClient, httpLink } from '@trpc/client';
|
||||
import { ensureEnv } from './env';
|
||||
import { getServiceHost } from './utils';
|
||||
|
|
@ -57,6 +65,56 @@ const signUpUserViaEmail = async (
|
|||
}
|
||||
};
|
||||
|
||||
const createSessionAtHome = async (
|
||||
supertokensStore: SuperTokensStore,
|
||||
superTokensUserId: string,
|
||||
email: string,
|
||||
oidcIntegrationId: string | null,
|
||||
) => {
|
||||
const graphqlAddress = await getServiceHost('server', 8082);
|
||||
|
||||
const internalApi = createTRPCProxyClient<InternalApi>({
|
||||
links: [
|
||||
httpLink({
|
||||
url: `http://${graphqlAddress}/trpc`,
|
||||
fetch,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
const ensureUserResult = await internalApi.ensureUser.mutate({
|
||||
superTokensUserId,
|
||||
email,
|
||||
oidcIntegrationId,
|
||||
firstName: null,
|
||||
lastName: null,
|
||||
});
|
||||
if (!ensureUserResult.ok) {
|
||||
throw new Error(ensureUserResult.reason);
|
||||
}
|
||||
|
||||
const session = await createNewSession(
|
||||
supertokensStore,
|
||||
{
|
||||
superTokensUserId,
|
||||
hiveUser: ensureUserResult.user,
|
||||
oidcIntegrationId,
|
||||
},
|
||||
{
|
||||
refreshTokenKey: process.env.SUPERTOKENS_REFRESH_TOKEN_KEY!,
|
||||
accessTokenKey: new AccessTokenKeyContainer(process.env.SUPERTOKENS_ACCESS_TOKEN_KEY!),
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* These are the required cookies that need to be set.
|
||||
*/
|
||||
return {
|
||||
access_token: session.accessToken.token,
|
||||
refresh_token: session.refreshToken,
|
||||
};
|
||||
};
|
||||
|
||||
const createSessionPayload = (payload: {
|
||||
superTokensUserId: string;
|
||||
userId: string;
|
||||
|
|
@ -155,31 +213,51 @@ const createSession = async (
|
|||
};
|
||||
|
||||
const password = 'ilikebigturtlesandicannotlie47';
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
export function userEmail(userId: string) {
|
||||
return `${userId}-${Date.now()}@localhost.localhost`;
|
||||
}
|
||||
|
||||
const tokenResponsePromise: {
|
||||
[key: string]: Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> | null;
|
||||
[key: string]: Promise<{
|
||||
userId: string;
|
||||
email: string;
|
||||
}> | null;
|
||||
} = {};
|
||||
|
||||
export function authenticate(
|
||||
export async function authenticate(
|
||||
pool: DatabasePool,
|
||||
email: string,
|
||||
): Promise<{ access_token: string; refresh_token: string }>;
|
||||
export function authenticate(
|
||||
email: string,
|
||||
oidcIntegrationId?: string,
|
||||
): Promise<{ access_token: string; refresh_token: string }>;
|
||||
export function authenticate(
|
||||
email: string | string,
|
||||
oidcIntegrationId?: string,
|
||||
): Promise<{ access_token: string; refresh_token: string }> {
|
||||
if (process.env.SUPERTOKENS_AT_HOME === '1') {
|
||||
const supertokensStore = new SuperTokensStore(pool, new NoopLogger());
|
||||
if (!tokenResponsePromise[email]) {
|
||||
tokenResponsePromise[email] = supertokensStore.createEmailPasswordUser({
|
||||
email,
|
||||
passwordHash: hashedPassword,
|
||||
});
|
||||
}
|
||||
|
||||
const user = await tokenResponsePromise[email]!;
|
||||
|
||||
return await createSessionAtHome(
|
||||
supertokensStore,
|
||||
user.userId,
|
||||
email,
|
||||
oidcIntegrationId ?? null,
|
||||
);
|
||||
}
|
||||
|
||||
if (!tokenResponsePromise[email]) {
|
||||
tokenResponsePromise[email] = signUpUserViaEmail(email, password);
|
||||
tokenResponsePromise[email] = signUpUserViaEmail(email, password).then(res => ({
|
||||
email: res.user.email,
|
||||
userId: res.user.id,
|
||||
}));
|
||||
}
|
||||
|
||||
return tokenResponsePromise[email]!.then(data =>
|
||||
createSession(data.user.id, data.user.email, oidcIntegrationId ?? null),
|
||||
createSession(data.userId, data.email, oidcIntegrationId ?? null),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,32 +62,44 @@ import { UpdateSchemaPolicyForOrganization, UpdateSchemaPolicyForProject } from
|
|||
import { collect, CollectedOperation, legacyCollect } from './usage';
|
||||
import { generateUnique, getServiceHost } from './utils';
|
||||
|
||||
export function initSeed() {
|
||||
function createConnectionPool() {
|
||||
const pg = {
|
||||
user: ensureEnv('POSTGRES_USER'),
|
||||
password: ensureEnv('POSTGRES_PASSWORD'),
|
||||
host: ensureEnv('POSTGRES_HOST'),
|
||||
port: ensureEnv('POSTGRES_PORT'),
|
||||
db: ensureEnv('POSTGRES_DB'),
|
||||
};
|
||||
function createConnectionPool() {
|
||||
const pg = {
|
||||
user: ensureEnv('POSTGRES_USER'),
|
||||
password: ensureEnv('POSTGRES_PASSWORD'),
|
||||
host: ensureEnv('POSTGRES_HOST'),
|
||||
port: ensureEnv('POSTGRES_PORT'),
|
||||
db: ensureEnv('POSTGRES_DB'),
|
||||
};
|
||||
|
||||
return createPool(
|
||||
`postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`,
|
||||
);
|
||||
return createPool(
|
||||
`postgres://${pg.user}:${pg.password}@${pg.host}:${pg.port}/${pg.db}?sslmode=disable`,
|
||||
);
|
||||
}
|
||||
|
||||
async function createDbConnection() {
|
||||
const pool = await createConnectionPool();
|
||||
return {
|
||||
pool,
|
||||
[Symbol.asyncDispose]: async () => {
|
||||
await pool.end();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function initSeed() {
|
||||
let sharedDBPoolPromise: ReturnType<typeof createDbConnection>;
|
||||
|
||||
async function doAuthenticate(email: string, oidcIntegrationId?: string) {
|
||||
if (!sharedDBPoolPromise) {
|
||||
sharedDBPoolPromise = createDbConnection();
|
||||
}
|
||||
const sharedPool = await sharedDBPoolPromise;
|
||||
return await authenticate(sharedPool.pool, email, oidcIntegrationId);
|
||||
}
|
||||
|
||||
return {
|
||||
async createDbConnection() {
|
||||
const pool = await createConnectionPool();
|
||||
return {
|
||||
pool,
|
||||
[Symbol.asyncDispose]: async () => {
|
||||
await pool.end();
|
||||
},
|
||||
};
|
||||
},
|
||||
authenticate,
|
||||
createDbConnection,
|
||||
authenticate: doAuthenticate,
|
||||
generateEmail: () => userEmail(generateUnique()),
|
||||
async purgeOrganizationAccessTokenById(id: string) {
|
||||
const registryAddress = await getServiceHost('server', 8082);
|
||||
|
|
@ -100,7 +112,7 @@ export function initSeed() {
|
|||
},
|
||||
async createOwner() {
|
||||
const ownerEmail = userEmail(generateUnique());
|
||||
const auth = await authenticate(ownerEmail);
|
||||
const auth = await doAuthenticate(ownerEmail);
|
||||
const ownerRefreshToken = auth.refresh_token;
|
||||
const ownerToken = auth.access_token;
|
||||
|
||||
|
|
@ -904,7 +916,7 @@ export function initSeed() {
|
|||
},
|
||||
);
|
||||
const memberEmail = userEmail(generateUnique());
|
||||
const memberToken = await authenticate(memberEmail, oidcIntegrationId).then(
|
||||
const memberToken = await doAuthenticate(memberEmail, oidcIntegrationId).then(
|
||||
r => r.access_token,
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,14 @@ export default defineConfig({
|
|||
'../packages/services/service-common/src/index.ts',
|
||||
import.meta.url,
|
||||
).pathname,
|
||||
'@hive/server/supertokens-at-home/shared': new URL(
|
||||
'../packages/services/server/src/supertokens-at-home/shared.ts',
|
||||
import.meta.url,
|
||||
).pathname,
|
||||
'@hive/api/modules/auth/lib/supertokens-at-home/crypto': new URL(
|
||||
'../packages/services/api/src/modules/auth/lib/supertokens-at-home/crypto.ts',
|
||||
import.meta.url,
|
||||
).pathname,
|
||||
},
|
||||
setupFiles,
|
||||
testTimeout: 90_000,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,168 @@
|
|||
import { type MigrationExecutor } from '../pg-migrator';
|
||||
|
||||
export default {
|
||||
name: '2026.02.18T00-00-00.ensure-supertokens-tables.ts',
|
||||
run: ({ sql }) => [
|
||||
{
|
||||
name: 'seed required tables',
|
||||
query: sql`
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_apps (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
created_at_time int8 NULL,
|
||||
CONSTRAINT supertokens_apps_pkey PRIMARY KEY (app_id)
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_tenants (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
tenant_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
created_at_time int8 NULL,
|
||||
CONSTRAINT supertokens_tenants_pkey PRIMARY KEY (app_id, tenant_id),
|
||||
CONSTRAINT supertokens_tenants_app_id_fkey FOREIGN KEY (app_id) REFERENCES supertokens_apps(app_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS tenants_app_id_index ON supertokens_tenants USING btree (app_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_session_info (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
tenant_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
session_handle varchar(255) NOT NULL,
|
||||
user_id varchar(128) NOT NULL,
|
||||
refresh_token_hash_2 varchar(128) NOT NULL,
|
||||
session_data text NULL,
|
||||
expires_at int8 NOT NULL,
|
||||
created_at_time int8 NOT NULL,
|
||||
jwt_user_payload text NULL,
|
||||
use_static_key bool NOT NULL,
|
||||
CONSTRAINT supertokens_session_info_pkey PRIMARY KEY (app_id, tenant_id, session_handle),
|
||||
CONSTRAINT supertokens_session_info_tenant_id_fkey FOREIGN KEY (app_id, tenant_id) REFERENCES supertokens_tenants(app_id,tenant_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS session_expiry_index ON supertokens_session_info USING btree (expires_at);
|
||||
CREATE INDEX IF NOT EXISTS session_info_tenant_id_index ON supertokens_session_info USING btree (app_id, tenant_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_app_id_to_user_id (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
recipe_id varchar(128) NOT NULL,
|
||||
primary_or_recipe_user_id bpchar(36) NOT NULL,
|
||||
is_linked_or_is_a_primary_user bool DEFAULT false NOT NULL,
|
||||
CONSTRAINT supertokens_app_id_to_user_id_pkey PRIMARY KEY (app_id, user_id),
|
||||
CONSTRAINT supertokens_app_id_to_user_id_app_id_fkey FOREIGN KEY (app_id) REFERENCES supertokens_apps(app_id) ON DELETE CASCADE,
|
||||
CONSTRAINT supertokens_app_id_to_user_id_primary_or_recipe_user_id_fkey FOREIGN KEY (app_id,primary_or_recipe_user_id) REFERENCES supertokens_app_id_to_user_id(app_id,user_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS app_id_to_user_id_app_id_index ON supertokens_app_id_to_user_id USING btree (app_id);
|
||||
CREATE INDEX IF NOT EXISTS app_id_to_user_id_primary_user_id_index ON supertokens_app_id_to_user_id USING btree (primary_or_recipe_user_id, app_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_emailpassword_users (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
email varchar(256) NOT NULL,
|
||||
password_hash varchar(256) NOT NULL,
|
||||
time_joined int8 NOT NULL,
|
||||
CONSTRAINT supertokens_emailpassword_users_pkey PRIMARY KEY (app_id, user_id),
|
||||
CONSTRAINT supertokens_emailpassword_users_user_id_fkey FOREIGN KEY (app_id,user_id) REFERENCES supertokens_app_id_to_user_id(app_id,user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_thirdparty_users (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
third_party_id varchar(28) NOT NULL,
|
||||
third_party_user_id varchar(256) NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
email varchar(256) NOT NULL,
|
||||
time_joined int8 NOT NULL,
|
||||
CONSTRAINT supertokens_thirdparty_users_pkey PRIMARY KEY (app_id, user_id),
|
||||
CONSTRAINT supertokens_thirdparty_users_user_id_fkey FOREIGN KEY (app_id,user_id) REFERENCES supertokens_app_id_to_user_id(app_id,user_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS thirdparty_users_email_index ON supertokens_thirdparty_users USING btree (app_id, email);
|
||||
CREATE INDEX IF NOT EXISTS thirdparty_users_thirdparty_user_id_index ON supertokens_thirdparty_users USING btree (app_id, third_party_id, third_party_user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_all_auth_recipe_users (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
tenant_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
primary_or_recipe_user_id bpchar(36) NOT NULL,
|
||||
is_linked_or_is_a_primary_user bool DEFAULT false NOT NULL,
|
||||
recipe_id varchar(128) NOT NULL,
|
||||
time_joined int8 NOT NULL,
|
||||
primary_or_recipe_user_time_joined int8 NOT NULL,
|
||||
CONSTRAINT supertokens_all_auth_recipe_users_pkey PRIMARY KEY (app_id, tenant_id, user_id),
|
||||
CONSTRAINT supertokens_all_auth_recipe_users_primary_or_recipe_user_id_fke FOREIGN KEY (app_id,primary_or_recipe_user_id) REFERENCES supertokens_app_id_to_user_id(app_id,user_id) ON DELETE CASCADE,
|
||||
CONSTRAINT supertokens_all_auth_recipe_users_tenant_id_fkey FOREIGN KEY (app_id,tenant_id) REFERENCES supertokens_tenants(app_id,tenant_id) ON DELETE CASCADE,
|
||||
CONSTRAINT supertokens_all_auth_recipe_users_user_id_fkey FOREIGN KEY (app_id,user_id) REFERENCES supertokens_app_id_to_user_id(app_id,user_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_tenant_id_index ON supertokens_all_auth_recipe_users USING btree (app_id, tenant_id);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_user_id_index ON supertokens_all_auth_recipe_users USING btree (app_id, user_id);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_users_pagination_index1 ON supertokens_all_auth_recipe_users USING btree (app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_users_pagination_index2 ON supertokens_all_auth_recipe_users USING btree (app_id, tenant_id, primary_or_recipe_user_time_joined, primary_or_recipe_user_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_users_pagination_index3 ON supertokens_all_auth_recipe_users USING btree (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined DESC, primary_or_recipe_user_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_users_pagination_index4 ON supertokens_all_auth_recipe_users USING btree (recipe_id, app_id, tenant_id, primary_or_recipe_user_time_joined, primary_or_recipe_user_id DESC);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_users_primary_user_id_index ON supertokens_all_auth_recipe_users USING btree (primary_or_recipe_user_id, app_id);
|
||||
CREATE INDEX IF NOT EXISTS all_auth_recipe_users_recipe_id_index ON supertokens_all_auth_recipe_users USING btree (app_id, recipe_id, tenant_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_thirdparty_user_to_tenant (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
tenant_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
third_party_id varchar(28) NOT NULL,
|
||||
third_party_user_id varchar(256) NOT NULL,
|
||||
CONSTRAINT supertokens_thirdparty_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id),
|
||||
CONSTRAINT supertokens_thirdparty_user_to_tenant_third_party_user_id_key UNIQUE (app_id, tenant_id, third_party_id, third_party_user_id),
|
||||
CONSTRAINT supertokens_thirdparty_user_to_tenant_user_id_fkey FOREIGN KEY (app_id,tenant_id,user_id) REFERENCES supertokens_all_auth_recipe_users(app_id,tenant_id,user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_emailpassword_user_to_tenant (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
tenant_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
email varchar(256) NOT NULL,
|
||||
CONSTRAINT supertokens_emailpassword_user_to_tenant_email_key UNIQUE (app_id, tenant_id, email),
|
||||
CONSTRAINT supertokens_emailpassword_user_to_tenant_pkey PRIMARY KEY (app_id, tenant_id, user_id),
|
||||
CONSTRAINT supertokens_emailpassword_user_to_tenant_user_id_fkey FOREIGN KEY (app_id,tenant_id,user_id) REFERENCES supertokens_all_auth_recipe_users(app_id,tenant_id,user_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS supertokens_emailpassword_pswd_reset_tokens (
|
||||
app_id varchar(64) DEFAULT 'public'::character varying NOT NULL,
|
||||
user_id bpchar(36) NOT NULL,
|
||||
"token" varchar(128) NOT NULL,
|
||||
email varchar(256) NULL,
|
||||
token_expiry int8 NOT NULL,
|
||||
CONSTRAINT supertokens_emailpassword_pswd_reset_tokens_pkey PRIMARY KEY (app_id, user_id, token),
|
||||
CONSTRAINT supertokens_emailpassword_pswd_reset_tokens_token_key UNIQUE (token),
|
||||
CONSTRAINT supertokens_emailpassword_pswd_reset_tokens_user_id_fkey FOREIGN KEY (app_id,user_id) REFERENCES supertokens_app_id_to_user_id(app_id,user_id) ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS emailpassword_password_reset_token_expiry_index ON supertokens_emailpassword_pswd_reset_tokens USING btree (token_expiry);
|
||||
CREATE INDEX IF NOT EXISTS emailpassword_pswd_reset_tokens_user_id_index ON supertokens_emailpassword_pswd_reset_tokens USING btree (app_id, user_id);
|
||||
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'ensure app exists',
|
||||
query: sql`
|
||||
INSERT INTO "supertokens_apps" (
|
||||
app_id
|
||||
, created_at_time
|
||||
) VALUES (
|
||||
'public'
|
||||
, ${Date.now()}
|
||||
)
|
||||
ON CONFLICT (app_id) DO NOTHING
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'ensure app exists',
|
||||
query: sql`
|
||||
INSERT INTO "supertokens_tenants" (
|
||||
app_id
|
||||
, tenant_id
|
||||
, created_at_time
|
||||
) VALUES (
|
||||
'public'
|
||||
, 'public'
|
||||
, ${Date.now()}
|
||||
)
|
||||
ON CONFLICT (app_id, tenant_id) DO NOTHING;
|
||||
`,
|
||||
},
|
||||
],
|
||||
} satisfies MigrationExecutor;
|
||||
|
|
@ -34,6 +34,7 @@ const EnvironmentModel = zod.object({
|
|||
.union([zod.literal('1'), zod.literal('0')])
|
||||
.optional(),
|
||||
GRAPHQL_HIVE_ENVIRONMENT: emptyString(zod.enum(['prod', 'staging', 'dev']).optional()),
|
||||
SUPERTOKENS_AT_HOME: zod.union([zod.literal('1'), zod.literal('0')]).optional(),
|
||||
});
|
||||
|
||||
const PostgresModel = zod.object({
|
||||
|
|
@ -114,4 +115,5 @@ export const env = {
|
|||
isClickHouseMigrator: base.CLICKHOUSE_MIGRATOR === 'up',
|
||||
isHiveCloud: base.CLICKHOUSE_MIGRATOR_GRAPHQL_HIVE_CLOUD === '1',
|
||||
hiveCloudEnvironment: base.GRAPHQL_HIVE_ENVIRONMENT ?? null,
|
||||
useSupertokensAtHome: base.SUPERTOKENS_AT_HOME === '1',
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ import migration_2024_06_11T10_10_00_ms_teams_webhook from './actions/2024.06.11
|
|||
import migration_2024_07_16T13_44_00_oidc_only_access from './actions/2024.07.16T13-44-00.oidc-only-access';
|
||||
import migration_2024_07_17T00_00_00_app_deployments from './actions/2024.07.17T00-00-00.app-deployments';
|
||||
import migration_2024_07_23T_09_36_00_schema_cleanup_tracker from './actions/2024.07.23T09.36.00.schema-cleanup-tracker';
|
||||
import { env } from './environment';
|
||||
import { runMigrations } from './pg-migrator';
|
||||
|
||||
export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: string }) =>
|
||||
|
|
@ -183,5 +184,8 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
|
|||
await import('./actions/2026.01.30T00-00-00.account-linking'),
|
||||
await import('./actions/2026.02.06T00-00-00.zendesk-unique'),
|
||||
await import('./actions/2026.01.30T10-00-00.oidc-require-invitation'),
|
||||
...(env.useSupertokensAtHome
|
||||
? [await import('./actions/2026.02.18T00-00-00.ensure-supertokens-tables')]
|
||||
: []),
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,11 @@
|
|||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"exports": {
|
||||
"./modules/auth/lib/supertokens-at-home/crypto": "./src/modules/auth/lib/supertokens-at-home/crypto.ts",
|
||||
"./modules/auth/providers/supertokens-store": "./src/modules/auth/providers/supertokens-store.ts",
|
||||
"./modules/shared/providers/logger": "./src/modules/shared/providers/logger"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"graphql": "^16.0.0",
|
||||
"reflect-metadata": "0.2.2"
|
||||
|
|
@ -57,6 +62,7 @@
|
|||
"graphql-yoga": "5.13.3",
|
||||
"ioredis": "5.8.2",
|
||||
"ioredis-mock": "8.9.0",
|
||||
"jsonwebtoken": "9.0.3",
|
||||
"lodash": "4.17.23",
|
||||
"lru-cache": "7.18.3",
|
||||
"ms": "2.1.3",
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ import { PG_POOL_CONFIG } from './modules/shared/providers/pg-pool';
|
|||
import { PrometheusConfig } from './modules/shared/providers/prometheus-config';
|
||||
import { HivePubSub, PUB_SUB_CONFIG } from './modules/shared/providers/pub-sub';
|
||||
import { REDIS_INSTANCE } from './modules/shared/providers/redis';
|
||||
import { RedisRateLimiter } from './modules/shared/providers/redis-rate-limiter';
|
||||
import { S3_CONFIG, type S3Config } from './modules/shared/providers/s3-config';
|
||||
import { Storage } from './modules/shared/providers/storage';
|
||||
import { RateLimitConfig, WEB_APP_URL } from './modules/shared/providers/tokens';
|
||||
|
|
@ -218,6 +219,7 @@ export function createRegistry({
|
|||
CryptoProvider,
|
||||
InMemoryRateLimitStore,
|
||||
InMemoryRateLimiter,
|
||||
RedisRateLimiter,
|
||||
{
|
||||
provide: AuditLogS3Config,
|
||||
useValue: auditLogS3Config,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { createModule } from 'graphql-modules';
|
|||
import { AuditLogManager } from '../audit-logs/providers/audit-logs-manager';
|
||||
import { AuthManager } from './providers/auth-manager';
|
||||
import { EmailVerification } from './providers/email-verification';
|
||||
import { OAuthCache } from './providers/oauth-cache';
|
||||
import { OrganizationAccessTokenValidationCache } from './providers/organization-access-token-validation-cache';
|
||||
import { UserManager } from './providers/user-manager';
|
||||
import { resolvers } from './resolvers.generated';
|
||||
|
|
@ -18,5 +19,6 @@ export const authModule = createModule({
|
|||
UserManager,
|
||||
AuditLogManager,
|
||||
OrganizationAccessTokenValidationCache,
|
||||
OAuthCache,
|
||||
],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,263 @@
|
|||
/**
|
||||
* A collection of supertokens crypto utilities around.
|
||||
* - Refresh Tokens
|
||||
* - Access Tokens
|
||||
* - Front Tokens
|
||||
*/
|
||||
|
||||
import * as c from 'node:crypto';
|
||||
import { promisify } from 'node:util';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import z from 'zod';
|
||||
|
||||
export async function hashPassword(plaintextPassword: string): Promise<string> {
|
||||
// The "cost factor" or salt rounds. 10 is a good, standard balance of security and performance.
|
||||
// This value is included in the final hash string itself.
|
||||
const saltRounds = 10;
|
||||
|
||||
// bcrypt.hash handles the generation of a random salt and the hashing process.
|
||||
// The operation is asynchronous to prevent blocking the event loop.
|
||||
const hash = await bcrypt.hash(plaintextPassword, saltRounds);
|
||||
|
||||
return hash;
|
||||
}
|
||||
|
||||
export async function comparePassword(password: string, hash: string) {
|
||||
return await bcrypt.compare(password, hash);
|
||||
}
|
||||
|
||||
const pbkdf2Async = promisify(c.pbkdf2);
|
||||
|
||||
async function decryptRefreshToken(encodedData: string, masterKey: string) {
|
||||
// 1. Decode the incoming string (URL -> Base64 -> Buffer).
|
||||
const urlDecodedData = decodeURIComponent(encodedData);
|
||||
const buffer = Buffer.from(urlDecodedData, 'base64');
|
||||
|
||||
// 2. Deconstruct the buffer based on the Java encryption logic.
|
||||
// The first 12 bytes are the IV.
|
||||
const iv = buffer.slice(0, 12);
|
||||
|
||||
// The rest of the buffer is the encrypted data + 16-byte auth tag.
|
||||
const encryptedPayload = buffer.slice(12);
|
||||
|
||||
// 3. Re-derive the secret key using PBKDF2. This is the critical step.
|
||||
// The parameters MUST match the Java side exactly.
|
||||
// The IV is used as the salt.
|
||||
const iterations = 100;
|
||||
const keylen = 32; // 32 bytes = 256 bits
|
||||
const digest = 'sha512'; // NOTE: This is a guess. See explanation below.
|
||||
|
||||
const secretKey = await pbkdf2Async(masterKey, iv, iterations, keylen, digest);
|
||||
|
||||
// 4. Separate the encrypted data from the authentication tag.
|
||||
const authTagLength = 16; // 128 bits
|
||||
const encryptedData = encryptedPayload.slice(0, -authTagLength);
|
||||
const authTag = encryptedPayload.slice(-authTagLength);
|
||||
|
||||
// 5. Perform the decryption with the derived key and IV.
|
||||
const decipher = c.createDecipheriv('aes-256-gcm', secretKey, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
return Buffer.concat([decipher.update(encryptedData), decipher.final()]).toString('utf-8');
|
||||
}
|
||||
|
||||
function encryptRefreshToken(plaintext: string, masterKey: string) {
|
||||
// 1. Generate a random 12-byte IV (Initialization Vector), same as the Java side.
|
||||
const iv = c.randomBytes(12);
|
||||
|
||||
// 2. Derive the secret key using PBKDF2. The IV is used as the salt.
|
||||
// The parameters (iterations, key length, and digest) match the Java implementation.
|
||||
const iterations = 100;
|
||||
const keylen = 32; // 32 bytes = 256 bits
|
||||
const digest = 'sha512'; // From "PBKDF2WithHmacSHA512"
|
||||
|
||||
const secretKey = c.pbkdf2Sync(masterKey, iv, iterations, keylen, digest);
|
||||
|
||||
// 3. Create the AES-256-GCM cipher.
|
||||
const cipher = c.createCipheriv('aes-256-gcm', secretKey, iv);
|
||||
|
||||
// 4. Encrypt the plaintext.
|
||||
// The result is the ciphertext.
|
||||
let encrypted = cipher.update(plaintext, 'utf8');
|
||||
encrypted = Buffer.concat([encrypted, cipher.final()]);
|
||||
|
||||
// 5. Get the 16-byte authentication tag.
|
||||
// This is a crucial step in GCM.
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
// 6. Concatenate everything in the correct order: IV + Ciphertext + Auth Tag.
|
||||
// This matches the Java `ByteBuffer` logic.
|
||||
const finalBuffer = Buffer.concat([iv, encrypted, authTag]);
|
||||
|
||||
// 7. Base64-encode the final buffer
|
||||
const base64Data = finalBuffer.toString('base64');
|
||||
return base64Data;
|
||||
}
|
||||
|
||||
export function createRefreshToken(
|
||||
args: {
|
||||
sessionHandle: string;
|
||||
userId: string;
|
||||
parentRefreshTokenHash1: string | null;
|
||||
},
|
||||
masterKey: string,
|
||||
) {
|
||||
const newNonce = sha256(crypto.randomUUID());
|
||||
|
||||
const encryptedPayload = encryptRefreshToken(
|
||||
JSON.stringify({
|
||||
sessionHandle: args.sessionHandle,
|
||||
userId: args.userId,
|
||||
nonce: newNonce,
|
||||
parentRefreshTokenHash1: args.parentRefreshTokenHash1 ?? undefined,
|
||||
}),
|
||||
masterKey,
|
||||
);
|
||||
|
||||
const refreshToken = encryptedPayload + '.' + newNonce + '.' + 'V2';
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
const RefreshTokenPayloadModel = z.object({
|
||||
sessionHandle: z.string(),
|
||||
userId: z.string(),
|
||||
parentRefreshTokenHash1: z.string().optional(),
|
||||
nonce: z.string(),
|
||||
});
|
||||
|
||||
type RefreshTokenPayloadType = z.TypeOf<typeof RefreshTokenPayloadModel>;
|
||||
|
||||
export async function parseRefreshToken(refreshToken: string, masterKey: string) {
|
||||
const [payload, nonce, version] = refreshToken.split('.');
|
||||
|
||||
if (version !== 'V2') {
|
||||
return {
|
||||
type: 'error',
|
||||
code: 'ERR_INVALID_FORMAT',
|
||||
} as const;
|
||||
}
|
||||
|
||||
let refreshTokenPayload: RefreshTokenPayloadType;
|
||||
try {
|
||||
refreshTokenPayload = RefreshTokenPayloadModel.parse(
|
||||
JSON.parse(await decryptRefreshToken(payload, masterKey)),
|
||||
);
|
||||
} catch (err) {
|
||||
return {
|
||||
type: 'error',
|
||||
code: 'ERR_INVALID_PAYLOAD',
|
||||
} as const;
|
||||
}
|
||||
|
||||
if (refreshTokenPayload.nonce !== nonce) {
|
||||
return {
|
||||
type: 'error',
|
||||
code: 'ERR_INVALID_NONCE',
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'success',
|
||||
payload: refreshTokenPayload,
|
||||
} as const;
|
||||
}
|
||||
|
||||
export function sha256(str: string) {
|
||||
return c.createHash('sha256').update(str).digest('hex');
|
||||
}
|
||||
|
||||
export function createAccessToken(
|
||||
args: {
|
||||
sub: string;
|
||||
sessionHandle: string;
|
||||
refreshTokenHash1: string;
|
||||
parentRefreshTokenHash1: string | null;
|
||||
sessionData: Record<string, unknown>;
|
||||
},
|
||||
accessTokenKey: AccessTokenKeyContainer,
|
||||
) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
// Access tokens expires in 6 hours
|
||||
const expiresIn = Math.floor(now + 60 * 60 * 6 * 1000);
|
||||
|
||||
const data: AccessTokenInfo = {
|
||||
iat: now,
|
||||
exp: expiresIn,
|
||||
sub: args.sub,
|
||||
tId: 'public',
|
||||
rsub: args.sub,
|
||||
sessionHandle: args.sessionHandle,
|
||||
antiCsrfToken: null,
|
||||
refreshTokenHash1: args.refreshTokenHash1,
|
||||
parentRefreshTokenHash1: args.parentRefreshTokenHash1 ?? undefined,
|
||||
...args.sessionData,
|
||||
};
|
||||
|
||||
const token = jwt.sign(data, accessTokenKey.privateKey, {
|
||||
header: {
|
||||
kid: accessTokenKey.keyId,
|
||||
typ: 'JWT',
|
||||
alg: 'RS256',
|
||||
},
|
||||
});
|
||||
|
||||
return { token, expiresIn, d: jwt.decode(token) };
|
||||
}
|
||||
|
||||
export function isAccessToken(accessToken: string) {
|
||||
return !!jwt.decode(accessToken);
|
||||
}
|
||||
|
||||
export function parseAccessToken(accessToken: string, accessTokenPublicKey: string) {
|
||||
const token = jwt.verify(accessToken, accessTokenPublicKey, {
|
||||
algorithms: ['RS256'],
|
||||
});
|
||||
|
||||
return AccessTokenInfoModel.parse(token);
|
||||
}
|
||||
|
||||
export function createFrontToken(args: {
|
||||
superTokensUserId: string;
|
||||
accessToken: ReturnType<typeof createAccessToken>;
|
||||
}) {
|
||||
return Buffer.from(
|
||||
JSON.stringify({
|
||||
uid: args.superTokensUserId,
|
||||
ate: args.accessToken.expiresIn * 1000,
|
||||
up: args.accessToken.d,
|
||||
}),
|
||||
).toString('base64');
|
||||
}
|
||||
|
||||
const AccessTokenInfoModel = z.object({
|
||||
iat: z.number(),
|
||||
exp: z.number(),
|
||||
sub: z.string(),
|
||||
tId: z.string(),
|
||||
rsub: z.string(),
|
||||
sessionHandle: z.string(),
|
||||
refreshTokenHash1: z.string(),
|
||||
parentRefreshTokenHash1: z.string().optional(), // Making this optional as it may not always be present
|
||||
antiCsrfToken: z.string().nullable(),
|
||||
});
|
||||
|
||||
type AccessTokenInfo = z.TypeOf<typeof AccessTokenInfoModel>;
|
||||
|
||||
export function getPasswordResetHash() {
|
||||
return c.randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
export class AccessTokenKeyContainer {
|
||||
readonly keyId: string;
|
||||
readonly publicKey: string;
|
||||
readonly privateKey: string;
|
||||
|
||||
constructor(accessTokenKey: string) {
|
||||
const [keyName, publicKey, privateKey] = accessTokenKey.split('|');
|
||||
this.keyId = keyName;
|
||||
this.publicKey = `-----BEGIN PUBLIC KEY-----\n` + publicKey + `\n-----END PUBLIC KEY-----`;
|
||||
this.privateKey = `-----BEGIN PRIVATE KEY-----\n` + privateKey + `\n-----END PRIVATE KEY-----`;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
import c from 'node:crypto';
|
||||
import { parse as parseCookie } from 'cookie-es';
|
||||
import SessionNode from 'supertokens-node/recipe/session/index.js';
|
||||
import * as zod from 'zod';
|
||||
import type { FastifyReply, FastifyRequest } from '@hive/service-common';
|
||||
|
|
@ -8,7 +10,17 @@ import { OrganizationMembers } from '../../organization/providers/organization-m
|
|||
import { Logger } from '../../shared/providers/logger';
|
||||
import type { Storage } from '../../shared/providers/storage';
|
||||
import { EmailVerification } from '../providers/email-verification';
|
||||
import { SessionInfo, SuperTokensStore } from '../providers/supertokens-store';
|
||||
import { AuthNStrategy, AuthorizationPolicyStatement, Session, UserActor } from './authz';
|
||||
import {
|
||||
AccessTokenKeyContainer,
|
||||
isAccessToken,
|
||||
parseAccessToken,
|
||||
} from './supertokens-at-home/crypto';
|
||||
|
||||
function sha256(str: string) {
|
||||
return c.createHash('sha256').update(str).digest('hex');
|
||||
}
|
||||
|
||||
export class SuperTokensCookieBasedSession extends Session {
|
||||
public superTokensUserId: string;
|
||||
|
|
@ -155,32 +167,27 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
|
|||
private logger: Logger;
|
||||
private organizationMembers: OrganizationMembers;
|
||||
private storage: Storage;
|
||||
private supertokensStore: SuperTokensStore;
|
||||
private emailVerification: EmailVerification | null;
|
||||
private accessTokenKey: AccessTokenKeyContainer | null;
|
||||
|
||||
constructor(deps: {
|
||||
logger: Logger;
|
||||
storage: Storage;
|
||||
organizationMembers: OrganizationMembers;
|
||||
emailVerification: EmailVerification | null;
|
||||
accessTokenKey: AccessTokenKeyContainer | null;
|
||||
}) {
|
||||
super();
|
||||
this.logger = deps.logger.child({ module: 'SuperTokensUserAuthNStrategy' });
|
||||
this.organizationMembers = deps.organizationMembers;
|
||||
this.storage = deps.storage;
|
||||
this.emailVerification = deps.emailVerification;
|
||||
this.supertokensStore = new SuperTokensStore(deps.storage.pool, deps.logger);
|
||||
this.accessTokenKey = deps.accessTokenKey;
|
||||
}
|
||||
|
||||
private async verifySuperTokensSession(args: {
|
||||
req: FastifyRequest;
|
||||
reply: FastifyReply;
|
||||
}): Promise<SuperTokensSessionPayload | null> {
|
||||
this.logger.debug('Attempt verifying SuperTokens session');
|
||||
|
||||
if (args.req.headers['ignore-session']) {
|
||||
this.logger.debug('Ignoring session due to header');
|
||||
return null;
|
||||
}
|
||||
|
||||
private async _verifySuperTokensCoreSession(args: { req: FastifyRequest; reply: FastifyReply }) {
|
||||
let session: SessionNode.SessionContainer | undefined;
|
||||
|
||||
try {
|
||||
|
|
@ -219,11 +226,6 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
|
|||
|
||||
const payload = session.getAccessTokenPayload();
|
||||
|
||||
if (!payload) {
|
||||
this.logger.error('No access token payload found');
|
||||
return null;
|
||||
}
|
||||
|
||||
const result = SuperTokensSessionPayloadModel.safeParse(payload);
|
||||
|
||||
if (result.success === false) {
|
||||
|
|
@ -240,13 +242,133 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
|
|||
});
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
private async _verifySuperTokensAtHomeSession(
|
||||
args: {
|
||||
req: FastifyRequest;
|
||||
reply: FastifyReply;
|
||||
},
|
||||
accessTokenKey: AccessTokenKeyContainer,
|
||||
) {
|
||||
let session: SessionInfo | null = null;
|
||||
|
||||
args.req.log.debug('attempt parsing access token from cookie');
|
||||
|
||||
const cookie = parseCookie(args.req.headers.cookie ?? '');
|
||||
let rawAccessToken: string | undefined = cookie['sAccessToken'];
|
||||
|
||||
if (!rawAccessToken) {
|
||||
args.req.log.debug('attempt parsing access token authorization header');
|
||||
rawAccessToken = args.req.headers.authorization?.replace('Bearer ', '')?.trim();
|
||||
}
|
||||
|
||||
if (!rawAccessToken || !isAccessToken(rawAccessToken)) {
|
||||
args.req.log.debug('access token is not identified as a supertokens access token.');
|
||||
return null;
|
||||
}
|
||||
|
||||
let accessToken;
|
||||
try {
|
||||
accessToken = parseAccessToken(rawAccessToken, accessTokenKey.publicKey);
|
||||
} catch (err) {
|
||||
args.req.log.debug('Failed verifying the access token. Ask for refresh.');
|
||||
throw new HiveError('Invalid session', {
|
||||
extensions: {
|
||||
code: 'NEEDS_REFRESH',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (accessToken.exp < Date.now() / 1000) {
|
||||
args.req.log.debug('The access token is expired. Ask for refresh.');
|
||||
throw new HiveError('Invalid session', {
|
||||
extensions: {
|
||||
code: 'NEEDS_REFRESH',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
session = await this.supertokensStore.getSessionInfo(accessToken.sessionHandle);
|
||||
|
||||
if (!session) {
|
||||
args.req.log.debug('The access token is expired, no session was found. Ask for refresh.');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (session.expiresAt < Date.now()) {
|
||||
args.req.log.debug('The session is expired.');
|
||||
throw new HiveError('Invalid session.', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
accessToken.parentRefreshTokenHash1 &&
|
||||
sha256(accessToken.parentRefreshTokenHash1) !== session.refreshTokenHash2
|
||||
) {
|
||||
args.req.log.debug(
|
||||
'The access token is expired. A new access token has been issued for this session. Require refresh.',
|
||||
);
|
||||
|
||||
// old access token in use, there was alreadya refresh
|
||||
throw new HiveError('Invalid session.', {
|
||||
extensions: {
|
||||
code: 'NEEDS_REFRESH',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const result = SuperTokensSessionPayloadModel.safeParse(JSON.parse(session.sessionData));
|
||||
|
||||
if (result.success === false) {
|
||||
this.logger.error('SuperTokens session payload is invalid');
|
||||
this.logger.debug('SuperTokens session payload: %s', session.sessionData);
|
||||
this.logger.debug(
|
||||
'SuperTokens session parsing errors: %s',
|
||||
JSON.stringify(result.error.flatten().fieldErrors),
|
||||
);
|
||||
throw new HiveError('Invalid access token provided', {
|
||||
extensions: {
|
||||
code: 'UNAUTHENTICATED',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
private async verifySuperTokensSession(args: {
|
||||
req: FastifyRequest;
|
||||
reply: FastifyReply;
|
||||
}): Promise<SuperTokensSessionPayload | null> {
|
||||
this.logger.debug('Attempt verifying SuperTokens session');
|
||||
|
||||
if (args.req.headers['ignore-session']) {
|
||||
this.logger.debug('Ignoring session due to header');
|
||||
return null;
|
||||
}
|
||||
|
||||
const sessionData = this.accessTokenKey
|
||||
? await this._verifySuperTokensAtHomeSession(args, this.accessTokenKey)
|
||||
: await this._verifySuperTokensCoreSession(args);
|
||||
|
||||
if (!sessionData) {
|
||||
this.logger.debug('No session found');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.emailVerification) {
|
||||
// Check whether the email is already verified.
|
||||
// If it is not then we need to redirect to the email verification page - which will trigger the email sending.
|
||||
const { verified } = await this.emailVerification.checkUserEmailVerified({
|
||||
userIdentityId: session.getUserId(),
|
||||
email: result.data.email,
|
||||
userIdentityId: sessionData.superTokensUserId,
|
||||
email: sessionData.email,
|
||||
});
|
||||
|
||||
if (!verified) {
|
||||
throw new HiveError('Your account is not verified. Please verify your email address.', {
|
||||
extensions: {
|
||||
|
|
@ -257,7 +379,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
|
|||
}
|
||||
|
||||
this.logger.debug('SuperTokens session resolved.');
|
||||
return result.data;
|
||||
return sessionData;
|
||||
}
|
||||
|
||||
async parse(args: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||
import { type Redis } from 'ioredis';
|
||||
import z from 'zod';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { REDIS_INSTANCE } from '../../shared/providers/redis';
|
||||
import { sha256 } from '../lib/supertokens-at-home/crypto';
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Singleton,
|
||||
global: true,
|
||||
})
|
||||
export class OAuthCache {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(REDIS_INSTANCE) private redis: Redis,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ module: 'OAuthCache' });
|
||||
}
|
||||
|
||||
async put(state: string, data: Record) {
|
||||
const encodedData = JSON.stringify(data);
|
||||
const key = `oauth-cache:${sha256(state)}`;
|
||||
await this.redis.set(key, encodedData);
|
||||
await this.redis.expire(key, 60 * 5);
|
||||
}
|
||||
|
||||
async get(state: string) {
|
||||
const key = `oauth-cache:${sha256(state)}`;
|
||||
const encodedData = await this.redis.getdel(key);
|
||||
if (!encodedData) {
|
||||
return null;
|
||||
}
|
||||
const data = RecordModel.parse(JSON.parse(encodedData));
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
const RecordModel = z.object({
|
||||
oidIntegrationId: z.string().nullable(),
|
||||
pkceVerifier: z.string(),
|
||||
method: z.string(),
|
||||
});
|
||||
|
||||
type Record = z.TypeOf<typeof RecordModel>;
|
||||
|
|
@ -0,0 +1,598 @@
|
|||
import { Inject } from 'graphql-modules';
|
||||
import { sql, type DatabasePool } from 'slonik';
|
||||
import z from 'zod';
|
||||
import { Logger } from '../../shared/providers/logger';
|
||||
import { PG_POOL_CONFIG } from '../../shared/providers/pg-pool';
|
||||
|
||||
const SessionInfoModel = z.object({
|
||||
sessionHandle: z.string(),
|
||||
userId: z.string(),
|
||||
sessionData: z.string(),
|
||||
expiresAt: z.number(),
|
||||
createdAt: z.number(),
|
||||
refreshTokenHash2: z.string(),
|
||||
});
|
||||
|
||||
export type SessionInfo = z.TypeOf<typeof SessionInfoModel>;
|
||||
|
||||
const UserModel = z.object({
|
||||
userId: z.string(),
|
||||
recipeId: z.union([z.literal('emailpassword'), z.literal('thirdparty')]),
|
||||
});
|
||||
|
||||
const EmailPasswordUserModel = z.object({
|
||||
userId: z.string(),
|
||||
email: z.string(),
|
||||
passwordHash: z.string(),
|
||||
timeJoined: z.number(),
|
||||
});
|
||||
|
||||
type EmailPasswordUser = z.TypeOf<typeof EmailPasswordUserModel>;
|
||||
|
||||
const ThirdpartUserModel = z.object({
|
||||
thirdPartyId: z.string(),
|
||||
thirdPartyUserId: z.string(),
|
||||
userId: z.string(),
|
||||
email: z.string(),
|
||||
timeJoined: z.number(),
|
||||
});
|
||||
|
||||
const EmailPasswordOrThirdPartyUserModel = z.union([EmailPasswordUserModel, ThirdpartUserModel]);
|
||||
|
||||
export type EmailPasswordOrThirdPartyUser = z.TypeOf<typeof EmailPasswordOrThirdPartyUserModel>;
|
||||
|
||||
const EmailPasswordResetTokenModel = z.object({
|
||||
userId: z.string(),
|
||||
token: z.string(),
|
||||
email: z.string(),
|
||||
tokenExpiry: z.number(),
|
||||
});
|
||||
|
||||
export class SuperTokensStore {
|
||||
private logger: Logger;
|
||||
constructor(
|
||||
@Inject(PG_POOL_CONFIG) private pool: DatabasePool,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ module: 'SuperTokensStore' });
|
||||
}
|
||||
|
||||
async getSessionInfo(sessionHandle: string) {
|
||||
this.logger.debug('Lookup session. (sessionHandle=%s)', sessionHandle);
|
||||
|
||||
const query = sql`
|
||||
SELECT
|
||||
"session_handle" AS "sessionHandle"
|
||||
, "user_id" AS "userId"
|
||||
, "session_data" AS "sessionData"
|
||||
, "expires_at" AS "expiresAt"
|
||||
, "created_at_time" AS "createdAt"
|
||||
, "refresh_token_hash_2" AS "refreshTokenHash2"
|
||||
FROM
|
||||
"supertokens_session_info"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "tenant_id" = 'public'
|
||||
AND "session_handle" = ${sessionHandle}
|
||||
`;
|
||||
|
||||
const result = await this.pool.maybeOne(query);
|
||||
const record = SessionInfoModel.nullable().parse(result);
|
||||
if (!record) {
|
||||
this.logger.debug('Session not found (sessionHandle=%s)', sessionHandle);
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logger.debug('Session found (sessionHandle=%s)', sessionHandle);
|
||||
return record;
|
||||
}
|
||||
|
||||
async deleteSession(sessionHandle: string) {
|
||||
this.logger.debug('Delete session. (sessionHandle=%s)', sessionHandle);
|
||||
|
||||
const query = sql`
|
||||
DELETE
|
||||
FROM "supertokens_session_info"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "tenant_id" = 'public'
|
||||
AND "session_handle" = ${sessionHandle}
|
||||
`;
|
||||
|
||||
await this.pool.query(query);
|
||||
}
|
||||
|
||||
async findEmailPasswordUserByEmail(email: string) {
|
||||
const query = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "password_hash" AS "passwordHash"
|
||||
, "time_joined" AS "timeJoined"
|
||||
FROM
|
||||
"supertokens_emailpassword_users"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "email" = lower(${email})
|
||||
`;
|
||||
|
||||
const record = await this.pool.maybeOne(query).then(EmailPasswordUserModel.nullable().parse);
|
||||
return record;
|
||||
}
|
||||
|
||||
private async lookupEmailUserByUserId(userId: string) {
|
||||
const query = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "password_hash" AS "passwordHash"
|
||||
, "time_joined" AS "timeJoined"
|
||||
FROM
|
||||
"supertokens_emailpassword_users"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "user_id" = ${userId}
|
||||
`;
|
||||
|
||||
const record = await this.pool.maybeOne(query).then(EmailPasswordUserModel.nullable().parse);
|
||||
return record;
|
||||
}
|
||||
|
||||
public async lookupEmailUserByEmail(email: string) {
|
||||
const userToTenantQuery = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
FROM
|
||||
"supertokens_emailpassword_user_to_tenant"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "tenant_id" = 'public'
|
||||
AND "email" = lower(${email})
|
||||
`;
|
||||
|
||||
const userId = await this.pool
|
||||
.maybeOneFirst(userToTenantQuery)
|
||||
.then(z.string().nullable().parse);
|
||||
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const query = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "password_hash" AS "passwordHash"
|
||||
, "time_joined" AS "timeJoined"
|
||||
FROM
|
||||
"supertokens_emailpassword_users"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "user_id" = ${userId}
|
||||
`;
|
||||
|
||||
const record = await this.pool.maybeOne(query).then(EmailPasswordUserModel.nullable().parse);
|
||||
return record;
|
||||
}
|
||||
|
||||
private async lookupThirdPartyUserByUserId(userId: string) {
|
||||
const query = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "third_party_id" AS "thirdPartyId"
|
||||
, "third_party_user_id" AS "thirdPartyUserId"
|
||||
, "time_joined" AS "timeJoined"
|
||||
FROM
|
||||
"supertokens_thirdparty_users"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "user_id" = ${userId}
|
||||
`;
|
||||
|
||||
const record = await this.pool.maybeOne(query).then(ThirdpartUserModel.nullable().parse);
|
||||
return record;
|
||||
}
|
||||
|
||||
async lookupUserByUserId(userId: string) {
|
||||
this.logger.debug('Lookup user. (userId=%s)', userId);
|
||||
|
||||
const query = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
, "recipe_id" AS "recipeId"
|
||||
FROM
|
||||
"supertokens_all_auth_recipe_users"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "tenant_id" = 'public'
|
||||
AND "user_id" = ${userId}
|
||||
`;
|
||||
|
||||
const record = await this.pool.maybeOne(query).then(UserModel.nullable().parse);
|
||||
|
||||
if (!record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (record.recipeId === 'emailpassword') {
|
||||
return this.lookupEmailUserByUserId(record.userId);
|
||||
}
|
||||
|
||||
if (record.recipeId === 'thirdparty') {
|
||||
return this.lookupThirdPartyUserByUserId(record.userId);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async createSession(
|
||||
sessionHandle: string,
|
||||
userId: string,
|
||||
sessionDataInDatabase: string,
|
||||
accessTokenPayload: string,
|
||||
refreshTokenHash2: string,
|
||||
expiresAt: number,
|
||||
) {
|
||||
const query = sql`
|
||||
INSERT INTO "supertokens_session_info" (
|
||||
"app_id"
|
||||
, "tenant_id"
|
||||
, "session_handle"
|
||||
, "user_id"
|
||||
, "refresh_token_hash_2"
|
||||
, "session_data"
|
||||
, "expires_at"
|
||||
, "jwt_user_payload"
|
||||
, "use_static_key"
|
||||
, "created_at_time"
|
||||
) VALUES (
|
||||
'public'
|
||||
, 'public'
|
||||
, ${sessionHandle}
|
||||
, ${userId}
|
||||
, ${refreshTokenHash2}
|
||||
, ${sessionDataInDatabase}
|
||||
, ${expiresAt}
|
||||
, ${accessTokenPayload}
|
||||
, false
|
||||
, ${Date.now()}
|
||||
)
|
||||
RETURNING
|
||||
"session_handle" AS "sessionHandle"
|
||||
, "user_id" AS "userId"
|
||||
, "session_data" AS "sessionData"
|
||||
, "expires_at" AS "expiresAt"
|
||||
, "created_at_time" AS "createdAt"
|
||||
, "refresh_token_hash_2" AS "refreshTokenHash2"
|
||||
`;
|
||||
|
||||
return await this.pool.one(query).then(SessionInfoModel.parse);
|
||||
}
|
||||
|
||||
async updateSessionRefreshHash(
|
||||
sessionHandle: string,
|
||||
lastRefreshTokenHash2: string,
|
||||
newRefreshTokenHash2: string,
|
||||
) {
|
||||
const query = sql`
|
||||
UPDATE
|
||||
"supertokens_session_info"
|
||||
SET
|
||||
"refresh_token_hash_2" = ${newRefreshTokenHash2}
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "tenant_id" = 'public'
|
||||
AND "session_handle" = ${sessionHandle}
|
||||
AND "refresh_token_hash_2" = ${lastRefreshTokenHash2}
|
||||
RETURNING
|
||||
"session_handle" AS "sessionHandle"
|
||||
, "user_id" AS "userId"
|
||||
, "session_data" AS "sessionData"
|
||||
, "expires_at" AS "expiresAt"
|
||||
, "created_at_time" AS "createdAt"
|
||||
, "refresh_token_hash_2" AS "refreshTokenHash2"
|
||||
`;
|
||||
|
||||
return await this.pool.maybeOne(query).then(SessionInfoModel.nullable().parse);
|
||||
}
|
||||
|
||||
async findThirdPartyUser(args: { thirdPartyId: string; thirdPartyUserId: string }) {
|
||||
const query = sql`
|
||||
SELECT
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "third_party_id" AS "thirdPartyId"
|
||||
, "third_party_user_id" AS "thirdPartyUserId"
|
||||
, "time_joined" AS "timeJoined"
|
||||
FROM
|
||||
"supertokens_thirdparty_users"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "third_party_id" = ${args.thirdPartyId}
|
||||
AND "third_party_user_id" = ${args.thirdPartyUserId}
|
||||
`;
|
||||
|
||||
return await this.pool.maybeOne(query).then(ThirdpartUserModel.nullable().parse);
|
||||
}
|
||||
|
||||
async findOIDCUserBySubAndOIDCIntegrationId(args: { sub: string; oidcIntegrationId: string }) {
|
||||
return this.findThirdPartyUser({
|
||||
thirdPartyId: 'oidc',
|
||||
thirdPartyUserId: `${args.oidcIntegrationId}-${args.sub}`,
|
||||
});
|
||||
}
|
||||
|
||||
async createThirdPartyUser(args: {
|
||||
email: string;
|
||||
thirdPartyId: string;
|
||||
thirdPartyUserId: string;
|
||||
}) {
|
||||
const userId = crypto.randomUUID();
|
||||
const now = Date.now();
|
||||
|
||||
const allRecipeUsersQuery = sql`
|
||||
INSERT INTO "supertokens_all_auth_recipe_users" (
|
||||
"app_id"
|
||||
, "tenant_id"
|
||||
, "user_id"
|
||||
, "primary_or_recipe_user_id"
|
||||
, "is_linked_or_is_a_primary_user"
|
||||
, "recipe_id"
|
||||
, "time_joined"
|
||||
, "primary_or_recipe_user_time_joined"
|
||||
) VALUES (
|
||||
'public'
|
||||
, 'public'
|
||||
, ${userId}
|
||||
, ${userId}
|
||||
, false
|
||||
, 'thirdparty'
|
||||
, ${now}
|
||||
, ${now}
|
||||
)
|
||||
`;
|
||||
|
||||
const oidcUserQuery = sql`
|
||||
INSERT INTO "supertokens_thirdparty_users" (
|
||||
"app_id"
|
||||
, "third_party_id"
|
||||
, "third_party_user_id"
|
||||
, "user_id"
|
||||
, "email"
|
||||
, "time_joined"
|
||||
) VALUES (
|
||||
'public'
|
||||
, ${args.thirdPartyId}
|
||||
, ${args.thirdPartyUserId}
|
||||
, ${userId}
|
||||
, ${args.email}
|
||||
, ${now}
|
||||
)
|
||||
RETURNING
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "third_party_id" AS "thirdPartyId"
|
||||
, "third_party_user_id" AS "thirdPartyUserId"
|
||||
, "time_joined" AS "timeJoined"
|
||||
`;
|
||||
|
||||
const appIdToUserIdQuery = sql`
|
||||
INSERT INTO "supertokens_app_id_to_user_id" (
|
||||
"app_id"
|
||||
, "user_id"
|
||||
, "recipe_id"
|
||||
, "primary_or_recipe_user_id"
|
||||
, "is_linked_or_is_a_primary_user"
|
||||
) VALUES (
|
||||
'public'
|
||||
, ${userId}
|
||||
, 'thirdparty'
|
||||
, ${userId}
|
||||
, false
|
||||
)
|
||||
`;
|
||||
|
||||
const thirdpartyUserToTenant = sql`
|
||||
INSERT INTO "supertokens_thirdparty_user_to_tenant" (
|
||||
"app_id"
|
||||
, "tenant_id"
|
||||
, "user_id"
|
||||
, "third_party_id"
|
||||
, "third_party_user_id"
|
||||
) VALUES (
|
||||
'public'
|
||||
, 'public'
|
||||
, ${userId}
|
||||
, ${args.thirdPartyId}
|
||||
, ${args.thirdPartyUserId}
|
||||
)
|
||||
`;
|
||||
|
||||
return await this.pool
|
||||
.transaction(async t => {
|
||||
await t.query(appIdToUserIdQuery);
|
||||
const result = await t.one(oidcUserQuery);
|
||||
await t.query(allRecipeUsersQuery);
|
||||
await t.query(thirdpartyUserToTenant);
|
||||
return result;
|
||||
})
|
||||
.then(r => ThirdpartUserModel.parse(r));
|
||||
}
|
||||
|
||||
async createOIDCUser(args: { email: string; sub: string; oidcIntegrationId: string }) {
|
||||
return this.createThirdPartyUser({
|
||||
email: args.email,
|
||||
thirdPartyId: 'oidc',
|
||||
thirdPartyUserId: args.oidcIntegrationId + '-' + args.sub,
|
||||
});
|
||||
}
|
||||
|
||||
async createEmailPasswordUser(args: { email: string; passwordHash: string }) {
|
||||
const userId = crypto.randomUUID();
|
||||
const now = Date.now();
|
||||
const allRecipeUsersQuery = sql`
|
||||
INSERT INTO "supertokens_all_auth_recipe_users" (
|
||||
"app_id"
|
||||
, "tenant_id"
|
||||
, "user_id"
|
||||
, "primary_or_recipe_user_id"
|
||||
, "is_linked_or_is_a_primary_user"
|
||||
, "recipe_id"
|
||||
, "time_joined"
|
||||
, "primary_or_recipe_user_time_joined"
|
||||
) VALUES (
|
||||
'public'
|
||||
, 'public'
|
||||
, ${userId}
|
||||
, ${userId}
|
||||
, false
|
||||
, 'emailpassword'
|
||||
, ${now}
|
||||
, ${now}
|
||||
)
|
||||
`;
|
||||
|
||||
const emailPasswordUserQuery = sql`
|
||||
INSERT INTO "supertokens_emailpassword_users" (
|
||||
"app_id"
|
||||
, "user_id"
|
||||
, "email"
|
||||
, "password_hash"
|
||||
, "time_joined"
|
||||
) VALUES (
|
||||
'public'
|
||||
, ${userId}
|
||||
, lower(${args.email})
|
||||
, ${args.passwordHash}
|
||||
, ${now}
|
||||
)
|
||||
RETURNING
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "password_hash" AS "passwordHash"
|
||||
, "time_joined" AS "timeJoined"
|
||||
`;
|
||||
|
||||
const appIdToUserIdQuery = sql`
|
||||
INSERT INTO "supertokens_app_id_to_user_id" (
|
||||
"app_id"
|
||||
, "user_id"
|
||||
, "recipe_id"
|
||||
, "primary_or_recipe_user_id"
|
||||
, "is_linked_or_is_a_primary_user"
|
||||
) VALUES (
|
||||
'public'
|
||||
, ${userId}
|
||||
, 'emailpassword'
|
||||
, ${userId}
|
||||
, false
|
||||
)
|
||||
`;
|
||||
|
||||
const userToTenantQuery = sql`
|
||||
INSERT INTO "supertokens_emailpassword_user_to_tenant" (
|
||||
"app_id"
|
||||
, "tenant_id"
|
||||
, "user_id"
|
||||
, "email"
|
||||
) VALUES (
|
||||
'public'
|
||||
, 'public'
|
||||
, ${userId}
|
||||
, lower(${args.email})
|
||||
)
|
||||
`;
|
||||
|
||||
return await this.pool
|
||||
.transaction(async t => {
|
||||
await t.query(appIdToUserIdQuery);
|
||||
const result = await t.one(emailPasswordUserQuery);
|
||||
await t.query(allRecipeUsersQuery);
|
||||
await t.query(userToTenantQuery);
|
||||
return result;
|
||||
})
|
||||
.then(r => EmailPasswordUserModel.parse(r));
|
||||
}
|
||||
|
||||
async createEmailPasswordResetToken(args: {
|
||||
user: EmailPasswordUser;
|
||||
token: string;
|
||||
expiresAt: number;
|
||||
}) {
|
||||
const deletePendingRequestsQuery = sql`
|
||||
DELETE
|
||||
FROM "supertokens_emailpassword_pswd_reset_tokens"
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "user_id" =${args.user.userId}
|
||||
`;
|
||||
|
||||
const query = sql`
|
||||
INSERT INTO "supertokens_emailpassword_pswd_reset_tokens" (
|
||||
"app_id"
|
||||
, "user_id"
|
||||
, "token"
|
||||
, "email"
|
||||
, "token_expiry"
|
||||
) VALUES (
|
||||
'public'
|
||||
, ${args.user.userId}
|
||||
, ${args.token}
|
||||
, ${args.user.email}
|
||||
, ${args.expiresAt}
|
||||
)
|
||||
RETURNING
|
||||
"user_id" AS "userId"
|
||||
, "token"
|
||||
, "email"
|
||||
, "token_expiry" AS "tokenExpiry"
|
||||
`;
|
||||
|
||||
return await this.pool.transaction(async t => {
|
||||
await t.query(deletePendingRequestsQuery);
|
||||
return await t.one(query).then(EmailPasswordResetTokenModel.parse);
|
||||
});
|
||||
}
|
||||
|
||||
async updateEmailPasswordBasedOnResetToken(args: { token: string; newPasswordHash: string }) {
|
||||
const emailPasswordResetTokenQuery = sql`
|
||||
DELETE
|
||||
FROM
|
||||
"supertokens_emailpassword_pswd_reset_tokens"
|
||||
WHERE
|
||||
"token" = ${args.token}
|
||||
RETURNING
|
||||
"user_id" AS "userId"
|
||||
, "token"
|
||||
, "email"
|
||||
, "token_expiry" AS "tokenExpiry"
|
||||
`;
|
||||
|
||||
const updatePasswordHash = (userId: string) => sql`
|
||||
UPDATE "supertokens_emailpassword_users"
|
||||
SET
|
||||
"password_hash" = ${args.newPasswordHash}
|
||||
WHERE
|
||||
"app_id" = 'public'
|
||||
AND "user_id" = ${userId}
|
||||
RETURNING
|
||||
"user_id" AS "userId"
|
||||
, "email" AS "email"
|
||||
, "password_hash" AS "passwordHash"
|
||||
, "time_joined" AS "timeJoined"
|
||||
`;
|
||||
|
||||
return await this.pool.transaction(async t => {
|
||||
const resetToken = await t
|
||||
.maybeOne(emailPasswordResetTokenQuery)
|
||||
.then(EmailPasswordResetTokenModel.parse);
|
||||
|
||||
if (!resetToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await t.one(updatePasswordHash(resetToken.userId)).then(EmailPasswordUserModel.parse);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
import { Inject, Injectable, Scope } from 'graphql-modules';
|
||||
import { type Redis } from 'ioredis';
|
||||
import { type FastifyRequest } from '@hive/service-common';
|
||||
import { Logger } from './logger';
|
||||
import { REDIS_INSTANCE } from './redis';
|
||||
import { RateLimitConfig } from './tokens';
|
||||
|
||||
@Injectable({
|
||||
scope: Scope.Singleton,
|
||||
})
|
||||
export class RedisRateLimiter {
|
||||
logger: Logger;
|
||||
|
||||
constructor(
|
||||
@Inject(REDIS_INSTANCE) private redis: Redis,
|
||||
private config: RateLimitConfig,
|
||||
logger: Logger,
|
||||
) {
|
||||
this.logger = logger.child({ module: 'RateLimiter' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Rate limit Fastify request based on the route definition path.
|
||||
*/
|
||||
async isFastifyRouteRateLimited(
|
||||
req: FastifyRequest,
|
||||
/** duration of the time window */
|
||||
timeWindowSeconds = 5 * 60,
|
||||
/** maximum amount of requests allowed in the time window */
|
||||
maxActionsPerTimeWindow = 10,
|
||||
) {
|
||||
if (!this.config.config) {
|
||||
this.logger.debug('rate limiting is disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
req.routeOptions.url;
|
||||
|
||||
let ip = req.ip;
|
||||
|
||||
if (this.config.config.ipHeaderName && req.headers[this.config.config.ipHeaderName]) {
|
||||
this.logger.debug(
|
||||
'rate limit based on forwarded ip header %s',
|
||||
this.config.config.ipHeaderName,
|
||||
);
|
||||
ip = req.headers[this.config.config.ipHeaderName] as string;
|
||||
}
|
||||
|
||||
const key = `server-rate-limiter:${req.routeOptions.url}:${ip}`;
|
||||
|
||||
const current = await this.redis.incr(key);
|
||||
if (current === 1) {
|
||||
await this.redis.expire(key, timeWindowSeconds);
|
||||
}
|
||||
if (current > maxActionsPerTimeWindow) {
|
||||
this.logger.debug('request is rate limited');
|
||||
return true;
|
||||
}
|
||||
|
||||
this.logger.debug('request is not rate limited');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -84,3 +84,9 @@ AUTH_OKTA_CLIENT_SECRET="<sync>"
|
|||
OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
|
||||
|
||||
HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret
|
||||
|
||||
## Use supertokens at home
|
||||
|
||||
SUPERTOKENS_AT_HOME=0
|
||||
SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0
|
||||
SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjdpx/LNIAPKDCNgszZIzz2tepThK9/3mCWTrOjAJY8KI8YgMo5Gf+IwYvJ2VcpKYa36UJ70bD283P2O/yE/IjtXI6S9cJyd5LrYs+QDENc8au63iDy2iAiFpR1kO7cWQPDKK3OrD+hc5ZEHA3LN82Kb4ZnEA6tAulPfULVDU+RJfSWZOCE+LnkSZ8obvJjiMeknhNSSJko6V3WVuL5ToYfRIiOnueoTywB+3O3Mtp6lBj1j2rpQfO/qvLdRYLpDmLaoaScAyymWfeBp0hpwxd5Jm4vexyHgit2sK0S+tFl0pmh37iVGsRqPJEPISpEwQBcHOhKRj0uW+t/feK6U0WQIDAQAB|MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCN2nH8s0gA8oMI2CzNkjPPa16lOEr3/eYJZOs6MAljwojxiAyjkZ/4jBi8nZVykphrfpQnvRsPbzc/Y7/IT8iO1cjpL1wnJ3kutiz5AMQ1zxq7reIPLaICIWlHWQ7txZA8Morc6sP6FzlkQcDcs3zYpvhmcQDq0C6U99QtUNT5El9JZk4IT4ueRJnyhu8mOIx6SeE1JImSjpXdZW4vlOhh9EiI6e56hPLAH7c7cy2nqUGPWPaulB87+q8t1FgukOYtqhpJwDLKZZ94GnSGnDF3kmbi97HIeCK3awrRL60WXSmaHfuJUaxGo8kQ8hKkTBAFwc6EpGPS5b63994rpTRZAgMBAAECggEBAIl96/IFS4svg/Z0oah3RySKa2g1EeUhAXClkqIJoXBCRD3nomiAY8+i6u8Wxp4QnQ/D1pJV5v6ky6XzZxYezsQzTtNGBkolJn4yMZEAPy3wmXbD6VLQ5jCudb6kAaZRUaYnTxUlr+Kd1BDq8qZ4ik/sNuQEL+Fo+12EgPGTYXov7lWwWjNbzuzMQMm7b1BDU7D/8s/lGg4wimJffVSd4C++buN4Jxm1n1hWWREl7jkJC0sp2J50cpt9IhIIhi8DOnGAcJ4aTtABEJdZyXlO0QllN/D5FEbZBC0Jkbl3lmaIo1WVEYdDpcbSLZxGeYD0CkH4CF/BzUpeHq7FU0HkqOkCgYEA5tgLRFC4CS/FtR3JQ1YifHN4J2bI3BEyje6eiI/n9jmm5zUN/WjCQ6ui2fPzbKAC3yD60lcCgrP7YAn4KFmOv9RS7ApamUH+AX5UBfHlhvwzi4U9eenu7UHH8XrxEHlAwUC9mQbaqzoR/A7jEg8qqincMDUCkk1kjP4nNgQSBFcCgYEAnU/KSQf79m57nXMv+ge9QWkAggnHO7baRox5qlMM6l1gTZB5yELaLNXeik9D2mhVwcpezcQo2Uf+B4MviVeqpoTzYwgdxYg+ebYPUzhd3Ya8ANRDwKCB7SSoRULEDpWebV6ngOc+ruv9ii3ZbVEi7ribtHo6w7rVVJc2bMEKns8CgYB9ssp/yoxLxE2dz7hWCEMDDUUx/1AENQEYNATzS5j9hGsTntodULvnaUBl+eZlEcQ+h5DMlEBzt1l79DHClvGaFx2IFiM7LKoJWiaajhtzo0TWBhlxlyZY3ubm4RD+7WeLU5tqBkdv0VEVtW2D2epbeivBvDvIOog0Ffh3+0NsRQKBgGPA8w84hugPy0dega/VNIfD49SSCsqs+uD9tzDwlSIQsD6/PNpmuh7wR7wA45Ad1TOb9l4Y46ZU5psw7vXyp34MlKHZxbc63BMmBbXJ6ovNIm6MK6J8pacRNbslyVlOOzYzbZhqCu+1KgNza4rMhpBGdEYPtC/ly91mPdbc2rU1AoGBALl6eZqBMahE0S19X5SO/xykGe4ALf74UWCsfKrVEO4Zd3IcELnir0uEPABWvd5C/EAaGoi/a2xgdwuKG32GMpinjoXJywzsquQC6N8CcFIzDiQXaL4j4lFztjgowqNs/YwpOGbm1Dyr3Av072jDPajQGP/xX4fFxBZFnyk1vnXT
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@
|
|||
"type": "module",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"exports": {
|
||||
"./supertokens-at-home/shared": "./src/supertokens-at-home/shared.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsx ../../../scripts/runify.ts src/index.ts src/persisted-documents-worker.ts && tsx ./scripts/copy-persisted-operations.mts",
|
||||
"dev": "tsup-node --config ../../../configs/tsup/dev.config.node.ts src/dev.ts src/persisted-documents-worker.ts",
|
||||
|
|
@ -18,6 +21,7 @@
|
|||
"@escape.tech/graphql-armor-max-depth": "2.4.2",
|
||||
"@escape.tech/graphql-armor-max-directives": "2.3.1",
|
||||
"@escape.tech/graphql-armor-max-tokens": "2.5.1",
|
||||
"@fastify/cookie": "11.0.2",
|
||||
"@fastify/cors": "11.2.0",
|
||||
"@fastify/formbody": "8.0.2",
|
||||
"@graphql-hive/plugin-opentelemetry": "1.3.0",
|
||||
|
|
@ -44,6 +48,7 @@
|
|||
"graphql-yoga": "5.13.3",
|
||||
"hyperid": "3.3.0",
|
||||
"ioredis": "5.8.2",
|
||||
"openid-client": "6.8.2",
|
||||
"pino-pretty": "11.3.0",
|
||||
"prom-client": "15.1.3",
|
||||
"reflect-metadata": "0.2.2",
|
||||
|
|
|
|||
|
|
@ -99,12 +99,22 @@ const RedisModel = zod.object({
|
|||
REDIS_TLS_ENABLED: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
});
|
||||
|
||||
const SuperTokensModel = zod.object({
|
||||
SUPERTOKENS_CONNECTION_URI: zod.string().url(),
|
||||
SUPERTOKENS_API_KEY: zod.string(),
|
||||
SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()),
|
||||
});
|
||||
const SuperTokensModel = zod.union([
|
||||
zod.object({
|
||||
SUPERTOKENS_AT_HOME: emptyString(zod.literal('0').optional()),
|
||||
SUPERTOKENS_CONNECTION_URI: zod.string().url(),
|
||||
SUPERTOKENS_API_KEY: zod.string(),
|
||||
SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()),
|
||||
}),
|
||||
zod.object({
|
||||
SUPERTOKENS_AT_HOME: zod.literal('1'),
|
||||
SUPERTOKENS_REFRESH_TOKEN_KEY: zod.string(),
|
||||
SUPERTOKENS_ACCESS_TOKEN_KEY: zod.string(),
|
||||
SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
|
||||
SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()),
|
||||
}),
|
||||
]);
|
||||
|
||||
const GitHubModel = zod.union([
|
||||
zod.object({
|
||||
|
|
@ -431,16 +441,34 @@ export const env = {
|
|||
password: redis.REDIS_PASSWORD ?? '',
|
||||
tlsEnabled: redis.REDIS_TLS_ENABLED === '1',
|
||||
},
|
||||
supertokens: {
|
||||
connectionURI: supertokens.SUPERTOKENS_CONNECTION_URI,
|
||||
apiKey: supertokens.SUPERTOKENS_API_KEY,
|
||||
rateLimit:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT === '0'
|
||||
? null
|
||||
: {
|
||||
ipHeaderName: supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
|
||||
supertokens:
|
||||
supertokens.SUPERTOKENS_AT_HOME === '1'
|
||||
? {
|
||||
type: 'atHome' as const,
|
||||
secrets: {
|
||||
refreshTokenKey: supertokens.SUPERTOKENS_REFRESH_TOKEN_KEY,
|
||||
accessTokenKey: supertokens.SUPERTOKENS_ACCESS_TOKEN_KEY,
|
||||
},
|
||||
},
|
||||
rateLimit:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT === '0'
|
||||
? null
|
||||
: {
|
||||
ipHeaderName:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
|
||||
},
|
||||
}
|
||||
: {
|
||||
type: 'core' as const,
|
||||
connectionURI: supertokens.SUPERTOKENS_CONNECTION_URI,
|
||||
apiKey: supertokens.SUPERTOKENS_API_KEY,
|
||||
rateLimit:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT === '0'
|
||||
? null
|
||||
: {
|
||||
ipHeaderName:
|
||||
supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
|
||||
},
|
||||
},
|
||||
auth: {
|
||||
github:
|
||||
authGithub.AUTH_GITHUB === '1'
|
||||
|
|
|
|||
|
|
@ -36,10 +36,6 @@ export interface GraphQLHandlerOptions {
|
|||
registry: Registry;
|
||||
signature: string;
|
||||
tracing?: TracingInstance;
|
||||
supertokens: {
|
||||
connectionUri: string;
|
||||
apiKey: string;
|
||||
};
|
||||
isProduction: boolean;
|
||||
hiveUsageConfig: HiveUsageConfig;
|
||||
hivePersistedDocumentsConfig: HivePersistedDocumentsConfig;
|
||||
|
|
|
|||
|
|
@ -21,9 +21,12 @@ import {
|
|||
OrganizationMemberRoles,
|
||||
OrganizationMembers,
|
||||
} from '@hive/api';
|
||||
import { AccessTokenKeyContainer } from '@hive/api/modules/auth/lib/supertokens-at-home/crypto';
|
||||
import { EmailVerification } from '@hive/api/modules/auth/providers/email-verification';
|
||||
import { OAuthCache } from '@hive/api/modules/auth/providers/oauth-cache';
|
||||
import { HivePubSub } from '@hive/api/modules/shared/providers/pub-sub';
|
||||
import { createRedisClient } from '@hive/api/modules/shared/providers/redis';
|
||||
import { RedisRateLimiter } from '@hive/api/modules/shared/providers/redis-rate-limiter';
|
||||
import { TargetsByIdCache } from '@hive/api/modules/target/providers/targets-by-id-cache';
|
||||
import { TargetsBySlugCache } from '@hive/api/modules/target/providers/targets-by-slug-cache';
|
||||
import { createArtifactRequestHandler } from '@hive/cdn-script/artifact-handler';
|
||||
|
|
@ -59,6 +62,7 @@ import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics';
|
|||
import { createOtelAuthEndpoint } from './otel-auth-endpoint';
|
||||
import { createPublicGraphQLHandler } from './public-graphql-handler';
|
||||
import { initSupertokens, oidcIdLookup } from './supertokens';
|
||||
import { registerSupertokensAtHome } from './supertokens-at-home';
|
||||
|
||||
class CorsError extends Error {
|
||||
constructor() {
|
||||
|
|
@ -128,10 +132,14 @@ export async function main() {
|
|||
return res.status(403).send(err.message);
|
||||
}
|
||||
|
||||
// We can not upgrade Supertokens Node as it removed some APIs we rely on for
|
||||
// our SSO flow. This the as `any` cast here.
|
||||
// The code is still compatible and purely a type error.
|
||||
return supertokensErrorHandler()(err, req, res as any);
|
||||
if (env.supertokens.type === 'core') {
|
||||
// We can not upgrade Supertokens Node as it removed some APIs we rely on for
|
||||
// our SSO flow. This the as `any` cast here.
|
||||
// The code is still compatible and purely a type error.
|
||||
return supertokensErrorHandler()(err, req, res as any);
|
||||
}
|
||||
server.log.error(err);
|
||||
return res.status(500);
|
||||
});
|
||||
await server.register(cors, (_: unknown): FastifyCorsOptionsDelegateCallback => {
|
||||
return (req, callback) => {
|
||||
|
|
@ -159,7 +167,9 @@ export async function main() {
|
|||
'graphql-client-name',
|
||||
'ignore-session',
|
||||
'x-request-id',
|
||||
...supertokens.getAllCORSHeaders(),
|
||||
...(env.supertokens.type === 'atHome'
|
||||
? ['rid', 'fdi-version', 'anti-csrf', 'authorization', 'st-auth-mode']
|
||||
: supertokens.getAllCORSHeaders()),
|
||||
],
|
||||
});
|
||||
};
|
||||
|
|
@ -369,10 +379,6 @@ export async function main() {
|
|||
graphiqlEndpoint: graphqlPath,
|
||||
registry,
|
||||
signature,
|
||||
supertokens: {
|
||||
connectionUri: env.supertokens.connectionURI,
|
||||
apiKey: env.supertokens.apiKey,
|
||||
},
|
||||
isProduction: env.environment !== 'development',
|
||||
release: env.release,
|
||||
hiveUsageConfig: env.hive,
|
||||
|
|
@ -393,6 +399,10 @@ export async function main() {
|
|||
emailVerification: env.auth.requireEmailVerification
|
||||
? registry.injector.get(EmailVerification)
|
||||
: null,
|
||||
accessTokenKey:
|
||||
env.supertokens.type === 'atHome'
|
||||
? new AccessTokenKeyContainer(env.supertokens.secrets.accessTokenKey)
|
||||
: null,
|
||||
}),
|
||||
organizationAccessTokenStrategy,
|
||||
(logger: Logger) =>
|
||||
|
|
@ -443,22 +453,28 @@ export async function main() {
|
|||
|
||||
const crypto = new CryptoProvider(env.encryptionSecret);
|
||||
|
||||
initSupertokens({
|
||||
storage,
|
||||
crypto,
|
||||
logger: server.log,
|
||||
redis,
|
||||
taskScheduler,
|
||||
broadcastLog(id, message) {
|
||||
pubSub.publish('oidcIntegrationLogs', id, {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
});
|
||||
},
|
||||
});
|
||||
function broadcastLog(oidcId: string, message: string) {
|
||||
pubSub.publish('oidcIntegrationLogs', oidcId, {
|
||||
timestamp: new Date().toISOString(),
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
if (env.supertokens.type == 'core') {
|
||||
initSupertokens({
|
||||
storage,
|
||||
crypto,
|
||||
logger: server.log,
|
||||
redis,
|
||||
taskScheduler,
|
||||
broadcastLog,
|
||||
});
|
||||
}
|
||||
|
||||
await server.register(formDataPlugin);
|
||||
await server.register(supertokensFastifyPlugin);
|
||||
if (env.supertokens.type == 'core') {
|
||||
await server.register(supertokensFastifyPlugin);
|
||||
}
|
||||
|
||||
await registerTRPC(server, {
|
||||
router: internalApiRouter,
|
||||
|
|
@ -567,6 +583,19 @@ export async function main() {
|
|||
return;
|
||||
});
|
||||
|
||||
if (env.supertokens.type === 'atHome') {
|
||||
await registerSupertokensAtHome(
|
||||
server,
|
||||
storage,
|
||||
registry.injector.get(TaskScheduler),
|
||||
registry.injector.get(CryptoProvider),
|
||||
registry.injector.get(RedisRateLimiter),
|
||||
registry.injector.get(OAuthCache),
|
||||
broadcastLog,
|
||||
env.supertokens.secrets,
|
||||
);
|
||||
}
|
||||
|
||||
if (env.cdn.providers.api !== null) {
|
||||
const s3 = {
|
||||
client: new AwsClient({
|
||||
|
|
|
|||
1554
packages/services/server/src/supertokens-at-home.ts
Normal file
1554
packages/services/server/src/supertokens-at-home.ts
Normal file
File diff suppressed because it is too large
Load diff
145
packages/services/server/src/supertokens-at-home/README.md
Normal file
145
packages/services/server/src/supertokens-at-home/README.md
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
# Supertokens Implementation Notes
|
||||
|
||||
This file contains some useful information regarding the supertokens implementation.
|
||||
|
||||
## OIDC/Social Provider Login Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Browser
|
||||
participant Frontend App
|
||||
participant Backend Server
|
||||
participant OIDC Provider
|
||||
|
||||
Browser->>Frontend App: 1. User clicks "Login with Google/GitHub/etc."
|
||||
|
||||
Frontend App->>Backend Server: 2. Requests the provider's authorization URL (e.g., GET /auth/oidc/authorization-url)
|
||||
note right of Backend Server: The backend generates and stores a<br/>`state` value and a `pkce_verifier`<br/> for security. The `code_challenge`<br/>(derived from the verifier) is part of the URL.
|
||||
|
||||
Backend Server-->>Frontend App: 3. Responds with the unique authorization URL
|
||||
|
||||
Frontend App->>Browser: 4. Redirects the browser to the OIDC Provider's URL
|
||||
|
||||
Browser->>OIDC Provider: 5. User logs in with their credentials and grants consent to your application
|
||||
|
||||
OIDC Provider->>Browser: 6. Redirects back to your app's callback URL with an `authorization_code` and the `state` value
|
||||
|
||||
Browser->>Frontend App: 7. The Frontend App loads, capturing the `code` and `state` from the URL parameters
|
||||
|
||||
Frontend App->>Backend Server: 8. Sends the `authorization_code` and `state` to the backend callback endpoint (e.g., POST /auth/oidc/callback)
|
||||
note right of Backend Server: The backend first validates that the received `state`<br/>matches the one it stored earlier to prevent CSRF attacks.
|
||||
|
||||
Backend Server->>OIDC Provider: 9. Exchanges the `code` for tokens (includes `code`, `client_secret`, and the original `pkce_verifier`)
|
||||
|
||||
OIDC Provider-->>Backend Server: 10. Verifies the request and returns an `access_token` and `id_token`
|
||||
|
||||
Backend Server->>OIDC Provider: 11. Uses the `access_token` to request the user's identity from a `/userinfo` endpoint
|
||||
|
||||
OIDC Provider-->>Backend Server: 12. Returns the user's profile information (e.g., email, name, ID)
|
||||
note right of Backend Server: The backend now has trusted information about the user.
|
||||
|
||||
Backend Server->>Backend Server: 13. Finds an existing user or creates a new one in its database and creates a new session
|
||||
|
||||
Backend Server-->>Frontend App: 14. Responds with success and attaches the session cookies (`sAccessToken`, `sRefreshToken`) to the response
|
||||
|
||||
Frontend App->>Browser: 15. The login is complete. The browser now stores the session cookies, and the user is authenticated within your application.
|
||||
```
|
||||
|
||||
## Refresh Session Flow
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Session Refresh Flow
|
||||
A[POST /auth/session/refresh] --> B{Extract Refresh Token from Cookie};
|
||||
B --> E{Verify Refresh Token Signature & Expiry};
|
||||
E -- Invalid --> F[Clear Cookies & 401 Unauthorized];
|
||||
E -- Valid --> G{Extract Session Handle from Token};
|
||||
G --> H{Fetch Session from Store};
|
||||
H -- Not Found --> F;
|
||||
H -- Found --> I(Validate Session);
|
||||
end
|
||||
|
||||
subgraph Validate Session Sub-Flow
|
||||
I --> I1{Is Provided Refresh Token the LATEST one for this session?};
|
||||
I1 -- No (Old token was used) --> I2[TOKEN THEFT DETECTED];
|
||||
I2 --> I3[Revoke all sessions for this user & Clear Cookies];
|
||||
I1 -- Yes --> J{Generate New Access Token};
|
||||
end
|
||||
|
||||
subgraph Token Generation and Response
|
||||
J --> K{"Generate New Refresh Token (Token Rotation)"};
|
||||
K --> L{Update Session in Store with new token hash};
|
||||
L --> M{Set New Tokens in Cookies};
|
||||
M --> N[200 OK];
|
||||
end
|
||||
```
|
||||
|
||||
### Refresh Session Security
|
||||
|
||||
This section goes a bit into detail on how token theft is prevented.
|
||||
|
||||
The mechanism relies on a "chained" or "linked" token system, where each new refresh token is
|
||||
cryptographically linked to the one that came before it. The server only needs to store a single
|
||||
hash to validate the entire chain.
|
||||
|
||||
#### The Key Components
|
||||
|
||||
1. **The Refresh Token Payload**: When a new refresh token is created, it contains a special field
|
||||
called `parentRefreshTokenHash1`. This field holds a SHA256 hash of the _previous_ refresh token
|
||||
that was used. The very first refresh token in a session will not have this field.
|
||||
|
||||
2. **The Session Store (Database)**: The server does not store the raw refresh tokens. Instead, for
|
||||
each session, it stores a value called `refreshTokenHash2`. This is the hash of the **latest and
|
||||
currently valid** refresh token that has been issued to the client.
|
||||
|
||||
#### The Validation Process
|
||||
|
||||
When a client sends a request to `/auth/session/refresh`, the server performs the following checks,
|
||||
which you can see in the `supertokens-at-home.ts` file around lines 499-509:
|
||||
|
||||
```ts
|
||||
if (
|
||||
!payload.parentRefreshTokenHash1 &&
|
||||
sha256(sha256(refreshToken)) !== session.refreshTokenHash2
|
||||
) {
|
||||
req.log.debug('The refreshTokenHash2 does not match (first refresh).');
|
||||
return unsetAuthCookies(rep).status(404).send();
|
||||
}
|
||||
|
||||
if (
|
||||
payload.parentRefreshTokenHash1 &&
|
||||
session.refreshTokenHash2 !== sha256(payload.parentRefreshTokenHash1)
|
||||
) {
|
||||
```
|
||||
|
||||
##### Scenario 1: The Very First Refresh
|
||||
|
||||
- **Condition**: The incoming refresh token's payload **does not** contain a
|
||||
`parentRefreshTokenHash1`.
|
||||
- **Action**: The server knows this must be the original refresh token created at login. To verify
|
||||
it, it performs a double SHA256 hash on the raw token (`sha256(sha256(refreshToken))`) and
|
||||
compares it to the `refreshTokenHash2` stored in the session.
|
||||
- **Result**: If they match, the token is valid. If not, the request is rejected.
|
||||
|
||||
##### Scenario 2: All Subsequent Refreshes
|
||||
|
||||
- **Condition**: The incoming refresh token's payload **does** contain a `parentRefreshTokenHash1`.
|
||||
- **Action**: This is the crucial step for detecting token reuse. The server takes the
|
||||
`parentRefreshTokenHash1` from the payload, hashes it once
|
||||
(`sha256(payload.parentRefreshTokenHash1)`), and compares it to the `refreshTokenHash2` from the
|
||||
session store.
|
||||
- **Result**:
|
||||
- **Match**: This proves that the token used to generate the _current_ token was the latest one
|
||||
known to the server. The session is valid.
|
||||
- **Mismatch**: This is the **token theft** scenario. It means that the `refreshTokenHash2` in the
|
||||
database is newer than the one being presented. This can only happen if an older, already used
|
||||
token has been submitted. The request is immediately rejected.
|
||||
|
||||
### The "Rotation": Updating the Hash
|
||||
|
||||
If the validation is successful, the server generates a _new_ refresh token and then immediately
|
||||
updates the session in the database. The `refreshTokenHash2` is replaced with a new hash derived
|
||||
from the refresh token that was just used.
|
||||
|
||||
This ensures that for the next refresh cycle, only the newly issued refresh token will be considered
|
||||
valid, continuing the chain and maintaining security.
|
||||
115
packages/services/server/src/supertokens-at-home/shared.ts
Normal file
115
packages/services/server/src/supertokens-at-home/shared.ts
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import { z } from 'zod';
|
||||
import type { User } from '@hive/api';
|
||||
import {
|
||||
AccessTokenKeyContainer,
|
||||
createAccessToken,
|
||||
createRefreshToken,
|
||||
sha256,
|
||||
} from '@hive/api/modules/auth/lib/supertokens-at-home/crypto';
|
||||
import { SuperTokensStore } from '@hive/api/modules/auth/providers/supertokens-store';
|
||||
|
||||
export const SuperTokensSessionPayloadV2Model = z.object({
|
||||
version: z.literal('2'),
|
||||
superTokensUserId: z.string(),
|
||||
email: z.string(),
|
||||
userId: z.string(),
|
||||
oidcIntegrationId: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type SuperTokensSessionPayload = z.TypeOf<typeof SuperTokensSessionPayloadV2Model>;
|
||||
|
||||
const PasswordModel = z
|
||||
.string()
|
||||
.min(10, { message: 'Password must be at least 10 characters long.' })
|
||||
// Check 2: At least one uppercase letter
|
||||
.regex(/[A-Z]/, { message: 'Password must contain at least one uppercase letter.' })
|
||||
// Check 3: At least one special character
|
||||
.regex(/[!@#$%^&*(),.?":{}|<>]/, {
|
||||
message: 'Password must contain at least one special character.',
|
||||
})
|
||||
// Check 4: At least one digit
|
||||
.regex(/[0-9]/, { message: 'Password must contain at least one digit.' })
|
||||
// Check 5: At least one lowercase letter
|
||||
.regex(/[a-z]/, { message: 'Password must contain at least one lowercase letter.' });
|
||||
|
||||
export function validatePassword(password: string):
|
||||
| {
|
||||
status: 'OK';
|
||||
}
|
||||
| {
|
||||
status: 'INVALID';
|
||||
message: string;
|
||||
} {
|
||||
const result = PasswordModel.safeParse(password);
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
status: 'INVALID',
|
||||
message: result.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return { status: 'OK' };
|
||||
}
|
||||
|
||||
export async function createNewSession(
|
||||
supertokensStore: SuperTokensStore,
|
||||
args: {
|
||||
superTokensUserId: string;
|
||||
hiveUser: User;
|
||||
oidcIntegrationId: string | null;
|
||||
},
|
||||
secrets: {
|
||||
refreshTokenKey: string;
|
||||
accessTokenKey: AccessTokenKeyContainer;
|
||||
},
|
||||
) {
|
||||
const sessionHandle = crypto.randomUUID();
|
||||
// 1 week for now
|
||||
const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1_000;
|
||||
|
||||
const refreshToken = createRefreshToken(
|
||||
{
|
||||
sessionHandle,
|
||||
userId: args.superTokensUserId,
|
||||
parentRefreshTokenHash1: null,
|
||||
},
|
||||
secrets.refreshTokenKey,
|
||||
);
|
||||
|
||||
const payload: SuperTokensSessionPayload = {
|
||||
version: '2',
|
||||
superTokensUserId: args.superTokensUserId,
|
||||
userId: args.hiveUser.id,
|
||||
oidcIntegrationId: args.oidcIntegrationId ?? null,
|
||||
email: args.hiveUser.email,
|
||||
};
|
||||
|
||||
const stringifiedPayload = JSON.stringify(payload);
|
||||
|
||||
const session = await supertokensStore.createSession(
|
||||
sessionHandle,
|
||||
args.superTokensUserId,
|
||||
stringifiedPayload,
|
||||
stringifiedPayload,
|
||||
sha256(sha256(refreshToken)),
|
||||
expiresAt,
|
||||
);
|
||||
|
||||
const accessToken = createAccessToken(
|
||||
{
|
||||
sub: args.superTokensUserId,
|
||||
sessionHandle,
|
||||
sessionData: payload,
|
||||
refreshTokenHash1: sha256(refreshToken),
|
||||
parentRefreshTokenHash1: null,
|
||||
},
|
||||
secrets.accessTokenKey,
|
||||
);
|
||||
|
||||
return {
|
||||
session,
|
||||
refreshToken,
|
||||
accessToken,
|
||||
};
|
||||
}
|
||||
|
|
@ -101,7 +101,7 @@ export const backendConfig = (requirements: {
|
|||
return {
|
||||
framework: 'fastify',
|
||||
supertokens: {
|
||||
connectionURI: env.supertokens.connectionURI,
|
||||
connectionURI: env.supertokens.connectionURI ?? '',
|
||||
apiKey: env.supertokens.apiKey,
|
||||
},
|
||||
telemetry: false,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const frontendConfig = () => {
|
|||
},
|
||||
override: env.auth.oidc ? getOIDCOverrides() : undefined,
|
||||
}),
|
||||
SessionReact.init(),
|
||||
SessionReact.init({}),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -10,23 +10,33 @@ export const createThirdPartyEmailPasswordReactOIDCProvider = () => ({
|
|||
|
||||
const delimiter = '--';
|
||||
|
||||
let currentAuthUrl: null | string = null;
|
||||
|
||||
export const getOIDCOverrides = (): UserInput['override'] => ({
|
||||
functions: originalImplementation => ({
|
||||
...originalImplementation,
|
||||
generateStateToSendToOAuthProvider(input) {
|
||||
const hash = originalImplementation.generateStateToSendToOAuthProvider(input);
|
||||
let state: null | string = null;
|
||||
if (currentAuthUrl) {
|
||||
const url = new URL(currentAuthUrl);
|
||||
|
||||
state = url.searchParams.get('state');
|
||||
}
|
||||
|
||||
state ||= originalImplementation.generateStateToSendToOAuthProvider(input);
|
||||
|
||||
const oidcId = input?.userContext?.['oidcId'];
|
||||
|
||||
if (typeof oidcId === 'string') {
|
||||
return `${hash}${delimiter}${oidcId}`;
|
||||
return `${state}${delimiter}${oidcId}`;
|
||||
}
|
||||
|
||||
return hash;
|
||||
return state;
|
||||
},
|
||||
getAuthorisationURLFromBackend(input) {
|
||||
async getAuthorisationURLFromBackend(input) {
|
||||
const maybeId: unknown = input.userContext['oidcId'];
|
||||
|
||||
return originalImplementation.getAuthorisationURLFromBackend(
|
||||
const result = await originalImplementation.getAuthorisationURLFromBackend(
|
||||
typeof maybeId === 'string'
|
||||
? {
|
||||
...input,
|
||||
|
|
@ -43,6 +53,9 @@ export const getOIDCOverrides = (): UserInput['override'] => ({
|
|||
}
|
||||
: input,
|
||||
);
|
||||
|
||||
currentAuthUrl = result.urlWithQueryParams;
|
||||
return result;
|
||||
},
|
||||
thirdPartySignInAndUp(input) {
|
||||
const locationUrl = new URL(window.location.toString());
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ import { env } from '@/env/frontend';
|
|||
type Provider = 'google' | 'okta' | 'github' | 'oidc';
|
||||
const providers: Provider[] = ['google', 'okta', 'github', 'oidc'];
|
||||
|
||||
export const enabledProviders: Provider[] = providers.filter(
|
||||
provider => env.auth[provider] === true,
|
||||
);
|
||||
export const enabledProviders: Provider[] = providers.filter(provider => !!env.auth[provider]);
|
||||
|
||||
export function isProviderEnabled(provider: Provider) {
|
||||
return enabledProviders.includes(provider);
|
||||
|
|
|
|||
|
|
@ -292,3 +292,6 @@ type GraphQLPayload = {
|
|||
documentId: string;
|
||||
}
|
||||
);
|
||||
|
||||
// @ts-expect-error for testing purposes ok
|
||||
window.__YOU_ARE_FIRED_attemptSessionRefresh = () => Session.attemptRefreshingSession();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { exhaustiveGuard } from '@/lib/utils';
|
|||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { Link, Navigate } from '@tanstack/react-router';
|
||||
import { PasswordStringModel } from './auth-sign-up';
|
||||
|
||||
const ResetPasswordFormSchema = z.object({
|
||||
email: z
|
||||
|
|
@ -194,7 +195,7 @@ function AuthResetPasswordEmail(props: { email: string | null; redirectToPath: s
|
|||
}
|
||||
|
||||
const NewPasswordFormSchema = z.object({
|
||||
newPassword: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
newPassword: PasswordStringModel,
|
||||
});
|
||||
|
||||
type NewPasswordFormValues = z.infer<typeof NewPasswordFormSchema>;
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ const SignInFormSchema = z.object({
|
|||
required_error: 'Email is required',
|
||||
})
|
||||
.email('Invalid email address'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
type SignInFormValues = z.infer<typeof SignInFormSchema>;
|
||||
|
|
|
|||
|
|
@ -36,6 +36,22 @@ import { useMutation } from '@tanstack/react-query';
|
|||
import { Link, Navigate, useRouter } from '@tanstack/react-router';
|
||||
import { SignInButton } from './auth-sign-in';
|
||||
|
||||
export const PasswordStringModel = z
|
||||
.string({
|
||||
required_error: 'Password is required',
|
||||
})
|
||||
.min(10, { message: 'Password must be at least 10 characters long.' })
|
||||
// Check 2: At least one uppercase letter
|
||||
.regex(/[A-Z]/, { message: 'Password must contain at least one uppercase letter.' })
|
||||
// Check 3: At least one special character
|
||||
.regex(/[!@#$%^&*(),.?":{}|<>]/, {
|
||||
message: 'Password must contain at least one special character.',
|
||||
})
|
||||
// Check 4: At least one digit
|
||||
.regex(/[0-9]/, { message: 'Password must contain at least one digit.' })
|
||||
// Check 5: At least one lowercase letter
|
||||
.regex(/[a-z]/, { message: 'Password must contain at least one lowercase letter.' });
|
||||
|
||||
const SignUpFormSchema = z.object({
|
||||
firstName: z.string({
|
||||
required_error: 'First name is required',
|
||||
|
|
@ -48,7 +64,7 @@ const SignUpFormSchema = z.object({
|
|||
required_error: 'Email is required',
|
||||
})
|
||||
.email('Invalid email address'),
|
||||
password: z.string().min(8, 'Password must be at least 8 characters'),
|
||||
password: PasswordStringModel,
|
||||
});
|
||||
|
||||
type SignUpFormValues = z.infer<typeof SignUpFormSchema>;
|
||||
|
|
|
|||
260
pnpm-lock.yaml
260
pnpm-lock.yaml
|
|
@ -877,6 +877,9 @@ importers:
|
|||
ioredis-mock:
|
||||
specifier: 8.9.0
|
||||
version: 8.9.0(@types/ioredis-mock@8.2.5)(ioredis@5.8.2)
|
||||
jsonwebtoken:
|
||||
specifier: 9.0.3
|
||||
version: 9.0.3
|
||||
lodash:
|
||||
specifier: ^4.17.23
|
||||
version: 4.17.23
|
||||
|
|
@ -1232,6 +1235,9 @@ importers:
|
|||
'@escape.tech/graphql-armor-max-tokens':
|
||||
specifier: 2.5.1
|
||||
version: 2.5.1
|
||||
'@fastify/cookie':
|
||||
specifier: 11.0.2
|
||||
version: 11.0.2
|
||||
'@fastify/cors':
|
||||
specifier: 11.2.0
|
||||
version: 11.2.0
|
||||
|
|
@ -1310,6 +1316,9 @@ importers:
|
|||
ioredis:
|
||||
specifier: 5.8.2
|
||||
version: 5.8.2
|
||||
openid-client:
|
||||
specifier: 6.8.2
|
||||
version: 6.8.2
|
||||
pino-pretty:
|
||||
specifier: 11.3.0
|
||||
version: 11.3.0
|
||||
|
|
@ -1346,7 +1355,7 @@ importers:
|
|||
version: 1.0.9(pino@10.3.0)
|
||||
'@graphql-hive/plugin-opentelemetry':
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0(encoding@0.1.13)(graphql@16.12.0)(ioredis@5.8.2)(pino@10.3.0)(ws@8.18.0)
|
||||
version: 1.3.0(encoding@0.1.13)(graphql@16.12.0)(pino@10.3.0)(ws@8.18.0)
|
||||
'@opentelemetry/api':
|
||||
specifier: 1.9.0
|
||||
version: 1.9.0
|
||||
|
|
@ -3904,6 +3913,9 @@ packages:
|
|||
'@fastify/busboy@3.1.1':
|
||||
resolution: {integrity: sha512-5DGmA8FTdB2XbDeEwc/5ZXBl6UbBAyBOOLlPuBnZ/N1SwdH9Ii+cOX3tBROlDgcTXxjOYnLMVoKk9+FXAw0CJw==}
|
||||
|
||||
'@fastify/cookie@11.0.2':
|
||||
resolution: {integrity: sha512-GWdwdGlgJxyvNv+QcKiGNevSspMQXncjMZ1J8IvuDQk0jvkzgWWZFNC2En3s+nHndZBGV8IbLwOI/sxCZw/mzA==}
|
||||
|
||||
'@fastify/cors@11.2.0':
|
||||
resolution: {integrity: sha512-LbLHBuSAdGdSFZYTLVA3+Ch2t+sA6nq3Ejc6XLAKiQ6ViS2qFnvicpj0htsx03FyYeLs04HfRNBsz/a8SvbcUw==}
|
||||
|
||||
|
|
@ -14040,6 +14052,9 @@ packages:
|
|||
jose@4.15.9:
|
||||
resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==}
|
||||
|
||||
jose@6.1.3:
|
||||
resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==}
|
||||
|
||||
joycon@3.1.1:
|
||||
resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -14175,8 +14190,8 @@ packages:
|
|||
resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==}
|
||||
engines: {'0': node >= 0.2.0}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||
jsonwebtoken@9.0.3:
|
||||
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
|
||||
jsox@1.2.119:
|
||||
|
|
@ -14205,15 +14220,15 @@ packages:
|
|||
just-diff@6.0.2:
|
||||
resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==}
|
||||
|
||||
jwa@1.4.2:
|
||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jwks-rsa@3.2.0:
|
||||
resolution: {integrity: sha512-PwchfHcQK/5PSydeKCs1ylNym0w/SSv8a62DgHJ//7x2ZclCoinlsjAfDxAAbpoTPybOum/Jgy+vkvMmKz89Ww==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
jws@4.0.1:
|
||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||
|
||||
kafkajs@2.2.4:
|
||||
resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==}
|
||||
|
|
@ -15583,6 +15598,9 @@ packages:
|
|||
nullthrows@1.1.1:
|
||||
resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==}
|
||||
|
||||
oauth4webapi@3.8.5:
|
||||
resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==}
|
||||
|
||||
object-assign@4.1.1:
|
||||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
@ -15692,6 +15710,9 @@ packages:
|
|||
resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==}
|
||||
hasBin: true
|
||||
|
||||
openid-client@6.8.2:
|
||||
resolution: {integrity: sha512-uOvTCndr4udZsKihJ68H9bUICrriHdUVJ6Az+4Ns6cW55rwM5h0bjVIzDz2SxgOI84LKjFyjOFvERLzdTUROGA==}
|
||||
|
||||
opentelemetry-instrumentation-fetch-node@1.2.3:
|
||||
resolution: {integrity: sha512-Qb11T7KvoCevMaSeuamcLsAD+pZnavkhDnlVL0kRozfhl42dKG5Q3anUklAFKJZjY3twLR+BnRa6DlwwkIE/+A==}
|
||||
engines: {node: '>18.0.0'}
|
||||
|
|
@ -22144,6 +22165,11 @@ snapshots:
|
|||
|
||||
'@fastify/busboy@3.1.1': {}
|
||||
|
||||
'@fastify/cookie@11.0.2':
|
||||
dependencies:
|
||||
cookie: 1.1.1
|
||||
fastify-plugin: 5.1.0
|
||||
|
||||
'@fastify/cors@11.2.0':
|
||||
dependencies:
|
||||
fastify-plugin: 5.1.0
|
||||
|
|
@ -22726,6 +22752,56 @@ snapshots:
|
|||
- winston
|
||||
- ws
|
||||
|
||||
'@graphql-hive/gateway-runtime@2.5.0(graphql@16.12.0)(pino@10.3.0)(ws@8.18.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
'@envelop/disable-introspection': 9.0.0(@envelop/core@5.5.1)(graphql@16.12.0)
|
||||
'@envelop/generic-auth': 11.0.0(@envelop/core@5.5.1)(graphql@16.12.0)
|
||||
'@envelop/instrumentation': 1.0.0
|
||||
'@graphql-hive/core': 0.18.0(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-hive/logger': 1.0.9(pino@10.3.0)
|
||||
'@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2)
|
||||
'@graphql-hive/signal': 2.0.0
|
||||
'@graphql-hive/yoga': 0.46.0(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
'@graphql-mesh/fusion-runtime': 1.6.2(@types/node@25.0.2)(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-mesh/hmac-upstream-signature': 2.0.8(graphql@16.12.0)
|
||||
'@graphql-mesh/plugin-response-cache': 0.104.18(graphql@16.12.0)
|
||||
'@graphql-mesh/transport-common': 1.0.12(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
|
||||
'@graphql-tools/batch-delegate': 10.0.8(graphql@16.12.0)
|
||||
'@graphql-tools/delegate': 12.0.2(graphql@16.12.0)
|
||||
'@graphql-tools/executor-common': 1.0.5(graphql@16.12.0)
|
||||
'@graphql-tools/executor-http': 3.0.7(@types/node@25.0.2)(graphql@16.12.0)
|
||||
'@graphql-tools/federation': 4.2.6(@types/node@25.0.2)(graphql@16.12.0)
|
||||
'@graphql-tools/stitch': 10.1.6(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.11.0(graphql@16.12.0)
|
||||
'@graphql-tools/wrap': 11.1.2(graphql@16.12.0)
|
||||
'@graphql-yoga/plugin-apollo-usage-report': 0.12.1(@envelop/core@5.5.1)(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)
|
||||
'@graphql-yoga/plugin-csrf-prevention': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))
|
||||
'@graphql-yoga/plugin-defer-stream': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)
|
||||
'@graphql-yoga/plugin-persisted-operations': 3.16.2(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)
|
||||
'@types/node': 25.0.2
|
||||
'@whatwg-node/disposablestack': 0.0.6
|
||||
'@whatwg-node/promise-helpers': 1.3.2
|
||||
'@whatwg-node/server': 0.10.17
|
||||
'@whatwg-node/server-plugin-cookies': 1.0.5
|
||||
graphql: 16.12.0
|
||||
graphql-ws: 6.0.6(graphql@16.12.0)(ws@8.18.0)
|
||||
graphql-yoga: 5.17.1(graphql@16.12.0)
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@fastify/websocket'
|
||||
- '@logtape/logtape'
|
||||
- '@nats-io/nats-core'
|
||||
- crossws
|
||||
- ioredis
|
||||
- pino
|
||||
- uWebSockets.js
|
||||
- winston
|
||||
- ws
|
||||
|
||||
'@graphql-hive/gateway-runtime@2.5.0(graphql@16.9.0)(ioredis@5.8.2)(pino@10.3.0)(ws@8.18.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
|
|
@ -22911,6 +22987,45 @@ snapshots:
|
|||
- winston
|
||||
- ws
|
||||
|
||||
'@graphql-hive/plugin-opentelemetry@1.3.0(encoding@0.1.13)(graphql@16.12.0)(pino@10.3.0)(ws@8.18.0)':
|
||||
dependencies:
|
||||
'@graphql-hive/core': 0.18.0(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-hive/gateway-runtime': 2.5.0(graphql@16.12.0)(pino@10.3.0)(ws@8.18.0)
|
||||
'@graphql-hive/logger': 1.0.9(pino@10.3.0)
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
'@graphql-mesh/transport-common': 1.0.12(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.11.0(graphql@16.12.0)
|
||||
'@opentelemetry/api': 1.9.0
|
||||
'@opentelemetry/api-logs': 0.208.0
|
||||
'@opentelemetry/auto-instrumentations-node': 0.67.2(@opentelemetry/api@1.9.0)(@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0))(encoding@0.1.13)
|
||||
'@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/exporter-trace-otlp-grpc': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/exporter-trace-otlp-http': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/instrumentation': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-node': 0.208.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0)
|
||||
'@opentelemetry/semantic-conventions': 1.38.0
|
||||
'@whatwg-node/promise-helpers': 1.3.2
|
||||
graphql: 16.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@fastify/websocket'
|
||||
- '@logtape/logtape'
|
||||
- '@nats-io/nats-core'
|
||||
- crossws
|
||||
- encoding
|
||||
- ioredis
|
||||
- pino
|
||||
- supports-color
|
||||
- uWebSockets.js
|
||||
- winston
|
||||
- ws
|
||||
|
||||
'@graphql-hive/plugin-opentelemetry@1.3.0(encoding@0.1.13)(graphql@16.9.0)(ioredis@5.8.2)(pino@10.3.0)(ws@8.18.0)':
|
||||
dependencies:
|
||||
'@graphql-hive/core': 0.18.0(graphql@16.9.0)(pino@10.3.0)
|
||||
|
|
@ -23391,6 +23506,37 @@ snapshots:
|
|||
- pino
|
||||
- winston
|
||||
|
||||
'@graphql-mesh/fusion-runtime@1.6.2(@types/node@25.0.2)(graphql@16.12.0)(pino@10.3.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
'@envelop/instrumentation': 1.0.0
|
||||
'@graphql-hive/logger': 1.0.9(pino@10.3.0)
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
'@graphql-mesh/transport-common': 1.0.12(graphql@16.12.0)(pino@10.3.0)
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
|
||||
'@graphql-tools/batch-execute': 10.0.4(graphql@16.12.0)
|
||||
'@graphql-tools/delegate': 12.0.2(graphql@16.12.0)
|
||||
'@graphql-tools/executor': 1.4.13(graphql@16.12.0)
|
||||
'@graphql-tools/federation': 4.2.6(@types/node@25.0.2)(graphql@16.12.0)
|
||||
'@graphql-tools/merge': 9.1.5(graphql@16.12.0)
|
||||
'@graphql-tools/stitch': 10.1.6(graphql@16.12.0)
|
||||
'@graphql-tools/stitching-directives': 4.0.8(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.11.0(graphql@16.12.0)
|
||||
'@graphql-tools/wrap': 11.1.2(graphql@16.12.0)
|
||||
'@whatwg-node/disposablestack': 0.0.6
|
||||
'@whatwg-node/promise-helpers': 1.3.2
|
||||
graphql: 16.12.0
|
||||
graphql-yoga: 5.17.1(graphql@16.12.0)
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@logtape/logtape'
|
||||
- '@nats-io/nats-core'
|
||||
- '@types/node'
|
||||
- ioredis
|
||||
- pino
|
||||
- winston
|
||||
|
||||
'@graphql-mesh/fusion-runtime@1.6.2(@types/node@25.0.2)(graphql@16.9.0)(ioredis@5.8.2)(pino@10.3.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
|
|
@ -23422,6 +23568,21 @@ snapshots:
|
|||
- pino
|
||||
- winston
|
||||
|
||||
'@graphql-mesh/hmac-upstream-signature@2.0.8(graphql@16.12.0)':
|
||||
dependencies:
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
|
||||
'@graphql-tools/executor-common': 1.0.5(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.10.3(graphql@16.12.0)
|
||||
'@whatwg-node/promise-helpers': 1.3.2
|
||||
graphql: 16.12.0
|
||||
json-stable-stringify: 1.3.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@nats-io/nats-core'
|
||||
- ioredis
|
||||
|
||||
'@graphql-mesh/hmac-upstream-signature@2.0.8(graphql@16.12.0)(ioredis@5.8.2)':
|
||||
dependencies:
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
|
|
@ -23531,6 +23692,25 @@ snapshots:
|
|||
- '@nats-io/nats-core'
|
||||
- ioredis
|
||||
|
||||
'@graphql-mesh/plugin-response-cache@0.104.18(graphql@16.12.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
'@envelop/response-cache': 9.0.0(@envelop/core@5.5.1)(graphql@16.12.0)
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
'@graphql-mesh/string-interpolation': 0.5.9(graphql@16.12.0)
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.9.1(graphql@16.12.0)
|
||||
'@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.16.2(graphql@16.12.0))(graphql@16.12.0)
|
||||
'@whatwg-node/promise-helpers': 1.3.2
|
||||
cache-control-parser: 2.0.6
|
||||
graphql: 16.12.0
|
||||
graphql-yoga: 5.16.2(graphql@16.12.0)
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@nats-io/nats-core'
|
||||
- ioredis
|
||||
|
||||
'@graphql-mesh/plugin-response-cache@0.104.18(graphql@16.12.0)(ioredis@5.8.2)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
|
|
@ -23618,6 +23798,25 @@ snapshots:
|
|||
- pino
|
||||
- winston
|
||||
|
||||
'@graphql-mesh/transport-common@1.0.12(graphql@16.12.0)(pino@10.3.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
'@graphql-hive/logger': 1.0.9(pino@10.3.0)
|
||||
'@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2)
|
||||
'@graphql-hive/signal': 2.0.0
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-tools/executor': 1.4.13(graphql@16.12.0)
|
||||
'@graphql-tools/executor-common': 1.0.5(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.10.3(graphql@16.12.0)
|
||||
graphql: 16.12.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@logtape/logtape'
|
||||
- '@nats-io/nats-core'
|
||||
- ioredis
|
||||
- pino
|
||||
- winston
|
||||
|
||||
'@graphql-mesh/transport-common@1.0.12(graphql@16.9.0)(ioredis@5.8.2)(pino@10.3.0)':
|
||||
dependencies:
|
||||
'@envelop/core': 5.5.1
|
||||
|
|
@ -23734,6 +23933,30 @@ snapshots:
|
|||
- '@nats-io/nats-core'
|
||||
- ioredis
|
||||
|
||||
'@graphql-mesh/utils@0.104.16(graphql@16.12.0)':
|
||||
dependencies:
|
||||
'@envelop/instrumentation': 1.0.0
|
||||
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0)
|
||||
'@graphql-mesh/string-interpolation': 0.5.9(graphql@16.12.0)
|
||||
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2)
|
||||
'@graphql-tools/batch-delegate': 10.0.5(graphql@16.12.0)
|
||||
'@graphql-tools/delegate': 11.1.3(graphql@16.12.0)
|
||||
'@graphql-tools/utils': 10.9.1(graphql@16.12.0)
|
||||
'@graphql-tools/wrap': 11.0.5(graphql@16.12.0)
|
||||
'@whatwg-node/disposablestack': 0.0.6
|
||||
'@whatwg-node/fetch': 0.10.13
|
||||
'@whatwg-node/promise-helpers': 1.3.1
|
||||
dset: 3.1.4
|
||||
graphql: 16.12.0
|
||||
js-yaml: 4.1.1
|
||||
lodash.get: 4.4.2
|
||||
lodash.topath: 4.5.2
|
||||
tiny-lru: 11.4.7
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
- '@nats-io/nats-core'
|
||||
- ioredis
|
||||
|
||||
'@graphql-mesh/utils@0.104.16(graphql@16.12.0)(ioredis@5.8.2)':
|
||||
dependencies:
|
||||
'@envelop/instrumentation': 1.0.0
|
||||
|
|
@ -25285,7 +25508,7 @@ snapshots:
|
|||
'@whatwg-node/server-plugin-cookies': 1.0.5
|
||||
graphql: 16.12.0
|
||||
graphql-yoga: 5.16.2(graphql@16.12.0)
|
||||
jsonwebtoken: 9.0.2
|
||||
jsonwebtoken: 9.0.3
|
||||
jwks-rsa: 3.2.0
|
||||
tslib: 2.8.1
|
||||
transitivePeerDependencies:
|
||||
|
|
@ -36499,6 +36722,8 @@ snapshots:
|
|||
|
||||
jose@4.15.9: {}
|
||||
|
||||
jose@6.1.3: {}
|
||||
|
||||
joycon@3.1.1: {}
|
||||
|
||||
js-beautify@1.14.6:
|
||||
|
|
@ -36633,9 +36858,9 @@ snapshots:
|
|||
|
||||
jsonparse@1.3.1: {}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
jsonwebtoken@9.0.3:
|
||||
dependencies:
|
||||
jws: 3.2.2
|
||||
jws: 4.0.1
|
||||
lodash.includes: 4.3.0
|
||||
lodash.isboolean: 3.0.3
|
||||
lodash.isinteger: 4.0.4
|
||||
|
|
@ -36678,7 +36903,7 @@ snapshots:
|
|||
|
||||
just-diff@6.0.2: {}
|
||||
|
||||
jwa@1.4.2:
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
|
|
@ -36695,9 +36920,9 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
jws@3.2.2:
|
||||
jws@4.0.1:
|
||||
dependencies:
|
||||
jwa: 1.4.2
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
kafkajs@2.2.4: {}
|
||||
|
|
@ -38641,6 +38866,8 @@ snapshots:
|
|||
|
||||
nullthrows@1.1.1: {}
|
||||
|
||||
oauth4webapi@3.8.5: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
|
@ -38784,6 +39011,11 @@ snapshots:
|
|||
|
||||
opener@1.5.2: {}
|
||||
|
||||
openid-client@6.8.2:
|
||||
dependencies:
|
||||
jose: 6.1.3
|
||||
oauth4webapi: 3.8.5
|
||||
|
||||
opentelemetry-instrumentation-fetch-node@1.2.3(@opentelemetry/api@1.9.0):
|
||||
dependencies:
|
||||
'@opentelemetry/api': 1.9.0
|
||||
|
|
@ -41695,7 +41927,7 @@ snapshots:
|
|||
axios: 1.13.5(debug@4.4.1)
|
||||
dayjs: 1.11.13
|
||||
https-proxy-agent: 5.0.1
|
||||
jsonwebtoken: 9.0.2
|
||||
jsonwebtoken: 9.0.3
|
||||
qs: 6.14.2
|
||||
scmp: 2.1.0
|
||||
url-parse: 1.5.10
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
],
|
||||
"@hive/cdn-script/aws": ["./packages/services/cdn-worker/src/aws.ts"],
|
||||
"@hive/server": ["./packages/services/server/src/api.ts"],
|
||||
"@hive/server/*": ["./packages/services/server/src/*"],
|
||||
"@hive/schema": ["./packages/services/schema/src/api.ts"],
|
||||
"@hive/schema/*": ["./packages/services/schema/src/*"],
|
||||
"@hive/usage-common": ["./packages/services/usage-common/src/index.ts"],
|
||||
|
|
|
|||
Loading…
Reference in a new issue