* 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 | 53 | "devDependencies": { |
| 54 | 54 | "@effect/vitest": "catalog:", |
| 55 | + "@executor-js/emulate": "^0.14.0", | |
| 55 | 56 | "@executor-js/vite-plugin": "workspace:*", |
| 56 | 57 | "@tailwindcss/vite": "catalog:", |
| 57 | 58 | "@tanstack/router-plugin": "^1.167.12", |
| @@ -1,6 +1,13 @@ | ||
| 1 | 1 | import { betterAuth, type BetterAuthOptions } from "better-auth"; |
| 2 | 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 | 11 | import { apiKey } from "@better-auth/api-key"; |
| 5 | 12 | import { type Client } from "@libsql/client"; |
| 6 | 13 | import { LibsqlDialect, type LibsqlDialectConfig } from "@libsql/kysely-libsql"; |
| @@ -9,6 +16,7 @@ import { Context } from "effect"; · 2 unmodified lines | ||
| 9 | 16 | import { loadConfig } from "../config"; |
| 10 | 17 | import { seedOrgAndAdmin } from "./seed"; |
| 11 | 18 | import { consumeInviteCode, ensureInviteCodeTable, findRedeemableCode } from "./invites"; |
| 19 | +import { isAdmitted, isOAuthCallback, ssoProviderConfig } from "./sso"; | |
| 12 | 20 | |
| 13 | 21 | // The self-service signup gate: present only on the live (phase-2) auth |
| 14 | 22 | // instance, so the bootstrap seed's `createUser` — which |
| @@ -165,6 +173,14 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 150 unmodified lines | ||
| 165 | 173 | // is the page the user opens to confirm the code — the self-host app serves |
| 166 | 174 | // it at /device (this is also the Better Auth default; pinned for clarity). |
| 167 | 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 | 184 | // `consentPage` makes the MCP authorize flow redirect to a human approval |
| 169 | 185 | // screen instead of auto-issuing a code — but ONLY when the request |
| 170 | 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 | 216 | user: { |
| 201 | 217 | create: { |
| 202 | − before: async (_user, context) => { | |
| 203 | − if (context?.path !== SIGNUP_PATH) return; | |
| 218 | + before: async (user, context) => { | |
| 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 | 238 | if (await orgHasNoMembers(gate)) return; // first user claims the org |
| 205 | 239 | const code = inviteCodeFrom(context); |
| 206 | 240 | if (!code) { |
| @@ -217,9 +251,30 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 10 unmodified lines | ||
| 217 | 251 | } |
| 218 | 252 | }, |
| 219 | 253 | after: async (user, context) => { |
| 220 | − if (context?.path !== SIGNUP_PATH) return; | |
| 221 | 254 | const auth = gate.getAuth(); |
| 222 | 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 | 278 | // First user into an empty org becomes its owner (no code). |
| 224 | 279 | if (await orgHasNoMembers(gate)) { |
| 225 | 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 | 18 | export const SELF_HOST_NAMESPACE = "executor_selfhost"; |
| 19 | 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 | 41 | export interface SelfHostConfig { |
| 22 | 42 | /** Bind address. Defaults to loopback. */ |
| 23 | 43 | readonly host: string; |
| @@ -53,6 +73,8 @@ export interface SelfHostConfig { · 29 unmodified lines | ||
| 53 | 73 | * minutes (the same pattern as MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS on cloud). |
| 54 | 74 | */ |
| 55 | 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 | 79 | * How long an MCP session may sit idle before the in-process store evicts it, |
| 58 | 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 | 194 | organizationName: process.env.EXECUTOR_ORG_NAME ?? "Default", |
| 173 | 195 | orgSlug: resolveOrgSlug(), |
| 174 | 196 | sandboxTimeoutMs: resolveSandboxTimeoutMs(), |
| 197 | + sso: resolveSso(), | |
| 175 | 198 | mcpSessionIdleTtlMs: resolveMcpSessionIdleTtlMs(), |
| 176 | 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 | 259 | // A malformed value is refused rather than silently ignored: an operator who |
| 181 | 260 | // sets the knob and typos it should find out at boot, not by watching a |
| 182 | 261 | // runaway execution use the 5-minute default. |
| @@ -7,8 +7,10 @@ import { Schema } from "effect"; · 6 unmodified lines | ||
| 7 | 7 | // GET /api/health readiness probe (used by the container healthcheck) |
| 8 | 8 | // GET /api/setup-status whether the instance still needs first-run setup, so |
| 9 | 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 | 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 | 23 | export const HealthResponse = Schema.Struct({ status: Schema.String }); |
| 22 | 24 | export const SetupStatusResponse = Schema.Struct({ needsSetup: Schema.Boolean }); |
| 23 | 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 | 31 | const InviteStatusParams = { code: Schema.String }; |
| 26 | 32 | |
| @@ -37,6 +43,12 @@ export const SystemApi = HttpApiGroup.make("system") · 10 unmodified lines | ||
| 37 | 43 | error: [SystemError], |
| 38 | 44 | }), |
| 39 | 45 | ) |
| 46 | + .add( | |
| 47 | + HttpApiEndpoint.get("authConfig", "/auth-config", { | |
| 48 | + success: AuthConfigResponse, | |
| 49 | + error: [SystemError], | |
| 50 | + }), | |
| 51 | + ) | |
| 40 | 52 | .add( |
| 41 | 53 | HttpApiEndpoint.get("inviteStatus", "/invite-status/:code", { |
| 42 | 54 | params: InviteStatusParams, |
| @@ -6,6 +6,7 @@ import { SystemError, SystemHttpApi } from "./api"; · 5 unmodified lines | ||
| 6 | 6 | import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth"; |
| 7 | 7 | import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db"; |
| 8 | 8 | import { findRedeemableCode } from "../auth/invites"; |
| 9 | +import { loadConfig } from "../config"; | |
| 9 | 10 | |
| 10 | 11 | // --------------------------------------------------------------------------- |
| 11 | 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 | 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 | 53 | .handle("inviteStatus", ({ params }) => |
| 44 | 54 | Effect.gen(function* () { |
| 45 | 55 | const { client } = yield* SelfHostDb; |
| @@ -1,9 +1,12 @@ | ||
| 1 | 1 | import { createAuthClient } from "better-auth/react"; |
| 2 | +import { genericOAuthClient } from "better-auth/client/plugins"; | |
| 2 | 3 | |
| 3 | 4 | // Better Auth browser client. Talks to the self-host server's /api/auth (same |
| 4 | 5 | // origin); the session cookie it sets is what the shared AuthProvider's |
| 5 | 6 | // /account/me query and all API calls authenticate with. Only the login form |
| 6 | 7 | // and sign-out use this — auth STATE comes from the shared AuthProvider. |
| 8 | +// `genericOAuthClient` adds `signIn.oauth2` for the configured SSO provider. | |
| 7 | 9 | export const authClient = createAuthClient({ |
| 8 | 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 | 3 | import { Button } from "@executor-js/react/components/button"; |
| 4 | 4 | import { Input } from "@executor-js/react/components/input"; |
| 5 | 5 | import { Label } from "@executor-js/react/components/label"; |
| 6 | 6 | |
| 7 | 7 | import { authClient } from "./auth-client"; |
| 8 | +import { fetchSsoProviders, type SsoProvider } from "./auth-config"; | |
| 8 | 9 | import { AuthLayout } from "./auth-layout"; |
| 9 | 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 | −// reload so the shared AuthProvider re-reads /account/me and the AuthGate swaps | |
| 13 | −// in the app. (Cloud's equivalent is a WorkOS redirect — this is the | |
| 14 | −// provider-specific piece injected into the shared shell.) | |
| 12 | +// Self-host login: email + password sign-in via Better Auth, plus a provider | |
| 13 | +// button per configured SSO provider (read from /api/auth-config). On | |
| 14 | +// success we reload so the shared AuthProvider re-reads /account/me and the | |
| 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 | 18 | // There is no self-signup here: open registration is closed. New people join by |
| 17 | 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 | 29 | const [code, setCode] = useState(""); |
| 28 | 30 | const [error, setError] = useState<string | null>(null); |
| 29 | 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 | 44 | const signIn = async (event: FormEvent) => { |
| 32 | 45 | event.preventDefault(); |
| @@ -41,6 +54,22 @@ export const LoginPage = () => { · 8 unmodified lines | ||
| 41 | 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 | 73 | const redeem = (event: FormEvent) => { |
| 45 | 74 | event.preventDefault(); |
| 46 | 75 | const trimmed = code.trim(); |
| @@ -94,6 +123,27 @@ export const LoginPage = () => { · 47 unmodified lines | ||
| 94 | 123 | <Button type="submit" disabled={busy} className="w-full"> |
| 95 | 124 | {busy ? "…" : "Sign in"} |
| 96 | 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 | 147 | </form> |
| 98 | 148 | ) : ( |
| 99 | 149 | <form onSubmit={redeem} className="space-y-4"> |
| @@ -256,6 +256,7 @@ · 255 unmodified lines | ||
| 256 | 256 | }, |
| 257 | 257 | "devDependencies": { |
| 258 | 258 | "@effect/vitest": "catalog:", |
| 259 | + "@executor-js/emulate": "^0.14.0", | |
| 259 | 260 | "@executor-js/vite-plugin": "workspace:*", |
| 260 | 261 | "@tailwindcss/vite": "catalog:", |
| 261 | 262 | "@tanstack/router-plugin": "^1.167.12", |
| @@ -1412,24 +1413,6 @@ · 1150 unmodified lines | ||
| 1412 | 1413 | |
| 1413 | 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 | 1416 | "@azure/msal-common": ["@azure/msal-common@16.10.0", "", {}, "sha512-iYtjpanlv6963Jprs0MvzIap07V+QhultjQctfbEDQCflsDAEeO3R7XnVA5gk30fhoBFLdgJT7VqO0TGsEsN9w=="], |
| 1434 | 1417 | |
| 1435 | 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 | 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 | 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 | 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 | 3264 | "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], |
| 3284 | 3265 | |
| 3285 | 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 | 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 | 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 | 4976 | "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="], |
| 5004 | 4977 | |
| 5005 | 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 | 4980 | "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="], |
| 5010 | 4981 | |
| 5011 | 4982 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], |
| @@ -5462,16 +5433,12 @@ · 450 unmodified lines | ||
| 5462 | 5433 | |
| 5463 | 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 | 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 | 5438 | "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], |
| 5470 | 5439 | |
| 5471 | 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 | 5442 | "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="], |
| 5476 | 5443 | |
| 5477 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 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=="], | |
| 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=="], | |
| 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=="], | |
| 6218 | 6169 | |
| 6219 | 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 | 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 | 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 | 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 | 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 | 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 | 6794 | "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="], |