History

Feat/selfhost google sso (#1834)

* 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

Charlie Meyer committed 2 months ago

Commit b22d5361 parent 31a804212 files · +620 −68Browse files

Unified · Split

12 files
  1. .changeset/selfhost-sso-sign-in.md +9 −0
  2. apps/host-selfhost/package.json +1 −0
  3. apps/host-selfhost/src/auth/better-auth.ts +59 −4
  4. apps/host-selfhost/src/auth/sso.test.ts +313 −0
  5. apps/host-selfhost/src/auth/sso.ts +46 −0
  6. apps/host-selfhost/src/config.ts +79 −0
  7. apps/host-selfhost/src/system/api.ts +13 −1
  8. apps/host-selfhost/src/system/handlers.ts +10 −0
  9. apps/host-selfhost/web/auth-client.ts +3 −0
  10. apps/host-selfhost/web/auth-config.ts +29 −0
  11. apps/host-selfhost/web/login.tsx +55 −5
  12. bun.lock +3 −58
.changeset/selfhost-sso-sign-in.md+9−0
@@ -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`.
apps/host-selfhost/package.json+1−0
@@ -52,6 +52,7 @@ · 51 unmodified lines
5252 },
5353 "devDependencies": {
5454 "@effect/vitest": "catalog:",
55+ "@executor-js/emulate": "^0.14.0",
5556 "@executor-js/vite-plugin": "workspace:*",
5657 "@tailwindcss/vite": "catalog:",
5758 "@tanstack/router-plugin": "^1.167.12",
apps/host-selfhost/src/auth/better-auth.ts+59−4
@@ -1,6 +1,13 @@
11 import { betterAuth, type BetterAuthOptions } from "better-auth";
22 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";
411 import { apiKey } from "@better-auth/api-key";
512 import { type Client } from "@libsql/client";
613 import { LibsqlDialect, type LibsqlDialectConfig } from "@libsql/kysely-libsql";
@@ -9,6 +16,7 @@ import { Context } from "effect"; · 2 unmodified lines
916 import { loadConfig } from "../config";
1017 import { seedOrgAndAdmin } from "./seed";
1118 import { consumeInviteCode, ensureInviteCodeTable, findRedeemableCode } from "./invites";
19+import { isAdmitted, isOAuthCallback, ssoProviderConfig } from "./sso";
1220
1321 // The self-service signup gate: present only on the live (phase-2) auth
1422 // instance, so the bootstrap seed's `createUser` — which
@@ -165,6 +173,14 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 150 unmodified lines
165173 // is the page the user opens to confirm the code — the self-host app serves
166174 // it at /device (this is also the Better Auth default; pinned for clarity).
167175 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)] : [] }),
168184 // `consentPage` makes the MCP authorize flow redirect to a human approval
169185 // screen instead of auto-issuing a code — but ONLY when the request
170186 // 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
199215 ? {
200216 user: {
201217 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+ }
204238 if (await orgHasNoMembers(gate)) return; // first user claims the org
205239 const code = inviteCodeFrom(context);
206240 if (!code) {
@@ -217,9 +251,30 @@ const makeAuthOptions = (client: Client, getOrganizationId: () => string, gate?: · 10 unmodified lines
217251 }
218252 },
219253 after: async (user, context) => {
220− if (context?.path !== SIGNUP_PATH) return;
221254 const auth = gate.getAuth();
222255 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+ }
223278 // First user into an empty org becomes its owner (no code).
224279 if (await orgHasNoMembers(gate)) {
225280 await auth.api.addMember({
apps/host-selfhost/src/auth/sso.test.ts+313−0
@@ -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("&amp;", "&")
189+ .replaceAll("&quot;", '"')
190+ .replaceAll("&#x27;", "'")
191+ .replaceAll("&lt;", "<")
192+ .replaceAll("&gt;", ">");
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+});
apps/host-selfhost/src/auth/sso.ts+46−0
@@ -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+});
apps/host-selfhost/src/config.ts+79−0
@@ -18,6 +18,26 @@ import { · 17 unmodified lines
1818 export const SELF_HOST_NAMESPACE = "executor_selfhost";
1919 export const SELF_HOST_SCHEMA_VERSION = "1.0.0";
2020
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+
2141 export interface SelfHostConfig {
2242 /** Bind address. Defaults to loopback. */
2343 readonly host: string;
@@ -53,6 +73,8 @@ export interface SelfHostConfig { · 29 unmodified lines
5373 * minutes (the same pattern as MCP_PAUSED_SESSION_IDLE_TIMEOUT_MS on cloud).
5474 */
5575 readonly sandboxTimeoutMs: number | undefined;
76+ /** SSO sign-in, or undefined when the operator hasn't configured it. */
77+ readonly sso: SsoConfig | undefined;
5678 /**
5779 * How long an MCP session may sit idle before the in-process store evicts it,
5880 * or undefined for the store's own default (30 minutes). 0 disables eviction.
@@ -172,11 +194,68 @@ export const loadConfig = (): SelfHostConfig => { · 113 unmodified lines
172194 organizationName: process.env.EXECUTOR_ORG_NAME ?? "Default",
173195 orgSlug: resolveOrgSlug(),
174196 sandboxTimeoutMs: resolveSandboxTimeoutMs(),
197+ sso: resolveSso(),
175198 mcpSessionIdleTtlMs: resolveMcpSessionIdleTtlMs(),
176199 toolsSyncTtlMs: resolveToolsSyncTtlMs(),
177200 };
178201 };
179202
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+
180259 // A malformed value is refused rather than silently ignored: an operator who
181260 // sets the knob and typos it should find out at boot, not by watching a
182261 // runaway execution use the 5-minute default.
apps/host-selfhost/src/system/api.ts+13−1
@@ -7,8 +7,10 @@ import { Schema } from "effect"; · 6 unmodified lines
77 // GET /api/health readiness probe (used by the container healthcheck)
88 // GET /api/setup-status whether the instance still needs first-run setup, so
99 // 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
1012 //
11−// Both are deliberately unauthenticated and return only booleans/status — no
13+// All are deliberately unauthenticated and return only booleans/status — no
1214 // sensitive data — so they can be read before anyone has signed in.
1315 // ---------------------------------------------------------------------------
1416
@@ -21,6 +23,10 @@ export class SystemError extends Schema.TaggedErrorClass<SystemError>()( · 6 unmodified lines
2123 export const HealthResponse = Schema.Struct({ status: Schema.String });
2224 export const SetupStatusResponse = Schema.Struct({ needsSetup: Schema.Boolean });
2325 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+});
2430
2531 const InviteStatusParams = { code: Schema.String };
2632
@@ -37,6 +43,12 @@ export const SystemApi = HttpApiGroup.make("system") · 10 unmodified lines
3743 error: [SystemError],
3844 }),
3945 )
46+ .add(
47+ HttpApiEndpoint.get("authConfig", "/auth-config", {
48+ success: AuthConfigResponse,
49+ error: [SystemError],
50+ }),
51+ )
4052 .add(
4153 HttpApiEndpoint.get("inviteStatus", "/invite-status/:code", {
4254 params: InviteStatusParams,
apps/host-selfhost/src/system/handlers.ts+10−0
@@ -6,6 +6,7 @@ import { SystemError, SystemHttpApi } from "./api"; · 5 unmodified lines
66 import { BetterAuth, countOrgMembers, type BetterAuthHandle } from "../auth/better-auth";
77 import { SelfHostDb, type SelfHostDbHandle } from "../db/self-host-db";
88 import { findRedeemableCode } from "../auth/invites";
9+import { loadConfig } from "../config";
910
1011 // ---------------------------------------------------------------------------
1112 // 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
4041 return { needsSetup: count === 0 };
4142 }),
4243 )
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+ )
4353 .handle("inviteStatus", ({ params }) =>
4454 Effect.gen(function* () {
4555 const { client } = yield* SelfHostDb;
apps/host-selfhost/web/auth-client.ts+3−0
@@ -1,9 +1,12 @@
11 import { createAuthClient } from "better-auth/react";
2+import { genericOAuthClient } from "better-auth/client/plugins";
23
34 // Better Auth browser client. Talks to the self-host server's /api/auth (same
45 // origin); the session cookie it sets is what the shared AuthProvider's
56 // /account/me query and all API calls authenticate with. Only the login form
67 // and sign-out use this — auth STATE comes from the shared AuthProvider.
8+// `genericOAuthClient` adds `signIn.oauth2` for the configured SSO provider.
79 export const authClient = createAuthClient({
810 baseURL: `${window.location.origin}/api/auth`,
11+ plugins: [genericOAuthClient()],
912 });
apps/host-selfhost/web/auth-config.ts+29−0
@@ -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+};
apps/host-selfhost/web/login.tsx+55−5
@@ -1,17 +1,19 @@
1−import { useState, type FormEvent } from "react";
1+import { useEffect, useState, type FormEvent } from "react";
22
33 import { Button } from "@executor-js/react/components/button";
44 import { Input } from "@executor-js/react/components/input";
55 import { Label } from "@executor-js/react/components/label";
66
77 import { authClient } from "./auth-client";
8+import { fetchSsoProviders, type SsoProvider } from "./auth-config";
89 import { AuthLayout } from "./auth-layout";
910 import { postLoginTarget } from "../src/auth/return-to";
1011
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.)
1517 //
1618 // There is no self-signup here: open registration is closed. New people join by
1719 // redeeming an invite — either the full /join/<code> link, or by entering the
@@ -27,6 +29,17 @@ export const LoginPage = () => { · 9 unmodified lines
2729 const [code, setCode] = useState("");
2830 const [error, setError] = useState<string | null>(null);
2931 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+ }, []);
3043
3144 const signIn = async (event: FormEvent) => {
3245 event.preventDefault();
@@ -41,6 +54,22 @@ export const LoginPage = () => { · 8 unmodified lines
4154 window.location.href = postLogin;
4255 };
4356
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+
4473 const redeem = (event: FormEvent) => {
4574 event.preventDefault();
4675 const trimmed = code.trim();
@@ -94,6 +123,27 @@ export const LoginPage = () => { · 47 unmodified lines
94123 <Button type="submit" disabled={busy} className="w-full">
95124 {busy ? "…" : "Sign in"}
96125 </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+ )}
97147 </form>
98148 ) : (
99149 <form onSubmit={redeem} className="space-y-4">
bun.lock+3−58
@@ -256,6 +256,7 @@ · 255 unmodified lines
256256 },
257257 "devDependencies": {
258258 "@effect/vitest": "catalog:",
259+ "@executor-js/emulate": "^0.14.0",
259260 "@executor-js/vite-plugin": "workspace:*",
260261 "@tailwindcss/vite": "catalog:",
261262 "@tanstack/router-plugin": "^1.167.12",
@@ -1412,24 +1413,6 @@ · 1150 unmodified lines
14121413
14131414 "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="],
14141415
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−
14331416 "@azure/msal-common": ["@azure/msal-common@16.10.0", "", {}, "sha512-iYtjpanlv6963Jprs0MvzIap07V+QhultjQctfbEDQCflsDAEeO3R7XnVA5gk30fhoBFLdgJT7VqO0TGsEsN9w=="],
14341417
14351418 "@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
23362319
23372320 "@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=="],
23382321
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=="],
23402323
23412324 "@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=="],
23422325
@@ -3278,8 +3261,6 @@ · 935 unmodified lines
32783261
32793262 "@typescript/vfs": ["@typescript/vfs@1.6.4", "", { "dependencies": { "debug": "^4.4.3" }, "peerDependencies": { "typescript": "*" } }, "sha512-PJFXFS4ZJKiJ9Qiuix6Dz/OwEIqHD7Dme1UwZhTK11vR+5dqW2ACbdndWQexBzCx+CPuMe5WBYQWCsFyGlQLlQ=="],
32803263
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−
32833264 "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
32843265
32853266 "@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
49904971
49914972 "performance-now": ["performance-now@2.1.0", "", {}, "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="],
49924973
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−
49994974 "pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
50004975
5001− "pg-pool": ["pg-pool@3.13.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA=="],
5002−
50034976 "pg-protocol": ["pg-protocol@1.13.0", "", {}, "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w=="],
50044977
50054978 "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=="],
50064979
5007− "pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
5008−
50094980 "piccolore": ["piccolore@0.1.3", "", {}, "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw=="],
50104981
50114982 "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -5462,16 +5433,12 @@ · 450 unmodified lines
54625433
54635434 "split-string": ["split-string@3.1.0", "", { "dependencies": { "extend-shallow": "^3.0.0" } }, "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw=="],
54645435
5465− "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
5466−
54675436 "spotify-web-api-node": ["spotify-web-api-node@5.0.2", "", { "dependencies": { "superagent": "^6.1.0" } }, "sha512-r82dRWU9PMimHvHEzL0DwEJrzFk+SMCVfq249SLt3I7EFez7R+jeoKQd+M1//QcnjqlXPs2am4DFsGk8/GCsrA=="],
54685437
54695438 "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
54705439
54715440 "sql-highlight": ["sql-highlight@6.1.0", "", {}, "sha512-ed7OK4e9ywpE7pgRMkMQmZDPKSVdm0oX5IEtZiKnFucSF0zu6c80GZBe38UqHuVhTWJ9xsKgSMjCG2bml86KvA=="],
54725441
5473− "sql.js": ["sql.js@1.14.1", "", {}, "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A=="],
5474−
54755442 "srvx": ["srvx@0.11.15", "", { "bin": { "srvx": "bin/srvx.mjs" } }, "sha512-iXsux0UcOjdvs0LCMa2Ws3WwcDUozA3JN3BquNXkaFPP7TpRqgunKdEgoZ/uwb1J6xaYHfxtz9Twlh6yzwM6Tg=="],
54765443
54775444 "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
59025869
59035870 "@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=="],
59045871
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−
59095872 "@babel/core/@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="],
59105873
59115874 "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -6156,20 +6119,12 @@ · 244 unmodified lines
61566119
61576120 "@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=="],
61586121
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−
61616122 "@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=="],
61626123
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−
61656124 "@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=="],
61666125
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−
61696126 "@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=="],
61706127
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−
61736128 "@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=="],
61746129
61756130 "@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
62046159
62056160 "@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=="],
62066161
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−
62096162 "@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=="],
62106163
62116164 "@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=="],
62126165
62136166 "@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=="],
62146167
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=="],
62186169
62196170 "@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=="],
62206171
62216172 "@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=="],
62226173
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−
62256174 "@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=="],
62266175
62276176 "@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
68406789
68416790 "@astrojs/react/@vitejs/plugin-react/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.3", "", {}, "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q=="],
68426791
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−
68476792 "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@8.0.0", "", {}, "sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg=="],
68486793
68496794 "@babel/helper-annotate-as-pure/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@8.0.4", "", {}, "sha512-4wFaiLd0bVo4cIoTXI3zKI038NIWE/cr3jvBjejOVYVxV/m8Ltav1USiGzG1fmS5J2RhgEOgXNNK46cRPnRsrg=="],