* feat(host-selfhost): Google sign-in with domain-allowlist admission * feat(host-selfhost): generic OIDC SSO sign-in with verified-domain admission * refactor(host-selfhost): extract SSO admission helpers, trim comments
Unified · Split
| @@ -0,0 +1,9 @@ | |||
| 1 | --- | ||
| 2 | "executor": patch | ||
| 3 | --- | ||
| 4 | |||
| 5 | **Self-host: bring-your-own SSO (Google, Okta, any OIDC IdP) with a verified-domain allowlist** | ||
| 6 | |||
| 7 | Operators can enable a single OIDC sign-in provider on a self-hosted instance by setting `EXECUTOR_SSO_PROVIDER_ID`, `EXECUTOR_SSO_CLIENT_ID`, `EXECUTOR_SSO_CLIENT_SECRET`, and `EXECUTOR_SSO_ALLOWED_DOMAINS` (comma-separated email domains), plus `EXECUTOR_SSO_DISCOVERY_URL` for providers without a preset (`google` is preset; `EXECUTOR_SSO_PROVIDER_NAME` overrides the button label). The login page renders a "Continue with <provider>" button when configured (discovered through the new unauthenticated `GET /api/auth-config`, which returns provider id + display name only), and the MCP OAuth connect flow's login step gains the same option since it lands on the same page. | ||
| 8 | |||
| 9 | The domain allowlist replaces the invite code for SSO sign-ups: a sign-in whose IdP-verified email (`email_verified`) has an allowlisted domain auto-joins the instance organization as a member; unverified emails and any other domain are refused. Enabling the provider without an allowlist is refused at boot, as is a half-configured client id/secret pair, so SSO can never silently become open registration. Email/password sign-in and invite-based signup are unchanged. The end-to-end flow (discovery → redirect → consent → callback → membership) is exercised in tests against an emulated OIDC IdP from `@executor-js/emulate`. | ||
| @@ -52,6 +52,7 @@ · 51 unmodified lines | |||
| 52 | }, | 52 | }, |
| 53 | "devDependencies": { | 53 | "devDependencies": { |
| 54 | "@effect/vitest": "catalog:", | 54 | "@effect/vitest": "catalog:", |
| 55 | "@executor-js/emulate": "^0.14.0", | ||
| 55 | "@executor-js/vite-plugin": "workspace:*", | 56 | "@executor-js/vite-plugin": "workspace:*", |
| 56 | "@tailwindcss/vite": "catalog:", | 57 | "@tailwindcss/vite": "catalog:", |
| 57 | "@tanstack/router-plugin": "^1.167.12", | 58 | "@tanstack/router-plugin": "^1.167.12", |
| @@ -1,6 +1,13 @@ | |||
| 1 | import { betterAuth, type BetterAuthOptions } from "better-auth"; | 1 | import { betterAuth, type BetterAuthOptions } from "better-auth"; |
| 2 | import { APIError } from "better-auth/api"; | 2 | import { APIError } from "better-auth/api"; |
| 3 | import { admin, bearer, deviceAuthorization, mcp, organization } from "better-auth/plugins"; | 3 | import { |
| 4 | admin, | ||
| 5 | bearer, | ||
| 6 | deviceAuthorization, | ||
| 7 | genericOAuth, | ||
| 8 | mcp, | ||
| 9 | organization, | ||
| 10 | } from "better-auth/plugins"; | ||
| 4 | import { apiKey } from "@better-auth/api-key"; | 11 | import { apiKey } from "@better-auth/api-key"; |
| 5 | import { type Client } from "@libsql/client"; | 12 | import { type Client } from "@libsql/client"; |
| 6 | import { LibsqlDialect, type LibsqlDialectConfig } from "@libsql/kysely-libsql"; | 13 | import { LibsqlDialect, type LibsqlDialectConfig } from "@libsql/kysely-libsql"; |
| @@ -9,6 +16,7 @@ import { Context } from "effect"; · 2 unmodified lines | |||
| 9 | import { loadConfig } from "../config"; | 16 | import { loadConfig } from "../config"; |
| 10 | import { seedOrgAndAdmin } from "./seed"; | 17 | import { seedOrgAndAdmin } from "./seed"; |
| 11 | import { consumeInviteCode, ensureInviteCodeTable, findRedeemableCode } from "./invites"; | 18 | import { consumeInviteCode, ensureInviteCodeTable, findRedeemableCode } from "./invites"; |
| 19 | import { isAdmitted, isOAuthCallback, ssoProviderConfig } from "./sso"; | ||
| 12 | 20 | ||
| 13 | // The self-service signup gate: present only on the live (phase-2) auth | 21 | // The self-service signup gate: present only on the live (phase-2) auth |
| 14 | // instance, so the bootstrap seed's `createUser` — which | 22 | // instance, so the bootstrap seed's `createUser` — which |
| @@ -165,6 +173,14 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 150 unmodified lines | |||
| 165 | // is the page the user opens to confirm the code — the self-host app serves | 173 | // is the page the user opens to confirm the code — the self-host app serves |
| 166 | // it at /device (this is also the Better Auth default; pinned for clarity). | 174 | // it at /device (this is also the Better Auth default; pinned for clarity). |
| 167 | deviceAuthorization({ verificationUri: "/device" }), | 175 | deviceAuthorization({ verificationUri: "/device" }), |
| 176 | // The operator-configured SSO provider (see config.ts), spoken over plain | ||
| 177 | // OIDC discovery so Google, Okta, Entra, or any compliant IdP slots in — | ||
| 178 | // and so tests can point it at an emulated IdP. The domain gate below is | ||
| 179 | // what admits or refuses the users this creates; enabling the provider | ||
| 180 | // alone never opens registration. Always in the plugin tuple (an empty | ||
| 181 | // provider list serves no routes that match) so the inferred `auth.api` | ||
| 182 | // shape doesn't depend on the environment. | ||
| 183 | genericOAuth({ config: config.sso ? [ssoProviderConfig(config.sso)] : [] }), | ||
| 168 | // `consentPage` makes the MCP authorize flow redirect to a human approval | 184 | // `consentPage` makes the MCP authorize flow redirect to a human approval |
| 169 | // screen instead of auto-issuing a code — but ONLY when the request | 185 | // screen instead of auto-issuing a code — but ONLY when the request |
| 170 | // carries `prompt=consent`. MCP clients don't send that, so the self-host | 186 | // carries `prompt=consent`. MCP clients don't send that, so the self-host |
| @@ -199,8 +215,26 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 28 unmodified lines | |||
| 199 | ? { | 215 | ? { |
| 200 | user: { | 216 | user: { |
| 201 | create: { | 217 | create: { |
| 202 | before: async (_user, context) => { | 218 | before: async (user, context) => { |
| 203 | if (context?.path !== SIGNUP_PATH) return; | 219 | if (context?.path !== SIGNUP_PATH) { |
| 220 | // SSO sign-ups arrive on an OAuth callback path; the | ||
| 221 | // verified-domain allowlist gates them in place of an | ||
| 222 | // invite code. Server-side creation (the seed, admin | ||
| 223 | // add-user) passes. | ||
| 224 | const sso = config.sso; | ||
| 225 | if ( | ||
| 226 | isOAuthCallback(context?.path) && | ||
| 227 | !(sso !== undefined && isAdmitted(sso, user)) | ||
| 228 | ) { | ||
| 229 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: a Better Auth create hook rejects a request by throwing APIError | ||
| 230 | throw new APIError("FORBIDDEN", { | ||
| 231 | message: sso | ||
| 232 | ? `Sign-ups are restricted to verified ${sso.allowedDomains.map((d) => `@${d}`).join(", ")} accounts.` | ||
| 233 | : "SSO sign-up is not enabled on this instance.", | ||
| 234 | }); | ||
| 235 | } | ||
| 236 | return; | ||
| 237 | } | ||
| 204 | if (await orgHasNoMembers(gate)) return; // first user claims the org | 238 | if (await orgHasNoMembers(gate)) return; // first user claims the org |
| 205 | const code = inviteCodeFrom(context); | 239 | const code = inviteCodeFrom(context); |
| 206 | if (!code) { | 240 | if (!code) { |
| @@ -217,9 +251,30 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 10 unmodified lines | |||
| 217 | } | 251 | } |
| 218 | }, | 252 | }, |
| 219 | after: async (user, context) => { | 253 | after: async (user, context) => { |
| 220 | if (context?.path !== SIGNUP_PATH) return; | ||
| 221 | const auth = gate.getAuth(); | 254 | const auth = gate.getAuth(); |
| 222 | if (!auth) return; | 255 | if (!auth) return; |
| 256 | if (context?.path !== SIGNUP_PATH) { | ||
| 257 | // An SSO user that reached `after` was admitted by | ||
| 258 | // `before`; joining the instance org as a member is what an | ||
| 259 | // invite redemption would have done. Server-side creation | ||
| 260 | // (no callback path) is left alone — the seed manages its | ||
| 261 | // own membership. | ||
| 262 | const sso = config.sso; | ||
| 263 | if ( | ||
| 264 | isOAuthCallback(context?.path) && | ||
| 265 | sso !== undefined && | ||
| 266 | isAdmitted(sso, user) | ||
| 267 | ) { | ||
| 268 | await auth.api.addMember({ | ||
| 269 | body: { | ||
| 270 | userId: user.id, | ||
| 271 | role: "member", | ||
| 272 | organizationId: gate.organizationId, | ||
| 273 | }, | ||
| 274 | }); | ||
| 275 | } | ||
| 276 | return; | ||
| 277 | } | ||
| 223 | // First user into an empty org becomes its owner (no code). | 278 | // First user into an empty org becomes its owner (no code). |
| 224 | if (await orgHasNoMembers(gate)) { | 279 | if (await orgHasNoMembers(gate)) { |
| 225 | await auth.api.addMember({ | 280 | await auth.api.addMember({ |
| @@ -0,0 +1,313 @@ | |||
| 1 | import { mkdtempSync } from "node:fs"; | ||
| 2 | import { tmpdir } from "node:os"; | ||
| 3 | import { join } from "node:path"; | ||
| 4 | |||
| 5 | import { afterAll, describe, expect, it, test } from "@effect/vitest"; | ||
| 6 | import { createEmulator } from "@executor-js/emulate"; | ||
| 7 | |||
| 8 | import { mintInviteCode } from "../testing/mint-invite"; | ||
| 9 | |||
| 10 | // Real Better Auth path with an SSO provider configured — and a REAL IdP on the | ||
| 11 | // wire: an @executor-js/emulate Okta instance serving OIDC discovery, | ||
| 12 | // authorize, and token, so the round-trip tests below exercise the same | ||
| 13 | // discovery -> redirect -> consent -> callback flow a production Google/Okta | ||
| 14 | // deployment does. Emulator + registered client + env must all exist before | ||
| 15 | // the app import, so `loadConfig` sees a fully-configured instance at boot. | ||
| 16 | const BASE = "http://localhost:4788"; | ||
| 17 | const CALLBACK = `${BASE}/api/auth/oauth2/callback/okta`; | ||
| 18 | |||
| 19 | const idp = await createEmulator({ service: "okta", port: 4790 }); | ||
| 20 | afterAll(() => idp.close()); | ||
| 21 | |||
| 22 | const registration = await fetch(`${idp.url}/oauth2/v1/clients`, { | ||
| 23 | method: "POST", | ||
| 24 | headers: { "content-type": "application/json" }, | ||
| 25 | body: JSON.stringify({ | ||
| 26 | client_name: "executor-selfhost-test", | ||
| 27 | redirect_uris: [CALLBACK], | ||
| 28 | grant_types: ["authorization_code"], | ||
| 29 | response_types: ["code"], | ||
| 30 | token_endpoint_auth_method: "client_secret_post", | ||
| 31 | }), | ||
| 32 | }).then((r) => r.json() as Promise<{ client_id: string; client_secret: string }>); | ||
| 33 | |||
| 34 | const createIdpUser = (email: string, firstName: string) => | ||
| 35 | fetch(`${idp.url}/api/v1/users`, { | ||
| 36 | method: "POST", | ||
| 37 | headers: { "content-type": "application/json", authorization: "SSWS test" }, | ||
| 38 | body: JSON.stringify({ profile: { login: email, email, firstName, lastName: "User" } }), | ||
| 39 | }); | ||
| 40 | await createIdpUser("alice@example.com", "Alice"); | ||
| 41 | await createIdpUser("mallory@evil.test", "Mallory"); | ||
| 42 | |||
| 43 | process.env.EXECUTOR_DATA_DIR = mkdtempSync(join(tmpdir(), "eh-sso-")); | ||
| 44 | process.env.BETTER_AUTH_SECRET = "test-secret-0123456789-abcdefghijklmnop-qrstuv"; | ||
| 45 | process.env.EXECUTOR_BOOTSTRAP_ADMIN_EMAIL = "admin@test.local"; | ||
| 46 | process.env.EXECUTOR_BOOTSTRAP_ADMIN_PASSWORD = "admin-password-123"; | ||
| 47 | process.env.EXECUTOR_SSO_PROVIDER_ID = "okta"; | ||
| 48 | process.env.EXECUTOR_SSO_DISCOVERY_URL = `${idp.url}/.well-known/openid-configuration`; | ||
| 49 | process.env.EXECUTOR_SSO_CLIENT_ID = registration.client_id; | ||
| 50 | process.env.EXECUTOR_SSO_CLIENT_SECRET = registration.client_secret; | ||
| 51 | process.env.EXECUTOR_SSO_ALLOWED_DOMAINS = "Example.com, @second.example ,"; | ||
| 52 | |||
| 53 | const { loadConfig } = await import("../config"); | ||
| 54 | const { emailDomain } = await import("./sso"); | ||
| 55 | const { makeSelfHostApiHandler } = await import("../app"); | ||
| 56 | |||
| 57 | const { handler, dispose } = await makeSelfHostApiHandler(); | ||
| 58 | afterAll(() => dispose()); | ||
| 59 | |||
| 60 | const SSO_ENV_KEYS = [ | ||
| 61 | "EXECUTOR_SSO_PROVIDER_ID", | ||
| 62 | "EXECUTOR_SSO_PROVIDER_NAME", | ||
| 63 | "EXECUTOR_SSO_DISCOVERY_URL", | ||
| 64 | "EXECUTOR_SSO_CLIENT_ID", | ||
| 65 | "EXECUTOR_SSO_CLIENT_SECRET", | ||
| 66 | "EXECUTOR_SSO_ALLOWED_DOMAINS", | ||
| 67 | ] as const; | ||
| 68 | |||
| 69 | // Run a block with the SSO env vars swapped out, restoring them afterwards so | ||
| 70 | // the booted instance's request-time config reads stay consistent. | ||
| 71 | const withSsoEnv = <T>(overrides: Record<string, string | undefined>, run: () => T): T => { | ||
| 72 | const saved = Object.fromEntries(SSO_ENV_KEYS.map((k) => [k, process.env[k]])); | ||
| 73 | for (const key of SSO_ENV_KEYS) { | ||
| 74 | const value = overrides[key]; | ||
| 75 | if (value === undefined) delete process.env[key]; | ||
| 76 | else process.env[key] = value; | ||
| 77 | } | ||
| 78 | // oxlint-disable-next-line executor/no-try-catch-or-throw -- boundary: env save/restore around config reads must restore on assertion failure | ||
| 79 | try { | ||
| 80 | return run(); | ||
| 81 | } finally { | ||
| 82 | for (const key of SSO_ENV_KEYS) { | ||
| 83 | const value = saved[key]; | ||
| 84 | if (value === undefined) delete process.env[key]; | ||
| 85 | else process.env[key] = value; | ||
| 86 | } | ||
| 87 | } | ||
| 88 | }; | ||
| 89 | |||
| 90 | describe("sso config resolution", () => { | ||
| 91 | it("normalizes the domain allowlist (trim, lowercase, strip @, drop empties)", () => { | ||
| 92 | const sso = loadConfig().sso; | ||
| 93 | expect(sso).toBeDefined(); | ||
| 94 | expect(sso!.allowedDomains).toEqual(["example.com", "second.example"]); | ||
| 95 | }); | ||
| 96 | |||
| 97 | it("derives the display name from the provider id when not set", () => { | ||
| 98 | expect(loadConfig().sso!.providerName).toBe("Okta"); | ||
| 99 | }); | ||
| 100 | |||
| 101 | it("is undefined when no SSO env is set", () => { | ||
| 102 | withSsoEnv({}, () => { | ||
| 103 | expect(loadConfig().sso).toBeUndefined(); | ||
| 104 | }); | ||
| 105 | }); | ||
| 106 | |||
| 107 | it("refuses half-configured credentials", () => { | ||
| 108 | withSsoEnv({ EXECUTOR_SSO_CLIENT_ID: "id-only" }, () => { | ||
| 109 | expect(() => loadConfig()).toThrow(/must be set together/); | ||
| 110 | }); | ||
| 111 | }); | ||
| 112 | |||
| 113 | it("refuses credentials without a provider id", () => { | ||
| 114 | withSsoEnv({ EXECUTOR_SSO_CLIENT_ID: "id", EXECUTOR_SSO_CLIENT_SECRET: "secret" }, () => { | ||
| 115 | expect(() => loadConfig()).toThrow(/EXECUTOR_SSO_PROVIDER_ID/); | ||
| 116 | }); | ||
| 117 | }); | ||
| 118 | |||
| 119 | it("presets Google's discovery document so only credentials + domains are needed", () => { | ||
| 120 | withSsoEnv( | ||
| 121 | { | ||
| 122 | EXECUTOR_SSO_PROVIDER_ID: "google", | ||
| 123 | EXECUTOR_SSO_CLIENT_ID: "id.apps.googleusercontent.com", | ||
| 124 | EXECUTOR_SSO_CLIENT_SECRET: "secret", | ||
| 125 | EXECUTOR_SSO_ALLOWED_DOMAINS: "example.com", | ||
| 126 | }, | ||
| 127 | () => { | ||
| 128 | const sso = loadConfig().sso; | ||
| 129 | expect(sso!.discoveryUrl).toBe( | ||
| 130 | "https://accounts.google.com/.well-known/openid-configuration", | ||
| 131 | ); | ||
| 132 | expect(sso!.providerName).toBe("Google"); | ||
| 133 | }, | ||
| 134 | ); | ||
| 135 | }); | ||
| 136 | |||
| 137 | it("refuses a provider without a known or explicit discovery URL", () => { | ||
| 138 | withSsoEnv( | ||
| 139 | { | ||
| 140 | EXECUTOR_SSO_PROVIDER_ID: "okta", | ||
| 141 | EXECUTOR_SSO_CLIENT_ID: "id", | ||
| 142 | EXECUTOR_SSO_CLIENT_SECRET: "secret", | ||
| 143 | EXECUTOR_SSO_ALLOWED_DOMAINS: "example.com", | ||
| 144 | }, | ||
| 145 | () => { | ||
| 146 | expect(() => loadConfig()).toThrow(/EXECUTOR_SSO_DISCOVERY_URL/); | ||
| 147 | }, | ||
| 148 | ); | ||
| 149 | }); | ||
| 150 | |||
| 151 | it("refuses a configured provider without a domain allowlist", () => { | ||
| 152 | withSsoEnv( | ||
| 153 | { | ||
| 154 | EXECUTOR_SSO_PROVIDER_ID: "okta", | ||
| 155 | EXECUTOR_SSO_DISCOVERY_URL: "https://idp.example/.well-known/openid-configuration", | ||
| 156 | EXECUTOR_SSO_CLIENT_ID: "id", | ||
| 157 | EXECUTOR_SSO_CLIENT_SECRET: "secret", | ||
| 158 | }, | ||
| 159 | () => { | ||
| 160 | expect(() => loadConfig()).toThrow(/EXECUTOR_SSO_ALLOWED_DOMAINS/); | ||
| 161 | }, | ||
| 162 | ); | ||
| 163 | }); | ||
| 164 | }); | ||
| 165 | |||
| 166 | describe("emailDomain", () => { | ||
| 167 | it("extracts the lowercased domain of a well-formed address", () => { | ||
| 168 | expect(emailDomain("User@Example.COM")).toBe("example.com"); | ||
| 169 | }); | ||
| 170 | |||
| 171 | it("returns null for malformed addresses instead of a matchable domain", () => { | ||
| 172 | expect(emailDomain("@example.com")).toBeNull(); | ||
| 173 | expect(emailDomain("user@")).toBeNull(); | ||
| 174 | expect(emailDomain("no-at-sign")).toBeNull(); | ||
| 175 | }); | ||
| 176 | }); | ||
| 177 | |||
| 178 | test("auth-config advertises the configured provider (id + name only)", async () => { | ||
| 179 | const res = await handler(new Request(`${BASE}/api/auth-config`)); | ||
| 180 | expect(res.status).toBe(200); | ||
| 181 | expect(await res.json()).toEqual({ ssoProviders: [{ id: "okta", name: "Okta" }] }); | ||
| 182 | }); | ||
| 183 | |||
| 184 | // --- The full OIDC round-trip against the emulated IdP ----------------------- | ||
| 185 | |||
| 186 | const decodeHtml = (value: string): string => | ||
| 187 | value | ||
| 188 | .replaceAll("&", "&") | ||
| 189 | .replaceAll(""", '"') | ||
| 190 | .replaceAll("'", "'") | ||
| 191 | .replaceAll("<", "<") | ||
| 192 | .replaceAll(">", ">"); | ||
| 193 | |||
| 194 | const formFields = (form: string): Record<string, string> => { | ||
| 195 | const fields: Record<string, string> = {}; | ||
| 196 | for (const input of form.matchAll(/<input\b[^>]*>/gi)) { | ||
| 197 | const tag = input[0]; | ||
| 198 | const name = tag.match(/\bname=["']([^"']+)["']/i)?.[1]; | ||
| 199 | const value = tag.match(/\bvalue=["']([^"']*)["']/i)?.[1] ?? ""; | ||
| 200 | if (name) fields[decodeHtml(name)] = decodeHtml(value); | ||
| 201 | } | ||
| 202 | return fields; | ||
| 203 | }; | ||
| 204 | |||
| 205 | // Drive the whole flow as the given IdP account: sign-in redirect -> IdP | ||
| 206 | // consent page (pick the account's form) -> callback back into the app. | ||
| 207 | // Returns the callback response plus the cookies it set. | ||
| 208 | const signInThroughIdp = async (email: string) => { | ||
| 209 | const start = await handler( | ||
| 210 | new Request(`${BASE}/api/auth/sign-in/oauth2`, { | ||
| 211 | method: "POST", | ||
| 212 | headers: { "content-type": "application/json" }, | ||
| 213 | body: JSON.stringify({ providerId: "okta", callbackURL: "/" }), | ||
| 214 | }), | ||
| 215 | ); | ||
| 216 | expect(start.status).toBe(200); | ||
| 217 | const { url } = (await start.json()) as { url: string }; | ||
| 218 | expect(url.startsWith(idp.url)).toBe(true); | ||
| 219 | const stateCookies = start.headers | ||
| 220 | .getSetCookie() | ||
| 221 | .map((cookie) => cookie.split(";")[0]!) | ||
| 222 | .join("; "); | ||
| 223 | |||
| 224 | const consentHtml = await fetch(url).then((r) => r.text()); | ||
| 225 | const form = [...consentHtml.matchAll(/<form[\s\S]*?<\/form>/gi)] | ||
| 226 | .map((m) => m[0]) | ||
| 227 | .find((f) => f.includes(email)); | ||
| 228 | expect(form).toBeDefined(); | ||
| 229 | const action = decodeHtml(form!.match(/\baction=["']([^"']+)["']/i)![1]!); | ||
| 230 | const consent = await fetch(action, { | ||
| 231 | method: "POST", | ||
| 232 | headers: { "content-type": "application/x-www-form-urlencoded" }, | ||
| 233 | body: new URLSearchParams(formFields(form!)), | ||
| 234 | redirect: "manual", | ||
| 235 | }); | ||
| 236 | expect(consent.status).toBe(302); | ||
| 237 | const callbackUrl = consent.headers.get("location")!; | ||
| 238 | expect(callbackUrl.startsWith(CALLBACK)).toBe(true); | ||
| 239 | |||
| 240 | const callback = await handler(new Request(callbackUrl, { headers: { cookie: stateCookies } })); | ||
| 241 | const cookies = callback.headers | ||
| 242 | .getSetCookie() | ||
| 243 | .map((cookie) => cookie.split(";")[0]!) | ||
| 244 | .join("; "); | ||
| 245 | return { callback, cookies }; | ||
| 246 | }; | ||
| 247 | |||
| 248 | test("an allowlisted-domain IdP account signs in and joins the org as a member", async () => { | ||
| 249 | const { callback, cookies } = await signInThroughIdp("alice@example.com"); | ||
| 250 | expect(callback.status).toBe(302); | ||
| 251 | expect(callback.headers.get("location")).toBe("/"); | ||
| 252 | |||
| 253 | const session = await handler( | ||
| 254 | new Request(`${BASE}/api/auth/get-session`, { headers: { cookie: cookies } }), | ||
| 255 | ); | ||
| 256 | expect(session.status).toBe(200); | ||
| 257 | const sessionBody = (await session.json()) as { user?: { email?: string } }; | ||
| 258 | expect(sessionBody.user?.email).toBe("alice@example.com"); | ||
| 259 | |||
| 260 | const orgs = await handler( | ||
| 261 | new Request(`${BASE}/api/auth/organization/list`, { headers: { cookie: cookies } }), | ||
| 262 | ); | ||
| 263 | expect(orgs.status).toBe(200); | ||
| 264 | expect(((await orgs.json()) as unknown[]).length).toBe(1); | ||
| 265 | }); | ||
| 266 | |||
| 267 | test("an IdP account outside the allowlist is refused and gets no session", async () => { | ||
| 268 | const { callback, cookies } = await signInThroughIdp("mallory@evil.test"); | ||
| 269 | // The rejection surfaces as better-auth's error redirect, not a success. | ||
| 270 | expect(callback.status).toBe(302); | ||
| 271 | expect(callback.headers.get("location")).not.toBe("/"); | ||
| 272 | expect(callback.headers.get("location")).toContain("error"); | ||
| 273 | |||
| 274 | const session = await handler( | ||
| 275 | new Request(`${BASE}/api/auth/get-session`, { headers: { cookie: cookies } }), | ||
| 276 | ); | ||
| 277 | const sessionBody = (await session.json()) as { user?: unknown } | null; | ||
| 278 | expect(sessionBody?.user ?? null).toBeNull(); | ||
| 279 | }); | ||
| 280 | |||
| 281 | // --- Invite-code regressions with SSO configured ------------------------------ | ||
| 282 | |||
| 283 | test("invite-gated email signup still works with an SSO provider configured", async () => { | ||
| 284 | const inviteCode = await mintInviteCode(handler); | ||
| 285 | const signUp = await handler( | ||
| 286 | new Request(`${BASE}/api/auth/sign-up/email`, { | ||
| 287 | method: "POST", | ||
| 288 | headers: { "content-type": "application/json" }, | ||
| 289 | body: JSON.stringify({ | ||
| 290 | email: "member@test.local", | ||
| 291 | password: "member-password-123", | ||
| 292 | name: "Member", | ||
| 293 | inviteCode, | ||
| 294 | }), | ||
| 295 | }), | ||
| 296 | ); | ||
| 297 | expect(signUp.status).toBe(200); | ||
| 298 | }); | ||
| 299 | |||
| 300 | test("email signup without an invite is still refused with an SSO provider configured", async () => { | ||
| 301 | const signUp = await handler( | ||
| 302 | new Request(`${BASE}/api/auth/sign-up/email`, { | ||
| 303 | method: "POST", | ||
| 304 | headers: { "content-type": "application/json" }, | ||
| 305 | body: JSON.stringify({ | ||
| 306 | email: "stranger@example.com", | ||
| 307 | password: "stranger-password-123", | ||
| 308 | name: "Stranger", | ||
| 309 | }), | ||
| 310 | }), | ||
| 311 | ); | ||
| 312 | expect(signUp.status).toBe(403); | ||
| 313 | }); | ||
| @@ -0,0 +1,46 @@ | |||
| 1 | import { type SsoConfig } from "../config"; | ||
| 2 | |||
| 3 | // Better Auth serves OAuth sign-in callbacks at `/oauth2/callback/:providerId` | ||
| 4 | // (genericOAuth) and `/callback/:providerId` (built-in social providers) — the | ||
| 5 | // only paths an IdP-initiated user creation arrives on, so this splits "a | ||
| 6 | // stranger signed in at the IdP" from server-side creation (the seed, admin | ||
| 7 | // add-user), which never carries either. | ||
| 8 | export const isOAuthCallback = (path: string | undefined): boolean => | ||
| 9 | path?.startsWith("/oauth2/callback/") === true || path?.startsWith("/callback/") === true; | ||
| 10 | |||
| 11 | // The domain of a well-formed address, or null — so a malformed email can never | ||
| 12 | // match an allowlist entry (`emailDomain("@example.com")` is null, not "example.com"). | ||
| 13 | export const emailDomain = (email: string): string | null => { | ||
| 14 | const at = email.lastIndexOf("@"); | ||
| 15 | if (at <= 0 || at === email.length - 1) return null; | ||
| 16 | return email.slice(at + 1).toLowerCase(); | ||
| 17 | }; | ||
| 18 | |||
| 19 | // Admission = the IdP vouches for the address (`email_verified`, mapped to | ||
| 20 | // `emailVerified` by the genericOAuth callback) AND its domain is allowlisted. | ||
| 21 | // Without the verified check, anyone could register an IdP account with a | ||
| 22 | // made-up allowlisted address and walk in. | ||
| 23 | export const isAdmitted = ( | ||
| 24 | sso: SsoConfig, | ||
| 25 | user: { email: string; emailVerified: boolean }, | ||
| 26 | ): boolean => { | ||
| 27 | if (!user.emailVerified) return false; | ||
| 28 | const domain = emailDomain(user.email); | ||
| 29 | return domain !== null && sso.allowedDomains.includes(domain); | ||
| 30 | }; | ||
| 31 | |||
| 32 | // The genericOAuth registration for the configured provider, derived from its | ||
| 33 | // OIDC discovery document. For Google with a single allowed domain, `hd` | ||
| 34 | // pre-filters the account chooser — a UX hint only (Google treats it as | ||
| 35 | // advisory); the create-hook gate is the enforcement. | ||
| 36 | export const ssoProviderConfig = (sso: SsoConfig) => ({ | ||
| 37 | providerId: sso.providerId, | ||
| 38 | clientId: sso.clientId, | ||
| 39 | clientSecret: sso.clientSecret, | ||
| 40 | discoveryUrl: sso.discoveryUrl, | ||
| 41 | scopes: ["openid", "email", "profile"], | ||
| 42 | pkce: true, | ||
| 43 | ...(sso.providerId === "google" && sso.allowedDomains.length === 1 | ||
| 44 | ? { authorizationUrlParams: { hd: sso.allowedDomains[0]! } } | ||
| 45 | : {}), | ||
| 46 | }); | ||
| @@ -18,6 +18,26 @@ import { · 17 unmodified lines | |||
| 18 | export const SELF_HOST_NAMESPACE = "executor_selfhost"; | 18 | export const SELF_HOST_NAMESPACE = "executor_selfhost"; |
| 19 | export const SELF_HOST_SCHEMA_VERSION = "1.0.0"; | 19 | export const SELF_HOST_SCHEMA_VERSION = "1.0.0"; |
| 20 | 20 | ||
| 21 | /** | ||
| 22 | * SSO sign-in for the self-host login page and the MCP OAuth connect flow: one | ||
| 23 | * OIDC provider (Google, Okta, Entra, any discovery-compliant IdP) resolved | ||
| 24 | * from the environment. Present only when the operator configured it; the | ||
| 25 | * allowlist is what replaces the invite code for SSO sign-ups (the domain IS | ||
| 26 | * the invite), so it is required whenever the provider is enabled. | ||
| 27 | */ | ||
| 28 | export interface SsoConfig { | ||
| 29 | /** URL-safe id — also the OAuth callback path segment and the button key. */ | ||
| 30 | readonly providerId: string; | ||
| 31 | /** Display name for the login button (“Continue with <name>”). */ | ||
| 32 | readonly providerName: string; | ||
| 33 | /** The IdP's OIDC discovery document (…/.well-known/openid-configuration). */ | ||
| 34 | readonly discoveryUrl: string; | ||
| 35 | readonly clientId: string; | ||
| 36 | readonly clientSecret: string; | ||
| 37 | /** Lowercased email domains admitted without an invite code. */ | ||
| 38 | readonly allowedDomains: readonly string[]; | ||
| 39 | } | ||
| 40 | |||
| 21 | export interface SelfHostConfig { | 41 | export interface SelfHostConfig { |
| 22 | /** Bind address. Defaults to loopback. */ | 42 | /** Bind address. Defaults to loopback. */ |
| 23 | readonly host: string; | 43 | readonly host: string; |
| @@ -53,6 +73,8 @@ export interface SelfHostConfig { · 29 unmodified lines | |||
| 53 | * minutes (the same pattern as MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS on cloud). | 73 | * minutes (the same pattern as MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS on cloud). |
| 54 | */ | 74 | */ |
| 55 | readonly sandboxTimeoutMs: number | undefined; | 75 | readonly sandboxTimeoutMs: number | undefined; |
| 76 | /** SSO sign-in, or undefined when the operator hasn't configured it. */ | ||
| 77 | readonly sso: SsoConfig | undefined; | ||
| 56 | /** | 78 | /** |
| 57 | * How long an MCP session may sit idle before the in-process store evicts it, | 79 | * How long an MCP session may sit idle before the in-process store evicts it, |
| 58 | * or undefined for the store's own default (30 minutes). 0 disables eviction. | 80 | * or undefined for the store's own default (30 minutes). 0 disables eviction. |
| @@ -172,11 +194,68 @@ export const loadConfig = (): SelfHostConfig => { · 113 unmodified lines | |||
| 172 | organizationName: process.env.EXECUTOR_ORG_NAME ?? "Default", | 194 | organizationName: process.env.EXECUTOR_ORG_NAME ?? "Default", |
| 173 | orgSlug: resolveOrgSlug(), | 195 | orgSlug: resolveOrgSlug(), |
| 174 | sandboxTimeoutMs: resolveSandboxTimeoutMs(), | 196 | sandboxTimeoutMs: resolveSandboxTimeoutMs(), |
| 197 | sso: resolveSso(), | ||
| 175 | mcpSessionIdleTtlMs: resolveMcpSessionIdleTtlMs(), | 198 | mcpSessionIdleTtlMs: resolveMcpSessionIdleTtlMs(), |
| 176 | toolsSyncTtlMs: resolveToolsSyncTtlMs(), | 199 | toolsSyncTtlMs: resolveToolsSyncTtlMs(), |
| 177 | }; | 200 | }; |
| 178 | }; | 201 | }; |
| 179 | 202 | ||
| 203 | // Well-known discovery documents for providers an operator can name without | ||
| 204 | // hunting down the URL. Anything else (Okta, Entra, Auth0, …) has a | ||
| 205 | // tenant-specific issuer, so EXECUTOR_SSO_DISCOVERY_URL is required for it. | ||
| 206 | const DISCOVERY_PRESETS: Record<string, string> = { | ||
| 207 | google: "https://accounts.google.com/.well-known/openid-configuration", | ||
| 208 | }; | ||
| 209 | |||
| 210 | // The provider id doubles as the OAuth callback path segment | ||
| 211 | // (`/api/auth/oauth2/callback/<id>`), so it must be URL-safe. | ||
| 212 | const PROVIDER_ID_PATTERN = /^[a-z0-9-]{1,48}$/; | ||
| 213 | |||
| 214 | // A half-configured provider is refused rather than silently ignored (same | ||
| 215 | // posture as resolveSandboxTimeoutMs): an operator who set some of the | ||
| 216 | // variables should find out at boot, not by staring at a login page with no | ||
| 217 | // button. An empty domain allowlist is refused too — without it, SSO sign-in | ||
| 218 | // would be open registration for anyone with an account at the IdP, bypassing | ||
| 219 | // the invite gate entirely. | ||
| 220 | const resolveSso = (): SsoConfig | undefined => { | ||
| 221 | const clientId = process.env.EXECUTOR_SSO_CLIENT_ID?.trim(); | ||
| 222 | const clientSecret = process.env.EXECUTOR_SSO_CLIENT_SECRET?.trim(); | ||
| 223 | if (!clientId && !clientSecret) return undefined; | ||
| 224 | if (!clientId || !clientSecret) { | ||
| 225 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: refuse to boot on half-configured SSO credentials | ||
| 226 | throw new Error("EXECUTOR_SSO_CLIENT_ID and EXECUTOR_SSO_CLIENT_SECRET must be set together"); | ||
| 227 | } | ||
| 228 | const providerId = process.env.EXECUTOR_SSO_PROVIDER_ID?.trim().toLowerCase() ?? ""; | ||
| 229 | if (!PROVIDER_ID_PATTERN.test(providerId)) { | ||
| 230 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: refuse to boot on a missing/malformed provider id | ||
| 231 | throw new Error( | ||
| 232 | 'EXECUTOR_SSO_PROVIDER_ID is required when SSO is configured (1-48 chars of [a-z0-9-], e.g. "google" or "okta") — it names the provider and its OAuth callback path', | ||
| 233 | ); | ||
| 234 | } | ||
| 235 | const discoveryUrl = | ||
| 236 | process.env.EXECUTOR_SSO_DISCOVERY_URL?.trim() || DISCOVERY_PRESETS[providerId]; | ||
| 237 | if (!discoveryUrl) { | ||
| 238 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: refuse to boot without a way to reach the IdP | ||
| 239 | throw new Error( | ||
| 240 | `EXECUTOR_SSO_DISCOVERY_URL is required for provider ${JSON.stringify(providerId)} (the IdP's …/.well-known/openid-configuration URL)`, | ||
| 241 | ); | ||
| 242 | } | ||
| 243 | const allowedDomains = (process.env.EXECUTOR_SSO_ALLOWED_DOMAINS ?? "") | ||
| 244 | .split(",") | ||
| 245 | .map((domain) => domain.trim().replace(/^@/, "").toLowerCase()) | ||
| 246 | .filter((domain) => domain.length > 0); | ||
| 247 | if (allowedDomains.length === 0) { | ||
| 248 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: SSO sign-in without a domain allowlist is open registration; refuse to boot | ||
| 249 | throw new Error( | ||
| 250 | 'EXECUTOR_SSO_ALLOWED_DOMAINS is required when SSO is configured (comma-separated email domains, e.g. "example.com") — it is what gates sign-ups in place of an invite code', | ||
| 251 | ); | ||
| 252 | } | ||
| 253 | const providerName = | ||
| 254 | process.env.EXECUTOR_SSO_PROVIDER_NAME?.trim() || | ||
| 255 | providerId.charAt(0).toUpperCase() + providerId.slice(1); | ||
| 256 | return { providerId, providerName, discoveryUrl, clientId, clientSecret, allowedDomains }; | ||
| 257 | }; | ||
| 258 | |||
| 180 | // A malformed value is refused rather than silently ignored: an operator who | 259 | // A malformed value is refused rather than silently ignored: an operator who |
| 181 | // sets the knob and typos it should find out at boot, not by watching a | 260 | // sets the knob and typos it should find out at boot, not by watching a |
| 182 | // runaway execution use the 5-minute default. | 261 | // runaway execution use the 5-minute default. |
| @@ -7,8 +7,10 @@ import { Schema } from "effect"; · 6 unmodified lines | |||
| 7 | // GET /api/health readiness probe (used by the container healthcheck) | 7 | // GET /api/health readiness probe (used by the container healthcheck) |
| 8 | // GET /api/setup-status whether the instance still needs first-run setup, so | 8 | // GET /api/setup-status whether the instance still needs first-run setup, so |
| 9 | // the pre-login SPA can route a fresh operator to /setup | 9 | // the pre-login SPA can route a fresh operator to /setup |
| 10 | // GET /api/auth-config which SSO sign-in providers are configured, so the | ||
| 11 | // login page knows which provider buttons to render | ||
| 10 | // | 12 | // |
| 11 | // Both are deliberately unauthenticated and return only booleans/status — no | 13 | // All are deliberately unauthenticated and return only booleans/status — no |
| 12 | // sensitive data — so they can be read before anyone has signed in. | 14 | // sensitive data — so they can be read before anyone has signed in. |
| 13 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
| 14 | 16 | ||
| @@ -21,6 +23,10 @@ export class SystemError extends Schema.TaggedErrorClass<SystemError>()( · 6 unmodified lines | |||
| 21 | export const HealthResponse = Schema.Struct({ status: Schema.String }); | 23 | export const HealthResponse = Schema.Struct({ status: Schema.String }); |
| 22 | export const SetupStatusResponse = Schema.Struct({ needsSetup: Schema.Boolean }); | 24 | export const SetupStatusResponse = Schema.Struct({ needsSetup: Schema.Boolean }); |
| 23 | export const InviteStatusResponse = Schema.Struct({ valid: Schema.Boolean }); | 25 | export const InviteStatusResponse = Schema.Struct({ valid: Schema.Boolean }); |
| 26 | // Provider ids + display names only — never credentials or allowlists. | ||
| 27 | export const AuthConfigResponse = Schema.Struct({ | ||
| 28 | ssoProviders: Schema.Array(Schema.Struct({ id: Schema.String, name: Schema.String })), | ||
| 29 | }); | ||
| 24 | 30 | ||
| 25 | const InviteStatusParams = { code: Schema.String }; | 31 | const InviteStatusParams = { code: Schema.String }; |
| 26 | 32 | ||
| @@ -37,6 +43,12 @@ export const SystemApi = HttpApiGroup.make("system") · 10 unmodified lines | |||
| 37 | error: [SystemError], | 43 | error: [SystemError], |
| 38 | }), | 44 | }), |
| 39 | ) | 45 | ) |
| 46 | .add( | ||
| 47 | HttpApiEndpoint.get("authConfig", "/auth-config", { | ||
| 48 | success: AuthConfigResponse, | ||
| 49 | error: [SystemError], | ||
| 50 | }), | ||
| 51 | ) | ||
| 40 | .add( | 52 | .add( |
| 41 | HttpApiEndpoint.get("inviteStatus", "/invite-status/:code", { | 53 | HttpApiEndpoint.get("inviteStatus", "/invite-status/:code", { |
| 42 | params: InviteStatusParams, | 54 | params: InviteStatusParams, |
| @@ -6,6 +6,7 @@ import { SystemError, SystemHttpApi } from "./api"; · 5 unmodified lines | |||
| 6 | import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth"; | 6 | import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth"; |
| 7 | import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db"; | 7 | import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db"; |
| 8 | import { findRedeemableCode } from "../auth/invites"; | 8 | import { findRedeemableCode } from "../auth/invites"; |
| 9 | import { loadConfig } from "../config"; | ||
| 9 | 10 | ||
| 10 | // --------------------------------------------------------------------------- | 11 | // --------------------------------------------------------------------------- |
| 11 | // Handlers for the public system API. Unauthenticated; every DB touch is an | 12 | // Handlers for the public system API. Unauthenticated; every DB touch is an |
| @@ -40,6 +41,15 @@ export const SystemHandlers = HttpApiBuilder.group(SystemHttpApi, "system", (han · 28 unmodified lines | |||
| 40 | return { needsSetup: count === 0 }; | 41 | return { needsSetup: count === 0 }; |
| 41 | }), | 42 | }), |
| 42 | ) | 43 | ) |
| 44 | .handle("authConfig", () => | ||
| 45 | // Which SSO provider the operator configured — id + display name only, so | ||
| 46 | // the pre-login page knows which button to render. Config is env-derived | ||
| 47 | // and boot-validated, so this read cannot fail. | ||
| 48 | Effect.sync(() => { | ||
| 49 | const sso = loadConfig().sso; | ||
| 50 | return { ssoProviders: sso ? [{ id: sso.providerId, name: sso.providerName }] : [] }; | ||
| 51 | }), | ||
| 52 | ) | ||
| 43 | .handle("inviteStatus", ({ params }) => | 53 | .handle("inviteStatus", ({ params }) => |
| 44 | Effect.gen(function* () { | 54 | Effect.gen(function* () { |
| 45 | const { client } = yield* SelfHostDb; | 55 | const { client } = yield* SelfHostDb; |
| @@ -1,9 +1,12 @@ | |||
| 1 | import { createAuthClient } from "better-auth/react"; | 1 | import { createAuthClient } from "better-auth/react"; |
| 2 | import { genericOAuthClient } from "better-auth/client/plugins"; | ||
| 2 | 3 | ||
| 3 | // Better Auth browser client. Talks to the self-host server's /api/auth (same | 4 | // Better Auth browser client. Talks to the self-host server's /api/auth (same |
| 4 | // origin); the session cookie it sets is what the shared AuthProvider's | 5 | // origin); the session cookie it sets is what the shared AuthProvider's |
| 5 | // /account/me query and all API calls authenticate with. Only the login form | 6 | // /account/me query and all API calls authenticate with. Only the login form |
| 6 | // and sign-out use this — auth STATE comes from the shared AuthProvider. | 7 | // and sign-out use this — auth STATE comes from the shared AuthProvider. |
| 8 | // `genericOAuthClient` adds `signIn.oauth2` for the configured SSO provider. | ||
| 7 | export const authClient = createAuthClient({ | 9 | export const authClient = createAuthClient({ |
| 8 | baseURL: `${window.location.origin}/api/auth`, | 10 | baseURL: `${window.location.origin}/api/auth`, |
| 11 | plugins: [genericOAuthClient()], | ||
| 9 | }); | 12 | }); |
| @@ -0,0 +1,29 @@ | |||
| 1 | // Pre-login read of which SSO providers the operator configured, so the login | ||
| 2 | // page knows which provider buttons to render. Same boundary as setup-status: a | ||
| 3 | // plain same-origin fetch that runs before the atom registry exists. Fails soft | ||
| 4 | // to "no providers" — the email/password form is always available, so a hiccup | ||
| 5 | // here degrades to the baseline login, never a lockout. | ||
| 6 | |||
| 7 | export interface SsoProvider { | ||
| 8 | readonly id: string; | ||
| 9 | readonly name: string; | ||
| 10 | } | ||
| 11 | |||
| 12 | const isSsoProvider = (value: unknown): value is SsoProvider => | ||
| 13 | typeof value === "object" && | ||
| 14 | value !== null && | ||
| 15 | typeof (value as { id?: unknown }).id === "string" && | ||
| 16 | typeof (value as { name?: unknown }).name === "string"; | ||
| 17 | |||
| 18 | export const fetchSsoProviders = async (): Promise<readonly SsoProvider[]> => { | ||
| 19 | const response = await fetch("/api/auth-config", { credentials: "same-origin" }).then( | ||
| 20 | (r) => r, | ||
| 21 | () => null, | ||
| 22 | ); | ||
| 23 | if (!response?.ok) return []; | ||
| 24 | const data = (await response.json().then( | ||
| 25 | (d) => d, | ||
| 26 | () => ({}), | ||
| 27 | )) as { ssoProviders?: unknown }; | ||
| 28 | return Array.isArray(data.ssoProviders) ? data.ssoProviders.filter(isSsoProvider) : []; | ||
| 29 | }; | ||
| @@ -1,17 +1,19 @@ | |||
| 1 | import { useState, type FormEvent } from "react"; | 1 | import { useEffect, useState, type FormEvent } from "react"; |
| 2 | 2 | ||
| 3 | import { Button } from "@executor-js/react/components/button"; | 3 | import { Button } from "@executor-js/react/components/button"; |
| 4 | import { Input } from "@executor-js/react/components/input"; | 4 | import { Input } from "@executor-js/react/components/input"; |
| 5 | import { Label } from "@executor-js/react/components/label"; | 5 | import { Label } from "@executor-js/react/components/label"; |
| 6 | 6 | ||
| 7 | import { authClient } from "./auth-client"; | 7 | import { authClient } from "./auth-client"; |
| 8 | import { fetchSsoProviders, type SsoProvider } from "./auth-config"; | ||
| 8 | import { AuthLayout } from "./auth-layout"; | 9 | import { AuthLayout } from "./auth-layout"; |
| 9 | import { postLoginTarget } from "../src/auth/return-to"; | 10 | import { postLoginTarget } from "../src/auth/return-to"; |
| 10 | 11 | ||
| 11 | // Self-host login: email + password sign-in via Better Auth. On success we | 12 | // Self-host login: email + password sign-in via Better Auth, plus a provider |
| 12 | // reload so the shared AuthProvider re-reads /account/me and the AuthGate swaps | 13 | // button per configured SSO provider (read from /api/auth-config). On |
| 13 | // in the app. (Cloud's equivalent is a WorkOS redirect — this is the | 14 | // success we reload so the shared AuthProvider re-reads /account/me and the |
| 14 | // provider-specific piece injected into the shared shell.) | 15 | // AuthGate swaps in the app. (Cloud's equivalent is a WorkOS redirect — this is |
| 16 | // the provider-specific piece injected into the shared shell.) | ||
| 15 | // | 17 | // |
| 16 | // There is no self-signup here: open registration is closed. New people join by | 18 | // There is no self-signup here: open registration is closed. New people join by |
| 17 | // redeeming an invite — either the full /join/<code> link, or by entering the | 19 | // redeeming an invite — either the full /join/<code> link, or by entering the |
| @@ -27,6 +29,17 @@ export const LoginPage = () => { · 9 unmodified lines | |||
| 27 | const [code, setCode] = useState(""); | 29 | const [code, setCode] = useState(""); |
| 28 | const [error, setError] = useState<string | null>(null); | 30 | const [error, setError] = useState<string | null>(null); |
| 29 | const [busy, setBusy] = useState(false); | 31 | const [busy, setBusy] = useState(false); |
| 32 | const [ssoProviders, setSsoProviders] = useState<readonly SsoProvider[]>([]); | ||
| 33 | |||
| 34 | useEffect(() => { | ||
| 35 | let cancelled = false; | ||
| 36 | void fetchSsoProviders().then((providers) => { | ||
| 37 | if (!cancelled) setSsoProviders(providers); | ||
| 38 | }); | ||
| 39 | return () => { | ||
| 40 | cancelled = true; | ||
| 41 | }; | ||
| 42 | }, []); | ||
| 30 | 43 | ||
| 31 | const signIn = async (event: FormEvent) => { | 44 | const signIn = async (event: FormEvent) => { |
| 32 | event.preventDefault(); | 45 | event.preventDefault(); |
| @@ -41,6 +54,22 @@ export const LoginPage = () => { · 8 unmodified lines | |||
| 41 | window.location.href = postLogin; | 54 | window.location.href = postLogin; |
| 42 | }; | 55 | }; |
| 43 | 56 | ||
| 57 | const signInWithSso = async (providerId: string) => { | ||
| 58 | setBusy(true); | ||
| 59 | setError(null); | ||
| 60 | // Better Auth redirects the browser to the IdP; on callback it lands on | ||
| 61 | // `callbackURL`, so the deep-link target survives the round trip the same | ||
| 62 | // way it does for the email form's post-login navigation. | ||
| 63 | const result = await authClient.signIn.oauth2({ | ||
| 64 | providerId, | ||
| 65 | callbackURL: postLogin, | ||
| 66 | }); | ||
| 67 | if (result.error) { | ||
| 68 | setBusy(false); | ||
| 69 | setError(result.error.message ?? "Sign in failed"); | ||
| 70 | } | ||
| 71 | }; | ||
| 72 | |||
| 44 | const redeem = (event: FormEvent) => { | 73 | const redeem = (event: FormEvent) => { |
| 45 | event.preventDefault(); | 74 | event.preventDefault(); |
| 46 | const trimmed = code.trim(); | 75 | const trimmed = code.trim(); |
| @@ -94,6 +123,27 @@ export const LoginPage = () => { · 47 unmodified lines | |||
| 94 | <Button type="submit" disabled={busy} className="w-full"> | 123 | <Button type="submit" disabled={busy} className="w-full"> |
| 95 | {busy ? "…" : "Sign in"} | 124 | {busy ? "…" : "Sign in"} |
| 96 | </Button> | 125 | </Button> |
| 126 | {ssoProviders.length > 0 && ( | ||
| 127 | <> | ||
| 128 | <div className="flex items-center gap-3"> | ||
| 129 | <div className="h-px flex-1 bg-border" /> | ||
| 130 | <span className="text-xs text-muted-foreground">or</span> | ||
| 131 | <div className="h-px flex-1 bg-border" /> | ||
| 132 | </div> | ||
| 133 | {ssoProviders.map((provider) => ( | ||
| 134 | <Button | ||
| 135 | key={provider.id} | ||
| 136 | type="button" | ||
| 137 | variant="outline" | ||
| 138 | disabled={busy} | ||
| 139 | onClick={() => void signInWithSso(provider.id)} | ||
| 140 | className="w-full" | ||
| 141 | > | ||
| 142 | Continue with {provider.name} | ||
| 143 | </Button> | ||
| 144 | ))} | ||
| 145 | </> | ||
| 146 | )} | ||
| 97 | </form> | 147 | </form> |
| 98 | ) : ( | 148 | ) : ( |
| 99 | <form onSubmit={redeem} className="space-y-4"> | 149 | <form onSubmit={redeem} className="space-y-4"> |
| @@ -256,6 +256,7 @@ · 255 unmodified lines | |||
| 256 | }, | 256 | }, |
| 257 | "devDependencies": { | 257 | "devDependencies": { |
| 258 | "@effect/vitest": "catalog:", | 258 | "@effect/vitest": "catalog:", |
| 259 | "@executor-js/emulate": "^0.14.0", | ||
| 259 | "@executor-js/vite-plugin": "workspace:*", | 260 | "@executor-js/vite-plugin": "workspace:*", |
| 260 | "@tailwindcss/vite": "catalog:", | 261 | "@tailwindcss/vite": "catalog:", |
| 261 | "@tanstack/router-plugin": "^1.167.12", | 262 | "@tanstack/router-plugin": "^1.167.12", |
| @@ -1412,24 +1413,6 @@ · 1150 unmodified lines | |||
| 1412 | 1413 | ||
| 1413 | "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], | 1414 | "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], |
| 1414 | 1415 | ||
| 1415 | "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], | ||
| 1416 | |||
| 1417 | "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], | ||
| 1418 | |||
| 1419 | "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], | ||
| 1420 | |||
| 1421 | "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.23.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ=="], | ||
| 1422 | |||
| 1423 | "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], | ||
| 1424 | |||
| 1425 | "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], | ||
| 1426 | |||
| 1427 | "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], | ||
| 1428 | |||
| 1429 | "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], | ||
| 1430 | |||
| 1431 | "@azure/msal-browser": ["@azure/msal-browser@5.6.3", "", { "dependencies": { "@azure/msal-common": "16.4.1" } }, "sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w=="], | ||
| 1432 | |||
| 1433 | "@azure/msal-common": ["@azure/msal-common@16.10.0", "", {}, "sha512-iYtjpanlv6963Jprs0MvzIap07V+QhultjQctfbEDQCflsDAEeO3R7XnVA5gk30fhoBFLdgJT7VqO0TGsEsN9w=="], | 1416 | "@azure/msal-common": ["@azure/msal-common@16.10.0", "", {}, "sha512-iYtjpanlv6963Jprs0MvzIap07V+QhultjQctfbEDQCflsDAEeO3R7XnVA5gk30fhoBFLdgJT7VqO0TGsEsN9w=="], |
| 1434 | 1417 | ||
| 1435 | "@azure/msal-node": ["@azure/msal-node@5.3.0", "", { "dependencies": { "@azure/msal-common": "16.10.0", "jsonwebtoken": "^9.0.0" } }, "sha512-fXtJX811pX8y8QlrQqBSH6+plvWyKZDI0IxkheAcyAw9OtcpXyFivmTC7eGUqutLWaDlKXuQ3yOESD4zAmkjHg=="], | 1418 | "@azure/msal-node": ["@azure/msal-node@5.3.0", "", { "dependencies": { "@azure/msal-common": "16.10.0", "jsonwebtoken": "^9.0.0" } }, "sha512-fXtJX811pX8y8QlrQqBSH6+plvWyKZDI0IxkheAcyAw9OtcpXyFivmTC7eGUqutLWaDlKXuQ3yOESD4zAmkjHg=="], |
| @@ -2336,7 +2319,7 @@ · 900 unmodified lines | |||
| 2336 | 2319 | ||
| 2337 | "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], | 2320 | "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], |
| 2338 | 2321 | ||
| 2339 | "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/resources": "2.0.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-Bvy8QDjO05umd0+j+gDeWcTaVa1/R2lDj/eOvjzpm8VQj1K1vVZJuyjThpV5/lSHyYW2JaHF2IQ7Z8twJFAhjA=="], | 2322 | "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], |
| 2340 | 2323 | ||
| 2341 | "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/configuration": "0.214.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.214.0", "@opentelemetry/exporter-logs-otlp-http": "0.214.0", "@opentelemetry/exporter-logs-otlp-proto": "0.214.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.214.0", "@opentelemetry/exporter-prometheus": "0.214.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.214.0", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/exporter-trace-otlp-proto": "0.214.0", "@opentelemetry/exporter-zipkin": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/propagator-b3": "2.6.1", "@opentelemetry/propagator-jaeger": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-gl2XvQBJuPjhGcw9SsnQO5qxChAPMuGRPFaD8lqtF+Cey91NgGUQ0sio2vlDFOSm3JOLzc44vL+OAfx1dXuZjg=="], | 2324 | "@opentelemetry/sdk-node": ["@opentelemetry/sdk-node@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/configuration": "0.214.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/exporter-logs-otlp-grpc": "0.214.0", "@opentelemetry/exporter-logs-otlp-http": "0.214.0", "@opentelemetry/exporter-logs-otlp-proto": "0.214.0", "@opentelemetry/exporter-metrics-otlp-grpc": "0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "0.214.0", "@opentelemetry/exporter-metrics-otlp-proto": "0.214.0", "@opentelemetry/exporter-prometheus": "0.214.0", "@opentelemetry/exporter-trace-otlp-grpc": "0.214.0", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/exporter-trace-otlp-proto": "0.214.0", "@opentelemetry/exporter-zipkin": "2.6.1", "@opentelemetry/instrumentation": "0.214.0", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/propagator-b3": "2.6.1", "@opentelemetry/propagator-jaeger": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-gl2XvQBJuPjhGcw9SsnQO5qxChAPMuGRPFaD8lqtF+Cey91NgGUQ0sio2vlDFOSm3JOLzc44vL+OAfx1dXuZjg=="], |
| 2342 | 2325 | ||
| @@ -3278,8 +3261,6 @@ · 935 unmodified lines | |||
| 3278 | 3261 | ||
| 3279 | "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], | 3262 | "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="], |
| 3280 | 3263 | ||
| 3281 | "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.5", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-yURCknZhvywvQItHMMmFSo+fq5arCUIyz/CVk7jD89MSai7dkaX8ufjCWp3NttLojoTVbcE72ri+be/TnEbMHw=="], | ||
| 3282 | |||
| 3283 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], | 3264 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], |
| 3284 | 3265 | ||
| 3285 | "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], | 3266 | "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], |
| @@ -4990,22 +4971,12 @@ · 1704 unmodified lines | |||
| 4990 | 4971 | ||
| 4991 | "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], | 4972 | "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="], |
| 4992 | 4973 | ||
| 4993 | "pg": ["pg@8.20.0", "", { "dependencies": { "pg-connection-string": "^2.12.0", "pg-pool": "^3.13.0", "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.3.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA=="], | ||
| 4994 | |||
| 4995 | "pg-cloudflare": ["pg-cloudflare@1.3.0", "", {}, "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ=="], | ||
| 4996 | |||
| 4997 | "pg-connection-string": ["pg-connection-string@2.12.0", "", {}, "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ=="], | ||
| 4998 | |||
| 4999 | "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], | 4974 | "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="], |
| 5000 | 4975 | ||
| 5001 | "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="], | ||
| 5002 | |||
| 5003 | "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], | 4976 | "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], |
| 5004 | 4977 | ||
| 5005 | "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], | 4978 | "pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="], |
| 5006 | 4979 | ||
| 5007 | "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="], | ||
| 5008 | |||
| 5009 | "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], | 4980 | "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], |
| 5010 | 4981 | ||
| 5011 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], | 4982 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], |
| @@ -5462,16 +5433,12 @@ · 450 unmodified lines | |||
| 5462 | 5433 | ||
| 5463 | "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], | 5434 | "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="], |
| 5464 | 5435 | ||
| 5465 | "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], | ||
| 5466 | |||
| 5467 | "spotify-web-api-node": ["spotify-web-api-node@5.0.2", "", { "dependencies": { "superagent": "^6.1.0" } }, "sha512-r82dRWU9PMimHvHEzL0DwEJrzFk+SMCVfq249SLt3I7EFez7R+jeoKQd+M1//QcnjqlXPs2am4DFsGk8/GCsrA=="], | 5436 | "spotify-web-api-node": ["spotify-web-api-node@5.0.2", "", { "dependencies": { "superagent": "^6.1.0" } }, "sha512-r82dRWU9PMimHvHEzL0DwEJrzFk+SMCVfq249SLt3I7EFez7R+jeoKQd+M1//QcnjqlXPs2am4DFsGk8/GCsrA=="], |
| 5468 | 5437 | ||
| 5469 | "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], | 5438 | "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], |
| 5470 | 5439 | ||
| 5471 | "sql-highlight": ["sql-highlight@6.1.0", "", {}, "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA=="], | 5440 | "sql-highlight": ["sql-highlight@6.1.0", "", {}, "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA=="], |
| 5472 | 5441 | ||
| 5473 | "sql.js": ["sql.js@1.14.1", "", {}, "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A=="], | ||
| 5474 | |||
| 5475 | "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], | 5442 | "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], |
| 5476 | 5443 | ||
| 5477 | "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], | 5444 | "sshpk": ["sshpk@1.18.0", "", { "dependencies": { "asn1": "~0.2.3", "assert-plus": "^1.0.0", "bcrypt-pbkdf": "^1.0.0", "dashdash": "^1.12.0", "ecc-jsbn": "~0.1.1", "getpass": "^0.1.1", "jsbn": "~0.1.0", "safer-buffer": "^2.0.2", "tweetnacl": "~0.14.0" }, "bin": { "sshpk-conv": "bin/sshpk-conv", "sshpk-sign": "bin/sshpk-sign", "sshpk-verify": "bin/sshpk-verify" } }, "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ=="], |
| @@ -5902,10 +5869,6 @@ · 424 unmodified lines | |||
| 5902 | 5869 | ||
| 5903 | "@astrojs/react/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], | 5870 | "@astrojs/react/vite": ["vite@7.3.2", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], |
| 5904 | 5871 | ||
| 5905 | "@azure/identity/@azure/msal-node": ["@azure/msal-node@5.1.2", "", { "dependencies": { "@azure/msal-common": "16.4.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A=="], | ||
| 5906 | |||
| 5907 | "@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@16.4.1", "", {}, "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw=="], | ||
| 5908 | |||
| 5909 | "@babel/core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], | 5872 | "@babel/core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], |
| 5910 | 5873 | ||
| 5911 | "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], | 5874 | "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], |
| @@ -6156,20 +6119,12 @@ · 244 unmodified lines | |||
| 6156 | 6119 | ||
| 6157 | "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6120 | "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6158 | 6121 | ||
| 6159 | "@opentelemetry/exporter-metrics-otlp-grpc/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], | ||
| 6160 | |||
| 6161 | "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6122 | "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6162 | 6123 | ||
| 6163 | "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], | ||
| 6164 | |||
| 6165 | "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6124 | "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6166 | 6125 | ||
| 6167 | "@opentelemetry/exporter-metrics-otlp-proto/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], | ||
| 6168 | |||
| 6169 | "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6126 | "@opentelemetry/exporter-prometheus/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6170 | 6127 | ||
| 6171 | "@opentelemetry/exporter-prometheus/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], | ||
| 6172 | |||
| 6173 | "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6128 | "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6174 | 6129 | ||
| 6175 | "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], | 6130 | "@opentelemetry/exporter-trace-otlp-grpc/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], |
| @@ -6204,24 +6159,18 @@ · 28 unmodified lines | |||
| 6204 | 6159 | ||
| 6205 | "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6160 | "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6206 | 6161 | ||
| 6207 | "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], | ||
| 6208 | |||
| 6209 | "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], | 6162 | "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], |
| 6210 | 6163 | ||
| 6211 | "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], | 6164 | "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="], |
| 6212 | 6165 | ||
| 6213 | "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6166 | "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6214 | 6167 | ||
| 6215 | "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.0.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-SLX36allrcnVaPYG3R78F/UZZsBsvbc7lMCLx37LyH5MJ1KAAZ2E3mW9OAD3zGz0G8q/BtoS5VUrjzDydhD6LQ=="], | 6168 | "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6216 | |||
| 6217 | "@opentelemetry/sdk-metrics/@opentelemetry/resources": ["@opentelemetry/resources@2.0.0", "", { "dependencies": { "@opentelemetry/core": "2.0.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-rnZr6dML2z4IARI4zPGQV4arDikF/9OXZQzrC01dLmn0CZxU5U5OLd/m1T7YkGRj5UitjeoCtg/zorlgMQcdTg=="], | ||
| 6218 | 6169 | ||
| 6219 | "@opentelemetry/sdk-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], | 6170 | "@opentelemetry/sdk-node/@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="], |
| 6220 | 6171 | ||
| 6221 | "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], | 6172 | "@opentelemetry/sdk-node/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], |
| 6222 | 6173 | ||
| 6223 | "@opentelemetry/sdk-node/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], | ||
| 6224 | |||
| 6225 | "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], | 6174 | "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], |
| 6226 | 6175 | ||
| 6227 | "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], | 6176 | "@opentelemetry/sdk-node/@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="], |
| @@ -6840,10 +6789,6 @@ · 612 unmodified lines | |||
| 6840 | 6789 | ||
| 6841 | "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], | 6790 | "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="], |
| 6842 | 6791 | ||
| 6843 | "@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@16.4.1", "", {}, "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw=="], | ||
| 6844 | |||
| 6845 | "@azure/identity/@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], | ||
| 6846 | |||
| 6847 | "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="], | 6792 | "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="], |
| 6848 | 6793 | ||
| 6849 | "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="], | 6794 | "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="], |