From 47743a5fa84887c9cac22a6957bc7f36b1465168 Mon Sep 17 00:00:00 2001
From: Owen
Date: Wed, 11 Feb 2026 10:26:50 -0800
Subject: [PATCH 001/100] Fix private import -> dynamic
---
server/routers/badger/verifySession.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts
index 828960d15..c446e0f7f 100644
--- a/server/routers/badger/verifySession.ts
+++ b/server/routers/badger/verifySession.ts
@@ -39,7 +39,7 @@ import {
import { logRequestAudit } from "./logRequestAudit";
import cache from "@server/lib/cache";
import { APP_VERSION } from "@server/lib/consts";
-import { isSubscribed } from "#private/lib/isSubscribed";
+import { isSubscribed } from "#dynamic/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const verifyResourceSessionSchema = z.object({
From 040a9457741f7dd273f4cd53dde36b3e0d77b3a1 Mon Sep 17 00:00:00 2001
From: Owen
Date: Wed, 11 Feb 2026 10:30:25 -0800
Subject: [PATCH 002/100] Fix dynamic -> private import violations
---
server/private/routers/orgIdp/createOrgOidcIdp.ts | 2 +-
server/private/routers/orgIdp/updateOrgOidcIdp.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts
index 23fa4b7ce..d18740338 100644
--- a/server/private/routers/orgIdp/createOrgOidcIdp.ts
+++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts
@@ -25,7 +25,7 @@ import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
-import { isSubscribed } from "#dynamic/lib/isSubscribed";
+import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts
index d83ff569a..c56194606 100644
--- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts
+++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts
@@ -24,7 +24,7 @@ import { idp, idpOidcConfig } from "@server/db";
import { eq, and } from "drizzle-orm";
import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
-import { isSubscribed } from "#dynamic/lib/isSubscribed";
+import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
const paramsSchema = z
From 6475dceab9e1b5971aa18579c3f1ef00db0345d4 Mon Sep 17 00:00:00 2001
From: Owen
Date: Wed, 11 Feb 2026 12:38:18 -0800
Subject: [PATCH 003/100] Rename tiers in features and fix subscribed logic
issue
---
server/lib/billing/features.ts | 20 +++++++++----------
server/private/routers/billing/changeTier.ts | 12 +++++------
.../routers/billing/createCheckoutSession.ts | 12 +++++------
.../routers/billing/hooks/getSubType.ts | 12 +++++------
server/routers/idp/validateOidcCallback.ts | 2 +-
5 files changed, 29 insertions(+), 29 deletions(-)
diff --git a/server/lib/billing/features.ts b/server/lib/billing/features.ts
index 501fba36f..3fec53b41 100644
--- a/server/lib/billing/features.ts
+++ b/server/lib/billing/features.ts
@@ -56,22 +56,22 @@ export function getFeatureIdByMetricId(
export type FeaturePriceSet = Partial>;
-export const homeLabFeaturePriceSet: FeaturePriceSet = {
+export const tier1FeaturePriceSet: FeaturePriceSet = {
[FeatureId.TIER1]: "price_1SzVE3D3Ee2Ir7Wm6wT5Dl3G"
};
-export const homeLabFeaturePriceSetSandbox: FeaturePriceSet = {
+export const tier1FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.TIER1]: "price_1SxgpPDCpkOb237Bfo4rIsoT"
};
-export function getHomeLabFeaturePriceSet(): FeaturePriceSet {
+export function getTier1FeaturePriceSet(): FeaturePriceSet {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
) {
- return homeLabFeaturePriceSet;
+ return tier1FeaturePriceSet;
} else {
- return homeLabFeaturePriceSetSandbox;
+ return tier1FeaturePriceSetSandbox;
}
}
@@ -83,7 +83,7 @@ export const tier2FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SxaEHDCpkOb237BD9lBkPiR"
};
-export function getStarterFeaturePriceSet(): FeaturePriceSet {
+export function getTier2FeaturePriceSet(): FeaturePriceSet {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
@@ -102,7 +102,7 @@ export const tier3FeaturePriceSetSandbox: FeaturePriceSet = {
[FeatureId.USERS]: "price_1SxaEODCpkOb237BiXdCBSfs"
};
-export function getScaleFeaturePriceSet(): FeaturePriceSet {
+export function getTier3FeaturePriceSet(): FeaturePriceSet {
if (
process.env.ENVIRONMENT == "prod" &&
process.env.SANDBOX_MODE !== "true"
@@ -116,9 +116,9 @@ export function getScaleFeaturePriceSet(): FeaturePriceSet {
export function getFeatureIdByPriceId(priceId: string): FeatureId | undefined {
// Check all feature price sets
const allPriceSets = [
- getHomeLabFeaturePriceSet(),
- getStarterFeaturePriceSet(),
- getScaleFeaturePriceSet()
+ getTier1FeaturePriceSet(),
+ getTier2FeaturePriceSet(),
+ getTier3FeaturePriceSet()
];
for (const priceSet of allPriceSets) {
diff --git a/server/private/routers/billing/changeTier.ts b/server/private/routers/billing/changeTier.ts
index ee60c0ec7..3c9b8e437 100644
--- a/server/private/routers/billing/changeTier.ts
+++ b/server/private/routers/billing/changeTier.ts
@@ -22,9 +22,9 @@ import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import stripe from "#private/lib/stripe";
import {
- getHomeLabFeaturePriceSet,
- getScaleFeaturePriceSet,
- getStarterFeaturePriceSet,
+ getTier1FeaturePriceSet,
+ getTier3FeaturePriceSet,
+ getTier2FeaturePriceSet,
FeatureId,
type FeaturePriceSet
} from "@server/lib/billing";
@@ -113,11 +113,11 @@ export async function changeTier(
// Get the target tier's price set
let targetPriceSet: FeaturePriceSet;
if (tier === "tier1") {
- targetPriceSet = getHomeLabFeaturePriceSet();
+ targetPriceSet = getTier1FeaturePriceSet();
} else if (tier === "tier2") {
- targetPriceSet = getStarterFeaturePriceSet();
+ targetPriceSet = getTier2FeaturePriceSet();
} else if (tier === "tier3") {
- targetPriceSet = getScaleFeaturePriceSet();
+ targetPriceSet = getTier3FeaturePriceSet();
} else {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid tier"));
}
diff --git a/server/private/routers/billing/createCheckoutSession.ts b/server/private/routers/billing/createCheckoutSession.ts
index e5bb95db2..b35c65329 100644
--- a/server/private/routers/billing/createCheckoutSession.ts
+++ b/server/private/routers/billing/createCheckoutSession.ts
@@ -23,9 +23,9 @@ import config from "@server/lib/config";
import { fromError } from "zod-validation-error";
import stripe from "#private/lib/stripe";
import {
- getHomeLabFeaturePriceSet,
- getScaleFeaturePriceSet,
- getStarterFeaturePriceSet
+ getTier1FeaturePriceSet,
+ getTier3FeaturePriceSet,
+ getTier2FeaturePriceSet
} from "@server/lib/billing";
import { getLineItems } from "@server/lib/billing/getLineItems";
import Stripe from "stripe";
@@ -88,11 +88,11 @@ export async function createCheckoutSession(
let lineItems: Stripe.Checkout.SessionCreateParams.LineItem[];
if (tier === "tier1") {
- lineItems = await getLineItems(getHomeLabFeaturePriceSet(), orgId);
+ lineItems = await getLineItems(getTier1FeaturePriceSet(), orgId);
} else if (tier === "tier2") {
- lineItems = await getLineItems(getStarterFeaturePriceSet(), orgId);
+ lineItems = await getLineItems(getTier2FeaturePriceSet(), orgId);
} else if (tier === "tier3") {
- lineItems = await getLineItems(getScaleFeaturePriceSet(), orgId);
+ lineItems = await getLineItems(getTier3FeaturePriceSet(), orgId);
} else {
return next(createHttpError(HttpCode.BAD_REQUEST, "Invalid plan"));
}
diff --git a/server/private/routers/billing/hooks/getSubType.ts b/server/private/routers/billing/hooks/getSubType.ts
index a38290ebd..44cfe0026 100644
--- a/server/private/routers/billing/hooks/getSubType.ts
+++ b/server/private/routers/billing/hooks/getSubType.ts
@@ -15,9 +15,9 @@ import {
getLicensePriceSet,
} from "@server/lib/billing/licenses";
import {
- getHomeLabFeaturePriceSet,
- getStarterFeaturePriceSet,
- getScaleFeaturePriceSet,
+ getTier1FeaturePriceSet,
+ getTier2FeaturePriceSet,
+ getTier3FeaturePriceSet,
} from "@server/lib/billing/features";
import Stripe from "stripe";
import { Tier } from "@server/types/Tiers";
@@ -40,19 +40,19 @@ export function getSubType(fullSubscription: Stripe.Response
Date: Wed, 11 Feb 2026 12:41:22 -0800
Subject: [PATCH 004/100] Fix anouther subscribed logic issue
---
server/routers/user/createOrgUser.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts
index cac912f97..d0515e71e 100644
--- a/server/routers/user/createOrgUser.ts
+++ b/server/routers/user/createOrgUser.ts
@@ -132,7 +132,7 @@ export async function createOrgUser(
orgId,
tierMatrix.orgOidc
);
- if (subscribed) {
+ if (!subscribed) {
return next(
createHttpError(
HttpCode.FORBIDDEN,
From eab275095313fd016d6eab9c5c937e98a468e70d Mon Sep 17 00:00:00 2001
From: Owen
Date: Wed, 11 Feb 2026 17:21:15 -0800
Subject: [PATCH 005/100] Add migrations for 1.15.3
---
server/setup/migrationsPg.ts | 4 ++-
server/setup/migrationsSqlite.ts | 4 ++-
server/setup/scriptsPg/1.15.3.ts | 39 ++++++++++++++++++++++++++++
server/setup/scriptsSqlite/1.15.3.ts | 28 ++++++++++++++++++++
4 files changed, 73 insertions(+), 2 deletions(-)
create mode 100644 server/setup/scriptsPg/1.15.3.ts
create mode 100644 server/setup/scriptsSqlite/1.15.3.ts
diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts
index 7ae218364..c1ebfa09c 100644
--- a/server/setup/migrationsPg.ts
+++ b/server/setup/migrationsPg.ts
@@ -17,6 +17,7 @@ import m9 from "./scriptsPg/1.12.0";
import m10 from "./scriptsPg/1.13.0";
import m11 from "./scriptsPg/1.14.0";
import m12 from "./scriptsPg/1.15.0";
+import m13 from "./scriptsPg/1.15.3";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -34,7 +35,8 @@ const migrations = [
{ version: "1.12.0", run: m9 },
{ version: "1.13.0", run: m10 },
{ version: "1.14.0", run: m11 },
- { version: "1.15.0", run: m12 }
+ { version: "1.15.0", run: m12 },
+ { version: "1.15.3", run: m13 }
// Add new migrations here as they are created
] as {
version: string;
diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts
index 0bbc86ee3..170cf93d0 100644
--- a/server/setup/migrationsSqlite.ts
+++ b/server/setup/migrationsSqlite.ts
@@ -35,6 +35,7 @@ import m30 from "./scriptsSqlite/1.12.0";
import m31 from "./scriptsSqlite/1.13.0";
import m32 from "./scriptsSqlite/1.14.0";
import m33 from "./scriptsSqlite/1.15.0";
+import m34 from "./scriptsSqlite/1.15.3";
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
// EXCEPT FOR THE DATABASE AND THE SCHEMA
@@ -68,7 +69,8 @@ const migrations = [
{ version: "1.12.0", run: m30 },
{ version: "1.13.0", run: m31 },
{ version: "1.14.0", run: m32 },
- { version: "1.15.0", run: m33 }
+ { version: "1.15.0", run: m33 },
+ { version: "1.15.3", run: m34 }
// Add new migrations here as they are created
] as const;
diff --git a/server/setup/scriptsPg/1.15.3.ts b/server/setup/scriptsPg/1.15.3.ts
new file mode 100644
index 000000000..80c5f67d3
--- /dev/null
+++ b/server/setup/scriptsPg/1.15.3.ts
@@ -0,0 +1,39 @@
+import { db } from "@server/db/pg/driver";
+import { sql } from "drizzle-orm";
+import { __DIRNAME } from "@server/lib/consts";
+
+const version = "1.15.3";
+
+export default async function migration() {
+ console.log(`Running setup script ${version}...`);
+
+ try {
+ await db.execute(sql`BEGIN`);
+
+ await db.execute(
+ sql`ALTER TABLE "limits" ADD COLUMN "override" boolean DEFAULT false;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "subscriptionItems" ADD COLUMN "stripeSubscriptionItemId" varchar(255);`
+ );
+ await db.execute(
+ sql`ALTER TABLE "subscriptionItems" ADD COLUMN "featureId" varchar(255);`
+ );
+ await db.execute(
+ sql`ALTER TABLE "subscriptions" ADD COLUMN "version" integer;`
+ );
+ await db.execute(
+ sql`ALTER TABLE "subscriptions" ADD COLUMN "type" varchar(50);`
+ );
+
+ await db.execute(sql`COMMIT`);
+ console.log("Migrated database");
+ } catch (e) {
+ await db.execute(sql`ROLLBACK`);
+ console.log("Unable to migrate database");
+ console.log(e);
+ throw e;
+ }
+
+ console.log(`${version} migration complete`);
+}
diff --git a/server/setup/scriptsSqlite/1.15.3.ts b/server/setup/scriptsSqlite/1.15.3.ts
new file mode 100644
index 000000000..614b07598
--- /dev/null
+++ b/server/setup/scriptsSqlite/1.15.3.ts
@@ -0,0 +1,28 @@
+import { __DIRNAME, APP_PATH } from "@server/lib/consts";
+import Database from "better-sqlite3";
+import path from "path";
+
+const version = "1.15.3";
+
+export default async function migration() {
+ console.log(`Running setup script ${version}...`);
+
+ const location = path.join(APP_PATH, "db", "db.sqlite");
+ const db = new Database(location);
+
+ try {
+ db.transaction(() => {
+ db.prepare(`ALTER TABLE 'limits' ADD 'override' integer DEFAULT false;`).run();
+ db.prepare(`ALTER TABLE 'subscriptionItems' ADD 'featureId' text;`).run();
+ db.prepare(`ALTER TABLE 'subscriptions' ADD 'version' integer;`).run();
+ db.prepare(`ALTER TABLE 'subscriptions' ADD 'type' text;`).run();
+ })();
+
+ console.log(`Migrated database`);
+ } catch (e) {
+ console.log("Failed to migrate db:", e);
+ throw e;
+ }
+
+ console.log(`${version} migration complete`);
+}
From ba7239ac087af70fd554e1bc0d43b59e0a93cca9 Mon Sep 17 00:00:00 2001
From: Owen
Date: Wed, 11 Feb 2026 17:30:21 -0800
Subject: [PATCH 006/100] Verify everything we can
---
.github/workflows/cicd.yml | 189 +++++++------
.github/workflows/cicd.yml.backup | 426 ------------------------------
.github/workflows/saas.yml | 2 +-
3 files changed, 112 insertions(+), 505 deletions(-)
delete mode 100644 .github/workflows/cicd.yml.backup
diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml
index 0e4d9bc6c..2ebb46639 100644
--- a/.github/workflows/cicd.yml
+++ b/.github/workflows/cicd.yml
@@ -1,4 +1,4 @@
-name: CI/CD Pipeline
+name: Public Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
@@ -440,6 +440,10 @@ jobs:
issuer="https://token.actions.githubusercontent.com"
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
+ # Track failures
+ FAILED_TAGS=()
+ SUCCESSFUL_TAGS=()
+
# Determine if this is an RC release
IS_RC="false"
if [[ "$TAG" == *"-rc."* ]]; then
@@ -471,94 +475,123 @@ jobs:
for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
+ TAG_FAILED=false
- DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
- REF="${BASE_IMAGE}@${DIGEST}"
- echo "Resolved digest: ${REF}"
+ # Wrap the entire tag processing in error handling
+ (
+ set -e
+ DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
+ REF="${BASE_IMAGE}@${DIGEST}"
+ echo "Resolved digest: ${REF}"
- echo "==> cosign sign (keyless) --recursive ${REF}"
- cosign sign --recursive "${REF}"
+ echo "==> cosign sign (keyless) --recursive ${REF}"
+ cosign sign --recursive "${REF}"
- echo "==> cosign sign (key) --recursive ${REF}"
- cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
+ echo "==> cosign sign (key) --recursive ${REF}"
+ cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
- # Retry wrapper for verification to handle registry propagation delays
- retry_verify() {
- local cmd="$1"
- local attempts=6
- local delay=5
- local i=1
- until eval "$cmd"; do
- if [ $i -ge $attempts ]; then
- echo "Verification failed after $attempts attempts"
- return 1
- fi
- echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
- sleep $delay
- i=$((i+1))
- delay=$((delay*2))
- # Cap the delay to avoid very long waits
- if [ $delay -gt 60 ]; then delay=60; fi
- done
- return 0
- }
-
- echo "==> cosign verify (public key) ${REF}"
- if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then
- VERIFIED_INDEX=true
- else
- VERIFIED_INDEX=false
- fi
-
- echo "==> cosign verify (keyless policy) ${REF}"
- if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
- VERIFIED_INDEX_KEYLESS=true
- else
- VERIFIED_INDEX_KEYLESS=false
- fi
-
- # If index verification fails, attempt to verify child platform manifests
- if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
- echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
- CHILD_VERIFIED=false
-
- for ARCH in arm64 amd64; do
- CHILD_TAG="${IMAGE_TAG}-${ARCH}"
- echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
- CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
- if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
- CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
- echo "==> cosign verify (public key) child ${CHILD_REF}"
- if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
- CHILD_VERIFIED=true
- echo "Public key verification succeeded for child ${CHILD_REF}"
- else
- echo "Public key verification failed for child ${CHILD_REF}"
+ # Retry wrapper for verification to handle registry propagation delays
+ retry_verify() {
+ local cmd="$1"
+ local attempts=6
+ local delay=5
+ local i=1
+ until eval "$cmd"; do
+ if [ $i -ge $attempts ]; then
+ echo "Verification failed after $attempts attempts"
+ return 1
fi
+ echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
+ sleep $delay
+ i=$((i+1))
+ delay=$((delay*2))
+ # Cap the delay to avoid very long waits
+ if [ $delay -gt 60 ]; then delay=60; fi
+ done
+ return 0
+ }
- echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
- if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
- CHILD_VERIFIED=true
- echo "Keyless verification succeeded for child ${CHILD_REF}"
- else
- echo "Keyless verification failed for child ${CHILD_REF}"
- fi
- else
- echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
- fi
- done
-
- if [ "${CHILD_VERIFIED}" != "true" ]; then
- echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
- exit 10
+ echo "==> cosign verify (public key) ${REF}"
+ if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then
+ VERIFIED_INDEX=true
+ else
+ VERIFIED_INDEX=false
fi
- fi
- echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
+ echo "==> cosign verify (keyless policy) ${REF}"
+ if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
+ VERIFIED_INDEX_KEYLESS=true
+ else
+ VERIFIED_INDEX_KEYLESS=false
+ fi
+
+ # If index verification fails, attempt to verify child platform manifests
+ if [ "${VERIFIED_INDEX}" != "true" ] || [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
+ echo "Index verification not available; attempting child manifest verification for ${BASE_IMAGE}:${IMAGE_TAG}"
+ CHILD_VERIFIED=false
+
+ for ARCH in arm64 amd64; do
+ CHILD_TAG="${IMAGE_TAG}-${ARCH}"
+ echo "Resolving child digest for ${BASE_IMAGE}:${CHILD_TAG}"
+ CHILD_DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${CHILD_TAG} | jq -r '.Digest' || true)"
+ if [ -n "${CHILD_DIGEST}" ] && [ "${CHILD_DIGEST}" != "null" ]; then
+ CHILD_REF="${BASE_IMAGE}@${CHILD_DIGEST}"
+ echo "==> cosign verify (public key) child ${CHILD_REF}"
+ if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${CHILD_REF}' -o text"; then
+ CHILD_VERIFIED=true
+ echo "Public key verification succeeded for child ${CHILD_REF}"
+ else
+ echo "Public key verification failed for child ${CHILD_REF}"
+ fi
+
+ echo "==> cosign verify (keyless policy) child ${CHILD_REF}"
+ if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${CHILD_REF}' -o text"; then
+ CHILD_VERIFIED=true
+ echo "Keyless verification succeeded for child ${CHILD_REF}"
+ else
+ echo "Keyless verification failed for child ${CHILD_REF}"
+ fi
+ else
+ echo "No child digest found for ${BASE_IMAGE}:${CHILD_TAG}; skipping"
+ fi
+ done
+
+ if [ "${CHILD_VERIFIED}" != "true" ]; then
+ echo "Failed to verify index and no child manifests verified for ${BASE_IMAGE}:${IMAGE_TAG}"
+ exit 1
+ fi
+ fi
+ ) || TAG_FAILED=true
+
+ if [ "$TAG_FAILED" = "true" ]; then
+ echo "⚠️ WARNING: Failed to sign/verify ${BASE_IMAGE}:${IMAGE_TAG}"
+ FAILED_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
+ else
+ echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
+ SUCCESSFUL_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
+ fi
done
done
- echo "All images signed and verified successfully!"
+ # Report summary
+ echo ""
+ echo "=========================================="
+ echo "Sign and Verify Summary"
+ echo "=========================================="
+ echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
+ echo "Failed: ${#FAILED_TAGS[@]}"
+ echo ""
+
+ if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
+ echo "Failed tags:"
+ for tag in "${FAILED_TAGS[@]}"; do
+ echo " - $tag"
+ done
+ echo ""
+ echo "⚠️ WARNING: Some tags failed to sign/verify, but continuing anyway"
+ else
+ echo "✓ All images signed and verified successfully!"
+ fi
shell: bash
post-run:
diff --git a/.github/workflows/cicd.yml.backup b/.github/workflows/cicd.yml.backup
deleted file mode 100644
index 09e406ad6..000000000
--- a/.github/workflows/cicd.yml.backup
+++ /dev/null
@@ -1,426 +0,0 @@
-name: CI/CD Pipeline
-
-# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
-# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
-
-permissions:
- contents: read
- packages: write # for GHCR push
- id-token: write # for Cosign Keyless (OIDC) Signing
-
-# Required secrets:
-# - DOCKER_HUB_USERNAME / DOCKER_HUB_ACCESS_TOKEN: push to Docker Hub
-# - GITHUB_TOKEN: used for GHCR login and OIDC keyless signing
-# - COSIGN_PRIVATE_KEY / COSIGN_PASSWORD / COSIGN_PUBLIC_KEY: for key-based signing
-
-on:
- push:
- tags:
- - "[0-9]+.[0-9]+.[0-9]+"
- - "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
-
-concurrency:
- group: ${{ github.ref }}
- cancel-in-progress: true
-
-jobs:
- pre-run:
- runs-on: ubuntu-latest
- permissions: write-all
- steps:
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v2
- with:
- role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
- role-duration-seconds: 3600
- aws-region: ${{ secrets.AWS_REGION }}
-
- - name: Verify AWS identity
- run: aws sts get-caller-identity
-
- - name: Start EC2 instances
- run: |
- aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
- aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
- echo "EC2 instances started"
-
-
- release-arm:
- name: Build and Release (ARM64)
- runs-on: [self-hosted, linux, arm64, us-east-1]
- needs: [pre-run]
- if: >-
- ${{
- needs.pre-run.result == 'success'
- }}
- # Job-level timeout to avoid runaway or stuck runs
- timeout-minutes: 120
- env:
- # Target images
- DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
- GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Monitor storage space
- run: |
- THRESHOLD=75
- USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
- echo "Used space: $USED_SPACE%"
- if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
- echo "Used space is below the threshold of 75% free. Running Docker system prune."
- echo y | docker system prune -a
- else
- echo "Storage space is above the threshold. No action needed."
- fi
-
- - name: Log in to Docker Hub
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
- with:
- registry: docker.io
- username: ${{ secrets.DOCKER_HUB_USERNAME }}
- password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-
- - name: Extract tag name
- id: get-tag
- run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- shell: bash
-
- - name: Update version in package.json
- run: |
- TAG=${{ env.TAG }}
- sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
- cat server/lib/consts.ts
- shell: bash
-
- - name: Check if release candidate
- id: check-rc
- run: |
- TAG=${{ env.TAG }}
- if [[ "$TAG" == *"-rc."* ]]; then
- echo "IS_RC=true" >> $GITHUB_ENV
- else
- echo "IS_RC=false" >> $GITHUB_ENV
- fi
- shell: bash
-
- - name: Build and push Docker images (Docker Hub - ARM64)
- run: |
- TAG=${{ env.TAG }}
- if [ "$IS_RC" = "true" ]; then
- make build-rc-arm tag=$TAG
- else
- make build-release-arm tag=$TAG
- fi
- echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
- shell: bash
-
- release-amd:
- name: Build and Release (AMD64)
- runs-on: [self-hosted, linux, x64, us-east-1]
- needs: [pre-run]
- if: >-
- ${{
- needs.pre-run.result == 'success'
- }}
- # Job-level timeout to avoid runaway or stuck runs
- timeout-minutes: 120
- env:
- # Target images
- DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
- GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Monitor storage space
- run: |
- THRESHOLD=75
- USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
- echo "Used space: $USED_SPACE%"
- if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
- echo "Used space is below the threshold of 75% free. Running Docker system prune."
- echo y | docker system prune -a
- else
- echo "Storage space is above the threshold. No action needed."
- fi
-
- - name: Log in to Docker Hub
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
- with:
- registry: docker.io
- username: ${{ secrets.DOCKER_HUB_USERNAME }}
- password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-
- - name: Extract tag name
- id: get-tag
- run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- shell: bash
-
- - name: Update version in package.json
- run: |
- TAG=${{ env.TAG }}
- sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
- cat server/lib/consts.ts
- shell: bash
-
- - name: Check if release candidate
- id: check-rc
- run: |
- TAG=${{ env.TAG }}
- if [[ "$TAG" == *"-rc."* ]]; then
- echo "IS_RC=true" >> $GITHUB_ENV
- else
- echo "IS_RC=false" >> $GITHUB_ENV
- fi
- shell: bash
-
- - name: Build and push Docker images (Docker Hub - AMD64)
- run: |
- TAG=${{ env.TAG }}
- if [ "$IS_RC" = "true" ]; then
- make build-rc-amd tag=$TAG
- else
- make build-release-amd tag=$TAG
- fi
- echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
- shell: bash
-
- create-manifest:
- name: Create Multi-Arch Manifests
- runs-on: [self-hosted, linux, x64, us-east-1]
- needs: [release-arm, release-amd]
- if: >-
- ${{
- needs.release-arm.result == 'success' &&
- needs.release-amd.result == 'success'
- }}
- timeout-minutes: 30
- steps:
- - name: Checkout code
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Log in to Docker Hub
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
- with:
- registry: docker.io
- username: ${{ secrets.DOCKER_HUB_USERNAME }}
- password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
-
- - name: Extract tag name
- id: get-tag
- run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- shell: bash
-
- - name: Check if release candidate
- id: check-rc
- run: |
- TAG=${{ env.TAG }}
- if [[ "$TAG" == *"-rc."* ]]; then
- echo "IS_RC=true" >> $GITHUB_ENV
- else
- echo "IS_RC=false" >> $GITHUB_ENV
- fi
- shell: bash
-
- - name: Create multi-arch manifests
- run: |
- TAG=${{ env.TAG }}
- if [ "$IS_RC" = "true" ]; then
- make create-manifests-rc tag=$TAG
- else
- make create-manifests tag=$TAG
- fi
- echo "Created multi-arch manifests for tag: ${TAG}"
- shell: bash
-
- sign-and-package:
- name: Sign and Package
- runs-on: [self-hosted, linux, x64, us-east-1]
- needs: [release-arm, release-amd, create-manifest]
- if: >-
- ${{
- needs.release-arm.result == 'success' &&
- needs.release-amd.result == 'success' &&
- needs.create-manifest.result == 'success'
- }}
- # Job-level timeout to avoid runaway or stuck runs
- timeout-minutes: 120
- env:
- # Target images
- DOCKERHUB_IMAGE: docker.io/fosrl/${{ github.event.repository.name }}
- GHCR_IMAGE: ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}
-
- steps:
- - name: Checkout code
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
-
- - name: Extract tag name
- id: get-tag
- run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
- shell: bash
-
- - name: Install Go
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- with:
- go-version: 1.24
-
- - name: Update version in package.json
- run: |
- TAG=${{ env.TAG }}
- sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
- cat server/lib/consts.ts
- shell: bash
-
- - name: Pull latest Gerbil version
- id: get-gerbil-tag
- run: |
- LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
- echo "LATEST_GERBIL_TAG=$LATEST_TAG" >> $GITHUB_ENV
- shell: bash
-
- - name: Pull latest Badger version
- id: get-badger-tag
- run: |
- LATEST_TAG=$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
- echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
- shell: bash
-
- - name: Update install/main.go
- run: |
- PANGOLIN_VERSION=${{ env.TAG }}
- GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
- BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
- sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
- sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go
- sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
- echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
- cat install/main.go
- shell: bash
-
- - name: Build installer
- working-directory: install
- run: |
- make go-build-release
-
- - name: Upload artifacts from /install/bin
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
- with:
- name: install-bin
- path: install/bin/
-
- - name: Install skopeo + jq
- # skopeo: copy/inspect images between registries
- # jq: JSON parsing tool used to extract digest values
- run: |
- sudo apt-get update -y
- sudo apt-get install -y skopeo jq
- skopeo --version
- shell: bash
-
- - name: Login to GHCR
- env:
- REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
- run: |
- mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
- skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
- shell: bash
-
- - name: Copy tag from Docker Hub to GHCR
- # Mirror the already-built image (all architectures) to GHCR so we can sign it
- # Wait a bit for both architectures to be available in Docker Hub manifest
- env:
- REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
- run: |
- set -euo pipefail
- TAG=${{ env.TAG }}
- echo "Waiting for multi-arch manifest to be ready..."
- sleep 30
- echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
- skopeo copy --all --retry-times 3 \
- docker://$DOCKERHUB_IMAGE:$TAG \
- docker://$GHCR_IMAGE:$TAG
- shell: bash
-
- - name: Login to GitHub Container Registry (for cosign)
- uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Install cosign
- # cosign is used to sign and verify container images (key and keyless)
- uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
-
- - name: Dual-sign and verify (GHCR & Docker Hub)
- # Sign each image by digest using keyless (OIDC) and key-based signing,
- # then verify both the public key signature and the keyless OIDC signature.
- env:
- TAG: ${{ env.TAG }}
- COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
- COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
- COSIGN_PUBLIC_KEY: ${{ secrets.COSIGN_PUBLIC_KEY }}
- COSIGN_YES: "true"
- run: |
- set -euo pipefail
-
- issuer="https://token.actions.githubusercontent.com"
- id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
-
- for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
- echo "Processing ${IMAGE}:${TAG}"
-
- DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
- REF="${IMAGE}@${DIGEST}"
- echo "Resolved digest: ${REF}"
-
- echo "==> cosign sign (keyless) --recursive ${REF}"
- cosign sign --recursive "${REF}"
-
- echo "==> cosign sign (key) --recursive ${REF}"
- cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
-
- echo "==> cosign verify (public key) ${REF}"
- cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
-
- echo "==> cosign verify (keyless policy) ${REF}"
- cosign verify \
- --certificate-oidc-issuer "${issuer}" \
- --certificate-identity-regexp "${id_regex}" \
- "${REF}" -o text
- done
- shell: bash
-
- post-run:
- needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
- if: >-
- ${{
- always() &&
- needs.pre-run.result == 'success' &&
- (needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
- (needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
- (needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
- (needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
- }}
- runs-on: ubuntu-latest
- permissions: write-all
- steps:
- - name: Configure AWS credentials
- uses: aws-actions/configure-aws-credentials@v2
- with:
- role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
- role-duration-seconds: 3600
- aws-region: ${{ secrets.AWS_REGION }}
-
- - name: Verify AWS identity
- run: aws sts get-caller-identity
-
- - name: Stop EC2 instances
- run: |
- aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
- aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
- echo "EC2 instances stopped"
diff --git a/.github/workflows/saas.yml b/.github/workflows/saas.yml
index 4ceabfea0..5db7aa2f4 100644
--- a/.github/workflows/saas.yml
+++ b/.github/workflows/saas.yml
@@ -1,4 +1,4 @@
-name: CI/CD Pipeline
+name: SAAS Pipeline
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
From 937f6fdae82292e41b85bc188a713ce14bf307ce Mon Sep 17 00:00:00 2001
From: Owen
Date: Wed, 11 Feb 2026 17:56:58 -0800
Subject: [PATCH 007/100] Add better error message
---
server/routers/client/createClient.ts | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts
index 78764eaca..4eafb0616 100644
--- a/server/routers/client/createClient.ts
+++ b/server/routers/client/createClient.ts
@@ -26,6 +26,7 @@ import { generateId } from "@server/auth/sessions/app";
import { OpenAPITags, registry } from "@server/openApi";
import { rebuildClientAssociationsFromClient } from "@server/lib/rebuildClientAssociations";
import { getUniqueClientName } from "@server/db/names";
+import { build } from "@server/build";
const createClientParamsSchema = z.strictObject({
orgId: z.string()
@@ -195,6 +196,12 @@ export async function createClient(
const randomExitNode =
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
+ if (!randomExitNode) {
+ return next(
+ createHttpError(HttpCode.NOT_FOUND, `No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`)
+ );
+ }
+
const [adminRole] = await trx
.select()
.from(roles)
From 143acbae48b871da7a93ef7e3093d3fffdeb0ebc Mon Sep 17 00:00:00 2001
From: miloschwartz
Date: Wed, 11 Feb 2026 18:04:56 -0800
Subject: [PATCH 008/100] add identity provider mode setting
---
server/db/sqlite/schema/privateSchema.ts | 1 +
server/private/lib/config.ts | 11 +-
server/private/lib/readConfigFile.ts | 29 +-
.../routers/orgIdp/createOrgOidcIdp.ts | 13 +
server/private/routers/orgIdp/deleteOrgIdp.ts | 13 +
.../routers/orgIdp/updateOrgOidcIdp.ts | 13 +
.../proxy/[niceId]/authentication/page.tsx | 2 +-
src/app/auth/login/page.tsx | 11 +-
src/app/auth/org/[orgId]/page.tsx | 2 +-
src/app/auth/org/page.tsx | 2 +-
src/app/auth/resource/[resourceGuid]/page.tsx | 2 +-
src/app/navigation.tsx | 7 +-
src/components/AuthPageBrandingForm.tsx | 2 +-
src/components/PermissionsSelectBox.tsx | 2 +-
src/components/SignupForm.tsx | 756 +++++++++---------
src/lib/pullEnv.ts | 8 +-
src/lib/types/env.ts | 2 +-
17 files changed, 481 insertions(+), 395 deletions(-)
diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts
index 265cee10d..40f6d7134 100644
--- a/server/db/sqlite/schema/privateSchema.ts
+++ b/server/db/sqlite/schema/privateSchema.ts
@@ -79,6 +79,7 @@ export const subscriptionItems = sqliteTable("subscriptionItems", {
subscriptionItemId: integer("subscriptionItemId").primaryKey({
autoIncrement: true
}),
+ stripeSubscriptionItemId: text("stripeSubscriptionItemId"),
subscriptionId: text("subscriptionId")
.notNull()
.references(() => subscriptions.subscriptionId, {
diff --git a/server/private/lib/config.ts b/server/private/lib/config.ts
index a1baf5a62..2c3490baf 100644
--- a/server/private/lib/config.ts
+++ b/server/private/lib/config.ts
@@ -65,6 +65,11 @@ export class PrivateConfig {
this.rawPrivateConfig.branding?.logo?.dark_path || undefined;
}
+ if (this.rawPrivateConfig.app.identity_provider_mode) {
+ process.env.IDENTITY_PROVIDER_MODE =
+ this.rawPrivateConfig.app.identity_provider_mode;
+ }
+
process.env.BRANDING_LOGO_AUTH_WIDTH = this.rawPrivateConfig.branding
?.logo?.auth_page?.width
? this.rawPrivateConfig.branding?.logo?.auth_page?.width.toString()
@@ -129,10 +134,8 @@ export class PrivateConfig {
process.env.USE_PANGOLIN_DNS =
this.rawPrivateConfig.flags.use_pangolin_dns.toString();
}
- if (this.rawPrivateConfig.flags.use_org_only_idp) {
- process.env.USE_ORG_ONLY_IDP =
- this.rawPrivateConfig.flags.use_org_only_idp.toString();
- }
+
+ console.log(this.rawPrivateConfig.app.identity_provider_mode);
}
public getRawPrivateConfig() {
diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts
index ac528e736..e5efa4985 100644
--- a/server/private/lib/readConfigFile.ts
+++ b/server/private/lib/readConfigFile.ts
@@ -25,7 +25,8 @@ export const privateConfigSchema = z.object({
app: z
.object({
region: z.string().optional().default("default"),
- base_domain: z.string().optional()
+ base_domain: z.string().optional(),
+ identity_provider_mode: z.enum(["global", "org"]).optional()
})
.optional()
.default({
@@ -95,7 +96,7 @@ export const privateConfigSchema = z.object({
.object({
enable_redis: z.boolean().optional().default(false),
use_pangolin_dns: z.boolean().optional().default(false),
- use_org_only_idp: z.boolean().optional().default(false),
+ use_org_only_idp: z.boolean().optional()
})
.optional()
.prefault({}),
@@ -181,7 +182,29 @@ export const privateConfigSchema = z.object({
// localFilePath: z.string().optional()
})
.optional()
-});
+})
+ .transform((data) => {
+ // this to maintain backwards compatibility with the old config file
+ const identityProviderMode = data.app?.identity_provider_mode;
+ const useOrgOnlyIdp = data.flags?.use_org_only_idp;
+
+ if (identityProviderMode !== undefined) {
+ return data;
+ }
+ if (useOrgOnlyIdp === true) {
+ return {
+ ...data,
+ app: { ...data.app, identity_provider_mode: "org" as const }
+ };
+ }
+ if (useOrgOnlyIdp === false) {
+ return {
+ ...data,
+ app: { ...data.app, identity_provider_mode: "global" as const }
+ };
+ }
+ return data;
+ });
export function readPrivateConfigFile() {
if (build == "oss") {
diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts
index d18740338..77346fd93 100644
--- a/server/private/routers/orgIdp/createOrgOidcIdp.ts
+++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts
@@ -27,6 +27,7 @@ import config from "@server/lib/config";
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
+import privateConfig from "#private/lib/config";
const paramsSchema = z.strictObject({ orgId: z.string().nonempty() });
@@ -92,6 +93,18 @@ export async function createOrgOidcIdp(
);
}
+ if (
+ privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
+ "org"
+ ) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
+ )
+ );
+ }
+
const {
clientId,
clientSecret,
diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts
index 176f4238c..2d6b0899b 100644
--- a/server/private/routers/orgIdp/deleteOrgIdp.ts
+++ b/server/private/routers/orgIdp/deleteOrgIdp.ts
@@ -22,6 +22,7 @@ import { fromError } from "zod-validation-error";
import { idp, idpOidcConfig, idpOrg } from "@server/db";
import { eq } from "drizzle-orm";
import { OpenAPITags, registry } from "@server/openApi";
+import privateConfig from "#private/lib/config";
const paramsSchema = z
.object({
@@ -59,6 +60,18 @@ export async function deleteOrgIdp(
const { idpId } = parsedParams.data;
+ if (
+ privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
+ "org"
+ ) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
+ )
+ );
+ }
+
// Check if IDP exists
const [existingIdp] = await db
.select()
diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts
index c56194606..804afbe63 100644
--- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts
+++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts
@@ -26,6 +26,7 @@ import { encrypt } from "@server/lib/crypto";
import config from "@server/lib/config";
import { isSubscribed } from "#private/lib/isSubscribed";
import { tierMatrix } from "@server/lib/billing/tierMatrix";
+import privateConfig from "#private/lib/config";
const paramsSchema = z
.object({
@@ -97,6 +98,18 @@ export async function updateOrgOidcIdp(
);
}
+ if (
+ privateConfig.getRawPrivateConfig().app.identity_provider_mode !==
+ "org"
+ ) {
+ return next(
+ createHttpError(
+ HttpCode.BAD_REQUEST,
+ "Organization-specific IdP creation is not allowed in the current identity provider mode. Set app.identity_provider_mode to 'org' in the private configuration to enable this feature."
+ )
+ );
+ }
+
const { idpId, orgId } = parsedParams.data;
const {
clientId,
diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx
index 121e71961..b00ce1eeb 100644
--- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx
+++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx
@@ -132,7 +132,7 @@ export default function ResourceAuthenticationPage() {
const { data: orgIdps = [], isLoading: isLoadingOrgIdps } = useQuery(
orgQueries.identityProviders({
orgId: org.org.orgId,
- useOrgOnlyIdp: env.flags.useOrgOnlyIdp
+ useOrgOnlyIdp: env.app.identityProviderMode === "org"
})
);
diff --git a/src/app/auth/login/page.tsx b/src/app/auth/login/page.tsx
index 2ba4d7f81..bfb552df4 100644
--- a/src/app/auth/login/page.tsx
+++ b/src/app/auth/login/page.tsx
@@ -76,12 +76,13 @@ export default async function Page(props: {
// Only use SmartLoginForm if NOT (OSS build OR org-only IdP enabled)
const useSmartLogin =
- build === "saas" || (build === "enterprise" && env.flags.useOrgOnlyIdp);
+ build === "saas" ||
+ (build === "enterprise" && env.app.identityProviderMode === "org");
let loginIdps: LoginFormIDP[] = [];
if (!useSmartLogin) {
// Load IdPs for DashboardLoginForm (OSS or org-only IdP mode)
- if (build === "oss" || !env.flags.useOrgOnlyIdp) {
+ if (build === "oss" || env.app.identityProviderMode !== "org") {
const idpsRes = await cache(
async () =>
await priv.get>("/idp")
@@ -165,7 +166,8 @@ export default async function Page(props: {
forceLogin={forceLogin}
showOrgLogin={
!isInvite &&
- (build === "saas" || env.flags.useOrgOnlyIdp)
+ (build === "saas" ||
+ env.app.identityProviderMode === "org")
}
searchParams={searchParams}
defaultUser={defaultUser}
@@ -188,7 +190,8 @@ export default async function Page(props: {
)}
- {!isInvite && (build === "saas" || env.flags.useOrgOnlyIdp) ? (
+ {!isInvite &&
+ (build === "saas" || env.app.identityProviderMode === "org") ? (
diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx
index d74ef30b5..cb95099e6 100644
--- a/src/app/navigation.tsx
+++ b/src/app/navigation.tsx
@@ -124,7 +124,8 @@ export const orgNavSections = (env?: Env): SidebarNavSection[] => [
// PaidFeaturesAlert
...((build === "oss" && !env?.flags.disableEnterpriseFeatures) ||
build === "saas" ||
- env?.flags.useOrgOnlyIdp
+ env?.app.identityProviderMode === "org" ||
+ env?.app.identityProviderMode === undefined
? [
{
title: "sidebarIdentityProviders",
@@ -251,7 +252,9 @@ export const adminNavSections = (env?: Env): SidebarNavSection[] => [
href: "/admin/api-keys",
icon:
},
- ...(build === "oss" || !env?.flags.useOrgOnlyIdp
+ ...(build === "oss" ||
+ env?.app.identityProviderMode === "global" ||
+ env?.app.identityProviderMode === undefined
? [
{
title: "sidebarIdentityProviders",
diff --git a/src/components/AuthPageBrandingForm.tsx b/src/components/AuthPageBrandingForm.tsx
index daceb2faa..a19980627 100644
--- a/src/components/AuthPageBrandingForm.tsx
+++ b/src/components/AuthPageBrandingForm.tsx
@@ -322,7 +322,7 @@ export default function AuthPageBrandingForm({
{build === "saas" ||
- env.env.flags.useOrgOnlyIdp ? (
+ env.env.app.identityProviderMode === "org" ? (
<>
diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx
index 73f8a2120..b11c635a6 100644
--- a/src/components/PermissionsSelectBox.tsx
+++ b/src/components/PermissionsSelectBox.tsx
@@ -118,7 +118,7 @@ function getActionsCategories(root: boolean) {
}
};
- if (root || build === "saas" || env.flags.useOrgOnlyIdp) {
+ if (root || build === "saas" || env.app.identityProviderMode === "org") {
actionsByCategory["Identity Provider (IDP)"] = {
[t("actionCreateIdp")]: "createIdp",
[t("actionUpdateIdp")]: "updateIdp",
diff --git a/src/components/SignupForm.tsx b/src/components/SignupForm.tsx
index 9cb93757c..a54b1c238 100644
--- a/src/components/SignupForm.tsx
+++ b/src/components/SignupForm.tsx
@@ -204,7 +204,9 @@ export default function SignupForm({
? env.branding.logo?.authPage?.height || 44
: 44;
- const showOrgBanner = fromSmartLogin && (build === "saas" || env.flags.useOrgOnlyIdp);
+ const showOrgBanner =
+ fromSmartLogin &&
+ (build === "saas" || env.app.identityProviderMode === "org");
const orgBannerHref = redirect
? `/auth/org?redirect=${encodeURIComponent(redirect)}`
: "/auth/org";
@@ -226,388 +228,398 @@ export default function SignupForm({
)}
-
-
-
-
-
-
-
-