From 45a95e5a71c095dbf28ddad7fc70cd7855d8b487 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 18 Jun 2026 11:31:01 +0530 Subject: [PATCH 1/4] feat: OAuth2 server consent & device verification screens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the 'Sign in with Appwrite' identity-provider UI for the Console OAuth2 server — the two user-facing screens behind the new server env vars: - _APP_CONSOLE_OAUTH2_AUTHORIZATION_URL -> /oauth2/consent - _APP_CONSOLE_OAUTH2_VERIFICATION_URL -> /oauth2/device Consent (/oauth2/consent): authorization-code flow. Reads the grant, shows the requesting app's branding and what authorizing grants, and lets the user authorize or cancel. Approve redirects back to the client with a code; cancel returns access_denied. The permission list leads with full account access rather than the requested OIDC scopes, since console-project tokens receive the full users (member) role regardless of scopes. Device (/oauth2/device): Device Authorization Grant (RFC 8628). When opened via verification_uri_complete the code is rendered for the user to confirm it matches their device (no silent auto-submit); otherwise they type it in. After confirming, the same consent card is shown (with device-specific copy), then a terminal success state. SDK: bump @appwrite.io/console pin to 1a5604f which adds the Oauth2 grant service and Models.Oauth2Grant; wire Apps + Oauth2 into sdk.forConsole. Login: honor the redirect query param after email login (MFA-safe) so OAuth2 flows resume after sign-in, aligning login with register. --- bun.lock | 4 +- package.json | 2 +- src/lib/actions/analytics.ts | 3 + src/lib/helpers/oauth2-scopes.ts | 90 +++++ src/lib/stores/sdk.ts | 4 + .../(public)/(guest)/login/+page.svelte | 28 ++ src/routes/(public)/oauth2/+layout.svelte | 62 +++ .../(public)/oauth2/consent-card.svelte | 355 ++++++++++++++++++ .../(public)/oauth2/consent/+page.svelte | 209 +++++++++++ .../(public)/oauth2/device/+page.svelte | 317 ++++++++++++++++ 10 files changed, 1071 insertions(+), 3 deletions(-) create mode 100644 src/lib/helpers/oauth2-scopes.ts create mode 100644 src/routes/(public)/oauth2/+layout.svelte create mode 100644 src/routes/(public)/oauth2/consent-card.svelte create mode 100644 src/routes/(public)/oauth2/consent/+page.svelte create mode 100644 src/routes/(public)/oauth2/device/+page.svelte diff --git a/bun.lock b/bun.lock index cb48bc3826..4b8c853104 100644 --- a/bun.lock +++ b/bun.lock @@ -6,7 +6,7 @@ "name": "@appwrite/console", "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@82d2831", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@1a5604f", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", @@ -125,7 +125,7 @@ "@analytics/type-utils": ["@analytics/type-utils@0.6.4", "", {}, "sha512-Ou1gQxFakOWLcPnbFVsrPb8g1wLLUZYYJXDPjHkG07+5mustGs5yqACx42UAu4A6NszNN6Z5gGxhyH45zPWRxw=="], - "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@82d2831", { "dependencies": { "json-bigint": "1.0.0" } }], + "@appwrite.io/console": ["@appwrite.io/console@https://pkg.vc/-/@appwrite/@appwrite.io/console@1a5604f", { "dependencies": { "json-bigint": "1.0.0" } }], "@appwrite.io/pink-icons": ["@appwrite.io/pink-icons@0.25.0", "", {}, "sha512-0O3i2oEuh5mWvjO80i+X6rbzrWLJ1m5wmv2/M3a1p2PyBJsFxN8xQMTEmTn3Wl/D26SsM7SpzbdW6gmfgoVU9Q=="], diff --git a/package.json b/package.json index 03424e186d..27db47fd99 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ }, "dependencies": { "@ai-sdk/svelte": "^1.1.24", - "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@82d2831", + "@appwrite.io/console": "https://pkg.vc/-/@appwrite/@appwrite.io/console@1a5604f", "@appwrite.io/pink-icons": "0.25.0", "@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@bfe7ce3", "@appwrite.io/pink-legacy": "^1.0.3", diff --git a/src/lib/actions/analytics.ts b/src/lib/actions/analytics.ts index 3a8d89622a..e7e014de9e 100644 --- a/src/lib/actions/analytics.ts +++ b/src/lib/actions/analytics.ts @@ -225,6 +225,9 @@ export enum Submit { AccountRecoveryCodesCreate = 'submit_account_recovery_codes_create', AccountRecoveryCodesUpdate = 'submit_account_recovery_codes_update', AccountDeleteIdentity = 'submit_account_delete_identity', + AccountOAuth2ConsentApprove = 'submit_account_oauth2_consent_approve', + AccountOAuth2ConsentDeny = 'submit_account_oauth2_consent_deny', + AccountOAuth2DeviceVerify = 'submit_account_oauth2_device_verify', FeedbackSubmit = 'submit_leave_feedback', FilterClear = 'submit_clear_filter', FilterApply = 'submit_filter_apply', diff --git a/src/lib/helpers/oauth2-scopes.ts b/src/lib/helpers/oauth2-scopes.ts new file mode 100644 index 0000000000..6207316593 --- /dev/null +++ b/src/lib/helpers/oauth2-scopes.ts @@ -0,0 +1,90 @@ +import type { ComponentType } from 'svelte'; +import { + IconShieldCheck, + IconUser, + IconMail, + IconIdentification, + IconKey +} from '@appwrite.io/pink-icons-svelte'; + +export interface ScopeDescriptor { + id: string; + title: string; + description: string; + icon: ComponentType; +} + +/** + * This consent screen always authorizes against the Appwrite **console** + * project. On the server, any OAuth2 access token issued for the console + * project is granted the full `users` (member) role — the same access a + * signed-in console session has — regardless of the OIDC scopes requested. + * The `openid`/`profile`/`email` scopes only shape the OIDC identity claims; + * they do NOT limit what the application can do. So the consent screen must + * lead with the full-access reality rather than implying read-only access. + */ +export const FULL_ACCESS_SCOPE: ScopeDescriptor = { + id: '__full_access__', + title: 'Full access to your account', + description: 'Manage your organizations, projects, and all their resources on your behalf.', + icon: IconShieldCheck +}; + +const BUILTIN_SCOPES: Record> = { + openid: { + title: 'Verify your identity', + description: 'Confirm who you are using your Appwrite account.', + icon: IconIdentification + }, + profile: { + title: 'View your profile', + description: 'Read your name and profile details.', + icon: IconUser + }, + email: { + title: 'View your email address', + description: 'Read the email address associated with your account.', + icon: IconMail + } +}; + +function titleizeScope(scope: string): string { + const cleaned = scope.replace(/[._:-]+/g, ' ').trim(); + if (!cleaned) return scope; + return cleaned.charAt(0).toUpperCase() + cleaned.slice(1); +} + +export function describeScope(scope: string): ScopeDescriptor { + const builtin = BUILTIN_SCOPES[scope]; + if (builtin) { + return { id: scope, ...builtin }; + } + return { + id: scope, + title: titleizeScope(scope), + description: `Access to ${scope}.`, + icon: IconKey + }; +} + +export function describeScopes(scopes: string[]): ScopeDescriptor[] { + return scopes.map(describeScope); +} + +// Identity scopes shown (in this order) as secondary detail beneath the +// full-access item. `openid` is intentionally omitted — identity verification +// is implied by full account access, so listing it separately is redundant. +const CONSENT_IDENTITY_SCOPES = ['profile', 'email'] as const; + +/** + * Build the permission list for the console OAuth2 consent screen. Always leads + * with the full-access item (the true effect of authorizing), followed by the + * identity scopes the application actually reads (profile, email) when present. + */ +export function describeConsentScopes(scopes: string[]): ScopeDescriptor[] { + const requested = new Set(scopes); + const identity = CONSENT_IDENTITY_SCOPES.filter((scope) => requested.has(scope)).map( + describeScope + ); + return [FULL_ACCESS_SCOPE, ...identity]; +} diff --git a/src/lib/stores/sdk.ts b/src/lib/stores/sdk.ts index ddfc39d062..cdea8ebd19 100644 --- a/src/lib/stores/sdk.ts +++ b/src/lib/stores/sdk.ts @@ -2,6 +2,7 @@ import { isMultiRegionSupported, VARS } from '$lib/system'; import { registerImpersonationClients, restoreImpersonation } from '$lib/appwrite/impersonation'; import { Account, + Apps, Assistant, Avatars, Backups, @@ -12,6 +13,7 @@ import { Locale, Messaging, Migrations, + Oauth2, Organization, Project, Project as ProjectApi, @@ -48,6 +50,8 @@ function createConsoleSdk(client: Client) { return { client, account: new Account(client), + apps: new Apps(client), + oauth2: new Oauth2(client), avatars: new Avatars(client), functions: new Functions(client), health: new Health(client), diff --git a/src/routes/(public)/(guest)/login/+page.svelte b/src/routes/(public)/(guest)/login/+page.svelte index e45ae0b080..ef0b9f4657 100644 --- a/src/routes/(public)/(guest)/login/+page.svelte +++ b/src/routes/(public)/(guest)/login/+page.svelte @@ -62,6 +62,34 @@ return; } + // Honor the `redirect` query param so OAuth2 consent/device flows + // (and other deep links) resume after email login. MFA-safe: if + // additional factors are required, fall through to invalidate(ACCOUNT) + // so the root layout routes to /mfa carrying the redirect param from + // the current /login URL. + const redirect = page.url.searchParams.get('redirect'); + if (redirect) { + try { + await sdk.forConsole.account.get(); + page.url.searchParams.delete('redirect'); + await goto(`${redirect}${page.url.search}`); + await invalidate(Dependencies.ACCOUNT); + return; + } catch (mfaError) { + if (mfaError?.type !== 'user_more_factors_required') { + addNotification({ + type: 'error', + message: mfaError.message + }); + trackError(mfaError, Submit.AccountLogin); + disabled = false; + return; + } + // MFA required: fall through so the root layout redirects to + // /mfa with the redirect param preserved in the URL. + } + } + // no specific redirect, so redirect will happen through invalidating the account await invalidate(Dependencies.ACCOUNT); } catch (error) { diff --git a/src/routes/(public)/oauth2/+layout.svelte b/src/routes/(public)/oauth2/+layout.svelte new file mode 100644 index 0000000000..da38c1dc4b --- /dev/null +++ b/src/routes/(public)/oauth2/+layout.svelte @@ -0,0 +1,62 @@ + + +
+
+
+ +
+
+
+ POWERED BY + {#if $app.themeInUse === 'dark'} + Appwrite Logo + {:else} + Appwrite Logo + {/if} +
+
+ + diff --git a/src/routes/(public)/oauth2/consent-card.svelte b/src/routes/(public)/oauth2/consent-card.svelte new file mode 100644 index 0000000000..d81549accf --- /dev/null +++ b/src/routes/(public)/oauth2/consent-card.svelte @@ -0,0 +1,355 @@ + + + + + + {#if app.logoUri} + + {:else} +
+ {appInitial} +
+ {/if} + + + Authorize {app.name} + + + {app.tagline || `${app.name} wants to access your Appwrite account.`} + + +
+ + + + This will allow {app.name} to + +
    + {#each scopes as scope (scope.id)} +
  • + + + + + {scope.title} + {scope.description} + +
  • + {/each} +
+
+ + {#if details.length > 0} + + + Requested resources + +
    + {#each details as detail, i (`${detail.type}-${i}`)} +
  • + + {detail.type} +
  • + {/each} +
+
+ {/if} + + {#if error} +
+ + + {error} + +
+ {/if} + +
+ + + + +
+ + + {#if accountLabel} + Signed in as {accountLabel}.{' '} + {/if} + {#if flow === 'authorization' && redirectHost} + You'll be redirected to {redirectHost}. + {/if} + {#if flow === 'device'} + After authorizing, return to your device. + {/if} + + + {#if app.privacyPolicyUrl || app.termsUrl} + + {#if app.privacyPolicyUrl} + + Privacy Policy + + + {/if} + {#if app.privacyPolicyUrl && app.termsUrl} + {' · '} + {/if} + {#if app.termsUrl} + + Terms of Service + + + {/if} + + {/if} +
+
+ + diff --git a/src/routes/(public)/oauth2/consent/+page.svelte b/src/routes/(public)/oauth2/consent/+page.svelte new file mode 100644 index 0000000000..d06d86dbe9 --- /dev/null +++ b/src/routes/(public)/oauth2/consent/+page.svelte @@ -0,0 +1,209 @@ + + + + Authorize application - Appwrite + + + + + diff --git a/src/routes/(public)/oauth2/device/+page.svelte b/src/routes/(public)/oauth2/device/+page.svelte new file mode 100644 index 0000000000..43ee89b25b --- /dev/null +++ b/src/routes/(public)/oauth2/device/+page.svelte @@ -0,0 +1,317 @@ + + + + Connect a device - Appwrite + + +
+
+ {#if phase === 'loading'} +
+ +
+ {:else if phase === 'enter-code'} + +
+ + +
+ +
+ + + {hasPrefilledCode ? 'Confirm your code' : 'Connect a device'} + + + {hasPrefilledCode + ? 'Make sure this matches the code shown on your device, then continue.' + : 'Enter the code shown on your device to continue.'} + + +
+ + + + { + code = normalizeUserCode(e.currentTarget.value); + error = null; + }} + placeholder="XXXXXXXX" + autofocus + autocomplete="off" + autocapitalize="characters" + spellcheck="false" + maxlength={12} + disabled={submitting} + class="code-input" /> + {#if error} + + {error} + + {/if} + + + + + {#if account} + + Signed in as + {account.email || account.name}. + + {/if} +
+
+
+ {:else if phase === 'consent' && grant && app} + + {:else if phase === 'approved'} + + +
+ +
+ Device connected + + You've authorized {app?.name ?? 'the application'}. You can return to your + device — it will continue automatically. + +
+
+ {:else if phase === 'denied'} + + +
+ +
+ Request cancelled + + No access was granted. You can close this page. + +
+
+ {/if} +
+
+ + From bce070ca4ddb3d4be7670f2f868cdf52da2f00ee Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 18 Jun 2026 12:35:33 +0530 Subject: [PATCH 2/4] fix: prevent OAuth2 consent/device card from collapsing The oauth2 layout wrapped its slot in the generic .console-container class, which as a flex item in the row-flex section with no definite width shrank to min-content, collapsing the card (and its width:100% descendants) into a narrow sliver. Use a dedicated .oauth2-shell with a fixed max-width and make the section stretch and center its content. --- src/routes/(public)/oauth2/+layout.svelte | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/routes/(public)/oauth2/+layout.svelte b/src/routes/(public)/oauth2/+layout.svelte index da38c1dc4b..39bc61b821 100644 --- a/src/routes/(public)/oauth2/+layout.svelte +++ b/src/routes/(public)/oauth2/+layout.svelte @@ -9,7 +9,7 @@
-
+
@@ -47,8 +47,15 @@ justify-content: space-between; section { flex: 1; + width: 100%; display: flex; align-items: center; + justify-content: center; + padding-inline: 1rem; + } + .oauth2-shell { + width: 100%; + max-width: 28rem; } footer { padding: 2rem 1rem; From 7bcc6e9d210e4f3b2ee06b1463074065b4264a37 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 18 Jun 2026 12:40:55 +0530 Subject: [PATCH 3/4] fix: harden OAuth2 consent/device flows against stale requests Addresses code review findings on the OAuth2 screens: - consent: re-run the load effect on ANY authorize param change (full query string), not just grant_id/client_id, so a same-client request with a new redirect_uri/state/scope can't approve the previous grant - consent: validate max_age as a non-negative integer, omitting it instead of forwarding NaN to the SDK - consent-card: pin approve/reject to the grant id captured at call time and drop the result (and errors) if the parent swaps in a different grant mid-flight - device: track only the URL user_code and reset loaded grant/app/phase when it changes or is removed, so stale device requests can't be confirmed - device: ignore createGrant results/errors when the active code changed while awaiting, and guard against duplicate concurrent submits - login: resume to the stored redirect exactly instead of appending the login page's leftover query params (e.g. message=) to the OAuth2 route --- .../(public)/(guest)/login/+page.svelte | 7 +++-- .../(public)/oauth2/consent-card.svelte | 15 +++++++-- .../(public)/oauth2/consent/+page.svelte | 18 ++++++++--- .../(public)/oauth2/device/+page.svelte | 31 ++++++++++++------- 4 files changed, 52 insertions(+), 19 deletions(-) diff --git a/src/routes/(public)/(guest)/login/+page.svelte b/src/routes/(public)/(guest)/login/+page.svelte index ef0b9f4657..aca660b828 100644 --- a/src/routes/(public)/(guest)/login/+page.svelte +++ b/src/routes/(public)/(guest)/login/+page.svelte @@ -71,8 +71,11 @@ if (redirect) { try { await sdk.forConsole.account.get(); - page.url.searchParams.delete('redirect'); - await goto(`${redirect}${page.url.search}`); + // Resume to the stored redirect exactly — it already carries + // its own query string. Appending the login page's remaining + // search params would leak login-only params (e.g. `message`) + // into the OAuth2 request route. + await goto(redirect); await invalidate(Dependencies.ACCOUNT); return; } catch (mfaError) { diff --git a/src/routes/(public)/oauth2/consent-card.svelte b/src/routes/(public)/oauth2/consent-card.svelte index d81549accf..d9eabec4f6 100644 --- a/src/routes/(public)/oauth2/consent-card.svelte +++ b/src/routes/(public)/oauth2/consent-card.svelte @@ -61,12 +61,18 @@ async function approve() { if (busy) return; + // Pin the request we're acting on. If the parent swaps in a different + // grant while this call is in flight (the route moved to another + // consent request), drop the stale result so we never redirect with the + // previous grant's redirectUrl. + const grantId = grant.$id; error = null; approving = true; try { const result = await sdk.forConsole.oauth2.approve({ - grantId: grant.$id + grantId }); + if (grant.$id !== grantId) return; trackEvent(Submit.AccountOAuth2ConsentApprove, { app_id: grant.appId, flow @@ -77,6 +83,7 @@ } window.location.href = result.redirectUrl; } catch (e: unknown) { + if (grant.$id !== grantId) return; const message = (e as Error)?.message ?? 'Failed to authorize the application'; error = message; addNotification({ type: 'error', message }); @@ -88,12 +95,15 @@ async function reject() { if (busy) return; + // Pin the request we're acting on (see `approve`). + const grantId = grant.$id; error = null; rejecting = true; try { const result = await sdk.forConsole.oauth2.reject({ - grantId: grant.$id + grantId }); + if (grant.$id !== grantId) return; trackEvent(Submit.AccountOAuth2ConsentDeny, { app_id: grant.appId, flow @@ -104,6 +114,7 @@ } window.location.href = result.redirectUrl; } catch (e: unknown) { + if (grant.$id !== grantId) return; const message = (e as Error)?.message ?? 'Failed to cancel the request'; error = message; addNotification({ type: 'error', message }); diff --git a/src/routes/(public)/oauth2/consent/+page.svelte b/src/routes/(public)/oauth2/consent/+page.svelte index d06d86dbe9..829362e2cd 100644 --- a/src/routes/(public)/oauth2/consent/+page.svelte +++ b/src/routes/(public)/oauth2/consent/+page.svelte @@ -17,13 +17,23 @@ let account = $state | null>(null); let error = $state(null); + // OIDC `max_age` is a non-negative integer count of seconds. Reject anything + // else (e.g. `max_age=abc`) so we omit the param rather than forwarding NaN. + function parseMaxAge(raw: string | null): number | undefined { + if (!raw) return undefined; + const value = Number(raw); + return Number.isInteger(value) && value >= 0 ? value : undefined; + } + // Re-runs when the authorize params change (this route can stay mounted as // the router moves between requests). Reset to loading so a previously // loaded grant can never be approved against a different request. $effect(() => { - // Touch the search params so the effect re-runs on URL change. - page.url.searchParams.get('grant_id'); - page.url.searchParams.get('client_id'); + // Depend on the ENTIRE query string so the effect re-runs whenever any + // part of the authorize request changes — not just grant_id/client_id, + // but also redirect_uri, scope, state, nonce, PKCE fields, prompt, + // max_age and authorization_details. + page.url.searchParams.toString(); let cancelled = false; phase = 'loading'; @@ -106,7 +116,7 @@ codeChallenge: params.get('code_challenge') ?? undefined, codeChallengeMethod: params.get('code_challenge_method') ?? undefined, prompt: params.get('prompt') ?? undefined, - maxAge: params.get('max_age') ? Number(params.get('max_age')) : undefined, + maxAge: parseMaxAge(params.get('max_age')), authorizationDetails: params.get('authorization_details') ?? undefined }); if (cancelled) return; diff --git a/src/routes/(public)/oauth2/device/+page.svelte b/src/routes/(public)/oauth2/device/+page.svelte index 43ee89b25b..cc06f4bad9 100644 --- a/src/routes/(public)/oauth2/device/+page.svelte +++ b/src/routes/(public)/oauth2/device/+page.svelte @@ -68,24 +68,28 @@ }; }); - // Keep the shown code in sync when a new `user_code` arrives in the URL - // (e.g. the user follows a fresh `verification_uri_complete` link while this - // route stays mounted). A different code means a different request, so drop - // any loaded grant and return to confirmation rather than approving the old - // one. + // Sync state to the `user_code` in the URL. This tracks ONLY the URL param + // (not the locally-typed `code`), so typing in the field never re-triggers + // it. Whenever the URL code changes — including being removed — the previous + // request is no longer valid, so drop any loaded grant and return to + // confirmation rather than confirming/approving a stale one. + let lastUrlCode = $state(null); $effect(() => { - page.url.searchParams.get('user_code'); - const next = normalizeUserCode(page.url.searchParams.get('user_code') ?? ''); - if (!next || next === code) return; - code = next; + const urlCode = normalizeUserCode(page.url.searchParams.get('user_code') ?? ''); + if (urlCode === lastUrlCode) return; + lastUrlCode = urlCode; + code = urlCode; grant = null; app = null; error = null; - hasPrefilledCode = Boolean(next); - phase = phase === 'loading' ? phase : 'enter-code'; + hasPrefilledCode = Boolean(urlCode); + if (phase !== 'loading') phase = 'enter-code'; }); async function handleSubmit() { + // Guard against a double Enter / programmatic resubmit racing or + // invalidating the in-flight request. + if (submitting) return; const normalized = normalizeUserCode(code); if (!normalized) return; error = null; @@ -97,6 +101,9 @@ const loadedApp = await sdk.forConsole.apps.get({ appId: loadedGrant.appId }); + // A fresh `user_code` may have arrived while we awaited. Ignore this + // now-stale result so we never show consent for a superseded request. + if (normalizeUserCode(code) !== normalized) return; trackEvent(Submit.AccountOAuth2DeviceVerify, { app_id: loadedGrant.appId }); @@ -105,6 +112,8 @@ error = null; phase = 'consent'; } catch (e: unknown) { + // Drop errors from a submission the active code has already moved past. + if (normalizeUserCode(code) !== normalized) return; if (e instanceof AppwriteException && e.type === 'oauth2_invalid_user_code') { error = 'That code is invalid or has expired. Check your device and try again.'; } else { From cd81387798ccebdf810db678d887bdd95644b048 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Thu, 18 Jun 2026 12:47:03 +0530 Subject: [PATCH 4/4] fix: pin ws to ^8.21.0 to clear high-severity audit advisory bun audit (CI gate) flagged ws 8.19.0 (transitive via jsdom) for GHSA-96hv-2xvq-fx4p. Add an overrides pin to 8.21.0, matching the repo's existing security-pin convention. --- bun.lock | 3 ++- package.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/bun.lock b/bun.lock index 4b8c853104..15f8662cef 100644 --- a/bun.lock +++ b/bun.lock @@ -96,6 +96,7 @@ "minimatch": "10.2.3", "picomatch": "^4.0.4", "vite": "npm:rolldown-vite@latest", + "ws": "^8.21.0", "yaml": "^1.10.3", }, "packages": { @@ -1463,7 +1464,7 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "xml-name-validator": ["xml-name-validator@5.0.0", "", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="], diff --git a/package.json b/package.json index 27db47fd99..643ba8db43 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ "devalue": "^5.8.1", "yaml": "^1.10.3", "picomatch": "^4.0.4", - "cookie": "^0.7.0" + "cookie": "^0.7.0", + "ws": "^8.21.0" } }