feat: remove supertokens-core and supertokens-node (#7705)

This commit is contained in:
Laurin 2026-03-12 10:19:16 +01:00 committed by GitHub
parent a4640db48e
commit 14c73e5751
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 359 additions and 2196 deletions

View file

@ -0,0 +1,69 @@
---
'hive': major
---
**BREAKING** Remove support for `supertokens` service and replace it with native authentication solution.
## Upgrade Guide
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_REFRESH_TOKEN_KEY=`
- `SUPERTOKENS_ACCESS_TOKEN_KEY=`
### 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)"
```
### 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.

View file

@ -250,7 +250,9 @@ describe('oidc', () => {
cy.clearAllLocalStorage(); cy.clearAllLocalStorage();
cy.clearAllSessionStorage(); cy.clearAllSessionStorage();
cy.visit('/auth/oidc?id=invalid'); cy.visit('/auth/oidc?id=invalid');
cy.get('[data-cy="auth-card-header-description"]').contains('Could not find OIDC integration.'); cy.get('[data-cy="auth-card-header-description"]').contains(
'Something went wrong. Please try again',
);
}); });
describe('requireInvitation', () => { describe('requireInvitation', () => {
@ -278,7 +280,7 @@ describe('oidc', () => {
// Check if OIDC authentication failed as intended // Check if OIDC authentication failed as intended
cy.get(`a[href="/${slug}"]`).should('not.exist'); cy.get(`a[href="/${slug}"]`).should('not.exist');
cy.contains('not invited'); cy.contains('Sign in not allowed.');
}); });
}); });
}); });

View file

@ -24,7 +24,6 @@ import { deployS3, deployS3AuditLog, deployS3Mirror } from './services/s3';
import { deploySchema } from './services/schema'; import { deploySchema } from './services/schema';
import { configureSentry } from './services/sentry'; import { configureSentry } from './services/sentry';
import { configureSlackApp } from './services/slack-app'; import { configureSlackApp } from './services/slack-app';
import { deploySuperTokens } from './services/supertokens';
import { deployTokens } from './services/tokens'; import { deployTokens } from './services/tokens';
import { deployUsage } from './services/usage'; import { deployUsage } from './services/usage';
import { deployUsageIngestor } from './services/usage-ingestor'; import { deployUsageIngestor } from './services/usage-ingestor';
@ -203,7 +202,6 @@ deployWorkflows({
redis, redis,
}); });
const supertokens = deploySuperTokens(postgres, { dependencies: [dbMigrations] }, environment);
const zendesk = configureZendesk({ environment }); const zendesk = configureZendesk({ environment });
const githubApp = configureGithubApp(); const githubApp = configureGithubApp();
const slackApp = configureSlackApp(); const slackApp = configureSlackApp();
@ -222,7 +220,6 @@ const graphql = deployGraphQL({
usage, usage,
cdn, cdn,
commerce, commerce,
supertokens,
s3, s3,
s3Mirror, s3Mirror,
s3AuditLog, s3AuditLog,

View file

@ -47,7 +47,6 @@ export function deployDbMigrations({
// Since K8s job are immutable, we can't edit or ask K8s to re-run a Job, so we are doing a // Since K8s job are immutable, we can't edit or ask K8s to re-run a Job, so we are doing a
// pseudo change to an env var, which causes Pulumi to re-create the Job. // pseudo change to an env var, which causes Pulumi to re-create the Job.
IGNORE_RERUN_NONCE: force ? Date.now().toString() : '0', IGNORE_RERUN_NONCE: force ? Date.now().toString() : '0',
SUPERTOKENS_AT_HOME: '1',
}, },
}, },
[clickhouse.deployment, clickhouse.service, ...(dependencies || [])], [clickhouse.deployment, clickhouse.service, ...(dependencies || [])],

View file

@ -48,9 +48,6 @@ export function prepareEnvironment(input: {
general: { general: {
replicas: isProduction || isStaging ? 3 : 1, replicas: isProduction || isStaging ? 3 : 1,
}, },
supertokens: {
replicas: isProduction || isStaging ? 3 : 1,
},
envoy: { envoy: {
replicas: isProduction || isStaging ? 3 : 1, replicas: isProduction || isStaging ? 3 : 1,
cpuLimit: isProduction ? '1500m' : '120m', cpuLimit: isProduction ? '1500m' : '120m',

View file

@ -16,7 +16,6 @@ import { Redis } from './redis';
import { S3 } from './s3'; import { S3 } from './s3';
import { Schema } from './schema'; import { Schema } from './schema';
import { Sentry } from './sentry'; import { Sentry } from './sentry';
import { Supertokens } from './supertokens';
import { Tokens } from './tokens'; import { Tokens } from './tokens';
import { Usage } from './usage'; import { Usage } from './usage';
import { Zendesk } from './zendesk'; import { Zendesk } from './zendesk';
@ -40,7 +39,6 @@ export function deployGraphQL({
usage, usage,
commerce, commerce,
dbMigrations, dbMigrations,
supertokens,
s3, s3,
s3Mirror, s3Mirror,
s3AuditLog, s3AuditLog,
@ -68,7 +66,6 @@ export function deployGraphQL({
usage: Usage; usage: Usage;
dbMigrations: DbMigrations; dbMigrations: DbMigrations;
commerce: CommerceService; commerce: CommerceService;
supertokens: Supertokens;
zendesk: Zendesk; zendesk: Zendesk;
docker: Docker; docker: Docker;
sentry: Sentry; sentry: Sentry;
@ -144,7 +141,6 @@ export function deployGraphQL({
ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0', ZENDESK_SUPPORT: zendesk.enabled ? '1' : '0',
INTEGRATION_GITHUB: '1', INTEGRATION_GITHUB: '1',
// Auth // Auth
SUPERTOKENS_CONNECTION_URI: supertokens.localEndpoint,
AUTH_GITHUB: '1', AUTH_GITHUB: '1',
AUTH_GOOGLE: '1', AUTH_GOOGLE: '1',
AUTH_ORGANIZATION_OIDC: '1', AUTH_ORGANIZATION_OIDC: '1',
@ -155,7 +151,6 @@ export function deployGraphQL({
? observability.tracingEndpoint ? observability.tracingEndpoint
: '', : '',
S3_MIRROR: '1', S3_MIRROR: '1',
SUPERTOKENS_AT_HOME: '1',
}, },
exposesMetrics: true, exposesMetrics: true,
port: 4000, port: 4000,
@ -209,7 +204,6 @@ export function deployGraphQL({
.withSecret('S3_AUDIT_LOG_BUCKET_NAME', s3AuditLog.secret, 'bucket') .withSecret('S3_AUDIT_LOG_BUCKET_NAME', s3AuditLog.secret, 'bucket')
.withSecret('S3_AUDIT_LOG_ENDPOINT', s3AuditLog.secret, 'endpoint') .withSecret('S3_AUDIT_LOG_ENDPOINT', s3AuditLog.secret, 'endpoint')
// Auth // Auth
.withSecret('SUPERTOKENS_API_KEY', supertokens.secret, 'apiKey')
.withSecret('AUTH_GITHUB_CLIENT_ID', githubOAuthSecret, 'clientId') .withSecret('AUTH_GITHUB_CLIENT_ID', githubOAuthSecret, 'clientId')
.withSecret('AUTH_GITHUB_CLIENT_SECRET', githubOAuthSecret, 'clientSecret') .withSecret('AUTH_GITHUB_CLIENT_SECRET', githubOAuthSecret, 'clientSecret')
.withSecret('AUTH_GOOGLE_CLIENT_ID', googleOAuthSecret, 'clientId') .withSecret('AUTH_GOOGLE_CLIENT_ID', googleOAuthSecret, 'clientId')

View file

@ -1,109 +0,0 @@
import * as kx from '@pulumi/kubernetesx';
import * as pulumi from '@pulumi/pulumi';
import * as random from '@pulumi/random';
import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { createService } from '../utils/service-deployment';
import { Environment } from './environment';
import { Postgres } from './postgres';
export class SupertokensSecret extends ServiceSecret<{
apiKey: string | pulumi.Output<string>;
}> {}
export function deploySuperTokens(
postgres: Postgres,
resourceOptions: {
dependencies: pulumi.Resource[];
},
environment: Environment,
) {
const supertokensApiKey = new random.RandomPassword('supertokens-api-key', {
length: 31,
special: false,
}).result;
const secret = new SupertokensSecret('supertokens', {
apiKey: supertokensApiKey,
});
const port = 3567;
const pb = new kx.PodBuilder({
restartPolicy: 'Always',
containers: [
{
image: 'registry.supertokens.io/supertokens/supertokens-postgresql:9.3',
name: 'supertokens',
ports: {
http: port,
},
startupProbe: {
initialDelaySeconds: 15,
periodSeconds: 20,
failureThreshold: 5,
timeoutSeconds: 5,
httpGet: {
path: '/hello',
port,
},
},
readinessProbe: {
initialDelaySeconds: 5,
periodSeconds: 20,
failureThreshold: 5,
timeoutSeconds: 5,
httpGet: {
path: '/hello',
port,
},
},
livenessProbe: {
initialDelaySeconds: 3,
periodSeconds: 20,
failureThreshold: 10,
timeoutSeconds: 5,
httpGet: {
path: '/hello',
port,
},
},
env: {
POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens',
POSTGRESQL_CONNECTION_URI: {
secretKeyRef: {
name: postgres.secret.record.metadata.name,
key: 'connectionStringPostgresql',
},
},
API_KEYS: {
secretKeyRef: {
name: secret.record.metadata.name,
key: 'apiKey',
},
},
},
},
],
});
const deployment = new kx.Deployment(
'supertokens',
{
spec: pb.asDeploymentSpec({ replicas: environment.podsConfig.supertokens.replicas }),
},
{
dependsOn: resourceOptions.dependencies,
},
);
const service = createService('supertokens', deployment);
return {
deployment,
service,
localEndpoint: serviceLocalEndpoint(service),
secret,
};
}
export type Supertokens = ReturnType<typeof deploySuperTokens>;

View file

@ -89,22 +89,6 @@ services:
volumes: volumes:
- './.hive/redis/db:/bitnami/redis/data' - './.hive/redis/db:/bitnami/redis/data'
supertokens:
image: registry.supertokens.io/supertokens/supertokens-postgresql:9.3
depends_on:
db:
condition: service_healthy
networks:
- 'stack'
environment:
POSTGRESQL_USER: '${POSTGRES_USER}'
POSTGRESQL_PASSWORD: '${POSTGRES_PASSWORD}'
POSTGRESQL_DATABASE_NAME: '${POSTGRES_DB}'
POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens'
POSTGRESQL_HOST: db
POSTGRESQL_PORT: 5432
API_KEYS: '${SUPERTOKENS_API_KEY}'
s3: s3:
image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z image: quay.io/minio/minio:RELEASE.2025-09-07T16-13-09Z
command: server /data --console-address ":9001" command: server /data --console-address ":9001"
@ -234,9 +218,9 @@ services:
# Auth # Auth
AUTH_ORGANIZATION_OIDC: '1' AUTH_ORGANIZATION_OIDC: '1'
AUTH_REQUIRE_EMAIL_VERIFICATION: '0' AUTH_REQUIRE_EMAIL_VERIFICATION: '0'
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082 GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
SUPERTOKENS_REFRESH_TOKEN_KEY: '${SUPERTOKENS_REFRESH_TOKEN_KEY}'
SUPERTOKENS_ACCESS_TOKEN_KEY: '${SUPERTOKENS_ACCESS_TOKEN_KEY}'
# Tracing # Tracing
OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}' OPENTELEMETRY_COLLECTOR_ENDPOINT: '${OPENTELEMETRY_COLLECTOR_ENDPOINT:-}'
SENTRY: '${SENTRY:-0}' SENTRY: '${SENTRY:-0}'

View file

@ -123,25 +123,6 @@ services:
volumes: volumes:
- ./.hive-dev/broker/db:/var/lib/kafka/data - ./.hive-dev/broker/db:/var/lib/kafka/data
supertokens:
image: registry.supertokens.io/supertokens/supertokens-postgresql:9.3
mem_limit: 300m
depends_on:
db:
condition: service_healthy
networks:
- 'stack'
ports:
- '3567:3567'
environment:
POSTGRESQL_USER: postgres
POSTGRESQL_PASSWORD: postgres
POSTGRESQL_DATABASE_NAME: registry
POSTGRESQL_TABLE_NAMES_PREFIX: 'supertokens'
POSTGRESQL_HOST: db
POSTGRESQL_PORT: 5432
API_KEYS: bubatzbieber6942096420
oidc-server-mock: oidc-server-mock:
image: ghcr.io/soluto/oidc-server-mock:0.8.6 image: ghcr.io/soluto/oidc-server-mock:0.8.6
mem_limit: 200m mem_limit: 200m

View file

@ -33,10 +33,6 @@ services:
networks: networks:
- 'stack' - 'stack'
supertokens:
ports:
- '3567:3567'
db: db:
ports: ports:
- '5432:5432' - '5432:5432'

View file

@ -46,64 +46,6 @@ Add "user" field to ./docker/docker-compose.dev.yml
- Open the UI (`http://localhost:3000` by default) and Sign in with any of the identity provider - Open the UI (`http://localhost:3000` by default) and Sign in with any of the identity provider
- Once this is done, you should be able to log in and use the project - Once this is done, you should be able to log in and use the project
#### Possible Issues During Setup
If you encounter an error such as:
```
error: relation "supertokens_*" does not exist
```
it usually means that the **Supertokens database tables** were not initialized correctly.
##### Steps to fix it
1. **Ensure no local PostgreSQL instance is running**
- The local PostgreSQL service on your machine might conflict with the one running in Docker.
- Stop any locally running PostgreSQL service and make sure the database used by Hive is the one
from Docker Compose.
2. **Handle possible race conditions between `db` and `supertokens` containers**
- This issue may occur if `supertokens` starts before the `db` container is fully initialized.
- To fix:
1. Stop all running containers:
```bash
docker compose -f ./docker/docker-compose.dev.yml down
```
2. Start only the database:
```bash
docker compose -f ./docker/docker-compose.dev.yml up db
```
3. Wait until the database is ready (you should see “database system is ready to accept
connections” in logs).
4. Start the `supertokens` service:
```bash
docker compose -f ./docker/docker-compose.dev.yml up supertokens
```
5. Once `supertokens` successfully creates all the tables, start the rest of the containers:
```bash
docker compose -f ./docker/docker-compose.dev.yml up -d
```
3. **If only Supertokens tables were created**
- Run the setup command again to ensure all services are initialized properly:
```bash
pnpm local:setup
```
- Then restart the Hive Console using the VSCode “Start Hive” button.
After completing these steps, reload the Hive UI at [http://localhost:3000](http://localhost:3000),
and you should be able to log in successfully.
- Once you generate the token against your organization/personal account in hive, the same can be
added locally to `hive.json` within `packages/libraries/cli` which can be used to interact via the
hive cli with the registry (Use `http://localhost:3001/graphql` as the `registry.endpoint` value
in `hive.json`)
- Now you can use Hive locally. All other steps in this document are optional and only necessary if
you work on specific features.
## Development Seed ## Development Seed
We have a script to feed your local instance of Hive with initial seed data. This step is optional. We have a script to feed your local instance of Hive with initial seed data. This step is optional.

View file

@ -5,7 +5,6 @@ actor "CLI User" as cliuser
actor "Running Server" as gqlserver actor "Running Server" as gqlserver
component zookeeper component zookeeper
component supertokens
queue kafka queue kafka
@ -28,7 +27,6 @@ storageSvc ---d-> Postgres
kafka -l-> zookeeper kafka -l-> zookeeper
app --> supertokens
app --> server app --> server
app --> emails app --> emails
@ -49,9 +47,6 @@ server -d-> tokens
server -d-> webhooks server -d-> webhooks
server -d-> schema server -d-> schema
server -d-> emails server -d-> emails
server -d-> supertokens
supertokens -> Postgres
uiuser --> app uiuser --> app
cliuser --> server cliuser --> server
@ -60,4 +55,4 @@ emails -[hidden]-> webhooks
usage ----> tokens usage ----> tokens
@enduml @enduml

View file

@ -24,6 +24,5 @@ CLICKHOUSE_ASYNC_INSERT_MAX_DATA_SIZE=1000
EXTERNAL_COMPOSITION_SECRET=secretsecret EXTERNAL_COMPOSITION_SECRET=secretsecret
LIMIT_CACHE_UPDATE_INTERVAL_MS=2000 LIMIT_CACHE_UPDATE_INTERVAL_MS=2000
NODE_OPTIONS=--enable-source-maps NODE_OPTIONS=--enable-source-maps
SUPERTOKENS_AT_HOME=0
SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0 SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0
SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|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 SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|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

View file

@ -165,9 +165,10 @@ services:
WEB_APP_URL: '${HIVE_APP_BASE_URL}' WEB_APP_URL: '${HIVE_APP_BASE_URL}'
AUTH_ORGANIZATION_OIDC: '1' AUTH_ORGANIZATION_OIDC: '1'
AUTH_REQUIRE_EMAIL_VERIFICATION: '1' AUTH_REQUIRE_EMAIL_VERIFICATION: '1'
SUPERTOKENS_CONNECTION_URI: http://supertokens:3567
SUPERTOKENS_API_KEY: '${SUPERTOKENS_API_KEY}'
GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082 GRAPHQL_PUBLIC_ORIGIN: http://localhost:8082
SUPERTOKENS_RATE_LIMIT: '0'
SUPERTOKENS_REFRESH_TOKEN_KEY: '1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0'
SUPERTOKENS_ACCESS_TOKEN_KEY: 's-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|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'
extra_hosts: extra_hosts:
- 'host.docker.internal:host-gateway' - 'host.docker.internal:host-gateway'
@ -212,10 +213,6 @@ services:
- ./.hive/clickhouse/db:/var/lib/clickhouse - ./.hive/clickhouse/db:/var/lib/clickhouse
- ./configs/clickhouse:/etc/clickhouse-server/conf.d - ./configs/clickhouse:/etc/clickhouse-server/conf.d
supertokens:
ports:
- '3567:3567'
usage: usage:
environment: environment:
COMMERCE_ENDPOINT: '${COMMERCE_ENDPOINT}' COMMERCE_ENDPOINT: '${COMMERCE_ENDPOINT}'

View file

@ -12,8 +12,6 @@ const __dirname = import.meta.dirname;
const serverEnvVars = parse(readFileSync(__dirname + '/../packages/services/server/.env', 'utf-8')); const serverEnvVars = parse(readFileSync(__dirname + '/../packages/services/server/.env', 'utf-8'));
applyEnv({ applyEnv({
SUPERTOKENS_CONNECTION_URI: serverEnvVars.SUPERTOKENS_CONNECTION_URI,
SUPERTOKENS_API_KEY: serverEnvVars.SUPERTOKENS_API_KEY,
POSTGRES_USER: serverEnvVars.POSTGRES_USER, POSTGRES_USER: serverEnvVars.POSTGRES_USER,
POSTGRES_PASSWORD: serverEnvVars.POSTGRES_PASSWORD, POSTGRES_PASSWORD: serverEnvVars.POSTGRES_PASSWORD,
POSTGRES_DB: serverEnvVars.POSTGRES_DB, POSTGRES_DB: serverEnvVars.POSTGRES_DB,
@ -24,7 +22,6 @@ applyEnv({
CLICKHOUSE_USER: serverEnvVars.CLICKHOUSE_USERNAME, CLICKHOUSE_USER: serverEnvVars.CLICKHOUSE_USERNAME,
CLICKHOUSE_PASSWORD: serverEnvVars.CLICKHOUSE_PASSWORD, CLICKHOUSE_PASSWORD: serverEnvVars.CLICKHOUSE_PASSWORD,
HIVE_ENCRYPTION_SECRET: serverEnvVars.HIVE_ENCRYPTION_SECRET, HIVE_ENCRYPTION_SECRET: serverEnvVars.HIVE_ENCRYPTION_SECRET,
SUPERTOKENS_AT_HOME: serverEnvVars.SUPERTOKENS_AT_HOME,
SUPERTOKENS_REFRESH_TOKEN_KEY: serverEnvVars.SUPERTOKENS_REFRESH_TOKEN_KEY, SUPERTOKENS_REFRESH_TOKEN_KEY: serverEnvVars.SUPERTOKENS_REFRESH_TOKEN_KEY,
SUPERTOKENS_ACCESS_TOKEN_KEY: serverEnvVars.SUPERTOKENS_ACCESS_TOKEN_KEY, SUPERTOKENS_ACCESS_TOKEN_KEY: serverEnvVars.SUPERTOKENS_ACCESS_TOKEN_KEY,
}); });

View file

@ -1,5 +1,4 @@
import { DatabasePool } from 'slonik'; import { DatabasePool } from 'slonik';
import { z } from 'zod';
import { import {
AccessTokenKeyContainer, AccessTokenKeyContainer,
hashPassword, hashPassword,
@ -9,62 +8,8 @@ import { NoopLogger } from '@hive/api/modules/shared/providers/logger';
import type { InternalApi } from '@hive/server'; import type { InternalApi } from '@hive/server';
import { createNewSession } from '@hive/server/supertokens-at-home/shared'; import { createNewSession } from '@hive/server/supertokens-at-home/shared';
import { createTRPCProxyClient, httpLink } from '@trpc/client'; import { createTRPCProxyClient, httpLink } from '@trpc/client';
import { ensureEnv } from './env';
import { getServiceHost } from './utils'; import { getServiceHost } from './utils';
const SignUpSignInUserResponseModel = z
.object({
status: z.literal('OK'),
user: z.object({
emails: z.array(z.string()),
id: z.string(),
timeJoined: z.number(),
}),
})
.refine(response => response.user.emails.length === 1)
.transform(response => ({
...response,
user: {
id: response.user.id,
email: response.user.emails[0],
timeJoined: response.user.timeJoined,
},
}));
const signUpUserViaEmail = async (
email: string,
password: string,
): Promise<z.TypeOf<typeof SignUpSignInUserResponseModel>> => {
try {
const response = await fetch(
`${ensureEnv('SUPERTOKENS_CONNECTION_URI')}/appid-public/public/recipe/signup`,
{
method: 'POST',
headers: {
'content-type': 'application/json; charset=UTF-8',
'api-key': ensureEnv('SUPERTOKENS_API_KEY'),
'cdi-version': '4.0',
},
body: JSON.stringify({
email,
password,
}),
},
);
const body = await response.text();
if (response.status !== 200) {
throw new Error(`Signup failed. ${response.status}.\n ${body}`);
}
return SignUpSignInUserResponseModel.parse(JSON.parse(body));
} catch (e) {
console.warn(`Failed to sign up:`, e);
throw e;
}
};
const createSessionAtHome = async ( const createSessionAtHome = async (
supertokensStore: SuperTokensStore, supertokensStore: SuperTokensStore,
superTokensUserId: string, superTokensUserId: string,
@ -116,103 +61,6 @@ const createSessionAtHome = async (
}; };
}; };
const createSessionPayload = (payload: {
superTokensUserId: string;
userId: string;
oidcIntegrationId: string | null;
email: string;
}) => ({
version: '2',
superTokensUserId: payload.superTokensUserId,
userId: payload.userId,
oidcIntegrationId: payload.oidcIntegrationId,
email: payload.email,
});
const CreateSessionModel = z.object({
accessToken: z.object({
token: z.string(),
}),
refreshToken: z.object({
token: z.string(),
}),
});
const createSession = async (
superTokensUserId: string,
email: string,
oidcIntegrationId: string | null,
) => {
try {
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 sessionData = createSessionPayload({
superTokensUserId,
userId: ensureUserResult.user.id,
oidcIntegrationId,
email,
});
const payload = {
enableAntiCsrf: false,
userId: superTokensUserId,
userDataInDatabase: sessionData,
userDataInJWT: sessionData,
};
const response = await fetch(
`${ensureEnv('SUPERTOKENS_CONNECTION_URI')}/appid-public/public/recipe/session`,
{
method: 'POST',
headers: {
'content-type': 'application/json; charset=UTF-8',
'api-key': ensureEnv('SUPERTOKENS_API_KEY'),
rid: 'session',
'cdi-version': '4.0',
},
body: JSON.stringify(payload),
},
);
const body = await response.text();
if (response.status !== 200) {
throw new Error(`Create session failed. ${response.status}.\n ${body}`);
}
const data = CreateSessionModel.parse(JSON.parse(body));
/**
* These are the required cookies that need to be set.
*/
return {
access_token: data.accessToken.token,
refresh_token: data.refreshToken.token,
};
} catch (e) {
console.warn(`Failed to create session:`, e);
throw e;
}
};
const password = 'ilikebigturtlesandicannotlie47'; const password = 'ilikebigturtlesandicannotlie47';
const hashedPassword = await hashPassword(password); const hashedPassword = await hashPassword(password);
@ -232,34 +80,14 @@ export async function authenticate(
email: string, email: string,
oidcIntegrationId?: string, oidcIntegrationId?: string,
): Promise<{ access_token: string; refresh_token: string; supertokensUserId: string }> { ): Promise<{ access_token: string; refresh_token: string; supertokensUserId: string }> {
if (process.env.SUPERTOKENS_AT_HOME === '1') { const supertokensStore = new SuperTokensStore(pool, new NoopLogger());
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]) { if (!tokenResponsePromise[email]) {
tokenResponsePromise[email] = signUpUserViaEmail(email, password).then(res => ({ tokenResponsePromise[email] = supertokensStore.createEmailPasswordUser({
email: res.user.email, email,
userId: res.user.id, passwordHash: hashedPassword,
})); });
} }
return tokenResponsePromise[email]!.then(async data => ({ const user = await tokenResponsePromise[email]!;
...(await createSession(data.userId, data.email, oidcIntegrationId ?? null)), return await createSessionAtHome(supertokensStore, user.userId, email, oidcIntegrationId ?? null);
supertokensUserId: data.userId,
}));
} }

View file

@ -314,7 +314,7 @@ export async function createOIDCIntegration(args: {
.parse(rawBody); .parse(rawBody);
const cookies = setCookie.parse(result.headers.getSetCookie()); const cookies = setCookie.parse(result.headers.getSetCookie());
return { return {
accessToken: cookies.find(c => c.name === 'sAccessToken')?.value ?? ('' as string), accessToken: cookies.find(c => c.name === 'sAccessToken')?.value ?? '',
user: { user: {
id: body.user.id, id: body.user.id,
email: body.user.emails[0], email: body.user.emails[0],

View file

@ -119,7 +119,6 @@
"overrides.ip": "There is no update with fix for ip package, we use fork https://github.com/indutny/node-ip/issues/150#issuecomment-2325961380", "overrides.ip": "There is no update with fix for ip package, we use fork https://github.com/indutny/node-ip/issues/150#issuecomment-2325961380",
"overrides.miniflare@3>undici": "To address CVE: https://github.com/graphql-hive/console/security/dependabot/439", "overrides.miniflare@3>undici": "To address CVE: https://github.com/graphql-hive/console/security/dependabot/439",
"overrides.tar-fs": "https://github.com/graphql-hive/console/security/dependabot/290", "overrides.tar-fs": "https://github.com/graphql-hive/console/security/dependabot/290",
"overrides.nodemailer@^6.0.0": "supertokens-node override for vulnerable version",
"overrides.@types/nodemailer>@aws-sdk/client-sesv2": "@types/nodemailer depends on some AWS stuff that causes the 3.x.x version to stick around. We don't need that dependency. (https://github.com/graphql-hive/console/security/dependabot/436)", "overrides.@types/nodemailer>@aws-sdk/client-sesv2": "@types/nodemailer depends on some AWS stuff that causes the 3.x.x version to stick around. We don't need that dependency. (https://github.com/graphql-hive/console/security/dependabot/436)",
"overrides.tar@6.x.x": "address https://github.com/graphql-hive/console/security/dependabot/443", "overrides.tar@6.x.x": "address https://github.com/graphql-hive/console/security/dependabot/443",
"overrides.diff@<8.0.3": "address https://github.com/graphql-hive/console/security/dependabot/438", "overrides.diff@<8.0.3": "address https://github.com/graphql-hive/console/security/dependabot/438",
@ -148,7 +147,6 @@
"@tailwindcss/node>tailwindcss": "4.1.18", "@tailwindcss/node>tailwindcss": "4.1.18",
"@tailwindcss/vite>tailwindcss": "4.1.18", "@tailwindcss/vite>tailwindcss": "4.1.18",
"estree-util-value-to-estree": "^3.3.3", "estree-util-value-to-estree": "^3.3.3",
"nodemailer@^6.0.0": "^7.0.11",
"@types/nodemailer>@aws-sdk/client-sesv2": "-", "@types/nodemailer>@aws-sdk/client-sesv2": "-",
"tar@6.x.x": "^7.5.11", "tar@6.x.x": "^7.5.11",
"diff@<8.0.3": "^8.0.3", "diff@<8.0.3": "^8.0.3",

View file

@ -1,9 +0,0 @@
import { type MigrationExecutor } from '../pg-migrator';
export default {
name: '2024.11.11T00-00-00.supertokens-8.0.ts',
run: ({ sql }) => sql`
ALTER TABLE IF EXISTS "supertokens_user_roles"
DROP CONSTRAINT IF EXISTS "supertokens_user_roles_role_fkey";
`,
} satisfies MigrationExecutor;

View file

@ -1,11 +0,0 @@
import { type MigrationExecutor } from '../pg-migrator';
export default {
name: '2024.11.12T00-00-00.supertokens-9.0.ts',
run: ({ sql }) => sql`
ALTER TABLE IF EXISTS "supertokens_totp_user_devices"
ADD COLUMN IF NOT EXISTS "created_at" BIGINT default 0;
ALTER TABLE IF EXISTS "totp_user_devices"
ALTER COLUMN "created_at" DROP DEFAULT;
`,
} satisfies MigrationExecutor;

View file

@ -1,11 +0,0 @@
import { type MigrationExecutor } from '../pg-migrator';
export default {
name: '2024.11.12T00-00-00.supertokens-9.1.ts',
run: ({ sql }) => sql`
ALTER TABLE IF EXISTS "supertokens_tenant_configs"
ADD COLUMN IF NOT EXISTS "is_first_factors_null" BOOLEAN DEFAULT TRUE;
ALTER TABLE IF EXISTS "supertokens_tenant_configs"
ALTER COLUMN "is_first_factors_null" DROP DEFAULT;
`,
} satisfies MigrationExecutor;

View file

@ -1,15 +0,0 @@
import { type MigrationExecutor } from '../pg-migrator';
export default {
name: '2024.11.12T00-00-00.supertokens-9.2.ts',
run: ({ sql }) => sql`
DO $$
BEGIN
IF (SELECT to_regclass('supertokens_user_last_active') IS NOT null)
THEN
CREATE INDEX IF NOT EXISTS "supertokens_user_last_active_last_active_time_index"
ON "supertokens_user_last_active" ("last_active_time" DESC, "app_id" DESC);
END IF;
END $$;
`,
} satisfies MigrationExecutor;

View file

@ -1,66 +0,0 @@
import { type MigrationExecutor } from '../pg-migrator';
export default {
name: '2024.11.12T00-00-00.supertokens-9.3.ts',
run: ({ sql }) => sql`
DO $$
BEGIN
IF (SELECT to_regclass('supertokens_apps') IS NOT null)
THEN
CREATE TABLE IF NOT EXISTS "supertokens_oauth_clients" (
"app_id" VARCHAR(64),
"client_id" VARCHAR(255) NOT NULL,
"is_client_credentials_only" BOOLEAN NOT NULL,
PRIMARY KEY ("app_id", "client_id"),
FOREIGN KEY("app_id") REFERENCES "supertokens_apps"("app_id") ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS "supertokens_oauth_sessions" (
"gid" VARCHAR(255),
"app_id" VARCHAR(64) DEFAULT 'public',
"client_id" VARCHAR(255) NOT NULL,
"session_handle" VARCHAR(128),
"external_refresh_token" VARCHAR(255) UNIQUE,
"internal_refresh_token" VARCHAR(255) UNIQUE,
"jti" TEXT NOT NULL,
"exp" BIGINT NOT NULL,
PRIMARY KEY ("gid"),
FOREIGN KEY("app_id", "client_id") REFERENCES "supertokens_oauth_clients"("app_id", "client_id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "supertokens_oauth_session_exp_index"
ON "supertokens_oauth_sessions"("exp" DESC);
CREATE INDEX IF NOT EXISTS "supertokens_oauth_session_external_refresh_token_index"
ON "supertokens_oauth_sessions"("app_id", "external_refresh_token" DESC);
CREATE TABLE IF NOT EXISTS "supertokens_oauth_m2m_tokens" (
"app_id" VARCHAR(64) DEFAULT 'public',
"client_id" VARCHAR(255) NOT NULL,
"iat" BIGINT NOT NULL,
"exp" BIGINT NOT NULL,
PRIMARY KEY ("app_id", "client_id", "iat"),
FOREIGN KEY("app_id", "client_id") REFERENCES "supertokens_oauth_clients"("app_id", "client_id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "supertokens_oauth_m2m_token_iat_index"
ON "supertokens_oauth_m2m_tokens"("iat" DESC, "app_id" DESC);
CREATE INDEX IF NOT EXISTS "supertokens_oauth_m2m_token_exp_index"
ON "supertokens_oauth_m2m_tokens"("exp" DESC);
CREATE TABLE IF NOT EXISTS "supertokens_oauth_logout_challenges" (
"app_id" VARCHAR(64) DEFAULT 'public',
"challenge" VARCHAR(128) NOT NULL,
"client_id" VARCHAR(255) NOT NULL,
"post_logout_redirect_uri" VARCHAR(1024),
"session_handle" VARCHAR(128),
"state" VARCHAR(128),
"time_created" BIGINT NOT NULL,
PRIMARY KEY ("app_id", "challenge"),
FOREIGN KEY("app_id", "client_id") REFERENCES "supertokens_oauth_clients"("app_id", "client_id") ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS "oauth_logout_challenges_time_created_index"
ON "supertokens_oauth_logout_challenges"("time_created" DESC);
END IF;
END $$;
`,
} satisfies MigrationExecutor;

View file

@ -34,7 +34,6 @@ const EnvironmentModel = zod.object({
.union([zod.literal('1'), zod.literal('0')]) .union([zod.literal('1'), zod.literal('0')])
.optional(), .optional(),
GRAPHQL_HIVE_ENVIRONMENT: emptyString(zod.enum(['prod', 'staging', 'dev']).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({ const PostgresModel = zod.object({
@ -115,5 +114,4 @@ export const env = {
isClickHouseMigrator: base.CLICKHOUSE_MIGRATOR === 'up', isClickHouseMigrator: base.CLICKHOUSE_MIGRATOR === 'up',
isHiveCloud: base.CLICKHOUSE_MIGRATOR_GRAPHQL_HIVE_CLOUD === '1', isHiveCloud: base.CLICKHOUSE_MIGRATOR_GRAPHQL_HIVE_CLOUD === '1',
hiveCloudEnvironment: base.GRAPHQL_HIVE_ENVIRONMENT ?? null, hiveCloudEnvironment: base.GRAPHQL_HIVE_ENVIRONMENT ?? null,
useSupertokensAtHome: base.SUPERTOKENS_AT_HOME === '1',
} as const; } as const;

View file

@ -67,7 +67,6 @@ 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_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_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 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'; import { runMigrations } from './pg-migrator';
export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: string }) => export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: string }) =>
@ -143,11 +142,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
migration_2024_07_16T13_44_00_oidc_only_access, migration_2024_07_16T13_44_00_oidc_only_access,
migration_2024_07_17T00_00_00_app_deployments, migration_2024_07_17T00_00_00_app_deployments,
migration_2024_07_23T_09_36_00_schema_cleanup_tracker, migration_2024_07_23T_09_36_00_schema_cleanup_tracker,
await import('./actions/2024.11.11T00-00-00.supertokens-8.0'),
await import('./actions/2024.11.12T00-00-00.supertokens-9.0'),
await import('./actions/2024.11.12T00-00-00.supertokens-9.1'),
await import('./actions/2024.11.12T00-00-00.supertokens-9.2'),
await import('./actions/2024.11.12T00-00-00.supertokens-9.3'),
await import('./actions/2024.12.23T00-00-00.improve-version-index'), await import('./actions/2024.12.23T00-00-00.improve-version-index'),
await import('./actions/2024.12.24T00-00-00.improve-version-index-2'), await import('./actions/2024.12.24T00-00-00.improve-version-index-2'),
await import('./actions/2024.12.27T00.00.00.create-preflight-scripts'), await import('./actions/2024.12.27T00.00.00.create-preflight-scripts'),
@ -185,10 +179,8 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
await import('./actions/2026.01.30T00-00-00.account-linking'), await import('./actions/2026.01.30T00-00-00.account-linking'),
await import('./actions/2026.02.06T00-00-00.zendesk-unique'), await import('./actions/2026.02.06T00-00-00.zendesk-unique'),
await import('./actions/2026.01.30T10-00-00.oidc-require-invitation'), await import('./actions/2026.01.30T10-00-00.oidc-require-invitation'),
await import('./actions/2026.02.18T00-00-00.ensure-supertokens-tables'),
await import('./actions/2026.02.19T00-00-00.saved-filter-permission'), await import('./actions/2026.02.19T00-00-00.saved-filter-permission'),
...(env.useSupertokensAtHome
? [await import('./actions/2026.02.18T00-00-00.ensure-supertokens-tables')]
: []),
await import('./actions/2026.02.24T00-00-00.proposal-composition'), await import('./actions/2026.02.24T00-00-00.proposal-composition'),
await import('./actions/2026.02.25T00-00-00.oidc-integration-domains'), await import('./actions/2026.02.25T00-00-00.oidc-integration-domains'),
], ],

View file

@ -75,7 +75,6 @@
"redlock": "5.0.0-beta.2", "redlock": "5.0.0-beta.2",
"slonik": "30.4.4", "slonik": "30.4.4",
"stripe": "17.5.0", "stripe": "17.5.0",
"supertokens-node": "16.7.5",
"tslib": "2.8.1", "tslib": "2.8.1",
"undici": "7.18.2", "undici": "7.18.2",
"vitest": "4.0.9", "vitest": "4.0.9",

View file

@ -1,9 +1,7 @@
import c from 'node:crypto'; import c from 'node:crypto';
import { parse as parseCookie } from 'cookie-es'; import { parse as parseCookie } from 'cookie-es';
import SessionNode from 'supertokens-node/recipe/session/index.js';
import * as zod from 'zod'; import * as zod from 'zod';
import type { FastifyReply, FastifyRequest } from '@hive/service-common'; import type { FastifyReply, FastifyRequest } from '@hive/service-common';
import { captureException } from '@sentry/node';
import { AccessError, HiveError, OIDCRequiredError } from '../../../shared/errors'; import { AccessError, HiveError, OIDCRequiredError } from '../../../shared/errors';
import { isUUID } from '../../../shared/is-uuid'; import { isUUID } from '../../../shared/is-uuid';
import { OIDCIntegrationStore } from '../../oidc-integrations/providers/oidc-integration.store'; import { OIDCIntegrationStore } from '../../oidc-integrations/providers/oidc-integration.store';
@ -169,7 +167,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
private storage: Storage; private storage: Storage;
private supertokensStore: SuperTokensStore; private supertokensStore: SuperTokensStore;
private emailVerification: EmailVerification | null; private emailVerification: EmailVerification | null;
private accessTokenKey: AccessTokenKeyContainer | null; private accessTokenKey: AccessTokenKeyContainer;
private oidcIntegrationStore: OIDCIntegrationStore; private oidcIntegrationStore: OIDCIntegrationStore;
constructor(deps: { constructor(deps: {
@ -177,7 +175,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
storage: Storage; storage: Storage;
organizationMembers: OrganizationMembers; organizationMembers: OrganizationMembers;
emailVerification: EmailVerification | null; emailVerification: EmailVerification | null;
accessTokenKey: AccessTokenKeyContainer | null; accessTokenKey: AccessTokenKeyContainer;
oidcIntegrationStore: OIDCIntegrationStore; oidcIntegrationStore: OIDCIntegrationStore;
}) { }) {
super(); super();
@ -189,83 +187,10 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
this.oidcIntegrationStore = deps.oidcIntegrationStore; this.oidcIntegrationStore = deps.oidcIntegrationStore;
} }
private async _verifySuperTokensCoreSession(args: { private async _verifySuperTokensAtHomeSession(args: {
req: FastifyRequest; req: FastifyRequest;
reply: FastifyReply; reply: FastifyReply;
}): Promise<SuperTokensSessionPayloadV2 | null> { }): Promise<SuperTokensSessionPayloadV2 | null> {
let session: SessionNode.SessionContainer | undefined;
try {
session = await SessionNode.getSession(args.req, args.reply, {
sessionRequired: false,
antiCsrfCheck: false,
checkDatabase: true,
});
args.req.log.debug('Session resolution ended successfully');
} catch (error) {
args.req.log.debug('Session resolution failed');
if (SessionNode.Error.isErrorFromSuperTokens(error)) {
if (
error.type === SessionNode.Error.TRY_REFRESH_TOKEN ||
error.type === SessionNode.Error.UNAUTHORISED
) {
throw new HiveError('Invalid session', {
extensions: {
code: 'NEEDS_REFRESH',
},
});
}
}
args.req.log.error('Error while resolving user');
console.log(error);
captureException(error);
throw error;
}
if (!session) {
args.req.log.debug('No session found');
return null;
}
const payload = session.getAccessTokenPayload();
const result = SuperTokensSessionPayloadModel.safeParse(payload);
if (result.success === false) {
args.req.log.error('SuperTokens session payload is invalid');
args.req.log.debug('SuperTokens session payload: %s', JSON.stringify(payload));
args.req.log.debug(
'SuperTokens session parsing errors: %s',
JSON.stringify(result.error.flatten().fieldErrors),
);
throw new HiveError('Invalid access token provided', {
extensions: {
code: 'UNAUTHENTICATED',
},
});
}
if (result.data.version === '1') {
args.req.log.debug('legacy session detected, require session refresh');
throw new HiveError('Invalid session.', {
extensions: {
code: 'NEEDS_REFRESH',
},
});
}
return result.data;
}
private async _verifySuperTokensAtHomeSession(
args: {
req: FastifyRequest;
reply: FastifyReply;
},
accessTokenKey: AccessTokenKeyContainer,
): Promise<SuperTokensSessionPayloadV2 | null> {
let session: SessionInfo | null = null; let session: SessionInfo | null = null;
args.req.log.debug('attempt parsing access token from cookie'); args.req.log.debug('attempt parsing access token from cookie');
@ -285,7 +210,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
let accessToken; let accessToken;
try { try {
accessToken = parseAccessToken(rawAccessToken, accessTokenKey.publicKey); accessToken = parseAccessToken(rawAccessToken, this.accessTokenKey.publicKey);
} catch (err) { } catch (err) {
args.req.log.debug('Failed verifying the access token. Ask for refresh. err=%s', String(err)); args.req.log.debug('Failed verifying the access token. Ask for refresh. err=%s', String(err));
throw new HiveError('Invalid session', { throw new HiveError('Invalid session', {
@ -399,9 +324,7 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
return null; return null;
} }
const sessionData = this.accessTokenKey const sessionData = await this._verifySuperTokensAtHomeSession(args);
? await this._verifySuperTokensAtHomeSession(args, this.accessTokenKey)
: await this._verifySuperTokensCoreSession(args);
if (!sessionData) { if (!sessionData) {
args.req.log.debug('No session found'); args.req.log.debug('No session found');

View file

@ -54,10 +54,6 @@ ENCRYPTION_SECRET="97e4094d2463e71a981913cca4e56788"
FEEDBACK_SLACK_TOKEN="" FEEDBACK_SLACK_TOKEN=""
FEEDBACK_SLACK_CHANNEL="#hive" FEEDBACK_SLACK_CHANNEL="#hive"
# SuperTokens
SUPERTOKENS_CONNECTION_URI=http://localhost:3567
SUPERTOKENS_API_KEY=bubatzbieber6942096420
# Organization level Open ID Connect Authentication # Organization level Open ID Connect Authentication
AUTH_ORGANIZATION_OIDC=1 AUTH_ORGANIZATION_OIDC=1
@ -85,8 +81,5 @@ OPENTELEMETRY_COLLECTOR_ENDPOINT="<sync>"
HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret HIVE_ENCRYPTION_SECRET=wowverysecuremuchsecret
## Use supertokens at home
SUPERTOKENS_AT_HOME=0
SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0 SUPERTOKENS_REFRESH_TOKEN_KEY=1000:15e5968d52a9a48921c1c63d88145441a8099b4a44248809a5e1e733411b3eeb80d87a6e10d3390468c222f6a91fef3427f8afc8b91ea1820ab10c7dfd54a268:39f72164821e08edd6ace99f3bd4e387f45fa4221fe3cd80ecfee614850bc5d647ac2fddc14462a00647fff78c22e8d01bc306a91294f5b889a90ba891bf0aa0
SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|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 SUPERTOKENS_ACCESS_TOKEN_KEY=s-aaa5da0d-8678-46ef-b56d-9cd19e1cdb5b|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

View file

@ -4,87 +4,87 @@ The GraphQL API for GraphQL Hive.
## Configuration ## Configuration
| Name | Required | Description | Example Value | | Name | Required | Description | Example Value |
| ------------------------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
| `PORT` | **Yes** | The port this service is running on. | `4013` | | `PORT` | **Yes** | The port this service is running on. | `4013` |
| `ENCRYPTION_SECRET` | **Yes** | Secret for encrypting stuff. | `8ebe95cg21c1fee355e9fa32c8c33141` | | `ENCRYPTION_SECRET` | **Yes** | Secret for encrypting stuff. | `8ebe95cg21c1fee355e9fa32c8c33141` |
| `WEB_APP_URL` | **Yes** | The url of the web app. | `http://127.0.0.1:3000` | | `WEB_APP_URL` | **Yes** | The url of the web app. | `http://127.0.0.1:3000` |
| `GRAPHQL_PUBLIC_ORIGIN` | **Yes** | The origin of the GraphQL server. | `http://127.0.0.1:4013` | | `GRAPHQL_PUBLIC_ORIGIN` | **Yes** | The origin of the GraphQL server. | `http://127.0.0.1:4013` |
| `TOKENS_ENDPOINT` | **Yes** | The endpoint of the tokens service. | `http://127.0.0.1:6001` | | `TOKENS_ENDPOINT` | **Yes** | The endpoint of the tokens service. | `http://127.0.0.1:6001` |
| `SCHEMA_ENDPOINT` | **Yes** | The endpoint of the schema service. | `http://127.0.0.1:6500` | | `SCHEMA_ENDPOINT` | **Yes** | The endpoint of the schema service. | `http://127.0.0.1:6500` |
| `SCHEMA_POLICY_ENDPOINT` | **No** | The endpoint of the schema policy service. | `http://127.0.0.1:6600` | | `SCHEMA_POLICY_ENDPOINT` | **No** | The endpoint of the schema policy service. | `http://127.0.0.1:6600` |
| `POSTGRES_SSL` | No | Whether the postgres connection should be established via SSL. | `1` (enabled) or `0` (disabled) | | `POSTGRES_SSL` | No | Whether the postgres connection should be established via SSL. | `1` (enabled) or `0` (disabled) |
| `POSTGRES_HOST` | **Yes** | Host of the postgres database | `127.0.0.1` | | `POSTGRES_HOST` | **Yes** | Host of the postgres database | `127.0.0.1` |
| `POSTGRES_PORT` | **Yes** | Port of the postgres database | `5432` | | `POSTGRES_PORT` | **Yes** | Port of the postgres database | `5432` |
| `POSTGRES_DB` | **Yes** | Name of the postgres database. | `registry` | | `POSTGRES_DB` | **Yes** | Name of the postgres database. | `registry` |
| `POSTGRES_USER` | **Yes** | User name for accessing the postgres database. | `postgres` | | `POSTGRES_USER` | **Yes** | User name for accessing the postgres database. | `postgres` |
| `POSTGRES_PASSWORD` | No | Password for accessing the postgres database. | `postgres` | | `POSTGRES_PASSWORD` | No | Password for accessing the postgres database. | `postgres` |
| `CLICKHOUSE_PROTOCOL` | **Yes** | The clickhouse protocol for connecting to the clickhouse instance. | `http` | | `CLICKHOUSE_PROTOCOL` | **Yes** | The clickhouse protocol for connecting to the clickhouse instance. | `http` |
| `CLICKHOUSE_HOST` | **Yes** | The host of the clickhouse instance. | `127.0.0.1` | | `CLICKHOUSE_HOST` | **Yes** | The host of the clickhouse instance. | `127.0.0.1` |
| `CLICKHOUSE_PORT` | **Yes** | The port of the clickhouse instance | `8123` | | `CLICKHOUSE_PORT` | **Yes** | The port of the clickhouse instance | `8123` |
| `CLICKHOUSE_USERNAME` | **Yes** | The username for accessing the clickhouse instance. | `test` | | `CLICKHOUSE_USERNAME` | **Yes** | The username for accessing the clickhouse instance. | `test` |
| `CLICKHOUSE_PASSWORD` | **Yes** | The password for accessing the clickhouse instance. | `test` | | `CLICKHOUSE_PASSWORD` | **Yes** | The password for accessing the clickhouse instance. | `test` |
| `CLICKHOUSE_REQUEST_TIMEOUT` | No | Force a request timeout value for ClickHouse operations (in ms) | `30000` | | `CLICKHOUSE_REQUEST_TIMEOUT` | No | Force a request timeout value for ClickHouse operations (in ms) | `30000` |
| `REDIS_HOST` | **Yes** | The host of your redis instance. | `"127.0.0.1"` | | `REDIS_HOST` | **Yes** | The host of your redis instance. | `"127.0.0.1"` |
| `REDIS_PORT` | **Yes** | The port of your redis instance. | `6379` | | `REDIS_PORT` | **Yes** | The port of your redis instance. | `6379` |
| `REDIS_PASSWORD` | **Yes** | The password of your redis instance. | `"password"` | | `REDIS_PASSWORD` | **Yes** | The password of your redis instance. | `"password"` |
| `REDIS_TLS_ENABLED` | **No** | Enable TLS for redis connection (rediss://). | `"0"` | | `REDIS_TLS_ENABLED` | **No** | Enable TLS for redis connection (rediss://). | `"0"` |
| `S3_ENDPOINT` | **Yes** | The S3 endpoint. | `http://localhost:9000` | | `S3_ENDPOINT` | **Yes** | The S3 endpoint. | `http://localhost:9000` |
| `S3_ACCESS_KEY_ID` | **Yes** | The S3 access key id. | `minioadmin` | | `S3_ACCESS_KEY_ID` | **Yes** | The S3 access key id. | `minioadmin` |
| `S3_SECRET_ACCESS_KEY` | **Yes** | The S3 secret access key. | `minioadmin` | | `S3_SECRET_ACCESS_KEY` | **Yes** | The S3 secret access key. | `minioadmin` |
| `S3_BUCKET_NAME` | **Yes** | The S3 bucket name. | `artifacts` | | `S3_BUCKET_NAME` | **Yes** | The S3 bucket name. | `artifacts` |
| `S3_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` | | `S3_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` |
| `S3_MIRROR` | No | Whether S3 mirror is enabled | `1` (enabled) or `0` (disabled) | | `S3_MIRROR` | No | Whether S3 mirror is enabled | `1` (enabled) or `0` (disabled) |
| `S3_MIRROR_ENDPOINT` | **Yes** | The S3 endpoint. | `http://localhost:9000` | | `S3_MIRROR_ENDPOINT` | **Yes** | The S3 endpoint. | `http://localhost:9000` |
| `S3_MIRROR_ACCESS_KEY_ID` | **Yes** | The S3 access key id. | `minioadmin` | | `S3_MIRROR_ACCESS_KEY_ID` | **Yes** | The S3 access key id. | `minioadmin` |
| `S3_MIRROR_SECRET_ACCESS_KEY` | **Yes** | The S3 secret access key. | `minioadmin` | | `S3_MIRROR_SECRET_ACCESS_KEY` | **Yes** | The S3 secret access key. | `minioadmin` |
| `S3_MIRROR_BUCKET_NAME` | **Yes** | The S3 bucket name. | `artifacts` | | `S3_MIRROR_BUCKET_NAME` | **Yes** | The S3 bucket name. | `artifacts` |
| `S3_MIRROR_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` | | `S3_MIRROR_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` |
| `S3_MIRROR_PUBLIC_URL` | No | The public URL of the S3, in case it differs from the `S3_ENDPOINT`. | `http://localhost:8083` | | `S3_MIRROR_PUBLIC_URL` | No | The public URL of the S3, in case it differs from the `S3_ENDPOINT`. | `http://localhost:8083` |
| `CDN_API` | No | Whether the CDN exposed via API is enabled. | `1` (enabled) or `0` (disabled) | | `CDN_API` | No | Whether the CDN exposed via API is enabled. | `1` (enabled) or `0` (disabled) |
| `CDN_API_BASE_URL` | No (Yes if `CDN_API` is set to `1`) | The public base url of the API service. | `http://localhost:8082` | | `CDN_API_BASE_URL` | No (Yes if `CDN_API` is set to `1`) | The public base url of the API service. | `http://localhost:8082` |
| `CDN_API_KV_BASE_URL` | No (**Optional** if `CDN_API` is set to `1`) | The base URL for the KV for API Provider. Used for scenarios where we cache CDN access. | `https://key-cache.graphql-hive.com` | | `CDN_API_KV_BASE_URL` | No (**Optional** if `CDN_API` is set to `1`) | The base URL for the KV for API Provider. Used for scenarios where we cache CDN access. | `https://key-cache.graphql-hive.com` |
| `SUPERTOKENS_CONNECTION_URI` | **Yes** | The URI of the SuperTokens instance. | `http://127.0.0.1:3567` | | `SUPERTOKENS_API_KEY` | **Yes** [Instructions](https://the-guild.dev/graphql/hive/docs/schema-registry/self-hosting/get-started#running-hive-console) | The key for signing access tokens for user sessions. | string |
| `SUPERTOKENS_API_KEY` | **Yes** | The API KEY of the SuperTokens instance. | `iliketurtlesandicannotlie` | | `SUPERTOKENS_REFRESH_TOKEN_KEY` | **Yes** [Instructions](https://the-guild.dev/graphql/hive/docs/schema-registry/self-hosting/get-started#running-hive-console) | The key for signing refresh tokens for user sessions. | string |
| `SUPERTOKENS_RATE_LIMIT` | No (Default value: `1`) | Whether supertokens requests should be rate limited. | `1` (enabled) or `0` (disabled) | | `SUPERTOKENS_RATE_LIMIT` | No (Default value: `1`) | Whether supertokens requests should be rate limited. | `1` (enabled) or `0` (disabled) |
| `SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME` | No (Default value: `CF-Connecting-IP`) | Name of the header to be used for rate limiting. | `CF-Connecting-IP` | | `SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME` | No (Default value: `CF-Connecting-IP`) | Name of the header to be used for rate limiting. | `CF-Connecting-IP` |
| `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) | | `AUTH_GITHUB` | No | Whether login via GitHub should be allowed | `1` (enabled) or `0` (disabled) |
| `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` | | `AUTH_GITHUB_CLIENT_ID` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client ID. | `g6aff8102efda5e1d12e` |
| `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | | `AUTH_GITHUB_CLIENT_SECRET` | No (**Yes** if `AUTH_GITHUB` is set) | The GitHub client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
| `AUTH_GOOGLE` | No | Whether login via Google should be allowed | `1` (enabled) or `0` (disabled) | | `AUTH_GOOGLE` | No | Whether login via Google should be allowed | `1` (enabled) or `0` (disabled) |
| `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` | | `AUTH_GOOGLE_CLIENT_ID` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client ID. | `g6aff8102efda5e1d12e` |
| `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | | `AUTH_GOOGLE_CLIENT_SECRET` | No (**Yes** if `AUTH_GOOGLE` is set) | The Google client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
| `AUTH_ORGANIZATION_OIDC` | No | Whether linking a Hive organization to an Open ID Connect provider is allowed. (Default: `0`) | `1` (enabled) or `0` (disabled) | | `AUTH_ORGANIZATION_OIDC` | No | Whether linking a Hive organization to an Open ID Connect provider is allowed. (Default: `0`) | `1` (enabled) or `0` (disabled) |
| `AUTH_OKTA` | No | Whether login via Okta should be allowed | `1` (enabled) or `0` (disabled) | | `AUTH_OKTA` | No | Whether login via Okta should be allowed | `1` (enabled) or `0` (disabled) |
| `AUTH_OKTA_CLIENT_ENDPOINT` | No (**Yes** if `AUTH_OKTA` is set) | The Okta endpoint. | `https://dev-1234567.okta.com` | | `AUTH_OKTA_CLIENT_ENDPOINT` | No (**Yes** if `AUTH_OKTA` is set) | The Okta endpoint. | `https://dev-1234567.okta.com` |
| `AUTH_OKTA_HIDDEN` | No | Whether the Okta login button should be hidden. (Default: `0`) | `1` (enabled) or `0` (disabled) | | `AUTH_OKTA_HIDDEN` | No | Whether the Okta login button should be hidden. (Default: `0`) | `1` (enabled) or `0` (disabled) |
| `AUTH_OKTA_CLIENT_ID` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client ID. | `g6aff8102efda5e1d12e` | | `AUTH_OKTA_CLIENT_ID` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client ID. | `g6aff8102efda5e1d12e` |
| `AUTH_OKTA_CLIENT_SECRET` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` | | `AUTH_OKTA_CLIENT_SECRET` | No (**Yes** if `AUTH_OKTA` is set) | The Okta client secret. | `g12e552xx54xx2b127821dc4abc4491dxxxa6b187` |
| `AUTH_REQUIRE_EMAIL_VERIFICATION` | No | Whether verifying the email address is mandatory. | `1` (enabled) or `0` (disabled) | | `AUTH_REQUIRE_EMAIL_VERIFICATION` | No | Whether verifying the email address is mandatory. | `1` (enabled) or `0` (disabled) |
| `INTEGRATION_GITHUB` | No | Whether the GitHub integration is enabled | `1` (enabled) or `0` (disabled) | | `INTEGRATION_GITHUB` | No | Whether the GitHub integration is enabled | `1` (enabled) or `0` (disabled) |
| `INTEGRATION_GITHUB_GITHUB_APP_ID` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app id. | `123` | | `INTEGRATION_GITHUB_GITHUB_APP_ID` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app id. | `123` |
| `INTEGRATION_GITHUB_GITHUB_APP_PRIVATE_KEY` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app private key. | `letmein1` | | `INTEGRATION_GITHUB_GITHUB_APP_PRIVATE_KEY` | No (Yes if `INTEGRATION_GITHUB` is set to `1`) | The GitHub app private key. | `letmein1` |
| `FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED` | No | Whether app deployments should be enabled for every organization. | `1` (enabled **default**) or `0` (disabled) | | `FEATURE_FLAGS_APP_DEPLOYMENTS_ENABLED` | No | Whether app deployments should be enabled for every organization. | `1` (enabled **default**) or `0` (disabled) |
| `FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED` | No | Whether schema proposals should be enabled for every organization. | `1` (enabled) or `0` (disabled) | | `FEATURE_FLAGS_SCHEMA_PROPOSALS_ENABLED` | No | Whether schema proposals should be enabled for every organization. | `1` (enabled) or `0` (disabled) |
| `S3_AUDIT_LOG` | No (audit log uses default S3 if not configured) | Whether audit logs should be stored on another S3 bucket than the artifacts. | `1` (enabled) or `0` (disabled) | | `S3_AUDIT_LOG` | No (audit log uses default S3 if not configured) | Whether audit logs should be stored on another S3 bucket than the artifacts. | `1` (enabled) or `0` (disabled) |
| `S3_AUDIT_LOG_ENDPOINT` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 endpoint. | `http://localhost:9000` | | `S3_AUDIT_LOG_ENDPOINT` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 endpoint. | `http://localhost:9000` |
| `S3_AUDIT_LOG_ACCESS_KEY_ID` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 access key id. | `minioadmin` | | `S3_AUDIT_LOG_ACCESS_KEY_ID` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 access key id. | `minioadmin` |
| `S3_AUDIT_LOG_SECRET_ACCESS_KEY` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 secret access key. | `minioadmin` | | `S3_AUDIT_LOG_SECRET_ACCESS_KEY` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 secret access key. | `minioadmin` |
| `S3_AUDIT_LOG_BUCKET_NAME` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 bucket name. | `artifacts` | | `S3_AUDIT_LOG_BUCKET_NAME` | **Yes** (if `S3_AUDIT_LOG` is `1`) | The S3 bucket name. | `artifacts` |
| `S3_AUDIT_LOG_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` | | `S3_AUDIT_LOG_SESSION_TOKEN` | No | The S3 session token. | `dummytoken` |
| `S3_AUDIT_LOG_PUBLIC_URL` | No | The public URL of the S3, in case it differs from the `S3_ENDPOINT`. | `http://localhost:8083` | | `S3_AUDIT_LOG_PUBLIC_URL` | No | The public URL of the S3, in case it differs from the `S3_ENDPOINT`. | `http://localhost:8083` |
| `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` | | `ENVIRONMENT` | No | The environment of your Hive app. (**Note:** This will be used for Sentry reporting.) | `staging` |
| `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) | | `SENTRY` | No | Whether Sentry error reporting should be enabled. | `1` (enabled) or `0` (disabled) |
| `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` | | `SENTRY_DSN` | No | The DSN for reporting errors to Sentry. | `https://dooobars@o557896.ingest.sentry.io/12121212` |
| `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) | | `PROMETHEUS_METRICS` | No | Whether Prometheus metrics should be enabled | `1` (enabled) or `0` (disabled) |
| `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `server` | | `PROMETHEUS_METRICS_LABEL_INSTANCE` | No | The instance label added for the prometheus metrics. | `server` |
| `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` | | `PROMETHEUS_METRICS_PORT` | No | Port on which prometheus metrics are exposed | Defaults to `10254` |
| `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) | | `REQUEST_LOGGING` | No | Log http requests | `1` (enabled) or `0` (disabled) |
| `HIVE_PERSISTED_DOCUMENTS` | No | Whether persisted documents should be enabled or disabled | `1` (enabled) or `0` (disabled) | | `HIVE_PERSISTED_DOCUMENTS` | No | Whether persisted documents should be enabled or disabled | `1` (enabled) or `0` (disabled) |
| `HIVE_PERSISTED_DOCUMENTS_CDN_ENDPOINT` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The endpoint for the Hive persisted documents CDN. | `https://cdn.graphql-hive.com/artifacts/v1/<id>` | | `HIVE_PERSISTED_DOCUMENTS_CDN_ENDPOINT` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The endpoint for the Hive persisted documents CDN. | `https://cdn.graphql-hive.com/artifacts/v1/<id>` |
| `HIVE_PERSISTED_DOCUMENTS_CDN_ACCESS_KEY` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The access token key for the Hive CDN. | `hv2abcdefg` | | `HIVE_PERSISTED_DOCUMENTS_CDN_ACCESS_KEY` | No (Yes if `HIVE_PERSISTED_DOCUMENTS` is set to `1`) | The access token key for the Hive CDN. | `hv2abcdefg` |
| `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) | | `LOG_LEVEL` | No | The verbosity of the service logs. One of `trace`, `debug`, `info`, `warn` ,`error`, `fatal` or `silent` | `info` (default) |
| `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` | | `OPENTELEMETRY_COLLECTOR_ENDPOINT` | No | OpenTelemetry Collector endpoint. The expected traces transport is HTTP (port `4318`). | `http://localhost:4318/v1/traces` |
## Hive Cloud Configuration ## Hive Cloud Configuration

View file

@ -53,8 +53,6 @@
"pino-pretty": "11.3.0", "pino-pretty": "11.3.0",
"prom-client": "15.1.3", "prom-client": "15.1.3",
"reflect-metadata": "0.2.2", "reflect-metadata": "0.2.2",
"supertokens-js-override": "0.0.4",
"supertokens-node": "16.7.5",
"tslib": "2.8.1", "tslib": "2.8.1",
"zod": "3.25.76" "zod": "3.25.76"
}, },

View file

@ -102,24 +102,13 @@ const RedisModel = zod.object({
REDIS_TLS_ENABLED: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), REDIS_TLS_ENABLED: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
}); });
const SuperTokensModel = zod.union([ const SuperTokensModel = zod.object({
zod.object({ SUPERTOKENS_REFRESH_TOKEN_KEY: zod.string(),
SUPERTOKENS_AT_HOME: emptyString(zod.literal('0').optional()), SUPERTOKENS_ACCESS_TOKEN_KEY: zod.string(),
SUPERTOKENS_CONNECTION_URI: zod.string().url(), SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()),
SUPERTOKENS_API_KEY: zod.string(), SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()),
SUPERTOKENS_RATE_LIMIT: emptyString(zod.union([zod.literal('1'), zod.literal('0')]).optional()), SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()),
SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME: emptyString(zod.string().optional()), });
SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: 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()),
SUPERTOKENS_RATE_LIMIT_BYPASS_KEY: emptyString(zod.string().optional()),
}),
]);
const GitHubModel = zod.union([ const GitHubModel = zod.union([
zod.object({ zod.object({
@ -446,36 +435,19 @@ export const env = {
password: redis.REDIS_PASSWORD ?? '', password: redis.REDIS_PASSWORD ?? '',
tlsEnabled: redis.REDIS_TLS_ENABLED === '1', tlsEnabled: redis.REDIS_TLS_ENABLED === '1',
}, },
supertokens: supertokens: {
supertokens.SUPERTOKENS_AT_HOME === '1' secrets: {
? { refreshTokenKey: supertokens.SUPERTOKENS_REFRESH_TOKEN_KEY,
type: 'atHome' as const, accessTokenKey: supertokens.SUPERTOKENS_ACCESS_TOKEN_KEY,
secrets: { },
refreshTokenKey: supertokens.SUPERTOKENS_REFRESH_TOKEN_KEY, rateLimit:
accessTokenKey: supertokens.SUPERTOKENS_ACCESS_TOKEN_KEY, supertokens.SUPERTOKENS_RATE_LIMIT === '0'
? null
: {
ipHeaderName: supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null,
}, },
rateLimit: },
supertokens.SUPERTOKENS_RATE_LIMIT === '0'
? null
: {
ipHeaderName:
supertokens.SUPERTOKENS_RATE_LIMIT_IP_HEADER_NAME ?? 'CF-Connecting-IP',
bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null,
},
}
: {
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',
bypassKey: supertokens.SUPERTOKENS_RATE_LIMIT_BYPASS_KEY ?? null,
},
},
auth: { auth: {
github: github:
authGithub.AUTH_GITHUB === '1' authGithub.AUTH_GITHUB === '1'

View file

@ -1,15 +1,9 @@
#!/usr/bin/env node #!/usr/bin/env node
import got from 'got'; import got from 'got';
import { GraphQLError, stripIgnoredCharacters } from 'graphql'; import { GraphQLError, stripIgnoredCharacters } from 'graphql';
import supertokens from 'supertokens-node';
import {
errorHandler as supertokensErrorHandler,
plugin as supertokensFastifyPlugin,
} from 'supertokens-node/framework/fastify/index.js';
import cors from '@fastify/cors'; import cors from '@fastify/cors';
import type { FastifyCorsOptionsDelegateCallback } from '@fastify/cors'; import type { FastifyCorsOptionsDelegateCallback } from '@fastify/cors';
import 'reflect-metadata'; import 'reflect-metadata';
import { z } from 'zod';
import formDataPlugin from '@fastify/formbody'; import formDataPlugin from '@fastify/formbody';
import { import {
createRegistry, createRegistry,
@ -60,7 +54,6 @@ import { graphqlHandler } from './graphql-handler';
import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics'; import { clickHouseElapsedDuration, clickHouseReadDuration } from './metrics';
import { createOtelAuthEndpoint } from './otel-auth-endpoint'; import { createOtelAuthEndpoint } from './otel-auth-endpoint';
import { createPublicGraphQLHandler } from './public-graphql-handler'; import { createPublicGraphQLHandler } from './public-graphql-handler';
import { initSupertokens, oidcIdLookup } from './supertokens';
import { registerSupertokensAtHome } from './supertokens-at-home'; import { registerSupertokensAtHome } from './supertokens-at-home';
class CorsError extends Error { class CorsError extends Error {
@ -131,12 +124,6 @@ export async function main() {
return res.status(403).send(err.message); return res.status(403).send(err.message);
} }
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); server.log.error(err);
return res.status(500); return res.status(500);
}); });
@ -166,9 +153,11 @@ export async function main() {
'graphql-client-name', 'graphql-client-name',
'ignore-session', 'ignore-session',
'x-request-id', 'x-request-id',
...(env.supertokens.type === 'atHome' 'rid',
? ['rid', 'fdi-version', 'anti-csrf', 'authorization', 'st-auth-mode'] 'fdi-version',
: supertokens.getAllCORSHeaders()), 'anti-csrf',
'authorization',
'st-auth-mode',
], ],
}); });
}; };
@ -396,10 +385,7 @@ export async function main() {
emailVerification: env.auth.requireEmailVerification emailVerification: env.auth.requireEmailVerification
? registry.injector.get(EmailVerification) ? registry.injector.get(EmailVerification)
: null, : null,
accessTokenKey: accessTokenKey: new AccessTokenKeyContainer(env.supertokens.secrets.accessTokenKey),
env.supertokens.type === 'atHome'
? new AccessTokenKeyContainer(env.supertokens.secrets.accessTokenKey)
: null,
oidcIntegrationStore: new OIDCIntegrationStore(storage.pool, redis, logger), oidcIntegrationStore: new OIDCIntegrationStore(storage.pool, redis, logger),
}), }),
organizationAccessTokenStrategy, organizationAccessTokenStrategy,
@ -458,21 +444,7 @@ export async function main() {
}); });
} }
if (env.supertokens.type == 'core') {
initSupertokens({
storage,
crypto,
logger: server.log,
redis,
taskScheduler,
broadcastLog,
});
}
await server.register(formDataPlugin); await server.register(formDataPlugin);
if (env.supertokens.type == 'core') {
await server.register(supertokensFastifyPlugin);
}
await registerTRPC(server, { await registerTRPC(server, {
router: internalApiRouter, router: internalApiRouter,
@ -529,42 +501,6 @@ export async function main() {
}, },
}); });
const oidcIdLookupSchema = z.object({
slug: z.string({
required_error: 'Slug is required',
}),
});
server.post('/auth-api/oidc-id-lookup', async (req, res) => {
const inputResult = oidcIdLookupSchema.safeParse(req.body);
if (!inputResult.success) {
captureException(inputResult.error, {
extra: {
path: '/auth-api/oidc-id-lookup',
body: req.body,
},
});
void res.status(400).send({
ok: false,
title: 'Invalid input',
description: 'Failed to resolve SSO information due to invalid input.',
status: 400,
} satisfies Awaited<ReturnType<typeof oidcIdLookup>>);
return;
}
const result = await oidcIdLookup(inputResult.data.slug, storage, req.log);
if (result.ok) {
void res.status(200).send(result);
return;
}
void res.status(result.status).send(result);
return;
});
createOtelAuthEndpoint({ createOtelAuthEndpoint({
server, server,
authN, authN,
@ -581,18 +517,16 @@ export async function main() {
return; return;
}); });
if (env.supertokens.type === 'atHome') { await registerSupertokensAtHome(
await registerSupertokensAtHome( server,
server, storage,
storage, registry.injector.get(TaskScheduler),
registry.injector.get(TaskScheduler), registry.injector.get(CryptoProvider),
registry.injector.get(CryptoProvider), registry.injector.get(RedisRateLimiter),
registry.injector.get(RedisRateLimiter), registry.injector.get(OAuthCache),
registry.injector.get(OAuthCache), broadcastLog,
broadcastLog, env.supertokens.secrets,
env.supertokens.secrets, );
);
}
if (env.cdn.providers.api !== null) { if (env.cdn.providers.api !== null) {
const s3 = { const s3 = {

View file

@ -25,7 +25,8 @@ import { TaskScheduler } from '@hive/workflows/kit';
import { PasswordResetTask } from '@hive/workflows/tasks/password-reset'; import { PasswordResetTask } from '@hive/workflows/tasks/password-reset';
import { env } from './environment'; import { env } from './environment';
import { createNewSession, validatePassword } from './supertokens-at-home/shared'; import { createNewSession, validatePassword } from './supertokens-at-home/shared';
import { type BroadcastOIDCIntegrationLog } from './supertokens/oidc-provider';
type BroadcastOIDCIntegrationLog = (oidcOrganizationId: string, message: string) => void;
/** /**
* Registers the routes of the Supertokens at Home implementation to a fastify instance. * Registers the routes of the Supertokens at Home implementation to a fastify instance.
@ -70,6 +71,57 @@ export async function registerSupertokensAtHome(
}); });
} }
const OIDCIdLookupSchema = z.object({
slug: z.string({
required_error: 'Slug is required',
}),
});
server.post('/auth-api/oidc-id-lookup', async (req, res) => {
if (await rateLimiter.isFastifyRouteRateLimited(req)) {
return res.send({
ok: false,
title: 'Rate Limited',
description: 'Please try again later.',
status: 400,
});
}
req.log.debug('Looking up OIDC integration ID');
const inputResult = OIDCIdLookupSchema.safeParse(req.body);
if (!inputResult.success) {
req.log.debug('Invalid body sent. Failed to parse slug.');
return res.status(400).send({
ok: false,
title: 'Invalid input',
description: 'Failed to resolve SSO information due to invalid input.',
status: 400,
});
}
req.log.debug('Parsed slug (slug=%s)', inputResult.data.slug);
const oidcId = await storage.getOIDCIntegrationIdForOrganizationSlug({
slug: inputResult.data.slug,
});
if (!oidcId) {
req.log.debug('No SSO integration found (slug=%s)', inputResult.data.slug);
return res.status(404).send({
ok: false,
title: 'SSO integration not found',
description: 'Your organization lacks an SSO integration or it does not exist.',
status: 404,
});
}
return res.status(200).send({
ok: true,
id: oidcId,
});
});
server.route({ server.route({
url: '/auth-api/signout', url: '/auth-api/signout',
method: 'POST', method: 'POST',
@ -720,6 +772,13 @@ export async function registerSupertokensAtHome(
}); });
} }
if (!z.string().uuid().safeParse(query.data.oidc_id).success) {
return rep.status(200).send({
status: 'GENERAL_ERROR',
message: 'Something went wrong. Please try again',
});
}
const oidcIntegration = await storage.getOIDCIntegrationById({ const oidcIntegration = await storage.getOIDCIntegrationById({
oidcIntegrationId: query.data.oidc_id, oidcIntegrationId: query.data.oidc_id,
}); });
@ -735,7 +794,7 @@ export async function registerSupertokensAtHome(
const oidClientConfig = new oidClient.Configuration( const oidClientConfig = new oidClient.Configuration(
{ {
issuer: oidcIntegration.id, issuer: 'noop',
authorization_endpoint: oidcIntegration.authorizationEndpoint, authorization_endpoint: oidcIntegration.authorizationEndpoint,
userinfo_endpoint: oidcIntegration.userinfoEndpoint, userinfo_endpoint: oidcIntegration.userinfoEndpoint,
token_endpoint: oidcIntegration.tokenEndpoint, token_endpoint: oidcIntegration.tokenEndpoint,

View file

@ -1,542 +0,0 @@
import type { FastifyBaseLogger } from 'fastify';
import type Redis from 'ioredis';
import { CryptoProvider } from 'packages/services/api/src/modules/shared/providers/crypto';
import { OverrideableBuilder } from 'supertokens-js-override/lib/build/index.js';
import supertokens from 'supertokens-node';
import SessionNode from 'supertokens-node/recipe/session/index.js';
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
import ThirdPartyEmailPasswordNode from 'supertokens-node/recipe/thirdpartyemailpassword/index.js';
import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
import type { TypeInput } from 'supertokens-node/types';
import zod from 'zod';
import { type Storage } from '@hive/api';
import { TaskScheduler } from '@hive/workflows/kit';
import { PasswordResetTask } from '@hive/workflows/tasks/password-reset';
import { createInternalApiCaller } from './api';
import { env } from './environment';
import {
createOIDCSuperTokensProvider,
describeOIDCSignInError,
getLoggerFromUserContext,
getOIDCSuperTokensOverrides,
type BroadcastOIDCIntegrationLog,
} from './supertokens/oidc-provider';
import { createThirdPartyEmailPasswordNodeOktaProvider } from './supertokens/okta-provider';
const SuperTokensSessionPayloadV2Model = zod.object({
version: zod.literal('2'),
superTokensUserId: zod.string(),
email: zod.string(),
userId: zod.string(),
oidcIntegrationId: zod.string().nullable(),
});
type SuperTokensSessionPayload = zod.TypeOf<typeof SuperTokensSessionPayloadV2Model>;
export const backendConfig = (requirements: {
storage: Storage;
crypto: CryptoProvider;
logger: FastifyBaseLogger;
broadcastLog: BroadcastOIDCIntegrationLog;
redis: Redis;
taskScheduler: TaskScheduler;
}): TypeInput => {
const { logger } = requirements;
const internalApi = createInternalApiCaller({
storage: requirements.storage,
crypto: requirements.crypto,
});
const providers: ProviderInput[] = [];
if (env.auth.github) {
providers.push({
config: {
thirdPartyId: 'github',
clients: [
{
scope: ['read:user', 'user:email'],
clientId: env.auth.github.clientId,
clientSecret: env.auth.github.clientSecret,
},
],
},
});
}
if (env.auth.google) {
providers.push({
config: {
thirdPartyId: 'google',
clients: [
{
clientId: env.auth.google.clientId,
clientSecret: env.auth.google.clientSecret,
scope: [
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'openid',
],
},
],
},
});
}
if (env.auth.okta) {
providers.push(createThirdPartyEmailPasswordNodeOktaProvider(env.auth.okta));
}
if (env.auth.organizationOIDC) {
providers.push(
createOIDCSuperTokensProvider({
internalApi,
broadcastLog: requirements.broadcastLog,
}),
);
}
logger.info('SuperTokens providers: %s', providers.map(p => p.config.thirdPartyId).join(', '));
logger.info('SuperTokens websiteDomain: %s', env.hiveServices.webApp.url);
logger.info('SuperTokens apiDomain: %s', env.graphql.origin);
return {
framework: 'fastify',
supertokens: {
connectionURI: env.supertokens.connectionURI ?? '',
apiKey: env.supertokens.apiKey,
},
telemetry: false,
appInfo: {
// learn more about this on https://supertokens.com/docs/thirdpartyemailpassword/appinfo
appName: 'GraphQL Hive',
apiDomain: env.graphql.origin,
websiteDomain: env.hiveServices.webApp.url,
apiBasePath: '/auth-api',
websiteBasePath: '/auth',
},
recipeList: [
ThirdPartyEmailPasswordNode.init({
providers,
signUpFeature: {
formFields: [
{
id: 'firstName',
// optional because of OIDC integration
optional: true,
},
{
id: 'lastName',
optional: true,
},
],
},
emailDelivery: {
override: originalImplementation => ({
...originalImplementation,
async sendEmail(input) {
if (input.type === 'PASSWORD_RESET') {
await requirements.taskScheduler.scheduleTask(PasswordResetTask, {
user: {
id: input.user.id,
email: input.user.email,
},
passwordResetLink: input.passwordResetLink,
});
return Promise.resolve();
}
return Promise.reject(new Error('Unsupported email type.'));
},
}),
},
override: composeSuperTokensOverrides([
getEnsureUserOverrides(internalApi, requirements.redis),
env.auth.organizationOIDC ? getOIDCSuperTokensOverrides() : null,
]),
}),
SessionNode.init({
override: {
functions: originalImplementation => ({
...originalImplementation,
async createNewSession(input) {
console.log(`Creating a new session for "${input.userId}"`);
const user = await supertokens.getUser(input.userId, input.userContext);
if (!user) {
console.log(`Failed to find user with id "${input.userId}"`);
throw new Error(
`SuperTokens: Creating a new session failed. Could not find user with id ${input.userId}.`,
);
}
const ensureUserResult = await internalApi.ensureUser({
superTokensUserId: user.id,
email: user.emails[0],
oidcIntegrationId: input.userContext['oidcId'] ?? null,
firstName: null,
lastName: null,
});
if (!ensureUserResult.ok) {
throw new SessionCreationError(ensureUserResult.reason);
}
const payload: SuperTokensSessionPayload = {
version: '2',
superTokensUserId: input.userId,
userId: ensureUserResult.user.id,
oidcIntegrationId: input.userContext['oidcId'] ?? null,
email: user.emails[0],
};
input.accessTokenPayload = structuredClone(payload);
input.sessionDataInDatabase = structuredClone(payload);
return originalImplementation.createNewSession(input);
},
}),
},
}),
],
isInServerlessEnv: true,
};
};
function extractIPFromUserContext(userContext: unknown): string {
const defaultIp = (userContext as any)._default.request.original.ip;
if (!env.supertokens?.rateLimit) {
return defaultIp;
}
return (
(userContext as any)._default.request.getHeaderValue(env.supertokens.rateLimit.ipHeaderName) ??
defaultIp
);
}
function createRedisRateLimiter(redis: Redis, windowSeconds = 5 * 60, maxRequests = 10) {
async function isRateLimited(action: string, ip: string): Promise<boolean> {
if (env.supertokens.rateLimit === null) {
return false;
}
const key = `supertokens-rate-limit:${action}:${ip}`;
const current = await redis.incr(key);
if (current === 1) {
await redis.expire(key, windowSeconds);
}
if (current > maxRequests) {
return true;
}
return false;
}
return { isRateLimited };
}
const getEnsureUserOverrides = (
internalApi: ReturnType<typeof createInternalApiCaller>,
redis: Redis,
): ThirdPartEmailPasswordTypeInput['override'] => ({
apis: originalImplementation => ({
...originalImplementation,
emailPasswordSignUpPOST: async input => {
if (!originalImplementation.emailPasswordSignUpPOST) {
throw Error('emailPasswordSignUpPOST is not available');
}
const logger = getLoggerFromUserContext(input.userContext);
const ip = extractIPFromUserContext(input.userContext);
const rateLimiter = createRedisRateLimiter(redis);
if (await rateLimiter.isRateLimited('sign-up', ip)) {
logger.debug('email password sign up rate limited (ip=%s)', ip);
return {
status: 'GENERAL_ERROR',
message: 'Please try again in a few minutes.',
};
}
try {
const response = await originalImplementation.emailPasswordSignUpPOST(input);
const firstName = input.formFields.find(field => field.id === 'firstName')?.value ?? null;
const lastName = input.formFields.find(field => field.id === 'lastName')?.value ?? null;
if (response.status === 'SIGN_UP_NOT_ALLOWED') {
return {
status: 'SIGN_UP_NOT_ALLOWED',
reason: 'Sign up not allowed.',
};
}
if (response.status === 'OK') {
const result = await internalApi.ensureUser({
superTokensUserId: response.user.id,
email: response.user.emails[0],
oidcIntegrationId: null,
firstName,
lastName,
});
if (!result.ok) {
return {
status: 'SIGN_UP_NOT_ALLOWED',
reason: result.reason,
};
}
}
return response;
} catch (e) {
if (e instanceof SessionCreationError) {
return {
status: 'SIGN_UP_NOT_ALLOWED',
reason: e.reason,
};
}
throw e;
}
},
async emailPasswordSignInPOST(input) {
if (originalImplementation.emailPasswordSignInPOST === undefined) {
throw Error('Should never come here');
}
const logger = getLoggerFromUserContext(input.userContext);
const ip = extractIPFromUserContext(input.userContext);
const rateLimiter = createRedisRateLimiter(redis);
if (await rateLimiter.isRateLimited('sign-in', ip)) {
logger.debug('email sign in rate limited (ip=%s)', ip);
return {
status: 'GENERAL_ERROR',
message: 'Please try again in a few minutes.',
};
}
try {
const response = await originalImplementation.emailPasswordSignInPOST(input);
if (response.status === 'SIGN_IN_NOT_ALLOWED') {
return {
status: 'SIGN_IN_NOT_ALLOWED',
reason: 'Sign in not allowed.',
};
}
if (response.status === 'OK') {
const result = await internalApi.ensureUser({
superTokensUserId: response.user.id,
email: response.user.emails[0],
oidcIntegrationId: null,
// They are not available during sign in.
firstName: null,
lastName: null,
});
if (!result.ok) {
return {
status: 'SIGN_IN_NOT_ALLOWED',
reason: result.reason,
};
}
}
return response;
} catch (e) {
if (e instanceof SessionCreationError) {
return {
status: 'SIGN_IN_NOT_ALLOWED',
reason: e.reason,
};
}
throw e;
}
},
async thirdPartySignInUpPOST(input) {
if (originalImplementation.thirdPartySignInUpPOST === undefined) {
throw Error('Should never come here');
}
function extractOidcId(args: typeof input) {
if (input.provider.id === 'oidc') {
const oidcId: unknown = args.userContext['oidcId'];
if (typeof oidcId === 'string') {
return oidcId;
}
}
return null;
}
try {
const response = await originalImplementation.thirdPartySignInUpPOST(input);
if (response.status === 'SIGN_IN_UP_NOT_ALLOWED') {
return {
status: 'SIGN_IN_UP_NOT_ALLOWED',
reason: 'Sign in not allowed.',
};
}
if (response.status === 'OK') {
const result = await internalApi.ensureUser({
superTokensUserId: response.user.id,
email: response.user.emails[0],
oidcIntegrationId: extractOidcId(input),
// TODO: should we somehow extract the first and last name from the third party provider?
firstName: null,
lastName: null,
});
if (!result.ok) {
return {
status: 'SIGN_IN_UP_NOT_ALLOWED',
reason: result.reason,
};
}
}
return response;
} catch (e) {
if (e instanceof SessionCreationError) {
return {
status: 'SIGN_IN_UP_NOT_ALLOWED',
reason: e.reason,
};
}
if (input.provider.id === 'oidc') {
const logger = getLoggerFromUserContext(input.userContext);
logger.error(e, 'OIDC sign-in/sign-up failed');
return {
status: 'GENERAL_ERROR' as const,
message: describeOIDCSignInError(e),
};
}
throw e;
}
},
async passwordResetPOST(input) {
const logger = getLoggerFromUserContext(input.userContext);
const ip = extractIPFromUserContext(input.userContext);
const rateLimiter = createRedisRateLimiter(redis);
if (await rateLimiter.isRateLimited('password-reset', ip)) {
logger.debug('password reset rate limited (ip=%s)', ip);
return {
status: 'GENERAL_ERROR',
message: 'Please try again in a few minutes.',
};
}
const result = await originalImplementation.passwordResetPOST!(input);
// For security reasons we revoke all sessions when a password reset is performed.
if (result.status === 'OK' && result.user) {
await SessionNode.revokeAllSessionsForUser(result.user.id);
}
return result;
},
}),
});
const bindObjectFunctions = <T extends { [key: string]: CallableFunction | undefined }>(
obj: T,
bindTo: any,
) => {
return Object.fromEntries(
Object.entries(obj).map(([key, value]) => [key, value?.bind(bindTo)]),
) as T;
};
/**
* Utility function for composing multiple (dynamic SuperTokens overrides).
*/
const composeSuperTokensOverrides = (
overrides: Array<ThirdPartEmailPasswordTypeInput['override'] | null>,
) => ({
apis(
originalImplementation: ReturnType<
Exclude<Exclude<ThirdPartEmailPasswordTypeInput['override'], undefined>['apis'], undefined>
>,
builder: OverrideableBuilder<ThirdPartyEmailPasswordNode.APIInterface> | undefined,
) {
let impl = originalImplementation;
for (const override of overrides) {
if (typeof override?.apis === 'function') {
impl = bindObjectFunctions(override.apis(impl, builder), originalImplementation);
}
}
return impl;
},
functions(
originalImplementation: ReturnType<
Exclude<
Exclude<ThirdPartEmailPasswordTypeInput['override'], undefined>['functions'],
undefined
>
>,
) {
let impl = originalImplementation;
for (const override of overrides) {
if (typeof override?.functions === 'function') {
impl = bindObjectFunctions(override.functions(impl), originalImplementation);
}
}
return impl;
},
});
export function initSupertokens(requirements: {
storage: Storage;
crypto: CryptoProvider;
logger: FastifyBaseLogger;
broadcastLog: BroadcastOIDCIntegrationLog;
redis: Redis;
taskScheduler: TaskScheduler;
}) {
supertokens.init(backendConfig(requirements));
}
type OidcIdLookupResponse =
| {
ok: true;
id: string;
}
| {
ok: false;
title: string;
description: string;
status: number;
};
export async function oidcIdLookup(
slug: string,
storage: Storage,
logger: FastifyBaseLogger,
): Promise<OidcIdLookupResponse> {
logger.debug('Looking up OIDC integration ID for organization %s', slug);
const oidcId = await storage.getOIDCIntegrationIdForOrganizationSlug({ slug });
if (!oidcId) {
return {
ok: false,
title: 'SSO integration not found',
description: 'Your organization lacks an SSO integration or it does not exist.',
status: 404,
};
}
return {
ok: true,
id: oidcId,
};
}
class SessionCreationError extends Error {
constructor(public reason: string) {
super(reason);
}
}

View file

@ -1,144 +0,0 @@
import { describeOIDCSignInError } from './oidc-provider';
describe('describeOIDCSignInError', () => {
test('invalid_client error (e.g. expired client secret)', () => {
const error = new Error(
'Received response with status 401 and body {"error":"invalid_client","error_description":"AAD2SKSASFSLKAF: The provided client secret keys for app \'369sdsds1-8513-4ssa-ae64-292942jsjs\' are expired."}',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('invalid client credentials');
expect(message).toContain('client secret has expired');
expect(message).not.toContain('AAD2SKSASFSLKAF');
expect(message).not.toContain('369sdsds1-8513-4ssa-ae64-292942jsjs');
});
test('invalid_grant error (e.g. expired authorization code)', () => {
const error = new Error(
'Received response with status 400 and body {"error":"invalid_grant","error_description":"The authorization code has expired."}',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('authorization code has expired');
expect(message).toContain('try signing in again');
});
test('unauthorized_client error', () => {
const error = new Error(
'Received response with status 403 and body {"error":"unauthorized_client","error_description":"The client is not authorized."}',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('rejected the client authorization');
expect(message).toContain('OIDC integration configuration');
});
test('invalid_request error', () => {
const error = new Error(
'Received response with status 400 and body {"error":"invalid_request","error_description":"The request is missing a required parameter."}',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('rejected the token request as malformed');
expect(message).toContain('token endpoint URL');
});
test('unsupported_grant_type error', () => {
const error = new Error(
'Received response with status 400 and body {"error":"unsupported_grant_type","error_description":"The authorization grant type is not supported."}',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('does not support the authorization code grant type');
});
test('invalid_scope error', () => {
const error = new Error(
'Received response with status 400 and body {"error":"invalid_scope","error_description":"The requested scope is invalid."}',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('rejected the requested scopes');
expect(message).toContain('additional scopes');
});
test('network error: ECONNREFUSED', () => {
const error = new Error(
'request to https://login.example.com/token failed, reason: connect ECONNREFUSED 127.0.0.1:443',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('Could not connect');
expect(message).toContain('endpoint URLs');
});
test('network error: ENOTFOUND', () => {
const error = new Error(
'request to https://nonexistent.example.com/token failed, reason: getaddrinfo ENOTFOUND nonexistent.example.com',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('Could not connect');
});
test('network error: ETIMEDOUT', () => {
const error = new Error(
'request to https://slow.example.com/token failed, reason: connect ETIMEDOUT',
);
const message = describeOIDCSignInError(error);
expect(message).toContain('Could not connect');
});
test('network error: fetch failed', () => {
const error = new TypeError('fetch failed');
const message = describeOIDCSignInError(error);
expect(message).toContain('Could not connect');
});
test('OIDC integration not found', () => {
const error = new Error('Could not find OIDC integration.');
const message = describeOIDCSignInError(error);
expect(message).toContain('could not be found');
expect(message).toContain('contact your organization administrator');
});
test('userinfo endpoint returned non-200 status', () => {
const error = new Error(
"Received invalid status code. Could not retrieve user's profile info.",
);
const message = describeOIDCSignInError(error);
expect(message).toContain('user info endpoint returned an error');
expect(message).toContain('verify the user info endpoint URL');
});
test('userinfo endpoint returned non-JSON response', () => {
const error = new Error('Could not parse JSON response.');
const message = describeOIDCSignInError(error);
expect(message).toContain('returned an invalid response');
expect(message).toContain('verify the user info endpoint URL');
});
test('userinfo endpoint missing required fields (sub, email)', () => {
const error = new Error('Could not parse profile info.');
const message = describeOIDCSignInError(error);
expect(message).toContain('did not return the required fields');
expect(message).toContain('sub, email');
});
test('unknown error returns generic message', () => {
const error = new Error('Something completely unexpected happened');
const message = describeOIDCSignInError(error);
expect(message).toContain('unexpected error');
expect(message).toContain('verify your OIDC integration configuration');
});
test('non-Error value is handled', () => {
const message = describeOIDCSignInError('string error with invalid_client');
expect(message).toContain('invalid client credentials');
});
test('no sensitive information is leaked in any branch', () => {
const sensitiveError = new Error(
'Received response with status 401 and body {"error":"invalid_client","error_description":"AADAASJAD213122: The provided client secret keys for app \'3693bbf1-8513-4cda-ae64-77e3ca237f17\' are expired. Visit the Azure portal to create new keys for your app: https://aka.ms/NewClientSecret Trace ID: b8b7152f-4489-46ed-8b78-11ad45520300 Correlation ID: 45c48f07-0191-431d-8a19-ba8319a7cd18"}',
);
const message = describeOIDCSignInError(sensitiveError);
expect(message).not.toContain('3693bbf1');
expect(message).not.toContain('b8b7152f');
expect(message).not.toContain('45c48f07');
expect(message).not.toContain('aka.ms');
expect(message).not.toContain('Trace ID');
expect(message).not.toContain('Correlation ID');
});
});

View file

@ -1,377 +0,0 @@
import type { FastifyBaseLogger } from 'fastify';
import type { FastifyRequest } from 'supertokens-node/lib/build/framework/fastify/framework';
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
import type { TypeInput as ThirdPartEmailPasswordTypeInput } from 'supertokens-node/recipe/thirdpartyemailpassword/types';
import zod from 'zod';
import { createInternalApiCaller } from '../api';
const couldNotResolveOidcIntegrationSymbol = Symbol('could_not_resolve_oidc_integration');
type InternalApiCaller = ReturnType<typeof createInternalApiCaller>;
export const getOIDCSuperTokensOverrides = (): ThirdPartEmailPasswordTypeInput['override'] => ({
apis(originalImplementation) {
return {
...originalImplementation,
async authorisationUrlGET(input) {
if (input.userContext?.[couldNotResolveOidcIntegrationSymbol] === true) {
return {
status: 'GENERAL_ERROR',
message: 'Could not find OIDC integration.',
};
}
return originalImplementation.authorisationUrlGET!(input);
},
};
},
});
export type BroadcastOIDCIntegrationLog = (oidcId: string, message: string) => void;
export function getLoggerFromUserContext(userContext: unknown): FastifyBaseLogger {
return (userContext as any)._default.request.request.log;
}
export const createOIDCSuperTokensProvider = (args: {
internalApi: InternalApiCaller;
broadcastLog: BroadcastOIDCIntegrationLog;
}): ProviderInput => ({
config: {
thirdPartyId: 'oidc',
},
override(originalImplementation) {
return {
...originalImplementation,
async getConfigForClientType(input) {
const logger = getLoggerFromUserContext(input.userContext);
logger.info('resolve config for OIDC provider.');
const config = await getOIDCConfigFromInput(args.internalApi, logger, input);
if (!config) {
// In the next step the override `authorisationUrlGET` from `getOIDCSuperTokensOverrides` is called.
// We use the user context to return a `GENERAL_ERROR` with a human readable message.
// We cannot return an error here (except an "Unexpected error"), so we also need to return fake dat
input.userContext[couldNotResolveOidcIntegrationSymbol] = true;
return {
thirdPartyId: 'oidc',
get clientId(): string {
throw new Error('Noop value accessed.');
},
};
}
return {
thirdPartyId: 'oidc',
clientId: config.clientId,
clientSecret: config.clientSecret,
authorizationEndpoint: config.authorizationEndpoint,
userInfoEndpoint: config.userinfoEndpoint,
tokenEndpoint: config.tokenEndpoint,
scope: ['openid', 'email', ...config.additionalScopes],
};
},
async getAuthorisationRedirectURL(input) {
const logger = getLoggerFromUserContext(input.userContext);
logger.info('resolve authorization redirect url of OIDC provider.');
const oidcConfig = await getOIDCConfigFromInput(args.internalApi, logger, input);
if (!oidcConfig) {
// This case should never be reached (guarded by getConfigForClientType).
// We still have it for security reasons.
throw new Error('Could not find OIDC integration.');
}
const authorizationRedirectUrl =
await originalImplementation.getAuthorisationRedirectURL(input);
const url = new URL(authorizationRedirectUrl.urlWithQueryParams);
url.searchParams.set('state', oidcConfig.id);
const urlWithQueryParams = url.toString();
args.broadcastLog(oidcConfig.id, `redirect client to oauth provider ${urlWithQueryParams}`);
return {
...authorizationRedirectUrl,
urlWithQueryParams,
};
},
async exchangeAuthCodeForOAuthTokens(input) {
const logger = getLoggerFromUserContext(input.userContext);
const config = await getOIDCConfigFromInput(args.internalApi, logger, input);
if (!config) {
// This case should never be reached (guarded by getConfigForClientType).
// We still have it for security reasons.
throw new Error('Could not find OIDC integration.');
}
logger.info('exchange auth code for oauth token (oidcId=%s)', config.id);
args.broadcastLog(
config.id,
`attempt exchanging auth code for auth tokens on endpoint ${config.tokenEndpoint}`,
);
try {
// TODO: we should probably have our own custom implementation of this that uses fetch API.
// that way we can also do timeouts, retries and more detailed logging.
const result = await originalImplementation.exchangeAuthCodeForOAuthTokens(input);
args.broadcastLog(
config.id,
`successfully exchanged auth code for tokens on endpoint ${config.tokenEndpoint}`,
);
return result;
} catch (error) {
if (error instanceof Error) {
args.broadcastLog(
config.id,
`error while exchanging auth code for tokens on endpoint ${config.tokenEndpoint}: ${error.message}`,
);
}
throw error;
}
},
async getUserInfo(input) {
const logger = getLoggerFromUserContext(input.userContext);
logger.info('retrieve profile info from OIDC provider');
const config = await getOIDCConfigFromInput(args.internalApi, logger, input);
if (!config) {
// This case should never be reached (guarded by getConfigForClientType).
// We still have it for security reasons.
throw new Error('Could not find OIDC integration.');
}
logger.info('fetch info for OIDC provider (oidcId=%s)', config.id);
args.broadcastLog(
config.id,
`attempt fetching user info from endpoint with timeout 10 seconds ${config.userinfoEndpoint}`,
);
const abortController = new AbortController();
const timeout = setTimeout(() => {
abortController.abort();
args.broadcastLog(
config.id,
`failed fetching user info from endpoint ${config.userinfoEndpoint}. Request timed out.`,
);
}, 10_000);
const tokenResponse = OIDCTokenSchema.parse(input.oAuthTokens);
const response = await fetch(config.userinfoEndpoint, {
headers: {
authorization: `Bearer ${tokenResponse.access_token}`,
accept: 'application/json',
'content-type': 'application/json',
},
signal: abortController.signal,
});
if (response.status !== 200) {
clearTimeout(timeout);
logger.info('received invalid status code (oidcId=%s)', config.id);
args.broadcastLog(
config.id,
`failed fetching user info from endpoint "${config.userinfoEndpoint}". Received status code ${response.status}. Expected 200.`,
);
throw new Error("Received invalid status code. Could not retrieve user's profile info.");
}
const body = await response.text();
clearTimeout(timeout);
let rawData: unknown;
try {
rawData = JSON.parse(body);
} catch (err) {
logger.error('Could not parse JSON response from OIDC provider (oidcId=%s)', config.id);
if (err instanceof Error) {
args.broadcastLog(
config.id,
`failed parsing user info request body for response from user info endpoint "${config.userinfoEndpoint}". Error: ${err.message}.`,
);
}
throw new Error('Could not parse JSON response.');
}
logger.info('retrieved profile info for provider (oidcId=%s)', config.id);
const dataParseResult = OIDCProfileInfoSchema.safeParse(rawData);
if (!dataParseResult.success) {
logger.error('Could not parse profile info for OIDC provider (oidcId=%s)', config.id);
logger.error('Raw data: %s', JSON.stringify(rawData));
logger.error('Error: %s', JSON.stringify(dataParseResult.error));
for (const issue of dataParseResult.error.issues) {
logger.debug('Issue: %s', JSON.stringify(issue));
}
args.broadcastLog(
config.id,
`failed validating user info request body for response from user info endpoint "${config.userinfoEndpoint}". Issues: ${JSON.stringify(dataParseResult.error.issues)}`,
);
throw new Error('Could not parse profile info.');
}
args.broadcastLog(
config.id,
`successfully parsed user info request body for response from user info endpoint "${config.userinfoEndpoint}".`,
);
const profile = dataParseResult.data;
// Set the oidcId to the user context so it can be used in `thirdPartySignInUpPOST` for linking the user account to the OIDC integration.
input.userContext.oidcId = config.id;
return {
thirdPartyUserId: `${config.id}-${profile.sub}`,
email: {
id: profile.email,
isVerified: true,
},
rawUserInfoFromProvider: {
fromIdTokenPayload: undefined,
fromUserInfoAPI: undefined,
},
};
},
};
},
});
type OIDCConfig = {
id: string;
clientId: string;
clientSecret: string;
tokenEndpoint: string;
userinfoEndpoint: string;
authorizationEndpoint: string;
additionalScopes: string[];
};
const OIDCProfileInfoSchema = zod.object({
sub: zod.string(),
email: zod.string().email(),
});
const OIDCTokenSchema = zod.object({ access_token: zod.string() });
const getOIDCIdFromInput = (input: { userContext: any }, logger: FastifyBaseLogger): string => {
const fastifyRequest = input.userContext._default.request as FastifyRequest;
const originalUrl = 'http://localhost' + fastifyRequest.getOriginalURL();
const oidcId = new URL(originalUrl).searchParams.get('oidc_id');
if (typeof oidcId !== 'string') {
logger.error('Invalid OIDC ID sent from client: %s', oidcId);
throw new Error('Invalid OIDC ID sent from client.');
}
return oidcId;
};
const configCache = new WeakMap<FastifyRequest, OIDCConfig | null>();
/**
* Get cached OIDC config from the supertokens input.
*/
async function getOIDCConfigFromInput(
internalApi: InternalApiCaller,
logger: FastifyBaseLogger,
input: { userContext: any },
) {
const fastifyRequest = input.userContext._default.request as FastifyRequest;
if (configCache.has(fastifyRequest)) {
return configCache.get(fastifyRequest) ?? null;
}
const oidcIntegrationId = getOIDCIdFromInput(input, logger);
const config = await fetchOIDCConfig(internalApi, logger, oidcIntegrationId);
if (!config) {
configCache.set(fastifyRequest, null);
logger.error('Could not find OIDC integration (oidcId: %s)', oidcIntegrationId);
return null;
}
const resolvedConfig = { oidcIntegrationId, ...config };
configCache.set(fastifyRequest, resolvedConfig);
return resolvedConfig;
}
/**
* Classify an OIDC sign-in error into a user-safe description.
* Avoids leaking sensitive details (app IDs, trace IDs, internal URLs)
* while still pointing administrators toward the likely cause.
*/
export function describeOIDCSignInError(error: unknown): string {
const message = error instanceof Error ? error.message : String(error);
if (message.includes('invalid_client')) {
return 'Authentication with your OIDC provider failed due to invalid client credentials. This commonly happens when the client secret has expired or the client ID is incorrect. Please review your OIDC integration settings.';
}
if (message.includes('invalid_grant')) {
return 'The authorization could not be completed. This can happen if the authorization code has expired. Please try signing in again.';
}
if (message.includes('unauthorized_client')) {
return 'Your OIDC provider rejected the client authorization. Please verify your OIDC integration configuration.';
}
if (message.includes('invalid_request')) {
return 'Your OIDC provider rejected the token request as malformed. This may indicate a misconfigured token endpoint URL. Please review your OIDC integration settings.';
}
if (message.includes('unsupported_grant_type')) {
return 'Your OIDC provider does not support the authorization code grant type. Please verify the provider supports the OAuth 2.0 authorization code flow.';
}
if (message.includes('invalid_scope')) {
return 'Your OIDC provider rejected the requested scopes. Please review the additional scopes configured in your OIDC integration settings.';
}
if (message.includes('Could not find OIDC integration')) {
return 'The OIDC integration could not be found. It may have been removed or misconfigured. Please contact your organization administrator.';
}
if (message.includes("Could not retrieve user's profile info")) {
return "Your OIDC provider's user info endpoint returned an error. Please verify the user info endpoint URL in your OIDC integration settings is correct.";
}
if (message.includes('Could not parse JSON response')) {
return "Your OIDC provider's user info endpoint returned an invalid response. Please verify the user info endpoint URL in your OIDC integration settings is correct.";
}
if (message.includes('Could not parse profile info')) {
return "Your OIDC provider's user info endpoint did not return the required fields (sub, email). Please verify your OIDC provider is configured to include these claims.";
}
if (
message.includes('ECONNREFUSED') ||
message.includes('ENOTFOUND') ||
message.includes('ETIMEDOUT') ||
message.includes('fetch failed')
) {
return 'Could not connect to your OIDC provider. Please verify the endpoint URLs in your OIDC integration settings are correct and the server is accessible.';
}
return 'An unexpected error occurred while authenticating with your OIDC provider. Please verify your OIDC integration configuration or contact your administrator.';
}
const fetchOIDCConfig = async (
internalApi: InternalApiCaller,
logger: FastifyBaseLogger,
oidcIntegrationId: string,
): Promise<OIDCConfig | null> => {
const result = await internalApi.getOIDCIntegrationById({ oidcIntegrationId });
if (result === null) {
logger.error('OIDC integration not found. (oidcId=%s)', oidcIntegrationId);
return null;
}
return result;
};

View file

@ -1,77 +0,0 @@
import type { ProviderInput } from 'supertokens-node/recipe/thirdparty/types';
import zod from 'zod';
import { env } from '../environment';
type OktaConfig = Exclude<(typeof env)['auth']['okta'], null>;
/**
* Custom (server) provider for SuperTokens in order to allow Okta users to sign in.
*/
export const createThirdPartyEmailPasswordNodeOktaProvider = (
config: OktaConfig,
): ProviderInput => {
return {
config: {
thirdPartyId: 'okta',
clients: [
{
clientId: config.clientId,
clientSecret: config.clientSecret,
scope: ['openid', 'email', 'profile', 'okta.users.read.self'],
},
],
authorizationEndpoint: `${config.endpoint}/oauth2/v1/authorize`,
tokenEndpoint: `${config.endpoint}/oauth2/v1/token`,
},
override(originalImplementation) {
return {
...originalImplementation,
async getUserInfo(input) {
const data = OktaAccessTokenResponseModel.parse(input.oAuthTokens);
const userData = await fetchOktaProfile(config, data.access_token);
return {
thirdPartyUserId: userData.id,
email: {
id: userData.profile.email,
isVerified: true,
},
rawUserInfoFromProvider: {
fromIdTokenPayload: undefined,
fromUserInfoAPI: undefined,
},
};
},
};
},
};
};
const OktaAccessTokenResponseModel = zod.object({
access_token: zod.string(),
});
const OktaProfileModel = zod.object({
id: zod.string(),
profile: zod.object({
email: zod.string(),
}),
});
async function fetchOktaProfile(config: OktaConfig, accessToken: string) {
const response = await fetch(`${config.endpoint}/api/v1/users/me`, {
method: 'GET',
headers: {
'content-type': 'application/json',
accept: 'application/json',
authorization: `Bearer ${accessToken}`,
},
});
if (response.status !== 200) {
throw new Error(`Unexpected status code from Okta API: ${response.status}`);
}
const json = await response.json();
return OktaProfileModel.parse(json);
}

View file

@ -43,7 +43,6 @@ components:
[Azure Event Hubs](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about)) [Azure Event Hubs](https://learn.microsoft.com/en-us/azure/event-hubs/event-hubs-about))
- [ClickHouse database](https://clickhouse.com/) - [ClickHouse database](https://clickhouse.com/)
- [Redis](https://redis.io/) - [Redis](https://redis.io/)
- [SuperTokens instance](https://supertokens.com/)
- [S3 storage](https://aws.amazon.com/s3/) (or any other S3-compatible solution, like - [S3 storage](https://aws.amazon.com/s3/) (or any other S3-compatible solution, like
[minio](https://min.io/) or [CloudFlare R2](https://www.cloudflare.com/developer-platform/r2/)) [minio](https://min.io/) or [CloudFlare R2](https://www.cloudflare.com/developer-platform/r2/))
- Hive microservices - Hive microservices
@ -93,7 +92,6 @@ The following diagram shows the architecture of a self-hosted Hive Console insta
#### Microservices #### Microservices
- **SuperTokens**: an open-source project used for authentication and authorization.
- `webapp`: the main Hive Console web application. - `webapp`: the main Hive Console web application.
- `server`: the main Hive Console server, responsible for serving the GraphQL API and orchestrating - `server`: the main Hive Console server, responsible for serving the GraphQL API and orchestrating
calls to other services. calls to other services.
@ -192,7 +190,6 @@ export DOCKER_TAG=":9.7.1" # Pin this to an exact version
export HIVE_ENCRYPTION_SECRET=$(openssl rand -hex 16) export HIVE_ENCRYPTION_SECRET=$(openssl rand -hex 16)
export HIVE_APP_BASE_URL="http://localhost:8080" export HIVE_APP_BASE_URL="http://localhost:8080"
export HIVE_EMAIL_FROM="no-reply@graphql-hive.com" export HIVE_EMAIL_FROM="no-reply@graphql-hive.com"
export SUPERTOKENS_API_KEY=$(openssl rand -hex 16)
export CLICKHOUSE_USER="clickhouse" export CLICKHOUSE_USER="clickhouse"
export CLICKHOUSE_PASSWORD=$(openssl rand -hex 16) export CLICKHOUSE_PASSWORD=$(openssl rand -hex 16)
export REDIS_PASSWORD=$(openssl rand -hex 16) export REDIS_PASSWORD=$(openssl rand -hex 16)
@ -202,6 +199,13 @@ export POSTGRES_DB="registry"
export MINIO_ROOT_USER="minioadmin" export MINIO_ROOT_USER="minioadmin"
export MINIO_ROOT_PASSWORD="minioadmin" export MINIO_ROOT_PASSWORD="minioadmin"
export CDN_AUTH_PRIVATE_KEY=$(openssl rand -hex 16) export CDN_AUTH_PRIVATE_KEY=$(openssl rand -hex 16)
export SUPERTOKENS_REFRESH_TOKEN_KEY=$(echo "1000:$(openssl rand -hex 64):$(openssl rand -hex 64)")
KEY_NAME=$(uuidgen)
PRIVATE_KEY_PEM=$(openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048)
PUBLIC_KEY_PEM=$(echo "$PRIVATE_KEY_PEM" | openssl rsa -pubout)
PRIVATE_KEY_DATA=$(echo "$PRIVATE_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n')
PUBLIC_KEY_DATA=$(echo "$PUBLIC_KEY_PEM" | awk 'NF {if (NR!=1 && $0!~/-----END/) print}' | tr -d '\n')
export SUPERTOKENS_ACCESS_TOKEN_KEY=$(echo "${KEY_NAME}|${PUBLIC_KEY_DATA}|${PRIVATE_KEY_DATA}")
``` ```
<Callout type="info"> <Callout type="info">
@ -390,8 +394,6 @@ After doing your first testing with Hive Console you should consider the followi
new releases and updates. new releases and updates.
- Set up a weekly reminder for updating your Hive Console instance to the latest version and - Set up a weekly reminder for updating your Hive Console instance to the latest version and
applying maintenance. applying maintenance.
- Get yourself familiar with SuperTokens and follow their changelogs in order to keep your
SuperTokens instance up-to-date.
- In order to use longer retention periods e.g. for conditional breaking changes or schema explorer - In order to use longer retention periods e.g. for conditional breaking changes or schema explorer
overviews do the following: Open the postgres database, go to the table `organization`, find your overviews do the following: Open the postgres database, go to the table `organization`, find your
organization and change `plan_name` to ENTERPRISE, `limit_operations_monthly` to 0 and organization and change `plan_name` to ENTERPRISE, `limit_operations_monthly` to 0 and

View file

@ -22,7 +22,6 @@ overrides:
'@tailwindcss/node>tailwindcss': 4.1.18 '@tailwindcss/node>tailwindcss': 4.1.18
'@tailwindcss/vite>tailwindcss': 4.1.18 '@tailwindcss/vite>tailwindcss': 4.1.18
estree-util-value-to-estree: ^3.3.3 estree-util-value-to-estree: ^3.3.3
nodemailer@^6.0.0: ^7.0.11
'@types/nodemailer>@aws-sdk/client-sesv2': '-' '@types/nodemailer>@aws-sdk/client-sesv2': '-'
tar@6.x.x: ^7.5.11 tar@6.x.x: ^7.5.11
diff@<8.0.3: ^8.0.3 diff@<8.0.3: ^8.0.3
@ -1225,9 +1224,6 @@ importers:
stripe: stripe:
specifier: 17.5.0 specifier: 17.5.0
version: 17.5.0 version: 17.5.0
supertokens-node:
specifier: 16.7.5
version: 16.7.5(encoding@0.1.13)
tslib: tslib:
specifier: 2.8.1 specifier: 2.8.1
version: 2.8.1 version: 2.8.1
@ -1649,12 +1645,6 @@ importers:
reflect-metadata: reflect-metadata:
specifier: 0.2.2 specifier: 0.2.2
version: 0.2.2 version: 0.2.2
supertokens-js-override:
specifier: 0.0.4
version: 0.0.4
supertokens-node:
specifier: 16.7.5
version: 16.7.5(encoding@0.1.13)
tslib: tslib:
specifier: 2.8.1 specifier: 2.8.1
version: 2.8.1 version: 2.8.1
@ -12131,9 +12121,6 @@ packages:
crelt@1.0.6: crelt@1.0.6:
resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
cross-fetch@3.1.8:
resolution: {integrity: sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==}
cross-inspect@1.0.1: cross-inspect@1.0.1:
resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==}
engines: {node: '>=16.0.0'} engines: {node: '>=16.0.0'}
@ -14349,10 +14336,6 @@ packages:
resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==}
engines: {node: '>=12'} engines: {node: '>=12'}
inflation@2.1.0:
resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==}
engines: {node: '>= 0.8.0'}
inflight@1.0.6: inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@ -15109,9 +15092,6 @@ packages:
libmime@2.1.3: libmime@2.1.3:
resolution: {integrity: sha512-ABr2f4O+K99sypmkF/yPz2aXxUFHEZzv+iUkxItCeKZWHHXdQPpDXd6rV1kBBwL4PserzLU09EIzJ2lxC9hPfQ==} resolution: {integrity: sha512-ABr2f4O+K99sypmkF/yPz2aXxUFHEZzv+iUkxItCeKZWHHXdQPpDXd6rV1kBBwL4PserzLU09EIzJ2lxC9hPfQ==}
libphonenumber-js@1.12.17:
resolution: {integrity: sha512-bsxi8FoceAYR/bjHcLYc2ShJ/aVAzo5jaxAYiMHF0BD+NTp47405CGuPNKYpw+lHadN9k/ClFGc9X5vaZswIrA==}
libqp@1.1.0: libqp@1.1.0:
resolution: {integrity: sha512-4Rgfa0hZpG++t1Vi2IiqXG9Ad1ig4QTmtuZF946QJP4bPqOYC78ixUXgz5TW/wE7lNaNKlplSYTxQ+fR2KZ0EA==} resolution: {integrity: sha512-4Rgfa0hZpG++t1Vi2IiqXG9Ad1ig4QTmtuZF946QJP4bPqOYC78ixUXgz5TW/wE7lNaNKlplSYTxQ+fR2KZ0EA==}
@ -16955,9 +16935,6 @@ packages:
resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==} resolution: {integrity: sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==}
engines: {node: '>= 6'} engines: {node: '>= 6'}
pkce-challenge@3.1.0:
resolution: {integrity: sha512-bQ/0XPZZ7eX+cdAkd61uYWpfMhakH3NeteUF1R8GNa+LMqX8QFAkbCLqq+AYAns1/ueACBu/BMWhrlKGrdvGZg==}
pkg-dir@4.2.0: pkg-dir@4.2.0:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'} engines: {node: '>=8'}
@ -17341,9 +17318,6 @@ packages:
pseudomap@1.0.2: pseudomap@1.0.2:
resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==}
psl@1.8.0:
resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==}
pump@3.0.0: pump@3.0.0:
resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==}
@ -17376,9 +17350,6 @@ packages:
resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==} resolution: {integrity: sha512-MWkCOVIcJP9QSKU52Ngow6bsAWAPlPK2MludXvcrS2bGZSl+T1qX9MZvRIkqUIkGLJquMJHWfsT6eRqUpp4aWg==}
engines: {node: '>=18'} engines: {node: '>=18'}
querystringify@2.2.0:
resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==}
queue-microtask@1.2.3: queue-microtask@1.2.3:
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
@ -17872,9 +17843,6 @@ packages:
resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==}
engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'}
requires-port@1.0.0:
resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==}
reselect@5.1.1: reselect@5.1.1:
resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==}
@ -18108,10 +18076,6 @@ packages:
scheduler@0.27.0: scheduler@0.27.0:
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
scmp@2.1.0:
resolution: {integrity: sha512-o/mRQGk9Rcer/jEEw/yw4mwo3EU/NvYvp577/Btqrym9Qy5/MdWGBqipbALgd2lrdWTJ5/gqDusxfnQBxOxT2Q==}
deprecated: Just use Node.js's crypto.timingSafeEqual()
scroll-into-view-if-needed@3.1.0: scroll-into-view-if-needed@3.1.0:
resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==}
@ -18693,9 +18657,6 @@ packages:
supertokens-js-override@0.0.4: supertokens-js-override@0.0.4:
resolution: {integrity: sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==} resolution: {integrity: sha512-r0JFBjkMIdep3Lbk3JA+MpnpuOtw4RSyrlRAbrzMcxwiYco3GFWl/daimQZ5b1forOiUODpOlXbSOljP/oyurg==}
supertokens-node@16.7.5:
resolution: {integrity: sha512-xrVwi0GfgLIqHKaWXEp7fCgmVQ2wTiJTHG2CQGLkGMvrO9dUcxY+H6qozn3EvyXxbkm1BmflYD8N7ILNSIbR1g==}
supertokens-web-js@0.9.0: supertokens-web-js@0.9.0:
resolution: {integrity: sha512-7DucVUWxImrcjckza0oW6tkPfMzScj8V/qiQNZeUT/EfCqIbslNSO8holBHc9Eykc0vG/CC0d9ne5TRhAmRcxg==} resolution: {integrity: sha512-7DucVUWxImrcjckza0oW6tkPfMzScj8V/qiQNZeUT/EfCqIbslNSO8holBHc9Eykc0vG/CC0d9ne5TRhAmRcxg==}
@ -19165,10 +19126,6 @@ packages:
tweetnacl@0.14.5: tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
twilio@4.23.0:
resolution: {integrity: sha512-LdNBQfOe0dY2oJH2sAsrxazpgfFQo5yXGxe96QA8UWB5uu+433PrUbkv8gQ5RmrRCqUTPQ0aOrIyAdBr1aB03Q==}
engines: {node: '>=14.0'}
twoslash-protocol@0.2.12: twoslash-protocol@0.2.12:
resolution: {integrity: sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg==} resolution: {integrity: sha512-5qZLXVYfZ9ABdjqbvPc4RWMr7PrpPaaDSeaYY55vl/w1j6H6kzsWK/urAEIXlzYlyrFmyz1UbwIt+AA0ck+wbg==}
@ -19463,9 +19420,6 @@ packages:
uri-js@4.4.1: uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
urlpattern-polyfill@10.0.0: urlpattern-polyfill@10.0.0:
resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==}
@ -19989,10 +19943,6 @@ packages:
xml@1.0.1: xml@1.0.1:
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==} resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
xmlbuilder@13.0.2:
resolution: {integrity: sha512-Eux0i2QdDYKbdbA6AM6xE4m6ZTZr4G4xF9kahI2ukSEMCzwce2eX9WlTI5J3s+NU7hpasFsr8hWIONae7LluAQ==}
engines: {node: '>=6.0'}
xorshift@1.2.0: xorshift@1.2.0:
resolution: {integrity: sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==} resolution: {integrity: sha512-iYgNnGyeeJ4t6U11NpA/QiKy+PXn5Aa3Azg5qkwIFz1tBLllQrjjsk9yzD7IAK0naNU4JxdeDgqW9ov4u/hc4g==}
@ -23541,7 +23491,7 @@ snapshots:
'@graphql-mesh/hmac-upstream-signature': 2.0.8(graphql@16.12.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/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/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/types': 0.104.16(graphql@16.12.0)
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
'@graphql-tools/batch-delegate': 10.0.8(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/delegate': 12.0.2(graphql@16.12.0)
@ -23767,7 +23717,7 @@ snapshots:
'@graphql-hive/logger': 1.0.9(pino@10.3.0) '@graphql-hive/logger': 1.0.9(pino@10.3.0)
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.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/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/types': 0.104.16(graphql@16.12.0)
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
'@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0)
'@opentelemetry/api': 1.9.0 '@opentelemetry/api': 1.9.0
@ -24248,7 +24198,7 @@ snapshots:
'@graphql-hive/logger': 1.0.9(pino@10.3.0) '@graphql-hive/logger': 1.0.9(pino@10.3.0)
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.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/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/types': 0.104.16(graphql@16.12.0)
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
'@graphql-tools/batch-execute': 10.0.4(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/delegate': 12.0.2(graphql@16.12.0)
@ -24306,7 +24256,7 @@ snapshots:
'@graphql-mesh/hmac-upstream-signature@2.0.8(graphql@16.12.0)': '@graphql-mesh/hmac-upstream-signature@2.0.8(graphql@16.12.0)':
dependencies: dependencies:
'@graphql-mesh/cross-helpers': 0.4.10(graphql@16.12.0) '@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/types': 0.104.16(graphql@16.12.0)
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
'@graphql-tools/executor-common': 1.0.5(graphql@16.12.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.12.0)
'@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0)
@ -24433,7 +24383,7 @@ snapshots:
'@envelop/response-cache': 9.0.0(@envelop/core@5.5.1)(graphql@16.12.0) '@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/cross-helpers': 0.4.10(graphql@16.12.0)
'@graphql-mesh/string-interpolation': 0.5.9(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/types': 0.104.16(graphql@16.12.0)
'@graphql-mesh/utils': 0.104.16(graphql@16.12.0) '@graphql-mesh/utils': 0.104.16(graphql@16.12.0)
'@graphql-tools/utils': 10.9.1(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.17.1(graphql@16.12.0))(graphql@16.12.0) '@graphql-yoga/plugin-response-cache': 3.15.4(graphql-yoga@5.17.1(graphql@16.12.0))(graphql@16.12.0)
@ -24539,7 +24489,7 @@ snapshots:
'@graphql-hive/logger': 1.0.9(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/pubsub': 2.1.1(ioredis@5.8.2)
'@graphql-hive/signal': 2.0.0 '@graphql-hive/signal': 2.0.0
'@graphql-mesh/types': 0.104.16(graphql@16.12.0)(ioredis@5.8.2) '@graphql-mesh/types': 0.104.16(graphql@16.12.0)
'@graphql-tools/executor': 1.5.0(graphql@16.12.0) '@graphql-tools/executor': 1.5.0(graphql@16.12.0)
'@graphql-tools/executor-common': 1.0.5(graphql@16.12.0) '@graphql-tools/executor-common': 1.0.5(graphql@16.12.0)
'@graphql-tools/utils': 10.11.0(graphql@16.12.0) '@graphql-tools/utils': 10.11.0(graphql@16.12.0)
@ -24638,6 +24588,21 @@ snapshots:
- utf-8-validate - utf-8-validate
- winston - winston
'@graphql-mesh/types@0.104.16(graphql@16.12.0)':
dependencies:
'@graphql-hive/pubsub': 2.1.1(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-typed-document-node/core': 3.2.0(graphql@16.12.0)
'@repeaterjs/repeater': 3.0.6
'@whatwg-node/disposablestack': 0.0.6
graphql: 16.12.0
tslib: 2.8.1
transitivePeerDependencies:
- '@nats-io/nats-core'
- ioredis
'@graphql-mesh/types@0.104.16(graphql@16.12.0)(ioredis@5.8.2)': '@graphql-mesh/types@0.104.16(graphql@16.12.0)(ioredis@5.8.2)':
dependencies: dependencies:
'@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2) '@graphql-hive/pubsub': 2.1.1(ioredis@5.8.2)
@ -24673,7 +24638,7 @@ snapshots:
'@envelop/instrumentation': 1.0.0 '@envelop/instrumentation': 1.0.0
'@graphql-mesh/cross-helpers': 0.4.10(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/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/types': 0.104.16(graphql@16.12.0)
'@graphql-tools/batch-delegate': 10.0.5(graphql@16.12.0) '@graphql-tools/batch-delegate': 10.0.5(graphql@16.12.0)
'@graphql-tools/delegate': 11.1.3(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/utils': 10.9.1(graphql@16.12.0)
@ -30905,7 +30870,7 @@ snapshots:
'@slack/types': 2.16.0 '@slack/types': 2.16.0
'@types/node': 24.10.12 '@types/node': 24.10.12
'@types/retry': 0.12.0 '@types/retry': 0.12.0
axios: 1.13.5(debug@4.4.1) axios: 1.13.5
eventemitter3: 5.0.1 eventemitter3: 5.0.1
form-data: 4.0.4 form-data: 4.0.4
is-electron: 2.2.2 is-electron: 2.2.2
@ -33777,9 +33742,9 @@ snapshots:
axe-core@4.7.0: {} axe-core@4.7.0: {}
axios@1.13.5(debug@4.4.1): axios@1.13.5:
dependencies: dependencies:
follow-redirects: 1.15.11(debug@4.4.1) follow-redirects: 1.15.11
form-data: 4.0.5 form-data: 4.0.5
proxy-from-env: 1.1.0 proxy-from-env: 1.1.0
transitivePeerDependencies: transitivePeerDependencies:
@ -34741,12 +34706,6 @@ snapshots:
crelt@1.0.6: {} crelt@1.0.6: {}
cross-fetch@3.1.8(encoding@0.1.13):
dependencies:
node-fetch: 2.6.12(encoding@0.1.13)
transitivePeerDependencies:
- encoding
cross-inspect@1.0.1: cross-inspect@1.0.1:
dependencies: dependencies:
tslib: 2.8.1 tslib: 2.8.1
@ -36405,9 +36364,7 @@ snapshots:
fn-name@3.0.0: {} fn-name@3.0.0: {}
follow-redirects@1.15.11(debug@4.4.1): follow-redirects@1.15.11: {}
optionalDependencies:
debug: 4.4.1(supports-color@8.1.1)
for-each@0.3.3: for-each@0.3.3:
dependencies: dependencies:
@ -37682,8 +37639,6 @@ snapshots:
indent-string@5.0.0: {} indent-string@5.0.0: {}
inflation@2.1.0: {}
inflight@1.0.6: inflight@1.0.6:
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
@ -38450,8 +38405,6 @@ snapshots:
libbase64: 0.1.0 libbase64: 0.1.0
libqp: 1.1.0 libqp: 1.1.0
libphonenumber-js@1.12.17: {}
libqp@1.1.0: {} libqp@1.1.0: {}
lie@3.1.1: lie@3.1.1:
@ -40927,10 +40880,6 @@ snapshots:
pirates@4.0.5: {} pirates@4.0.5: {}
pkce-challenge@3.1.0:
dependencies:
crypto-js: 4.2.0
pkg-dir@4.2.0: pkg-dir@4.2.0:
dependencies: dependencies:
find-up: 4.1.0 find-up: 4.1.0
@ -41244,8 +41193,6 @@ snapshots:
pseudomap@1.0.2: {} pseudomap@1.0.2: {}
psl@1.8.0: {}
pump@3.0.0: pump@3.0.0:
dependencies: dependencies:
end-of-stream: 1.4.4 end-of-stream: 1.4.4
@ -41275,8 +41222,6 @@ snapshots:
filter-obj: 5.1.0 filter-obj: 5.1.0
split-on-first: 3.0.0 split-on-first: 3.0.0
querystringify@2.2.0: {}
queue-microtask@1.2.3: {} queue-microtask@1.2.3: {}
quick-format-unescaped@4.0.4: {} quick-format-unescaped@4.0.4: {}
@ -41902,8 +41847,6 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
requires-port@1.0.0: {}
reselect@5.1.1: {} reselect@5.1.1: {}
resolve-alpn@1.2.1: {} resolve-alpn@1.2.1: {}
@ -42175,8 +42118,6 @@ snapshots:
scheduler@0.27.0: {} scheduler@0.27.0: {}
scmp@2.1.0: {}
scroll-into-view-if-needed@3.1.0: scroll-into-view-if-needed@3.1.0:
dependencies: dependencies:
compute-scroll-into-view: 3.1.1 compute-scroll-into-view: 3.1.1
@ -42879,24 +42820,6 @@ snapshots:
supertokens-js-override@0.0.4: {} supertokens-js-override@0.0.4: {}
supertokens-node@16.7.5(encoding@0.1.13):
dependencies:
content-type: 1.0.5
cookie: 0.7.2
cross-fetch: 3.1.8(encoding@0.1.13)
debug: 4.4.1(supports-color@8.1.1)
inflation: 2.1.0
jose: 4.15.9
libphonenumber-js: 1.12.17
nodemailer: 7.0.11
pkce-challenge: 3.1.0
psl: 1.8.0
supertokens-js-override: 0.0.4
twilio: 4.23.0(debug@4.4.1)
transitivePeerDependencies:
- encoding
- supports-color
supertokens-web-js@0.9.0: supertokens-web-js@0.9.0:
dependencies: dependencies:
supertokens-js-override: 0.0.4 supertokens-js-override: 0.0.4
@ -43430,20 +43353,6 @@ snapshots:
tweetnacl@0.14.5: {} tweetnacl@0.14.5: {}
twilio@4.23.0(debug@4.4.1):
dependencies:
axios: 1.13.5(debug@4.4.1)
dayjs: 1.11.13
https-proxy-agent: 5.0.1
jsonwebtoken: 9.0.3
qs: 6.14.2
scmp: 2.1.0
url-parse: 1.5.10
xmlbuilder: 13.0.2
transitivePeerDependencies:
- debug
- supports-color
twoslash-protocol@0.2.12: {} twoslash-protocol@0.2.12: {}
twoslash@0.2.12(typescript@5.9.3): twoslash@0.2.12(typescript@5.9.3):
@ -43771,11 +43680,6 @@ snapshots:
dependencies: dependencies:
punycode: 2.1.1 punycode: 2.1.1
url-parse@1.5.10:
dependencies:
querystringify: 2.2.0
requires-port: 1.0.0
urlpattern-polyfill@10.0.0: {} urlpattern-polyfill@10.0.0: {}
urlpattern-polyfill@8.0.2: {} urlpattern-polyfill@8.0.2: {}
@ -44188,7 +44092,7 @@ snapshots:
wait-on@9.0.3: wait-on@9.0.3:
dependencies: dependencies:
axios: 1.13.5(debug@4.4.1) axios: 1.13.5
joi: 18.0.2 joi: 18.0.2
lodash: 4.17.23 lodash: 4.17.23
minimist: 1.2.8 minimist: 1.2.8
@ -44401,8 +44305,6 @@ snapshots:
xml@1.0.1: {} xml@1.0.1: {}
xmlbuilder@13.0.2: {}
xorshift@1.2.0: {} xorshift@1.2.0: {}
xtend@4.0.2: {} xtend@4.0.2: {}

View file

@ -13,7 +13,7 @@
*/ */
import * as readline from 'node:readline/promises'; import * as readline from 'node:readline/promises';
import { createTRPCProxyClient, httpLink } from '@trpc/client'; import setCookie from 'set-cookie-parser';
import type { CollectedOperation } from '../integration-tests/testkit/usage'; import type { CollectedOperation } from '../integration-tests/testkit/usage';
process.env.RUN_AGAINST_LOCAL_SERVICES = '1'; process.env.RUN_AGAINST_LOCAL_SERVICES = '1';
@ -42,92 +42,69 @@ const password = 'ilikebigturtlesandicannotlie47';
async function signInOrSignUp( async function signInOrSignUp(
email: string, email: string,
): Promise<{ access_token: string; refresh_token: string }> { ): Promise<{ access_token: string; refresh_token: string }> {
const supertokensUri = ensureEnv('SUPERTOKENS_CONNECTION_URI');
const apiKey = ensureEnv('SUPERTOKENS_API_KEY');
const headers = {
'content-type': 'application/json; charset=UTF-8',
'api-key': apiKey,
'cdi-version': '4.0',
};
const body = JSON.stringify({ email, password });
// Try signup first
let res = await fetch(`${supertokensUri}/appid-public/public/recipe/signup`, {
method: 'POST',
headers,
body,
});
let data = (await res.json()) as { status: string; user?: { id: string; emails: string[] } };
// If user already exists, look them up by email (avoids needing their password)
if (data.status === 'EMAIL_ALREADY_EXISTS_ERROR') {
res = await fetch(
`${supertokensUri}/appid-public/public/recipe/user?email=${encodeURIComponent(email)}`,
{ headers },
);
const lookupData = (await res.json()) as {
status: string;
user?: { id: string; emails: string[] };
};
if (lookupData.status !== 'OK' || !lookupData.user) {
throw new Error(`User lookup failed for ${email}: ${JSON.stringify(lookupData)}`);
}
data = { status: 'OK', user: lookupData.user };
}
if (data.status !== 'OK' || !data.user) {
throw new Error(`Auth failed for ${email}: ${JSON.stringify(data)}`);
}
const superTokensUserId = data.user.id;
// Ensure user exists in Hive DB
const graphqlAddress = await getServiceHost('server', 8082); const graphqlAddress = await getServiceHost('server', 8082);
const internalApi = createTRPCProxyClient<any>({
links: [httpLink({ url: `http://${graphqlAddress}/trpc`, fetch })],
});
const ensureUserResult = await internalApi.ensureUser.mutate({
superTokensUserId,
email,
oidcIntegrationId: null,
firstName: null,
lastName: null,
});
if (!ensureUserResult.ok) {
throw new Error(`ensureUser failed: ${ensureUserResult.reason}`);
}
// Create session let response = await fetch(`http://${graphqlAddress}/auth-api/signup`, {
const sessionPayload = {
version: '2',
superTokensUserId,
userId: ensureUserResult.user.id,
oidcIntegrationId: null,
email,
};
const sessionRes = await fetch(`${supertokensUri}/appid-public/public/recipe/session`, {
method: 'POST', method: 'POST',
headers: { ...headers, rid: 'session' },
body: JSON.stringify({ body: JSON.stringify({
enableAntiCsrf: false, formFields: [
userId: superTokensUserId, {
userDataInDatabase: sessionPayload, id: 'email',
userDataInJWT: sessionPayload, value: email,
},
{
id: 'password',
value: password,
},
],
}), }),
headers: {
'content-type': 'application/json',
},
}); });
const sessionData = (await sessionRes.json()) as {
accessToken?: { token: string };
refreshToken?: { token: string };
};
if (!sessionData.accessToken?.token || !sessionData.refreshToken?.token) { let body = await response.json();
throw new Error(`Session creation failed: ${JSON.stringify(sessionData)}`); if (body.status === 'OK') {
const cookies = setCookie.parse(response.headers.getSetCookie());
return {
access_token: cookies.find(c => c.name === 'sAccessToken')?.value ?? '',
refresh_token: cookies.find(c => c.name === 'sRefreshToken')?.value ?? '',
};
} }
return { console.log('signup response', JSON.stringify(body, null, 2));
access_token: sessionData.accessToken.token, console.log('attempt sign in');
refresh_token: sessionData.refreshToken.token,
}; response = await fetch(`http://${graphqlAddress}/auth-api/signin`, {
method: 'POST',
body: JSON.stringify({
formFields: [
{
id: 'email',
value: email,
},
{
id: 'password',
value: password,
},
],
}),
headers: {
'content-type': 'application/json',
},
});
body = await response.json();
if (body.status === 'OK') {
const cookies = setCookie.parse(response.headers.getSetCookie());
return {
access_token: cookies.find(c => c.name === 'sAccessToken')?.value ?? '',
refresh_token: cookies.find(c => c.name === 'sRefreshToken')?.value ?? '',
};
}
throw new Error('Failed to sign in or up ' + JSON.stringify(body, null, 2));
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------