diff --git a/.github/workflows/e2e-api-tests.yaml b/.github/workflows/e2e-api-tests.yaml
index d3d58d132c..a5ae9f466e 100644
--- a/.github/workflows/e2e-api-tests.yaml
+++ b/.github/workflows/e2e-api-tests.yaml
@@ -143,6 +143,15 @@ jobs:
tail: true
wait-for: 30s
log-output-if: true
+ - name: Start mock-saml-idp in background
+ uses: JarvusInnovations/background-action@v1.0.7
+ with:
+ run: pnpm run start:mock-saml-idp --log-order=stream &
+ wait-on: |
+ http://localhost:8142/idp
+ tail: true
+ wait-for: 30s
+ log-output-if: true
- name: Start run-email-queue in background
uses: JarvusInnovations/background-action@v1.0.7
with:
diff --git a/apps/dev-launchpad/public/index.html b/apps/dev-launchpad/public/index.html
index 8447dd6d98..ccb64c55d9 100644
--- a/apps/dev-launchpad/public/index.html
+++ b/apps/dev-launchpad/public/index.html
@@ -271,6 +271,13 @@
Background services
"Src: ./apps/mock-oauth-server",
],
},
+ {
+ name: "SAML mock IdP",
+ portSuffix: "42",
+ description: [
+ "Src: ./apps/mock-saml-idp",
+ ],
+ },
{
name: "examples/supabase",
portSuffix: "15",
diff --git a/apps/e2e/tests/snapshot-serializer.ts b/apps/e2e/tests/snapshot-serializer.ts
index 6a94aceb82..10337f8a5c 100644
--- a/apps/e2e/tests/snapshot-serializer.ts
+++ b/apps/e2e/tests/snapshot-serializer.ts
@@ -113,10 +113,17 @@ const stripUrlQueryParams = [
"code",
"code_challenge",
"interaction_uid",
+ // SAML — both URL-binding (query) and POST-binding (form) carry these,
+ // each encodes the AuthnRequest ID + timestamps + signature so snapshots
+ // would re-roll on every test run.
+ "SAMLRequest",
+ "SAMLResponse",
+ "RelayState",
] as const;
const keyedCookieNamePrefixes = [
"stack-oauth-inner-",
+ "stack-saml-inner-",
] as const;
const stringRegexReplacements = [
@@ -124,6 +131,8 @@ const stringRegexReplacements = [
[new RegExp(`localhost\:${getPortPrefix()}`, "gi"), "localhost:<$$NEXT_PUBLIC_STACK_PORT_PREFIX>"],
[new RegExp(`localhost\%3A${getPortPrefix()}`, "gi"), "localhost%3A%3C%24NEXT_PUBLIC_STACK_PORT_PREFIX%3E"],
[/(Timeout exceeded: elapsed )[0-9.]+( ms)/gi, "$1$2"],
+ // SAML XML timestamps (e.g. IssueInstant, NotBefore, NotOnOrAfter).
+ [/(IssueInstant|NotBefore|NotOnOrAfter)="[^"]+"/g, '$1=""'],
] as const;
@@ -219,8 +228,9 @@ const snapshotSerializer: SnapshotSerializer = {
if (headerName === "set-cookie") {
const partsStrings = value.split(";").map((part) => part.trim());
let cookieName = partsStrings[0].split("=")[0];
- if (keyedCookieNamePrefixes.some((prefix) => cookieName.startsWith(prefix))) {
- cookieName = `${keyedCookieNamePrefixes}`;
+ const matchedPrefix = keyedCookieNamePrefixes.find((prefix) => cookieName.startsWith(prefix));
+ if (matchedPrefix) {
+ cookieName = `${matchedPrefix}`;
}
const cookieValue = partsStrings[0].split("=")[1];
const parts = new Map(partsStrings.map((part) => {
diff --git a/apps/mock-saml-idp/.eslintrc.cjs b/apps/mock-saml-idp/.eslintrc.cjs
new file mode 100644
index 0000000000..51b882f29c
--- /dev/null
+++ b/apps/mock-saml-idp/.eslintrc.cjs
@@ -0,0 +1,6 @@
+module.exports = {
+ "extends": [
+ "../../configs/eslint/defaults.js",
+ ],
+ "ignorePatterns": ['/*', '!/src']
+};
diff --git a/apps/mock-saml-idp/package.json b/apps/mock-saml-idp/package.json
new file mode 100644
index 0000000000..59151b555c
--- /dev/null
+++ b/apps/mock-saml-idp/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "@stackframe/mock-saml-idp",
+ "version": "2.8.86",
+ "repository": "https://github.com/stack-auth/stack-auth",
+ "private": true,
+ "main": "index.js",
+ "scripts": {
+ "start": "tsx src/index.ts",
+ "dev": "tsx watch --clear-screen=false src/index.ts",
+ "typecheck": "tsc --noEmit",
+ "lint": "eslint --ext .tsx,.ts .",
+ "clean": "rimraf dist && rimraf node_modules"
+ },
+ "dependencies": {
+ "express": "^4.21.2",
+ "handlebars": "^4.7.8",
+ "node-forge": "^1.3.1",
+ "samlify": "^2.10.0"
+ },
+ "devDependencies": {
+ "@types/express": "^5.0.0",
+ "@types/node-forge": "^1.3.11",
+ "tsx": "^4.16.2"
+ },
+ "packageManager": "pnpm@10.23.0"
+}
diff --git a/apps/mock-saml-idp/src/index.ts b/apps/mock-saml-idp/src/index.ts
new file mode 100644
index 0000000000..ca8db66e14
--- /dev/null
+++ b/apps/mock-saml-idp/src/index.ts
@@ -0,0 +1,507 @@
+/**
+ * Mock SAML 2.0 Identity Provider for e2e tests + local development.
+ *
+ * Multi-tenant: serves N virtual IdPs under /idp/:tenant/. Each tenant has
+ * its own RSA keypair + self-signed cert generated at startup. This lets one
+ * mock service back many SamlConnection rows in tests and exercise per-
+ * connection isolation.
+ *
+ * IMPORTANT: Uses `samlify` deliberately because the backend SAML wrapper
+ * (added in the stacked backend PR) uses `@node-saml/node-saml`. Different
+ * libraries on each side means a bug in either library's signature
+ * canonicalization surfaces as a test failure instead of being masked by
+ * both sides agreeing.
+ */
+import express from 'express';
+import handlebars from 'handlebars';
+import forge from 'node-forge';
+import * as samlify from 'samlify';
+
+const stackPortPrefix = process.env.NEXT_PUBLIC_STACK_PORT_PREFIX ?? "81";
+const defaultPort = Number(`${stackPortPrefix}42`);
+const port = Number(process.env.STACK_SAML_MOCK_PORT ?? process.env.PORT ?? defaultPort);
+const tenantSlugs = (process.env.STACK_MOCK_SAML_TENANTS ?? "acme,globex").split(",").map(s => s.trim()).filter(Boolean);
+
+// samlify requires a schema validator. For a test mock we skip XSD validation
+// so we don't need to ship the SAML schema files.
+samlify.setSchemaValidator({
+ validate: async () => "skipped",
+});
+
+type Misbehavior =
+ | { kind: 'none' }
+ | { kind: 'bad-signature' } // sign with another tenant's key
+ | { kind: 'expired' } // NotOnOrAfter in the past
+ | { kind: 'not-yet-valid' } // NotBefore in the far future
+ | { kind: 'wrong-audience' } // Audience set to "https://wrong.example/"
+ | { kind: 'wrong-in-response-to' } // InResponseTo set to a random ID
+ | { kind: 'missing-name-id' } // strip the
+ | { kind: 'missing-email' } // omit the email attribute
+ | { kind: 'replay' } // emit the previous tenant's response again
+ | { kind: 'sign-with-tenant', tenant: string } // sign with a specified tenant's key
+;
+
+type TenantState = {
+ slug: string,
+ entityId: string,
+ privateKeyPem: string,
+ certPem: string,
+ certForMetadata: string, // cert with PEM headers stripped, base64-only (per SAML spec)
+ idp: ReturnType,
+ // Pending misbehavior consumed on the next assertion. Cleared after one use.
+ nextMisbehavior: Misbehavior,
+ // Last successful response context, used for the `replay` misbehavior.
+ lastResponse: { samlResponseB64: string, relayState: string, acsUrl: string } | null,
+};
+
+const tenants = new Map();
+
+// ---------- key + cert generation ----------------------------------------
+
+function generateSelfSignedCert(commonName: string): { privateKeyPem: string, certPem: string } {
+ const keys = forge.pki.rsa.generateKeyPair(2048);
+ const cert = forge.pki.createCertificate();
+ cert.publicKey = keys.publicKey;
+ cert.serialNumber = '01';
+ cert.validity.notBefore = new Date();
+ cert.validity.notAfter = new Date();
+ cert.validity.notAfter.setFullYear(cert.validity.notBefore.getFullYear() + 10);
+ const attrs = [
+ { name: 'commonName', value: commonName },
+ { name: 'organizationName', value: 'Stack Auth Mock SAML IdP' },
+ ];
+ cert.setSubject(attrs);
+ cert.setIssuer(attrs);
+ cert.sign(keys.privateKey, forge.md.sha256.create());
+ return {
+ privateKeyPem: forge.pki.privateKeyToPem(keys.privateKey),
+ certPem: forge.pki.certificateToPem(cert),
+ };
+}
+
+function pemToBase64Cert(pem: string): string {
+ return pem
+ .replace(/-----BEGIN CERTIFICATE-----/g, '')
+ .replace(/-----END CERTIFICATE-----/g, '')
+ .replace(/\s+/g, '');
+}
+
+function entityIdFor(slug: string): string {
+ return `http://localhost:${port}/idp/${slug}/metadata`;
+}
+
+function ssoUrlFor(slug: string): string {
+ return `http://localhost:${port}/idp/${slug}/sso`;
+}
+
+function buildIdpMetadataXml(tenant: TenantState): string {
+ return `
+
+
+
+
+
+ ${tenant.certForMetadata}
+
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
+
+
+`;
+}
+
+// samlify's loginResponseTemplate. Placeholders are substituted in the
+// customTagReplacement callback so we can inject misbehaviors there.
+const loginResponseTemplate = {
+ context: `{Issuer}{Issuer}{NameID}{Audience}{Email}{DisplayName}urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport`,
+};
+
+// ---------- tenant init --------------------------------------------------
+
+for (const slug of tenantSlugs) {
+ const { privateKeyPem, certPem } = generateSelfSignedCert(`mock-saml-idp-${slug}`);
+ const certForMetadata = pemToBase64Cert(certPem);
+ const entityId = entityIdFor(slug);
+
+ // Build minimal IdP metadata for samlify's IdentityProvider constructor.
+ const metadata = `
+
+
+ ${certForMetadata}
+
+
+
+`;
+
+ const idp = samlify.IdentityProvider({
+ metadata,
+ privateKey: privateKeyPem,
+ isAssertionEncrypted: false,
+ loginResponseTemplate,
+ });
+
+ tenants.set(slug, {
+ slug,
+ entityId,
+ privateKeyPem,
+ certPem,
+ certForMetadata,
+ idp,
+ nextMisbehavior: { kind: 'none' },
+ lastResponse: null,
+ });
+}
+
+// ---------- request parsing ----------------------------------------------
+
+type ParsedRequest = {
+ requestId: string,
+ issuer: string, // SP entity ID
+ acsUrl: string, // AssertionConsumerService URL from request
+ relayState: string,
+};
+
+// Decode an HTTP-Redirect AuthnRequest. samlify can do this but to keep the
+// request parsing lib-independent (so a samlify bug here doesn't mask
+// backend bugs), we decode the XML ourselves and pull the fields we need.
+async function parseAuthnRequestRedirect(samlRequestParam: string, relayState: string): Promise {
+ const compressed = Buffer.from(samlRequestParam, 'base64');
+ const zlib = await import('zlib');
+ const xml = zlib.inflateRawSync(compressed).toString('utf-8');
+ return extractRequestFields(xml, relayState);
+}
+
+function extractRequestFields(xml: string, relayState: string): ParsedRequest {
+ const idMatch = xml.match(/ID="([^"]+)"/);
+ const issuerMatch = xml.match(/<(?:saml:)?Issuer[^>]*>([^<]+)<\/(?:saml:)?Issuer>/);
+ const acsMatch = xml.match(/AssertionConsumerServiceURL="([^"]+)"/);
+ if (!idMatch || !issuerMatch || !acsMatch) {
+ throw new Error(`Mock IdP could not parse AuthnRequest (id=${!!idMatch}, issuer=${!!issuerMatch}, acs=${!!acsMatch})`);
+ }
+ return {
+ requestId: idMatch[1],
+ issuer: issuerMatch[1],
+ acsUrl: acsMatch[1],
+ relayState,
+ };
+}
+
+// ---------- assertion building -------------------------------------------
+
+const ASSERTION_LIFETIME_MS = 5 * 60 * 1000;
+
+function isoNow(offsetMs = 0): string {
+ return new Date(Date.now() + offsetMs).toISOString();
+}
+
+type AssertionFields = {
+ audience: string,
+ inResponseTo: string,
+ conditionsNotBefore: string,
+ conditionsNotOnOrAfter: string,
+};
+
+type AssertionResult = {
+ samlResponseB64: string,
+ acsUrl: string,
+ relayState: string,
+};
+
+function consumeNextMisbehavior(tenant: TenantState): Misbehavior {
+ const m = tenant.nextMisbehavior;
+ tenant.nextMisbehavior = { kind: 'none' };
+ return m;
+}
+
+function resolveSigningTenant(tenant: TenantState, misbehavior: Misbehavior): TenantState {
+ if (misbehavior.kind === 'bad-signature') {
+ const other = Array.from(tenants.values()).find(t => t.slug !== tenant.slug);
+ if (!other) {
+ throw new Error('bad-signature misbehavior requires at least 2 tenants configured');
+ }
+ return other;
+ }
+ if (misbehavior.kind === 'sign-with-tenant') {
+ const other = tenants.get(misbehavior.tenant);
+ if (!other) {
+ throw new Error(`sign-with-tenant misbehavior references unknown tenant ${misbehavior.tenant}`);
+ }
+ return other;
+ }
+ return tenant;
+}
+
+function buildAssertionFields(parsed: ParsedRequest, misbehavior: Misbehavior): AssertionFields {
+ return {
+ audience: misbehavior.kind === 'wrong-audience'
+ ? 'https://wrong.example/audience'
+ : parsed.issuer,
+ inResponseTo: misbehavior.kind === 'wrong-in-response-to'
+ ? `_mock_misbehave_${Math.random().toString(36).slice(2)}`
+ : parsed.requestId,
+ conditionsNotBefore: misbehavior.kind === 'not-yet-valid'
+ ? isoNow(60 * 60 * 1000) // +1 hour
+ : isoNow(-30 * 1000),
+ conditionsNotOnOrAfter: misbehavior.kind === 'expired'
+ ? isoNow(-60 * 1000) // expired 1 minute ago
+ : isoNow(ASSERTION_LIFETIME_MS),
+ };
+}
+
+async function renderLoginResponseXml(
+ signingTenant: TenantState,
+ issuerEntityId: string,
+ parsed: ParsedRequest,
+ user: { email: string, displayName: string },
+ fields: AssertionFields,
+ misbehavior: Misbehavior,
+): Promise {
+ // Build inline SP — derive from the AuthnRequest to avoid pre-registration.
+ const sp = samlify.ServiceProvider({
+ entityID: parsed.issuer,
+ assertionConsumerService: [{
+ Binding: samlify.Constants.namespace.binding.post,
+ Location: parsed.acsUrl,
+ }],
+ });
+
+ const result = await signingTenant.idp.createLoginResponse(
+ sp,
+ { extract: { request: { id: parsed.requestId } } } as any,
+ 'post',
+ user,
+ (template: string) => {
+ const id = `_mock_resp_${Math.random().toString(36).slice(2)}`;
+ const assertionId = `_mock_assert_${Math.random().toString(36).slice(2)}`;
+ const issueInstant = isoNow();
+
+ let context = template
+ .replace(/\{ID\}/g, id)
+ .replace(/\{AssertionID\}/g, assertionId)
+ .replace(/\{IssueInstant\}/g, issueInstant)
+ .replace(/\{Destination\}/g, parsed.acsUrl)
+ .replace(/\{Issuer\}/g, issuerEntityId)
+ .replace(/\{StatusCode\}/g, 'urn:oasis:names:tc:SAML:2.0:status:Success')
+ .replace(/\{NameID\}/g, user.email)
+ .replace(/\{NameIDFormat\}/g, 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')
+ .replace(/\{SubjectConfirmationDataNotOnOrAfter\}/g, isoNow(ASSERTION_LIFETIME_MS))
+ .replace(/\{SubjectRecipient\}/g, parsed.acsUrl)
+ .replace(/\{InResponseTo\}/g, fields.inResponseTo)
+ .replace(/\{ConditionsNotBefore\}/g, fields.conditionsNotBefore)
+ .replace(/\{ConditionsNotOnOrAfter\}/g, fields.conditionsNotOnOrAfter)
+ .replace(/\{Audience\}/g, fields.audience)
+ .replace(/\{Email\}/g, user.email)
+ .replace(/\{DisplayName\}/g, user.displayName);
+
+ if (misbehavior.kind === 'missing-name-id') {
+ context = context.replace(/]*>[^<]*<\/saml:NameID>/, '');
+ }
+ if (misbehavior.kind === 'missing-email') {
+ context = context.replace(
+ //,
+ '',
+ );
+ }
+ return { id, context };
+ },
+ );
+
+ return result.context;
+}
+
+function cacheReplayableResponse(tenant: TenantState, parsed: ParsedRequest, samlResponseB64: string): void {
+ tenant.lastResponse = {
+ samlResponseB64,
+ relayState: parsed.relayState,
+ acsUrl: parsed.acsUrl,
+ };
+}
+
+async function buildAssertion(
+ tenant: TenantState,
+ parsed: ParsedRequest,
+ user: { email: string, displayName: string },
+): Promise {
+ const misbehavior = consumeNextMisbehavior(tenant);
+
+ // True replay: re-emit the previous response *and* the previous RelayState
+ // so the entire POST body matches the cached one. (Returning fresh
+ // RelayState here would test "old response + new state", which is a
+ // different attack class than replay.)
+ if (misbehavior.kind === 'replay') {
+ if (!tenant.lastResponse) {
+ throw new Error('replay misbehavior requested but no previous response cached for this tenant');
+ }
+ return {
+ samlResponseB64: tenant.lastResponse.samlResponseB64,
+ acsUrl: tenant.lastResponse.acsUrl,
+ relayState: tenant.lastResponse.relayState,
+ };
+ }
+
+ const signingTenant = resolveSigningTenant(tenant, misbehavior);
+ const fields = buildAssertionFields(parsed, misbehavior);
+ const samlResponseB64 = await renderLoginResponseXml(
+ signingTenant,
+ tenant.entityId,
+ parsed,
+ user,
+ fields,
+ misbehavior,
+ );
+
+ // Only happy-path responses get cached for replay.
+ if (misbehavior.kind === 'none') {
+ cacheReplayableResponse(tenant, parsed, samlResponseB64);
+ }
+
+ return {
+ samlResponseB64,
+ acsUrl: parsed.acsUrl,
+ relayState: parsed.relayState,
+ };
+}
+
+// ---------- HTTP server --------------------------------------------------
+
+const app = express();
+app.use(express.urlencoded({ extended: false }));
+app.use(express.json());
+
+const loginFormSource = `
+Mock SAML IdP — {{tenant}}
+
+
Mock SAML IdP
+
Tenant: {{tenant}} · Request ID: {{requestId}}
+
+
No password — this is a test IdP. The submitted email becomes the NameID.
+
`;
+const loginForm = handlebars.compile(loginFormSource);
+
+const autoPostFormSource = `
+
+
+`;
+const autoPostForm = handlebars.compile(autoPostFormSource);
+
+function getTenant(req: express.Request, res: express.Response): TenantState | null {
+ const slug = req.params.tenant;
+ const t = tenants.get(slug);
+ if (!t) {
+ res.status(404).send(`Unknown tenant "${slug}". Configured: ${Array.from(tenants.keys()).join(", ")}`);
+ return null;
+ }
+ return t;
+}
+
+// Metadata
+app.get('/idp/:tenant/metadata', (req, res) => {
+ const t = getTenant(req, res);
+ if (!t) return;
+ res.type('application/xml').send(buildIdpMetadataXml(t));
+});
+
+// SSO endpoint — HTTP-Redirect binding (GET) shows the login form.
+app.get('/idp/:tenant/sso', async (req, res) => {
+ const t = getTenant(req, res);
+ if (!t) return;
+ const samlRequest = req.query.SAMLRequest;
+ const relayState = (req.query.RelayState as string | undefined) ?? '';
+ if (typeof samlRequest !== 'string') {
+ res.status(400).send('Missing SAMLRequest query parameter');
+ return;
+ }
+ try {
+ const parsed = await parseAuthnRequestRedirect(samlRequest, relayState);
+ res.send(loginForm({
+ tenant: t.slug,
+ requestId: parsed.requestId,
+ samlRequest,
+ relayState,
+ }));
+ } catch (err: any) {
+ res.status(400).send(`Mock IdP failed to parse AuthnRequest: ${err.message}`);
+ }
+});
+
+// Login form submission — builds the assertion and auto-POSTs to ACS.
+app.post('/idp/:tenant/login', async (req, res) => {
+ const t = getTenant(req, res);
+ if (!t) return;
+ const email = String(req.body.email ?? '').trim();
+ const displayName = String(req.body.displayName ?? email.split('@')[0] ?? 'Mock User').trim();
+ const samlRequest = String(req.body.SAMLRequest ?? '');
+ const relayState = String(req.body.RelayState ?? '');
+ if (!email || !samlRequest) {
+ res.status(400).send('Missing email or SAMLRequest');
+ return;
+ }
+ try {
+ const parsed = await parseAuthnRequestRedirect(samlRequest, relayState);
+ const { samlResponseB64, acsUrl, relayState: outRelayState } = await buildAssertion(t, parsed, { email, displayName });
+ res.send(autoPostForm({ acsUrl, samlResponse: samlResponseB64, relayState: outRelayState }));
+ } catch (err: any) {
+ res.status(500).send(`Mock IdP failed to build assertion: ${err.message}`);
+ }
+});
+
+// Test-controls — set the next assertion to misbehave in a specific way.
+// E2E tests call this BEFORE driving the login flow.
+app.post('/idp/:tenant/test-controls', (req, res) => {
+ const t = getTenant(req, res);
+ if (!t) return;
+ const body = req.body as { kind?: unknown };
+ if (typeof body.kind !== 'string') {
+ res.status(400).json({ error: 'body must be a Misbehavior object with `kind`' });
+ return;
+ }
+ t.nextMisbehavior = body as Misbehavior;
+ res.json({ ok: true, queued: body });
+});
+
+// Health + introspection
+app.get('/idp', (req, res) => {
+ res.json({
+ tenants: Array.from(tenants.values()).map(t => ({
+ slug: t.slug,
+ entityId: t.entityId,
+ metadataUrl: `http://localhost:${port}/idp/${t.slug}/metadata`,
+ ssoUrl: ssoUrlFor(t.slug),
+ nextMisbehavior: t.nextMisbehavior,
+ })),
+ });
+});
+
+app.listen(port, () => {
+ console.log(`Mock SAML IdP listening on http://localhost:${port}`);
+ console.log(` tenants: ${Array.from(tenants.keys()).join(", ")}`);
+ for (const t of Array.from(tenants.values())) {
+ console.log(` /idp/${t.slug}/metadata`);
+ }
+});
diff --git a/apps/mock-saml-idp/tsconfig.json b/apps/mock-saml-idp/tsconfig.json
new file mode 100644
index 0000000000..2d891a868c
--- /dev/null
+++ b/apps/mock-saml-idp/tsconfig.json
@@ -0,0 +1,33 @@
+{
+ "compilerOptions": {
+ "target": "es2022",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "strict": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "noErrorTruncation": true,
+ "paths": {
+ "@/*": [
+ "./src/*"
+ ]
+ },
+ "skipLibCheck": true
+ },
+ "include": [
+ "src/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/package.json b/package.json
index 7828d0154a..8a7c6120bd 100644
--- a/package.json
+++ b/package.json
@@ -54,15 +54,16 @@
"dev:tui": "pnpm pre && (trap 'kill 0' EXIT; pnpm run generate-sdks:watch & pnpm run generate-openapi-docs:watch & turbo run dev --ui tui --concurrency 99999 --filter=./apps/* --filter=@stackframe/stack-docs --filter=./packages/* --filter=./examples/demo)",
"dev:inspect": "pnpm pre && STACK_BACKEND_DEV_EXTRA_ARGS=\"--inspect\" pnpm run dev",
"dev:profile": "pnpm pre && STACK_BACKEND_DEV_EXTRA_ARGS=\"--experimental-cpu-prof\" pnpm run dev",
- "dev:basic": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/backend --filter=@stackframe/dashboard --filter=@stackframe/mock-oauth-server\"",
+ "dev:basic": "pnpm pre && concurrently -k \"pnpm run generate-sdks:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/backend --filter=@stackframe/dashboard --filter=@stackframe/mock-oauth-server --filter=@stackframe/mock-saml-idp\"",
"dev:docs": "pnpm pre && concurrently -k \"pnpm run generate-openapi-docs:watch\" \"turbo run dev --concurrency 99999 --filter=@stackframe/stack-docs\"",
"dev:named": "pnpm pre && concurrently -k \"pnpm run dev\" \"node -e \\\"process.title='node (stack-named-dev-server)'; process.stdin.resume();\\\"\"",
"kill-dev:named": "(pgrep -f 'stack-named-dev-server' | xargs -r -n1 pkill -P); echo 'Killed named dev server (if found). Sleeping to give some time for it to shut down...' && sleep 10",
- "kms": "PREFIX=${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}; for p in 00 01 02 03 04 06 14; do pids=$(lsof -i :$PREFIX$p 2>/dev/null | grep LISTEN | awk '$1 != \"OrbStack\" {print $2}' | sort -u); [ -n \"$pids\" ] && echo $pids | xargs kill -9 2>/dev/null; done; echo Done.",
+ "kms": "PREFIX=${NEXT_PUBLIC_STACK_PORT_PREFIX:-81}; for p in 00 01 02 03 04 06 14 42; do pids=$(lsof -i :$PREFIX$p 2>/dev/null | grep LISTEN | awk '$1 != \"OrbStack\" {print $2}' | sort -u); [ -n \"$pids\" ] && echo $pids | xargs kill -9 2>/dev/null; done; echo Done.",
"start": "pnpm pre && turbo run start --concurrency 99999",
"start:backend": "pnpm pre && turbo run start --concurrency 99999 --filter=@stackframe/backend",
"start:dashboard": "pnpm pre && turbo run start --concurrency 99999 --filter=@stackframe/dashboard",
"start:mock-oauth-server": "pnpm pre && turbo run start --concurrency 99999 --filter=@stackframe/mock-oauth-server",
+ "start:mock-saml-idp": "pnpm pre && turbo run start --concurrency 99999 --filter=@stackframe/mock-saml-idp",
"lint": "pnpm pre && turbo run lint --continue -- --max-warnings=0",
"release": "pnpm pre && release",
"dotenv": "dotenv",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 482352d2f2..3cef648e0a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -749,7 +749,7 @@ importers:
version: 1.166.6(crossws@0.4.4(srvx@0.8.16))
nitro:
specifier: ^3.0.0
- version: 3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2)
+ version: 3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.0-rc.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2)
react:
specifier: 19.2.1
version: 19.2.1
@@ -865,6 +865,31 @@ importers:
specifier: ^4.16.2
version: 4.16.2
+ apps/mock-saml-idp:
+ dependencies:
+ express:
+ specifier: ^4.21.2
+ version: 4.21.2
+ handlebars:
+ specifier: ^4.7.8
+ version: 4.7.8
+ node-forge:
+ specifier: ^1.3.1
+ version: 1.4.0
+ samlify:
+ specifier: ^2.10.0
+ version: 2.12.0
+ devDependencies:
+ '@types/express':
+ specifier: ^5.0.0
+ version: 5.0.0
+ '@types/node-forge':
+ specifier: ^1.3.11
+ version: 1.3.14
+ tsx:
+ specifier: ^4.16.2
+ version: 4.21.0
+
docs:
dependencies:
2027-track:
@@ -2712,6 +2737,10 @@ packages:
'@asyncapi/specs@6.8.1':
resolution: {integrity: sha512-czHoAk3PeXTLR+X8IUaD+IpT+g+zUvkcgMDJVothBsan+oHN3jfcFcFUNdOPAAFoUCQN1hXF1dWuphWy05THlA==}
+ '@authenio/xml-encryption@2.0.2':
+ resolution: {integrity: sha512-cTlrKttbrRHEw3W+0/I609A2Matj5JQaRvfLtEIGZvlN0RaPi+3ANsMeqAyCAVlH/lUIW2tmtBlSMni74lcXeg==}
+ engines: {node: '>=12'}
+
'@aws-crypto/crc32@5.2.0':
resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==}
engines: {node: '>=16.0.0'}
@@ -10492,6 +10521,9 @@ packages:
'@types/nlcst@2.0.3':
resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==}
+ '@types/node-forge@1.3.14':
+ resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
+
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
@@ -10928,6 +10960,14 @@ packages:
'@webgpu/types@0.1.66':
resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
+ '@xmldom/is-dom-node@1.0.1':
+ resolution: {integrity: sha512-CJDxIgE5I0FH+ttq/Fxy6nRpxP70+e2O048EPe85J2use3XKdatVM7dDVvFNjQudd9B49NPoZ+8PG49zj4Er8Q==}
+ engines: {node: '>= 16'}
+
+ '@xmldom/xmldom@0.8.13':
+ resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==}
+ engines: {node: '>=10.0.0'}
+
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
@@ -11206,6 +11246,9 @@ packages:
resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
engines: {node: '>= 0.4'}
+ asn1@0.2.6:
+ resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
+
asn1js@3.0.7:
resolution: {integrity: sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==}
engines: {node: '>=12.0.0'}
@@ -15847,6 +15890,10 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ node-forge@1.4.0:
+ resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==}
+ engines: {node: '>= 6.13.0'}
+
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
@@ -15860,6 +15907,9 @@ packages:
node-releases@2.0.27:
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+ node-rsa@1.1.1:
+ resolution: {integrity: sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw==}
+
nodemailer@6.9.13:
resolution: {integrity: sha512-7o38Yogx6krdoBf3jCAqnIN4oSQFx+fMa0I7dK1D+me9kBxx12D+/33wSb+fhOCtIxvYJ+4x4IMEhmhCKfAiOA==}
engines: {node: '>=6.0.0'}
@@ -17434,6 +17484,9 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
+ samlify@2.12.0:
+ resolution: {integrity: sha512-ewGsHyY4kInDH0BfprlAZ1rHpH1jBmbqYiXDbuI3t1Y8h71gqEt4Z7jdCFyPHFR8jItJkbdckTijUZGg14CDlg==}
+
sax@1.4.1:
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
@@ -19335,6 +19388,13 @@ packages:
resolution: {integrity: sha512-sqMMuL1rc0FmMBOzCpd0yuy9trqF2yTTVe+E9ogwCSWQCdDEtQUwrZPT6AxqtsFGRNxycgncbP/xmOOSPw5ZUw==}
engines: {node: '>= 6.0'}
+ xml-crypto@6.1.2:
+ resolution: {integrity: sha512-leBOVQdVi8FvPJrMYoum7Ici9qyxfE4kVi+AkpUoYCSXaQF4IlBm1cneTK9oAxR61LpYxTx7lNcsnBIeRpGW2w==}
+ engines: {node: '>=16'}
+
+ xml-escape@1.1.0:
+ resolution: {integrity: sha512-B/T4sDK8Z6aUh/qNr7mjKAwwncIljFuUP+DO/D5hloYFj+90O88z8Wf7oSucZTHxBAsC1/CTP4rtx/x1Uf72Mg==}
+
xml-js@1.6.11:
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
hasBin: true
@@ -19347,6 +19407,9 @@ packages:
resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==}
engines: {node: '>=4.0.0'}
+ xml@1.0.1:
+ resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
+
xmlbuilder2@4.0.3:
resolution: {integrity: sha512-bx8Q1STctnNaaDymWnkfQLKofs0mGNN7rLLapJlGuV3VlvegD7Ls4ggMjE3aUSWItCCzU0PEv45lI87iSigiCA==}
engines: {node: '>=20.0'}
@@ -19358,6 +19421,18 @@ packages:
xmlchars@2.2.0:
resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==}
+ xpath@0.0.32:
+ resolution: {integrity: sha512-rxMJhSIoiO8vXcWvSifKqhvV96GjiD5wYb8/QHdoRyQvraTpp4IEv944nhGausZZ3u7dhQXteZuZbaqfpB7uYw==}
+ engines: {node: '>=0.6.0'}
+
+ xpath@0.0.33:
+ resolution: {integrity: sha512-NNXnzrkDrAzalLhIUc01jO2mOzXGXh1JwPgkihcLLzw98c0WgYDmmjSh1Kl3wzaxSVWMuA+fe0WTWOBDWCBmNA==}
+ engines: {node: '>=0.6.0'}
+
+ xpath@0.0.34:
+ resolution: {integrity: sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==}
+ engines: {node: '>=0.6.0'}
+
xss@1.0.15:
resolution: {integrity: sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==}
engines: {node: '>= 0.10.0'}
@@ -19806,6 +19881,12 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
+ '@authenio/xml-encryption@2.0.2':
+ dependencies:
+ '@xmldom/xmldom': 0.8.13
+ escape-html: 1.0.3
+ xpath: 0.0.32
+
'@aws-crypto/crc32@5.2.0':
dependencies:
'@aws-crypto/util': 5.2.0
@@ -30230,6 +30311,10 @@ snapshots:
dependencies:
'@types/unist': 3.0.3
+ '@types/node-forge@1.3.14':
+ dependencies:
+ '@types/node': 22.19.0
+
'@types/node@12.20.55': {}
'@types/node@20.17.6':
@@ -30636,13 +30721,6 @@ snapshots:
optionalDependencies:
'@aws-sdk/credential-provider-web-identity': 3.972.27
- '@vercel/mcp-adapter@1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))':
- dependencies:
- '@modelcontextprotocol/sdk': 1.17.2
- mcp-handler: 1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))
- optionalDependencies:
- next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-
'@vercel/mcp-adapter@1.0.0(@modelcontextprotocol/sdk@1.17.2)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))':
dependencies:
'@modelcontextprotocol/sdk': 1.17.2
@@ -30831,6 +30909,10 @@ snapshots:
'@webgpu/types@0.1.66': {}
+ '@xmldom/is-dom-node@1.0.1': {}
+
+ '@xmldom/xmldom@0.8.13': {}
+
'@xstate/fsm@1.6.5': {}
'@xtuc/ieee754@1.2.0': {}
@@ -31141,6 +31223,10 @@ snapshots:
get-intrinsic: 1.3.0
is-array-buffer: 3.0.5
+ asn1@0.2.6:
+ dependencies:
+ safer-buffer: 2.1.2
+
asn1js@3.0.7:
dependencies:
pvtsutils: 1.3.6
@@ -33358,7 +33444,7 @@ snapshots:
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.1)
fast-glob: 3.3.3
- get-tsconfig: 4.8.1
+ get-tsconfig: 4.13.6
is-core-module: 2.15.1
is-glob: 4.0.3
transitivePeerDependencies:
@@ -33373,7 +33459,7 @@ snapshots:
debug: 4.4.3
enhanced-resolve: 5.17.1
eslint: 8.57.1
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
fast-glob: 3.3.3
get-tsconfig: 4.8.1
is-bun-module: 1.2.1
@@ -33416,7 +33502,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
+ eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1):
dependencies:
debug: 3.2.7
optionalDependencies:
@@ -33494,7 +33580,7 @@ snapshots:
doctrine: 2.1.0
eslint: 8.57.1
eslint-import-resolver-node: 0.3.9
- eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.56.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.1)
hasown: 2.0.2
is-core-module: 2.15.1
is-glob: 4.0.3
@@ -34181,7 +34267,7 @@ snapshots:
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
- statuses: 2.0.1
+ statuses: 2.0.2
transitivePeerDependencies:
- supports-color
@@ -36229,15 +36315,6 @@ snapshots:
math-intrinsics@1.1.0: {}
- mcp-handler@1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)):
- dependencies:
- '@modelcontextprotocol/sdk': 1.17.2
- chalk: 5.6.2
- commander: 11.1.0
- redis: 4.7.1
- optionalDependencies:
- next: 15.5.10(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
-
mcp-handler@1.0.1(@modelcontextprotocol/sdk@1.17.2)(next@16.1.7(@babel/core@7.26.0)(@opentelemetry/api@1.9.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)):
dependencies:
'@modelcontextprotocol/sdk': 1.17.2
@@ -37367,7 +37444,7 @@ snapshots:
jsonpath-plus: 10.4.0
lodash.topath: 4.5.2
- nitro@3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2):
+ nitro@3.0.0(@electric-sql/pglite@0.3.2)(chokidar@4.0.3)(lru-cache@11.2.2)(mysql2@3.15.3)(rolldown@1.0.0-rc.3)(vite@7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0))(xml2js@0.6.2):
dependencies:
consola: 3.4.2
cookie-es: 2.0.0
@@ -37387,6 +37464,7 @@ snapshots:
unenv: 2.0.0-rc.21
unstorage: 2.0.0-alpha.3(chokidar@4.0.3)(db0@0.3.4(@electric-sql/pglite@0.3.2)(mysql2@3.15.3))(lru-cache@11.2.2)(ofetch@1.5.1)
optionalDependencies:
+ rolldown: 1.0.0-rc.3
vite: 7.3.1(@types/node@22.19.0)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.21.0)(yaml@2.8.0)
xml2js: 0.6.2
transitivePeerDependencies:
@@ -37454,6 +37532,8 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
+ node-forge@1.4.0: {}
+
node-gyp-build@4.8.4: {}
node-releases@2.0.14: {}
@@ -37462,6 +37542,10 @@ snapshots:
node-releases@2.0.27: {}
+ node-rsa@1.1.1:
+ dependencies:
+ asn1: 0.2.6
+
nodemailer@6.9.13: {}
non-error@0.1.0: {}
@@ -39537,6 +39621,16 @@ snapshots:
safer-buffer@2.1.2: {}
+ samlify@2.12.0:
+ dependencies:
+ '@authenio/xml-encryption': 2.0.2
+ '@xmldom/xmldom': 0.8.13
+ node-rsa: 1.1.1
+ xml: 1.0.1
+ xml-crypto: 6.1.2
+ xml-escape: 1.1.0
+ xpath: 0.0.34
+
sax@1.4.1: {}
saxes@6.0.0:
@@ -39641,7 +39735,7 @@ snapshots:
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
- statuses: 2.0.1
+ statuses: 2.0.2
transitivePeerDependencies:
- supports-color
@@ -40861,7 +40955,7 @@ snapshots:
tsx@4.21.0:
dependencies:
esbuild: 0.27.1
- get-tsconfig: 4.8.1
+ get-tsconfig: 4.13.6
optionalDependencies:
fsevents: 2.3.3
@@ -41875,6 +41969,14 @@ snapshots:
dependencies:
os-paths: 4.4.0
+ xml-crypto@6.1.2:
+ dependencies:
+ '@xmldom/is-dom-node': 1.0.1
+ '@xmldom/xmldom': 0.8.13
+ xpath: 0.0.33
+
+ xml-escape@1.1.0: {}
+
xml-js@1.6.11:
dependencies:
sax: 1.4.1
@@ -41886,6 +41988,8 @@ snapshots:
sax: 1.4.1
xmlbuilder: 11.0.1
+ xml@1.0.1: {}
+
xmlbuilder2@4.0.3:
dependencies:
'@oozcitak/dom': 2.0.2
@@ -41897,6 +42001,12 @@ snapshots:
xmlchars@2.2.0: {}
+ xpath@0.0.32: {}
+
+ xpath@0.0.33: {}
+
+ xpath@0.0.34: {}
+
xss@1.0.15:
dependencies:
commander: 2.20.3