From b9f6c3ee6ccb1ba097b078db7c8515de45406690 Mon Sep 17 00:00:00 2001 From: RedStar Date: Tue, 11 Aug 2026 19:30:29 +0200 Subject: [PATCH 01/12] refactor(dashboard): group app code into supastarter-style feature modules Reorganizes apps/dashboard/app into modules/{auth,dashboard,shared}, moving components, composables, types and utils under the module that owns them, and turns pages/ into a thin routing layer using (auth) and (dashboard) route groups so the URLs stay /login and /. Component tag names and composable call sites are unchanged: nuxt.config.ts now registers the three module component roots explicitly, so each file keeps the auto-import name it had under the default app/components scan. The former package-root shared/ directory is folded into modules/shared/utils. It existed for Nuxt's app-plus-server sharing convention, but the dashboard owns no server, so the #shared alias is dropped from vitest, knip and tsconfig. --- apps/dashboard/app/error.vue | 2 +- .../{ => modules/auth}/components/UserMenu.vue | 0 .../auth}/composables/useAuthErrorMessage.ts | 0 .../dashboard}/components/RunnerMetrics.vue | 2 +- .../dashboard}/components/task/Inspector.vue | 2 +- .../dashboard}/components/task/Status.vue | 2 +- .../dashboard}/components/task/Table.vue | 2 +- .../dashboard}/components/task/Timeline.vue | 2 +- .../{ => modules/dashboard}/types/dashboard.ts | 0 .../shared}/components/ColorModeToggle.vue | 0 .../{ => modules/shared}/components/ErrorPage.vue | 2 +- .../shared}/components/LocaleSwitcher.vue | 0 .../shared}/components/app/Sidebar.vue | 0 .../shared}/composables/useSidebarCollapsed.ts | 0 .../modules}/shared/utils/error-status.ts | 0 apps/dashboard/app/pages/{ => (auth)}/login.vue | 0 .../app/pages/{ => (dashboard)}/index.vue | 2 +- apps/dashboard/app/plugins/.gitkeep | 0 apps/dashboard/app/types/.gitkeep | 0 apps/dashboard/nuxt.config.ts | 15 +++++++++++++++ .../test/nuxt/components/LocaleSwitcher.spec.ts | 2 +- .../test/nuxt/components/UserMenu.spec.ts | 2 +- .../nuxt/composables/useAuthErrorMessage.spec.ts | 2 +- apps/dashboard/test/nuxt/pages/login.spec.ts | 2 +- apps/dashboard/test/unit/error-status.test.ts | 2 +- apps/dashboard/tsconfig.json | 1 - apps/dashboard/vitest.config.ts | 3 +-- knip.jsonc | 9 ++++----- 28 files changed, 33 insertions(+), 21 deletions(-) rename apps/dashboard/app/{ => modules/auth}/components/UserMenu.vue (100%) rename apps/dashboard/app/{ => modules/auth}/composables/useAuthErrorMessage.ts (100%) rename apps/dashboard/app/{ => modules/dashboard}/components/RunnerMetrics.vue (96%) rename apps/dashboard/app/{ => modules/dashboard}/components/task/Inspector.vue (97%) rename apps/dashboard/app/{ => modules/dashboard}/components/task/Status.vue (91%) rename apps/dashboard/app/{ => modules/dashboard}/components/task/Table.vue (97%) rename apps/dashboard/app/{ => modules/dashboard}/components/task/Timeline.vue (95%) rename apps/dashboard/app/{ => modules/dashboard}/types/dashboard.ts (100%) rename apps/dashboard/app/{ => modules/shared}/components/ColorModeToggle.vue (100%) rename apps/dashboard/app/{ => modules/shared}/components/ErrorPage.vue (97%) rename apps/dashboard/app/{ => modules/shared}/components/LocaleSwitcher.vue (100%) rename apps/dashboard/app/{ => modules/shared}/components/app/Sidebar.vue (100%) rename apps/dashboard/app/{ => modules/shared}/composables/useSidebarCollapsed.ts (100%) rename apps/dashboard/{ => app/modules}/shared/utils/error-status.ts (100%) rename apps/dashboard/app/pages/{ => (auth)}/login.vue (100%) rename apps/dashboard/app/pages/{ => (dashboard)}/index.vue (97%) create mode 100644 apps/dashboard/app/plugins/.gitkeep create mode 100644 apps/dashboard/app/types/.gitkeep diff --git a/apps/dashboard/app/error.vue b/apps/dashboard/app/error.vue index c1061a6..9dacd89 100644 --- a/apps/dashboard/app/error.vue +++ b/apps/dashboard/app/error.vue @@ -12,7 +12,7 @@ import { isNotFoundStatus, isServerErrorStatus, resolveErrorStatus, -} from '#shared/utils/error-status'; +} from '~/modules/shared/utils/error-status'; const { error } = defineProps<{ error: NuxtError; diff --git a/apps/dashboard/app/components/UserMenu.vue b/apps/dashboard/app/modules/auth/components/UserMenu.vue similarity index 100% rename from apps/dashboard/app/components/UserMenu.vue rename to apps/dashboard/app/modules/auth/components/UserMenu.vue diff --git a/apps/dashboard/app/composables/useAuthErrorMessage.ts b/apps/dashboard/app/modules/auth/composables/useAuthErrorMessage.ts similarity index 100% rename from apps/dashboard/app/composables/useAuthErrorMessage.ts rename to apps/dashboard/app/modules/auth/composables/useAuthErrorMessage.ts diff --git a/apps/dashboard/app/components/RunnerMetrics.vue b/apps/dashboard/app/modules/dashboard/components/RunnerMetrics.vue similarity index 96% rename from apps/dashboard/app/components/RunnerMetrics.vue rename to apps/dashboard/app/modules/dashboard/components/RunnerMetrics.vue index c3016d2..a515fd6 100644 --- a/apps/dashboard/app/components/RunnerMetrics.vue +++ b/apps/dashboard/app/modules/dashboard/components/RunnerMetrics.vue @@ -28,7 +28,7 @@ diff --git a/apps/dashboard/app/types/dashboard.ts b/apps/dashboard/app/modules/dashboard/types/dashboard.ts similarity index 100% rename from apps/dashboard/app/types/dashboard.ts rename to apps/dashboard/app/modules/dashboard/types/dashboard.ts diff --git a/apps/dashboard/app/components/ColorModeToggle.vue b/apps/dashboard/app/modules/shared/components/ColorModeToggle.vue similarity index 100% rename from apps/dashboard/app/components/ColorModeToggle.vue rename to apps/dashboard/app/modules/shared/components/ColorModeToggle.vue diff --git a/apps/dashboard/app/components/ErrorPage.vue b/apps/dashboard/app/modules/shared/components/ErrorPage.vue similarity index 97% rename from apps/dashboard/app/components/ErrorPage.vue rename to apps/dashboard/app/modules/shared/components/ErrorPage.vue index 28f1466..ce85a6f 100644 --- a/apps/dashboard/app/components/ErrorPage.vue +++ b/apps/dashboard/app/modules/shared/components/ErrorPage.vue @@ -28,7 +28,7 @@ import { isNotFoundStatus, isServerErrorStatus, resolveErrorStatus, -} from '#shared/utils/error-status'; +} from '~/modules/shared/utils/error-status'; const { error } = defineProps<{ error: NuxtError; diff --git a/apps/dashboard/app/components/LocaleSwitcher.vue b/apps/dashboard/app/modules/shared/components/LocaleSwitcher.vue similarity index 100% rename from apps/dashboard/app/components/LocaleSwitcher.vue rename to apps/dashboard/app/modules/shared/components/LocaleSwitcher.vue diff --git a/apps/dashboard/app/components/app/Sidebar.vue b/apps/dashboard/app/modules/shared/components/app/Sidebar.vue similarity index 100% rename from apps/dashboard/app/components/app/Sidebar.vue rename to apps/dashboard/app/modules/shared/components/app/Sidebar.vue diff --git a/apps/dashboard/app/composables/useSidebarCollapsed.ts b/apps/dashboard/app/modules/shared/composables/useSidebarCollapsed.ts similarity index 100% rename from apps/dashboard/app/composables/useSidebarCollapsed.ts rename to apps/dashboard/app/modules/shared/composables/useSidebarCollapsed.ts diff --git a/apps/dashboard/shared/utils/error-status.ts b/apps/dashboard/app/modules/shared/utils/error-status.ts similarity index 100% rename from apps/dashboard/shared/utils/error-status.ts rename to apps/dashboard/app/modules/shared/utils/error-status.ts diff --git a/apps/dashboard/app/pages/login.vue b/apps/dashboard/app/pages/(auth)/login.vue similarity index 100% rename from apps/dashboard/app/pages/login.vue rename to apps/dashboard/app/pages/(auth)/login.vue diff --git a/apps/dashboard/app/pages/index.vue b/apps/dashboard/app/pages/(dashboard)/index.vue similarity index 97% rename from apps/dashboard/app/pages/index.vue rename to apps/dashboard/app/pages/(dashboard)/index.vue index 21277f3..fd137ad 100644 --- a/apps/dashboard/app/pages/index.vue +++ b/apps/dashboard/app/pages/(dashboard)/index.vue @@ -56,7 +56,7 @@ + + diff --git a/packages/mail/emails/OrganizationInvitation.vue b/packages/mail/emails/OrganizationInvitation.vue new file mode 100644 index 0000000..e0f92ea --- /dev/null +++ b/packages/mail/emails/OrganizationInvitation.vue @@ -0,0 +1,22 @@ + + + diff --git a/packages/mail/emails/PasswordReset.vue b/packages/mail/emails/PasswordReset.vue new file mode 100644 index 0000000..08b7d10 --- /dev/null +++ b/packages/mail/emails/PasswordReset.vue @@ -0,0 +1,23 @@ + + + diff --git a/packages/mail/emails/layouts/Base.vue b/packages/mail/emails/layouts/Base.vue new file mode 100644 index 0000000..8eb3340 --- /dev/null +++ b/packages/mail/emails/layouts/Base.vue @@ -0,0 +1,17 @@ + + + diff --git a/packages/mail/maizzle.config.ts b/packages/mail/maizzle.config.ts new file mode 100644 index 0000000..872616e --- /dev/null +++ b/packages/mail/maizzle.config.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@maizzle/framework'; + +/** + * Baseline render settings shared by every template. + * + * `sendEmail` merges the per-message context over this object, so anything declared here is + * available to templates through `useConfig()`. + */ +export default defineConfig({ + build: { + content: ['emails/**/*.vue'], + }, + css: { + inline: true, + purge: true, + }, +}); diff --git a/packages/mail/package.json b/packages/mail/package.json new file mode 100644 index 0000000..6950227 --- /dev/null +++ b/packages/mail/package.json @@ -0,0 +1,58 @@ +{ + "name": "@agent-zero/mail", + "version": "0.3.0", + "files": [ + "dist", + "emails" + ], + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.mts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "scripts": { + "build": "tsdown", + "clean": "tsc -b --clean", + "lint": "oxlint --config ../../.oxlintrc.json --type-aware --type-check src", + "test": "vitest run src --passWithNoTests", + "typecheck": "tsc --project tsconfig.json --pretty false --noEmit" + }, + "dependencies": { + "@maizzle/framework": "^6.0.13", + "@maizzle/tailwindcss": "^1.5.6" + }, + "devDependencies": { + "@types/nodemailer": "^8.0.1", + "nodemailer": "^9.0.5", + "oxlint": "^1.44.0", + "oxlint-tsgolint": "^7.0.2001", + "resend": "6.17.1", + "tsdown": "^0.22.14", + "typescript": "^5.9.2", + "vitest": "^3.2.4" + }, + "peerDependencies": { + "nodemailer": "^9.0.5", + "resend": "^6.17.1" + }, + "peerDependenciesMeta": { + "nodemailer": { + "optional": true + }, + "resend": { + "optional": true + } + } +} diff --git a/packages/mail/src/index.ts b/packages/mail/src/index.ts new file mode 100644 index 0000000..1088bee --- /dev/null +++ b/packages/mail/src/index.ts @@ -0,0 +1,20 @@ +export { + createMailer, + sendEmail, + type MailerOptions, + type SendEmail, + type SendEmailOptions, +} from './mail.js'; +export { + createConsoleProvider, + createResendProvider, + createSmtpProvider, + MAIL_PROVIDER_NAMES, + mailProviderFromEnvironment, + type MailProvider, + type MailProviderName, + type OutgoingMail, + type ResendProviderOptions, + type SmtpProviderOptions, +} from './provider/index.js'; +export { mailTemplates, type MailTemplateContext, type MailTemplateId } from './util/templates.js'; diff --git a/packages/mail/src/mail.test.ts b/packages/mail/src/mail.test.ts new file mode 100644 index 0000000..b4e16cd --- /dev/null +++ b/packages/mail/src/mail.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { createMailer, sendEmail } from './mail.js'; +import type { OutgoingMail } from './provider/types.js'; + +/** + * These render through the real Maizzle pipeline rather than a stub. Rendering is local and + * deterministic, and the failure this guards against — a template that compiles but drops its + * interpolated values — is invisible to a mocked renderer. + */ +function recordingProvider() { + const sent: OutgoingMail[] = []; + return { + sent, + provider: (mail: OutgoingMail) => { + sent.push(mail); + return Promise.resolve(); + }, + }; +} + +describe('sendEmail', () => { + it('renders the invitation template with its context in both HTML and plaintext', async () => { + const { sent, provider } = recordingProvider(); + + await sendEmail( + { + to: 'invitee@example.com', + templateId: 'organizationInvitation', + context: { + organizationName: 'Acme Ops', + inviterName: 'Dana', + acceptUrl: 'https://dashboard.example.com/accept?token=abc', + }, + }, + { provider, from: 'noreply@example.com' }, + ); + + expect(sent).toHaveLength(1); + const [mail] = sent as [OutgoingMail]; + expect(mail.to).toBe('invitee@example.com'); + expect(mail.from).toBe('noreply@example.com'); + expect(mail.subject).toBe('You have been invited to an organization'); + expect(mail.html).toContain('Acme Ops'); + expect(mail.html).toContain('Dana'); + expect(mail.html).toContain('https://dashboard.example.com/accept?token=abc'); + // Clients that refuse HTML still have to be able to act on the invitation. + expect(mail.text).toContain('Acme Ops'); + expect(mail.text).toContain('https://dashboard.example.com/accept?token=abc'); + }); + + it('inlines styles so the message survives clients that drop stylesheets', async () => { + const { sent, provider } = recordingProvider(); + + await sendEmail( + { + to: 'invitee@example.com', + templateId: 'passwordReset', + context: { name: 'Dana', resetUrl: 'https://dashboard.example.com/reset?token=abc' }, + }, + { provider, from: 'noreply@example.com' }, + ); + + const [mail] = sent as [OutgoingMail]; + expect(mail.html).toMatch(/style="/); + // Utility classes are inlined and purged; a leftover class attribute means the CSS step + // silently did nothing and the message would arrive unstyled. + expect(mail.html).not.toMatch(/class="/); + }); + + it('lets the caller override the registered subject', async () => { + const { sent, provider } = recordingProvider(); + + await sendEmail( + { + to: 'invitee@example.com', + templateId: 'emailVerification', + context: { name: 'Dana', verifyUrl: 'https://dashboard.example.com/verify?token=abc' }, + subject: 'Conferma il tuo indirizzo email', + }, + { provider, from: 'noreply@example.com' }, + ); + + expect((sent[0] as OutgoingMail).subject).toBe('Conferma il tuo indirizzo email'); + }); + + it('refuses to send without a sender address rather than inventing one', async () => { + const { provider } = recordingProvider(); + vi.stubEnv('MAIL_FROM', ''); + + await expect( + sendEmail( + { + to: 'invitee@example.com', + templateId: 'emailVerification', + context: { name: 'Dana', verifyUrl: 'https://dashboard.example.com/verify' }, + }, + { provider }, + ), + ).rejects.toThrow(/MAIL_FROM/); + + vi.unstubAllEnvs(); + }); +}); + +describe('createMailer', () => { + it('binds the provider and sender once for injection', async () => { + const { sent, provider } = recordingProvider(); + const mailer = createMailer({ provider, from: 'ops@example.com' }); + + await mailer({ + to: 'invitee@example.com', + templateId: 'emailVerification', + context: { name: 'Dana', verifyUrl: 'https://dashboard.example.com/verify?token=abc' }, + }); + + expect((sent[0] as OutgoingMail).from).toBe('ops@example.com'); + }); +}); diff --git a/packages/mail/src/mail.ts b/packages/mail/src/mail.ts new file mode 100644 index 0000000..c232b66 --- /dev/null +++ b/packages/mail/src/mail.ts @@ -0,0 +1,75 @@ +import { fileURLToPath } from 'node:url'; + +import { render } from '@maizzle/framework'; + +import { mailProviderFromEnvironment } from './provider/index.js'; +import type { MailProvider } from './provider/types.js'; +import { mailTemplates, type MailTemplateContext, type MailTemplateId } from './util/templates.js'; + +/** Templates ship beside the compiled output, so resolve them relative to this module. */ +const emailsDirectory = fileURLToPath(new URL('../emails/', import.meta.url)); + +/** A message to deliver, addressed by template id rather than by file path. */ +export interface SendEmailOptions { + readonly to: string; + readonly templateId: Id; + /** Values the template interpolates. Typed per template by {@link MailTemplateContext}. */ + readonly context: MailTemplateContext[Id]; + /** Overrides the registered subject when a caller needs to localise it. */ + readonly subject?: string; +} + +/** Everything `sendEmail` needs that is not the message itself. */ +export interface MailerOptions { + /** Delivery backend. Defaults to the one selected by `MAIL_PROVIDER`. */ + readonly provider?: MailProvider; + /** Sender address. Defaults to `MAIL_FROM`. */ + readonly from?: string; +} + +function resolveFrom(from: string | undefined): string { + const address = from ?? process.env.MAIL_FROM?.trim(); + if (!address) throw new Error('missing required environment variable: MAIL_FROM'); + return address; +} + +/** + * Render a template and hand it to the configured provider. + * + * Rendering happens per send rather than at build time: the templates carry per-recipient tokens, + * so there is no reusable compiled artifact to cache, and Maizzle's pipeline (SSR, CSS inlining, + * plaintext) is what turns the Vue source into something a mail client renders. + */ +export async function sendEmail( + options: SendEmailOptions, + mailer: MailerOptions = {}, +): Promise { + const template = mailTemplates[options.templateId]; + const { html, plaintext } = await render(`${emailsDirectory}${template.file}`, { + ...options.context, + plaintext: true, + }); + + const provider = mailer.provider ?? mailProviderFromEnvironment(); + + await provider({ + to: options.to, + subject: options.subject ?? template.subject, + html, + text: plaintext ?? '', + from: resolveFrom(mailer.from), + }); +} + +/** + * Bind a provider and sender once. + * + * Composition roots build one of these at startup and inject it, which keeps the packages that + * need to send mail free of any dependency on this one. + */ +export function createMailer(mailer: MailerOptions = {}) { + return (options: SendEmailOptions) => sendEmail(options, mailer); +} + +/** The injectable shape a consumer depends on, so no caller has to import this package's internals. */ +export type SendEmail = ReturnType; diff --git a/packages/mail/src/provider/console.ts b/packages/mail/src/provider/console.ts new file mode 100644 index 0000000..a7dd8a7 --- /dev/null +++ b/packages/mail/src/provider/console.ts @@ -0,0 +1,16 @@ +import type { MailProvider, OutgoingMail } from './types.js'; + +/** + * Development provider: logs the message instead of delivering it. + * + * This is the default so that a deployment which has not configured a transport cannot silently + * attempt real delivery, and so the test suite never opens a socket. The body is deliberately not + * logged: invitation and password-reset messages carry single-use tokens in their links, and this + * output routinely lands in shared terminal scrollback and CI logs. + */ +export function createConsoleProvider(log: (message: string) => void = console.info): MailProvider { + return (mail: OutgoingMail) => { + log(`[mail] would deliver "${mail.subject}" from ${mail.from} to ${mail.to}`); + return Promise.resolve(); + }; +} diff --git a/packages/mail/src/provider/index.test.ts b/packages/mail/src/provider/index.test.ts new file mode 100644 index 0000000..2ed6775 --- /dev/null +++ b/packages/mail/src/provider/index.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { createConsoleProvider, mailProviderFromEnvironment } from './index.js'; + +/** + * Provider selection is deployment configuration, so the failure modes that matter are the + * misconfigured ones: they must fail loudly rather than fall back to a transport the operator did + * not choose. + */ +describe('mailProviderFromEnvironment', () => { + it('defaults to the console provider when nothing is configured', () => { + expect(() => mailProviderFromEnvironment({})).not.toThrow(); + }); + + it('rejects an unknown provider name instead of silently defaulting', () => { + expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'carrier-pigeon' })).toThrow( + /invalid MAIL_PROVIDER/, + ); + }); + + it('requires an API key before selecting Resend', () => { + expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'resend' })).toThrow( + /RESEND_API_KEY/, + ); + }); + + it('requires host and port before selecting SMTP', () => { + expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp' })).toThrow(/SMTP_HOST/); + expect(() => + mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp', SMTP_HOST: 'localhost' }), + ).toThrow(/SMTP_PORT/); + }); + + it('rejects a port that is not a whole number in range', () => { + for (const port of ['587abc', '0', '70000']) { + expect(() => + mailProviderFromEnvironment({ + MAIL_PROVIDER: 'smtp', + SMTP_HOST: 'localhost', + SMTP_PORT: port, + }), + ).toThrow(/SMTP_PORT/); + } + }); + + it('never echoes a credential in its error messages', () => { + // A thrown connection string or key routinely ends up in a crash log. + expect(() => + mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp', SMTP_PASSWORD: 'hunter2' }), + ).toThrow(expect.not.stringMatching(/hunter2/) as unknown as string); + }); +}); + +describe('createConsoleProvider', () => { + it('reports the message without logging its body', async () => { + const log = vi.fn(); + await createConsoleProvider(log)({ + to: 'operator@example.com', + subject: 'Reset your password', + html: 'Reset', + text: 'https://example.com/reset?token=secret-token', + from: 'noreply@example.com', + }); + + expect(log).toHaveBeenCalledOnce(); + const [message] = log.mock.calls[0] as [string]; + expect(message).toContain('operator@example.com'); + // Invitation and reset links are single-use credentials; they must not reach the log. + expect(message).not.toContain('secret-token'); + }); +}); diff --git a/packages/mail/src/provider/index.ts b/packages/mail/src/provider/index.ts new file mode 100644 index 0000000..1b4b451 --- /dev/null +++ b/packages/mail/src/provider/index.ts @@ -0,0 +1,75 @@ +import { createConsoleProvider } from './console.js'; +import { createResendProvider } from './resend.js'; +import { createSmtpProvider } from './smtp.js'; +import type { MailProvider } from './types.js'; + +export { createConsoleProvider } from './console.js'; +export { createResendProvider, type ResendProviderOptions } from './resend.js'; +export { createSmtpProvider, type SmtpProviderOptions } from './smtp.js'; +export type { MailProvider, OutgoingMail } from './types.js'; + +/** Transports a deployment can select through `MAIL_PROVIDER`. */ +export const MAIL_PROVIDER_NAMES = ['console', 'resend', 'smtp'] as const; + +export type MailProviderName = (typeof MAIL_PROVIDER_NAMES)[number]; + +function isProviderName(value: string): value is MailProviderName { + return (MAIL_PROVIDER_NAMES as readonly string[]).includes(value); +} + +/** Missing configuration is a deployment error; a default would send through the wrong transport. */ +function requireEnvironmentValue( + environment: Readonly>, + name: string, +): string { + const value = environment[name]?.trim(); + if (!value) throw new Error(`missing required environment variable: ${name}`); + return value; +} + +function requirePort( + environment: Readonly>, + name: string, +): number { + const raw = requireEnvironmentValue(environment, name); + // Validate the whole value: `Number.parseInt` would truncate `587abc` to a usable port. + if (!/^\d+$/u.test(raw)) throw new Error(`invalid ${name}: expected a port number`); + const port = Number.parseInt(raw, 10); + if (port < 1 || port > 65_535) throw new Error(`invalid ${name}: expected a port number`); + return port; +} + +/** + * Resolve the configured transport. + * + * The single place delivery backends are selected, so callers depend on {@link MailProvider} + * rather than on any one SDK. Defaults to the console provider: a deployment that has not chosen + * a transport should log rather than fail to deliver silently, and no error message here echoes a + * credential. + */ +export function mailProviderFromEnvironment( + environment: Readonly> = process.env, +): MailProvider { + const configured = environment.MAIL_PROVIDER?.trim() ?? 'console'; + if (!isProviderName(configured)) + throw new Error( + `invalid MAIL_PROVIDER: expected one of ${MAIL_PROVIDER_NAMES.join(', ')}, received ${configured}`, + ); + + switch (configured) { + case 'resend': + return createResendProvider({ + apiKey: requireEnvironmentValue(environment, 'RESEND_API_KEY'), + }); + case 'smtp': + return createSmtpProvider({ + host: requireEnvironmentValue(environment, 'SMTP_HOST'), + port: requirePort(environment, 'SMTP_PORT'), + secure: environment.SMTP_SECURE === 'true', + ...(environment.SMTP_USER?.trim() ? { user: environment.SMTP_USER.trim() } : {}), + ...(environment.SMTP_PASSWORD ? { password: environment.SMTP_PASSWORD } : {}), + }); + case 'console': + return createConsoleProvider(); + } +} diff --git a/packages/mail/src/provider/resend.ts b/packages/mail/src/provider/resend.ts new file mode 100644 index 0000000..0414e09 --- /dev/null +++ b/packages/mail/src/provider/resend.ts @@ -0,0 +1,31 @@ +import type { MailProvider, OutgoingMail } from './types.js'; + +/** Options the Resend transport needs. */ +export interface ResendProviderOptions { + readonly apiKey: string; +} + +/** + * Resend transport. + * + * `resend` is an optional peer dependency, so the module is imported lazily: a deployment using + * SMTP or the console provider must not need the package installed to boot. + */ +export function createResendProvider(options: ResendProviderOptions): MailProvider { + return async (mail: OutgoingMail) => { + const { Resend } = await import('resend'); + const client = new Resend(options.apiKey); + + const { error } = await client.emails.send({ + from: mail.from, + to: mail.to, + subject: mail.subject, + html: mail.html, + text: mail.text, + }); + + // The SDK reports delivery failures in the payload rather than by throwing, so an unchecked + // call would treat a rejected message as sent. + if (error) throw new Error(`resend rejected the message: ${error.message}`); + }; +} diff --git a/packages/mail/src/provider/smtp.ts b/packages/mail/src/provider/smtp.ts new file mode 100644 index 0000000..c7ee7c5 --- /dev/null +++ b/packages/mail/src/provider/smtp.ts @@ -0,0 +1,41 @@ +import type { MailProvider, OutgoingMail } from './types.js'; + +/** Connection details for a self-hosted SMTP relay. */ +export interface SmtpProviderOptions { + readonly host: string; + readonly port: number; + /** Implicit TLS. Conventionally true on 465 and false on 587, which upgrades via STARTTLS. */ + readonly secure: boolean; + readonly user?: string; + readonly password?: string; +} + +/** + * Nodemailer SMTP transport, for deployments that relay through their own infrastructure. + * + * `nodemailer` is an optional peer dependency and is imported lazily for the same reason as the + * Resend client: only the configured transport should have to be installed. + */ +export function createSmtpProvider(options: SmtpProviderOptions): MailProvider { + return async (mail: OutgoingMail) => { + const nodemailer = await import('nodemailer'); + const transport = nodemailer.createTransport({ + host: options.host, + port: options.port, + secure: options.secure, + // Anonymous relays are legitimate on an internal network, so credentials stay optional + // rather than being forced into a half-configured auth block. + ...(options.user && options.password + ? { auth: { user: options.user, pass: options.password } } + : {}), + }); + + await transport.sendMail({ + from: mail.from, + to: mail.to, + subject: mail.subject, + html: mail.html, + text: mail.text, + }); + }; +} diff --git a/packages/mail/src/provider/types.ts b/packages/mail/src/provider/types.ts new file mode 100644 index 0000000..eab5e49 --- /dev/null +++ b/packages/mail/src/provider/types.ts @@ -0,0 +1,27 @@ +/** + * The contract every delivery backend satisfies. + * + * Kept free of provider SDK types so that swapping Resend for SMTP is a configuration change + * rather than a change to callers, and so tests can substitute a recording provider without + * pulling a network client into the process. + */ + +/** A fully rendered message, ready for delivery. */ +export interface OutgoingMail { + readonly to: string; + readonly subject: string; + readonly html: string; + /** Plaintext alternative. Always populated by `sendEmail`, which renders it alongside the HTML. */ + readonly text: string; + /** Sender address. Resolved from `MAIL_FROM` unless the caller overrides it. */ + readonly from: string; +} + +/** + * Delivers a rendered message. + * + * Implementations must reject on failure rather than resolving quietly: a silently dropped + * password reset or organization invitation is indistinguishable from a delivered one, and the + * caller has no other signal to act on. + */ +export type MailProvider = (mail: OutgoingMail) => Promise; diff --git a/packages/mail/src/util/templates.ts b/packages/mail/src/util/templates.ts new file mode 100644 index 0000000..d5f4809 --- /dev/null +++ b/packages/mail/src/util/templates.ts @@ -0,0 +1,49 @@ +/** + * Template registry. + * + * Every message the product can send is declared here with its Maizzle template and subject, so + * that adding a template is one edit and callers address messages by id rather than by file path. + */ + +/** Data each template interpolates. Keyed by template id so `sendEmail` can check the context. */ +export interface MailTemplateContext { + readonly organizationInvitation: { + /** Display name of the organization the recipient is being invited to. */ + readonly organizationName: string; + /** Who sent the invitation, shown so the recipient can judge whether it is expected. */ + readonly inviterName: string; + /** Absolute URL that accepts the invitation. Carries a single-use token. */ + readonly acceptUrl: string; + }; + readonly emailVerification: { + readonly name: string; + readonly verifyUrl: string; + }; + readonly passwordReset: { + readonly name: string; + readonly resetUrl: string; + }; +} + +export type MailTemplateId = keyof MailTemplateContext; + +interface MailTemplateDefinition { + /** Path relative to this package's `emails/` directory. */ + readonly file: string; + readonly subject: string; +} + +export const mailTemplates: Readonly> = { + organizationInvitation: { + file: 'OrganizationInvitation.vue', + subject: 'You have been invited to an organization', + }, + emailVerification: { + file: 'EmailVerification.vue', + subject: 'Confirm your email address', + }, + passwordReset: { + file: 'PasswordReset.vue', + subject: 'Reset your password', + }, +}; diff --git a/packages/mail/tsconfig.json b/packages/mail/tsconfig.json new file mode 100644 index 0000000..5ee9c86 --- /dev/null +++ b/packages/mail/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { "rootDir": "src", "outDir": "dist" }, + "include": ["src/**/*.ts"] +} diff --git a/packages/mail/tsdown.config.ts b/packages/mail/tsdown.config.ts new file mode 100644 index 0000000..bcb566f --- /dev/null +++ b/packages/mail/tsdown.config.ts @@ -0,0 +1,3 @@ +import { definePackageConfig } from '../../scripts/tsdown.config.ts'; + +export default definePackageConfig(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37ebcc..0330d2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -651,6 +651,67 @@ importers: specifier: ^3.2.4 version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.2) + packages/mail: + dependencies: + '@arethetypeswrong/core': + specifier: ^0.18.1 + version: 0.18.5 + '@maizzle/framework': + specifier: ^6.0.13 + version: 6.0.13(shiki@4.4.3)(tailwind-merge@3.6.0)(vue@3.5.41) + '@maizzle/tailwindcss': + specifier: ^1.5.6 + version: 1.5.6 + '@types/debug': + specifier: ^4.1.12 + version: 4.1.13 + '@types/node': + specifier: ^18.0.0 || ^20.0.0 || >=22.0.0 + version: 24.13.3 + happy-dom: + specifier: '*' + version: 20.11.2 + publint: + specifier: ^0.3.8 + version: 0.3.23 + shiki: + specifier: ^1 || ^2 || ^3 || ^4 + version: 4.4.3 + tailwind-merge: + specifier: ^2 || ^3 + version: 3.6.0 + tsx: + specifier: '*' + version: 4.23.11 + vue: + specifier: ^3 + version: 3.5.41 + devDependencies: + '@types/nodemailer': + specifier: ^8.0.1 + version: 8.0.1 + nodemailer: + specifier: ^9.0.5 + version: 9.0.5 + oxlint: + specifier: ^1.44.0 + version: 1.77.0(oxlint-tsgolint@7.0.2001) + oxlint-tsgolint: + specifier: ^7.0.2001 + version: 7.0.2001 + resend: + specifier: 6.17.1 + version: 6.17.1 + tsdown: + specifier: ^0.22.14 + version: 0.22.14(@arethetypeswrong/core@0.18.5)(publint@0.3.23)(tsx@4.23.11) + typescript: + specifier: ^5.9.2 + version: typescript-native-bridge@6.0.3-bridge.12.tsgo.7.0.2 + vitest: + specifier: ^3.2.4 + version: 3.2.7(@types/debug@4.1.13)(@types/node@24.13.3)(happy-dom@20.11.2) + packages/models: dependencies: '@agent-zero/shared': @@ -894,6 +955,10 @@ packages: resolution: {integrity: sha512-6or44XprPzKbr8zkmzosowSE0pxkvJcoojBL+mCZvPUt3kvXp3XSNqeVun9golb1acEfSo6yaEBRT18h2VU+1Q==} engines: {node: '>=22'} + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + '@andrewbranch/untar.js@1.0.4': resolution: {integrity: sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==} @@ -1648,6 +1713,18 @@ packages: resolution: {integrity: sha512-TzlTVpKPjaqW6qOYjQcYUDuGsLCNsvFHVBXkYGTAnf5V37jCWrE5haKNXzz0WZUtVHjrpV76L1buANjwXMfT8w==} engines: {node: '>=22'} + '@csstools/selector-resolve-nested@4.0.1': + resolution: {integrity: sha512-j3vdQu0XwLME5qOTWxm8cnmvsf423R2YL6DbKklCHZwkDm7UdKNu6RPlw4REIJhSlKBICY3B70/7QZdicLqZgg==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + + '@csstools/selector-specificity@6.0.0': + resolution: {integrity: sha512-4sSgl78OtOXEX/2d++8A83zHNTgwCJMaR24FvsYL7Uf/VS8HZk9PTwR51elTbGqMuwH3szLvvOXEaVnqn0Z3zA==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss-selector-parser: ^7.1.1 + '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -2355,6 +2432,18 @@ packages: '@fastify/busboy@3.2.0': resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + '@floating-ui/core@1.8.0': + resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==} + + '@floating-ui/dom@1.8.0': + resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==} + + '@floating-ui/utils@0.2.12': + resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==} + + '@floating-ui/vue@1.1.11': + resolution: {integrity: sha512-HzHKCNVxnGS35r9fCHBc3+uCnjw9IWIlCPL683cGgM9Kgj2BiAl8x1mS7vtvP6F9S/e/q4O6MApwSHj8hNLGfw==} + '@google-cloud/paginator@5.0.2': resolution: {integrity: sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==} engines: {node: '>=14.0.0'} @@ -2596,6 +2685,12 @@ packages: cpu: [x64] os: [win32] + '@internationalized/date@3.12.3': + resolution: {integrity: sha512-fuLX+3ZKLsxI73y8b01EG/WjHb6gE6weCqlfawPO27kBWGMh9G1yH6Csv1uU7/cac9H2GHmOMt6CjmuQ1aia4Q==} + + '@internationalized/number@3.6.7': + resolution: {integrity: sha512-3ji1fcrT+FPAK86UqEhB/psHixYo6niWPJtt7+qRaYFynt/BaJG8GhAPimtWUpEiVSTq8ZM8L5psMxGquiB/Vg==} + '@intlify/bundle-utils@11.2.4': resolution: {integrity: sha512-eE18yR9eM9k5n8snCkHIYp2MuVTxa19aF8z9OMyxXWv0frz2HlBZDGIPFjA38pP3OJ1IlRBXC/dW5GILeLMSCQ==} engines: {node: '>= 22.13'} @@ -2788,6 +2883,11 @@ packages: '@loaderkit/resolve@1.0.6': resolution: {integrity: sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==} + '@lucide/vue@1.31.0': + resolution: {integrity: sha512-NtjEHhcAa7umPh40wrRmlESJqNGdnpc7LziBKFnW3fmlrW0g2xMCDpdWU7WeEHRSl3xOpxbqiFTLKxoK3CoVCg==} + peerDependencies: + vue: '>=3.0.1' + '@lukeed/csprng@1.1.0': resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} engines: {node: '>=8'} @@ -2796,11 +2896,34 @@ packages: resolution: {integrity: sha512-N0PDFIitA/Vzh5V6BtTacWU9jgSDJJtPLHamRLMU85mohmyuVW/pggHX8dzdV5ieqZeHEz8pDZSzH4I0hOYxOg==, tarball: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935} version: 0.1.1 + '@maizzle/framework@6.0.13': + resolution: {integrity: sha512-cWoRA98Ur4JulzzYEhYMvMkOshniIh/hRCm/UE4VslzgCoVOiPhvEi5G9oZqVN3M3EmTpKWZx8B2aAyPK0mNew==} + hasBin: true + peerDependencies: + shiki: ^1 || ^2 || ^3 || ^4 + tailwind-merge: ^2 || ^3 + vue: ^3 + + '@maizzle/tailwindcss@1.5.6': + resolution: {integrity: sha512-JBJJLV3x1SpG7zqgdpjMGhGzANcZ/T6qxKzxMe0TzfJJ+12a2UV4N55dWzos9xLgcdt91NySPqQRMNKWZexMUg==} + '@mapbox/node-pre-gyp@2.0.3': resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} engines: {node: '>=18'} hasBin: true + '@mdit-vue/plugin-component@3.0.2': + resolution: {integrity: sha512-Fu53MajrZMOAjOIPGMTdTXgHLgGU9KwTqKtYc6WNYtFZNKw04euSfJ/zFg8eBY/2MlciVngkF7Gyc2IL7e8Bsw==} + engines: {node: '>=20.0.0'} + + '@mdit-vue/plugin-frontmatter@3.0.2': + resolution: {integrity: sha512-QKKgIva31YtqHgSAz7S7hRcL7cHXiqdog4wxTfxeQCHo+9IP4Oi5/r1Y5E93nTPccpadDWzAwr3A0F+kAEnsVQ==} + engines: {node: '>=20.0.0'} + + '@mdit-vue/types@3.0.2': + resolution: {integrity: sha512-00aAZ0F0NLik6I6Yba2emGbHLxv+QYrPH00qQ5dFKXlAo1Ll2RHDXwY7nN2WAfrx2pP+WrvSRFTGFCNGdzBDHw==} + engines: {node: '>=20.0.0'} + '@mdream/crawl@1.5.12': resolution: {integrity: sha512-jNR0HTQGagYwzKuO0oMG4HN+Og5esckbphtYlQI3esW9Ax8Pvd/EJK9evExikW/qikhamckMA6vgM9z+ZUR6AA==} hasBin: true @@ -4268,48 +4391,97 @@ packages: cpu: [x64] os: [win32] + '@oxfmt/binding-android-arm-eabi@0.61.0': + resolution: {integrity: sha512-BaS+1OVvg9sr+Xav0+KdWedQRcAzrdoEcwMZeqoc2F6ieC1s/t5eM35YQoRPQ7vAqkZ+p3tbQb1r9I9mrV5oGA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + '@oxfmt/binding-android-arm-eabi@0.62.0': resolution: {integrity: sha512-pdsv0C4gPjJ8H1+sd8u0BDx+yLACTL+rgeMIOL1ln4ihSnhw8CWXtYWgvcSkyTfgGBIzFKab+d8rx9Xl4en/Kw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] + '@oxfmt/binding-android-arm64@0.61.0': + resolution: {integrity: sha512-of8atAV0M1egGcVOMbgZCvc10sFOP3ayQBNQV5h5G3fNq8gACdEswfFk9bzGrdbM23rtg0Coxi7np7oPLcueNw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@oxfmt/binding-android-arm64@0.62.0': resolution: {integrity: sha512-WC3YQ7uS/KtDrjmqwBviwFKe9qeoi+eXx8aX1z/ffG23Md75myjrJaQqTuJvdOLPoa4EYTjDWH0dHXfwulCVog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] + '@oxfmt/binding-darwin-arm64@0.61.0': + resolution: {integrity: sha512-7l8+5ov4BGwtAcmpzvEik/TG3bciwyw/S3e6j5GKH7pcQqcgCVxD3AuJeP6upto+SOTBKQ4wrrdbMt0gq8fHSQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@oxfmt/binding-darwin-arm64@0.62.0': resolution: {integrity: sha512-GM8Yf3LjjaR1I8PD0SfeoIlwhsh9GvSF+cQ8sf624Yxnjsyumn95aFzYfKJVefblfDIiOAnZ7QVm2sa21Er/0Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] + '@oxfmt/binding-darwin-x64@0.61.0': + resolution: {integrity: sha512-Fnz4dDDXBb7udk+DmwelNjxbD6yptyxwCqwCH2ebo4RVLxVsRfFsn/AHJC49KIltPrVokamGv4SSOsiV50DTxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@oxfmt/binding-darwin-x64@0.62.0': resolution: {integrity: sha512-d5THp7F8bCxLqNogEXDORRsQD6dosf3EyFtnXfBer6v+8tGdcWIjoDX9WaXrrF/26zOmL8qHpPTKCEvpBDmZkQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] + '@oxfmt/binding-freebsd-x64@0.61.0': + resolution: {integrity: sha512-mddOebKNCP+AucmzfNsk3jgbr681qAUvgMqi865GW5gWLJ/AnzXbvjQRrny0e++NAN8aphav/aRSrfFxNsNjpA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@oxfmt/binding-freebsd-x64@0.62.0': resolution: {integrity: sha512-1DnrtXGZooOZ0fHgAXZUaDQzBVh1CM2MNW4oBXyQ2aWKvCHjyljvT9fgBkOM0fEOb96X5eqtcfJ0YUVt9jj66g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] + '@oxfmt/binding-linux-arm-gnueabihf@0.61.0': + resolution: {integrity: sha512-svx59iYL+DbaZGZUIoice4W0CjRXGExnbz7Re+awIb60rVxBS2KrU7Hnlx+nZYanLGLpjneUEgo/VFEKkSZAyQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxfmt/binding-linux-arm-gnueabihf@0.62.0': resolution: {integrity: sha512-4pQDHOYRH+Huqe0StIaWyvk2CVl/aTaqSrbZpA3/pLS2xH24ME7lBgYprhQF2fRkHBzhGGGKliwxFsDdHwx59g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxfmt/binding-linux-arm-musleabihf@0.61.0': + resolution: {integrity: sha512-BYK9MPJPCf6d+fLKMTruThmEyCtHzQ1zLcsrTlUVkmnoXIaHAbfpeLYQwX1tkjs7W11dyzoi6HFvKcdnvX1zNg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@oxfmt/binding-linux-arm-musleabihf@0.62.0': resolution: {integrity: sha512-X0jAaZJFMCVKhB6YyWVTQ/wN2DLsBcZKSMqTS76bF6riT+XZdtg2FPEdjDvdVbunO9cG+tWiVaEs4Zs38lxYog==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] + '@oxfmt/binding-linux-arm64-gnu@0.61.0': + resolution: {integrity: sha512-QUaCNLq2/EC6G5ljOuFanl9Lgw6ZWp4co7rs4+KOMUzbGfA4Lq58FHRjjF9sVIG+93XSbo343MxFATrOU1qctA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-arm64-gnu@0.62.0': resolution: {integrity: sha512-682Z8T5s8T5ATArYtsejKvbIfd8LEAXyyDkKkoZVq8HND7Vx8TYLlrDjDSeYfodMeVwHOgkj13lJYR8cj6vUSg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4317,6 +4489,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-arm64-musl@0.61.0': + resolution: {integrity: sha512-S6uvJ6MXnRXl+zTs0CARNDvkE+cymj0EVWEKKsyKnlLlqTyQJBjw5s4D2pSIOZc+S46cy4STefzcr/sm0VzVPA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@oxfmt/binding-linux-arm64-musl@0.62.0': resolution: {integrity: sha512-lk25fAl7KWaLWVJcW0CHEXB7QlQZtx5eDkjpaGMK0hzXTjUe0Wmlu8IKuFHoviSOcEJedRTs4VE/506VqGxGew==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4324,6 +4503,13 @@ packages: os: [linux] libc: [musl] + '@oxfmt/binding-linux-ppc64-gnu@0.61.0': + resolution: {integrity: sha512-6VDlRcytvZG6UlSIdAFKDLbppo9tvPxrWzle6vHldYFMeuDPQEfMKrkwezp7FaBq1wik9ra554ZZeRPsyIkFpg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-ppc64-gnu@0.62.0': resolution: {integrity: sha512-SFyNqHQLwySceWNLhiSldx7wPXRAzP0L0WcW9GegP3uWrpZGJiZlQO85NbHAFPEfxR9PhZ9qSnZryEh7+v+4Gw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4331,6 +4517,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-riscv64-gnu@0.61.0': + resolution: {integrity: sha512-KkBTYbzExpbmn15XjKPLu2fRV2PVlq+KWt+brad5rwIa03vdYoaDRWiS7raHII/dCTR6Ro4UpYUCH4t6lif4WQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-riscv64-gnu@0.62.0': resolution: {integrity: sha512-KYj55C1ywJfHo6+aKDuEmUtVEdJALsC5GwayDGsI6FGz2GxFqNr/mA8nxVsNbJzm7sE5MRqTQ9ziImSzhYXysA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4338,6 +4531,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-riscv64-musl@0.61.0': + resolution: {integrity: sha512-69tzIq7sJLVB9dxYYtvMzcSSsnZHSO+U2U19O2RqDqgj6+Q4O7HjSXdaszbcgqzhsUwzSH7z5kWvk8nmf6BHTg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [riscv64] + os: [linux] + libc: [musl] + '@oxfmt/binding-linux-riscv64-musl@0.62.0': resolution: {integrity: sha512-BhZDNo5GOU5nC378RhD0/XpvaEBHsH3HLgJp8YZX3A0InC7oivzA63HsRmiXFLtLSHAstEVrDf6fbC7Rs8Jh/A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4345,6 +4545,13 @@ packages: os: [linux] libc: [musl] + '@oxfmt/binding-linux-s390x-gnu@0.61.0': + resolution: {integrity: sha512-Oqi/N0OvtOVXsPKAOOhKgGH3msRYF8BLJaNBbWiupRiKoKVyc8JRKPCfarkQJC+RgP9U8raUKLe+bNwd0HUMiA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-s390x-gnu@0.62.0': resolution: {integrity: sha512-UyAFmyHkgSgUJ/wOM4p3U8AC2yAFvRH5PNBs7TnK0fObTT/XSWcdr/lAzPSWaekHaZFaMeFZyk9n93Joq3J93A==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4352,6 +4559,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-x64-gnu@0.61.0': + resolution: {integrity: sha512-3TKwv/ed4uwJSemAA8P9XcoqETpjQI4waquF9UilhA9Mn/dhr1PdUEXWlL74mtc6ZNfmKPA9+NEJm01nRF8CVA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@oxfmt/binding-linux-x64-gnu@0.62.0': resolution: {integrity: sha512-1iYMP0leytWazFubD/WnINJuIrzRPuoL1aWEJdlGezEzDbTxcd29R4r8IUzP2oWeKst5V02uMJgR2NILlPlG6w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4359,6 +4573,13 @@ packages: os: [linux] libc: [glibc] + '@oxfmt/binding-linux-x64-musl@0.61.0': + resolution: {integrity: sha512-uFso4u4nLkVSlMCpgjyvWV60Gt7GvDQHnk1mmRxHIkZTMB0ljpUKwCD9FYGgN9H97x2wYl0UwEjgRZaPIuhEhw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@oxfmt/binding-linux-x64-musl@0.62.0': resolution: {integrity: sha512-4rA/URtJSTVNVAQz6Q8wf7SaRvOXVy+TizriT9hs/Y1XhLR/R+92uWKRQG8yFWRAIEBbFHJ6WevQcl/G9SXEfw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4366,24 +4587,48 @@ packages: os: [linux] libc: [musl] + '@oxfmt/binding-openharmony-arm64@0.61.0': + resolution: {integrity: sha512-keGLkzeOvkMpNmPp4hffXWpfoSsY6e1K8++KXD4mSSfxdvM8q9QUDsYY689TB1k6Co832DZn1MnaaVx6cIBMWQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@oxfmt/binding-openharmony-arm64@0.62.0': resolution: {integrity: sha512-mSZuFHU2ar1KLUjXpI2QBQcJ1VsOB3mOCgQXuXCpKs19dgh4u+OaovNfrWDfiJb+ihJ2+f7YFcaO9bS2dlTCXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] + '@oxfmt/binding-win32-arm64-msvc@0.61.0': + resolution: {integrity: sha512-VzsAISkFxmNhJ5LBDEL9VuH6tJsVJMtqYit2LyIUf/HLnsCe4Pg9SMOjjVQzGWt0bnpyfJ94CrqTqcpNZzK+ug==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@oxfmt/binding-win32-arm64-msvc@0.62.0': resolution: {integrity: sha512-OfwuhkcjDlqC4EgDojtiV9mzpLqeB9KqTOWPOjLEYBVdDCVSxqW3qzp/xcIxsbtI0UgGCnKvAqYKyY25kf5JZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] + '@oxfmt/binding-win32-ia32-msvc@0.61.0': + resolution: {integrity: sha512-xv4t7yzwJoYaLB6Zv28B3W+j7brEjsyv50rLTAQgmxJzddce9fAMCxed8dSAkbWES0zz2J29nYK5FaTuD2YBHg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ia32] + os: [win32] + '@oxfmt/binding-win32-ia32-msvc@0.62.0': resolution: {integrity: sha512-P9uDDNFRzghO3X8QAzhkjKhK7JvtABsVn8UYtFX7uor12IAnwNt8nNIctvfWj1JkQU/kE+fmLRPiw7XlrIHsZw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] + '@oxfmt/binding-win32-x64-msvc@0.61.0': + resolution: {integrity: sha512-6EZXFkqOwxdDYjIn3TSNnPk3ST5E5GiYd4FiM6UF/mCL/LZSfr6D6UygTfW3R1PCQP2quCKpCEGRlij8E3VYbg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@oxfmt/binding-win32-x64-msvc@0.62.0': resolution: {integrity: sha512-dlI5SY7XYQCiCBafntWagCR6HcAJB/NpsLtdlPx8x08+Osz8Ok1HHz1GZuusegCe/VoJ6pAnF5a4pd5OZAq7qQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -5068,6 +5313,37 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + '@shikijs/core@4.4.3': + resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.4.3': + resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.4.3': + resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} + engines: {node: '>=20'} + + '@shikijs/langs@4.4.3': + resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.4.3': + resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} + engines: {node: '>=20'} + + '@shikijs/themes@4.4.3': + resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} + engines: {node: '>=20'} + + '@shikijs/types@4.4.3': + resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@simple-git/args-pathspec@1.0.3': resolution: {integrity: sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA==} @@ -5137,6 +5413,9 @@ packages: '@speed-highlight/core@1.2.24': resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} @@ -5263,9 +5542,110 @@ packages: '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} + '@swc/helpers@0.5.23': + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} + '@swc/types@0.1.28': resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@tailwindcss/postcss@4.3.3': + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} + + '@tailwindcss/vite@4.3.3': + resolution: {integrity: sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanstack/virtual-core@3.17.7': + resolution: {integrity: sha512-bp+v10y65sp2H7WpWfIMyxTNfl8ZVfxFTLRjPIFRryi6FV/J33z4IS53WO4pTk36KlvJ4iLiQz+oaydDC1xbcA==} + + '@tanstack/vue-virtual@3.13.35': + resolution: {integrity: sha512-lOfSPvgPdlaH6Qy+CyIc3XpycitaSQ9GECndGpTuDiu+uDA1am+90yWXwzDSd/20ZM196ggWJLS+Qb6WjVd/OA==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -5367,6 +5747,9 @@ packages: '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/hast@3.0.5': + resolution: {integrity: sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==} + '@types/http-cache-semantics@4.2.0': resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} @@ -5379,9 +5762,15 @@ packages: '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + '@types/lodash@4.17.25': resolution: {integrity: sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -5418,6 +5807,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -5499,6 +5891,9 @@ packages: resolution: {integrity: sha512-bLMpVcWZNzq6lYOybwFwOAR1IXKcHnhUNqYeHjl1bET/qE3jFPFH+p8Wrh3rU4xwdnifPxmKNESBYnvnmc75aA==} engines: {node: '>=22.0.0'} + '@ungap/structured-clone@1.3.3': + resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@unhead/bundler@3.3.1': resolution: {integrity: sha512-F9gEgqpUKqHFzOv+7Pgm3TbmXhUdLX+WjZiPAK/huLDzblzZmvAUE9vRDymWaOST7jqGT/tPKkwl2kqbgb3nPw==} peerDependencies: @@ -6136,6 +6531,19 @@ packages: '@vue/server-renderer': optional: true + '@vueuse/core@14.4.0': + resolution: {integrity: sha512-X4WHz1HlCzCBoYXesUkifzzWBAcZgXG8Fi5iNPQg/epdzOB3gu8Fawj3hvuwYR1nGcXGnvxwYYcUC/71++svtQ==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@14.4.0': + resolution: {integrity: sha512-swx/255R6JyHZFJhx845iz5CRWDZdCfvkZOpACWc5+c5WHcG24mv8gUT1WIdFQaHt6dq79rvILd9QnCWiyVm9g==} + + '@vueuse/shared@14.4.0': + resolution: {integrity: sha512-JRgY90Sz8DDtPMsaDflvPMp9xYk69JZAmbuDvAquUVXKr2gEjqtzGNTTthLfckH0BzBqvnu31gb4a8TGLRe79g==} + peerDependencies: + vue: ^3.5.0 + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -6587,6 +6995,10 @@ packages: ansi-align@3.0.1: resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} engines: {node: '>=8'} @@ -6637,6 +7049,9 @@ packages: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -6644,6 +7059,18 @@ packages: resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} engines: {node: '>=22'} + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-pull-all-with-glob@7.1.3: + resolution: {integrity: sha512-BCkrHE6iK3EXywYuivo1okICHMD5WnKcoMsyM7O5AjpvTpsYM96UTFaukKv/pRAj42rzmXrNuSAMZXwFKNWn4Q==} + engines: {node: '>=14.18.0'} + + arrayiffy-if-string@5.1.3: + resolution: {integrity: sha512-LQk6w4KAE/65Yr1v9/1Z6dMXNnWrU5TxtQm5nFBNbqzoimKReG1tfYgmIctzMiYW1KgnsGXL8F9G0vlH0r01Ww==} + engines: {node: '>=14.18.0'} + arrify@2.0.1: resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==} engines: {node: '>=8'} @@ -6944,6 +7371,10 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boolbase@2.0.0: + resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} + engines: {node: '>=20.19.0'} + boolean@3.2.0: resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. @@ -7104,6 +7535,12 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + character-entities@2.0.2: resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} @@ -7126,8 +7563,15 @@ packages: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} - chevrotain@10.5.0: - resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} chokidar@4.0.3: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} @@ -7160,6 +7604,9 @@ packages: cjs-module-lexer@2.2.0: resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -7187,6 +7634,10 @@ packages: resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} engines: {node: '>=6'} + cli-spinners@3.4.0: + resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==} + engines: {node: '>=18.20'} + cli-table3@0.6.5: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} @@ -7202,10 +7653,18 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + cluster-key-slot@1.1.1: resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} engines: {node: '>=0.10.0'} + codsen-utils@1.7.3: + resolution: {integrity: sha512-YIFQQ1n2NSgwoB3sCe7RpkZzsrPxTMek6jc7wC9fXOm1wwfWAKja9gLOMEjlXOUd3LKV3o6Jci7n9BoHs5Z8Sg==} + engines: {node: '>=14.18.0'} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} @@ -7213,6 +7672,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-shorthand-hex-to-six-digit@5.1.3: + resolution: {integrity: sha512-uHNVXSceG5/sJ5abbk9i6ZuzYVIRI2MwgdcX13rFHD7oxiGq8bJOkOiJI7Pbjrl0zIm13hfEGefGoiQaLTi3XQ==} + engines: {node: '>=14.18.0'} + colorette@2.0.20: resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} @@ -7234,6 +7697,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -7246,6 +7712,10 @@ packages: resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} engines: {node: '>=18'} + commander@14.0.3: + resolution: {integrity: sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==} + engines: {node: '>=20'} + commander@15.0.0: resolution: {integrity: sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==} engines: {node: '>=22.12.0'} @@ -7407,6 +7877,10 @@ packages: css-select@5.2.2: resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + css-select@7.0.0: + resolution: {integrity: sha512-snmjEVXy+1LnwXdxhYvTMj1d9tOh4HxkA1YmoayVBeeyR2C14Pum7fcxJIm4SswYspVy866eYNwlH6xC3/VH5g==} + engines: {node: '>=20.19.0'} + css-tree@2.2.1: resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} @@ -7419,6 +7893,10 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + css-what@8.0.0: + resolution: {integrity: sha512-DH0Bqq3DNp5tdOReuNyAA+Ev4Y2GS5FMbZpeTLP6C4CDi0h5nL0BmUPChXw3o/qbHLDWHl49sbNqQVY7bMSDdw==} + engines: {node: '>=20.19.0'} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -7449,6 +7927,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + culori@4.0.2: + resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + d3-array@3.2.1: resolution: {integrity: sha512-gUY/qeHq/yNqqoCKNq4vtpFLdoCdvyNpWoC/KNjhGbhDuQpAM9sIQQKkXSNpXa9h5KySs/gzm7R88WkUutgwWQ==} engines: {node: '>=12'} @@ -7546,6 +8028,10 @@ packages: decode-named-character-reference@1.3.0: resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + decode-uri-component@0.5.0: + resolution: {integrity: sha512-1BiQVoK8C9gUbQU6NzAtO/tkz2qOFpEObMWpcFvhx4fYnj4Oc5yzaJN/LD36ihkVUdXyh5ZekzX+yM+ty/SrPg==} + engines: {node: '>=14.16'} + decompress-response@10.0.0: resolution: {integrity: sha512-oj7KWToJuuxlPr7VV0vabvxEIiqNMo+q0NueIiL3XhtwC6FVOX7Hr1c0C4eD0bmf7Zr+S/dSf2xvkH3Ad6sU3Q==} engines: {node: '>=20'} @@ -7949,6 +8435,10 @@ packages: elkjs@0.11.1: resolution: {integrity: sha512-zxxR9k+rx5ktMwT/FwyLdPCrq7xN6e4VGGHH8hA01vVYKjTFik7nHOxBnAYtrgYUB1RpAiLvA1/U2YraWxyKKg==} + email-comb@7.1.3: + resolution: {integrity: sha512-QOv6wPr7qU+aM/8cJ8Eddrb86q2djGyDTnLdvAp/7B2FQ+XFRrk4VNn22xarc6NzvcDSEslaM4oF2hp10xFNZw==} + engines: {node: '>=14.18.0'} + emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -7973,6 +8463,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} @@ -7988,6 +8481,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + entities@7.0.1: resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} engines: {node: '>=0.12'} @@ -8090,6 +8587,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} @@ -8241,6 +8742,10 @@ packages: resolution: {integrity: sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==} engines: {node: '>=4'} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -8285,6 +8790,9 @@ packages: fast-safe-stringify@2.1.1: resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fast-string-truncated-width@3.0.3: resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} @@ -8500,6 +9008,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + filter-obj@5.1.0: + resolution: {integrity: sha512-qWeTREPoT7I0bifpPUXtxkZJ1XJzxWtfoWWkdVGqa+eCr3SHW/Ocp89o8vLvbUuQnadybJpjOKu4V+RwO6sGng==} + engines: {node: '>=14.16'} + finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} @@ -8780,6 +9292,10 @@ packages: graphmatch@1.1.1: resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -8852,6 +9368,15 @@ packages: resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hex-color-regex@1.1.0: + resolution: {integrity: sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -8865,13 +9390,26 @@ packages: hookable@6.1.1: resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} + html-crush@6.1.3: + resolution: {integrity: sha512-IrDC4BrdrMmV+GMYfXtx6BtzQ6hVf+GjLSDhmg/14DkpNbXtxAcbH3Wk9VeNZbg/y2MpobWTVOSrCzr/HWBwcw==} + engines: {node: '>=14.18.0'} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + htmlparser2@12.0.0: resolution: {integrity: sha512-Tz7u1i95/g2x2jz81+x0FBVhBhY5aRTvD3tXXdFaljuNdzDLJ8UGNRrTcj2cgQvAg3iW/h77Fz15nLW0L0CrZw==} engines: {node: '>=20.19.0'} + htmlparser2@9.1.0: + resolution: {integrity: sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==} + http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} @@ -8927,6 +9465,10 @@ packages: resolution: {integrity: sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==} engines: {node: '>=20.0.0'} + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} @@ -9045,6 +9587,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} hasBin: true + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -9136,6 +9682,10 @@ packages: is-unsafe@2.0.0: resolution: {integrity: sha512-2LdV822R+wmI86unXA93WCFpL6g+av8ynWk0nrHyJqGop5VoocYsSLFgN8jrfalT6iGeLNM4KXuVSsULP53kEA==} + is-url-superb@6.1.0: + resolution: {integrity: sha512-LXdhGlYqUPdvEyIhWPEEwYYK3yrUiPcBjmFGlZNv1u5GtIL5qQRf7ddDyPNAvsMFqdzS923FROpTQU97tLe3JQ==} + engines: {node: '>=12'} + is-valid-glob@1.0.0: resolution: {integrity: sha512-AhiROmoEFDSsjx8hW+5sGwgKVIORcXnrlAx/R0ZSeaPw70Vw0CqkGBBhHGL58Uox2eXnU1AnvXJl1XlyedO5bA==} engines: {node: '>=0.10.0'} @@ -9226,6 +9776,10 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.15.1: + resolution: {integrity: sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==} + hasBin: true + js-yaml@4.3.1: resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true @@ -9292,6 +9846,11 @@ packages: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} + juice@12.1.2: + resolution: {integrity: sha512-qNFegEEo14v4aCh0OwdWhGsHKoxWx6RYl1GyzMhxNHfFnVGFi+MFgAiJ6pyFWPw0KucvPwmHctXPZwrsD0ufqg==} + engines: {node: '>=22.12.0'} + hasBin: true + just-bash@3.2.0: resolution: {integrity: sha512-hRTLLWBXCKuosjaNFJR7uPYBza+T2vjG3NdPBz4wxlctlnxwrbLjtLQf6RtSKmy/jzNJ6/URCtvxrXjy6yFGeQ==} engines: {node: '>=20.18.1'} @@ -9360,36 +9919,73 @@ packages: cpu: [x64, arm64, wasm32, arm] os: [darwin, linux, win32] + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-android-arm64@1.33.0: resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-arm64@1.33.0: resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-darwin-x64@1.33.0: resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-freebsd-x64@1.33.0: resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} engines: {node: '>= 12.0.0'} @@ -9397,6 +9993,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} engines: {node: '>= 12.0.0'} @@ -9404,6 +10007,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} engines: {node: '>= 12.0.0'} @@ -9411,6 +10021,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-musl@1.33.0: resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} engines: {node: '>= 12.0.0'} @@ -9418,18 +10035,34 @@ packages: os: [linux] libc: [musl] + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss-win32-x64-msvc@1.33.0: resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} engines: {node: '>= 12.0.0'} @@ -9477,6 +10110,9 @@ packages: resolution: {integrity: sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lodash-es@4.18.1: + resolution: {integrity: sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==} + lodash.includes@4.3.0: resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} @@ -9508,6 +10144,10 @@ packages: resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==} engines: {node: '>=18'} + log-symbols@7.0.1: + resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==} + engines: {node: '>=18'} + log-update@8.0.0: resolution: {integrity: sha512-lddSgOt3bPASrylL54ZSpy8nBHns+vBVSoILlVOx+dei300pnLRN958rj/EdlVLKuWlSESU3qdnDZdAI7FXYGg==} engines: {node: '>=22'} @@ -9566,9 +10206,16 @@ packages: resolution: {integrity: sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==} engines: {node: '>=18'} + markdown-exit@1.0.0-beta.9: + resolution: {integrity: sha512-5tzrMKMF367amyBly131vm6eGuWRL2DjBqWaFmPzPbLyuxP0XOmyyyroOAIXuBAMF/3kZbbfqOxvW/SotqKqbQ==} + markdown-exit@1.1.0-beta.2: resolution: {integrity: sha512-8CzMGVlFZ4DEfnc8KU+4ycUW2SIOuiXqCHD7z51ecVEi/weyc0f2ylQbCm4KoKuVlTZSuMUMnWT0hTyquZ7anQ==} + markdown-it@14.3.0: + resolution: {integrity: sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==} + hasBin: true + markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -9587,6 +10234,10 @@ packages: resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} engines: {node: '>=10'} + matcher@6.0.0: + resolution: {integrity: sha512-TzDerdcNtI79w7Av4GT57bLdElPA/VAkjqdMZv8yhuc8geU2z0ljW9anXbX/55aHEMTpYypZb1lxsA/46r9oOQ==} + engines: {node: '>=20'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -9618,6 +10269,9 @@ packages: mdast-util-phrasing@4.1.0: resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdast-util-to-markdown@2.1.2: resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} @@ -9759,6 +10413,11 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} @@ -10118,6 +10777,10 @@ packages: resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} engines: {node: '>=18'} + nodemailer@9.0.5: + resolution: {integrity: sha512-wvjiKvjczmsN7U/8006JOdXubgBk2XFAbioDMbT+sM7cPs0QrhJTa6KBRX7P5REGGkDcLUz/EarWidb8G8C1jQ==} + engines: {node: '>=6.0.0'} + nopt@7.2.1: resolution: {integrity: sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -10154,6 +10817,10 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nth-check@3.0.1: + resolution: {integrity: sha512-GX0gsdbGVCgnRgbeGaubfjpBXyYRWOOCVeYh08bSQvDZqxz5ndXs1OTfAt/h36G1xvI94YIspsI0sVFqAV9+RQ==} + engines: {node: '>=20.19.0'} + nuxt-define@1.0.0: resolution: {integrity: sha512-CYZ2WjU+KCyCDVzjYUM4eEpMF0rkPmkpiFrybTqqQCRpUbPt2h3snswWIpFPXTi+osRCY6Og0W/XLAQgDL4FfQ==} @@ -10180,6 +10847,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-boolean-combinations@6.2.3: + resolution: {integrity: sha512-A2inWgy5Hy3+9prMyiKBUBYcTpbuZbKOMt+YKMD3ZAsli7TwKh6TPSZdK+kNF5hmJFhaVbjHwP/xsfjO2Z/GmQ==} + engines: {node: '>=14.18.0'} + object-identity@0.2.3: resolution: {integrity: sha512-2J8Joz2Tf7aaylhqFvIUJHNgpuGR38Hh75Voq9GzTbStBxJUaOtN0K1aOd3cV5qp+ij1pMqRbPrGCGMOyX303w==} @@ -10230,6 +10901,12 @@ packages: resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==} engines: {node: '>=18'} + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + onnxruntime-common@1.24.0-dev.20251116-b39e144322: resolution: {integrity: sha512-BOoomdHYmNRL5r4iQ4bMvsl2t0/hzVQ3OM3PHD0gxeXu1PmggqBv3puZicEUVOA3AtHHYmqZtjMj9FOfGrATTw==} @@ -10275,6 +10952,10 @@ packages: resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==} engines: {node: '>=18'} + ora@9.4.1: + resolution: {integrity: sha512-6VlU9MLXbjVQD04AZCMX28hVtA5bUoadvUqO76MUCVA0ilwJbMiHsITRPfyVm6p/BC0Av/BXMujx39WCe1LEqw==} + engines: {node: '>=20'} + os-paths@4.4.0: resolution: {integrity: sha512-wrAwOeXp1RRMFfQY8Sy7VaGVmPocaLwSFOYCGKSyo8qmJ+/yaafCl5BCA1IQZWqFSRBrKDYFeR9d/VyQzfH/jg==} engines: {node: '>= 6.0'} @@ -10321,6 +11002,19 @@ packages: rolldown: optional: true + oxfmt@0.61.0: + resolution: {integrity: sha512-DxdHBEMYpcEnHoUHjjOigUqV2TYKsvxLwUPXnVYBjgFdqrcQ/91OtwubtZ2PUodCs3sStI8R5Qw3fKNGK4e8wQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + svelte: ^5.0.0 + vite-plus: '*' + peerDependenciesMeta: + svelte: + optional: true + vite-plus: + optional: true + oxfmt@0.62.0: resolution: {integrity: sha512-vxgGHTmnDU9j4CX7dDBLzxgmHxfda/yPcgJkGCMUSCwRmz+euo/V08xXLNgXTeqAB9Fhf3Pe2nO1RNKLCVgphQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -10434,12 +11128,21 @@ packages: parse5-htmlparser2-tree-adapter@6.0.1: resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + parse5@5.1.1: resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -10583,6 +11286,9 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postal-mime@2.7.4: + resolution: {integrity: sha512-0WdnFQYUrPGGTFu1uOqD2s7omwua8xaeYGdO6rb88oD5yJ/4pPHDA4sdWqfD8wQVfCny563n/HQS7zTFft+f/g==} + postcss-calc@10.1.1: resolution: {integrity: sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==} engines: {node: ^18.12 || ^20.9 || >=22.0} @@ -10661,6 +11367,12 @@ packages: peerDependencies: postcss: ^8.5.25 + postcss-nesting@14.0.1: + resolution: {integrity: sha512-80MH7KcmMtb7ffSBX82uZwnogltubaXF0sagu+OGD8M9jViR3ngRgCdoTzKCvKEtllB0MW2U7Rgfcub5fR1Bug==} + engines: {node: '>=20.19.0'} + peerDependencies: + postcss: ^8.4 + postcss-normalize-charset@8.0.2: resolution: {integrity: sha512-iy3/b+gX+dHfbNZq/4rfThbMAqxhneBJEhS71y5toliav5rLowkZ6g0ZJGymXRZytDOt/u+fRSFkNJLkRPYaug==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} @@ -10733,10 +11445,22 @@ packages: peerDependencies: postcss: ^8.5.25 + postcss-safe-parser@7.0.1: + resolution: {integrity: sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.4.31 + postcss-selector-parser@7.1.5: resolution: {integrity: sha512-KvvtD7SrlBP7dlgkBghEE3r84CABm5SmV2aNcG4oCA+qDnJ/tvKonFVvwWAyyWUEwxuNawdfEAZKP9zM3oZ2Uw==} engines: {node: '>=4'} + postcss-sort-media-queries@6.7.1: + resolution: {integrity: sha512-NU+cVdLaPMYspggR6JSpk7YdAoRY9Y9jNacHRjA/5vJ0iP6/Stgg2FV82IVnryTQKbtp7B5cQ33KrWbVe07VSQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + postcss: ^8.5.21 + postcss-svgo@8.0.3: resolution: {integrity: sha512-ADG8YNtwE5bcqvxw1gU0X7FkgdvinZZxWKHSCMwayX7gPl4XRmBMyiC84Ukal5bPS9Nk/2tI7siJwqxIN4/grg==} engines: {node: ^22.11.0 || ^24.11.0 || >=26.0} @@ -10837,6 +11561,9 @@ packages: proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + property-information@7.2.0: + resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} + proto-list@1.2.4: resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} @@ -10887,6 +11614,10 @@ packages: quansync@1.0.0: resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==} + query-string@9.5.0: + resolution: {integrity: sha512-YlJmwNyi0RGYjlxYcuDncMsxFU7YyutbuI7gTm8ySxIGBlwx5yiBCOD5ig9ZNoHkawk/1Dey0N5mEfcUybMVAA==} + engines: {node: '>=18'} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -10912,6 +11643,22 @@ packages: resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} engines: {node: '>= 0.6'} + ranges-apply@7.1.3: + resolution: {integrity: sha512-+dpc801TK6qUoMKU9dnwD0wH6XORtZicXLVm4p43jEp+te7Q+Tw0Pa4cHX9Mj1W0zshVYh1S4RIYI38ZUPglqQ==} + engines: {node: '>=14.18.0'} + + ranges-merge@9.1.3: + resolution: {integrity: sha512-dPS11e7AHD1tnuzrboYa+n07JqbHccMqt94C3cSlJ9tTC/RN6P+iGePhTyvCPpoNDWVOEVX4JGyGH0oHjwas0g==} + engines: {node: '>=14.18.0'} + + ranges-push@7.1.3: + resolution: {integrity: sha512-3laGXNa4CW1vt6e2RqC35xRmxXpcWwLNpepTQJ+dkftc/7rH+d9pQ/UQgV6/peN6DsDcpJuu+PjLjWprz2UjDA==} + engines: {node: '>=14.18.0'} + + ranges-sort@6.1.3: + resolution: {integrity: sha512-JsTMmurWEOPYowp1OwUbjcfXyDfaTB+AWoiZwjV+3qOv0uUI7A1CExGwyFwfXEO5HYThOTDC96qC9a6BYqFbyg==} + engines: {node: '>=14.18.0'} + raw-body@3.0.2: resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} engines: {node: '>= 0.10'} @@ -10968,6 +11715,19 @@ packages: reflect-metadata@0.2.2: resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + regex-empty-conditional-comments@3.1.3: + resolution: {integrity: sha512-1bld//jNjpNXXfcuv7Ch7gDaHaK4DU8I7B52E7c20jPjwUJk5X9TpP7BmLr6KtyLUZgKqTzZME+TRL1JzfOROg==} + engines: {node: '>=14.18.0'} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + regexp-to-ast@0.5.0: resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} @@ -10975,6 +11735,11 @@ packages: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true + reka-ui@2.10.3: + resolution: {integrity: sha512-nJGZbwcha8AcP2wbnjodfzsciTKBqp1mIzepC9Pi2xDI/YK3++ej3vpJOSPyxqrkq1Oorj4K+BdJkbVCV6x6ag==} + peerDependencies: + vue: '>= 3.4.0' + remark-gfm@4.0.1: resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} @@ -11002,6 +11767,15 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + resend@6.17.1: + resolution: {integrity: sha512-QhHHIRPPM9Tq2uilWmNF8C8kd6nLkUmiFgDQCt1v4eR4o+6p86qrK+Vn12jnAs0jR8Gf6ABdxI18/90LGQ7GVA==} + engines: {node: '>=20'} + peerDependencies: + '@react-email/render': '*' + peerDependenciesMeta: + '@react-email/render': + optional: true + resolve-alpn@1.2.1: resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} @@ -11090,6 +11864,9 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rimraf@5.0.10: resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} hasBin: true @@ -11206,6 +11983,10 @@ packages: scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + seedrandom@3.0.5: resolution: {integrity: sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==} @@ -11302,6 +12083,10 @@ packages: resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} + shiki@4.4.3: + resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} + engines: {node: '>=20'} + side-channel-list@1.0.1: resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} engines: {node: '>= 0.4'} @@ -11389,6 +12174,10 @@ packages: resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + sort-css-media-queries@3.0.5: + resolution: {integrity: sha512-wRgTa9kOgx5nV+lp/uwT0XBlH/WN5dpsOxyIkbtQud65Ie66TYjHceWH/8d1C0siMjcdSjjXg4zV022QmvLsaw==} + engines: {node: '>= 16'} + sort-keys-length@1.0.1: resolution: {integrity: sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==} engines: {node: '>=0.10.0'} @@ -11412,13 +12201,23 @@ packages: resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} engines: {node: '>= 12'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sparse-bitfield@3.0.3: resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==} + split-on-first@3.0.0: + resolution: {integrity: sha512-qxQJTx2ryR0Dw0ITYyekNQWpz6f8dGd7vffGNflQQ3Iqj9NJ6qiZ7ELpZsJ/QBhIVAiDfXdag3+Gp8RvWa62AA==} + engines: {node: '>=12'} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + sprintf-js@1.1.3: resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} @@ -11471,6 +12270,9 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -11485,6 +12287,10 @@ packages: resolution: {integrity: sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==} engines: {node: '>=18'} + stdin-discarder@0.3.2: + resolution: {integrity: sha512-eCPu1qRxPVkl5605OTWF8Wz40b4Mf45NY5LQmVPQ599knfs5QhASUm9GbJ5BDMDOXgrnh0wyEdvzmL//YMlw0A==} + engines: {node: '>=18'} + stream-browserify@3.0.0: resolution: {integrity: sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==} @@ -11497,6 +12303,42 @@ packages: streamx@2.28.0: resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + string-character-is-astral-surrogate@3.1.3: + resolution: {integrity: sha512-sqj1xo8SMWp6WG/pUYoIiYNgSHQTTuJA/BmdpnvoAoo121RVOTJJQ7V1RKAOwT+vVOYhwiLydb9ul5tgnMPY9w==} + engines: {node: '>=14.18.0'} + + string-collapse-leading-whitespace@7.1.3: + resolution: {integrity: sha512-bCgNODedM9daANnnNPOHuz++qUeVHgsPMghNPznbLLnBviNyuKt9vzHIa6OGTSpWzJUO4Rlffu4+RD47pSTq3g==} + engines: {node: '>=14.18.0'} + + string-extract-class-names@8.1.3: + resolution: {integrity: sha512-C93V3u9dZHHsIaNab765gQv+ewuRvkgx6tseXclrxN1DgLakhbfL8s48QG30uRryXqRttCJrLCQs3EeO5WAz9w==} + engines: {node: '>=14.18.0'} + + string-left-right@6.1.3: + resolution: {integrity: sha512-XPqLphMTTbPMRs4DPX0flLtVFsDWZ+Og3tyku1FiiE2b/+SUHmiO1KRg7NBlb5e9UrojE5F3ryXORL0VYhwwgA==} + engines: {node: '>=14.18.0'} + + string-match-left-right@9.1.3: + resolution: {integrity: sha512-X+lhDoz+Ef7aGeczV+s6W219mpygsytvt/N+WbnJ/ZSi38siK0LgYfAKHe6LPZiRYTYgN2UPXA/FW6bExcxRLg==} + engines: {node: '>=14.18.0'} + + string-range-expander@4.1.3: + resolution: {integrity: sha512-gUBIVv6YYHhXU+fqyEhTQzGHhsVq6NjUpX7y3ZcOtiSbIEiIYzeUzx/+p1koy20LZkfeWbhusRuR4AHdh1JxNg==} + engines: {node: '>=14.18.0'} + + string-strip-html@13.5.3: + resolution: {integrity: sha512-MjrGYjcyOfY8VLXpbEJRtiHLFkWRRIdRt7hcBxGUo2GGM/YSyIfiRQ3q0y2pb+RwYHUfVbw3euuyLzxqyxhyIA==} + engines: {node: '>=14.18.0'} + + string-trim-spaces-only@5.1.3: + resolution: {integrity: sha512-Flb782YX49j4GkLv/M2rqJik/lUq+OJnT/q7kQ6byZtgknAo1wb1XFBN3b+7UMW5I6VB1QdNAoh9oiUa9izBpA==} + engines: {node: '>=14.18.0'} + + string-uglify@3.1.3: + resolution: {integrity: sha512-g93pMqRK4swYKXxYOVma28u7ON/2bg+g5iectIvRZEu6ZSCEMOE19pa2cMGmzqkfS4DxG1Jc4y11wf/luTF3Mg==} + engines: {node: '>=14.18.0'} + string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -11519,6 +12361,9 @@ packages: string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -11527,6 +12372,10 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + strip-dirs@3.0.0: resolution: {integrity: sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ==} @@ -11627,6 +12476,12 @@ packages: resolution: {integrity: sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==} engines: {node: '>=20'} + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + tapable@2.3.3: resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} engines: {node: '>=6'} @@ -11664,6 +12519,10 @@ packages: engines: {node: '>=10'} hasBin: true + test-mixer@4.2.3: + resolution: {integrity: sha512-6sBlzwiDARX7Qp13MwYygwjeNrtlkbHj+6t/+1OnROQWLX+rpquOc+XXbjVs5xYUtltNo6ZpvFM4nNWtOdInHw==} + engines: {node: '>=14.18.0'} + text-decoder@1.2.7: resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} @@ -11779,6 +12638,9 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + trough@2.2.0: resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} @@ -11844,6 +12706,9 @@ packages: resolution: {integrity: sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ==} engines: {node: '>=18', npm: '>=9'} + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -12078,6 +12943,9 @@ packages: unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-stringify-position@4.0.0: resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} @@ -12109,10 +12977,38 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin-auto-import@21.1.0: + resolution: {integrity: sha512-EzrSqWIBulEqCuP7idADXH+tVKYrbwKlR5+r/lOWik0o+Ksny1kitmtryHGNSv7puzzORlcIwT/x2JStiszlHA==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@nuxt/kit': ^4.0.0 + '@vueuse/core': '*' + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@vueuse/core': + optional: true + unplugin-utils@0.3.2: resolution: {integrity: sha512-xVToRh2CTmLk2HnEG7ac4rl1MJTT3RFkpS8B++/SnB0kXvuaavD+n3m/vrzyWQOdJNSZQACnbz01pnppbwV5BA==} engines: {node: '>=20.19.0'} + unplugin-vue-components@32.1.0: + resolution: {integrity: sha512-YiUkSxuRjab18XFOrX5VsIxXzccrfmHVGsGeJgSgklb829DQmCy9E4vvDUE4tuvZZdxyFJZX0Oc4TPnnxiiMyg==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@nuxt/kit': ^3.2.2 || ^4.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + unplugin-vue-markdown@32.0.0: + resolution: {integrity: sha512-K9uiYJF9kvngrN/NRx8fVPZFXiqJR7tbnXQV4mVFxjcsKhuiL6+vVQ5woam59RqeCDprecBmbg+cCGADz0somA==} + engines: {node: '>=22'} + peerDependencies: + vite: ^2.0.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + unplugin@2.3.11: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} @@ -12364,6 +13260,10 @@ packages: typescript: optional: true + valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + validate-npm-package-name@5.0.1: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -12624,6 +13524,17 @@ packages: vue-component-type-helpers@3.3.9: resolution: {integrity: sha512-3c/UfMe0SqyEfcGTyH7mfshHagJ9QTCbppCb0/uGpHZpFug7+If3GeGZN7I0YheKEExemx3xldQPoO7PQSOLQg==} + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + vue-devtools-stub@0.1.0: resolution: {integrity: sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==} @@ -12680,6 +13591,10 @@ packages: wcwidth@1.0.1: resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + web-resource-inliner@8.0.0: + resolution: {integrity: sha512-Ezr98sqXW/+OCGoUEXuOKVR+oVFlSdn1tIySEEJdiSAw4IjrW8hQkwARSSBJTSB5Us5dnytDgL0ZDliAYBhaNA==} + engines: {node: '>=10.0.0'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -12711,10 +13626,19 @@ packages: webpack-cli: optional: true + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@3.0.0: resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} engines: {node: '>=12'} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + whatwg-url@14.2.0: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} @@ -13037,6 +13961,8 @@ snapshots: dependencies: json-schema: 0.4.0 + '@alloc/quick-lru@5.2.0': {} + '@andrewbranch/untar.js@1.0.4': {} '@antfu/install-pkg@1.1.0': @@ -14119,11 +15045,19 @@ snapshots: '@conventional-changelog/template@1.2.1': {} - '@drizzle-team/brocli@0.10.2': {} + '@csstools/selector-resolve-nested@4.0.1(postcss-selector-parser@7.1.5)': + dependencies: + postcss-selector-parser: 7.1.5 - '@dxup/nuxt@0.5.6(esbuild@0.28.2)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2)': + '@csstools/selector-specificity@6.0.0(postcss-selector-parser@7.1.5)': dependencies: - '@dxup/unimport': 0.1.2 + postcss-selector-parser: 7.1.5 + + '@drizzle-team/brocli@0.10.2': {} + + '@dxup/nuxt@0.5.6(esbuild@0.28.2)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2)': + dependencies: + '@dxup/unimport': 0.1.2 '@nuxt/kit': 4.5.2(magic-string@1.1.0)(rolldown@1.2.3)(unplugin@3.3.0(esbuild@0.28.2)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2)) '@vue/compiler-dom': 3.5.41 chokidar: 5.0.0 @@ -14568,6 +15502,25 @@ snapshots: '@fastify/busboy@3.2.0': {} + '@floating-ui/core@1.8.0': + dependencies: + '@floating-ui/utils': 0.2.12 + + '@floating-ui/dom@1.8.0': + dependencies: + '@floating-ui/core': 1.8.0 + '@floating-ui/utils': 0.2.12 + + '@floating-ui/utils@0.2.12': {} + + '@floating-ui/vue@1.1.11(vue@3.5.41)': + dependencies: + '@floating-ui/dom': 1.8.0 + '@floating-ui/utils': 0.2.12 + vue-demi: 0.14.10(vue@3.5.41) + transitivePeerDependencies: + - '@vue/composition-api' + '@google-cloud/paginator@5.0.2': dependencies: arrify: 2.0.1 @@ -14767,6 +15720,14 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@internationalized/date@3.12.3': + dependencies: + '@swc/helpers': 0.5.23 + + '@internationalized/number@3.6.7': + dependencies: + '@swc/helpers': 0.5.23 + '@intlify/bundle-utils@11.2.4(vue-i18n@11.4.8(vue@3.5.41))': dependencies: '@intlify/message-compiler': 11.4.8 @@ -14970,6 +15931,10 @@ snapshots: dependencies: '@braidai/lang': 1.1.2 + '@lucide/vue@1.31.0(vue@3.5.41)': + dependencies: + vue: 3.5.41 + '@lukeed/csprng@1.1.0': {} '@lunariajs/core@https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935': @@ -14988,6 +15953,83 @@ snapshots: transitivePeerDependencies: - supports-color + '@maizzle/framework@6.0.13(shiki@4.4.3)(tailwind-merge@3.6.0)(vue@3.5.41)': + dependencies: + '@lucide/vue': 1.31.0(vue@3.5.41) + '@maizzle/tailwindcss': 1.5.6 + '@tailwindcss/postcss': 4.3.3 + '@tailwindcss/vite': 4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0)) + '@unhead/vue': 3.3.1(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(vue@3.5.41)(webpack@5.109.2) + '@vitejs/plugin-vue': 6.0.8(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(vue@3.5.41) + '@vueuse/core': 14.4.0(vue@3.5.41) + class-variance-authority: 0.7.1 + clsx: 2.1.1 + color-shorthand-hex-to-six-digit: 5.1.3 + css-select: 7.0.0 + culori: 4.0.2 + defu: 6.1.7 + dom-serializer: 3.1.1 + domhandler: 6.0.1 + email-comb: 7.1.3 + html-crush: 6.1.3 + htmlparser2: 12.0.0 + is-url-superb: 6.1.0 + jiti: 2.7.0 + juice: 12.1.2 + markdown-exit: 1.1.0-beta.2 + nodemailer: 9.0.5 + ora: 9.4.1 + oxfmt: 0.61.0 + pathe: 2.0.3 + postcss: 8.5.26 + postcss-calc: 10.1.1(postcss@8.5.26) + postcss-merge-longhand: 8.0.2(postcss@8.5.26) + postcss-safe-parser: 7.0.1(postcss@8.5.26) + postcss-sort-media-queries: 6.7.1(postcss@8.5.26) + postcss-value-parser: 4.2.0 + query-string: 9.5.0 + reka-ui: 2.10.3(vue@3.5.41) + shiki: 4.4.3 + string-strip-html: 13.5.3 + tailwind-merge: 3.6.0 + tinyglobby: 0.2.17 + tinypool: 2.1.0 + tw-animate-css: 1.4.0 + typescript: typescript-native-bridge@6.0.3-bridge.12.tsgo.7.0.2 + unplugin-auto-import: 21.1.0(@vueuse/core@14.4.0(vue@3.5.41)) + unplugin-vue-components: 32.1.0(@nuxt/kit@3.21.11)(vue@3.5.41) + unplugin-vue-markdown: 32.0.0(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0)) + uqr: 0.1.3 + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0) + vue: 3.5.41 + vue-router: 5.2.0(@vue/compiler-sfc@3.5.41)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(vue@3.5.41) + transitivePeerDependencies: + - '@farmfe/core' + - '@nuxt/kit' + - '@oxc-project/types' + - '@pinia/colada' + - '@rspack/core' + - '@unhead/cli' + - '@vitejs/devtools' + - '@vitejs/devtools-kit' + - '@vue/composition-api' + - bun-types-no-globals + - less + - lightningcss + - oxc-parser + - pinia + - sass + - sass-embedded + - stylus + - sugarss + - svelte + - unloader + - vite-plus + bundledDependencies: + - maizzle + + '@maizzle/tailwindcss@1.5.6': {} + '@mapbox/node-pre-gyp@2.0.3': dependencies: consola: 3.4.2 @@ -15001,6 +16043,20 @@ snapshots: - encoding - supports-color + '@mdit-vue/plugin-component@3.0.2': + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.3.0 + + '@mdit-vue/plugin-frontmatter@3.0.2': + dependencies: + '@mdit-vue/types': 3.0.2 + '@types/markdown-it': 14.1.2 + gray-matter: 4.0.3 + markdown-it: 14.3.0 + + '@mdit-vue/types@3.0.2': {} + '@mdream/crawl@1.5.12': dependencies: '@clack/prompts': 1.7.0 @@ -16418,60 +17474,117 @@ snapshots: '@oxc-transform/binding-win32-x64-msvc@0.141.0': optional: true + '@oxfmt/binding-android-arm-eabi@0.61.0': + optional: true + '@oxfmt/binding-android-arm-eabi@0.62.0': optional: true + '@oxfmt/binding-android-arm64@0.61.0': + optional: true + '@oxfmt/binding-android-arm64@0.62.0': optional: true + '@oxfmt/binding-darwin-arm64@0.61.0': + optional: true + '@oxfmt/binding-darwin-arm64@0.62.0': optional: true + '@oxfmt/binding-darwin-x64@0.61.0': + optional: true + '@oxfmt/binding-darwin-x64@0.62.0': optional: true + '@oxfmt/binding-freebsd-x64@0.61.0': + optional: true + '@oxfmt/binding-freebsd-x64@0.62.0': optional: true + '@oxfmt/binding-linux-arm-gnueabihf@0.61.0': + optional: true + '@oxfmt/binding-linux-arm-gnueabihf@0.62.0': optional: true + '@oxfmt/binding-linux-arm-musleabihf@0.61.0': + optional: true + '@oxfmt/binding-linux-arm-musleabihf@0.62.0': optional: true + '@oxfmt/binding-linux-arm64-gnu@0.61.0': + optional: true + '@oxfmt/binding-linux-arm64-gnu@0.62.0': optional: true + '@oxfmt/binding-linux-arm64-musl@0.61.0': + optional: true + '@oxfmt/binding-linux-arm64-musl@0.62.0': optional: true + '@oxfmt/binding-linux-ppc64-gnu@0.61.0': + optional: true + '@oxfmt/binding-linux-ppc64-gnu@0.62.0': optional: true + '@oxfmt/binding-linux-riscv64-gnu@0.61.0': + optional: true + '@oxfmt/binding-linux-riscv64-gnu@0.62.0': optional: true + '@oxfmt/binding-linux-riscv64-musl@0.61.0': + optional: true + '@oxfmt/binding-linux-riscv64-musl@0.62.0': optional: true + '@oxfmt/binding-linux-s390x-gnu@0.61.0': + optional: true + '@oxfmt/binding-linux-s390x-gnu@0.62.0': optional: true + '@oxfmt/binding-linux-x64-gnu@0.61.0': + optional: true + '@oxfmt/binding-linux-x64-gnu@0.62.0': optional: true + '@oxfmt/binding-linux-x64-musl@0.61.0': + optional: true + '@oxfmt/binding-linux-x64-musl@0.62.0': optional: true + '@oxfmt/binding-openharmony-arm64@0.61.0': + optional: true + '@oxfmt/binding-openharmony-arm64@0.62.0': optional: true + '@oxfmt/binding-win32-arm64-msvc@0.61.0': + optional: true + '@oxfmt/binding-win32-arm64-msvc@0.62.0': optional: true + '@oxfmt/binding-win32-ia32-msvc@0.61.0': + optional: true + '@oxfmt/binding-win32-ia32-msvc@0.62.0': optional: true + '@oxfmt/binding-win32-x64-msvc@0.61.0': + optional: true + '@oxfmt/binding-win32-x64-msvc@0.62.0': optional: true @@ -16944,6 +18057,46 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} + '@shikijs/core@4.4.3': + dependencies: + '@shikijs/primitive': 4.4.3 + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + + '@shikijs/primitive@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/themes@4.4.3': + dependencies: + '@shikijs/types': 4.4.3 + + '@shikijs/types@4.4.3': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + + '@shikijs/vscode-textmate@10.0.2': {} + '@simple-git/args-pathspec@1.0.3': {} '@simple-git/argv-parser@1.1.1': @@ -17017,6 +18170,8 @@ snapshots: '@speed-highlight/core@1.2.24': {} + '@stablelib/base64@1.0.1': {} + '@standard-schema/spec@1.0.0': {} '@standard-schema/spec@1.0.0-beta.4': {} @@ -17127,10 +18282,104 @@ snapshots: '@swc/counter@0.1.3': {} + '@swc/helpers@0.5.23': + dependencies: + tslib: 2.8.1 + '@swc/types@0.1.28': dependencies: '@swc/counter': 0.1.3 + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + bundledDependencies: + - '@emnapi/core' + - '@emnapi/runtime' + - '@emnapi/wasi-threads' + - '@napi-rs/wasm-runtime' + - '@tybys/wasm-util' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@tailwindcss/postcss@4.3.3': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + postcss: 8.5.26 + tailwindcss: 4.3.3 + + '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))': + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + tailwindcss: 4.3.3 + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0) + + '@tanstack/virtual-core@3.17.7': {} + + '@tanstack/vue-virtual@3.13.35(vue@3.5.41)': + dependencies: + '@tanstack/virtual-core': 3.17.7 + vue: 3.5.41 + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -17216,6 +18465,10 @@ snapshots: '@types/geojson@7946.0.16': {} + '@types/hast@3.0.5': + dependencies: + '@types/unist': 3.0.3 + '@types/http-cache-semantics@4.2.0': {} '@types/jsesc@2.5.1': {} @@ -17224,8 +18477,17 @@ snapshots: '@types/linkify-it@5.0.0': {} + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.25 + '@types/lodash@4.17.25': {} + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -17267,6 +18529,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/web-bluetooth@0.0.21': {} + '@types/webidl-conversions@7.0.3': {} '@types/whatwg-mimetype@3.0.2': {} @@ -17348,6 +18612,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@ungap/structured-clone@1.3.3': {} + '@unhead/bundler@3.3.1(esbuild@0.28.2)(rolldown@1.2.3)(unhead@3.3.1(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0)))(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2)': dependencies: esbuild: 0.28.2 @@ -18288,6 +19554,19 @@ snapshots: vue: 3.5.41 vue-component-type-helpers: 3.3.9 + '@vueuse/core@14.4.0(vue@3.5.41)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 14.4.0 + '@vueuse/shared': 14.4.0(vue@3.5.41) + vue: 3.5.41 + + '@vueuse/metadata@14.4.0': {} + + '@vueuse/shared@14.4.0(vue@3.5.41)': + dependencies: + vue: 3.5.41 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -18916,6 +20195,8 @@ snapshots: dependencies: string-width: 4.2.3 + ansi-colors@4.1.3: {} + ansi-escapes@4.3.2: dependencies: type-fest: 0.21.3 @@ -18971,10 +20252,24 @@ snapshots: - bare-buffer - react-native-b4a + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} argue-cli@3.1.0: {} + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-pull-all-with-glob@7.1.3: + dependencies: + matcher: 6.0.0 + + arrayiffy-if-string@5.1.3: {} + arrify@2.0.1: {} assertion-error@2.0.1: {} @@ -19233,6 +20528,8 @@ snapshots: boolbase@1.0.0: {} + boolbase@2.0.0: {} + boolean@3.2.0: {} bowser@2.14.1: {} @@ -19439,6 +20736,10 @@ snapshots: char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + character-entities@2.0.2: {} chat@4.35.0(ai@7.0.19(zod@4.1.11))(workflow@5.0.0-beta.35)(zod@4.1.11): @@ -19458,6 +20759,29 @@ snapshots: check-error@2.1.3: {} + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.29.0 + whatwg-mimetype: 4.0.0 + chevrotain@10.5.0: dependencies: '@chevrotain/cst-dts-gen': 10.5.0 @@ -19492,6 +20816,10 @@ snapshots: cjs-module-lexer@2.2.0: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + classnames@2.5.1: {} clean-git-ref@2.0.1: {} @@ -19517,6 +20845,8 @@ snapshots: cli-spinners@2.9.2: {} + cli-spinners@3.4.0: {} + cli-table3@0.6.5: dependencies: string-width: 4.2.3 @@ -19538,14 +20868,26 @@ snapshots: clone@1.0.4: optional: true + clsx@2.1.1: {} + cluster-key-slot@1.1.1: {} + codsen-utils@1.7.3: + dependencies: + rfdc: 1.4.1 + color-convert@2.0.1: dependencies: color-name: 1.1.4 color-name@1.1.4: {} + color-shorthand-hex-to-six-digit@5.1.3: + dependencies: + codsen-utils: 1.7.3 + hex-color-regex: 1.1.0 + rfdc: 1.4.1 + colorette@2.0.20: {} comark@0.5.1: @@ -19559,12 +20901,16 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + commander@10.0.1: {} commander@11.1.0: {} commander@12.1.0: {} + commander@14.0.3: {} + commander@15.0.0: {} commander@2.20.3: {} @@ -19693,6 +21039,14 @@ snapshots: domutils: 3.2.2 nth-check: 2.1.1 + css-select@7.0.0: + dependencies: + boolbase: 2.0.0 + css-what: 8.0.0 + domhandler: 6.0.1 + domutils: 4.0.2 + nth-check: 3.0.1 + css-tree@2.2.1: dependencies: mdn-data: 2.0.28 @@ -19705,6 +21059,8 @@ snapshots: css-what@6.2.2: {} + css-what@8.0.0: {} + cssesc@3.0.0: {} cssnano-preset-default@8.0.4(postcss@8.5.26): @@ -19756,6 +21112,8 @@ snapshots: csstype@3.2.3: {} + culori@4.0.2: {} + d3-array@3.2.1: dependencies: internmap: 2.0.3 @@ -19832,6 +21190,8 @@ snapshots: dependencies: character-entities: 2.0.2 + decode-uri-component@0.5.0: {} + decompress-response@10.0.0: dependencies: mimic-response: 4.0.0 @@ -20106,6 +21466,21 @@ snapshots: elkjs@0.11.1: {} + email-comb@7.1.3: + dependencies: + array-pull-all-with-glob: 7.1.3 + codsen-utils: 1.7.3 + html-crush: 6.1.3 + matcher: 6.0.0 + ranges-apply: 7.1.3 + ranges-push: 7.1.3 + regex-empty-conditional-comments: 3.1.3 + string-extract-class-names: 8.1.3 + string-left-right: 6.1.3 + string-match-left-right: 9.1.3 + string-range-expander: 4.1.3 + string-uglify: 3.1.3 + emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} @@ -20120,6 +21495,11 @@ snapshots: encodeurl@2.0.0: {} + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + end-of-stream@1.4.5: dependencies: once: 1.4.0 @@ -20136,6 +21516,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + entities@7.0.1: {} entities@8.0.0: {} @@ -20301,6 +21683,8 @@ snapshots: escalade@3.2.0: {} + escape-goat@3.0.0: {} + escape-html@1.0.3: {} escape-string-regexp@4.0.0: {} @@ -20554,6 +21938,10 @@ snapshots: ext-list: 2.2.2 sort-keys-length: 1.0.1 + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + extend@3.0.2: {} fake-indexeddb@6.2.5: {} @@ -20594,6 +21982,8 @@ snapshots: fast-safe-stringify@2.1.1: {} + fast-sha256@1.3.0: {} + fast-string-truncated-width@3.0.3: {} fast-string-width@3.0.2: @@ -20702,6 +22092,8 @@ snapshots: dependencies: to-regex-range: 5.0.1 + filter-obj@5.1.0: {} + finalhandler@2.1.1: dependencies: debug: 4.4.3 @@ -21070,6 +22462,13 @@ snapshots: graphmatch@1.1.1: {} + gray-matter@4.0.3: + dependencies: + js-yaml: 3.15.1 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -21152,6 +22551,26 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.5 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.2.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.5 + + hex-color-regex@1.1.0: {} + highlight.js@10.7.3: {} hono@4.13.1: {} @@ -21160,8 +22579,27 @@ snapshots: hookable@6.1.1: {} + html-crush@6.1.3: + dependencies: + codsen-utils: 1.7.3 + ranges-apply: 7.1.3 + ranges-push: 7.1.3 + string-left-right: 6.1.3 + string-match-left-right: 9.1.3 + string-range-expander: 4.1.3 + test-mixer: 4.2.3 + html-entities@2.6.0: {} + html-void-elements@3.0.0: {} + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + htmlparser2@12.0.0: dependencies: domelementtype: 3.0.0 @@ -21169,6 +22607,13 @@ snapshots: domutils: 4.0.2 entities: 8.0.0 + htmlparser2@9.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + http-cache-semantics@4.2.0: {} http-errors@2.0.1: @@ -21227,6 +22672,10 @@ snapshots: iceberg-js@0.8.1: {} + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 @@ -21326,6 +22775,8 @@ snapshots: is-docker@3.0.0: {} + is-extendable@0.1.1: {} + is-extglob@2.1.1: {} is-fullwidth-code-point@3.0.0: {} @@ -21387,6 +22838,8 @@ snapshots: is-unsafe@2.0.0: {} + is-url-superb@6.1.0: {} + is-valid-glob@1.0.0: {} is-wsl@2.2.0: @@ -21473,6 +22926,11 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.15.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.3.1: dependencies: argparse: 2.0.1 @@ -21540,6 +22998,17 @@ snapshots: ms: 2.1.3 semver: 7.8.5 + juice@12.1.2: + dependencies: + cheerio: 1.2.0 + commander: 14.0.3 + entities: 8.0.0 + postcss: 8.5.26 + postcss-nesting: 14.0.1(postcss@8.5.26) + postcss-safe-parser: 7.0.1(postcss@8.5.26) + postcss-selector-parser: 7.1.5 + web-resource-inliner: 8.0.0 + just-bash@3.2.0: dependencies: diff: 8.0.4 @@ -21644,39 +23113,88 @@ snapshots: '@libsql/linux-x64-musl': 0.5.29 '@libsql/win32-x64-msvc': 0.5.29 + lightningcss-android-arm64@1.32.0: + optional: true + lightningcss-android-arm64@1.33.0: optional: true + lightningcss-darwin-arm64@1.32.0: + optional: true + lightningcss-darwin-arm64@1.33.0: optional: true + lightningcss-darwin-x64@1.32.0: + optional: true + lightningcss-darwin-x64@1.33.0: optional: true + lightningcss-freebsd-x64@1.32.0: + optional: true + lightningcss-freebsd-x64@1.33.0: optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: optional: true + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + lightningcss-linux-arm64-gnu@1.33.0: optional: true + lightningcss-linux-arm64-musl@1.32.0: + optional: true + lightningcss-linux-arm64-musl@1.33.0: optional: true + lightningcss-linux-x64-gnu@1.32.0: + optional: true + lightningcss-linux-x64-gnu@1.33.0: optional: true + lightningcss-linux-x64-musl@1.32.0: + optional: true + lightningcss-linux-x64-musl@1.33.0: optional: true + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + lightningcss-win32-arm64-msvc@1.33.0: optional: true + lightningcss-win32-x64-msvc@1.32.0: + optional: true + lightningcss-win32-x64-msvc@1.33.0: optional: true + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: dependencies: detect-libc: 2.1.2 @@ -21740,6 +23258,8 @@ snapshots: dependencies: p-locate: 6.0.0 + lodash-es@4.18.1: {} + lodash.includes@4.3.0: {} lodash.isboolean@3.0.3: {} @@ -21763,6 +23283,11 @@ snapshots: chalk: 5.6.2 is-unicode-supported: 1.3.0 + log-symbols@7.0.1: + dependencies: + is-unicode-supported: 2.1.0 + yoctocolors: 2.2.0 + log-update@8.0.0: dependencies: ansi-escapes: 7.3.0 @@ -21838,6 +23363,16 @@ snapshots: type-fest: 4.41.0 web-worker: 1.5.0 + markdown-exit@1.0.0-beta.9: + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + entities: 7.0.1 + linkify-it: 5.0.2 + mdurl: 2.1.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-exit@1.1.0-beta.2: dependencies: '@types/linkify-it': 5.0.0 @@ -21848,6 +23383,15 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + markdown-it@14.3.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.2 + mdurl: 2.1.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-table@3.0.4: {} marked-terminal@7.3.0(marked@9.1.6): @@ -21867,6 +23411,10 @@ snapshots: dependencies: escape-string-regexp: 4.0.0 + matcher@6.0.0: + dependencies: + escape-string-regexp: 5.0.0 + math-intrinsics@1.1.0: {} mdast-util-find-and-replace@3.0.2: @@ -21955,6 +23503,18 @@ snapshots: '@types/mdast': 4.0.4 unist-util-is: 6.0.1 + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.5 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.3 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 @@ -22211,6 +23771,8 @@ snapshots: dependencies: mime-db: 1.54.0 + mime@2.6.0: {} + mime@3.0.0: {} mime@4.1.0: {} @@ -22585,6 +24147,8 @@ snapshots: node-releases@2.0.53: {} + nodemailer@9.0.5: {} + nopt@7.2.1: dependencies: abbrev: 2.0.0 @@ -22616,6 +24180,10 @@ snapshots: dependencies: boolbase: 1.0.0 + nth-check@3.0.1: + dependencies: + boolbase: 2.0.0 + nuxt-define@1.0.0: {} nuxt@4.5.2(@types/node@24.13.3)(rolldown@1.2.3): @@ -22753,6 +24321,11 @@ snapshots: object-assign@4.1.1: {} + object-boolean-combinations@6.2.3: + dependencies: + codsen-utils: 1.7.3 + rfdc: 1.4.1 + object-identity@0.2.3: {} object-inspect@1.13.4: {} @@ -22797,6 +24370,14 @@ snapshots: dependencies: mimic-function: 5.0.1 + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + onnxruntime-common@1.24.0-dev.20251116-b39e144322: {} onnxruntime-common@1.24.3: {} @@ -22861,6 +24442,17 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 + ora@9.4.1: + dependencies: + chalk: 5.6.2 + cli-cursor: 5.0.0 + cli-spinners: 3.4.0 + is-interactive: 2.0.0 + is-unicode-supported: 2.1.0 + log-symbols: 7.0.1 + stdin-discarder: 0.3.2 + string-width: 8.2.2 + os-paths@4.4.0: {} oxc-parser@0.131.0: @@ -23021,6 +24613,30 @@ snapshots: dependencies: rolldown: 1.2.3 + oxfmt@0.61.0: + dependencies: + tinypool: 2.1.0 + optionalDependencies: + '@oxfmt/binding-android-arm-eabi': 0.61.0 + '@oxfmt/binding-android-arm64': 0.61.0 + '@oxfmt/binding-darwin-arm64': 0.61.0 + '@oxfmt/binding-darwin-x64': 0.61.0 + '@oxfmt/binding-freebsd-x64': 0.61.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.61.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.61.0 + '@oxfmt/binding-linux-arm64-gnu': 0.61.0 + '@oxfmt/binding-linux-arm64-musl': 0.61.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.61.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.61.0 + '@oxfmt/binding-linux-riscv64-musl': 0.61.0 + '@oxfmt/binding-linux-s390x-gnu': 0.61.0 + '@oxfmt/binding-linux-x64-gnu': 0.61.0 + '@oxfmt/binding-linux-x64-musl': 0.61.0 + '@oxfmt/binding-openharmony-arm64': 0.61.0 + '@oxfmt/binding-win32-arm64-msvc': 0.61.0 + '@oxfmt/binding-win32-ia32-msvc': 0.61.0 + '@oxfmt/binding-win32-x64-msvc': 0.61.0 + oxfmt@0.62.0: dependencies: tinypool: 2.1.0 @@ -23162,10 +24778,23 @@ snapshots: dependencies: parse5: 6.0.1 + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + parse5@5.1.1: {} parse5@6.0.1: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} partial-json@0.1.7: {} @@ -23281,6 +24910,8 @@ snapshots: possible-typed-array-names@1.1.0: {} + postal-mime@2.7.4: {} + postcss-calc@10.1.1(postcss@8.5.26): dependencies: postcss: 8.5.26 @@ -23359,6 +24990,13 @@ snapshots: postcss: 8.5.26 postcss-selector-parser: 7.1.5 + postcss-nesting@14.0.1(postcss@8.5.26): + dependencies: + '@csstools/selector-resolve-nested': 4.0.1(postcss-selector-parser@7.1.5) + '@csstools/selector-specificity': 6.0.0(postcss-selector-parser@7.1.5) + postcss: 8.5.26 + postcss-selector-parser: 7.1.5 + postcss-normalize-charset@8.0.2(postcss@8.5.26): dependencies: postcss: 8.5.26 @@ -23421,11 +25059,20 @@ snapshots: postcss: 8.5.26 postcss-value-parser: 4.2.0 + postcss-safe-parser@7.0.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + postcss-selector-parser@7.1.5: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 + postcss-sort-media-queries@6.7.1(postcss@8.5.26): + dependencies: + postcss: 8.5.26 + sort-css-media-queries: 3.0.5 + postcss-svgo@8.0.3(postcss@8.5.26): dependencies: postcss: 8.5.26 @@ -23517,6 +25164,8 @@ snapshots: retry: 0.12.0 signal-exit: 3.0.7 + property-information@7.2.0: {} + proto-list@1.2.4: {} protobufjs@7.6.5: @@ -23583,6 +25232,12 @@ snapshots: quansync@1.0.0: {} + query-string@9.5.0: + dependencies: + decode-uri-component: 0.5.0 + filter-obj: 5.1.0 + split-on-first: 3.0.0 + queue-microtask@1.2.3: {} quick-lru@5.1.1: {} @@ -23605,6 +25260,25 @@ snapshots: range-parser@1.3.0: {} + ranges-apply@7.1.3: + dependencies: + ranges-merge: 9.1.3 + tiny-invariant: 1.3.3 + + ranges-merge@9.1.3: + dependencies: + ranges-push: 7.1.3 + ranges-sort: 6.1.3 + + ranges-push@7.1.3: + dependencies: + codsen-utils: 1.7.3 + ranges-sort: 6.1.3 + string-collapse-leading-whitespace: 7.1.3 + string-trim-spaces-only: 5.1.3 + + ranges-sort@6.1.3: {} + raw-body@3.0.2: dependencies: bytes: 3.1.2 @@ -23674,10 +25348,38 @@ snapshots: reflect-metadata@0.2.2: {} + regex-empty-conditional-comments@3.1.3: {} + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + regexp-to-ast@0.5.0: {} regexp-tree@0.1.27: {} + reka-ui@2.10.3(vue@3.5.41): + dependencies: + '@floating-ui/dom': 1.8.0 + '@floating-ui/vue': 1.1.11(vue@3.5.41) + '@internationalized/date': 3.12.3 + '@internationalized/number': 3.6.7 + '@tanstack/vue-virtual': 3.13.35(vue@3.5.41) + '@vueuse/core': 14.4.0(vue@3.5.41) + '@vueuse/shared': 14.4.0(vue@3.5.41) + aria-hidden: 1.2.6 + defu: 6.1.7 + ohash: 2.0.11 + vue: 3.5.41 + transitivePeerDependencies: + - '@vue/composition-api' + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 @@ -23719,6 +25421,11 @@ snapshots: transitivePeerDependencies: - supports-color + resend@6.17.1: + dependencies: + postal-mime: 2.7.4 + standardwebhooks: 1.0.0 + resolve-alpn@1.2.1: {} resolve-from@4.0.0: {} @@ -23764,6 +25471,8 @@ snapshots: reusify@1.1.0: {} + rfdc@1.4.1: {} + rimraf@5.0.10: dependencies: glob: 10.5.0 @@ -23910,6 +25619,11 @@ snapshots: scule@1.3.0: {} + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + seedrandom@3.0.5: {} seek-bzip@2.0.0: @@ -24031,6 +25745,17 @@ snapshots: shell-quote@1.10.0: {} + shiki@4.4.3: + dependencies: + '@shikijs/core': 4.4.3 + '@shikijs/engine-javascript': 4.4.3 + '@shikijs/engine-oniguruma': 4.4.3 + '@shikijs/langs': 4.4.3 + '@shikijs/themes': 4.4.3 + '@shikijs/types': 4.4.3 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.5 + side-channel-list@1.0.1: dependencies: es-errors: 1.3.0 @@ -24186,6 +25911,8 @@ snapshots: ip-address: 10.4.0 smart-buffer: 4.2.0 + sort-css-media-queries@3.0.5: {} + sort-keys-length@1.0.1: dependencies: sort-keys: 1.1.2 @@ -24205,12 +25932,18 @@ snapshots: source-map@0.7.6: {} + space-separated-tokens@2.0.2: {} + sparse-bitfield@3.0.3: dependencies: memory-pager: 1.5.0 + split-on-first@3.0.0: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} + sprintf-js@1.1.3: {} sqids@0.3.0: {} @@ -24248,6 +25981,11 @@ snapshots: standard-as-callback@2.1.0: {} + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + statuses@2.0.2: {} std-env@3.10.0: {} @@ -24256,6 +25994,8 @@ snapshots: stdin-discarder@0.2.2: {} + stdin-discarder@0.3.2: {} + stream-browserify@3.0.0: dependencies: inherits: 2.0.4 @@ -24276,6 +26016,43 @@ snapshots: - bare-abort-controller - react-native-b4a + string-character-is-astral-surrogate@3.1.3: {} + + string-collapse-leading-whitespace@7.1.3: {} + + string-extract-class-names@8.1.3: + dependencies: + string-left-right: 6.1.3 + + string-left-right@6.1.3: + dependencies: + codsen-utils: 1.7.3 + rfdc: 1.4.1 + + string-match-left-right@9.1.3: + dependencies: + arrayiffy-if-string: 5.1.3 + codsen-utils: 1.7.3 + string-character-is-astral-surrogate: 3.1.3 + + string-range-expander@4.1.3: + dependencies: + codsen-utils: 1.7.3 + + string-strip-html@13.5.3: + dependencies: + '@types/lodash-es': 4.17.12 + codsen-utils: 1.7.3 + html-entities: 2.6.0 + lodash-es: 4.18.1 + ranges-apply: 7.1.3 + ranges-push: 7.1.3 + string-left-right: 6.1.3 + + string-trim-spaces-only@5.1.3: {} + + string-uglify@3.1.3: {} + string-width@4.2.3: dependencies: emoji-regex: 8.0.0 @@ -24307,6 +26084,11 @@ snapshots: dependencies: safe-buffer: 5.2.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -24315,6 +26097,8 @@ snapshots: dependencies: ansi-regex: 6.2.2 + strip-bom-string@1.0.0: {} + strip-dirs@3.0.0: dependencies: inspect-with-kind: 1.0.5 @@ -24411,6 +26195,10 @@ snapshots: tagged-tag@1.0.0: {} + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.3: {} + tapable@2.3.3: {} tar-fs@2.1.5: @@ -24488,6 +26276,11 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + test-mixer@4.2.3: + dependencies: + object-boolean-combinations: 6.2.3 + rfdc: 1.4.1 + text-decoder@1.2.7: dependencies: b4a: 1.8.1 @@ -24579,6 +26372,8 @@ snapshots: tree-kill@1.2.2: {} + trim-lines@3.0.1: {} + trough@2.2.0: {} ts-algebra@2.0.0: {} @@ -24640,6 +26435,8 @@ snapshots: dependencies: '@mixmark-io/domino': 2.2.0 + tw-animate-css@1.4.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -24813,6 +26610,10 @@ snapshots: dependencies: '@types/unist': 3.0.3 + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 @@ -24853,11 +26654,61 @@ snapshots: unpipe@1.0.0: {} + unplugin-auto-import@21.1.0(@vueuse/core@14.4.0(vue@3.5.41)): + dependencies: + '@vueuse/core': 14.4.0(vue@3.5.41) + local-pkg: 1.2.1 + magic-string: 1.1.0 + picomatch: 4.0.5 + unimport: 6.4.0(rolldown@1.2.3) + unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2) + unplugin-utils: 0.3.2 + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - oxc-parser + - unloader + unplugin-utils@0.3.2: dependencies: pathe: 2.0.3 picomatch: 4.0.5 + unplugin-vue-components@32.1.0(@nuxt/kit@3.21.11)(vue@3.5.41): + dependencies: + '@nuxt/kit': 3.21.11 + chokidar: 5.0.0 + local-pkg: 1.2.1 + magic-string: 0.30.21 + mlly: 1.8.2 + obug: 2.1.4 + picomatch: 4.0.5 + tinyglobby: 0.2.17 + unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2) + unplugin-utils: 0.3.2 + vue: 3.5.41 + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - unloader + + unplugin-vue-markdown@32.0.0(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0)): + dependencies: + '@mdit-vue/plugin-component': 3.0.2 + '@mdit-vue/plugin-frontmatter': 3.0.2 + '@mdit-vue/types': 3.0.2 + markdown-exit: 1.0.0-beta.9 + unplugin: 3.3.0(esbuild@0.28.2)(rolldown@1.2.3)(rollup@4.62.4)(vite@8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0))(webpack@5.109.2) + unplugin-utils: 0.3.2 + vite: 8.2.1(@types/node@24.13.3)(esbuild@0.27.7)(jiti@2.7.0)(terser@5.49.2)(tsx@4.23.11)(yaml@2.9.0) + transitivePeerDependencies: + - '@farmfe/core' + - '@rspack/core' + - bun-types-no-globals + - unloader + unplugin@2.3.11: dependencies: '@jridgewell/remapping': 2.3.5 @@ -24968,6 +26819,8 @@ snapshots: valibot@1.4.2: {} + valid-data-url@3.0.1: {} + validate-npm-package-name@5.0.1: {} vary@1.1.2: {} @@ -25364,6 +27217,10 @@ snapshots: vue-component-type-helpers@3.3.9: {} + vue-demi@0.14.10(vue@3.5.41): + dependencies: + vue: 3.5.41 + vue-devtools-stub@0.1.0: {} vue-i18n-extract@2.0.7: @@ -25436,6 +27293,14 @@ snapshots: defaults: 1.0.4 optional: true + web-resource-inliner@8.0.0: + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 9.1.0 + mime: 2.6.0 + valid-data-url: 3.0.1 + web-streams-polyfill@3.3.3: {} web-worker@1.5.0: {} @@ -25483,8 +27348,14 @@ snapshots: - postcss - uglify-js + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} + whatwg-url@14.2.0: dependencies: tr46: 5.1.1 From f3810e21fe3f5652b46f9bc4072f4b111c5f205a Mon Sep 17 00:00:00 2001 From: RedStar Date: Tue, 11 Aug 2026 20:33:12 +0200 Subject: [PATCH 03/12] feat: add mailing and organizations --- .env.example | 19 + .github/workflows/ci.yaml | 35 + apps/auth-server/package.json | 1 + apps/auth-server/src/index.ts | 17 +- apps/dashboard/app/auth.config.ts | 5 + .../organizations/components/InviteForm.vue | 74 ++ .../organizations/components/MemberList.vue | 49 ++ .../organizations/components/Switcher.vue | 34 + .../composables/useOrganizations.ts | 125 ++++ .../organizations/types/organization.ts | 28 + .../modules/shared/components/app/Sidebar.vue | 6 + .../organizations/accept-invitation/[id].vue | 51 ++ .../(organizations)/organizations/index.vue | 33 + apps/dashboard/i18n/locale-features.json | 2 +- .../i18n/locales/en/organizations.json | 43 ++ .../i18n/locales/it/organizations.json | 43 ++ .../i18n/schemas/organizations.schema.json | 132 ++++ apps/dashboard/nuxt.config.ts | 14 +- .../drizzle/0001_peaceful_franklin_storm.sql | 41 ++ packages/auth/drizzle/meta/0001_snapshot.json | 688 ++++++++++++++++++ packages/auth/drizzle/meta/_journal.json | 7 + packages/auth/src/auth.test.ts | 36 +- packages/auth/src/auth.ts | 65 ++ packages/auth/src/config.test.ts | 24 + packages/auth/src/config.ts | 30 + packages/auth/src/index.ts | 3 + packages/auth/src/schema.ts | 67 +- pnpm-lock.yaml | 3 + 28 files changed, 1670 insertions(+), 5 deletions(-) create mode 100644 apps/dashboard/app/modules/organizations/components/InviteForm.vue create mode 100644 apps/dashboard/app/modules/organizations/components/MemberList.vue create mode 100644 apps/dashboard/app/modules/organizations/components/Switcher.vue create mode 100644 apps/dashboard/app/modules/organizations/composables/useOrganizations.ts create mode 100644 apps/dashboard/app/modules/organizations/types/organization.ts create mode 100644 apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue create mode 100644 apps/dashboard/app/pages/(organizations)/organizations/index.vue create mode 100644 apps/dashboard/i18n/locales/en/organizations.json create mode 100644 apps/dashboard/i18n/locales/it/organizations.json create mode 100644 apps/dashboard/i18n/schemas/organizations.schema.json create mode 100644 packages/auth/drizzle/0001_peaceful_franklin_storm.sql create mode 100644 packages/auth/drizzle/meta/0001_snapshot.json diff --git a/.env.example b/.env.example index 986ee1c..6d343e5 100644 --- a/.env.example +++ b/.env.example @@ -16,6 +16,25 @@ AUTH_ENABLE_SIGNUP=false # Both halves are required before the GitHub button is offered. GITHUB_CLIENT_ID= GITHUB_CLIENT_SECRET= +# Organizations are off unless this is exactly "true". Enabling them requires a working mail +# transport below, because invitations are delivered by email. +AUTH_ENABLE_ORGANIZATIONS=false +# Whether any signed-in user may create an organization. Ignored while organizations are off. +AUTH_ALLOW_ORGANIZATION_CREATION=false + +# Mail (packages/mail). "console" logs instead of delivering and is the default, so an +# unconfigured deployment cannot silently attempt real delivery. Use "resend" or "smtp" in +# production. +MAIL_PROVIDER=console +MAIL_FROM=noreply@example.com +# Required when MAIL_PROVIDER=resend. +RESEND_API_KEY= +# Required when MAIL_PROVIDER=smtp. SMTP_SECURE is implicit TLS: true on 465, false on 587. +SMTP_HOST= +SMTP_PORT=587 +SMTP_SECURE=false +SMTP_USER= +SMTP_PASSWORD= # Dashboard (apps/dashboard). Points the Better Auth client at the adapter. Sign-in capabilities # are derived from AUTH_ENABLE_SIGNUP and the GitHub OAuth credentials above at build time, so diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d1a895..e32375a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -89,3 +89,38 @@ jobs: - name: 📦 Verify package version run: | node --input-type=module --eval "Promise.all([import('./packages/shared/dist/index.mjs'), import('./packages/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" + + knip: + name: 🧹 Unused code check + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup-toolchain + + - name: 🧹 Check for unused code + run: aube run knip + + i18n: + name: 🌐 i18n validation + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup-toolchain + + - name: 🌐 Check for missing or dynamic i18n keys + run: aube run i18n:report + + - name: 🌐 Check i18n schema is up to date + run: | + aube run i18n:schema + git diff --exit-code packages/i18n/schema.json packages/i18n/schemas packages/i18n/locales diff --git a/apps/auth-server/package.json b/apps/auth-server/package.json index 7067dae..f9f1c00 100644 --- a/apps/auth-server/package.json +++ b/apps/auth-server/package.json @@ -14,6 +14,7 @@ }, "dependencies": { "@agent-zero/auth": "workspace:*", + "@agent-zero/mail": "workspace:*", "@agent-zero/shared": "workspace:*", "@hono/node-server": "^1.19.17", "hono": "^4.13.1" diff --git a/apps/auth-server/src/index.ts b/apps/auth-server/src/index.ts index b45be6f..6362d1f 100644 --- a/apps/auth-server/src/index.ts +++ b/apps/auth-server/src/index.ts @@ -1,6 +1,7 @@ import process from 'node:process'; import { authOptionsFromEnvironment, createAuth } from '@agent-zero/auth'; +import { createMailer } from '@agent-zero/mail'; import { redactSecrets, secretValuesFromEnvironment } from '@agent-zero/shared'; import { serve } from '@hono/node-server'; import { Hono } from 'hono'; @@ -37,7 +38,21 @@ function resolvePort(value: string | undefined): number { } const options = authOptionsFromEnvironment(); -const auth = createAuth(options); + +// This process is the composition root for authentication, so it is where the mail transport is +// bound and injected. `packages/auth` declares the delivery contract structurally and never +// imports `@agent-zero/mail`, which keeps one capability package from depending on another. +const sendMail = createMailer(); + +const auth = createAuth({ + ...options, + sendInvitationEmail: ({ to, organizationName, inviterName, acceptUrl }) => + sendMail({ + to, + templateId: 'organizationInvitation', + context: { organizationName, inviterName, acceptUrl }, + }), +}); const app = new Hono(); diff --git a/apps/dashboard/app/auth.config.ts b/apps/dashboard/app/auth.config.ts index 9400d45..be2af5a 100644 --- a/apps/dashboard/app/auth.config.ts +++ b/apps/dashboard/app/auth.config.ts @@ -1,4 +1,5 @@ import { defineClientAuth } from '@onmax/nuxt-better-auth/config'; +import { organizationClient } from 'better-auth/client/plugins'; // Better Auth runs in `apps/auth-server`, on its own origin, so in client-only mode `siteUrl` // resolves to the auth adapter rather than to this app. Every call is therefore cross-origin: the @@ -6,4 +7,8 @@ import { defineClientAuth } from '@onmax/nuxt-better-auth/config'; export default defineClientAuth((ctx) => ({ baseURL: ctx.siteUrl, fetchOptions: { credentials: 'include' }, + // Registered unconditionally: the client plugin only adds callable methods, and whether the + // deployment actually serves them is decided by the auth server's own policy. Gating it on a + // build-time flag would let a stale dashboard build lose access to an enabled feature. + plugins: [organizationClient()], })); diff --git a/apps/dashboard/app/modules/organizations/components/InviteForm.vue b/apps/dashboard/app/modules/organizations/components/InviteForm.vue new file mode 100644 index 0000000..76f270a --- /dev/null +++ b/apps/dashboard/app/modules/organizations/components/InviteForm.vue @@ -0,0 +1,74 @@ + + + diff --git a/apps/dashboard/app/modules/organizations/components/MemberList.vue b/apps/dashboard/app/modules/organizations/components/MemberList.vue new file mode 100644 index 0000000..c8df3e8 --- /dev/null +++ b/apps/dashboard/app/modules/organizations/components/MemberList.vue @@ -0,0 +1,49 @@ + + + diff --git a/apps/dashboard/app/modules/organizations/components/Switcher.vue b/apps/dashboard/app/modules/organizations/components/Switcher.vue new file mode 100644 index 0000000..cbdbcb8 --- /dev/null +++ b/apps/dashboard/app/modules/organizations/components/Switcher.vue @@ -0,0 +1,34 @@ + + + diff --git a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts new file mode 100644 index 0000000..9045242 --- /dev/null +++ b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts @@ -0,0 +1,125 @@ +// Imported explicitly rather than relying on Nuxt auto-imports: the package's plain `tsc` pass +// checks `app/**/*.ts` without the generated auto-import declarations that `vue-tsc` sees. +import { useAuthClient } from '@onmax/nuxt-better-auth/composables'; +import { useState } from 'nuxt/app'; + +import type { Organization, OrganizationMember, OrganizationRole } from '../types/organization.js'; + +/** + * Organization state for the dashboard. + * + * Wraps the Better Auth client so components deal in plain reactive state and a few actions + * rather than in client-plugin call shapes. Every mutation refetches rather than patching local + * state: the auth server owns membership and roles, and a locally patched list would diverge the + * moment a call is rejected by a policy the dashboard cannot see. + */ +export function useOrganizations() { + // Null in client-only mode before hydration, so every action guards on it rather than assuming + // a client exists. + const client = useAuthClient(); + + const organizations = useState('organizations', () => []); + const activeOrganization = useState('organizations:active', () => null); + const members = useState('organizations:members', () => []); + const pending = useState('organizations:pending', () => false); + const error = useState('organizations:error', () => null); + + /** Surface the server's message without assuming a shape the plugin may not return. */ + function messageFrom(cause: unknown): string { + if (cause && typeof cause === 'object' && 'message' in cause) { + const { message } = cause as { message?: unknown }; + if (typeof message === 'string' && message !== '') return message; + } + return 'organizations.errors.generic'; + } + + /** + * Run one client call with shared pending and error handling. + * + * The null check lives here so each action stays a single call: before hydration there is no + * client, and that is an ordinary "nothing to do yet" rather than an error to surface. + */ + async function run( + action: (authClient: NonNullable) => Promise, + ): Promise { + if (!client) return undefined; + pending.value = true; + error.value = null; + try { + return await action(client); + } catch (cause) { + error.value = messageFrom(cause); + return undefined; + } finally { + pending.value = false; + } + } + + async function refresh() { + await run(async (authClient) => { + const { data } = await authClient.organization.list(); + organizations.value = data ?? []; + }); + } + + async function refreshMembers() { + const organizationId = activeOrganization.value?.id; + if (!organizationId) { + members.value = []; + return; + } + await run(async (authClient) => { + const { data } = await authClient.organization.listMembers({ query: { organizationId } }); + members.value = data?.members ?? []; + }); + } + + async function setActive(organizationId: string) { + await run(async (authClient) => { + const { data } = await authClient.organization.setActive({ organizationId }); + activeOrganization.value = data ?? null; + }); + await refreshMembers(); + } + + async function create(input: { name: string; slug: string }) { + const created = await run(async (authClient) => { + const { data } = await authClient.organization.create(input); + return data; + }); + if (created) await refresh(); + return created; + } + + async function inviteMember(input: { email: string; role: OrganizationRole }) { + const organizationId = activeOrganization.value?.id; + if (!organizationId) return undefined; + return run(async (authClient) => { + const { data } = await authClient.organization.inviteMember({ ...input, organizationId }); + return data; + }); + } + + async function removeMember(memberIdOrEmail: string) { + const organizationId = activeOrganization.value?.id; + if (!organizationId) return; + await run(async (authClient) => { + await authClient.organization.removeMember({ memberIdOrEmail, organizationId }); + }); + await refreshMembers(); + } + + return { + organizations, + activeOrganization, + members, + pending, + error, + refresh, + refreshMembers, + setActive, + create, + inviteMember, + removeMember, + }; +} diff --git a/apps/dashboard/app/modules/organizations/types/organization.ts b/apps/dashboard/app/modules/organizations/types/organization.ts new file mode 100644 index 0000000..0d34a72 --- /dev/null +++ b/apps/dashboard/app/modules/organizations/types/organization.ts @@ -0,0 +1,28 @@ +/** + * The organization shapes the dashboard renders. + * + * Declared here rather than imported from `@agent-zero/auth`: that package pulls Better Auth and + * its database adapter, which must not reach a browser bundle. These mirror the fields the client + * plugin returns, narrowed to what the UI actually reads. + */ + +/** Roles the invitation and member views can assign. */ +export const ORGANIZATION_ROLES = ['member', 'admin', 'owner'] as const; + +export type OrganizationRole = (typeof ORGANIZATION_ROLES)[number]; + +export interface Organization { + readonly id: string; + readonly name: string; + readonly slug: string; + readonly logo?: string | null; +} + +export interface OrganizationMember { + readonly id: string; + readonly role: string; + readonly user: { + readonly name: string; + readonly email: string; + }; +} diff --git a/apps/dashboard/app/modules/shared/components/app/Sidebar.vue b/apps/dashboard/app/modules/shared/components/app/Sidebar.vue index 1edb81f..3df9447 100644 --- a/apps/dashboard/app/modules/shared/components/app/Sidebar.vue +++ b/apps/dashboard/app/modules/shared/components/app/Sidebar.vue @@ -45,6 +45,9 @@ + + @@ -98,6 +101,9 @@ import { version } from '~~/package.json'; const collapsed = useSidebarCollapsed(); +const appConfig = useAppConfig(); +const enableOrganizations = appConfig.auth.enableOrganizations; + const navItems = [ { key: 'control', diff --git a/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue b/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue new file mode 100644 index 0000000..b91e454 --- /dev/null +++ b/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue @@ -0,0 +1,51 @@ + + + diff --git a/apps/dashboard/app/pages/(organizations)/organizations/index.vue b/apps/dashboard/app/pages/(organizations)/organizations/index.vue new file mode 100644 index 0000000..96e6288 --- /dev/null +++ b/apps/dashboard/app/pages/(organizations)/organizations/index.vue @@ -0,0 +1,33 @@ + + + diff --git a/apps/dashboard/i18n/locale-features.json b/apps/dashboard/i18n/locale-features.json index 7f33744..aa443db 100644 --- a/apps/dashboard/i18n/locale-features.json +++ b/apps/dashboard/i18n/locale-features.json @@ -1,3 +1,3 @@ { - "features": ["common.json", "errors.json", "auth.json", "dashboard.json"] + "features": ["common.json", "errors.json", "auth.json", "dashboard.json", "organizations.json"] } diff --git a/apps/dashboard/i18n/locales/en/organizations.json b/apps/dashboard/i18n/locales/en/organizations.json new file mode 100644 index 0000000..a53ec64 --- /dev/null +++ b/apps/dashboard/i18n/locales/en/organizations.json @@ -0,0 +1,43 @@ +{ + "$schema": "../../schemas/organizations.schema.json", + "organizations": { + "title": "Organizations", + "subtitle": "Members and invitations for the selected organization.", + "noneSelected": "Select an organization to manage its members.", + "switcher": { + "label": "Organization" + }, + "roles": { + "member": "Member", + "admin": "Admin", + "owner": "Owner" + }, + "members": { + "title": "Members", + "empty": "This organization has no members yet.", + "name": "Name", + "email": "Email", + "role": "Role", + "actions": "Actions", + "remove": "Remove" + }, + "invite": { + "title": "Invite a member", + "email": "Email", + "role": "Role", + "submit": "Send invitation", + "submitPending": "Sending…", + "sent": "Invitation sent to {email}." + }, + "accept": { + "title": "Organization invitation", + "pending": "Accepting your invitation…", + "accepted": "You have joined the organization.", + "failed": "This invitation is no longer valid. It may have expired or already been used.", + "continue": "Go to dashboard" + }, + "errors": { + "generic": "That organization action could not be completed." + } + } +} diff --git a/apps/dashboard/i18n/locales/it/organizations.json b/apps/dashboard/i18n/locales/it/organizations.json new file mode 100644 index 0000000..1dac5a0 --- /dev/null +++ b/apps/dashboard/i18n/locales/it/organizations.json @@ -0,0 +1,43 @@ +{ + "$schema": "../../schemas/organizations.schema.json", + "organizations": { + "title": "Organizzazioni", + "subtitle": "Membri e inviti dell'organizzazione selezionata.", + "noneSelected": "Seleziona un'organizzazione per gestirne i membri.", + "switcher": { + "label": "Organizzazione" + }, + "roles": { + "member": "Membro", + "admin": "Amministratore", + "owner": "Proprietario" + }, + "members": { + "title": "Membri", + "empty": "Questa organizzazione non ha ancora membri.", + "name": "Nome", + "email": "Email", + "role": "Ruolo", + "actions": "Azioni", + "remove": "Rimuovi" + }, + "invite": { + "title": "Invita un membro", + "email": "Email", + "role": "Ruolo", + "submit": "Invia invito", + "submitPending": "Invio in corso…", + "sent": "Invito inviato a {email}." + }, + "accept": { + "title": "Invito all'organizzazione", + "pending": "Accettazione dell'invito in corso…", + "accepted": "Sei entrato nell'organizzazione.", + "failed": "Questo invito non è più valido. Potrebbe essere scaduto o già stato usato.", + "continue": "Vai alla dashboard" + }, + "errors": { + "generic": "Non è stato possibile completare l'operazione sull'organizzazione." + } + } +} diff --git a/apps/dashboard/i18n/schemas/organizations.schema.json b/apps/dashboard/i18n/schemas/organizations.schema.json new file mode 100644 index 0000000..4774551 --- /dev/null +++ b/apps/dashboard/i18n/schemas/organizations.schema.json @@ -0,0 +1,132 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Agent Zero dashboard i18n locale file (organizations.json)", + "description": "Schema for organizations.json. Generated from en/organizations.json — do not edit manually.", + "type": "object", + "properties": { + "organizations": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "noneSelected": { + "type": "string" + }, + "switcher": { + "type": "object", + "properties": { + "label": { + "type": "string" + } + }, + "additionalProperties": false + }, + "roles": { + "type": "object", + "properties": { + "member": { + "type": "string" + }, + "admin": { + "type": "string" + }, + "owner": { + "type": "string" + } + }, + "additionalProperties": false + }, + "members": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "empty": { + "type": "string" + }, + "name": { + "type": "string" + }, + "email": { + "type": "string" + }, + "role": { + "type": "string" + }, + "actions": { + "type": "string" + }, + "remove": { + "type": "string" + } + }, + "additionalProperties": false + }, + "invite": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "email": { + "type": "string" + }, + "role": { + "type": "string" + }, + "submit": { + "type": "string" + }, + "submitPending": { + "type": "string" + }, + "sent": { + "type": "string" + } + }, + "additionalProperties": false + }, + "accept": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "pending": { + "type": "string" + }, + "accepted": { + "type": "string" + }, + "failed": { + "type": "string" + }, + "continue": { + "type": "string" + } + }, + "additionalProperties": false + }, + "errors": { + "type": "object", + "properties": { + "generic": { + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "$schema": { + "type": "string" + } + }, + "additionalProperties": false +} diff --git a/apps/dashboard/nuxt.config.ts b/apps/dashboard/nuxt.config.ts index 1f943b3..9381a4c 100644 --- a/apps/dashboard/nuxt.config.ts +++ b/apps/dashboard/nuxt.config.ts @@ -49,11 +49,18 @@ export default defineNuxtConfig({ { path: '~/modules/shared/components' }, { path: '~/modules/auth/components' }, { path: '~/modules/dashboard/components' }, + // Prefixed so the module's generic names (Switcher, MemberList, InviteForm) cannot collide + // with another module's component of the same name. + { path: '~/modules/organizations/components', prefix: 'Organizations' }, ], imports: { // Composables also moved out of `app/composables`; Nuxt auto-imports by exported symbol name, // so call sites (useAuthErrorMessage(), useSidebarCollapsed()) are unaffected. - dirs: ['modules/auth/composables', 'modules/shared/composables'], + dirs: [ + 'modules/auth/composables', + 'modules/shared/composables', + 'modules/organizations/composables', + ], }, icon: { // The dashboard owns no Nitro routes and its e2e suite asserts that nothing hits a local @@ -111,6 +118,7 @@ export default defineNuxtConfig({ auth: { enableSignup: authPolicy.enableSignup, enableGithubOauth: authPolicy.enableGithubOauth, + enableOrganizations: authPolicy.enableOrganizations, }, }, app: { @@ -122,6 +130,10 @@ export default defineNuxtConfig({ routeRules: { '/': { appLayout: 'default', auth: { only: 'user' } }, [loginPath]: { auth: { only: 'guest' } }, + '/organizations': { appLayout: 'default', auth: { only: 'user' } }, + // Reached from an invitation email, so the visitor is frequently signed out at that moment: + // requiring a session sends them through /login and back, rather than rejecting the link. + '/organizations/accept-invitation/**': { appLayout: 'default', auth: { only: 'user' } }, }, typescript: { strict: true, diff --git a/packages/auth/drizzle/0001_peaceful_franklin_storm.sql b/packages/auth/drizzle/0001_peaceful_franklin_storm.sql new file mode 100644 index 0000000..63ad550 --- /dev/null +++ b/packages/auth/drizzle/0001_peaceful_franklin_storm.sql @@ -0,0 +1,41 @@ +CREATE TABLE "invitation" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "email" text NOT NULL, + "role" text, + "status" text NOT NULL, + "expires_at" timestamp with time zone NOT NULL, + "inviter_id" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "member" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "user_id" text NOT NULL, + "role" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "organization" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "slug" text NOT NULL, + "logo" text, + "metadata" text, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + "updated_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "session" ADD COLUMN "active_organization_id" text;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "invitation" ADD CONSTRAINT "invitation_inviter_id_user_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "member" ADD CONSTRAINT "member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "invitation_organization_id_idx" ON "invitation" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "invitation_email_idx" ON "invitation" USING btree ("email");--> statement-breakpoint +CREATE UNIQUE INDEX "member_organization_user_unique" ON "member" USING btree ("organization_id","user_id");--> statement-breakpoint +CREATE INDEX "member_user_id_idx" ON "member" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "organization_slug_unique" ON "organization" USING btree ("slug"); \ No newline at end of file diff --git a/packages/auth/drizzle/meta/0001_snapshot.json b/packages/auth/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..db11e37 --- /dev/null +++ b/packages/auth/drizzle/meta/0001_snapshot.json @@ -0,0 +1,688 @@ +{ + "id": "fcdfbaa4-55ba-48af-9ee4-01a9432e8d24", + "prevId": "69dbb32c-24e3-476a-be2f-cb20d37a19c4", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_organization_user_unique": { + "name": "member_organization_user_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_user_id_idx": { + "name": "member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_slug_unique": { + "name": "organization_slug_unique", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "session_token_unique": { + "name": "session_token_unique", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/auth/drizzle/meta/_journal.json b/packages/auth/drizzle/meta/_journal.json index c55154a..94b8d1e 100644 --- a/packages/auth/drizzle/meta/_journal.json +++ b/packages/auth/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1786382437259, "tag": "0000_unusual_quentin_quire", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1786470134471, + "tag": "0001_peaceful_franklin_storm", + "breakpoints": true } ] } diff --git a/packages/auth/src/auth.test.ts b/packages/auth/src/auth.test.ts index 052520a..30fa5cb 100644 --- a/packages/auth/src/auth.test.ts +++ b/packages/auth/src/auth.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { authOptionsFromEnvironment } from './auth.js'; +import { authOptionsFromEnvironment, createAuth } from './auth.js'; +import { defaultAuthConfig } from './config.js'; const completeEnvironment = { BETTER_AUTH_SECRET: 'a-very-secret-value', @@ -62,4 +63,37 @@ describe('authOptionsFromEnvironment', () => { expect(message).toMatch(MISSING_URL_MESSAGE); expect(message).not.toContain(completeEnvironment.BETTER_AUTH_SECRET); }); + + it('points invitation links at the dashboard, not at the auth server', () => { + const options = authOptionsFromEnvironment(completeEnvironment); + + // The recipient needs the UI that can accept the invitation; the auth origin only serves the + // Better Auth handler. + expect(options.dashboardUrl).toBe(completeEnvironment.AUTH_DASHBOARD_ORIGIN); + expect(options.dashboardUrl).not.toBe(options.baseUrl); + }); +}); + +describe('createAuth with organizations', () => { + const instanceOptions = { + databaseUrl: completeEnvironment.AUTH_DATABASE_URL, + secret: completeEnvironment.BETTER_AUTH_SECRET, + baseUrl: completeEnvironment.BETTER_AUTH_URL, + trustedOrigins: [completeEnvironment.AUTH_DASHBOARD_ORIGIN], + dashboardUrl: completeEnvironment.AUTH_DASHBOARD_ORIGIN, + }; + + it('refuses to construct when organizations are enabled without a delivery transport', () => { + // Otherwise an invitation is recorded and nobody is ever told about it. + expect(() => + createAuth({ + ...instanceOptions, + config: { ...defaultAuthConfig, enableOrganizations: true }, + }), + ).toThrow(/sendInvitationEmail/); + }); + + it('constructs without a transport while organizations are off', () => { + expect(() => createAuth({ ...instanceOptions, config: defaultAuthConfig })).not.toThrow(); + }); }); diff --git a/packages/auth/src/auth.ts b/packages/auth/src/auth.ts index 2a14207..7e816a3 100644 --- a/packages/auth/src/auth.ts +++ b/packages/auth/src/auth.ts @@ -1,12 +1,27 @@ import { betterAuth } from 'better-auth'; import type { BetterAuthOptions } from 'better-auth'; import { drizzleAdapter } from 'better-auth/adapters/drizzle'; +import { organization } from 'better-auth/plugins'; import { authConfigFromEnvironment, githubCredentialsFromEnvironment } from './config.js'; import type { AuthConfig, GithubOauthCredentials } from './config.js'; import { createAuthDatabase } from './database.js'; import { schema } from './schema.js'; +/** + * Delivers an organization invitation. + * + * Declared structurally rather than imported from `@agent-zero/mail`: this package owns + * authentication policy, and taking a dependency on the mail package would make one capability + * package depend on another. The composition root supplies the implementation. + */ +export type SendInvitationEmail = (invitation: { + readonly to: string; + readonly organizationName: string; + readonly inviterName: string; + readonly acceptUrl: string; +}) => Promise; + /** Everything the Better Auth instance needs that is not policy. */ export interface AuthInstanceOptions { /** Postgres connection string holding users and sessions. */ @@ -17,8 +32,15 @@ export interface AuthInstanceOptions { readonly baseUrl: string; /** Origins allowed to complete a credentialed round trip, typically the dashboard. */ readonly trustedOrigins: readonly string[]; + /** Dashboard origin invitation links point at, so a recipient lands on the UI, not the API. */ + readonly dashboardUrl: string; readonly config: AuthConfig; readonly github?: GithubOauthCredentials; + /** + * How invitations are delivered. Required when organizations are enabled: without it an + * invitation would be created that nobody is ever told about. + */ + readonly sendInvitationEmail?: SendInvitationEmail; } /** @@ -29,6 +51,12 @@ export interface AuthInstanceOptions { */ export function createAuth(options: AuthInstanceOptions) { const { config } = options; + + // Fail at construction rather than at the first invitation: a deployment that enables + // organizations without a transport would accept invitations and silently never deliver them. + if (config.enableOrganizations && !options.sendInvitationEmail) + throw new Error('organizations are enabled but no sendInvitationEmail was provided'); + // Widened to the option type Better Auth declares. Left as the concrete adapter type, the // inferred return type embeds types TypeScript cannot name in the emitted declarations. const database: BetterAuthOptions['database'] = drizzleAdapter( @@ -52,9 +80,44 @@ export function createAuth(options: AuthInstanceOptions) { ...(options.github ? { socialProviders: { github: { ...options.github, disableSignUp: !config.enableSignup } } } : {}), + plugins: config.enableOrganizations + ? [ + organization({ + allowUserToCreateOrganization: config.allowUserToCreateOrganization, + membershipLimit: config.organizationMembershipLimit, + invitationExpiresIn: config.invitationExpiresInSeconds, + sendInvitationEmail: async (data) => { + // Checked in the guard above; narrowing here keeps the callback total. + const send = options.sendInvitationEmail; + if (!send) return; + await send({ + to: data.email, + organizationName: data.organization.name, + // Better Auth exposes the inviter as a member record wrapping the user. + inviterName: data.inviter.user.name, + acceptUrl: invitationAcceptUrl(options.dashboardUrl, data.id), + }); + }, + }), + ] + : [], }); } +/** + * Build the link an invitation email points at. + * + * Resolved against the dashboard origin rather than the auth server's: the recipient needs the UI + * that can accept the invitation, and `URL` keeps a misconfigured origin from silently producing + * a relative link. + */ +function invitationAcceptUrl(dashboardUrl: string, invitationId: string): string { + return new URL( + `/organizations/accept-invitation/${encodeURIComponent(invitationId)}`, + dashboardUrl, + ).toString(); +} + /** Missing configuration is a deployment error, not something to paper over with a default. */ function requireEnvironmentValue( environment: Readonly>, @@ -82,6 +145,8 @@ export function authOptionsFromEnvironment( secret: requireEnvironmentValue(environment, 'BETTER_AUTH_SECRET'), baseUrl: requireEnvironmentValue(environment, 'BETTER_AUTH_URL'), trustedOrigins: [dashboardOrigin], + // The same origin the dashboard is served from, so invitation links resolve to the UI. + dashboardUrl: dashboardOrigin, config: authConfigFromEnvironment(environment), ...(github ? { github } : {}), }; diff --git a/packages/auth/src/config.test.ts b/packages/auth/src/config.test.ts index 00bce19..a39430f 100644 --- a/packages/auth/src/config.test.ts +++ b/packages/auth/src/config.test.ts @@ -56,4 +56,28 @@ describe('authConfigFromEnvironment', () => { .enableGithubOauth, ).toBe(true); }); + + it('keeps organizations disabled unless explicitly opted in', () => { + expect(authConfigFromEnvironment({}).enableOrganizations).toBe(false); + expect( + authConfigFromEnvironment({ AUTH_ENABLE_ORGANIZATIONS: 'TRUE' }).enableOrganizations, + ).toBe(false); + expect( + authConfigFromEnvironment({ AUTH_ENABLE_ORGANIZATIONS: 'true' }).enableOrganizations, + ).toBe(true); + }); + + it('refuses to advertise organization creation while organizations are off', () => { + // A stale AUTH_ALLOW_ORGANIZATION_CREATION must not survive turning the feature back off. + expect( + authConfigFromEnvironment({ AUTH_ALLOW_ORGANIZATION_CREATION: 'true' }) + .allowUserToCreateOrganization, + ).toBe(false); + expect( + authConfigFromEnvironment({ + AUTH_ENABLE_ORGANIZATIONS: 'true', + AUTH_ALLOW_ORGANIZATION_CREATION: 'true', + }).allowUserToCreateOrganization, + ).toBe(true); + }); }); diff --git a/packages/auth/src/config.ts b/packages/auth/src/config.ts index db6b417..97ab919 100644 --- a/packages/auth/src/config.ts +++ b/packages/auth/src/config.ts @@ -10,6 +10,12 @@ export const MINIMUM_PASSWORD_LENGTH = 8; /** How long a session stays valid without re-authentication. */ export const SESSION_MAXIMUM_AGE_SECONDS = 60 * 60 * 24 * 7; +/** How long an unaccepted organization invitation stays valid. */ +export const INVITATION_EXPIRES_IN_SECONDS = 60 * 60 * 48; + +/** Upper bound on members in a single organization. */ +export const ORGANIZATION_MEMBERSHIP_LIMIT = 100; + /** Capabilities the deployment exposes on its sign-in surface. */ export interface AuthConfig { /** Whether new accounts may be created through the sign-in page. */ @@ -18,8 +24,19 @@ export interface AuthConfig { readonly enablePasswordLogin: boolean; /** Whether the GitHub OAuth button is offered. */ readonly enableGithubOauth: boolean; + /** Whether organizations, memberships and invitations are exposed at all. */ + readonly enableOrganizations: boolean; + /** + * Whether any signed-in user may create an organization. + * + * Separate from {@link enableOrganizations} so an operator can run a deployment where + * organizations exist but only pre-provisioned ones do. + */ + readonly allowUserToCreateOrganization: boolean; readonly minimumPasswordLength: number; readonly sessionMaximumAgeSeconds: number; + readonly invitationExpiresInSeconds: number; + readonly organizationMembershipLimit: number; } /** @@ -30,8 +47,15 @@ export const defaultAuthConfig: AuthConfig = { enableSignup: false, enablePasswordLogin: true, enableGithubOauth: false, + // Organizations stay off until an operator asks for them: enabling them changes what every + // authenticated request is scoped to, which is not something a deployment should acquire by + // upgrading. + enableOrganizations: false, + allowUserToCreateOrganization: false, minimumPasswordLength: MINIMUM_PASSWORD_LENGTH, sessionMaximumAgeSeconds: SESSION_MAXIMUM_AGE_SECONDS, + invitationExpiresInSeconds: INVITATION_EXPIRES_IN_SECONDS, + organizationMembershipLimit: ORGANIZATION_MEMBERSHIP_LIMIT, }; /** GitHub OAuth credentials, present only when both halves are configured. */ @@ -66,9 +90,15 @@ export function githubCredentialsFromEnvironment( export function authConfigFromEnvironment( environment: Readonly> = process.env, ): AuthConfig { + const enableOrganizations = environment.AUTH_ENABLE_ORGANIZATIONS === 'true'; return { ...defaultAuthConfig, enableSignup: environment.AUTH_ENABLE_SIGNUP === 'true', enableGithubOauth: githubCredentialsFromEnvironment(environment) !== undefined, + enableOrganizations, + // Gated on the feature itself, so a deployment that never turned organizations on cannot + // advertise creation through a stale variable. + allowUserToCreateOrganization: + enableOrganizations && environment.AUTH_ALLOW_ORGANIZATION_CREATION === 'true', }; } diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index 32d126f..a5ff3ff 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -3,6 +3,7 @@ export { createAuth, type AuthInstance, type AuthInstanceOptions, + type SendInvitationEmail, type Session, type User, } from './auth.js'; @@ -10,7 +11,9 @@ export { authConfigFromEnvironment, defaultAuthConfig, githubCredentialsFromEnvironment, + INVITATION_EXPIRES_IN_SECONDS, MINIMUM_PASSWORD_LENGTH, + ORGANIZATION_MEMBERSHIP_LIMIT, SESSION_MAXIMUM_AGE_SECONDS, type AuthConfig, type GithubOauthCredentials, diff --git a/packages/auth/src/schema.ts b/packages/auth/src/schema.ts index 759f581..74edfe9 100644 --- a/packages/auth/src/schema.ts +++ b/packages/auth/src/schema.ts @@ -38,6 +38,9 @@ export const session = pgTable( .notNull() // Signing out a deleted account must not leave a usable session behind. .references(() => user.id, { onDelete: 'cascade' }), + // Which organization the session is currently acting in. Written by the organization plugin + // when the user switches context; null means no organization is selected. + activeOrganizationId: text('active_organization_id'), ...timestampColumns, }, (table) => [ @@ -79,5 +82,67 @@ export const verification = pgTable( (table) => [index('verification_identifier_idx').on(table.identifier)], ); +export const organization = pgTable( + 'organization', + { + id: text('id').primaryKey(), + name: text('name').notNull(), + slug: text('slug').notNull(), + logo: text('logo'), + // Better Auth stores this as a JSON-encoded string, not as a jsonb column. + metadata: text('metadata'), + ...timestampColumns, + }, + // The slug addresses an organization in URLs, so collisions have to be rejected by the database + // rather than by whichever request happened to check first. + (table) => [uniqueIndex('organization_slug_unique').on(table.slug)], +); + +export const member = pgTable( + 'member', + { + id: text('id').primaryKey(), + organizationId: text('organization_id') + .notNull() + .references(() => organization.id, { onDelete: 'cascade' }), + userId: text('user_id') + .notNull() + // A deleted account must not keep conferring access to an organization. + .references(() => user.id, { onDelete: 'cascade' }), + role: text('role').notNull(), + ...timestampColumns, + }, + (table) => [ + // One membership per user per organization: a duplicate row would make role changes + // order-dependent and could silently re-grant a revoked role. + uniqueIndex('member_organization_user_unique').on(table.organizationId, table.userId), + index('member_user_id_idx').on(table.userId), + ], +); + +export const invitation = pgTable( + 'invitation', + { + id: text('id').primaryKey(), + organizationId: text('organization_id') + .notNull() + .references(() => organization.id, { onDelete: 'cascade' }), + email: text('email').notNull(), + role: text('role'), + status: text('status').notNull(), + expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(), + inviterId: text('inviter_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + ...timestampColumns, + }, + (table) => [ + index('invitation_organization_id_idx').on(table.organizationId), + // Accepting an invitation is a lookup by email; without this it degrades to a scan as the + // table accumulates expired rows. + index('invitation_email_idx').on(table.email), + ], +); + /** The object shape the Better Auth Drizzle adapter resolves models against. */ -export const schema = { user, session, account, verification }; +export const schema = { user, session, account, verification, organization, member, invitation }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0330d2d..91be108 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ importers: '@agent-zero/auth': specifier: workspace:* version: 0.3.0 + '@agent-zero/mail': + specifier: workspace:* + version: 0.3.0 '@agent-zero/shared': specifier: workspace:* version: 0.3.0 From 49ffbcb21472ef1020e6c8034e01d3f9cbfc89ea Mon Sep 17 00:00:00 2001 From: RedStar Date: Wed, 12 Aug 2026 09:54:00 +0200 Subject: [PATCH 04/12] chore(ci): drop unrelated knip/i18n jobs swept into the mailing/organizations commit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These two jobs predate this branch and are unrelated to mailing or organizations. The i18n job also checks packages/i18n/schema.json, packages/i18n/schemas and packages/i18n/locales, none of which exist in this repo — i18n lives under apps/dashboard/i18n/ — so the check would either fail or silently no-op against the wrong paths. Left for whoever intended to add CI coverage for the dashboard's i18n tooling to reintroduce correctly, in its own change. --- .github/workflows/ci.yaml | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e32375a..9d1a895 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -89,38 +89,3 @@ jobs: - name: 📦 Verify package version run: | node --input-type=module --eval "Promise.all([import('./packages/shared/dist/index.mjs'), import('./packages/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" - - knip: - name: 🧹 Unused code check - runs-on: ubuntu-24.04-arm - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup - uses: ./.github/actions/setup-toolchain - - - name: 🧹 Check for unused code - run: aube run knip - - i18n: - name: 🌐 i18n validation - runs-on: ubuntu-24.04-arm - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - - - name: Setup - uses: ./.github/actions/setup-toolchain - - - name: 🌐 Check for missing or dynamic i18n keys - run: aube run i18n:report - - - name: 🌐 Check i18n schema is up to date - run: | - aube run i18n:schema - git diff --exit-code packages/i18n/schema.json packages/i18n/schemas packages/i18n/locales From 5108a70247cf275d45b19de3831ac04008c65725 Mon Sep 17 00:00:00 2001 From: RedStar Date: Wed, 12 Aug 2026 09:58:26 +0200 Subject: [PATCH 05/12] fix(mail,auth,dashboard): resolve lint findings in mailing/organizations code Hoists regex literals used inside functions to module scope, replaces unsafe type assertions in mail/provider tests with a narrowing assertion function and optional chaining, rewrites mailProviderFromEnvironment's provider switch as if/else so every path is a recognized return, moves useOrganizations' messageFrom helper to module scope since it captures nothing from its parent, and adds a vi.fn() type parameter. Also declares packages/mail in knip.jsonc: maizzle.config.ts is discovered by Maizzle's render() through its own filesystem convention rather than imported, and @maizzle/tailwindcss is resolved by package name as a Maizzle plugin. --- .../composables/useOrganizations.ts | 18 +++++----- knip.jsonc | 8 +++++ packages/auth/src/auth.test.ts | 3 +- packages/mail/src/mail.test.ts | 29 +++++++++++---- packages/mail/src/provider/index.test.ts | 24 ++++++++----- packages/mail/src/provider/index.ts | 36 ++++++++++--------- 6 files changed, 76 insertions(+), 42 deletions(-) diff --git a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts index 9045242..e20891e 100644 --- a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts +++ b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts @@ -5,6 +5,15 @@ import { useState } from 'nuxt/app'; import type { Organization, OrganizationMember, OrganizationRole } from '../types/organization.js'; +/** Surface the server's message without assuming a shape the plugin may not return. */ +function messageFrom(cause: unknown): string { + if (cause && typeof cause === 'object' && 'message' in cause) { + const { message } = cause as { message?: unknown }; + if (typeof message === 'string' && message !== '') return message; + } + return 'organizations.errors.generic'; +} + /** * Organization state for the dashboard. * @@ -24,15 +33,6 @@ export function useOrganizations() { const pending = useState('organizations:pending', () => false); const error = useState('organizations:error', () => null); - /** Surface the server's message without assuming a shape the plugin may not return. */ - function messageFrom(cause: unknown): string { - if (cause && typeof cause === 'object' && 'message' in cause) { - const { message } = cause as { message?: unknown }; - if (typeof message === 'string' && message !== '') return message; - } - return 'organizations.errors.generic'; - } - /** * Run one client call with shared pending and error handling. * diff --git a/knip.jsonc b/knip.jsonc index e4e561b..841ecc0 100644 --- a/knip.jsonc +++ b/knip.jsonc @@ -13,6 +13,14 @@ "nano-staged", ], "workspaces": { + "packages/mail": { + // maizzle.config.ts is discovered by Maizzle's own `render()` call by filesystem + // convention (it walks up from the template path), not imported by anything in src/. + "entry": ["src/index.ts", "maizzle.config.ts"], + // Resolved by Maizzle's Tailwind integration from the package name, not imported + // directly: it registers itself as the `@maizzle/tailwindcss` plugin for `css.inline`. + "ignoreDependencies": ["@maizzle/tailwindcss"], + }, "apps/server": { // Nitro discovers server routes, middleware, and utilities from the // filesystem, so knip cannot see who references them. diff --git a/packages/auth/src/auth.test.ts b/packages/auth/src/auth.test.ts index 30fa5cb..4f1f4bc 100644 --- a/packages/auth/src/auth.test.ts +++ b/packages/auth/src/auth.test.ts @@ -11,6 +11,7 @@ const completeEnvironment = { }; const MISSING_URL_MESSAGE = /missing required environment variable: BETTER_AUTH_URL/; +const MISSING_SEND_INVITATION_EMAIL_PATTERN = /sendInvitationEmail/; /** Return the failure message so assertions stay outside the catch block. */ function messageFrom(run: () => unknown): string { @@ -90,7 +91,7 @@ describe('createAuth with organizations', () => { ...instanceOptions, config: { ...defaultAuthConfig, enableOrganizations: true }, }), - ).toThrow(/sendInvitationEmail/); + ).toThrow(MISSING_SEND_INVITATION_EMAIL_PATTERN); }); it('constructs without a transport while organizations are off', () => { diff --git a/packages/mail/src/mail.test.ts b/packages/mail/src/mail.test.ts index b4e16cd..6df7caa 100644 --- a/packages/mail/src/mail.test.ts +++ b/packages/mail/src/mail.test.ts @@ -3,6 +3,10 @@ import { describe, expect, it, vi } from 'vitest'; import { createMailer, sendEmail } from './mail.js'; import type { OutgoingMail } from './provider/types.js'; +const INLINE_STYLE_PATTERN = /style="/; +const LEAKED_CLASS_ATTRIBUTE_PATTERN = /class="/; +const MISSING_MAIL_FROM_PATTERN = /MAIL_FROM/; + /** * These render through the real Maizzle pipeline rather than a stub. Rendering is local and * deterministic, and the failure this guards against — a template that compiles but drops its @@ -19,6 +23,11 @@ function recordingProvider() { }; } +/** Narrows `OutgoingMail | undefined` without an unsafe cast, failing the test with a clear message. */ +function assertSent(mail: OutgoingMail | undefined): asserts mail is OutgoingMail { + if (!mail) throw new Error('expected a message to have been recorded'); +} + describe('sendEmail', () => { it('renders the invitation template with its context in both HTML and plaintext', async () => { const { sent, provider } = recordingProvider(); @@ -37,7 +46,8 @@ describe('sendEmail', () => { ); expect(sent).toHaveLength(1); - const [mail] = sent as [OutgoingMail]; + const [mail] = sent; + assertSent(mail); expect(mail.to).toBe('invitee@example.com'); expect(mail.from).toBe('noreply@example.com'); expect(mail.subject).toBe('You have been invited to an organization'); @@ -61,11 +71,12 @@ describe('sendEmail', () => { { provider, from: 'noreply@example.com' }, ); - const [mail] = sent as [OutgoingMail]; - expect(mail.html).toMatch(/style="/); + const [mail] = sent; + assertSent(mail); + expect(mail.html).toMatch(INLINE_STYLE_PATTERN); // Utility classes are inlined and purged; a leftover class attribute means the CSS step // silently did nothing and the message would arrive unstyled. - expect(mail.html).not.toMatch(/class="/); + expect(mail.html).not.toMatch(LEAKED_CLASS_ATTRIBUTE_PATTERN); }); it('lets the caller override the registered subject', async () => { @@ -81,7 +92,9 @@ describe('sendEmail', () => { { provider, from: 'noreply@example.com' }, ); - expect((sent[0] as OutgoingMail).subject).toBe('Conferma il tuo indirizzo email'); + const [mail] = sent; + assertSent(mail); + expect(mail.subject).toBe('Conferma il tuo indirizzo email'); }); it('refuses to send without a sender address rather than inventing one', async () => { @@ -97,7 +110,7 @@ describe('sendEmail', () => { }, { provider }, ), - ).rejects.toThrow(/MAIL_FROM/); + ).rejects.toThrow(MISSING_MAIL_FROM_PATTERN); vi.unstubAllEnvs(); }); @@ -114,6 +127,8 @@ describe('createMailer', () => { context: { name: 'Dana', verifyUrl: 'https://dashboard.example.com/verify?token=abc' }, }); - expect((sent[0] as OutgoingMail).from).toBe('ops@example.com'); + const [mail] = sent; + assertSent(mail); + expect(mail.from).toBe('ops@example.com'); }); }); diff --git a/packages/mail/src/provider/index.test.ts b/packages/mail/src/provider/index.test.ts index 2ed6775..92aab88 100644 --- a/packages/mail/src/provider/index.test.ts +++ b/packages/mail/src/provider/index.test.ts @@ -2,6 +2,12 @@ import { describe, expect, it, vi } from 'vitest'; import { createConsoleProvider, mailProviderFromEnvironment } from './index.js'; +const INVALID_MAIL_PROVIDER_PATTERN = /invalid MAIL_PROVIDER/; +const MISSING_RESEND_API_KEY_PATTERN = /RESEND_API_KEY/; +const MISSING_SMTP_HOST_PATTERN = /SMTP_HOST/; +const MISSING_SMTP_PORT_PATTERN = /SMTP_PORT/; +const LEAKED_SMTP_PASSWORD_PATTERN = /hunter2/; + /** * Provider selection is deployment configuration, so the failure modes that matter are the * misconfigured ones: they must fail loudly rather than fall back to a transport the operator did @@ -14,21 +20,23 @@ describe('mailProviderFromEnvironment', () => { it('rejects an unknown provider name instead of silently defaulting', () => { expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'carrier-pigeon' })).toThrow( - /invalid MAIL_PROVIDER/, + INVALID_MAIL_PROVIDER_PATTERN, ); }); it('requires an API key before selecting Resend', () => { expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'resend' })).toThrow( - /RESEND_API_KEY/, + MISSING_RESEND_API_KEY_PATTERN, ); }); it('requires host and port before selecting SMTP', () => { - expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp' })).toThrow(/SMTP_HOST/); + expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp' })).toThrow( + MISSING_SMTP_HOST_PATTERN, + ); expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp', SMTP_HOST: 'localhost' }), - ).toThrow(/SMTP_PORT/); + ).toThrow(MISSING_SMTP_PORT_PATTERN); }); it('rejects a port that is not a whole number in range', () => { @@ -39,7 +47,7 @@ describe('mailProviderFromEnvironment', () => { SMTP_HOST: 'localhost', SMTP_PORT: port, }), - ).toThrow(/SMTP_PORT/); + ).toThrow(MISSING_SMTP_PORT_PATTERN); } }); @@ -47,13 +55,13 @@ describe('mailProviderFromEnvironment', () => { // A thrown connection string or key routinely ends up in a crash log. expect(() => mailProviderFromEnvironment({ MAIL_PROVIDER: 'smtp', SMTP_PASSWORD: 'hunter2' }), - ).toThrow(expect.not.stringMatching(/hunter2/) as unknown as string); + ).toThrow(expect.not.stringMatching(LEAKED_SMTP_PASSWORD_PATTERN)); }); }); describe('createConsoleProvider', () => { it('reports the message without logging its body', async () => { - const log = vi.fn(); + const log = vi.fn<(message: string) => void>(); await createConsoleProvider(log)({ to: 'operator@example.com', subject: 'Reset your password', @@ -63,7 +71,7 @@ describe('createConsoleProvider', () => { }); expect(log).toHaveBeenCalledOnce(); - const [message] = log.mock.calls[0] as [string]; + const [message] = log.mock.calls[0] ?? []; expect(message).toContain('operator@example.com'); // Invitation and reset links are single-use credentials; they must not reach the log. expect(message).not.toContain('secret-token'); diff --git a/packages/mail/src/provider/index.ts b/packages/mail/src/provider/index.ts index 1b4b451..95cc1d6 100644 --- a/packages/mail/src/provider/index.ts +++ b/packages/mail/src/provider/index.ts @@ -27,13 +27,15 @@ function requireEnvironmentValue( return value; } +const DECIMAL_PORT_PATTERN = /^\d+$/u; + function requirePort( environment: Readonly>, name: string, ): number { const raw = requireEnvironmentValue(environment, name); // Validate the whole value: `Number.parseInt` would truncate `587abc` to a usable port. - if (!/^\d+$/u.test(raw)) throw new Error(`invalid ${name}: expected a port number`); + if (!DECIMAL_PORT_PATTERN.test(raw)) throw new Error(`invalid ${name}: expected a port number`); const port = Number.parseInt(raw, 10); if (port < 1 || port > 65_535) throw new Error(`invalid ${name}: expected a port number`); return port; @@ -56,20 +58,20 @@ export function mailProviderFromEnvironment( `invalid MAIL_PROVIDER: expected one of ${MAIL_PROVIDER_NAMES.join(', ')}, received ${configured}`, ); - switch (configured) { - case 'resend': - return createResendProvider({ - apiKey: requireEnvironmentValue(environment, 'RESEND_API_KEY'), - }); - case 'smtp': - return createSmtpProvider({ - host: requireEnvironmentValue(environment, 'SMTP_HOST'), - port: requirePort(environment, 'SMTP_PORT'), - secure: environment.SMTP_SECURE === 'true', - ...(environment.SMTP_USER?.trim() ? { user: environment.SMTP_USER.trim() } : {}), - ...(environment.SMTP_PASSWORD ? { password: environment.SMTP_PASSWORD } : {}), - }); - case 'console': - return createConsoleProvider(); - } + if (configured === 'resend') + return createResendProvider({ + apiKey: requireEnvironmentValue(environment, 'RESEND_API_KEY'), + }); + + if (configured === 'smtp') + return createSmtpProvider({ + host: requireEnvironmentValue(environment, 'SMTP_HOST'), + port: requirePort(environment, 'SMTP_PORT'), + secure: environment.SMTP_SECURE === 'true', + ...(environment.SMTP_USER?.trim() ? { user: environment.SMTP_USER.trim() } : {}), + ...(environment.SMTP_PASSWORD ? { password: environment.SMTP_PASSWORD } : {}), + }); + + // The only remaining member of MailProviderName, narrowed by isProviderName above. + return createConsoleProvider(); } From 3b8fc6e785b5d3d10dd1040978b786b534c7e7b6 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Wed, 12 Aug 2026 08:22:34 +0000 Subject: [PATCH 06/12] fix(mail,auth,dashboard): address review findings on error handling and delivery guarantees - surface Better Auth API errors that resolve with { data, error } in the organizations composable and the accept-invitation page instead of treating them as success - initialize the active organization on refresh so the switcher's displayed selection matches state - require STARTTLS on non-implicit-TLS SMTP connections so delivery never falls back to plaintext - withhold sendInvitationEmail from createAuth when the console mail provider is configured, so the startup guard rejects an organizations-enabled deployment with no delivering transport Co-authored-by: Codesmith --- apps/auth-server/src/index.ts | 24 ++++++++---- .../composables/useOrganizations.ts | 39 +++++++++++++------ .../organizations/accept-invitation/[id].vue | 6 ++- packages/mail/src/index.ts | 1 + packages/mail/src/provider/index.test.ts | 26 ++++++++++++- packages/mail/src/provider/index.ts | 23 ++++++++--- packages/mail/src/provider/smtp.ts | 9 ++++- 7 files changed, 100 insertions(+), 28 deletions(-) diff --git a/apps/auth-server/src/index.ts b/apps/auth-server/src/index.ts index 6362d1f..63ca8f5 100644 --- a/apps/auth-server/src/index.ts +++ b/apps/auth-server/src/index.ts @@ -1,7 +1,7 @@ import process from 'node:process'; import { authOptionsFromEnvironment, createAuth } from '@agent-zero/auth'; -import { createMailer } from '@agent-zero/mail'; +import { createMailer, mailProviderNameFromEnvironment } from '@agent-zero/mail'; import { redactSecrets, secretValuesFromEnvironment } from '@agent-zero/shared'; import { serve } from '@hono/node-server'; import { Hono } from 'hono'; @@ -42,16 +42,26 @@ const options = authOptionsFromEnvironment(); // This process is the composition root for authentication, so it is where the mail transport is // bound and injected. `packages/auth` declares the delivery contract structurally and never // imports `@agent-zero/mail`, which keeps one capability package from depending on another. +// +// The transport is only injected when the configured provider actually delivers: the console +// default logs an envelope instead of sending, so wiring it in would satisfy `createAuth`'s +// startup guard while every invitation silently reached nobody. Withholding the callback lets +// that guard fail startup when organizations are enabled without a real transport. const sendMail = createMailer(); +const deliversMail = mailProviderNameFromEnvironment() !== 'console'; const auth = createAuth({ ...options, - sendInvitationEmail: ({ to, organizationName, inviterName, acceptUrl }) => - sendMail({ - to, - templateId: 'organizationInvitation', - context: { organizationName, inviterName, acceptUrl }, - }), + ...(deliversMail + ? { + sendInvitationEmail: ({ to, organizationName, inviterName, acceptUrl }) => + sendMail({ + to, + templateId: 'organizationInvitation', + context: { organizationName, inviterName, acceptUrl }, + }), + } + : {}), }); const app = new Hono(); diff --git a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts index e20891e..dde9307 100644 --- a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts +++ b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts @@ -14,6 +14,16 @@ function messageFrom(cause: unknown): string { return 'organizations.errors.generic'; } +/** + * Better Auth client calls resolve with `{ data, error }` rather than rejecting on an API error. + * Throwing the resolved error routes it through `run`'s shared handling, so a failed action + * surfaces instead of looking successful with empty data. + */ +function unwrap(result: { data: T; error: unknown }): T { + if (result.error) throw result.error; + return result.data; +} + /** * Organization state for the dashboard. * @@ -57,9 +67,14 @@ export function useOrganizations() { async function refresh() { await run(async (authClient) => { - const { data } = await authClient.organization.list(); + const data = unwrap(await authClient.organization.list()); organizations.value = data ?? []; }); + // A fresh session arrives with no active organization while the switcher renders the first + // entry, so the selection is synchronized here or members and invitations would stay + // disabled until the user re-selects manually. + const first = organizations.value[0]; + if (!activeOrganization.value && first) await setActive(first.id); } async function refreshMembers() { @@ -69,24 +84,25 @@ export function useOrganizations() { return; } await run(async (authClient) => { - const { data } = await authClient.organization.listMembers({ query: { organizationId } }); + const data = unwrap( + await authClient.organization.listMembers({ query: { organizationId } }), + ); members.value = data?.members ?? []; }); } async function setActive(organizationId: string) { await run(async (authClient) => { - const { data } = await authClient.organization.setActive({ organizationId }); + const data = unwrap(await authClient.organization.setActive({ organizationId })); activeOrganization.value = data ?? null; }); await refreshMembers(); } async function create(input: { name: string; slug: string }) { - const created = await run(async (authClient) => { - const { data } = await authClient.organization.create(input); - return data; - }); + const created = await run(async (authClient) => + unwrap(await authClient.organization.create(input)), + ); if (created) await refresh(); return created; } @@ -94,17 +110,16 @@ export function useOrganizations() { async function inviteMember(input: { email: string; role: OrganizationRole }) { const organizationId = activeOrganization.value?.id; if (!organizationId) return undefined; - return run(async (authClient) => { - const { data } = await authClient.organization.inviteMember({ ...input, organizationId }); - return data; - }); + return run(async (authClient) => + unwrap(await authClient.organization.inviteMember({ ...input, organizationId })), + ); } async function removeMember(memberIdOrEmail: string) { const organizationId = activeOrganization.value?.id; if (!organizationId) return; await run(async (authClient) => { - await authClient.organization.removeMember({ memberIdOrEmail, organizationId }); + unwrap(await authClient.organization.removeMember({ memberIdOrEmail, organizationId })); }); await refreshMembers(); } diff --git a/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue b/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue index b91e454..3831e47 100644 --- a/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue +++ b/apps/dashboard/app/pages/(organizations)/organizations/accept-invitation/[id].vue @@ -42,8 +42,10 @@ onMounted(async () => { } try { - await client.organization.acceptInvitation({ invitationId }); - status.value = 'accepted'; + // Better Auth resolves API failures as `{ data, error }` rather than rejecting, so an invalid + // or expired invitation must be read from the resolved error, not just the catch path. + const { error } = await client.organization.acceptInvitation({ invitationId }); + status.value = error ? 'failed' : 'accepted'; } catch { status.value = 'failed'; } diff --git a/packages/mail/src/index.ts b/packages/mail/src/index.ts index 1088bee..4fff57a 100644 --- a/packages/mail/src/index.ts +++ b/packages/mail/src/index.ts @@ -11,6 +11,7 @@ export { createSmtpProvider, MAIL_PROVIDER_NAMES, mailProviderFromEnvironment, + mailProviderNameFromEnvironment, type MailProvider, type MailProviderName, type OutgoingMail, diff --git a/packages/mail/src/provider/index.test.ts b/packages/mail/src/provider/index.test.ts index 92aab88..1771ab0 100644 --- a/packages/mail/src/provider/index.test.ts +++ b/packages/mail/src/provider/index.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it, vi } from 'vitest'; -import { createConsoleProvider, mailProviderFromEnvironment } from './index.js'; +import { + createConsoleProvider, + mailProviderFromEnvironment, + mailProviderNameFromEnvironment, +} from './index.js'; const INVALID_MAIL_PROVIDER_PATTERN = /invalid MAIL_PROVIDER/; const MISSING_RESEND_API_KEY_PATTERN = /RESEND_API_KEY/; @@ -59,6 +63,26 @@ describe('mailProviderFromEnvironment', () => { }); }); +/** + * Composition roots branch on this name to withhold capabilities (such as invitation delivery) + * from the non-delivering console default, so it must report exactly what was configured. + */ +describe('mailProviderNameFromEnvironment', () => { + it('reports the console default when nothing is configured', () => { + expect(mailProviderNameFromEnvironment({})).toBe('console'); + }); + + it('reports the configured transport name', () => { + expect(mailProviderNameFromEnvironment({ MAIL_PROVIDER: 'smtp' })).toBe('smtp'); + }); + + it('rejects an unknown provider name instead of silently defaulting', () => { + expect(() => mailProviderNameFromEnvironment({ MAIL_PROVIDER: 'carrier-pigeon' })).toThrow( + INVALID_MAIL_PROVIDER_PATTERN, + ); + }); +}); + describe('createConsoleProvider', () => { it('reports the message without logging its body', async () => { const log = vi.fn<(message: string) => void>(); diff --git a/packages/mail/src/provider/index.ts b/packages/mail/src/provider/index.ts index 95cc1d6..7e203fb 100644 --- a/packages/mail/src/provider/index.ts +++ b/packages/mail/src/provider/index.ts @@ -41,6 +41,23 @@ function requirePort( return port; } +/** + * Resolve and validate the configured transport name without constructing the transport. + * + * Composition roots use this to tell a delivering transport apart from the console default when + * a feature (such as organization invitations) must not silently log mail instead of sending it. + */ +export function mailProviderNameFromEnvironment( + environment: Readonly> = process.env, +): MailProviderName { + const configured = environment.MAIL_PROVIDER?.trim() ?? 'console'; + if (!isProviderName(configured)) + throw new Error( + `invalid MAIL_PROVIDER: expected one of ${MAIL_PROVIDER_NAMES.join(', ')}, received ${configured}`, + ); + return configured; +} + /** * Resolve the configured transport. * @@ -52,11 +69,7 @@ function requirePort( export function mailProviderFromEnvironment( environment: Readonly> = process.env, ): MailProvider { - const configured = environment.MAIL_PROVIDER?.trim() ?? 'console'; - if (!isProviderName(configured)) - throw new Error( - `invalid MAIL_PROVIDER: expected one of ${MAIL_PROVIDER_NAMES.join(', ')}, received ${configured}`, - ); + const configured = mailProviderNameFromEnvironment(environment); if (configured === 'resend') return createResendProvider({ diff --git a/packages/mail/src/provider/smtp.ts b/packages/mail/src/provider/smtp.ts index c7ee7c5..9263120 100644 --- a/packages/mail/src/provider/smtp.ts +++ b/packages/mail/src/provider/smtp.ts @@ -4,7 +4,10 @@ import type { MailProvider, OutgoingMail } from './types.js'; export interface SmtpProviderOptions { readonly host: string; readonly port: number; - /** Implicit TLS. Conventionally true on 465 and false on 587, which upgrades via STARTTLS. */ + /** + * Implicit TLS. Conventionally true on 465 and false on 587. When false the connection must + * still upgrade via STARTTLS; delivery never falls back to plaintext. + */ readonly secure: boolean; readonly user?: string; readonly password?: string; @@ -23,6 +26,10 @@ export function createSmtpProvider(options: SmtpProviderOptions): MailProvider { host: options.host, port: options.port, secure: options.secure, + // Without this, a relay that does not advertise STARTTLS (or an on-path party stripping + // the advertisement) would silently downgrade reset, verification, and invitation links + // plus the AUTH credentials to plaintext. Implicit-TLS connections already encrypt. + requireTLS: !options.secure, // Anonymous relays are legitimate on an internal network, so credentials stay optional // rather than being forced into a half-configured auth block. ...(options.user && options.password From 72bcbc8c43f868c7f3b403284bf5f05be73ef851 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Wed, 12 Aug 2026 08:23:22 +0000 Subject: [PATCH 07/12] [autofix.ci] apply automated fixes --- .../app/modules/organizations/composables/useOrganizations.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts index dde9307..39b7439 100644 --- a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts +++ b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts @@ -84,9 +84,7 @@ export function useOrganizations() { return; } await run(async (authClient) => { - const data = unwrap( - await authClient.organization.listMembers({ query: { organizationId } }), - ); + const data = unwrap(await authClient.organization.listMembers({ query: { organizationId } })); members.value = data?.members ?? []; }); } From 7ce9157d59f87b0c78fd69792356771257768632 Mon Sep 17 00:00:00 2001 From: RedStar071 Date: Wed, 12 Aug 2026 08:27:07 +0000 Subject: [PATCH 08/12] fix(dashboard): check Better Auth resolved errors inline to keep client result types intact The generic unwrap helper collapsed the client's { data, error } result union to {}, failing typecheck and type-aware lint. Destructuring at each call site and throwing via throwOnApiError preserves the inferred data types while still routing resolved API errors through run's shared error handling. Co-authored-by: Codesmith --- .../composables/useOrganizations.ts | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts index 39b7439..8cbfa32 100644 --- a/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts +++ b/apps/dashboard/app/modules/organizations/composables/useOrganizations.ts @@ -17,11 +17,11 @@ function messageFrom(cause: unknown): string { /** * Better Auth client calls resolve with `{ data, error }` rather than rejecting on an API error. * Throwing the resolved error routes it through `run`'s shared handling, so a failed action - * surfaces instead of looking successful with empty data. + * surfaces instead of looking successful with empty data. Checked inline at each call site + * because a generic unwrap helper collapses the client's `{ data, error }` union to `{}`. */ -function unwrap(result: { data: T; error: unknown }): T { - if (result.error) throw result.error; - return result.data; +function throwOnApiError(apiError: unknown): void { + if (apiError) throw apiError; } /** @@ -67,7 +67,8 @@ export function useOrganizations() { async function refresh() { await run(async (authClient) => { - const data = unwrap(await authClient.organization.list()); + const { data, error: apiError } = await authClient.organization.list(); + throwOnApiError(apiError); organizations.value = data ?? []; }); // A fresh session arrives with no active organization while the switcher renders the first @@ -84,23 +85,29 @@ export function useOrganizations() { return; } await run(async (authClient) => { - const data = unwrap(await authClient.organization.listMembers({ query: { organizationId } })); + const { data, error: apiError } = await authClient.organization.listMembers({ + query: { organizationId }, + }); + throwOnApiError(apiError); members.value = data?.members ?? []; }); } async function setActive(organizationId: string) { await run(async (authClient) => { - const data = unwrap(await authClient.organization.setActive({ organizationId })); + const { data, error: apiError } = await authClient.organization.setActive({ organizationId }); + throwOnApiError(apiError); activeOrganization.value = data ?? null; }); await refreshMembers(); } async function create(input: { name: string; slug: string }) { - const created = await run(async (authClient) => - unwrap(await authClient.organization.create(input)), - ); + const created = await run(async (authClient) => { + const { data, error: apiError } = await authClient.organization.create(input); + throwOnApiError(apiError); + return data; + }); if (created) await refresh(); return created; } @@ -108,16 +115,25 @@ export function useOrganizations() { async function inviteMember(input: { email: string; role: OrganizationRole }) { const organizationId = activeOrganization.value?.id; if (!organizationId) return undefined; - return run(async (authClient) => - unwrap(await authClient.organization.inviteMember({ ...input, organizationId })), - ); + return run(async (authClient) => { + const { data, error: apiError } = await authClient.organization.inviteMember({ + ...input, + organizationId, + }); + throwOnApiError(apiError); + return data; + }); } async function removeMember(memberIdOrEmail: string) { const organizationId = activeOrganization.value?.id; if (!organizationId) return; await run(async (authClient) => { - unwrap(await authClient.organization.removeMember({ memberIdOrEmail, organizationId })); + const { error: apiError } = await authClient.organization.removeMember({ + memberIdOrEmail, + organizationId, + }); + throwOnApiError(apiError); }); await refreshMembers(); } From f34ebce240c4afa7b85c6c877bcca15ecc277154 Mon Sep 17 00:00:00 2001 From: RedStar Date: Wed, 12 Aug 2026 10:31:41 +0200 Subject: [PATCH 09/12] feat(i18n): extract packages/i18n (config, translations, tooling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Following supastarter's package layout, moves everything i18n-related out of apps/dashboard into a new shared @agent-zero/i18n package: the locale registry (config/i18n.ts -> src/config.ts), the translation content (i18n/locales, i18n/schemas), the maintenance scripts (compare-translations, find-invalid-translations, generate-i18n-schema, i18n-status, remove-unused-translations, and their shared i18n-locale-files.ts helpers), and the Lunaria translation-status config. config/i18n-empty-placeholders.ts stays in the dashboard — it's a Vite plugin, not shared content. apps/dashboard now imports @agent-zero/i18n. @nuxtjs/i18n's langDir does not support absolute paths in production, so nuxt.config.ts resolves the package's installed locales/ directory and passes absolute file paths per locale instead (the module's own documented pattern for module-provided locale files), keeping the package's own i18nLocales export portable. Also fixes the two i18n tooling scripts that scan the consuming app's source (find-invalid-translations, remove-unused-translations): their VUE_FILES_GLOB now points at ../../apps/dashboard/app since the scripts no longer run from inside that app. Running the moved i18n:report surfaced a real pre-existing issue in this branch's own InviteForm.vue — a dynamic i18n key (`organizations.roles.${value}`) that vue-i18n-extract cannot statically verify — replaced with three static t() calls. Restores the CI jobs (knip, i18n) dropped earlier in this branch as broken: the i18n job's schema-drift check now points at the real packages/i18n/{schemas,locales} paths. Also wires i18n:report and i18n:schema as root aube run scripts (turbo run passthrough) alongside the existing i18n:status — neither had ever been wired at the root, so the CI job would have failed regardless of the path fix. --- .github/workflows/ci.yaml | 35 + apps/dashboard/app/app.vue | 4 +- .../organizations/components/InviteForm.vue | 18 +- .../shared/components/LocaleSwitcher.vue | 4 +- apps/dashboard/nuxt.config.ts | 21 +- apps/dashboard/package.json | 9 +- apps/dashboard/tsconfig.json | 8 +- knip.jsonc | 6 + package.json | 2 + .../i18n/locales/en/auth.json | 0 .../i18n/locales/en/common.json | 0 .../i18n/locales/en/dashboard.json | 0 .../i18n/locales/en/errors.json | 0 .../i18n/locales/en/organizations.json | 0 .../i18n/locales/it/auth.json | 0 .../i18n/locales/it/common.json | 0 .../i18n/locales/it/dashboard.json | 0 .../i18n/locales/it/errors.json | 0 .../i18n/locales/it/organizations.json | 0 .../i18n/locales}/locale-features.json | 0 .../i18n}/lunaria.config.ts | 12 +- packages/i18n/package.json | 50 + .../i18n/schemas/auth.schema.json | 2 +- .../i18n/schemas/common.schema.json | 2 +- .../i18n/schemas/dashboard.schema.json | 2 +- .../i18n/schemas/errors.schema.json | 2 +- .../i18n/schemas/organizations.schema.json | 2 +- .../i18n}/scripts/compare-translations.ts | 20 +- .../scripts/find-invalid-translations.ts | 9 +- .../i18n}/scripts/generate-i18n-schema.ts | 37 +- .../i18n}/scripts/i18n-status.ts | 0 .../scripts/remove-unused-translations.ts | 21 +- .../i18n}/scripts/utils/i18n-locale-files.ts | 21 +- .../i18n/src/config.test.ts | 12 +- .../i18n.ts => packages/i18n/src/config.ts | 6 +- packages/i18n/src/index.ts | 8 + packages/i18n/tsconfig.json | 11 + packages/i18n/tsdown.config.ts | 3 + pnpm-lock.yaml | 1868 ++++++++--------- turbo.json | 4 + 40 files changed, 1162 insertions(+), 1037 deletions(-) rename {apps/dashboard => packages}/i18n/locales/en/auth.json (100%) rename {apps/dashboard => packages}/i18n/locales/en/common.json (100%) rename {apps/dashboard => packages}/i18n/locales/en/dashboard.json (100%) rename {apps/dashboard => packages}/i18n/locales/en/errors.json (100%) rename {apps/dashboard => packages}/i18n/locales/en/organizations.json (100%) rename {apps/dashboard => packages}/i18n/locales/it/auth.json (100%) rename {apps/dashboard => packages}/i18n/locales/it/common.json (100%) rename {apps/dashboard => packages}/i18n/locales/it/dashboard.json (100%) rename {apps/dashboard => packages}/i18n/locales/it/errors.json (100%) rename {apps/dashboard => packages}/i18n/locales/it/organizations.json (100%) rename {apps/dashboard/i18n => packages/i18n/locales}/locale-features.json (100%) rename {apps/dashboard => packages/i18n}/lunaria.config.ts (64%) create mode 100644 packages/i18n/package.json rename {apps/dashboard => packages}/i18n/schemas/auth.schema.json (97%) rename {apps/dashboard => packages}/i18n/schemas/common.schema.json (96%) rename {apps/dashboard => packages}/i18n/schemas/dashboard.schema.json (98%) rename {apps/dashboard => packages}/i18n/schemas/errors.schema.json (92%) rename {apps/dashboard => packages}/i18n/schemas/organizations.schema.json (97%) rename {apps/dashboard => packages/i18n}/scripts/compare-translations.ts (91%) rename {apps/dashboard => packages/i18n}/scripts/find-invalid-translations.ts (90%) rename {apps/dashboard => packages/i18n}/scripts/generate-i18n-schema.ts (65%) rename {apps/dashboard => packages/i18n}/scripts/i18n-status.ts (100%) rename {apps/dashboard => packages/i18n}/scripts/remove-unused-translations.ts (80%) rename {apps/dashboard => packages/i18n}/scripts/utils/i18n-locale-files.ts (65%) rename apps/dashboard/test/unit/i18n.test.ts => packages/i18n/src/config.test.ts (82%) rename apps/dashboard/config/i18n.ts => packages/i18n/src/config.ts (87%) create mode 100644 packages/i18n/src/index.ts create mode 100644 packages/i18n/tsconfig.json create mode 100644 packages/i18n/tsdown.config.ts diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 9d1a895..57c24aa 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -89,3 +89,38 @@ jobs: - name: 📦 Verify package version run: | node --input-type=module --eval "Promise.all([import('./packages/shared/dist/index.mjs'), import('./packages/shared/package.json', { with: { type: 'json' } })]).then(([built, pkg]) => { if (built.version !== pkg.default.version) { console.error('Injected version', built.version, 'does not match package.json version', pkg.default.version); process.exit(1) } })" + + knip: + name: 🧹 Unused code check + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup-toolchain + + - name: 🧹 Check for unused code + run: aube run knip + + i18n: + name: 🌐 i18n validation + runs-on: ubuntu-24.04-arm + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup + uses: ./.github/actions/setup-toolchain + + - name: 🌐 Check for missing or dynamic i18n keys + run: aube run i18n:report + + - name: 🌐 Check i18n schema is up to date + run: | + aube run i18n:schema + git diff --exit-code packages/i18n/schemas packages/i18n/locales diff --git a/apps/dashboard/app/app.vue b/apps/dashboard/app/app.vue index 766c398..66e8078 100644 --- a/apps/dashboard/app/app.vue +++ b/apps/dashboard/app/app.vue @@ -7,9 +7,9 @@