diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3071ac905..3c2a79f42 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -87,6 +87,7 @@ # Framework and platform integrations. Hong Minhee maintains all of # them; ChanHaeng Lee co-maintains the ones he contributed to, and # leads the Nuxt integration. +/packages/adonisjs/ @scammo @dahlia /packages/astro/ @2chanhaeng @dahlia /packages/cfworkers/ @dahlia @2chanhaeng /packages/elysia/ @dahlia diff --git a/.hongdown.toml b/.hongdown.toml index 9d34efbaa..5d67e6ba9 100644 --- a/.hongdown.toml +++ b/.hongdown.toml @@ -21,6 +21,7 @@ exclude = [ [heading] sentence_case = true proper_nouns = [ + "@fedify/adonisjs", "@fedify/cli", "@fedify/elysia", "@fedify/express", @@ -45,6 +46,7 @@ proper_nouns = [ "ActivityKit", "ActivityPub", "ActivityStreams", + "AdonisJS", "Akkoma", "Biome", "BotKit", diff --git a/CHANGES.md b/CHANGES.md index 64f470515..a258a5bcd 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -134,6 +134,18 @@ To be released. [#934]: https://github.com/fedify-dev/fedify/pull/934 [#968]: https://github.com/fedify-dev/fedify/pull/968 +### @fedify/adonisjs + + - Added the new *@fedify/adonisjs* package, an integration for the AdonisJS + framework. It provides a server middleware that mounts a `Federation` + inside an AdonisJS application, a service provider that owns the + federation's lifecycle, a `node ace configure` hook that scaffolds + *config/fedify.ts* and the federation preload files, and a `ctx.federation` + request context. The package targets Node.js and is published to npm only. + [[#139]] + +[#139]: https://github.com/fedify-dev/fedify/issues/139 + ### @fedify/astro - Added and continuously tested support for Astro 6 and 7 while retaining diff --git a/changes.d/adonisjs/adonisjs-integration.md b/changes.d/adonisjs/adonisjs-integration.md new file mode 100644 index 000000000..d88947ccc --- /dev/null +++ b/changes.d/adonisjs/adonisjs-integration.md @@ -0,0 +1,11 @@ +--- +links: + '#139': https://github.com/fedify-dev/fedify/issues/139 +--- + - Added the new *@fedify/adonisjs* package, an integration for the AdonisJS + framework. It provides a server middleware that mounts a `Federation` + inside an AdonisJS application, a service provider that owns the + federation's lifecycle, a `node ace configure` hook that scaffolds + *config/fedify.ts* and the federation preload files, and a `ctx.federation` + request context. The package targets Node.js and is published to npm only. + [[#139]] diff --git a/cspell.json b/cspell.json index 07a948002..fa8d362ac 100644 --- a/cspell.json +++ b/cspell.json @@ -3,6 +3,8 @@ "aarch64", "activitypub", "activitystreams", + "adonis", + "adonisjs", "aitertools", "amqp", "amqplib", @@ -18,6 +20,7 @@ "cfworker", "cfworkers", "codegen", + "codemods", "compactable", "cryptosuite", "decorrelated", @@ -64,6 +67,7 @@ "lifecycles", "litepub", "logtape", + "lucid", "lume", "lumocs", "metas", diff --git a/deno.json b/deno.json index 279f0df69..255046784 100644 --- a/deno.json +++ b/deno.json @@ -129,6 +129,7 @@ "./packages" ], "exclude": [ + "./packages/adonisjs/**", "./packages/init/src/templates/**", "./packages/next/**" ] diff --git a/docs/manual/integration.md b/docs/manual/integration.md index b0b372fde..33bf44fb6 100644 --- a/docs/manual/integration.md +++ b/docs/manual/integration.md @@ -79,6 +79,95 @@ sequenceDiagram [content negotiation]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation +AdonisJS +-------- + +*This API is available since Fedify 2.4.0.* + +[AdonisJS] is a batteries-included TypeScript framework for Node.js, with its +own IoC container, service providers and CLI. The *@fedify/adonisjs* package +integrates Fedify with AdonisJS. It requires Node.js 24 or later, matching +the `engines` requirement `@adonisjs/core` itself declares, and targets +Node.js only: AdonisJS documents Node.js as its sole runtime, so the package +is published to npm and not to JSR, and is not tested on Deno or Bun. + +::: code-group + +~~~~ sh [npm] +node ace add @fedify/adonisjs +~~~~ + +~~~~ sh [pnpm] +node ace add --package-manager=pnpm @fedify/adonisjs +~~~~ + +~~~~ sh [Yarn] +node ace add --package-manager=yarn @fedify/adonisjs +~~~~ + +::: + +Unlike the other integrations, there is nothing to wire up by hand. The +`configure` hook that `node ace add` runs registers the service provider and the +server middleware, and writes *config/fedify.ts*, *start/federation.ts* and +*app/federation/main.ts*. + +Set the canonical origin in *config/fedify.ts*—Fedify mints actor URIs from it, +so it has to be the address remote servers can reach: + +~~~~ typescript twoslash +// config/fedify.ts +import { defineConfig } from "@fedify/adonisjs"; + +export default defineConfig({ + origin: "https://example.com", // [!code highlight] +}); +~~~~ + +Then register dispatchers on the builder that +*@fedify/adonisjs/services/builder* exports. The generated +*start/federation.ts* preload file imports this module, which AdonisJS loads +after every service provider has booted—so dispatchers may use Lucid models and +anything else the container provides: + +~~~~ typescript twoslash +// app/federation/main.ts +import federation from "@fedify/adonisjs/services/builder"; +import { Person } from "@fedify/vocab"; + +federation.setActorDispatcher( + "/actors/{identifier}", + (ctx, identifier) => { // [!code highlight] + return new Person({ // [!code highlight] + id: ctx.getActorUri(identifier), // [!code highlight] + preferredUsername: identifier, // [!code highlight] + }); // [!code highlight] + }, +); +~~~~ + +Every request then carries a Fedify `RequestContext` as `ctx.federation`, so an +ordinary controller can mint URIs, look remote objects up, or send activities: + +~~~~ typescript twoslash +import "@fedify/adonisjs/types"; +import type { HttpContext } from "@adonisjs/core/http"; +// ---cut-before--- +export default class ActorsController { + async show({ params, federation }: HttpContext) { + return { actor: federation.getActorUri(params.identifier).href }; + } +} +~~~~ + +The middleware is registered in the *server* middleware stack rather than the +router stack, because Fedify serves paths the AdonisJS router knows nothing +about—*/.well-known/webfinger* among them—and because HTTP Signature +verification needs the request body before the body parser consumes it. + +[AdonisJS]: https://adonisjs.com/ + + Express ------- diff --git a/docs/package.json b/docs/package.json index b8d24c4ec..5ab649d73 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,8 +1,10 @@ { "devDependencies": { + "@adonisjs/core": "catalog:", "@braintree/sanitize-url": "^7.1.2", "@cloudflare/workers-types": "catalog:", "@deno/kv": "^0.8.4", + "@fedify/adonisjs": "workspace:^", "@fedify/amqp": "workspace:^", "@fedify/astro": "workspace:^", "@fedify/backfill": "workspace:^", @@ -17,8 +19,8 @@ "@fedify/koa": "workspace:^", "@fedify/lint": "workspace:^", "@fedify/mysql": "workspace:^", - "@fedify/netlify": "workspace:^", "@fedify/nestjs": "workspace:^", + "@fedify/netlify": "workspace:^", "@fedify/next": "workspace:^", "@fedify/postgres": "workspace:^", "@fedify/redis": "workspace:^", diff --git a/packages/adonisjs/README.md b/packages/adonisjs/README.md new file mode 100644 index 000000000..e3274416c --- /dev/null +++ b/packages/adonisjs/README.md @@ -0,0 +1,332 @@ + + +@fedify/adonisjs: Integrate Fedify with AdonisJS +================================================ + +[![npm][npm badge]][npm] +[![Matrix][Matrix badge]][Matrix] +[![@fedify@hackers.pub][@fedify@hackers.pub badge]][@fedify@hackers.pub] + +*This package is available since Fedify 2.4.0.* + +[Fedify] is a TypeScript library for building federated server applications +powered by [ActivityPub] and other standards, so-called [fediverse]. This +package integrates Fedify with [AdonisJS], a fully featured Node.js web +framework. + +It gives an AdonisJS application everything Fedify needs and nothing it does +not: a server middleware that mounts the federation, a service provider that +owns the `Federation` object's lifecycle, a `config/fedify.ts` file, and a +`ctx.federation` context on every request. + +[npm badge]: https://img.shields.io/npm/v/@fedify/adonisjs?logo=npm +[npm]: https://www.npmjs.com/package/@fedify/adonisjs +[Matrix badge]: https://img.shields.io/matrix/fedify%3Amatrix.org +[Matrix]: https://matrix.to/#/#fedify:matrix.org +[@fedify@hackers.pub badge]: https://fedi-badge.minhee.org/@fedify@hackers.pub/followers.svg +[@fedify@hackers.pub]: https://hackers.pub/@fedify +[Fedify]: https://fedify.dev/ +[ActivityPub]: https://www.w3.org/TR/activitypub/ +[fediverse]: https://en.wikipedia.org/wiki/Fediverse +[AdonisJS]: https://adonisjs.com/ + + +Requirements +------------ + + - AdonisJS 7.0 or later + - Fedify 2.3.4 or later + - Node.js 24 or later — and Node.js only; see below + +The package ships both ESM and CommonJS builds. The CommonJS one works because +Node.js 24 supports `require()` of ESM dependencies — which is also why the +Node.js floor is not lower. + +### Runtime support: Node.js only + +> [!IMPORTANT] +> This package targets Node.js exclusively. It is published to npm only, never +> to JSR, and it is neither tested nor supported on Deno or Bun. + +That is a deliberate scope decision inherited from the framework rather than an +oversight, for three reasons. + +**AdonisJS documents Node.js as its only runtime.** Its installation guide +lists Node.js ≥ 24 and npm ≥ 11 as the sole prerequisites and never mentions +Deno or Bun, and its maintainers have +[declined to support multiple runtimes][adonisjs-runtimes]. An integration +package cannot credibly promise a runtime that the framework it integrates does +not. + +**The Node.js 24 floor is AdonisJS's own.** `@adonisjs/core` declares +`engines.node >= 24.0.0`; this package matches that floor rather than inventing +one. + +**JSR cannot represent this package.** The AdonisJS-native layer works by +augmenting AdonisJS's own module types — `ctx.federation` on `HttpContext`, and +the `fedify`, `fedify.builder` and `fedify.config` container bindings. JSR's +[“slow types”][jsr-slow-types] rules reject ambient `declare module` +augmentation anywhere in a published module graph, and the augmentation cannot +be hidden from that graph without also hiding it from the applications that need +it. So the feature and JSR publication are mutually exclusive, and the feature +is the entire point of the package. + +Consequently this package ships no *deno.json*, defines no `test:bun` script, +and stays out of the Fedify monorepo's Deno workspace — the same position as +`@fedify/nestjs` and `@fedify/next`. + +[adonisjs-runtimes]: https://github.com/orgs/adonisjs/discussions/4191 +[jsr-slow-types]: https://jsr.io/docs/about-slow-types#global-augmentation + + +Installation +------------ + +~~~~ sh +node ace add @fedify/adonisjs +~~~~ + +That installs the package and runs its `configure` hook, which does the whole +wiring: + + - installs `@fedify/fedify` and `@fedify/vocab`, + - adds `PUBLIC_URL` (and an optional `FEDERATION_HANDLE_HOST`) to + *start/env.ts* and *.env*, + - writes *config/fedify.ts*, *start/federation.ts* and + *app/federation/main.ts*, + - registers the service provider and the `start/federation.ts` preload file in + *adonisrc.ts*, and + - adds the middleware to the **server** middleware stack in *start/kernel.ts*. + +To install it without running the hook, use `npm install @fedify/adonisjs` +followed by `node ace configure @fedify/adonisjs`. + + +Usage +----- + +### Registering dispatchers + +Actor dispatchers, collection dispatchers and inbox listeners are registered on +the `FederationBuilder` exported by `@fedify/adonisjs/services/builder`: + +~~~~ typescript +// app/federation/main.ts +import federation from '@fedify/adonisjs/services/builder' +import { Endpoints, Person } from '@fedify/vocab' +import Actor from '#models/actor' + +federation + .setActorDispatcher('/actors/{identifier}', async (ctx, identifier) => { + const actor = await Actor.findBy('identifier', identifier) + if (actor === null) return null + + const keys = await ctx.getActorKeyPairs(identifier) + + return new Person({ + id: ctx.getActorUri(identifier), + preferredUsername: identifier, + name: actor.name, + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), + publicKeys: keys.map((keyPair) => keyPair.cryptographicKey), + assertionMethods: keys.map((keyPair) => keyPair.multikey), + }) + }) + .setKeyPairsDispatcher(async (ctx, identifier) => { + // Load or generate the actor's key pairs. + }) +~~~~ + +The module is loaded by the *start/federation.ts* preload file, which AdonisJS +imports after every service provider has booted — so dispatchers may use Lucid +models, the container, and anything else the application provides. Import +further modules from that file as the federation grows. + +Registration happens once per process. Do not add *app/federation* to +`hotHook.boundaries`: re-importing a module on a hot reload would register the +same dispatcher twice, and Fedify rejects that. Restart the dev server after +changing a dispatcher. + +### Using the federation from controllers + +The middleware attaches a Fedify `RequestContext` to every request: + +~~~~ typescript +import type { HttpContext } from '@adonisjs/core/http' + +export default class ActorsController { + // GET /actors/:identifier — the same URL template the actor dispatcher + // serves, so the route parameter is the local identifier. + async show({ params, view, federation }: HttpContext) { + return view.render('pages/actors/show', { + actorUri: federation.getActorUri(params.identifier).href, + }) + } + + // GET /lookup/:handle — dereference a remote object by its fediverse handle + // or URI, for example `@fedify@hackers.pub`. + async lookup({ params, view, federation }: HttpContext) { + const object = await federation.lookupObject(params.handle) + + return view.render('pages/actors/lookup', { object }) + } +} +~~~~ + +The context is created lazily on first access, so requests that never touch it +pay nothing for it. + +Outside an HTTP request — in an Ace command, a scheduled job or a queue worker — +resolve the `Federation` object from the container and create a context from +the canonical origin: + +~~~~ typescript +import env from '#start/env' + +const federation = await this.app.container.make('fedify') +const ctx = federation.createContext(new URL(env.get('PUBLIC_URL')), null) +await ctx.sendActivity({ identifier: 'me' }, recipient, activity) +~~~~ + +The binding resolves only once the application is ready; resolving it earlier — +from a preload file or a service provider — throws `E_FEDERATION_NOT_READY` +rather than handing back a federation with no routes. Preload files should +register dispatchers on *@fedify/adonisjs/services/builder* instead. + +### Without the container + +`fedifyMiddleware` is the package's main integration function and its default +export. It takes a `Federation` and an optional context-data factory, and has +nothing to do with the IoC container, so an application that would rather build +its own `Federation` can skip the provider entirely: + +~~~~ typescript +// start/kernel.ts +import fedifyMiddleware from '@fedify/adonisjs' +import federation from '#federation/main' + +server.use([async () => ({ default: fedifyMiddleware(federation) })]) +~~~~ + +### Configuration + +*config/fedify.ts* takes every [`FederationOptions`] Fedify accepts, plus a few +AdonisJS-specific keys: + +~~~~ typescript +import { defineConfig } from '@fedify/adonisjs' +import { SqliteKvStore, SqliteMessageQueue } from '@fedify/sqlite' +import env from '#start/env' + +export default defineConfig({ + origin: env.get('PUBLIC_URL'), + kv: new SqliteKvStore(database), + queue: new SqliteMessageQueue(database), + queueContextData: () => null, + ignoreRoutePrefixes: ['/@vite/'], +}) +~~~~ + +| Option | Purpose | +| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| `origin` | Required. The canonical public origin. Actor URIs and activity IDs are minted from it. | +| `kv` | Fedify's key–value store. Defaults to an in-memory store, with a warning in production. | +| `contextDataFactory` | Builds the per-request context data. Defaults to the `HttpContext` itself, and is required once `FedifyTypes` is augmented. | +| `ignoreRoutePrefixes` | URL path prefixes that skip Fedify entirely. Defaults to none. | +| `logging` | Whether to pipe Fedify's LogTape output into the AdonisJS logger. Defaults to `true`. | +| `queueContextData` | Who drains the queue: a callback (this process) or `false` (a separate worker). Required whenever `queue` is set. | + +[`FederationOptions`]: https://jsr.io/@fedify/fedify/doc/~/FederationOptions + +### Module exports + +| Specifier | What it is | +| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| `@fedify/adonisjs` | `fedifyMiddleware` (default), `defineConfig`, `configure`, `configureFedifyLogging`, the error classes and the types | +| `@fedify/adonisjs/types` | The augmentation target for `FedifyTypes` | +| `@fedify/adonisjs/provider` | Referenced by specifier from *adonisrc.ts* | +| `@fedify/adonisjs/middleware` | Referenced by specifier from *start/kernel.ts* | +| `@fedify/adonisjs/services/builder` | The `FederationBuilder`, as an AdonisJS service module | + +Each subpath exists because AdonisJS reaches for it by module specifier or by +service-module convention; everything else lives on the root export. The built +`Federation` deliberately has no service module of its own — resolve it with +`app.container.make('fedify')`, which can also tell you *why* when it is not +available yet. + +### Context data + +Fedify threads an application-defined value through every dispatcher and inbox +listener. By default that value is `HttpContext | null` — `null` when Fedify is +working outside a request, such as inside a queue worker. To carry something +else, augment the register interface and supply a factory. The augmentation +makes `contextDataFactory` a required option, so forgetting it is a type error +in *config/fedify.ts* rather than an `HttpContext` turning up where a dispatcher +expected the augmented value: + +~~~~ typescript +declare module '@fedify/adonisjs/types' { + interface FedifyTypes { + contextData: { user: User | null } + } +} + +export default defineConfig({ + origin: env.get('PUBLIC_URL'), + contextDataFactory: async (ctx) => ({ user: await ctx.auth.user ?? null }), +}) +~~~~ + + +How requests are routed +----------------------- + +The middleware runs in the **server** stack, before route matching and before +the body parser. Both matter: Fedify serves paths the AdonisJS router knows +nothing about, and HTTP Signature verification needs the raw request body. It +also runs before `@adonisjs/shield`, so inbox `POST`s are not rejected by CSRF +protection. + +Every request reaches Fedify first, and one of three things happens. + + - **Fedify serves it.** Actor documents, collections, inboxes, + */.well-known/webfinger* and */.well-known/nodeinfo* never reach the + AdonisJS router. + - **Fedify has no route for the path.** The request is handed to the AdonisJS + router, and the application answers exactly as it would without this + package. + - **Fedify has a route but cannot satisfy the `Accept` header.** This is the + interesting one. Rather than answering `406` immediately, the middleware + gives the AdonisJS router a chance first, and only answers + `406 Not Acceptable` if the router had nothing either. + +That last rule is what lets a single URL serve an HTML profile page to browsers +and an ActivityPub actor document to servers. Register an ordinary route on the +same path as the actor dispatcher and it will handle the HTML half: + +~~~~ typescript +// start/routes.ts — same path as setActorDispatcher('/actors/{identifier}', …) +router.get('/actors/:identifier', [controllers.Actors, 'show']) +~~~~ + + +Deployment notes +---------------- + + - **Set `origin` to the public URL.** Fedify mints actor URIs from it, so + behind a reverse proxy, a load balancer or a tunnel it must be the address + remote servers can reach, not the address Node listens on. When developing + with `fedify tunnel`, point `PUBLIC_URL` at the tunnel and restart. + - **Configure a persistent `kv` and a `queue`.** Without a queue, delivery is + synchronous and failed deliveries are never retried. `@fedify/sqlite`, + `@fedify/postgres`, `@fedify/redis` and `@fedify/mysql` all provide both. + Setting `queue` also requires `queueContextData`, which says who drains it: + a callback has the web process do it (started in the `ready` hook, stopped + on shutdown, and never started in an Ace command or a test run), while + `false` leaves the queue to a separate worker. + - **Configure `trustProxy`** in *config/app.ts* if AdonisJS needs to see the + original protocol and host. + +A complete, runnable application using all of this lives in the +[*examples/adonisjs*](../../examples/adonisjs) directory. diff --git a/packages/adonisjs/configure.ts b/packages/adonisjs/configure.ts new file mode 100644 index 000000000..dde690090 --- /dev/null +++ b/packages/adonisjs/configure.ts @@ -0,0 +1,110 @@ +/** + * The configure hook, run by `node ace configure @fedify/adonisjs`. + * + * It performs every wiring step the package needs, so that a freshly configured + * application already federates: install the Fedify runtime packages, declare + * the environment variables, publish the config file and the federation preload + * files, register the service provider, and add the middleware to the server + * stack. + * + * @module + */ +import type ConfigureCommand from "@adonisjs/core/commands/configure"; + +import { stubsRoot } from "./stubs/main.ts"; + +/** + * The slice of `@adonisjs/assembler`'s `RcFileTransformer` this hook touches. + * + * Spelled out rather than inferred because the assembler is an *optional* peer + * dependency: wherever it is absent — a type-check of a tree that did not + * install it, for instance — the inferred parameter type of `updateRcFile`'s + * callback degrades to `any`, and this file stops compiling under + * `noImplicitAny`. + */ +interface RcFileEditor { + addProvider(providerPath: string): unknown; + addPreloadFile(modulePath: string): unknown; +} + +export async function configure(command: ConfigureCommand): Promise { + const codemods = await command.createCodemods(); + + /** + * Fedify itself is a peer dependency: the application, not this package, + * decides which version it runs, and having exactly one copy in the tree + * matters because Fedify's vocabulary classes are compared with `instanceof`. + * + * `@fedify/vocab` carries the ActivityStreams classes (`Person`, `Follow`, + * `Note`, …) that dispatchers and inbox listeners are written against. + */ + await codemods.installPackages([ + { name: "@fedify/fedify", isDevDependency: false }, + { name: "@fedify/vocab", isDevDependency: false }, + ]); + + /** + * `PUBLIC_URL` is the canonical origin remote servers use to reach this + * application. Fedify mints actor URIs and activity IDs from it, so it has to + * be the public address rather than the listen address — behind a proxy or a + * `fedify tunnel` those differ. + * + * `FEDERATION_HANDLE_HOST` is optional and only needed when fediverse handles + * live on a different domain than the web origin, for example `@me@example.com` + * served from `https://social.example.com`. + */ + await codemods.defineEnvValidations({ + leadingComment: "Variables for configuring Fedify", + variables: { + PUBLIC_URL: `Env.schema.string({ format: 'url', tld: false })`, + FEDERATION_HANDLE_HOST: `Env.schema.string.optional({ format: 'host' })`, + }, + }); + + await codemods.defineEnvVariables({ PUBLIC_URL: "http://localhost:3333" }); + + /** + * `config/fedify.ts` — the federation options. + */ + await codemods.makeUsingStub(stubsRoot, "config/fedify.stub", {}); + + /** + * `start/federation.ts` — a preload file that imports the application's + * dispatcher modules. + * + * A preload file rather than directory globbing: preload files are ordinary + * application source, so `node ace build` compiles them and their relative + * imports along with everything else, and AdonisJS imports them after every + * provider has booted, which is what lets dispatchers use Lucid models. + */ + await codemods.makeUsingStub(stubsRoot, "start/federation.stub", {}); + + /** + * `app/federation/main.ts` — a starting point for dispatchers. + */ + await codemods.makeUsingStub(stubsRoot, "federation/main.stub", {}); + + await codemods.updateRcFile((rcFile: RcFileEditor) => { + rcFile.addProvider("@fedify/adonisjs/provider"); + rcFile.addPreloadFile("#start/federation"); + }); + + /** + * The middleware must live in the **server** stack. + * + * Fedify answers paths the AdonisJS router does not know about + * (`/.well-known/webfinger`, `/.well-known/nodeinfo`, actor and inbox + * endpoints), so it has to run before route matching. The server stack also + * runs before `bodyparser_middleware` and before `@adonisjs/shield`, which + * matters twice over: HTTP Signature verification needs the raw, unparsed + * request body, and inbox `POST`s must not be rejected by CSRF protection. + */ + await codemods.registerMiddleware("server", [{ + path: "@fedify/adonisjs/middleware", + }]); + + command.logger.success("Configured @fedify/adonisjs"); + command.logger.info( + 'Register dispatchers in app/federation/main.ts, then run "node ace serve"', + ); +} diff --git a/packages/adonisjs/index.ts b/packages/adonisjs/index.ts new file mode 100644 index 000000000..412373410 --- /dev/null +++ b/packages/adonisjs/index.ts @@ -0,0 +1,53 @@ +/** + * `@fedify/adonisjs` — integrate [Fedify] with [AdonisJS]. + * + * The package has two layers. Most applications only need the first: + * + * 1. **The AdonisJS-native layer.** Run `node ace configure @fedify/adonisjs` + * and the package installs a service provider, a server middleware, a + * `config/fedify.ts` file built with {@link defineConfig}, and a + * `start/federation.ts` preload file. Dispatchers are registered on the + * builder exported by `@fedify/adonisjs/services/builder`, and every request + * gets a `ctx.federation` context. + * + * 2. **The plain middleware layer.** {@link fedifyMiddleware} — the default + * export — takes a `Federation` instance and an optional context-data + * factory and returns an AdonisJS middleware, with no container or + * configuration involved. This mirrors `integrateFederation()` from + * `@fedify/express` and is there for applications that build their + * `Federation` object themselves. + * + * [Fedify]: https://fedify.dev/ + * [AdonisJS]: https://adonisjs.com/ + * + * @module + */ +export { configure } from "./configure.ts"; +// Re-exported so that `node ace eject --pkg=@fedify/adonisjs` can find +// the templates; the configure command picks it up from here too. +export { stubsRoot } from "./stubs/main.ts"; +export { defineConfig } from "./src/define_config.ts"; +export { default, fedifyMiddleware } from "./src/middleware.ts"; +export { configureFedifyLogging } from "./src/logging.ts"; +export { + E_FEDERATION_NOT_READY, + E_FEDIFY_CONTEXT_UNAVAILABLE, + E_INVALID_FEDIFY_CONFIG, +} from "./src/errors.ts"; + +export type { + FedifyMiddlewareClass, + FedifyMiddlewareHandler, + FedifyMiddlewareOptions, +} from "./src/middleware.ts"; +export type { FedifyLoggingOptions } from "./src/logging.ts"; +export type { + ContextData, + ContextDataFactory, + ContextDataFactoryOption, + FedifyConfig, + FedifyConfigBase, + FedifyLoggingConfig, + FedifyTypes, + ResolvedFedifyConfig, +} from "./src/types.ts"; diff --git a/packages/adonisjs/package.json b/packages/adonisjs/package.json new file mode 100644 index 000000000..00f28c1db --- /dev/null +++ b/packages/adonisjs/package.json @@ -0,0 +1,126 @@ +{ + "name": "@fedify/adonisjs", + "version": "2.4.0", + "description": "Integration package for Fedify with AdonisJS", + "keywords": [ + "Fedify", + "Federation", + "ActivityPub", + "AdonisJS", + "fediverse" + ], + "author": { + "name": "scammo", + "email": "pfannkuchensack.ae@gmail.com" + }, + "homepage": "https://fedify.dev/", + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/adonisjs" + }, + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "license": "MIT", + "type": "module", + "engines": { + "node": ">=24.0.0" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": { + "import": "./dist/index.d.ts", + "require": "./dist/index.d.cts", + "default": "./dist/index.d.ts" + }, + "import": "./dist/index.js", + "require": "./dist/index.cjs", + "default": "./dist/index.js" + }, + "./types": { + "types": { + "import": "./dist/src/types.d.ts", + "require": "./dist/src/types.d.cts", + "default": "./dist/src/types.d.ts" + }, + "import": "./dist/src/types.js", + "require": "./dist/src/types.cjs", + "default": "./dist/src/types.js" + }, + "./provider": { + "types": { + "import": "./dist/src/provider.d.ts", + "require": "./dist/src/provider.d.cts", + "default": "./dist/src/provider.d.ts" + }, + "import": "./dist/src/provider.js", + "require": "./dist/src/provider.cjs", + "default": "./dist/src/provider.js" + }, + "./middleware": { + "types": { + "import": "./dist/src/fedify_middleware.d.ts", + "require": "./dist/src/fedify_middleware.d.cts", + "default": "./dist/src/fedify_middleware.d.ts" + }, + "import": "./dist/src/fedify_middleware.js", + "require": "./dist/src/fedify_middleware.cjs", + "default": "./dist/src/fedify_middleware.js" + }, + "./services/builder": { + "types": { + "import": "./dist/src/services/builder.d.ts", + "require": "./dist/src/services/builder.d.cts", + "default": "./dist/src/services/builder.d.ts" + }, + "import": "./dist/src/services/builder.js", + "require": "./dist/src/services/builder.cjs", + "default": "./dist/src/services/builder.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "package.json" + ], + "scripts": { + "clean": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "build:self": "tsdown", + "build": "pnpm --filter @fedify/adonisjs... run build:self", + "prepack": "pnpm build", + "prepublish": "pnpm build", + "pretest": "pnpm run build:self", + "typecheck": "tsc --noEmit", + "test": "node --test \"src/**/*.test.ts\"" + }, + "peerDependencies": { + "@adonisjs/assembler": "catalog:", + "@adonisjs/core": "catalog:", + "@fedify/fedify": "workspace:^" + }, + "peerDependenciesMeta": { + "@adonisjs/assembler": { + "optional": true + } + }, + "dependencies": { + "@logtape/logtape": "catalog:" + }, + "devDependencies": { + "@adonisjs/assembler": "catalog:", + "@adonisjs/core": "catalog:", + "@fedify/fedify": "workspace:^", + "@fedify/vocab": "workspace:^", + "@types/node": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:" + } +} diff --git a/packages/adonisjs/src/builder.ts b/packages/adonisjs/src/builder.ts new file mode 100644 index 000000000..b400786e0 --- /dev/null +++ b/packages/adonisjs/src/builder.ts @@ -0,0 +1,46 @@ +/** + * The application-wide {@link FederationBuilder}. + * + * Fedify's builder pattern separates *registering* dispatchers from *building* + * the `Federation` object. That separation is what makes an AdonisJS + * integration possible at all: dispatcher registration happens while the + * application boots (in preload files, which may import Lucid models and other + * container services), while the `Federation` itself can only be built once + * every registration is in — and once `config/fedify.ts` has been resolved. + * + * The builder is a module-level singleton for the same reason AdonisJS's own + * `router` and `server` service modules are: application code has to be able to + * reach it from a plain `import` at module scope, without an `await`. + * + * Note that this module deliberately imports nothing from AdonisJS. The + * original prototype of this package bound the builder into the IoC container + * as an import side effect, which meant the container binding existed only if + * some other module happened to import this one first. Binding is the service + * provider's job instead; see `provider.ts`. + * + * On the dual build: this is the one module with state, so the classic + * dual-package hazard applies — a process that loaded both the ESM and the + * CommonJS copy would hold two builders. In practice it cannot happen where it + * would matter: AdonisJS applications are ESM by construction, so the provider, + * the preload files and the app's federation modules all resolve the ESM copy. + * The CommonJS entry exists for the container-free `fedifyMiddleware` layer, + * which is stateless. + * + * @module + */ +import { + createFederationBuilder, + type FederationBuilder, +} from "@fedify/fedify"; + +import type { ContextData } from "./types.ts"; + +/** + * The builder that dispatchers and inbox listeners are registered on. + * + * Prefer importing it through the `@fedify/adonisjs/services/builder` service + * module, which is the path the generated stubs and the documentation use. + */ +export const builder: FederationBuilder = createFederationBuilder< + ContextData +>(); diff --git a/packages/adonisjs/src/define_config.test.ts b/packages/adonisjs/src/define_config.test.ts new file mode 100644 index 000000000..a7210eb1c --- /dev/null +++ b/packages/adonisjs/src/define_config.test.ts @@ -0,0 +1,258 @@ +/** + * Tests for `defineConfig()` and the service provider's lifecycle. + * + * @module + */ +import { match, rejects, strictEqual } from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { configProvider } from "@adonisjs/core"; +import { AppFactory } from "@adonisjs/core/factories/app"; +import type { ApplicationService } from "@adonisjs/core/types"; +import { + InProcessMessageQueue, + MemoryKvStore, + type MessageQueue, + type MessageQueueListenOptions, +} from "@fedify/fedify"; + +import { defineConfig } from "./define_config.ts"; +import { E_FEDERATION_NOT_READY } from "./errors.ts"; +import FedifyProvider from "./provider.ts"; +import type { FedifyConfig, ResolvedFedifyConfig } from "./types.ts"; + +async function createApp( + config: Record = {}, + environment?: "web" | "console" | "test" | "repl", +): Promise { + const app = new AppFactory().create( + new URL("../", import.meta.url), + () => import("node:util"), + ) as ApplicationService; + app.useConfig(config); + // The environment is frozen once the application boots, and the provider only + // starts a queue worker in the `web` one. + if (environment !== undefined) app.setEnvironment(environment); + await app.init(); + // `app.config` only exists after boot, and the provider reads it. + await app.boot(); + return app; +} + +async function resolve( + config: FedifyConfig, + app?: ApplicationService, +): Promise { + const application = app ?? (await createApp()); + const resolved = await configProvider.resolve( + application, + defineConfig(config), + ); + strictEqual(resolved !== null, true); + return resolved!; +} + +/** + * Swaps the container's `logger` binding for one that records what it is told. + * + * Both `defineConfig` and the provider resolve the logger from the container + * rather than importing one, so a `bindValue` is enough to capture it — and the + * bare `AppFactory` application has no `logger` binding of its own. + */ +function bindStubLogger( + app: ApplicationService, +): { warnings: string[]; errors: unknown[] } { + const warnings: string[] = []; + const errors: unknown[] = []; + app.container.bindValue( + "logger", + { + warn: (message: string) => void warnings.push(message), + error: (details: unknown) => void errors.push(details), + } as never, + ); + return { warnings, errors }; +} + +describe("defineConfig", () => { + it("fills in the AdonisJS-specific defaults", async () => { + const config = await resolve({ origin: "https://example.com" }); + + strictEqual(config.origin, "https://example.com"); + strictEqual(config.kv instanceof MemoryKvStore, true); + strictEqual(config.logging, true); + strictEqual(config.ignoreRoutePrefixes.length, 0); + strictEqual(typeof config.contextDataFactory, "function"); + }); + + it("defaults the context data factory to the HttpContext itself", async () => { + const config = await resolve({ origin: "https://example.com" }); + const fakeContext = { marker: "ctx" }; + + strictEqual( + await config.contextDataFactory(fakeContext as never), + fakeContext, + ); + }); + + it("rejects a configuration without an origin", async () => { + await rejects( + () => resolve({ origin: "" }), + (error: Error) => /Missing "origin"/.test(error.message), + ); + }); + + it("keeps a caller-provided key-value store", async () => { + const kv = new MemoryKvStore(); + const config = await resolve({ origin: "https://example.com", kv }); + + strictEqual(config.kv, kv); + }); + + it("forces manual queue start when the application drains the queue", async () => { + const config = await resolve({ + origin: "https://example.com", + queueContextData: () => null as never, + }); + + strictEqual(config.manuallyStartQueue, true); + }); + + it("leaves manuallyStartQueue alone when no queue context data is given", async () => { + const config = await resolve({ origin: "https://example.com" }); + + strictEqual(config.manuallyStartQueue, undefined); + }); + + it("rejects a queue without queue context data", async () => { + // There is no safe default: Fedify's own lazy start would capture whichever + // request happened to enqueue first and reuse its context data forever. + await rejects( + () => + resolve({ + origin: "https://example.com", + queue: new InProcessMessageQueue(), + }), + (error: Error) => /"queueContextData"/.test(error.message), + ); + }); + + it("warns about the in-memory key-value store in production", async () => { + const app = await createApp(); + Object.defineProperty(app, "inProduction", { value: true }); + const { warnings } = bindStubLogger(app); + + const config = await resolve({ origin: "https://example.com" }, app); + + strictEqual(config.kv instanceof MemoryKvStore, true); + strictEqual(warnings.length, 1); + match(warnings[0]!, /in-memory key-value store in production/); + }); + + it("stays quiet about a caller-provided store in production", async () => { + const app = await createApp(); + Object.defineProperty(app, "inProduction", { value: true }); + const { warnings } = bindStubLogger(app); + + await resolve( + { origin: "https://example.com", kv: new MemoryKvStore() }, + app, + ); + + strictEqual(warnings.length, 0); + }); +}); + +/** + * A queue whose `listen()` keeps running for a turn after it is aborted, the + * way a real backend finishes the delivery it is holding. + */ +class SlowQueue implements MessageQueue { + listening = false; + stopped = false; + + enqueue(): Promise { + return Promise.resolve(); + } + + async listen( + _handler: (message: unknown) => Promise | void, + options: MessageQueueListenOptions = {}, + ): Promise { + this.listening = true; + await new Promise((resolve) => { + options.signal?.addEventListener( + "abort", + () => setTimeout(resolve, 10), + ); + }); + this.stopped = true; + } +} + +describe("FedifyProvider", () => { + it("binds the builder and refuses to build the federation too early", async () => { + const app = await createApp({ + fedify: defineConfig({ origin: "https://example.com" }), + }); + + const provider = new FedifyProvider(app); + provider.register(); + + // The builder is available immediately: preload files register dispatchers + // on it long before the federation itself can exist. + const builder = await app.container.make("fedify.builder"); + strictEqual(typeof builder.build, "function"); + + // The federation is not, and says why. + await rejects( + () => app.container.make("fedify"), + (error: Error) => error instanceof E_FEDERATION_NOT_READY, + ); + }); + + it("builds the federation once the application is ready", async () => { + const app = await createApp({ + fedify: defineConfig({ origin: "https://example.com", logging: false }), + }); + + const provider = new FedifyProvider(app); + provider.register(); + await provider.boot(); + await provider.ready(); + + const federation = await app.container.make("fedify"); + strictEqual(typeof federation.fetch, "function"); + + await provider.shutdown(); + }); + + it("waits for the queue worker to stop before shutdown resolves", async () => { + // Aborting only asks the listeners to stop. `shutdown()` has to return the + // `startQueue()` promise, or AdonisJS tears the rest of the application + // down -- the database connection above all -- around a job still running. + const queue = new SlowQueue(); + const app = await createApp({ + fedify: defineConfig({ + origin: "https://example.com", + logging: false, + queue, + queueContextData: () => null as never, + }), + }, "web"); + + bindStubLogger(app); + + const provider = new FedifyProvider(app); + provider.register(); + await provider.boot(); + await provider.ready(); + + strictEqual(queue.listening, true); + strictEqual(queue.stopped, false); + + await provider.shutdown(); + + strictEqual(queue.stopped, true); + }); +}); diff --git a/packages/adonisjs/src/define_config.ts b/packages/adonisjs/src/define_config.ts new file mode 100644 index 000000000..b911bd681 --- /dev/null +++ b/packages/adonisjs/src/define_config.ts @@ -0,0 +1,118 @@ +/** + * The `defineConfig` helper used by `config/fedify.ts`. + * + * @module + */ +import { configProvider } from "@adonisjs/core"; +import type { ConfigProvider } from "@adonisjs/core/types"; +import { InvalidArgumentsException } from "@adonisjs/core/exceptions"; +import { MemoryKvStore } from "@fedify/fedify"; + +import type { + ContextData, + FedifyConfig, + ResolvedFedifyConfig, +} from "./types.ts"; + +/** + * Defines the Fedify configuration of an AdonisJS application. + * + * The return value is an AdonisJS *config provider* rather than a plain object, + * which lets the resolution step reach the application instance — needed to + * detect production mode and to log through the application's logger — while + * `config/fedify.ts` itself stays a simple, synchronous, statically analysable + * module. + * + * @example + * ```ts + * // config/fedify.ts + * import { defineConfig } from '@fedify/adonisjs' + * import { SqliteKvStore } from '@fedify/sqlite' + * import env from '#start/env' + * + * export default defineConfig({ + * origin: env.get('PUBLIC_URL'), + * kv: new SqliteKvStore(database), + * }) + * ``` + */ +export function defineConfig( + config: FedifyConfig, +): ConfigProvider { + return configProvider.create(async (app) => { + if (!config.origin) { + throw new InvalidArgumentsException( + 'Missing "origin" in config/fedify.ts. It must be the canonical public URL of this server, ' + + "usually read from the PUBLIC_URL environment variable", + ); + } + + let kv = config.kv; + if (kv === undefined) { + kv = new MemoryKvStore(); + + if (app.inProduction) { + // Not fatal — an application may genuinely run a single process and + // accept the trade-off — but silently losing inbox idempotency and the + // document cache on every restart is worth shouting about. + const logger = await app.container.make("logger"); + logger.warn( + "Fedify is using an in-memory key-value store in production. Inbox idempotency, " + + "the document cache and outbox state will be lost on restart and cannot be shared " + + 'between processes. Configure "kv" in config/fedify.ts with a persistent store such ' + + "as @fedify/sqlite, @fedify/postgres or @fedify/redis", + ); + } + } + + const { + contextDataFactory, + ignoreRoutePrefixes, + logging, + queueContextData, + ...federationOptions + } = config; + + /** + * A configured queue forces the "who drains it" decision to be explicit. + * + * Fedify's own default starts the queue lazily on the first enqueued task, + * capturing that request's context data and reusing it for every background + * job thereafter. For this integration the captured value would be a single + * stale `HttpContext`, whose request has long since finished. Rather than + * pick a wrong default, refuse to guess. + */ + if ( + federationOptions.queue !== undefined && queueContextData === undefined + ) { + throw new InvalidArgumentsException( + 'config/fedify.ts configures a "queue" but not "queueContextData". Set ' + + '"queueContextData" to a callback to have this process drain the queue, or to ' + + '"false" when a separate worker process owns it', + ); + } + + return { + ...federationOptions, + kv, + + // The queue is always started deliberately — by this package's service + // provider, or by a separate worker — never lazily by Fedify. + manuallyStartQueue: queueContextData !== undefined + ? true + : config.manuallyStartQueue, + + /** + * The cast is sound because `ContextDataFactoryOption` makes + * `contextDataFactory` a *required* key the moment an application + * augments `FedifyTypes`. This branch is therefore only reachable while + * `ContextData` still includes `HttpContext`, which is exactly what it + * returns. + */ + contextDataFactory: contextDataFactory ?? ((ctx) => ctx as ContextData), + ignoreRoutePrefixes: ignoreRoutePrefixes ?? [], + logging: logging ?? true, + queueContextData, + }; + }); +} diff --git a/packages/adonisjs/src/dist.test.ts b/packages/adonisjs/src/dist.test.ts new file mode 100644 index 000000000..60e176d83 --- /dev/null +++ b/packages/adonisjs/src/dist.test.ts @@ -0,0 +1,65 @@ +/** + * Guards the built artifacts, not the sources. + * + * The CommonJS half of the dual build only works because `require()` of an ESM + * graph is supported on this package's Node.js range (>= 24) **and** no module + * here uses top-level await. Both halves of that condition are silent — a + * top-level `await` would build fine and only fail at `require()` time — so + * this suite loads every published entry through the exports map in both + * module systems. + * + * The suite skips itself when `dist/` has not been built; run `npm run build` + * first for full coverage. + * + * @module + */ +import { ok, strictEqual } from "node:assert/strict"; +import { existsSync } from "node:fs"; +import { createRequire } from "node:module"; +import { describe, it } from "node:test"; + +const require = createRequire(import.meta.url); + +const distBuilt = existsSync(new URL("../dist/index.cjs", import.meta.url)); + +/** + * Every subpath the exports map advertises, as bare specifiers so the exports + * map itself is what resolves them. + */ +const ENTRIES = [ + "@fedify/adonisjs", + "@fedify/adonisjs/types", + "@fedify/adonisjs/provider", + "@fedify/adonisjs/middleware", + "@fedify/adonisjs/services/builder", +]; + +describe("built package", { skip: !distBuilt && "dist/ is not built" }, () => { + it("loads every entry from CommonJS", () => { + for (const entry of ENTRIES) { + // Throws ERR_REQUIRE_ESM if top-level await ever creeps into the graph. + require(entry); + } + }); + + it("loads every entry from ESM", async () => { + for (const entry of ENTRIES) { + await import(entry); + } + }); + + it("exposes a usable main function in both module systems", async () => { + const cjs = require("@fedify/adonisjs"); + const esm = await import("@fedify/adonisjs"); + + for (const m of [cjs, esm]) { + strictEqual(typeof m.fedifyMiddleware, "function"); + strictEqual(typeof m.defineConfig, "function"); + strictEqual(typeof m.configure, "function"); + } + + // The default export is the main function, per the integration guide. + strictEqual(esm.default, esm.fedifyMiddleware); + ok(new URL(cjs.stubsRoot, "file://").pathname.length > 0); + }); +}); diff --git a/packages/adonisjs/src/errors.ts b/packages/adonisjs/src/errors.ts new file mode 100644 index 000000000..e565a7138 --- /dev/null +++ b/packages/adonisjs/src/errors.ts @@ -0,0 +1,57 @@ +/** + * Exceptions raised by `@fedify/adonisjs`. + * + * They extend AdonisJS's `Exception` base class, so applications can match on + * `error.code` and the framework's exception handler renders them with the + * right HTTP status. + * + * Every message says what to do about the problem rather than only what went + * wrong: all three of these are configuration and lifecycle mistakes whose + * natural symptom would otherwise be a silent 404 from every federation + * endpoint. + * + * @module + */ +import { Exception } from "@adonisjs/core/exceptions"; + +/** + * Raised when `config/fedify.ts` is missing, or does not export the result of + * `defineConfig()`. + */ +export class E_INVALID_FEDIFY_CONFIG extends Exception { + static override code = "E_INVALID_FEDIFY_CONFIG"; + static override status = 500; + static override message = + 'Invalid "config/fedify.ts" file. Make sure to export the return value of the "defineConfig" method'; +} + +/** + * Raised when the `Federation` instance is resolved from the container before + * the application has finished booting. + * + * Dispatchers are registered on the `FederationBuilder` by preload files, and + * the builder can only be turned into a `Federation` once every preload has + * run. Resolving `'fedify'` earlier would silently produce a `Federation` with + * no routes at all, which is far harder to debug than an explicit error. + */ +export class E_FEDERATION_NOT_READY extends Exception { + static override code = "E_FEDERATION_NOT_READY"; + static override status = 500; + static override message = + 'The Federation instance is not available yet. It is built after the application boots, so it cannot be resolved from a service provider\'s register or boot method. Use "ctx.federation" inside HTTP requests, the "Context" argument inside dispatchers, or resolve it with "app.container.make(\'fedify\')" from code that runs once the application is ready'; +} + +/** + * Raised when `ctx.federation` is accessed on a request the Fedify middleware + * deliberately skipped, or on a request it never saw at all. + */ +export class E_FEDIFY_CONTEXT_UNAVAILABLE extends Exception { + static override code = "E_FEDIFY_CONTEXT_UNAVAILABLE"; + static override status = 500; + + constructor(path: string) { + super( + `Cannot access "ctx.federation" for "${path}". Either the request path matches "ignoreRoutePrefixes" in config/fedify.ts, the request method is one the Fetch API cannot represent (CONNECT, TRACE, TRACK), or the Fedify middleware is not registered in the server middleware stack of start/kernel.ts`, + ); + } +} diff --git a/packages/adonisjs/src/fedify_middleware.ts b/packages/adonisjs/src/fedify_middleware.ts new file mode 100644 index 000000000..36f79d5d2 --- /dev/null +++ b/packages/adonisjs/src/fedify_middleware.ts @@ -0,0 +1,54 @@ +/** + * The AdonisJS server middleware that mounts Fedify. + * + * Register it in the **server** stack of `start/kernel.ts` — the `configure` + * hook does this automatically: + * + * ```ts + * server.use([ + * // ... + * () => import('@fedify/adonisjs/middleware'), + * ]) + * ``` + * + * All of the actual work lives in `middleware.ts`; this class only resolves the + * federation instance and the configuration from the IoC container. + * + * @module + */ +import type { HttpContext } from "@adonisjs/core/http"; +import type { NextFn } from "@adonisjs/core/types/http"; + +import { fedifyMiddleware } from "./middleware.ts"; +import type { ContextData } from "./types.ts"; + +export default class FedifyMiddleware { + /** + * Nothing is cached here on purpose. + * + * AdonisJS constructs a fresh middleware instance for every request, so an + * instance field would never be read twice, and `ContainerResolver` does not + * expose the application it belongs to, so a keyed module-level cache cannot + * be built safely either. It does not matter: both bindings are memoised by + * the service provider, so resolving them is a map lookup, and + * `fedifyMiddleware` only closes over its three arguments. + * + * The request-scoped resolver is used rather than the application container + * so that resolution participates in whatever request-scoped bindings the + * application has registered. Both reach the same singletons. + */ + async handle(ctx: HttpContext, next: NextFn): Promise { + const federation = await ctx.containerResolver.make("fedify"); + const config = await ctx.containerResolver.make("fedify.config"); + + const Middleware = fedifyMiddleware( + federation, + config.contextDataFactory, + { + ignoreRoutePrefixes: config.ignoreRoutePrefixes, + }, + ); + + return new Middleware().handle(ctx, next); + } +} diff --git a/packages/adonisjs/src/logging.test.ts b/packages/adonisjs/src/logging.test.ts new file mode 100644 index 000000000..2055ab0ff --- /dev/null +++ b/packages/adonisjs/src/logging.test.ts @@ -0,0 +1,76 @@ +/** + * Tests for the LogTape → AdonisJS logger bridge. + * + * LogTape's configuration is process-global, so every test resets it again + * afterwards. Node's test runner gives each test file its own process, which + * keeps that global out of the other suites. + * + * @module + */ +import { match, strictEqual } from "node:assert/strict"; +import { afterEach, describe, it } from "node:test"; + +import type { Logger } from "@adonisjs/core/logger"; +import { configure, getLogger, reset } from "@logtape/logtape"; + +import { configureFedifyLogging } from "./logging.ts"; + +/** + * A stand-in for the AdonisJS (Pino) logger that records what it was told. + */ +function createLogger(): { logger: Logger; records: [string, string][] } { + const records: [string, string][] = []; + const record = (level: string) => (message: string) => { + records.push([level, message]); + }; + + const logger = { + trace: record("trace"), + debug: record("debug"), + info: record("info"), + warn: record("warn"), + error: record("error"), + fatal: record("fatal"), + } as unknown as Logger; + + return { logger, records }; +} + +describe("configureFedifyLogging", () => { + afterEach(async () => { + await reset(); + }); + + it("routes Fedify's records into the AdonisJS logger", async () => { + const { logger, records } = createLogger(); + await configureFedifyLogging({ logger }); + + getLogger(["fedify", "federation"]).warn("Delivery failed."); + + // The first record is LogTape's own meta logger announcing itself, which + // the default categories deliberately route here too. + const delivery = records.find(([, message]) => + message.includes("Delivery failed.") + ); + strictEqual(delivery !== undefined, true); + // LogTape's `warning` maps onto Pino's `warn`. + strictEqual(delivery![0], "warn"); + // The timestamp and the level are stripped; the AdonisJS logger adds both, + // so the rendered message starts at the category. + match(delivery![1], /^fedify.federation: Delivery failed\.$/); + }); + + it("leaves an existing LogTape configuration alone", async () => { + await configure({ sinks: {}, loggers: [] }); + + const { logger, records } = createLogger(); + // Without the guard `configure()` would throw ConfigError("Already + // configured; ..."), which would take the whole application down during + // the provider's boot() over a logging bridge. + await configureFedifyLogging({ logger }); + + strictEqual(records.length, 1); + strictEqual(records[0]![0], "debug"); + match(records[0]![1], /already configured/i); + }); +}); diff --git a/packages/adonisjs/src/logging.ts b/packages/adonisjs/src/logging.ts new file mode 100644 index 000000000..dfb0ad2f8 --- /dev/null +++ b/packages/adonisjs/src/logging.ts @@ -0,0 +1,130 @@ +/** + * Bridges Fedify's [LogTape] logs into the AdonisJS logger. + * + * Fedify reports a great deal of operational detail through LogTape: HTTP + * Signature verification results, delivery attempts and retries, WebFinger + * lookups, JSON-LD document loading. None of it reaches the terminal unless + * LogTape is configured, and "my inbox silently drops everything" is a + * miserable thing to debug without those logs. + * + * This module wires LogTape's records into the application's Pino logger, so + * Fedify's output lands in the same stream, with the same formatting and the + * same log level filtering, as everything else the application logs. + * + * [LogTape]: https://logtape.org/ + * + * @module + */ +import type { Logger } from "@adonisjs/core/logger"; +import { + configure, + getConfig, + getTextFormatter, + type LogLevel, + type LogRecord, + type TextFormatter, +} from "@logtape/logtape"; + +/** + * Options for {@link configureFedifyLogging}. + */ +export interface FedifyLoggingOptions { + /** + * The AdonisJS logger to forward records to. + */ + logger: Logger; + + /** + * The LogTape categories to route into the AdonisJS logger. + * + * The default covers Fedify's own logs plus LogTape's meta category, which is + * where LogTape reports its own misconfiguration. + * + * @default `[['logtape', 'meta'], ['fedify']]` + */ + categories?: (string | string[])[]; +} + +/** + * Maps a LogTape level onto the matching Pino method. + * + * The only mismatch is LogTape's `warning` versus Pino's `warn`. + */ +function logWith( + logger: Logger, + level: LogLevel, +): ((message: string) => void) | undefined { + switch (level) { + case "trace": + return logger.trace.bind(logger); + case "debug": + return logger.debug.bind(logger); + case "info": + return logger.info.bind(logger); + case "warning": + return logger.warn.bind(logger); + case "error": + return logger.error.bind(logger); + case "fatal": + return logger.fatal.bind(logger); + default: + return undefined; + } +} + +/** + * Configures LogTape so that Fedify's logs flow into the AdonisJS logger. + * + * LogTape is a process-global singleton, and `configure()` refuses to run twice: + * it throws `ConfigError("Already configured; ...")` unless the `reset` flag is + * set. Two ordinary situations reach a second call — an application that + * configures LogTape itself but forgets `logging: false` in `config/fedify.ts`, + * and a test process that boots more than one application instance — and in + * both of them the existing configuration is the one to keep. So the existing + * configuration wins and this function returns after saying so through the + * logger, rather than taking the application down over a logging bridge. + * + * The service provider calls it during `boot()` unless the application sets + * `logging: false` in `config/fedify.ts`, which is the escape hatch for + * applications that configure LogTape themselves. + * + * Timestamps and levels are stripped from the rendered message because the + * AdonisJS logger adds both; leaving them in produces doubled-up output. + */ +export async function configureFedifyLogging( + options: FedifyLoggingOptions, +): Promise { + const { logger } = options; + const categories = options.categories ?? [["logtape", "meta"], ["fedify"]]; + + if (getConfig() != null) { + logger.debug( + "LogTape is already configured, so the Fedify logging bridge was left " + + 'alone. Set "logging" to false in config/fedify.ts to make that ' + + "explicit.", + ); + return; + } + + const formatter: TextFormatter = getTextFormatter({ + timestamp: "disabled", + level: () => "", + }); + + await configure({ + sinks: { + adonisjs: (record: LogRecord) => { + const write = logWith(logger, record.level); + /* c8 ignore next 3 -- defensive: LogTape may add levels in a future release */ + if (write === undefined) { + logger.info(formatter(record)); + return; + } + // The formatter still emits an empty `[]` where the level would go. + write(formatter(record).replace(/^\[\]\s+/, "").trimEnd()); + }, + }, + filters: {}, + loggers: categories.map((category) => ({ category, sinks: ["adonisjs"] })), + }); +} diff --git a/packages/adonisjs/src/middleware.test.ts b/packages/adonisjs/src/middleware.test.ts new file mode 100644 index 000000000..fcac4a3a9 --- /dev/null +++ b/packages/adonisjs/src/middleware.test.ts @@ -0,0 +1,588 @@ +/** + * Specification-compliance tests for the AdonisJS ↔ Fedify middleware. + * + * The first four scenarios are the ones every Fedify integration package is + * required to cover. The rest guard the AdonisJS-specific decisions this + * package makes — chiefly the use of `ctx.route` to detect whether the + * framework matched a route, and the lazy `ctx.federation` accessor. + * + * @module + */ +import { + deepStrictEqual, + match, + notStrictEqual, + ok, + strictEqual, +} from "node:assert/strict"; +import { after, describe, it } from "node:test"; + +import type { HttpContext } from "@adonisjs/core/http"; +import type { NextFn } from "@adonisjs/core/types/http"; +import type { Federation } from "@fedify/fedify"; + +import { fedifyMiddleware } from "./middleware.ts"; +import { + ACTIVITY_PUB_ACCEPT, + BROWSER_ACCEPT, + createTestFederation, + fetchRaw, + startTestServer, + type TestServer, +} from "./test_utils.ts"; + +/** + * Boots one server per test and tears it down afterwards. + */ +async function withServer( + options: + & Omit< + Parameters>[0], + "federation" | "contextDataFactory" + > + & { + /** Defaults to {@link createTestFederation}. */ + federation?: Federation; + contextDataFactory?: () => null; + }, +): Promise { + const server = await startTestServer({ + federation: options.federation ?? createTestFederation(), + contextDataFactory: options.contextDataFactory ?? (() => null), + ignoreRoutePrefixes: options.ignoreRoutePrefixes, + defineRoutes: options.defineRoutes, + afterFedify: options.afterFedify, + }); + after(() => server.close()); + return server; +} + +describe("Fedify middleware", () => { + it("serves a federation request as JSON-LD", async () => { + const server = await withServer({}); + + const response = await server.fetch("/users/alice", { + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + + strictEqual(response.status, 200); + match( + response.headers.get("content-type") ?? "", + /activity\+json|ld\+json/, + ); + + const document = (await response.json()) as Record; + strictEqual(document.type, "Person"); + strictEqual(document.preferredUsername, "alice"); + strictEqual(document.id, `${server.url}/users/alice`); + }); + + it("serves WebFinger without any AdonisJS route", async () => { + const server = await withServer({}); + + const response = await server.fetch( + `/.well-known/webfinger?resource=${ + encodeURIComponent(`${server.url}/users/alice`) + }`, + ); + + strictEqual(response.status, 200); + const document = (await response.json()) as { subject: string }; + strictEqual(document.subject, `${server.url}/users/alice`); + }); + + it("delegates non-federation requests to AdonisJS", async () => { + const server = await withServer({ + defineRoutes(router) { + router.get( + "/hello", + ({ response }) => response.send("hello from adonis"), + ); + }, + }); + + const response = await server.fetch("/hello"); + + strictEqual(response.status, 200); + strictEqual(await response.text(), "hello from adonis"); + }); + + it("returns 406 when the client does not accept ActivityPub and no route matches", async () => { + const server = await withServer({}); + + const response = await server.fetch("/users/alice", { + headers: { Accept: BROWSER_ACCEPT }, + }); + + strictEqual(response.status, 406); + strictEqual(await response.text(), "Not acceptable"); + match(response.headers.get("content-type") ?? "", /text\/plain/); + strictEqual(response.headers.get("vary"), "Accept"); + }); + + it("lets AdonisJS serve a shared URL when the client prefers HTML", async () => { + const server = await withServer({ + defineRoutes(router) { + router.get( + "/users/:identifier", + ({ params, response }) => + response.header("content-type", "text/html").send( + `

${params.identifier}

`, + ), + ); + }, + }); + + const html = await server.fetch("/users/alice", { + headers: { Accept: BROWSER_ACCEPT }, + }); + strictEqual(html.status, 200); + strictEqual(await html.text(), "

alice

"); + strictEqual(html.headers.get("vary"), "Accept"); + + // The very same URL still answers with the actor document for servers. + const jsonLd = await server.fetch("/users/alice", { + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + strictEqual(jsonLd.status, 200); + strictEqual(((await jsonLd.json()) as { type: string }).type, "Person"); + }); + + it("keeps the AdonisJS 404 for paths neither side knows", async () => { + const server = await withServer({ + defineRoutes(router) { + router.get( + "/hello", + ({ response }) => response.send("hello from adonis"), + ); + }, + }); + + const response = await server.fetch("/nowhere", { + headers: { Accept: BROWSER_ACCEPT }, + }); + + strictEqual(response.status, 404); + // Not Fedify's "Not found" body: AdonisJS rendered this one. + notStrictEqual(await response.text(), "Not found"); + }); + + it("answers 406 rather than 404 when a shared route exists but the identifier does not", async () => { + // Fedify knows the /users/{identifier} template, so it reports "not + // acceptable" (not "not found") for a browser request. AdonisJS has no + // route at all here, so the deferred 406 must win. + const server = await withServer({}); + + const response = await server.fetch("/users/bob", { + headers: { Accept: BROWSER_ACCEPT }, + }); + + strictEqual(response.status, 406); + }); + + it("keeps Accept in the 406's Vary when downstream middleware replaces it", async () => { + // A later middleware can replace `Vary` rather than append to it; a 406 + // cached without `Accept` could then reach an ActivityPub client. + const server = await withServer({ + afterFedify: { + async handle(ctx: HttpContext, next: NextFn) { + ctx.response.header("Vary", "Origin"); + await next(); + }, + }, + }); + + const response = await server.fetch("/users/alice", { + headers: { Accept: BROWSER_ACCEPT }, + }); + + strictEqual(response.status, 406); + strictEqual(response.headers.get("vary"), "Origin, Accept"); + }); + + describe("ctx.federation", () => { + it("is available inside AdonisJS route handlers", async () => { + const server = await withServer({ + defineRoutes(router) { + router.get("/whoami/:identifier", (ctx: HttpContext) => { + return ctx.response.json({ + actor: ctx.federation.getActorUri(ctx.params.identifier).href, + origin: ctx.federation.origin, + }); + }); + }, + }); + + const response = await server.fetch("/whoami/alice"); + + strictEqual(response.status, 200); + deepStrictEqual(await response.json(), { + actor: `${server.url}/users/alice`, + origin: server.url, + }); + }); + + it("receives the value produced by the context data factory", async () => { + let seen: unknown = "not called"; + const federation = createTestFederation(); + const server = await startTestServer({ + federation, + contextDataFactory: (ctx) => { + seen = ctx.request.url(); + return null; + }, + defineRoutes(router) { + router.get( + "/ping", + (ctx: HttpContext) => + ctx.response.send(String(ctx.federation.data)), + ); + }, + }); + after(() => server.close()); + + const response = await server.fetch("/ping"); + + strictEqual(await response.text(), "null"); + strictEqual(seen, "/ping"); + }); + + it("throws on routes excluded via ignoreRoutePrefixes", async () => { + const server = await withServer({ + ignoreRoutePrefixes: ["/assets/"], + defineRoutes(router) { + router.get("/assets/app.css", (ctx: HttpContext) => { + try { + void ctx.federation; + return ctx.response.send("no error"); + } catch (error) { + return ctx.response.send( + (error as { code?: string }).code ?? "unknown", + ); + } + }); + }, + }); + + const response = await server.fetch("/assets/app.css"); + + strictEqual(await response.text(), "E_FEDIFY_CONTEXT_UNAVAILABLE"); + }); + + it("matches ignoreRoutePrefixes against the raw request target", async () => { + // `ctx.request.url()` decodes the parsed path, so `/%61ssets/app.css` + // would read as `/assets/app.css` and bypass Fedify -- while + // `toFetchRequest` hands Fedify the encoded form, which is what its + // router matches. Both decisions have to read the same value, so the + // prefix is compared against the raw target and this request is *not* + // excluded. + const server = await withServer({ + ignoreRoutePrefixes: ["/assets/"], + defineRoutes(router) { + router.get("/assets/app.css", (ctx: HttpContext) => { + try { + void ctx.federation; + return ctx.response.send("context available"); + } catch (error) { + return ctx.response.send( + (error as { code?: string }).code ?? "unknown", + ); + } + }); + }, + }); + + const response = await server.fetch("/%61ssets/app.css"); + + strictEqual(await response.text(), "context available"); + }); + + it("matches ignoreRoutePrefixes against an absolute-form target", async () => { + // RFC 9112 lets a client write the whole URL as the request target + // (`GET http://example.com/users/alice HTTP/1.1`), and Node reports it in + // `req.url` exactly as it arrived. Fedify is handed a parsed URL either + // way, so comparing the prefix against the unparsed target would leave the + // opt-out silently inapplicable to such a request -- and Fedify would + // answer for a path the application excluded. + const server = await withServer({ ignoreRoutePrefixes: ["/users/"] }); + + const { status } = await fetchRaw( + server, + `${server.url}/users/alice`, + { headers: { Accept: ACTIVITY_PUB_ACCEPT } }, + ); + + // AdonisJS does not normalise the absolute form for its own router + // either, so nothing matches and the request ends as a 404 -- which is + // the point: Fedify did not serve the excluded path. + strictEqual(status, 404); + }); + + it("does not intercept federation routes below an ignored prefix", async () => { + // A prefix that shadows a Fedify route must genuinely bypass Fedify; + // this documents the footgun rather than papering over it. + const server = await withServer({ ignoreRoutePrefixes: ["/users/"] }); + + const response = await server.fetch("/users/alice", { + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + + strictEqual(response.status, 404); + }); + }); + + it("works with no context data factory at all", async () => { + // Fedify's integration guide asks for the second argument to be optional, + // defaulting to a factory that produces `undefined`. This is the whole + // API surface an application gets if it never touches the container. + const server = await startTestServer({ + federation: createTestFederation(), + defineRoutes(router) { + router.get( + "/data", + (ctx: HttpContext) => ctx.response.send(String(ctx.federation.data)), + ); + }, + }); + after(() => server.close()); + + const actor = await server.fetch("/users/alice", { + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + strictEqual(actor.status, 200); + + const data = await server.fetch("/data"); + strictEqual(await data.text(), "undefined"); + }); + + it("returns a class that server.use() can construct", () => { + // The README's container-free example passes the return value straight to + // `server.use()`, which resolves each entry through the IoC container and + // constructs it. A plain object fails there with `Cannot construct value`, + // so the return type has to be the class itself. + const Middleware = fedifyMiddleware(createTestFederation()); + + strictEqual(typeof Middleware, "function"); + strictEqual(typeof new Middleware().handle, "function"); + }); + + describe("regressions", () => { + it("does not stall a large body destined for an AdonisJS route", async () => { + // Converting the raw stream eagerly used to attach a reader that nobody + // ever drained; past the adapter's ~128 KiB high-water mark it paused the + // socket for good and any flowing-mode consumer downstream hung forever. + const payload = "x".repeat(300 * 1024); + + const server = await withServer({ + defineRoutes(router) { + router.post("/upload", async (ctx: HttpContext) => { + const size = await new Promise((resolve, reject) => { + let bytes = 0; + // Flowing mode, which is what @adonisjs/core/bodyparser_middleware + // uses through raw-body. + ctx.request.request.on("data", (chunk: Buffer) => { + bytes += chunk.length; + }); + ctx.request.request.on("end", () => resolve(bytes)); + ctx.request.request.on("error", reject); + }); + return ctx.response.send(String(size)); + }); + }, + }); + + const response = await server.fetch("/upload", { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: payload, + signal: AbortSignal.timeout(15_000), + }); + + strictEqual(await response.text(), String(payload.length)); + }); + + it("does not overwrite a downstream response that matched no route", async () => { + // The deferred 406 may only replace the framework's own 404. A server + // middleware registered after this one -- @adonisjs/cors answering a + // preflight is the canonical case -- answers without the router running, + // so ctx.route stays undefined even though the response is deliberate. + const server = await withServer({ + afterFedify: { + handle(ctx: HttpContext) { + ctx.response.status(204).header("x-answered-by", "downstream").send( + "", + ); + }, + }, + }); + + const response = await server.fetch("/users/alice", { + headers: { Accept: BROWSER_ACCEPT }, + }); + + strictEqual(response.status, 204); + strictEqual(response.headers.get("x-answered-by"), "downstream"); + }); + + it("ignores method spoofing when deciding what Fedify sees", async () => { + // `_method` is an AdonisJS form-submission convenience. Letting it reach + // the bridge would turn POST /inbox?_method=GET into a bodyless GET and + // route the activity away from the inbox. + const server = await withServer({}); + + const response = await server.fetch("/users/alice/inbox?_method=GET", { + method: "POST", + headers: { "Content-Type": "application/activity+json" }, + body: JSON.stringify({ type: "Follow" }), + }); + + // Fedify handled it as the POST it really is (and rejected it as + // unsigned) rather than 404ing on a spoofed GET. + ok( + response.status >= 400 && response.status < 500, + `unexpected status ${response.status}`, + ); + }); + + it("gives Fedify the raw, still-encoded request target", async () => { + // AdonisJS decodes the parsed path, which would hand Fedify + // `/users/alice!` for a target of `/users/alice%21` -- a path Fedify's + // own URI templates would then fail to match. + const server = await withServer({}); + + const encoded = await server.fetch("/users/alice%21", { + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + + // `alice!` is not a known identifier, so Fedify answers 404 -- but only + // because it saw the percent-encoded target and matched its template. + strictEqual(encoded.status, 404); + + // And the ordinary identifier still resolves. + const plain = await server.fetch("/users/alice", { + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + strictEqual(plain.status, 200); + }); + }); + + describe("request and response bridging", () => { + it("leaves the request body intact for AdonisJS routes", async () => { + const server = await withServer({ + defineRoutes(router) { + router.post("/echo", async (ctx: HttpContext) => { + const chunks: Buffer[] = []; + for await (const chunk of ctx.request.request) { + chunks.push(chunk as Buffer); + } + return ctx.response.send(Buffer.concat(chunks).toString("utf8")); + }); + }, + }); + + const response = await server.fetch("/echo", { + method: "POST", + headers: { "Content-Type": "text/plain" }, + body: "a federated payload", + }); + + strictEqual(response.status, 200); + strictEqual(await response.text(), "a federated payload"); + }); + + it("accepts an inbox POST and answers without a body", async () => { + const server = await withServer({}); + + const response = await server.fetch("/users/alice/inbox", { + method: "POST", + headers: { "Content-Type": "application/activity+json" }, + body: JSON.stringify({ type: "Follow" }), + }); + + // Unsigned activities are rejected, but the point is that Fedify — not + // AdonisJS — answered, and that the streamed body survived the bridge. + ok( + response.status >= 400 && response.status < 500, + `unexpected status ${response.status}`, + ); + }); + + it("answers HEAD requests with headers but no body", async () => { + const server = await withServer({}); + + const response = await server.fetch("/users/alice", { + method: "HEAD", + headers: { Accept: ACTIVITY_PUB_ACCEPT }, + }); + + strictEqual(response.status, 200); + strictEqual(await response.text(), ""); + }); + + it("delegates TRACE, which no Request can be built from", async () => { + // Building a `Request` from a forbidden method throws before `next()` + // runs, turning a request AdonisJS can answer into a 500. + const server = await withServer({ + defineRoutes(router) { + router.route( + "/ping", + ["TRACE"], + (ctx: HttpContext) => ctx.response.send("route ran"), + ); + }, + }); + + const { status, raw } = await fetchRaw(server, "/ping", { + method: "TRACE", + }); + + strictEqual(status, 200); + ok(raw.endsWith("route ran")); + }); + + it("preserves multiple Set-Cookie headers from a Fedify response", async () => { + // Guards the response writer's use of getSetCookie(): iterating Headers + // would fold these into one comma-joined value. Fedify has to be the one + // answering, because a delegated response never reaches the writer -- so + // this stands in for a federation whose handler sets cookies. + const headers = new Headers({ "content-type": "text/plain" }); + headers.append("set-cookie", "a=1; Path=/"); + headers.append("set-cookie", "b=2; Path=/"); + + const server = await withServer({ + federation: { + fetch: () => Promise.resolve(new Response("ok", { headers })), + } as unknown as Federation, + }); + + const response = await server.fetch("/anywhere"); + + deepStrictEqual(response.headers.getSetCookie(), [ + "a=1; Path=/", + "b=2; Path=/", + ]); + strictEqual(await response.text(), "ok"); + }); + + it("leaves Set-Cookie headers from a delegated AdonisJS route alone", async () => { + // The counterpart of the test above: Fedify declines, AdonisJS answers, + // and the middleware must not touch the response it produced. + const server = await withServer({ + defineRoutes(router) { + router.get("/cookies", (ctx: HttpContext) => { + ctx.response.append("set-cookie", "a=1; Path=/"); + ctx.response.append("set-cookie", "b=2; Path=/"); + return ctx.response.send("ok"); + }); + }, + }); + + const response = await server.fetch("/cookies"); + + deepStrictEqual(response.headers.getSetCookie(), [ + "a=1; Path=/", + "b=2; Path=/", + ]); + }); + }); +}); diff --git a/packages/adonisjs/src/middleware.ts b/packages/adonisjs/src/middleware.ts new file mode 100644 index 000000000..51a6ce76d --- /dev/null +++ b/packages/adonisjs/src/middleware.ts @@ -0,0 +1,455 @@ +/** + * The framework-agnostic core of the AdonisJS ↔ Fedify bridge. + * + * Everything in this module works with a plain `Federation` instance and an + * `HttpContext`. It knows nothing about the IoC container, `config/fedify.ts`, + * or the service provider, which makes it directly usable by applications that + * would rather wire Fedify up by hand (see {@link fedifyMiddleware}) and + * makes it testable without booting a full AdonisJS application. + * + * The Adonis-native layer in `fedify_middleware.ts` is a thin wrapper that + * resolves the arguments from the container and delegates here. + * + * @module + */ +import { Readable } from "node:stream"; + +import type { HttpContext } from "@adonisjs/core/http"; +import type { NextFn } from "@adonisjs/core/types/http"; +import type { Federation, RequestContext } from "@fedify/fedify"; + +import { E_FEDIFY_CONTEXT_UNAVAILABLE } from "./errors.ts"; +import type { ContextDataFactory } from "./types.ts"; + +/** + * Options accepted by {@link fedifyMiddleware}. + */ +export interface FedifyMiddlewareOptions { + /** + * URL path prefixes to bypass entirely. See + * {@link import('./types.js').FedifyConfig.ignoreRoutePrefixes}. + * + * @default `[]` + */ + ignoreRoutePrefixes?: string[]; +} + +/** + * An instance of the middleware {@link fedifyMiddleware} builds. + */ +export interface FedifyMiddlewareHandler { + handle(ctx: HttpContext, next: NextFn): Promise; +} + +/** + * What {@link fedifyMiddleware} returns: a middleware **class**. + * + * AdonisJS's `server.use()` resolves each entry through the IoC container and + * constructs it, so a plain object is rejected at runtime with `Cannot + * construct value`. Returning the class means the result can be handed to + * `server.use()` as-is. + */ +export type FedifyMiddlewareClass = new () => FedifyMiddlewareHandler; + +/** + * Methods `new Request()` refuses to construct. Node rejects `TRACK` itself + * and never emits `request` for `CONNECT`, so only `TRACE` arrives here. + */ +const FORBIDDEN_METHODS = new Set(["CONNECT", "TRACE", "TRACK"]); + +/** + * Wraps the raw `IncomingMessage` in a `ReadableStream` that stays completely + * inert until somebody actually reads from it. + * + * This laziness is load-bearing, not an optimisation. Fedify only reads the + * body of requests it handles — an inbox `POST`, essentially — but the `Request` + * has to be constructed *before* Fedify decides, so on every other `POST` the + * body is handed over and then abandoned. + * + * Calling `Readable.toWeb()` eagerly would be fatal there. Node's adapter + * attaches its own `data` listener to the `IncomingMessage` immediately and + * starts filling an internal queue; once that queue passes its high-water mark + * (~128 KiB) with nobody reading, it calls `pause()` on the socket and never + * resumes. A downstream reader in flowing mode — which is exactly what + * `@adonisjs/core/bodyparser_middleware` uses — would then receive the first + * chunks and nothing more, and the request would hang until the client gave up. + * + * With `highWaterMark: 0` the underlying source is not even created until the + * first `pull()`, so a request Fedify declines reaches the AdonisJS body parser + * with its stream untouched. + */ +function lazyRequestBody( + message: HttpContext["request"]["request"], +): ReadableStream { + let reader: ReadableStreamDefaultReader | undefined; + + return new ReadableStream( + { + async pull(controller) { + reader ??= (Readable.toWeb(message) as ReadableStream) + .getReader(); + + const { done, value } = await reader.read(); + if (done) controller.close(); + else controller.enqueue(value); + }, + cancel(reason) { + return reader?.cancel(reason); + }, + }, + { highWaterMark: 0 }, + ); +} + +/** + * Builds the URL Fedify sees from the raw request target. + * + * `completeUrl()` would `decodeURI()` the path, but Fedify's URI templates + * percent-encode sub-delims, so `/actors/alice%21` has to reach it encoded. + * The raw target also keeps the bytes a peer signed, and resolving it against + * an origin normalises the absolute-form target RFC 9112 permits + * (`GET http://host/inbox HTTP/1.1`). + */ +function toRequestUrl(request: HttpContext["request"]): URL { + // Fedify rewrites the origin to `FederationOptions.origin`, so this only has + // to be well-formed. + const authority = request.host() ?? request.hostname() ?? "localhost"; + return new URL( + request.request.url ?? "/", + `${request.protocol()}://${authority}`, + ); +} + +/** + * Converts an AdonisJS request into a WHATWG `Request` that Fedify can consume. + * + * A few details matter here: + * + * - The URL comes from {@link toRequestUrl}, which builds it from the raw + * request target. + * - `intended()`, not `method()`. `method()` honours AdonisJS's `_method` form + * spoofing, which is a convenience for HTML forms and must never reach a + * protocol bridge: `POST /inbox?_method=GET` would otherwise arrive at Fedify + * as a bodyless `GET` and bypass inbox handling entirely. + * - Header values can be arrays (Node collapses most duplicates, but not all), + * so each element is appended rather than set. + * - `duplex: 'half'` is required by Node whenever a `Request` is constructed + * with a streaming body. + */ +function toFetchRequest(ctx: HttpContext): Request { + const request = ctx.request; + const method = request.intended().toUpperCase(); + + const headers = new Headers(); + for (const [key, value] of Object.entries(request.headers())) { + if (Array.isArray(value)) { + for (const item of value) headers.append(key, item); + } else if (typeof value === "string") { + headers.append(key, value); + } + } + + const url = toRequestUrl(request); + + const hasBody = method !== "GET" && method !== "HEAD"; + + return new Request(url, { + method, + headers, + body: hasBody ? lazyRequestBody(request.request) : undefined, + // `duplex` is not part of the DOM lib's `RequestInit`, but Node requires it + // for streaming bodies. + ...{ duplex: "half" }, + }); +} + +/** + * Copies a WHATWG `Response` produced by Fedify onto the AdonisJS response. + * + * Notable cases: + * + * - `Set-Cookie` must be read through `getSetCookie()`. Iterating `Headers` + * folds repeated headers into a single comma-joined value, which is valid for + * most headers but corrupts cookies. + * - Responses that carry no body (`204`, `304`, and any reply to a `HEAD` + * request) must not be streamed. AdonisJS finalises such a response by + * flushing the status line and headers, which is exactly what is wanted. + * - The body is a web `ReadableStream`; `response.stream()` accepts one + * directly since `@adonisjs/http-server` v9 and takes care of backpressure + * and cleanup. The error callback keeps a mid-stream failure from hanging + * the socket. + */ +function writeFedifyResponse(response: Response, ctx: HttpContext): void { + ctx.response.status(response.status); + + for (const [key, value] of response.headers) { + if (key.toLowerCase() === "set-cookie") continue; + ctx.response.header(key, value); + } + + for (const cookie of response.headers.getSetCookie()) { + ctx.response.append("set-cookie", cookie); + } + + const bodyless = response.body === null || + response.status === 204 || + response.status === 304 || + // `intended()` rather than `method()`: a spoofed `_method=HEAD` must not + // suppress the body of a response the client is expecting in full. + ctx.request.intended().toUpperCase() === "HEAD"; + + if (bodyless) { + // Discard the body so the underlying stream is not left dangling when + // Fedify produced one anyway (a HEAD request being the usual case). + void response.body?.cancel(); + return; + } + + ctx.response.stream(response.body!, (error) => { + ctx.logger.error({ err: error }, "Failed to stream the Fedify response"); + return ["Internal server error", 500]; + }); +} + +/** + * Installs the lazy `ctx.federation` accessor. + * + * The property is declared as non-optional on `HttpContext`, so it must be + * present on every request the middleware touches — including the ones Fedify + * declines to handle, because a controller serving the HTML half of a shared + * URL is precisely where `ctx.federation` is most useful. + * + * Creating a `RequestContext` eagerly would mean doing that work on every + * single request, including the ones that never look at it. A getter defers it + * to first access and memoises the result. + */ +function defineFederationContext( + ctx: HttpContext, + create: () => RequestContext, +): void { + let cached: RequestContext | undefined; + let created = false; + + Object.defineProperty(ctx, "federation", { + configurable: true, + enumerable: false, + get() { + if (!created) { + cached = create(); + created = true; + } + return cached; + }, + }); +} + +/** + * Creates the middleware that mounts a `Federation` instance inside an AdonisJS + * application. + * + * This is the main integration function of the package, and the counterpart of + * `integrateFederation()` in `@fedify/express` and `federation()` in + * `@fedify/hono`. It is named `fedifyMiddleware` and exported as the default + * export because AdonisJS's own documentation calls these things middleware, + * which is the naming convention Fedify's integration guide asks for. + * + * Register the returned object in the **server** middleware stack, not the + * router stack. Fedify serves paths that the AdonisJS router knows nothing + * about (`/.well-known/webfinger`, `/.well-known/nodeinfo`, actor and inbox + * endpoints), so the middleware has to run before route matching. Running in + * the server stack also means the request body stream is still intact, which + * inbox signature verification depends on. + * + * Most applications never call this directly: `node ace configure` registers + * `@fedify/adonisjs/middleware`, which resolves the federation and the + * configuration from the IoC container and delegates here. Reach for it when + * you would rather build the `Federation` object yourself. + * + * @example + * ```ts + * // start/kernel.ts + * import fedifyMiddleware from '@fedify/adonisjs' + * import federation from '#federation/main' + * + * server.use([async () => ({ default: fedifyMiddleware(federation) })]) + * ``` + * + * @returns A middleware class, which is what `server.use()` constructs. + * + * @param federation The federation instance to mount. + * @param contextDataFactory Produces the per-request context data. Defaults to + * producing `undefined`, which is what a federation whose `TContextData` is + * `void` wants; the AdonisJS-native layer passes the factory resolved from + * `config/fedify.ts` instead. + * @param options Additional behaviour toggles. + */ +export function fedifyMiddleware( + federation: Federation, + contextDataFactory: ContextDataFactory = () => + void 0 as TContextData, + options: FedifyMiddlewareOptions = {}, +): FedifyMiddlewareClass { + const ignoreRoutePrefixes = options.ignoreRoutePrefixes ?? []; + + const handler: FedifyMiddlewareHandler = { + async handle(ctx: HttpContext, next: NextFn): Promise { + // This check and Fedify have to read the same path, so both derive it + // through `toRequestUrl()` rather than from `ctx.request.url()`. + const path = toRequestUrl(ctx.request).pathname; + + if ( + ignoreRoutePrefixes.some((prefix) => path.startsWith(prefix)) || + FORBIDDEN_METHODS.has(ctx.request.intended().toUpperCase()) + ) { + // Opted out, or impossible to represent as a `Request`. Touching + // `ctx.federation` is then a programming error, but it is declared + // non-optional, so it still has to exist and say so. + defineFederationContext(ctx, () => { + throw new E_FEDIFY_CONTEXT_UNAVAILABLE(path); + }); + return next(); + } + + const request = toFetchRequest(ctx); + const contextData = await contextDataFactory(ctx); + + defineFederationContext( + ctx, + () => federation.createContext(request, contextData), + ); + + /** + * What happened while Fedify was deciding. Held in an object rather than + * two `let` bindings so that TypeScript does not narrow the values based + * on their initialisers — the callbacks below are the only writers, and + * the compiler cannot see that they run. + * + * - `delegated` records which of Fedify's fall-through callbacks fired, + * if any. Fedify calls at most one of them per request, and only when + * it has decided not to serve the request itself. + * - `routeMatched` records whether the AdonisJS router matched a route + * once control was handed to it. `ctx.route` is assigned by the router + * immediately before a matched route's handler runs, so after + * `await next()` it is set if and only if something matched. This is + * the AdonisJS analogue of the `req.route` check `@fedify/express` + * uses, and it is strictly more reliable than inspecting the response + * body: a matched route may legitimately answer with a redirect, a + * stream, or an empty `204`, none of which leave "content" behind. + * - `status` records what AdonisJS ended up answering, so that a + * deliberate downstream response is never mistaken for "nothing here". + */ + const outcome: { + delegated: "none" | "notFound" | "notAcceptable"; + routeMatched: boolean; + status: number; + } = { delegated: "none", routeMatched: false, status: 0 }; + + const response = await federation.fetch(request, { + contextData, + + /** + * Fedify has no route for this path: hand the request to AdonisJS. + * + * `await next()` runs the remainder of the server middleware stack and + * then the router. It resolves even when nothing matches, because the + * server's middleware runner routes `E_ROUTE_NOT_FOUND` through the + * application's exception handler, which renders a 404 into + * `ctx.response`. Either way AdonisJS has now produced the response, + * so the value returned here is discarded. + */ + onNotFound: async () => { + outcome.delegated = "notFound"; + await next(); + outcome.routeMatched = ctx.route !== undefined; + return new Response("Not found", { status: 404 }); + }, + + /** + * Fedify has a route for this path but cannot satisfy the `Accept` + * header — the classic case being a browser asking for HTML at an actor + * URL that Fedify serves as JSON-LD. + * + * Per the Fedify integration specification, the correct behaviour is + * *not* to answer 406 straight away. The framework gets a chance to + * serve its own representation first, and 406 is only the answer if the + * framework has nothing either. That is what makes a single URL able to + * serve an HTML profile page to browsers and an ActivityPub actor + * document to servers. + * + * `Vary: Accept` is set before delegating so that whatever AdonisJS + * returns is cached per `Accept` header rather than shared across + * content types. + */ + onNotAcceptable: async () => { + outcome.delegated = "notAcceptable"; + ctx.response.header("Vary", "Accept"); + await next(); + outcome.routeMatched = ctx.route !== undefined; + outcome.status = ctx.response.getStatus(); + + // The 406 is produced below, on the `delegated === "notAcceptable"` + // path, never here. The callback's return value can only ever be + // used when `delegated === "none"`, which is unreachable here — so + // this is a type-satisfying placeholder, not the real response. + return new Response("Not acceptable", { status: 406 }); + }, + }); + + /** + * Only replace what AdonisJS produced when it really produced nothing. + * + * "No route matched" alone is not enough evidence. A server middleware + * registered after this one can answer a request without the router ever + * running — `@adonisjs/cors` replying `204` to a preflight is the common + * case, and its own configure hook appends it after this middleware — and + * a downstream failure can render a 5xx the same way. Overwriting either + * with a 406 would be wrong. Requiring the status to still be the + * framework's own 404 keeps the specification behaviour while leaving any + * deliberate downstream response alone. + */ + if ( + outcome.delegated === "notAcceptable" && + !outcome.routeMatched && + outcome.status === 404 + ) { + // Downstream middleware may have replaced `Vary`, and a 406 cached + // without `Accept` could be served to an ActivityPub client. + const vary = String(ctx.response.getHeader("Vary") ?? "") + .split(",") + .map((value) => value.trim()) + .filter((value) => value !== ""); + if (!vary.some((v) => v === "*" || v.toLowerCase() === "accept")) { + vary.push("Accept"); + } + + ctx.response + .status(406) + .header("Vary", vary.join(", ")) + .type("text/plain") + .send("Not acceptable"); + return; + } + + if (outcome.delegated !== "none") { + // AdonisJS produced the response — either from a matched route, or as + // its own 404 page. Leave it alone. + return; + } + + writeFedifyResponse(response, ctx); + }, + }; + + return class FedifyMiddleware { + handle = handler.handle; + }; +} + +/** + * The main integration function, also available as the default export. + * + * `@fedify/fastify` does the same for `fedifyPlugin`: the default export is + * what Fedify's integration guide asks for, and the named one keeps + * `import { fedifyMiddleware }` working for anyone who prefers it. + */ +export default fedifyMiddleware; diff --git a/packages/adonisjs/src/provider.ts b/packages/adonisjs/src/provider.ts new file mode 100644 index 000000000..7fe00f0ec --- /dev/null +++ b/packages/adonisjs/src/provider.ts @@ -0,0 +1,198 @@ +/** + * The `@fedify/adonisjs` service provider. + * + * It owns the whole lifecycle of the `Federation` object: reading + * `config/fedify.ts`, exposing the builder, building the federation once every + * dispatcher has been registered, starting the task queue, and shutting it down + * again. + * + * @module + */ +import { configProvider } from "@adonisjs/core"; +import type { ApplicationService, ConfigProvider } from "@adonisjs/core/types"; +import type { Federation } from "@fedify/fedify"; + +import { builder } from "./builder.ts"; +import { E_FEDERATION_NOT_READY, E_INVALID_FEDIFY_CONFIG } from "./errors.ts"; +import { configureFedifyLogging } from "./logging.ts"; +import type { ContextData, ResolvedFedifyConfig } from "./types.ts"; + +export default class FedifyProvider { + /** + * Aborts the queue worker when the application shuts down. + */ + #queueController = new AbortController(); + + /** + * The `startQueue()` promise, which settles once every queue listener has + * stopped. + * + * `shutdown()` returns it so that AdonisJS waits for the workers instead of + * tearing the process down around them. Undefined whenever this process does + * not drain the queue. + */ + #queue?: Promise; + + /** + * Whether the builder may be sealed into a `Federation` object. + * + * Registration of dispatchers happens in preload files, which AdonisJS + * imports *after* every provider's `boot()` and `start()` have run. Building + * the federation before then would produce an object with no routes, and the + * only symptom would be 404s from every federation endpoint. Refusing to + * build early turns that into an error message that says what to do. + */ + #buildable = false; + + /** + * Memoised results for the two container bindings. + * + * They are held here rather than relying on `container.singleton()`, which + * memoises *rejections* as well as values: one early, swallowed resolution of + * `'fedify'` would cache `E_FEDERATION_NOT_READY` for the lifetime of the + * process, and `ready()` would then re-throw it and the application would + * never start. Caching only successful results keeps a transient guard + * transient. + */ + #config?: Promise; + #federation?: Promise>; + + protected app: ApplicationService; + + /** + * Written out longhand rather than as a TypeScript parameter property, which + * Node's built-in type stripping does not support — the test suite runs these + * sources directly through it. + */ + constructor(app: ApplicationService) { + this.app = app; + } + + register(): void { + /** + * The builder is bound as a value, not a singleton factory, because it is a + * module-level singleton that application code also imports directly. Both + * routes must reach the same object. + */ + this.app.container.bindValue("fedify.builder", builder); + + this.app.container.bind("fedify.config", () => { + this.#config ??= (async () => { + const provider = this.app.config.get< + ConfigProvider + >("fedify"); + const config = await configProvider.resolve( + this.app, + provider, + ); + + if (!config) { + throw new E_INVALID_FEDIFY_CONFIG(); + } + + return config; + })().catch((error: unknown) => { + this.#config = undefined; + throw error; + }); + + return this.#config; + }); + + this.app.container.bind("fedify", () => { + if (!this.#buildable) { + throw new E_FEDERATION_NOT_READY(); + } + + this.#federation ??= (async () => { + const config = await this.app.container.make("fedify.config"); + const federationBuilder = await this.app.container.make( + "fedify.builder", + ); + + return federationBuilder.build(config); + })().catch((error: unknown) => { + this.#federation = undefined; + throw error; + }); + + return this.#federation; + }); + } + + async boot(): Promise { + const config = await this.app.container.make("fedify.config"); + + if (config.logging !== false) { + const logger = await this.app.container.make("logger"); + await configureFedifyLogging({ + logger, + categories: config.logging === true + ? undefined + : config.logging.categories, + }); + } + } + + /** + * Runs after preload files have been imported, i.e. after every dispatcher + * has been registered on the builder. + */ + async ready(): Promise { + this.#buildable = true; + + // Build eagerly so that a misconfiguration surfaces at boot rather than on + // the first federation request. + const federation = await this.app.container.make("fedify"); + const config = await this.app.container.make("fedify.config"); + + /** + * Only the web process drains the queue. + * + * Ace commands, the REPL and the test runner all reach `ready()` too, and + * starting a worker there would have short-lived commands linger while they + * finish deliveries they happened to pick up, and would have several + * processes competing for the same queue during a test run. + */ + if ( + typeof config.queueContextData === "function" && + this.app.getEnvironment() === "web" + ) { + const logger = await this.app.container.make("logger"); + const contextData = await config.queueContextData(); + + // `startQueue` only settles when the queue stops, so it must not be + // awaited here or the application would never finish starting. The + // promise is kept for `shutdown()`, and the `catch` is part of what is + // kept so that awaiting it there can never reject. + this.#queue = federation + .startQueue(contextData, { signal: this.#queueController.signal }) + .catch((error: unknown) => { + if (this.#queueController.signal.aborted) return; + logger.error( + { err: error }, + "The Fedify task queue stopped unexpectedly", + ); + }); + } + } + + /** + * Stops the queue worker and waits for it. + * + * Aborting the controller only *asks* the listeners to stop; `startQueue()` + * settles once they actually have, which may be one in-flight delivery later. + * Returning that promise keeps the rest of the AdonisJS shutdown sequence — + * closing the database connection, most importantly — behind it instead of + * pulling the ground out from under a job that is still running. + * + * A process that does not drain the queue has nothing to wait for, and the + * return type stays a promise there too: AdonisJS invokes provider hooks + * through a diagnostics-channel tracer that warns when a hook returns a + * non-thenable. + */ + shutdown(): Promise { + this.#queueController.abort(); + return this.#queue ?? Promise.resolve(); + } +} diff --git a/packages/adonisjs/src/services/builder.ts b/packages/adonisjs/src/services/builder.ts new file mode 100644 index 000000000..c12a52dc9 --- /dev/null +++ b/packages/adonisjs/src/services/builder.ts @@ -0,0 +1,29 @@ +/** + * Service module exposing the Fedify {@link FederationBuilder}. + * + * This is the module application code imports to register dispatchers and inbox + * listeners: + * + * ```ts + * // app/federation/actors.ts + * import federation from '@fedify/adonisjs/services/builder' + * + * federation.setActorDispatcher('/actors/{identifier}', async (ctx, identifier) => { + * // ... + * }) + * ``` + * + * Registration must happen while the application boots — the generated + * `start/federation.ts` preload file is the intended place — because the + * builder is sealed into a `Federation` object once booting completes. + * + * @module + */ +import type { FederationBuilder } from "@fedify/fedify"; + +import { builder } from "../builder.ts"; +import type { ContextData } from "../types.ts"; + +const federation: FederationBuilder = builder; + +export default federation; diff --git a/packages/adonisjs/src/services/services.test.ts b/packages/adonisjs/src/services/services.test.ts new file mode 100644 index 000000000..6d9b6842d --- /dev/null +++ b/packages/adonisjs/src/services/services.test.ts @@ -0,0 +1,85 @@ +/** + * Tests for the builder service module and the documented way to reach the + * built `Federation` from outside an HTTP request. + * + * `@fedify/adonisjs/services/builder` is a public entry point that application + * code imports directly, so it deserves coverage of its own. There is no + * companion `services/federation` module on purpose: the built instance is + * resolved from the container (`app.container.make('fedify')`), which is what + * the second suite pins down. + * + * @module + */ +import { rejects, strictEqual } from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { AppFactory } from "@adonisjs/core/factories/app"; +import { setApp } from "@adonisjs/core/services/app"; +import type { ApplicationService } from "@adonisjs/core/types"; + +import { defineConfig } from "../define_config.ts"; +import { E_FEDERATION_NOT_READY } from "../errors.ts"; +import FedifyProvider from "../provider.ts"; + +/** + * Builds an application and publishes it as the global one, which is what the + * service modules resolve through — exactly as the Ignitor does at runtime. + */ +async function createGlobalApp(): Promise { + const app = new AppFactory().create( + new URL("../../", import.meta.url), + () => import("node:util"), + ) as ApplicationService; + + app.useConfig({ + fedify: defineConfig({ origin: "https://example.com", logging: false }), + }); + await app.init(); + await app.boot(); + + setApp(app); + return app; +} + +describe("services/builder", () => { + it("is the same object the container binds", async () => { + const app = await createGlobalApp(); + const provider = new FedifyProvider(app); + provider.register(); + + const { default: fromService } = await import("./builder.ts"); + const fromContainer = await app.container.make("fedify.builder"); + + // Dispatchers registered through the service module have to end up on the + // very builder the provider later seals into a Federation. + strictEqual(fromService, fromContainer); + strictEqual(typeof fromService.build, "function"); + }); +}); + +describe("resolving the federation outside a request", () => { + it("works through the container once the application is ready", async () => { + const app = await createGlobalApp(); + const provider = new FedifyProvider(app); + provider.register(); + await provider.boot(); + + // Before `ready()`, resolving the federation is an error rather than a + // silently empty object. + await rejects( + () => app.container.make("fedify"), + (error: Error) => error instanceof E_FEDERATION_NOT_READY, + ); + + await provider.ready(); + await app.start(async () => {}); + + // The documented pattern for Ace commands and queue workers. + const federation = await app.container.make("fedify"); + + strictEqual(typeof federation.fetch, "function"); + strictEqual(typeof federation.createContext, "function"); + + await provider.shutdown(); + }); +}); diff --git a/packages/adonisjs/src/test_utils.ts b/packages/adonisjs/src/test_utils.ts new file mode 100644 index 000000000..7ac5054f8 --- /dev/null +++ b/packages/adonisjs/src/test_utils.ts @@ -0,0 +1,277 @@ +/** + * Test helpers for the `@fedify/adonisjs` test suite. + * + * They boot a real AdonisJS HTTP server — real server middleware stack, real + * router, real exception handler — on an ephemeral port, with the Fedify + * middleware installed. Testing against the real pipeline rather than a mock + * matters here: the whole design rests on two behaviours of that pipeline + * (`ctx.route` being assigned only on a match, and `await next()` resolving + * even when nothing matches), and a mock would happily reproduce whatever the + * implementation assumed. + * + * This file is only ever imported by `*.test.ts` files and is excluded from the + * published package. + * + * @module + */ +import { createServer, type Server as NodeHttpServer } from "node:http"; +import { type AddressInfo, connect } from "node:net"; + +import { AppFactory } from "@adonisjs/core/factories/app"; +import { ServerFactory } from "@adonisjs/core/factories/http"; +import type { HttpContext } from "@adonisjs/core/http"; +import type { Router } from "@adonisjs/core/http"; +import type { MiddlewareAsClass, NextFn } from "@adonisjs/core/types/http"; +import { + createFederation, + type Federation, + MemoryKvStore, +} from "@fedify/fedify"; +import { Endpoints, Person } from "@fedify/vocab"; + +import { + fedifyMiddleware, + type FedifyMiddlewareHandler, +} from "./middleware.ts"; +import type { ContextDataFactory } from "./types.ts"; + +/** + * A minimal federation used by the tests: one actor, `alice`, plus an inbox so + * that the actor document is complete enough for Fedify to serve it. + */ +export function createTestFederation(): Federation { + const federation = createFederation({ + kv: new MemoryKvStore(), + // The tests never make outbound requests, but Fedify's SSRF guard would + // otherwise reject the loopback addresses used by the harness. + allowPrivateAddress: true, + }); + + federation + .setActorDispatcher("/users/{identifier}", (ctx, identifier) => { + if (identifier !== "alice") return null; + return new Person({ + id: ctx.getActorUri(identifier), + preferredUsername: identifier, + name: "Alice", + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), + }); + }) + .setKeyPairsDispatcher(() => []); + + federation.setInboxListeners("/users/{identifier}/inbox", "/inbox"); + + return federation; +} + +/** + * A running test server. + */ +export interface TestServer { + /** The base URL, for example `http://127.0.0.1:34567`. */ + url: string; + /** `fetch()` bound to {@link url}, so tests can pass bare paths. */ + fetch(path: string, init?: RequestInit): Promise; + close(): Promise; +} + +export interface TestServerOptions { + federation: Federation; + /** Omit it to exercise the default that `fedifyMiddleware` supplies. */ + contextDataFactory?: ContextDataFactory; + ignoreRoutePrefixes?: string[]; + /** Registers the AdonisJS routes that the Fedify middleware falls through to. */ + defineRoutes?: (router: Router) => void; + /** + * A server middleware registered *after* the Fedify one, standing in for + * something like `@adonisjs/cors` that answers a request itself without the + * router ever running. + */ + afterFedify?: FedifyMiddlewareHandler | { + handle(ctx: HttpContext): Promise | void; + }; +} + +/** + * A server middleware the tests register *after* the Fedify one. + * + * `next` is part of the signature but a stub that answers the request itself may + * omit it: a function of fewer parameters is assignable to one of more. + */ +interface DownstreamMiddleware { + handle(ctx: HttpContext, next: NextFn): Promise | void; +} + +/** + * Wraps a plain middleware object in the class shape that `server.use()` + * expects. + */ +function asMiddlewareClass( + middleware: DownstreamMiddleware, +): MiddlewareAsClass { + return class TestMiddleware { + handle = middleware.handle.bind(middleware); + }; +} + +/** + * An exception handler for the harness, behaviourally identical to the one + * `@adonisjs/http-server` falls back to but returning promises. + * + * Registering it works around an upstream bug rather than a behaviour this + * package needs. The server invokes the error handler through + * `tracingChannel.tracePromise`, but the built-in fallback's `handle` is + * synchronous, so Node prints + * + * > tracePromise was called with the function 'handle', which returned a + * > non-thenable + * + * on every request that reaches it. The fall-through tests reach it by design + * — an unmatched route raises `E_ROUTE_NOT_FOUND` — so without this the warning + * appears several times in an otherwise green run. A real application never + * sees it: `node ace configure` scaffolds *app/exceptions/handler.ts* extending + * `ExceptionHandler`, whose `handle` is async. + */ +class TestExceptionHandler { + report(): Promise { + return Promise.resolve(); + } + + handle( + error: { status?: number; message?: string }, + ctx: HttpContext, + ): Promise { + ctx.response + .status(error.status ?? 500) + .send(error.message ?? "Internal server error"); + return Promise.resolve(); + } +} + +export async function startTestServer( + options: TestServerOptions, +): Promise { + const app = new AppFactory().create( + new URL("../", import.meta.url), + () => import("node:util"), + ); + await app.init(); + + const middleware = fedifyMiddleware( + options.federation, + options.contextDataFactory, + { + ignoreRoutePrefixes: options.ignoreRoutePrefixes, + }, + ); + + const server = new ServerFactory().merge({ app }).create(); + server.errorHandler(() => Promise.resolve({ default: TestExceptionHandler })); + + // `fedifyMiddleware` already returns the class `server.use()` constructs. + const stack: (() => Promise<{ default: MiddlewareAsClass }>)[] = [ + () => Promise.resolve({ default: middleware }), + ]; + if (options.afterFedify) { + const downstream = options.afterFedify; + stack.push(() => + Promise.resolve({ default: asMiddlewareClass(downstream) }) + ); + } + + server.use(stack); + + options.defineRoutes?.(server.getRouter()); + + await server.boot(); + + const httpServer: NodeHttpServer = createServer(server.handle.bind(server)); + // Without the `error` listener a failed bind never settles the promise, and + // the run stalls until the test timeout with nothing said about the cause. + await new Promise((resolve, reject) => { + httpServer.once("error", reject); + httpServer.listen(0, "127.0.0.1", () => { + httpServer.removeListener("error", reject); + resolve(); + }); + }); + + const { port } = httpServer.address() as AddressInfo; + const url = `http://127.0.0.1:${port}`; + + return { + url, + fetch(path, init) { + return fetch(new URL(path, url), init); + }, + async close() { + await new Promise((resolve, reject) => { + httpServer.close((error) => (error ? reject(error) : resolve())); + // `close()` stops new connections but waits for the open ones to end, + // and `fetch()` keeps its sockets alive, so the callback would only + // fire once undici's idle timeout expired. + httpServer.closeIdleConnections(); + }); + await app.terminate(); + }, + }; +} + +/** + * Sends a verbatim request line, which `fetch()` cannot: it rejects forbidden + * methods such as `TRACE`, and never sends an absolute-form target. + * + * The response is returned unparsed apart from its status code, which keeps the + * helper out of body-framing concerns. + */ +export function fetchRaw( + server: TestServer, + target: string, + init: { method?: string; headers?: Record } = {}, +): Promise<{ status: number; raw: string }> { + const { hostname, port } = new URL(server.url); + + return new Promise((resolve, reject) => { + const socket = connect({ host: hostname, port: Number(port) }); + socket.setEncoding("utf8"); + + const chunks: string[] = []; + socket.on("error", reject); + socket.on("data", (chunk: string) => chunks.push(chunk)); + // `Connection: close` makes the server end the socket after replying, so + // this fires once the whole response has arrived. + socket.on("close", () => { + const raw = chunks.join(""); + resolve({ status: Number(raw.split(" ")[1]), raw }); + }); + socket.on("connect", () => { + socket.end( + [ + `${init.method ?? "GET"} ${target} HTTP/1.1`, + `Host: ${hostname}:${port}`, + "Connection: close", + ...Object.entries(init.headers ?? {}).map( + ([key, value]) => `${key}: ${value}`, + ), + "", + "", + ].join("\r\n"), + ); + }); + }); +} + +/** + * The `Accept` header a fediverse server sends when dereferencing an object. + */ +export const ACTIVITY_PUB_ACCEPT = + 'application/activity+json, application/ld+json; profile="https://www.w3.org/ns/activitystreams"'; + +/** + * The `Accept` header a browser sends. + */ +export const BROWSER_ACCEPT = + "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"; + +export type { HttpContext }; diff --git a/packages/adonisjs/src/types.ts b/packages/adonisjs/src/types.ts new file mode 100644 index 000000000..778a87a74 --- /dev/null +++ b/packages/adonisjs/src/types.ts @@ -0,0 +1,292 @@ +/** + * Types for `@fedify/adonisjs`. + * + * This module also carries the *module augmentations* that teach AdonisJS about + * Fedify: the `ctx.federation` property on `HttpContext` and the three IoC + * container bindings. Importing this module (directly or, more usually, via + * the package entry point) is what makes those types visible to an + * application's TypeScript project. + * + * @module + */ +import type { HttpContext } from "@adonisjs/core/http"; +// TypeScript only binds a `declare module` augmentation when the target module +// is part of the program, so the module has to be imported even though nothing +// is used from it directly. +import type {} from "@adonisjs/core/types"; +import type { + Federation, + FederationBuilder, + FederationOptions, + FederationOrigin, + KvStore, + RequestContext, +} from "@fedify/fedify"; + +/** + * The "register pattern" interface used to configure the context data type of + * an application, in the same spirit as AdonisJS's own `ContainerBindings` or + * `EventsList` interfaces. + * + * Fedify threads an application-defined value — `TContextData` — through every + * dispatcher, collection callback and inbox listener. Because a package cannot + * know that type in advance, an application declares it by augmenting this + * interface once, usually in `config/fedify.ts`: + * + * ```ts + * declare module '@fedify/adonisjs/types' { + * interface FedifyTypes { + * contextData: { user: User | null } + * } + * } + * ``` + * + * After that augmentation, {@link ContextData} resolves to the declared type + * everywhere in the application, including inside `ctx.federation`. + */ +// The empty interface is intentional: it is the extension point applications +// augment, so it has to start out with no members. +// deno-lint-ignore no-empty-interface +export interface FedifyTypes {} + +/** + * The context data type of the current application. + * + * Resolves to whatever {@link FedifyTypes.contextData} was augmented to, and + * falls back to `HttpContext | null` when the application has not augmented + * anything. + * + * The `| null` half of the default is deliberate and important. Fedify invokes + * dispatchers and inbox listeners from two very different places: + * + * - During an HTTP request, where an `HttpContext` exists, and + * - From a background queue worker or an Ace command, where one does *not*. + * + * Defaulting to a non-nullable `HttpContext` would let application code write + * `ctx.data.auth.user` in an inbox listener and only discover at 3 a.m. in + * production — once a real server delivers an activity through the queue — that + * there was never an HTTP context to read. The nullable default forces that + * case to be handled at compile time. + */ +export type ContextData = FedifyTypes extends + { contextData: infer TContextData } ? TContextData + : HttpContext | null; + +/** + * A callback that produces the Fedify context data for a single HTTP request. + * + * This mirrors the `ContextDataFactory` type exported by the other Fedify + * integration packages (`@fedify/express`, `@fedify/hono`, `@fedify/koa`, …). + * Like Hono's and Koa's variants — and unlike Express's, which only receives + * the request — it is handed the whole framework context, so it can read the + * authenticated user, the request-scoped container resolver, or anything else + * hanging off `HttpContext`. + * + * It may be synchronous or asynchronous, and it is invoked at most once per + * request. + */ +export type ContextDataFactory = ( + ctx: HttpContext, +) => TContextData | Promise; + +/** + * Options for bridging Fedify's LogTape logs into the AdonisJS logger. + */ +export interface FedifyLoggingConfig { + /** + * The LogTape categories to route into the AdonisJS logger. + * + * Defaults to Fedify's own category plus LogTape's meta category, which is + * where LogTape reports its own configuration problems. + * + * @default `[['logtape', 'meta'], ['fedify']]` + */ + categories?: (string | string[])[]; +} + +/** + * Everything {@link FedifyConfig} accepts except `contextDataFactory`, whose + * optionality is decided separately by {@link ContextDataFactoryOption}. + * + * It is Fedify's own {@link FederationOptions} — so every Fedify option is + * available and documented in Fedify's own reference — plus a handful of + * AdonisJS-specific keys, and with two differences: + * + * - `origin` is **required** (Fedify treats it as optional and derives the + * origin from the incoming request, which produces wrong actor URIs the + * moment the app sits behind a proxy or a tunnel), and + * - `kv` is **optional** (it falls back to an in-memory store in development). + */ +export interface FedifyConfigBase + extends Omit, "kv" | "origin"> { + /** + * The canonical public origin of this server, for example + * `https://social.example.com`. + * + * Fedify mints actor URIs, activity IDs and collection URLs from this value, + * so it must be the address remote servers can reach — not the address the + * Node process happens to be listening on. Read it from an environment + * variable (the `configure` hook adds `PUBLIC_URL` for exactly this purpose). + * + * Pass a {@link FederationOrigin} object when the WebFinger handle domain + * differs from the web origin, for example handles `@me@example.com` served + * from `https://social.example.com`. + */ + origin: string | FederationOrigin; + + /** + * The key–value store Fedify uses for caching remote documents, tracking + * inbox idempotency, and holding outbox state. + * + * Defaults to an in-memory store, which is fine for development and tests but + * loses all state on restart and cannot be shared between processes. A + * warning is logged if the default is used in production. Use a persistent + * implementation such as `@fedify/sqlite`, `@fedify/postgres`, + * `@fedify/redis`, or `@fedify/mysql` in production. + */ + kv?: KvStore; + + /** + * URL path prefixes that Fedify should never see. + * + * Requests whose path starts with one of these prefixes skip the Fedify + * middleware entirely and go straight to the AdonisJS router. This is a pure + * performance escape hatch for hot paths that can never be federation + * endpoints — Vite's dev-server routes are the usual example. + * + * Accessing `ctx.federation` inside a request that matched one of these + * prefixes throws, because no Fedify context was created for it. + * + * @default `[]` + */ + ignoreRoutePrefixes?: string[]; + + /** + * Whether to pipe Fedify's LogTape logs into the AdonisJS logger. + * + * Fedify logs a great deal of useful diagnostic detail (signature + * verification, delivery attempts, WebFinger lookups) through LogTape. + * Without this bridge, none of it appears in the application's log output. + * + * Set to `false` if the application configures LogTape itself. + * + * @default `true` + */ + logging?: boolean | FedifyLoggingConfig; + + /** + * Who drains Fedify's task queue, and with what context data. + * + * Required whenever {@link FederationOptions.queue} is set, because there is + * no safe default. Letting Fedify start the queue lazily — which is what it + * does out of the box — captures the context data of whichever HTTP request + * happened to enqueue the first task, and every background job for the rest + * of the process then runs with that one stale request's `HttpContext`. + * + * Pass a callback to have the **web process** drain the queue: the service + * provider starts the worker in the `ready` lifecycle hook and stops it on + * shutdown. There is no `HttpContext` behind a queued task, hence the + * callback takes no arguments. + * + * Pass `false` when a **separate worker process** owns the queue. Nothing is + * started in-process, and it is then that worker's job to call + * `federation.startQueue()`. + */ + queueContextData?: (() => ContextData | Promise) | false; +} + +/** + * The `contextDataFactory` half of {@link FedifyConfig}, whose optionality + * depends on {@link ContextData}. + * + * The runtime default is `(ctx) => ctx`, which is only a correct + * {@link ContextData} while the application has not augmented + * {@link FedifyTypes} — after an augmentation an `HttpContext` is precisely + * what a dispatcher must *not* receive. Making the key required in that case + * turns "every dispatcher silently gets the wrong value" into a compile error + * on `config/fedify.ts`, which is the only place that can fix it. + * + * The tuple wrappers keep the conditional from distributing over a union + * {@link ContextData}, so an augmentation such as `User | null` is compared as + * a whole. + */ +export type ContextDataFactoryOption = [HttpContext] extends [ContextData] ? { + /** + * Produces the {@link ContextData} for each incoming HTTP request. + * + * Defaults to `(ctx) => ctx`, i.e. the `HttpContext` itself, which matches + * the default of {@link ContextData}. + */ + contextDataFactory?: ContextDataFactory; + } + : { + /** + * Produces the {@link ContextData} for each incoming HTTP request. + * + * Required here because the application augmented {@link FedifyTypes}: + * the built-in default hands dispatchers the `HttpContext`, which is no + * longer the declared context data type. + */ + contextDataFactory: ContextDataFactory; + }; + +/** + * The shape of the object passed to {@link defineConfig} in `config/fedify.ts`. + * + * {@link FedifyConfigBase} plus {@link ContextDataFactoryOption}, which makes + * `contextDataFactory` required exactly when {@link FedifyTypes} has been + * augmented. + */ +export type FedifyConfig = FedifyConfigBase & ContextDataFactoryOption; + +/** + * The fully-resolved configuration held by the `fedify.config` container + * binding: {@link FedifyConfig} with every optional AdonisJS-specific key + * filled in. + */ +export interface ResolvedFedifyConfig extends FederationOptions { + contextDataFactory: ContextDataFactory; + ignoreRoutePrefixes: string[]; + logging: boolean | FedifyLoggingConfig; + queueContextData?: (() => ContextData | Promise) | false; +} + +declare module "@adonisjs/core/types" { + interface ContainerBindings { + /** + * The built {@link Federation} instance. Resolvable only once the + * application is ready — see the service provider for why. + */ + "fedify": Federation; + + /** + * The {@link FederationBuilder} that dispatchers and inbox listeners are + * registered on, before the `Federation` instance is built. + */ + "fedify.builder": FederationBuilder; + + /** + * The resolved contents of `config/fedify.ts`. + */ + "fedify.config": ResolvedFedifyConfig; + } +} + +declare module "@adonisjs/core/http" { + interface HttpContext { + /** + * The Fedify {@link RequestContext} for the current request. + * + * Use it to mint canonical URIs (`ctx.federation.getActorUri('me')`), look + * remote objects up (`ctx.federation.lookupObject('@user@example.com')`), + * or send activities (`ctx.federation.sendActivity(...)`) from ordinary + * AdonisJS controllers. + * + * The context is created lazily on first access, so requests that never + * touch it pay nothing. Accessing it throws if the request path matched + * {@link FedifyConfig.ignoreRoutePrefixes}, or if the Fedify server + * middleware is not registered in `start/kernel.ts`. + */ + federation: RequestContext; + } +} diff --git a/packages/adonisjs/stubs/config/fedify.stub b/packages/adonisjs/stubs/config/fedify.stub new file mode 100644 index 000000000..f697532fe --- /dev/null +++ b/packages/adonisjs/stubs/config/fedify.stub @@ -0,0 +1,85 @@ +{{{ + exports({ to: app.configPath('fedify.ts') }) +}}} +import { defineConfig } from '@fedify/adonisjs' +import env from '#start/env' + +/** + * The domain that fediverse handles live on, if it differs from the web origin. + * Leave FEDERATION_HANDLE_HOST unset when handles and pages share a domain. + */ +const handleHost = env.get('FEDERATION_HANDLE_HOST') + +export default defineConfig({ + /** + * The canonical public origin of this server. + * + * Fedify builds actor URIs, activity IDs and collection URLs from this value, + * so it must be the address remote servers can reach. When developing behind + * "fedify tunnel" or ngrok, set PUBLIC_URL to the tunnel URL. + */ + origin: handleHost + ? { handleHost, webOrigin: env.get('PUBLIC_URL') } + : env.get('PUBLIC_URL'), + + /** + * Where Fedify caches remote documents, tracks inbox idempotency and keeps + * outbox state. + * + * Defaults to an in-memory store, which is fine for development but is lost + * on restart and cannot be shared between processes. Install one of + * "@fedify/sqlite", "@fedify/postgres", "@fedify/redis" or "@fedify/mysql" + * before going to production, then uncomment the lines below: + * + * import { DatabaseSync } from 'node:sqlite' + * import { SqliteKvStore, SqliteMessageQueue } from '@fedify/sqlite' + * import app from '@adonisjs/core/services/app' + * + * const database = new DatabaseSync(app.tmpPath('fedify.sqlite3')) + */ + // kv: new SqliteKvStore(database), + + /** + * The queue used to deliver outgoing activities and to process incoming ones. + * + * Without a queue, delivery happens inline: the request that triggered it + * blocks until every recipient server has answered, and a failed delivery is + * never retried. Production deployments should always configure one. + * + * Setting "queue" makes "queueContextData" below mandatory. + */ + // queue: new SqliteMessageQueue(database), + + /** + * Who drains the queue. + * + * A callback means this process does, and the context data it returns is what + * background jobs see -- there is no HTTP request behind them. Use "false" + * when a separate worker process owns the queue. + */ + // queueContextData: () => null, + + /** + * URL path prefixes that skip Fedify entirely. + * + * A pure performance escape hatch for paths that can never be federation + * endpoints. Accessing "ctx.federation" on such a request throws. + */ + ignoreRoutePrefixes: ['/@vite/', '/resources/', '/node_modules/'], +}) + +/** + * The value Fedify threads through every dispatcher and inbox listener as + * "ctx.data". + * + * It defaults to "HttpContext | null" -- null when Fedify is working outside a + * request, such as in a queue worker. Augment it here to carry something more + * specific, and set "contextDataFactory" above to build it -- the option becomes + * required once you do: + * + * declare module '@fedify/adonisjs/types' { + * interface FedifyTypes { + * contextData: { user: User | null } + * } + * } + */ diff --git a/packages/adonisjs/stubs/federation/main.stub b/packages/adonisjs/stubs/federation/main.stub new file mode 100644 index 000000000..5a809c332 --- /dev/null +++ b/packages/adonisjs/stubs/federation/main.stub @@ -0,0 +1,64 @@ +{{{ + exports({ to: app.makePath('app/federation/main.ts') }) +}}} +/* +|-------------------------------------------------------------------------- +| Federation dispatchers +|-------------------------------------------------------------------------- +| +| Registered on the Fedify builder while the application boots, from the +| "start/federation.ts" preload file. +| +| Registration happens exactly once per process. Do not add this directory +| to "hotHook.boundaries" in package.json: re-importing the module on a hot +| reload would register the same dispatcher twice and Fedify would throw. +| Restart the dev server after changing a dispatcher. +| +| See https://fedify.dev/manual/actor and https://fedify.dev/manual/inbox +| +*/ + +import federation from '@fedify/adonisjs/services/builder' +import { Endpoints, Person } from '@fedify/vocab' + +/** + * Serves the actor document at "/actors/", and -- through the key + * pairs dispatcher -- the keys that sign this actor's outgoing activities. + */ +federation + .setActorDispatcher('/actors/{identifier}', async (ctx, identifier) => { + // Look the actor up in your database and return null when it is unknown. + if (identifier !== 'me') return null + + const keys = await ctx.getActorKeyPairs(identifier) + + return new Person({ + id: ctx.getActorUri(identifier), + preferredUsername: identifier, + name: 'Me', + inbox: ctx.getInboxUri(identifier), + endpoints: new Endpoints({ sharedInbox: ctx.getInboxUri() }), + url: new URL('/', ctx.url), + + // Two keys, two purposes: the RSA key signs HTTP Signatures (what + // Mastodon verifies), the Ed25519 key signs Object Integrity Proofs. + publicKeys: keys.map((keyPair) => keyPair.cryptographicKey), + assertionMethods: keys.map((keyPair) => keyPair.multikey), + }) + }) + .setKeyPairsDispatcher(async (_ctx, _identifier) => { + // Load the actor's key pairs from your database, generating and persisting + // them the first time. Private keys are secrets: never log them, never + // expose them through an endpoint. + // + // See https://fedify.dev/manual/actor#public-keys-of-an-actor + return [] + }) + +/** + * Receives activities from other servers. + * + * The second path is the shared inbox, which servers use to deliver one + * activity destined for many local actors. + */ +federation.setInboxListeners('/actors/{identifier}/inbox', '/inbox') diff --git a/packages/adonisjs/stubs/main.ts b/packages/adonisjs/stubs/main.ts new file mode 100644 index 000000000..6926ba9cf --- /dev/null +++ b/packages/adonisjs/stubs/main.ts @@ -0,0 +1,10 @@ +import { dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** + * The root of the stub templates shipped with this package. + * + * Used by `configure.ts` and by `node ace eject --pkg=@fedify/adonisjs` + * when an application wants to customise a generated file. + */ +export const stubsRoot: string = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/adonisjs/stubs/start/federation.stub b/packages/adonisjs/stubs/start/federation.stub new file mode 100644 index 000000000..82d12a69a --- /dev/null +++ b/packages/adonisjs/stubs/start/federation.stub @@ -0,0 +1,20 @@ +{{{ + exports({ to: app.makePath('start/federation.ts') }) +}}} +/* +|-------------------------------------------------------------------------- +| Federation preload file +|-------------------------------------------------------------------------- +| +| Imported while the application boots, after every service provider has +| booted and before the HTTP server starts accepting connections. This is +| where Fedify's actor dispatchers, collection dispatchers and inbox +| listeners get registered. +| +| Import each of your federation modules here. They register themselves on +| the builder exported by "@fedify/adonisjs/services/builder", and the +| builder is sealed into a Federation object once this file has run. +| +*/ + +import '../app/federation/main.js' diff --git a/packages/adonisjs/tsconfig.json b/packages/adonisjs/tsconfig.json new file mode 100644 index 000000000..0121ad1a8 --- /dev/null +++ b/packages/adonisjs/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "target": "ESNext", + "lib": ["ESNext"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "moduleDetection": "force", + "rootDir": ".", + "outDir": "./dist", + + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "verbatimModuleSyntax": true, + + // The Fedify integration-package template builds declarations with + // `isolatedDeclarations`, which requires every exported symbol to carry an + // explicit type annotation. Enabling it here means `npm run typecheck` + // catches violations before the declaration build does. + "isolatedDeclarations": true, + "isolatedModules": true, + "declaration": true, + + // AdonisJS v7 and its own packages write relative imports with a `.ts` + // extension and let the compiler rewrite them to `.js` on emit. Doing the + // same here means `node --test src/**/*.test.ts` runs the sources directly + // through Node's built-in type stripping, with no loader and no flags. + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true, + + "skipLibCheck": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "types": ["node"] + }, + "include": ["index.ts", "configure.ts", "src/**/*.ts", "stubs/main.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/adonisjs/tsdown.config.ts b/packages/adonisjs/tsdown.config.ts new file mode 100644 index 000000000..38f6876af --- /dev/null +++ b/packages/adonisjs/tsdown.config.ts @@ -0,0 +1,79 @@ +import { defineConfig } from "tsdown"; + +/** + * Build configuration for `@fedify/adonisjs`, following the Fedify + * integration-package template: dual ESM + CommonJS output with `.cjs` / + * `.d.cts` extensions for the CommonJS half, and declaration files built with + * `isolatedDeclarations`. + * + * The CommonJS build is real, not decorative. `@adonisjs/core` is published as + * ESM only, but this package requires Node.js >= 24, where `require()` of an + * ESM graph without top-level await is fully supported — and every module here + * is written without top-level await for exactly that reason. The test suite + * loads each `.cjs` entry through `createRequire` to keep that true. + * + * One deviation from the template: multiple entry points instead of a single + * `src/mod.ts`. AdonisJS packages expose their provider, middleware and + * container services through dedicated subpath exports so that `adonisrc.ts` + * and `start/kernel.ts` can lazily import exactly one module (see + * `@adonisjs/cors` for the canonical example). `stubs/main.ts` is an entry + * because `configure.ts` resolves the stub templates relative to the compiled + * location of that module. + */ +export default defineConfig({ + entry: [ + "index.ts", + "configure.ts", + "src/types.ts", + "src/builder.ts", + "src/define_config.ts", + "src/errors.ts", + "src/middleware.ts", + "src/provider.ts", + "src/fedify_middleware.ts", + "src/services/builder.ts", + "stubs/main.ts", + ], + outDir: "dist", + platform: "node", + format: ["esm", "cjs"], + target: "esnext", + outExtensions({ format }) { + return { + js: format === "cjs" ? ".cjs" : ".js", + dts: format === "cjs" ? ".d.cts" : ".d.ts", + }; + }, + dts: { + compilerOptions: { + isolatedDeclarations: true, + declaration: true, + }, + }, + outputOptions: { + exports: "named", + }, + // The stub templates are data, not modules, so nothing imports them and the + // bundler would not otherwise emit them. `configure.ts` resolves them through + // `stubsRoot` from the compiled `stubs/main.js`, so they have to land in + // `dist/stubs/` with their directory structure intact. The glob deliberately + // matches only `*.stub`: `stubs/main.ts` is an entry point and is compiled, + // not copied. + // `flatten` defaults to true, which would collapse `config/fedify.stub` and + // `federation/main.stub` into a single directory and break both lookups. With + // it off, paths are preserved relative to the glob's base (`stubs/`), so `to` + // has to name `dist/stubs` — which is what `stubsRoot` resolves to, being the + // directory of the compiled `stubs/main.js`. + copy: [{ from: "stubs/**/*.stub", to: "dist/stubs", flatten: false }], + // Keep the emitted files readable: this package is meant to be studied by + // people who want to write their own AdonisJS integrations. + minify: false, + treeshake: false, + sourcemap: true, + // Cleaning here rather than in a separate `clean` step keeps `build:self` a + // single self-contained command, which is what the monorepo's `mise run + // prepare` invokes. Without it, a renamed or removed entry point would leave + // a stale artefact behind for `src/dist.test.ts` to load and pass on. + clean: true, + unbundle: true, +}); diff --git a/packages/fedify/README.md b/packages/fedify/README.md index 0d1e16f7d..0c081a3a4 100644 --- a/packages/fedify/README.md +++ b/packages/fedify/README.md @@ -100,6 +100,7 @@ Here is the list of packages: | [@fedify/fedify](/packages/fedify/) | [JSR] | [npm] | The core framework of Fedify | | [@fedify/cli](/packages/cli/) | [JSR][jsr:@fedify/cli] | [npm][npm:@fedify/cli] | CLI toolchain for testing and debugging | | [@fedify/create](/packages/create/) | | [npm][npm:@fedify/create] | Create a new Fedify project | +| [@fedify/adonisjs](/packages/adonisjs/) | | [npm][npm:@fedify/adonisjs] | AdonisJS integration | | [@fedify/amqp](/packages/amqp/) | [JSR][jsr:@fedify/amqp] | [npm][npm:@fedify/amqp] | AMQP/RabbitMQ driver | | [@fedify/astro](/packages/astro/) | [JSR][jsr:@fedify/astro] | [npm][npm:@fedify/astro] | Astro integration | | [@fedify/backfill](/packages/backfill/) | [JSR][jsr:@fedify/backfill] | [npm][npm:@fedify/backfill] | ActivityPub backfill support | @@ -137,6 +138,7 @@ Here is the list of packages: [jsr:@fedify/cli]: https://jsr.io/@fedify/cli [npm:@fedify/cli]: https://www.npmjs.com/package/@fedify/cli [npm:@fedify/create]: https://www.npmjs.com/package/@fedify/create +[npm:@fedify/adonisjs]: https://www.npmjs.com/package/@fedify/adonisjs [jsr:@fedify/amqp]: https://jsr.io/@fedify/amqp [npm:@fedify/amqp]: https://www.npmjs.com/package/@fedify/amqp [jsr:@fedify/astro]: https://jsr.io/@fedify/astro diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aee876aba..fb395c3fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,6 +6,12 @@ settings: catalogs: default: + '@adonisjs/assembler': + specifier: ^8.0.0 + version: 8.4.0 + '@adonisjs/core': + specifier: ^7.0.0 + version: 7.4.0 '@astrojs/netlify': specifier: ^6.6.5 version: 6.6.5 @@ -205,6 +211,9 @@ importers: docs: devDependencies: + '@adonisjs/core': + specifier: 'catalog:' + version: 7.4.0(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(youch@4.1.1) '@braintree/sanitize-url': specifier: ^7.1.2 version: 7.1.2 @@ -214,6 +223,9 @@ importers: '@deno/kv': specifier: ^0.8.4 version: 0.8.4 + '@fedify/adonisjs': + specifier: workspace:^ + version: link:../packages/adonisjs '@fedify/amqp': specifier: workspace:^ version: link:../packages/amqp @@ -490,7 +502,7 @@ importers: version: link:../../packages/vocab elysia: specifier: 'catalog:' - version: 1.3.8(exact-mirror@0.1.3(@sinclair/typebox@0.34.38))(file-type@21.3.4)(typescript@6.0.3) + version: 1.3.8(exact-mirror@0.1.3(@sinclair/typebox@0.34.38))(file-type@22.0.1)(typescript@6.0.3) devDependencies: bun-types: specifier: ^1.2.19 @@ -728,7 +740,7 @@ importers: version: 8.5.6 tailwindcss: specifier: ^3.4.1 - version: 3.4.17(ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3)) + version: 3.4.17(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3)) typescript: specifier: 'catalog:' version: 6.0.3 @@ -879,7 +891,7 @@ importers: version: 10.1.8(eslint@9.32.0(jiti@2.6.1)) eslint-plugin-svelte: specifier: ^3.0.0 - version: 3.11.0(eslint@9.32.0(jiti@2.6.1))(svelte@5.38.3)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)) + version: 3.11.0(eslint@9.32.0(jiti@2.6.1))(svelte@5.38.3)(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@24.3.0)(typescript@5.9.2)) globals: specifier: ^16.0.0 version: 16.3.0 @@ -911,6 +923,34 @@ importers: specifier: ^7.0.4 version: 7.1.3(@types/node@24.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) + packages/adonisjs: + dependencies: + '@logtape/logtape': + specifier: 'catalog:' + version: 2.2.0 + devDependencies: + '@adonisjs/assembler': + specifier: 'catalog:' + version: 8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3) + '@adonisjs/core': + specifier: 'catalog:' + version: 7.4.0(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(youch@4.1.1) + '@fedify/fedify': + specifier: workspace:^ + version: link:../fedify + '@fedify/vocab': + specifier: workspace:^ + version: link:../vocab + '@types/node': + specifier: 'catalog:' + version: 22.19.1 + tsdown: + specifier: 'catalog:' + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) + typescript: + specifier: 'catalog:' + version: 6.0.3 + packages/amqp: dependencies: '@fedify/fedify': @@ -940,7 +980,7 @@ importers: version: 0.10.8 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -956,7 +996,7 @@ importers: version: 7.0.7(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(@netlify/blobs@10.7.9)(@types/node@24.3.0)(db0@0.3.4(@electric-sql/pglite@0.3.16)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.3.16)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@24.3.0))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@24.3.0)))(ioredis@5.10.1)(jiti@2.6.1)(rollup@4.60.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0) tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -969,7 +1009,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -988,7 +1028,7 @@ importers: version: 0.8.71(@cloudflare/workers-types@4.20260511.1)(@vitest/runner@3.2.4)(@vitest/snapshot@3.2.4)(vitest@3.2.4(@types/debug@4.1.12)(@types/node@24.3.0)(jiti@2.6.1)(jsdom@20.0.3)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0)) tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1139,7 +1179,7 @@ importers: version: 6.0.5 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1164,7 +1204,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1198,7 +1238,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1210,14 +1250,14 @@ importers: version: link:../fedify elysia: specifier: ^1.3.6 - version: 1.3.8(exact-mirror@0.1.3(@sinclair/typebox@0.34.38))(file-type@21.3.4)(typescript@6.0.3) + version: 1.3.8(exact-mirror@0.1.3(@sinclair/typebox@0.34.38))(file-type@22.0.1)(typescript@6.0.3) devDependencies: bun-types: specifier: ^1.2.19 version: 1.2.19(@types/react@19.1.8) tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1239,7 +1279,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1261,7 +1301,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1349,7 +1389,7 @@ importers: version: 4.20250617.4 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) tsx: specifier: ^4.21.0 version: 4.21.0 @@ -1383,7 +1423,7 @@ importers: version: 0.5.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1402,7 +1442,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1418,7 +1458,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1461,7 +1501,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1483,7 +1523,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1505,7 +1545,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1542,7 +1582,7 @@ importers: version: 1.66.0 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1576,7 +1616,7 @@ importers: version: 0.5.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1601,7 +1641,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1635,13 +1675,13 @@ importers: version: 22.19.1 netlify-cli: specifier: 'catalog:' - version: 26.2.0(@types/node@22.19.1)(db0@0.3.4(@electric-sql/pglite@0.3.16)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.3.16)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(ioredis@5.10.1)(picomatch@4.0.5)(rollup@4.60.2) + version: 26.2.0(@swc/core@1.15.47)(@types/node@22.19.1)(db0@0.3.4(@electric-sql/pglite@0.3.16)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.3.16)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(ioredis@5.10.1)(picomatch@4.0.5)(rollup@4.60.2) postgres: specifier: 'catalog:' version: 3.4.7 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1657,7 +1697,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1688,7 +1728,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1722,7 +1762,7 @@ importers: version: '@jsr/std__async@1.0.13' tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1759,7 +1799,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1784,7 +1824,7 @@ importers: version: link:../vocab-runtime tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1803,7 +1843,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1834,7 +1874,7 @@ importers: version: '@jsr/std__async@1.0.13' tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1850,7 +1890,7 @@ importers: devDependencies: tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1887,7 +1927,7 @@ importers: version: '@jsr/std__async@1.0.13' tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1902,7 +1942,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1957,7 +1997,7 @@ importers: version: 12.6.0 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -1997,7 +2037,7 @@ importers: version: 12.6.0 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -2022,7 +2062,7 @@ importers: version: 22.19.1 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 @@ -2053,13 +2093,138 @@ importers: version: 12.6.0 tsdown: specifier: 'catalog:' - version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)) + version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34) typescript: specifier: 'catalog:' version: 6.0.3 packages: + '@adonisjs/ace@14.1.0': + resolution: {integrity: sha512-8N8z1YKePBiXz7wLxHFz/HSqjCRSL/9Vzs4XQt8gk8G17u4PXwNncWt0vSgYEcDrvPAt+QOavY1vMeKOOWe29w==} + engines: {node: '>=24.0.0'} + peerDependencies: + youch: ^4.1.0-beta.11 || ^4.1.0 + + '@adonisjs/application@9.0.1': + resolution: {integrity: sha512-bSUC8vcECEr9qvvHeMAhjvH73ngAm5c6kOnEJYNCczP6rgG7AdH03MGtrY2ewKYDyS/QepNpDKj4oVE2XtxVtw==} + engines: {node: '>=24.0.0'} + peerDependencies: + '@adonisjs/assembler': ^8.0.0-next.23 || ^8.0.0 + '@adonisjs/config': ^6.1.0-next.0 || ^6.0.0 + '@adonisjs/fold': ^11.0.0-next.3 || ^11.0.0 + peerDependenciesMeta: + '@adonisjs/assembler': + optional: true + + '@adonisjs/assembler@8.4.0': + resolution: {integrity: sha512-Nxi6UU2fd/Wq8iLb+FwicK+7ePyvZcmtbJaT25RhGbgSZ2tgbxzaI7YF4TbaLKDIsF48DOcTP0dTfUIcgjBchw==} + engines: {node: '>=24.0.0'} + peerDependencies: + typescript: ^5.0.0 || ^6.0.0 + + '@adonisjs/bodyparser@11.0.4': + resolution: {integrity: sha512-amqMPVFbzxkOoCMbkFs5sC+rTu7cER2rSv+1v8dsGKszolk5lnGuyNjkha1Lqksvw1qykIIbC1fXr37B9WJTdg==} + engines: {node: '>=24.0.0'} + peerDependencies: + '@adonisjs/http-server': ^8.0.0-next.17 || ^8.0.0 || ^9.0.0 + + '@adonisjs/config@6.1.0': + resolution: {integrity: sha512-YVDRL8xHCtM6iMnAefOBaz6iXVpojwBPDQWPKxnVSucycYeNGrGitJiLy+cGaeAU7Gjm8al9SJRJt3rRPr5PKg==} + engines: {node: '>=24.0.0'} + + '@adonisjs/core@7.4.0': + resolution: {integrity: sha512-57s/gnY4fpC5yE+scwDHyAuMTfbNdkMWeVl/E27212SCeY57Pk6PdadTQ1hjZUi2ksADjUJ5uT8xvWuM5/Op7A==} + engines: {node: '>=24.0.0'} + hasBin: true + peerDependencies: + '@adonisjs/assembler': ^8.0.0-next.23 || ^8.0.0 + '@vinejs/vine': ^4.0.0 + argon2: ^0.44.0 || ^0.45.1 + bcrypt: ^6.0.0 + edge.js: ^6.2.0 + pino-pretty: ^13.1.3 + youch: ^4.1.0-beta.13 || ^4.1.0 + peerDependenciesMeta: + '@adonisjs/assembler': + optional: true + '@vinejs/vine': + optional: true + argon2: + optional: true + bcrypt: + optional: true + edge.js: + optional: true + pino-pretty: + optional: true + youch: + optional: true + + '@adonisjs/env@7.0.0': + resolution: {integrity: sha512-9lSGONI4B1E7LxyVZiUd1yCH9BOri4Ybp4b9x3ojT9AkKfYwqvj4S2USIvFAlkE7eHUC2WMvPgMLX17342Y3ww==} + engines: {node: '>=24.0.0'} + + '@adonisjs/events@10.2.0': + resolution: {integrity: sha512-SzwzbmTLsybSZd47zZMZ3df7puwhY7D8vZ5Uy79SiHjLKbr2eVzUuKjjoYB6/0pZu6IwK9Qx06dI43sl+tLoDw==} + engines: {node: '>=24.0.0'} + peerDependencies: + '@adonisjs/application': ^9.0.0-next.14 || ^9.0.0 + '@adonisjs/fold': ^11.0.0-next.4 || ^11.0.0 + + '@adonisjs/fold@11.0.0': + resolution: {integrity: sha512-RnmDPWz2imVp/B74xitxCPqTdoP07bZvfJe1bh9CD9Rmia4jjDvehZF67KFyGNMZ24MuKasqs3jOcM1vGJp0GA==} + engines: {node: '>=24.0.0'} + + '@adonisjs/hash@10.1.1': + resolution: {integrity: sha512-i12srCoJ2uomD8fTMPEp16lcRTsB9pI8AlGFrtkrq7xcoLPBVi67lovXkdS6BNHdvdciH9HLcRe73KB2kUvgfw==} + engines: {node: '>=20.6.0'} + peerDependencies: + argon2: ^0.31.2 || ^0.41.0 || ^0.43.0 || ^0.44.0 || ^0.45.1 + bcrypt: ^5.1.1 || ^6.0.0 + peerDependenciesMeta: + argon2: + optional: true + bcrypt: + optional: true + + '@adonisjs/health@3.1.0': + resolution: {integrity: sha512-m3doBnSwi/l9v1DO79xmjAoSPl9b2XCp+crGwF8QUlhe5CgWgtfnL0SeFNEiZGliD3c4gYdihKz4Pnydctva8A==} + engines: {node: '>=24.0.0'} + + '@adonisjs/http-server@9.2.0': + resolution: {integrity: sha512-yTrnmo1Sn3k6d1/O9J+8mpaMMAflFwYM1P+Y8drAr9thVT55G9ywxRUSXeEp2RnOyxNcGVdjfdRACt6M/aP5Dw==} + engines: {node: '>=24.0.0'} + peerDependencies: + '@adonisjs/application': ^9.0.0-next.14 || ^9.0.0 + '@adonisjs/events': ^10.1.0-next.4 || ^10.1.0 + '@adonisjs/fold': ^11.0.0-next.4 || ^11.0.0 + '@adonisjs/logger': ^7.1.0-next.3 || ^7.1.0 + '@boringnode/encryption': ^0.2.0 || ^1.0.0 + youch: ^4.1.0-beta.13 || ^4.1.0 + peerDependenciesMeta: + youch: + optional: true + + '@adonisjs/http-transformers@2.3.1': + resolution: {integrity: sha512-N3gBcKyoPHDeVvVIedTzc+ARSUURwNGTPid/e3iLdM4v/myoSnXd76FL/GGzMmjfqqxegpjI2IEMibG7ylrKSQ==} + engines: {node: '>=24.0.0'} + peerDependencies: + '@adonisjs/fold': ^11.0.0-next.2 + + '@adonisjs/logger@7.1.1': + resolution: {integrity: sha512-MmUlp8xBMT6zZy0+vnQcQjHIlNfU4pUJARlINr7Bqha9BvhIn03QZgJL5QJ+kJe1tl6ZpNAryoRTJUiOk/wINQ==} + engines: {node: '>=24.0.0'} + peerDependencies: + pino-pretty: ^13.1.1 + peerDependenciesMeta: + pino-pretty: + optional: true + + '@adonisjs/repl@5.0.0': + resolution: {integrity: sha512-cPp0w5svYUA3M1gdxQBO2Mx5g+SZfPweqKCAxk7C1Os/Qu67JKgWqXqNnl1q0Tzv/l0L19Ms1C7ivzQxeBNajg==} + engines: {node: '>=24.0.0'} + '@alinea/suite@0.6.3': resolution: {integrity: sha512-4oGhbwAGq3rQeuuq9ylmybMkIT1mAl6e+DiiTLwwmwNzHFQiVihishgOpkIGrs0fGVSD4T8jOLyNTuQ30RtVuQ==} @@ -2077,6 +2242,68 @@ packages: '@antfu/utils@8.1.1': resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + '@arr/every@1.0.1': + resolution: {integrity: sha512-UQFQ6SgyJ6LX42W8rHCs8KVc0JS0tzVL9ct4XYedJukskYVWTo49tNiMEK9C2HTyarbNiT/RVIRSY82vH+6sTg==} + engines: {node: '>=4'} + + '@ast-grep/napi-darwin-arm64@0.42.3': + resolution: {integrity: sha512-pcTBI8UiNQ9xVOYfu3Ww5KXcrrHV/jJqECQ6efpJpzTGwW+mOpzgihfrq5GKjbMONhr0OaRARCMwEsvKgBXCpA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@ast-grep/napi-darwin-x64@0.42.3': + resolution: {integrity: sha512-mQeDYeMWUruf0oiYJejzRzG9BEXjopbhPML8rJombviX6ePmulNjh6SDX/Mxe+vqNGt0JHXgdYtsAtDu9d0LQg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@ast-grep/napi-linux-arm64-gnu@0.42.3': + resolution: {integrity: sha512-T2/8eEvFTKSEXd9Dql34/bsyWogFYh7IGsuOoqYNJOcYwT+TPCLTWPZdcpGqrdYdTYT92qX+4H0dUmaZ+TuOPw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-arm64-musl@0.42.3': + resolution: {integrity: sha512-O/K4p/GB5MiK2NE8s7pCBDpZ0EOUrMkMF0HbZKi/HIXH9HQdkoYYO+NqKumk92YNr3gUo8fkGkIG6v4wowANvQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@ast-grep/napi-linux-x64-gnu@0.42.3': + resolution: {integrity: sha512-HnljxyLZmt7t7al46j/j/fLj7e9qG8Nlk6/vrj+AD6nRqoeu7pW4YHcQA9NXst79GBz2CU0nba1WEtjUfkNgGg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-linux-x64-musl@0.42.3': + resolution: {integrity: sha512-6WLvV3ErwWXQcpMS4BEVjD7QiTGO2Hr3LuDouQoUUBSMu9gI0bBAZGQaqGfWidI+aXYZ0eL5KmCCDB4X6Oyskg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@ast-grep/napi-win32-arm64-msvc@0.42.3': + resolution: {integrity: sha512-LiDOhkvBEeXi+yJomrfUYgnLvCntrWianruKXMzIK1QpxWmFSVC2uwWacMbq2v/DTk0MlQ09225yD6sAuL5pEQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@ast-grep/napi-win32-ia32-msvc@0.42.3': + resolution: {integrity: sha512-DOAzKQPWQF42a9jgvCz4ajitARcMm9j2xKzcSNwkgZjrconAsRphqCTG3qkfEXX2x7KaVHAqoUp4XgPZXZ5vlA==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@ast-grep/napi-win32-x64-msvc@0.42.3': + resolution: {integrity: sha512-1bl5eAbRGI5VRlKzXVPiq+sFJezh60O3ur6G+w8elA20uEZ7ik+WLQZu9T5tScFxOfCQQcjV9v4fQiaGNgHJsA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@ast-grep/napi@0.42.3': + resolution: {integrity: sha512-iWuH84nptrXpfrpYAsc93CD6JRnyRLYWFRzP9j1KRMK2hspVMBCbmfRyEoMd2B572dd2sSF3eXIQvY/CqIxYmQ==} + engines: {node: '>= 10'} + '@astrojs/check@0.9.9': resolution: {integrity: sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg==} hasBin: true @@ -2373,6 +2600,10 @@ packages: '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + '@boringnode/encryption@1.0.1': + resolution: {integrity: sha512-+UL9erFUngnxNEDdXSKCnGQA8D62C/x2vxLF5vc9zaX7T2JolV/bGjpZAvFLzlY2gIffHyw+KsqsEWod5+zV0w==} + engines: {node: '>=20.6'} + '@braintree/sanitize-url@6.0.4': resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==} @@ -6747,6 +6978,10 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -6788,6 +7023,9 @@ packages: '@popperjs/core@2.11.8': resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@poppinss/cliui@6.8.1': + resolution: {integrity: sha512-o/ssbwr+r6woG65rk9eFHnn9dVUphZr/Rk+4+05ENVMBWYpYhTJGdE9RobTG5JLFubvO4gWIyFeNlC+I4EM6eA==} + '@poppinss/colors@4.1.6': resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} @@ -6800,6 +7038,44 @@ packages: '@poppinss/exception@1.2.3': resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@poppinss/hooks@7.3.0': + resolution: {integrity: sha512-/H35z/bWqHg7085QOxWUDYMidx6Kl6b8kIyzIXlRYzWvsk1xm9hQOlXWdWEYch+Gmn8eL7tThx59MBj8BLxDrQ==} + + '@poppinss/macroable@1.1.2': + resolution: {integrity: sha512-FAVBRzzWhYP5mA3lCwLH1A0fKBqq5anyjGet90Z81aRK5c/+LTGUE1zJhZrErjaenBSOOI9BVUs3WVmotneFQA==} + + '@poppinss/matchit@3.2.0': + resolution: {integrity: sha512-9SoMICN+LMO7ZtMj2ja8N7RHlC4mmuv5WwIBXWjabMd2SyXE1dIydh29exlgm+dGMP84PjwvfJH1TmWL4qz1og==} + + '@poppinss/middleware@3.2.7': + resolution: {integrity: sha512-MZC0Z97ozSz+PpfyxUPUy/ImuthpqvBbY7qku7f4Q2maHz+2uXfchfO8OggXLS6zEJ078l+jpAHZ2rDIRdjeVg==} + + '@poppinss/multiparty@3.0.0': + resolution: {integrity: sha512-z9jchUzsv7E+7sa4tWHb0+95Byx7w0ydlPGxg3nzyb7h3QlRdeW8/QkU9SexUY4lsT12do93AfNBAhSuOoVqjA==} + + '@poppinss/object-builder@1.1.0': + resolution: {integrity: sha512-FOrOq52l7u8goR5yncX14+k+Ewi5djnrt1JwXeS/FvnwAPOiveFhiczCDuvXdssAwamtrV2hp5Rw9v+n2T7hQg==} + engines: {node: '>=20.6.0'} + + '@poppinss/prompts@3.1.6': + resolution: {integrity: sha512-cKHfkID6b3wl1kbHJJRC/pznQ3KnRVydyk7CE38NfTV3VS45BDYCxeZZ7bfDin71qMzITh18lKnu8iuLxBngHA==} + + '@poppinss/qs@6.15.0': + resolution: {integrity: sha512-QzfMhxrRB5EPeGz0l8hTwKZ5dFX6ed0aETGbuD369StCO8Ad3SW4wWBYamOK5IKeM/dfOeKaCwUZPTnGcj+jKg==} + engines: {node: '>=0.6'} + + '@poppinss/string@1.7.2': + resolution: {integrity: sha512-A182GLDfi36iDCbhDrHB0xzrPM1fO3GHnhCDIdadf8C6eycgct4m7zusbLwEh6GPaj2Pz5BVos7XK16w7tZ7wQ==} + + '@poppinss/types@1.2.1': + resolution: {integrity: sha512-qUYnzl0m9HJTWsXtr8Xo7CwDx6wcjrvo14bOVbIMIlKJCzKrm3LX55dRTDr1/x4PpSvKVgmxvC6Ly2YiqXKOvQ==} + + '@poppinss/utils@7.0.1': + resolution: {integrity: sha512-mveSvLI2YPC114mK5HCuSYfUtjpClf1wHG1VCqZJCp4U2ypPhIt62Iku5urh0kPAFvnvCVHx2bXBSH14qMTOlQ==} + + '@poppinss/validator-lite@2.1.2': + resolution: {integrity: sha512-UhSG1ouT6r67VbEFHK/8ax3EMZYHioew9PqGmEZjV41G15aPZi6cyhXtBVvF9xqkHMflA5V680k7bQzV0kfD5w==} + '@prisma/instrumentation@5.22.0': resolution: {integrity: sha512-LxccF392NN37ISGxIurUljZSh1YWnphO34V5a0+T7FVQG2u9bhAXRTJpgmQ3483woVhkraQZFF7cbRrpbw/F4Q==} @@ -7500,6 +7776,14 @@ packages: resolution: {integrity: sha512-rO92VvpgMc3kfiTjGT52LEtJ8Yc5kCWhZjLQ3LwlA4pSgPpQO7bVpYXParOD8Jwf+cVQECJo3yP/4I8aZtUQTQ==} engines: {node: '>=18'} + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + + '@sindresorhus/is@8.1.0': + resolution: {integrity: sha512-2SX/1jW6CIMAiebvVv5ZInoCEuWQmMyBoJXXGC6Vjakjp/fpxP5eHs7/V6WKuPEIbuK06+VpjH+vjLQhr98rDQ==} + engines: {node: '>=22'} + '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -7612,6 +7896,87 @@ packages: svelte: ^5.0.0 vite: ^6.3.0 || ^7.0.0 + '@swc/core-darwin-arm64@1.15.47': + resolution: {integrity: sha512-GsoMtan3ojGGMGFbl31mmRu5ctZ56re8grGE8mO/OHJ8O+JRkzod02fe7X6ZQ8JvamA3imkEkx/h3u+vsOgPgA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [darwin] + + '@swc/core-darwin-x64@1.15.47': + resolution: {integrity: sha512-leTi7Rx3KF4zcC637iqWgk9SoV8VXAD8ppQYXsep63px5A/UftOcxLN1pmr8Z1si/YvX90ompP/rHgpYkgwXWg==} + engines: {node: '>=10'} + cpu: [x64] + os: [darwin] + + '@swc/core-linux-arm-gnueabihf@1.15.47': + resolution: {integrity: sha512-hBqHuoWKKIsKmDBn9qVeWqj5GWZhtlcczVaqQmNRXsDfq+voR5CxKRfamA367QjJXtceYuliLFfEL8QsskRM2g==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux] + + '@swc/core-linux-arm64-gnu@1.15.47': + resolution: {integrity: sha512-TBxvRz+B4K205TWHHZxWVxkC2RFNP/Mz3PNcECBos5PsKwxjg3QSJzdoebr0VCf0Bfh8HOPldKxAP/8XkFe9gA==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-arm64-musl@1.15.47': + resolution: {integrity: sha512-3Yu3Uq/VgytqsPjTMbkPU1ExADytbdWbruJYhA584E9jrpE2Ki+R6VVPoZCeAVk1Cb7QxcRTgblw6bSa6a/R+w==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux] + + '@swc/core-linux-ppc64-gnu@1.15.47': + resolution: {integrity: sha512-wfdMi5IaOaNtmh2/6geRoxIdNfqylUZFdtzTKS655y1axWfIWyx7As74vv0wVdjeCIZ3WmCI9odDd4rUttXOSQ==} + engines: {node: '>=10'} + cpu: [ppc64] + os: [linux] + + '@swc/core-linux-s390x-gnu@1.15.47': + resolution: {integrity: sha512-3hHYBY0yx8Ez7GMRrkhXHQzMdR5IZA6Wq5Ee4svlgwvSECLpnAJ9+0AimEGUFDvuLwE7nV/2+PYe8+Nm4rvNcQ==} + engines: {node: '>=10'} + cpu: [s390x] + os: [linux] + + '@swc/core-linux-x64-gnu@1.15.47': + resolution: {integrity: sha512-TjfhjgP/jGCfFHYC3JQPhJA1HwErbIJ9JfREDc1KNkvY6P0LodCgKVIlQ5deeTbkG7ih3bF5PHJLuLpaZjdRyQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-linux-x64-musl@1.15.47': + resolution: {integrity: sha512-CQpS8Ge/avfjZd0UEwG/sds83Uu32deQXcV1Jo3jD0mmvQQqtYAjpsDZXugmheeAwmt+YIuoVtVHro8LMYHqsQ==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux] + + '@swc/core-win32-arm64-msvc@1.15.47': + resolution: {integrity: sha512-0W8IKHsUTYiT7G2RqtOoVWk+89yzZikIiDUb/sCK6BmQDBhN91hQSfyUtW12jhEWLzYgcfmisfsZrmZE+84U1A==} + engines: {node: '>=10'} + cpu: [arm64] + os: [win32] + + '@swc/core-win32-ia32-msvc@1.15.47': + resolution: {integrity: sha512-ZIp49d2Z4/ka2jO9otOg4hDvTdPmp86kVOgS2M5FCPI7eKKZ1W0boxWn+8XeZrfERtFGW0AlMRm4JhlJa7l3NA==} + engines: {node: '>=10'} + cpu: [ia32] + os: [win32] + + '@swc/core-win32-x64-msvc@1.15.47': + resolution: {integrity: sha512-2h8Iek95vnixkBRCo+H8p09+Q5ll2NgSMFrWTy0iKt7+/t+8/T5mBpiT6c0ZxSS7wcWjwZ9sGZkK70tTSYHdDw==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@swc/core@1.15.47': + resolution: {integrity: sha512-FbsO5JcfOjfH38W/rohBRBweJeERsAuIP4f377lmkmxTcq9exjtx4SkRuZY5CdfhR2CBVwDIJegBpJDffwNsOg==} + engines: {node: '>=10'} + peerDependencies: + '@swc/helpers': '>=0.5.17' + peerDependenciesMeta: + '@swc/helpers': + optional: true + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -7621,6 +7986,9 @@ packages: '@swc/helpers@0.5.5': resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} + '@swc/types@0.1.28': + resolution: {integrity: sha512-V6Mnml8v09QALx6K0elJ7o9K/MkVDtW3t6L+7Ou/JcWtb3xwId2AH4FeOceySd2JaO87IMw4+6vSZxLm34LPbw==} + '@szmarczak/http-timer@4.0.6': resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} @@ -7864,6 +8232,9 @@ packages: '@ts-morph/common@0.26.1': resolution: {integrity: sha512-Sn28TGl/4cFpcM+jwsH1wLncYq3FtN/BIpem+HOygfBWPT5pAeS5dB4VFVzV8FbnOKHpDLZmvAl4AjPEev5idA==} + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + '@tsconfig/node10@1.0.12': resolution: {integrity: sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==} @@ -8086,6 +8457,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/he@1.2.3': + resolution: {integrity: sha512-q67/qwlxblDzEDvzHhVkwc1gzVWxaNxeyHUBF4xElrvjL11O+Ytze+1fGpBHlr/H9myiBUaUXNnNPmBHxxfAcA==} + '@types/http-assert@1.5.6': resolution: {integrity: sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==} @@ -8192,6 +8566,9 @@ packages: '@types/pg@8.6.1': resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==} + '@types/pluralize@0.0.33': + resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} + '@types/pngjs@6.0.5': resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==} @@ -9659,6 +10036,10 @@ packages: resolution: {integrity: sha512-X/WM2UQs6VMHUtjUDnZTRI+i1crWteJySFzr9UpGoQa4WQffXVTTXuekjl7TjZRlcF2XfjgITT0HxZ9RnxeT0w==} engines: {node: '>=0.10.0'} + case-anything@3.1.2: + resolution: {integrity: sha512-wljhAjDDIv/hM2FzgJnYQg90AWmZMNtESCjTeLH680qTzdo0nErlCxOmgzgX4ZsZAtIvqHyD87ES8QyriXB+BQ==} + engines: {node: '>=18'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -9699,6 +10080,10 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + check-error@2.1.1: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} @@ -9761,6 +10146,10 @@ packages: resolution: {integrity: sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==} engines: {node: '>=10'} + cli-boxes@4.0.1: + resolution: {integrity: sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw==} + engines: {node: '>=18.20 <19 || >=20.10'} + cli-cursor@3.1.0: resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} engines: {node: '>=8'} @@ -9782,6 +10171,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cli-truncate@5.2.0: + resolution: {integrity: sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==} + engines: {node: '>=20'} + cli-width@3.0.0: resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==} engines: {node: '>= 10'} @@ -10894,6 +11287,10 @@ packages: file-type: '>= 20.0.0' typescript: '>= 5.0.0' + emittery@1.2.1: + resolution: {integrity: sha512-sFz64DCRjirhwHLxofFqxYQm6DCp6o0Ix7jwKQvuCHPn4GMRZNuBZyLPu9Ccmk/QSCAMZt6FOUqA8JZCQvA9fw==} + engines: {node: '>=14.16'} + emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -10992,6 +11389,9 @@ packages: error-stack-parser-es@1.0.5: resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + error-stack-parser-es@2.0.1: + resolution: {integrity: sha512-J36ntO+rMQVRuR/umlmxmfLi4TpWwtmTnHoJoXTiC2xDNazs0VDPKcX6pbhZMDd2HFtH9isMBJXMdbki+A++Pg==} + error-stack-parser@2.1.4: resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} @@ -11590,6 +11990,10 @@ packages: resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} engines: {node: '>=20'} + file-type@22.0.1: + resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==} + engines: {node: '>=22'} + file-uri-to-path@1.0.0: resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} @@ -11872,6 +12276,9 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + get-tsconfig@4.14.2: + resolution: {integrity: sha512-XpwZALwwl/BaKTAyC6+c5T8y6kCg2jk+XGqOVrKIQmW49pNypYLMRjCUXqa28tQgJlhS2RlzP7sc+Rx7W6qsfw==} + get-tsconfig@5.0.0-beta.4: resolution: {integrity: sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==} engines: {node: '>=20.20.0'} @@ -12347,6 +12754,10 @@ packages: resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==} engines: {node: '>=18'} + inflation@2.1.0: + resolution: {integrity: sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==} + engines: {node: '>= 0.8.0'} + inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -12923,6 +13334,9 @@ packages: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsonschema@1.5.0: + resolution: {integrity: sha512-K+A9hhqbn0f3pJX17Q/7H6yQfD/5OXgdrR5UE12gMXCiN9D5Xq2o5mddV2QEcX/bjla99ASsAAQUyMCCRWAEhw==} + jsonwebtoken@9.0.3: resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} engines: {node: '>=12', npm: '>=6'} @@ -14466,9 +14880,6 @@ packages: resolution: {integrity: sha512-ua1L4OgXSBdsu1FPb7F3tYH0F48a6kxvod4pLUlGY9COeJAJQNX/sNH2IiEmsxw7lqYiAwrdHMjz1FctOsyDQg==} engines: {node: '>=18'} - package-manager-detector@1.3.0: - resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} - package-manager-detector@1.6.0: resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==} @@ -14513,6 +14924,10 @@ packages: resolution: {integrity: sha512-OL/zLggRp8mFhKL0rNORUTR4yBYujK/uU+xZL+/0Rgm2QE4nLO9v8PzEweSJEbMGKmDRjJE4R3IMJlL2di4JeQ==} engines: {node: '>= 18'} + parse-imports@3.0.0: + resolution: {integrity: sha512-IwiqoJANa4O6M76LBWEvoS2iPIUqBOnKG1lV3/J0oVM6V2XjED+mYAXedEMX5xUglVjfGpZOfaEyuOUjBuUE4g==} + engines: {node: '>= 22'} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -14733,6 +15148,10 @@ packages: resolution: {integrity: sha512-+KD8hJtqQMYoTuL1bbGOqxb4z+nZkTAwVdNtWwe8Tc2xNbEmdJYIYoc6Qt0uF55e6YW6KuTHw1DjQ18gMhzepw==} engines: {node: '>=16.0.0'} + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + pngjs@3.4.0: resolution: {integrity: sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==} engines: {node: '>=4.0.0'} @@ -15139,6 +15558,10 @@ packages: pretty-error@2.1.2: resolution: {integrity: sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==} + pretty-hrtime@1.0.3: + resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} + engines: {node: '>= 0.8'} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -16046,6 +16469,10 @@ packages: speedometer@1.0.0: resolution: {integrity: sha512-lgxErLl/7A5+vgIIXsh9MbeukOaCb2axgQ+bKCdIE+ibNT4XNYGNCR1qFEGq6F+YDASXK3Fh/c5FgtZchFolxw==} + split-lines@3.0.0: + resolution: {integrity: sha512-d0TpRBL/VfKDXsk8JxPF7zgF5pCUDdBMSlEL36xBgVeaX448t+yGXcJaikUyzkoKOJ0l6KpMfygzJU9naIuivw==} + engines: {node: '>=12'} + split2@1.1.1: resolution: {integrity: sha512-cfurE2q8LamExY+lJ9Ex3ZfBwqAPduzOKVscPDXNCLLMvyaeD3DTz1yk7fVIs6Chco+12XeD0BB6HEoYzPYbXA==} @@ -16444,6 +16871,10 @@ packages: temporal-utils@1.0.1: resolution: {integrity: sha512-HAixuesxFQIUaQk3ptX2jhfO/FsOkgVkDDMawvp6n/fkB1q6BKfs3lURw9I+pK/2e2e/q/vrLrxmeqauBoyGMQ==} + tempura@0.4.1: + resolution: {integrity: sha512-NQ4Cs23jM6UUp3CcS5vjmyjTC6dtA5EsflBG2cyG0wZvP65AV26tJ920MGvTRYIImCY13RBpOhc7q4/pu+FG5A==} + engines: {node: '>=10'} + terminal-link@4.0.0: resolution: {integrity: sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA==} engines: {node: '>=18'} @@ -16452,6 +16883,10 @@ packages: resolution: {integrity: sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==} engines: {node: '>=20'} + terminal-size@4.0.1: + resolution: {integrity: sha512-avMLDQpUI9I5XFrklECw1ZEUPJhqzcwSWsyyI8blhRLT+8N1jLJWLWWYQpB2q2xthq8xDvjZPISVh53T/+CLYQ==} + engines: {node: '>=18'} + terracotta@1.1.0: resolution: {integrity: sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww==} engines: {node: '>=10'} @@ -16557,6 +16992,10 @@ packages: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} + tmp-cache@1.1.0: + resolution: {integrity: sha512-j040fkL/x+XAZQ9K3bKGEPwgYhOZNBQLa3NXEADUiuno9C+3N2JJA4bVPDREixp604G3/vTXWA3DIPpA9lu1RQ==} + engines: {node: '>=6'} + tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} @@ -16668,6 +17107,9 @@ packages: ts-morph@25.0.1: resolution: {integrity: sha512-QJEiTdnz1YjrB3JFhd626gX4rKHDLSjSVMvGGG4v7ONc3RBwa0Eei98G9AT9uNFDMtV54JyuXsFeC+OH0n6bXQ==} + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + ts-node@10.9.2: resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} hasBin: true @@ -16880,6 +17322,10 @@ packages: resolution: {integrity: sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ==} engines: {node: '>=18'} + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + ulid@3.0.2: resolution: {integrity: sha512-yu26mwteFYzBAot7KVMqFGCVpsF6g8wXfJzQUHvu1no3+rRRSFcSV2nKeYvNPLD2J4b08jYBDhHUjeH0ygIl9w==} hasBin: true @@ -18138,6 +18584,184 @@ packages: snapshots: + '@adonisjs/ace@14.1.0(youch@4.1.1)': + dependencies: + '@poppinss/cliui': 6.8.1 + '@poppinss/hooks': 7.3.0 + '@poppinss/macroable': 1.1.2 + '@poppinss/prompts': 3.1.6 + '@poppinss/utils': 7.0.1 + fastest-levenshtein: 1.0.16 + jsonschema: 1.5.0 + string-width: 8.2.2 + yargs-parser: 22.0.0 + youch: 4.1.1 + + '@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0)': + dependencies: + '@adonisjs/config': 6.1.0 + '@adonisjs/fold': 11.0.0 + '@poppinss/hooks': 7.3.0 + '@poppinss/macroable': 1.1.2 + '@poppinss/utils': 7.0.1 + glob-parent: 6.0.2 + tempura: 0.4.1 + optionalDependencies: + '@adonisjs/assembler': 8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3) + + '@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3)': + dependencies: + '@adonisjs/env': 7.0.0 + '@antfu/install-pkg': 1.1.0 + '@ast-grep/napi': 0.42.3 + '@poppinss/cliui': 6.8.1 + '@poppinss/hooks': 7.3.0 + '@poppinss/utils': 7.0.1 + chokidar: 5.0.0 + dedent: 1.7.2(babel-plugin-macros@3.1.0) + execa: 9.6.1 + fast-glob: 3.3.3 + fdir: 6.5.0(picomatch@4.0.5) + get-port: 7.2.0 + get-tsconfig: 4.14.2 + import-meta-resolve: 4.2.0 + junk: 4.0.1 + open: 11.0.0 + parse-imports: 3.0.0 + picomatch: 4.0.5 + pretty-hrtime: 1.0.3 + tmp-cache: 1.1.0 + ts-morph: 27.0.2 + typescript: 6.0.3 + transitivePeerDependencies: + - babel-plugin-macros + + '@adonisjs/bodyparser@11.0.4(@adonisjs/http-server@9.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/events@10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)(@adonisjs/logger@7.1.1)(@boringnode/encryption@1.0.1)(youch@4.1.1))': + dependencies: + '@adonisjs/http-server': 9.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/events@10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)(@adonisjs/logger@7.1.1)(@boringnode/encryption@1.0.1)(youch@4.1.1) + '@poppinss/macroable': 1.1.2 + '@poppinss/middleware': 3.2.7 + '@poppinss/multiparty': 3.0.0 + '@poppinss/qs': 6.15.0 + '@poppinss/utils': 7.0.1 + file-type: 22.0.1 + inflation: 2.1.0 + media-typer: 1.1.0 + raw-body: 3.0.2 + transitivePeerDependencies: + - supports-color + + '@adonisjs/config@6.1.0': + dependencies: + '@poppinss/utils': 7.0.1 + + '@adonisjs/core@7.4.0(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(youch@4.1.1)': + dependencies: + '@adonisjs/ace': 14.1.0(youch@4.1.1) + '@adonisjs/application': 9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0) + '@adonisjs/bodyparser': 11.0.4(@adonisjs/http-server@9.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/events@10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)(@adonisjs/logger@7.1.1)(@boringnode/encryption@1.0.1)(youch@4.1.1)) + '@adonisjs/config': 6.1.0 + '@adonisjs/env': 7.0.0 + '@adonisjs/events': 10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0) + '@adonisjs/fold': 11.0.0 + '@adonisjs/hash': 10.1.1 + '@adonisjs/health': 3.1.0 + '@adonisjs/http-server': 9.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/events@10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)(@adonisjs/logger@7.1.1)(@boringnode/encryption@1.0.1)(youch@4.1.1) + '@adonisjs/http-transformers': 2.3.1(@adonisjs/fold@11.0.0) + '@adonisjs/logger': 7.1.1 + '@adonisjs/repl': 5.0.0 + '@boringnode/encryption': 1.0.1 + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.7.0 + '@poppinss/macroable': 1.1.2 + '@poppinss/utils': 7.0.1 + '@sindresorhus/is': 8.1.0 + '@types/he': 1.2.3 + error-stack-parser-es: 2.0.1 + he: 1.2.0 + pretty-hrtime: 1.0.3 + string-width: 8.2.2 + optionalDependencies: + '@adonisjs/assembler': 8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3) + youch: 4.1.1 + transitivePeerDependencies: + - supports-color + + '@adonisjs/env@7.0.0': + dependencies: + '@poppinss/utils': 7.0.1 + '@poppinss/validator-lite': 2.1.2 + split-lines: 3.0.0 + + '@adonisjs/events@10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)': + dependencies: + '@adonisjs/application': 9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0) + '@adonisjs/fold': 11.0.0 + '@poppinss/utils': 7.0.1 + '@sindresorhus/is': 7.2.0 + emittery: 1.2.1 + + '@adonisjs/fold@11.0.0': + dependencies: + '@poppinss/utils': 7.0.1 + parse-imports: 3.0.0 + + '@adonisjs/hash@10.1.1': + dependencies: + '@phc/format': 1.0.0 + '@poppinss/utils': 7.0.1 + + '@adonisjs/health@3.1.0': + dependencies: + '@poppinss/utils': 7.0.1 + check-disk-space: 3.4.0 + + '@adonisjs/http-server@9.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/events@10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0)(@adonisjs/logger@7.1.1)(@boringnode/encryption@1.0.1)(youch@4.1.1)': + dependencies: + '@adonisjs/application': 9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0) + '@adonisjs/events': 10.2.0(@adonisjs/application@9.0.1(@adonisjs/assembler@8.4.0(babel-plugin-macros@3.1.0)(typescript@6.0.3))(@adonisjs/config@6.1.0)(@adonisjs/fold@11.0.0))(@adonisjs/fold@11.0.0) + '@adonisjs/fold': 11.0.0 + '@adonisjs/logger': 7.1.1 + '@boringnode/encryption': 1.0.1 + '@poppinss/macroable': 1.1.2 + '@poppinss/matchit': 3.2.0 + '@poppinss/middleware': 3.2.7 + '@poppinss/qs': 6.15.0 + '@poppinss/types': 1.2.1 + '@poppinss/utils': 7.0.1 + '@sindresorhus/is': 8.1.0 + content-disposition: 1.1.0 + cookie-es: 3.1.1 + destroy: 1.2.0 + encodeurl: 2.0.0 + etag: 1.8.1 + fresh: 0.5.2 + mime-types: 3.0.2 + on-finished: 2.4.1 + proxy-addr: 2.0.7 + tmp-cache: 1.1.0 + type-is: 2.1.0 + vary: 1.1.2 + optionalDependencies: + youch: 4.1.1 + + '@adonisjs/http-transformers@2.3.1(@adonisjs/fold@11.0.0)': + dependencies: + '@adonisjs/fold': 11.0.0 + '@poppinss/exception': 1.2.3 + '@poppinss/types': 1.2.1 + + '@adonisjs/logger@7.1.1': + dependencies: + '@poppinss/utils': 7.0.1 + abstract-logging: 2.0.1 + pino: 10.3.1 + + '@adonisjs/repl@5.0.0': + dependencies: + '@poppinss/colors': 4.1.6 + string-width: 8.2.2 + '@alinea/suite@0.6.3': {} '@alloc/quick-lru@5.2.0': {} @@ -18149,11 +18773,52 @@ snapshots: '@antfu/install-pkg@1.1.0': dependencies: - package-manager-detector: 1.3.0 + package-manager-detector: 1.6.0 tinyexec: 1.1.2 '@antfu/utils@8.1.1': {} + '@arr/every@1.0.1': {} + + '@ast-grep/napi-darwin-arm64@0.42.3': + optional: true + + '@ast-grep/napi-darwin-x64@0.42.3': + optional: true + + '@ast-grep/napi-linux-arm64-gnu@0.42.3': + optional: true + + '@ast-grep/napi-linux-arm64-musl@0.42.3': + optional: true + + '@ast-grep/napi-linux-x64-gnu@0.42.3': + optional: true + + '@ast-grep/napi-linux-x64-musl@0.42.3': + optional: true + + '@ast-grep/napi-win32-arm64-msvc@0.42.3': + optional: true + + '@ast-grep/napi-win32-ia32-msvc@0.42.3': + optional: true + + '@ast-grep/napi-win32-x64-msvc@0.42.3': + optional: true + + '@ast-grep/napi@0.42.3': + optionalDependencies: + '@ast-grep/napi-darwin-arm64': 0.42.3 + '@ast-grep/napi-darwin-x64': 0.42.3 + '@ast-grep/napi-linux-arm64-gnu': 0.42.3 + '@ast-grep/napi-linux-arm64-musl': 0.42.3 + '@ast-grep/napi-linux-x64-gnu': 0.42.3 + '@ast-grep/napi-linux-x64-musl': 0.42.3 + '@ast-grep/napi-win32-arm64-msvc': 0.42.3 + '@ast-grep/napi-win32-ia32-msvc': 0.42.3 + '@ast-grep/napi-win32-x64-msvc': 0.42.3 + '@astrojs/check@0.9.9(prettier@3.9.5)(typescript@6.0.3)': dependencies: '@astrojs/language-server': 2.16.12(prettier@3.9.5)(typescript@6.0.3) @@ -18564,6 +19229,10 @@ snapshots: '@borewit/text-codec@0.2.2': {} + '@boringnode/encryption@1.0.1': + dependencies: + '@poppinss/utils': 7.0.1 + '@braintree/sanitize-url@6.0.4': optional: true @@ -20860,7 +21529,7 @@ snapshots: yaml: 2.9.0 yargs: 17.7.2 - '@netlify/build@35.15.0(@opentelemetry/api@1.9.1)(@types/node@22.19.1)(picomatch@4.0.5)(rollup@4.60.2)': + '@netlify/build@35.15.0(@opentelemetry/api@1.9.1)(@swc/core@1.15.47)(@types/node@22.19.1)(picomatch@4.0.5)(rollup@4.60.2)': dependencies: '@bugsnag/js': 8.10.0 '@netlify/blobs': 10.7.9(supports-color@10.2.2) @@ -20909,7 +21578,7 @@ snapshots: string-width: 7.2.0 supports-color: 10.2.2 terminal-link: 4.0.0 - ts-node: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) + ts-node: 10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@5.9.3) typescript: 5.9.3 yaml: 2.9.0 yargs: 17.7.2 @@ -22412,7 +23081,7 @@ snapshots: dependencies: '@netlify/netlify-design-tokens': 7.1.0 '@tailwindcss/container-queries': 0.1.1(tailwindcss@3.4.17) - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3)) + tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3)) transitivePeerDependencies: - ts-node @@ -24020,6 +24689,8 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 + '@phc/format@1.0.0': {} + '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -24060,6 +24731,18 @@ snapshots: '@popperjs/core@2.11.8': {} + '@poppinss/cliui@6.8.1': + dependencies: + '@poppinss/colors': 4.1.6 + cli-boxes: 4.0.1 + cli-table3: 0.6.5 + cli-truncate: 5.2.0 + log-update: 7.2.0 + pretty-hrtime: 1.0.3 + string-width: 8.2.2 + supports-color: 10.2.2 + terminal-size: 4.0.1 + '@poppinss/colors@4.1.6': dependencies: kleur: 4.1.5 @@ -24078,6 +24761,50 @@ snapshots: '@poppinss/exception@1.2.3': {} + '@poppinss/hooks@7.3.0': {} + + '@poppinss/macroable@1.1.2': {} + + '@poppinss/matchit@3.2.0': + dependencies: + '@arr/every': 1.0.1 + + '@poppinss/middleware@3.2.7': {} + + '@poppinss/multiparty@3.0.0': + dependencies: + http-errors: 2.0.1 + + '@poppinss/object-builder@1.1.0': {} + + '@poppinss/prompts@3.1.6': + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/exception': 1.2.3 + '@poppinss/object-builder': 1.1.0 + enquirer: 2.4.1 + + '@poppinss/qs@6.15.0': {} + + '@poppinss/string@1.7.2': + dependencies: + '@types/pluralize': 0.0.33 + case-anything: 3.1.2 + pluralize: 8.0.0 + slugify: 1.6.9 + + '@poppinss/types@1.2.1': {} + + '@poppinss/utils@7.0.1': + dependencies: + '@poppinss/exception': 1.2.3 + '@poppinss/object-builder': 1.1.0 + '@poppinss/string': 1.7.2 + '@poppinss/types': 1.2.1 + flattie: 1.1.1 + + '@poppinss/validator-lite@2.1.2': {} + '@prisma/instrumentation@5.22.0': dependencies: '@opentelemetry/api': 1.9.1 @@ -24690,6 +25417,10 @@ snapshots: '@sindresorhus/is@7.1.1': {} + '@sindresorhus/is@7.2.0': {} + + '@sindresorhus/is@8.1.0': {} + '@sindresorhus/merge-streams@2.3.0': {} '@sindresorhus/merge-streams@4.0.0': {} @@ -24759,7 +25490,7 @@ snapshots: - react-native-b4a - supports-color - '@stackbit/cms-contentful@0.4.56(debug@4.4.1)(graphql@16.14.2)': + '@stackbit/cms-contentful@0.4.56(debug@4.4.1)': dependencies: '@contentful/rich-text-types': 15.15.1 '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) @@ -24773,7 +25504,6 @@ snapshots: - bare-buffer - debug - encoding - - graphql - react-native-b4a - supports-color @@ -24849,57 +25579,47 @@ snapshots: - supports-color - utf-8-validate - '@stackbit/cms-git@0.4.18(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': + '@stackbit/cms-git@0.4.18': dependencies: '@stackbit/artisanal-names': 1.0.1 - '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) + '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 '@stackbit/utils': 0.4.19 - axios: 0.24.0(debug@4.4.1) + axios: 0.24.0 fs-extra: 11.3.6 git-url-parse: 13.1.1 lodash: 4.18.1 micromatch: 4.0.8 slugify: 1.6.9 transitivePeerDependencies: - - '@types/react' - bare-abort-controller - bare-buffer - - bufferutil - - canvas - debug - encoding - - graphql - react-native-b4a - supports-color - - utf-8-validate - '@stackbit/cms-git@0.4.18(@types/react@19.1.8)(graphql@16.14.2)': + '@stackbit/cms-git@0.4.18(debug@4.4.1)': dependencies: '@stackbit/artisanal-names': 1.0.1 - '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) + '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 '@stackbit/utils': 0.4.19 - axios: 0.24.0 + axios: 0.24.0(debug@4.4.1) fs-extra: 11.3.6 git-url-parse: 13.1.1 lodash: 4.18.1 micromatch: 4.0.8 slugify: 1.6.9 transitivePeerDependencies: - - '@types/react' - bare-abort-controller - bare-buffer - - bufferutil - - canvas - debug - encoding - - graphql - react-native-b4a - supports-color - - utf-8-validate '@stackbit/cms-sanity@0.2.56(@types/react@19.1.8)': dependencies: @@ -24928,7 +25648,7 @@ snapshots: - supports-color - utf-8-validate - '@stackbit/cms-sanity@0.2.56(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': + '@stackbit/cms-sanity@0.2.56(@types/react@19.1.8)(debug@4.4.1)': dependencies: '@sanity/block-tools': 2.36.6 '@sanity/client': 3.4.1 @@ -24951,7 +25671,6 @@ snapshots: - canvas - debug - encoding - - graphql - react-native-b4a - supports-color - utf-8-validate @@ -24962,7 +25681,7 @@ snapshots: '@stackbit/artisanal-names': 1.0.1 '@stackbit/cms-contentful': 0.4.56 '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(graphql@16.14.2) + '@stackbit/cms-git': 0.4.18 '@stackbit/cms-sanity': 0.2.56(@types/react@19.1.8) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 @@ -24996,14 +25715,14 @@ snapshots: - supports-color - utf-8-validate - '@stackbit/dev-common@0.5.51(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': + '@stackbit/dev-common@0.5.51(@types/react@19.1.8)(debug@4.4.1)': dependencies: '@iarna/toml': 2.2.5 '@stackbit/artisanal-names': 1.0.1 - '@stackbit/cms-contentful': 0.4.56(debug@4.4.1)(graphql@16.14.2) + '@stackbit/cms-contentful': 0.4.56(debug@4.4.1) '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) - '@stackbit/cms-sanity': 0.2.56(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) + '@stackbit/cms-git': 0.4.18(debug@4.4.1) + '@stackbit/cms-sanity': 0.2.56(@types/react@19.1.8)(debug@4.4.1) '@stackbit/sdk': 2.0.1 '@stackbit/types': 1.0.1 '@stackbit/utils': 0.4.19 @@ -25032,7 +25751,6 @@ snapshots: - canvas - debug - encoding - - graphql - react-native-b4a - supports-color - utf-8-validate @@ -25040,8 +25758,8 @@ snapshots: '@stackbit/dev@1.0.35(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2)': dependencies: '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) - '@stackbit/dev-common': 0.5.51(@types/react@19.1.8)(debug@4.4.1)(graphql@16.14.2) + '@stackbit/cms-git': 0.4.18(debug@4.4.1) + '@stackbit/dev-common': 0.5.51(@types/react@19.1.8)(debug@4.4.1) '@stackbit/sdk': 2.0.1 axios: 0.25.0(debug@4.4.1) chalk: 4.1.2 @@ -25075,7 +25793,7 @@ snapshots: '@stackbit/dev@1.0.35(@types/react@19.1.8)(graphql@16.14.2)': dependencies: '@stackbit/cms-core': 2.0.1(@types/react@19.1.8)(graphql@16.14.2) - '@stackbit/cms-git': 0.4.18(@types/react@19.1.8)(graphql@16.14.2) + '@stackbit/cms-git': 0.4.18 '@stackbit/dev-common': 0.5.51(@types/react@19.1.8) '@stackbit/sdk': 2.0.1 axios: 0.25.0 @@ -25236,6 +25954,61 @@ snapshots: transitivePeerDependencies: - supports-color + '@swc/core-darwin-arm64@1.15.47': + optional: true + + '@swc/core-darwin-x64@1.15.47': + optional: true + + '@swc/core-linux-arm-gnueabihf@1.15.47': + optional: true + + '@swc/core-linux-arm64-gnu@1.15.47': + optional: true + + '@swc/core-linux-arm64-musl@1.15.47': + optional: true + + '@swc/core-linux-ppc64-gnu@1.15.47': + optional: true + + '@swc/core-linux-s390x-gnu@1.15.47': + optional: true + + '@swc/core-linux-x64-gnu@1.15.47': + optional: true + + '@swc/core-linux-x64-musl@1.15.47': + optional: true + + '@swc/core-win32-arm64-msvc@1.15.47': + optional: true + + '@swc/core-win32-ia32-msvc@1.15.47': + optional: true + + '@swc/core-win32-x64-msvc@1.15.47': + optional: true + + '@swc/core@1.15.47': + dependencies: + '@swc/counter': 0.1.3 + '@swc/types': 0.1.28 + optionalDependencies: + '@swc/core-darwin-arm64': 1.15.47 + '@swc/core-darwin-x64': 1.15.47 + '@swc/core-linux-arm-gnueabihf': 1.15.47 + '@swc/core-linux-arm64-gnu': 1.15.47 + '@swc/core-linux-arm64-musl': 1.15.47 + '@swc/core-linux-ppc64-gnu': 1.15.47 + '@swc/core-linux-s390x-gnu': 1.15.47 + '@swc/core-linux-x64-gnu': 1.15.47 + '@swc/core-linux-x64-musl': 1.15.47 + '@swc/core-win32-arm64-msvc': 1.15.47 + '@swc/core-win32-ia32-msvc': 1.15.47 + '@swc/core-win32-x64-msvc': 1.15.47 + optional: true + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.15': @@ -25247,13 +26020,18 @@ snapshots: '@swc/counter': 0.1.3 tslib: 2.8.1 + '@swc/types@0.1.28': + dependencies: + '@swc/counter': 0.1.3 + optional: true + '@szmarczak/http-timer@4.0.6': dependencies: defer-to-connect: 2.0.1 '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.17)': dependencies: - tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3)) + tailwindcss: 3.4.17(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3)) '@tailwindcss/node@4.1.11': dependencies: @@ -25485,6 +26263,12 @@ snapshots: minimatch: 9.0.5 path-browserify: 1.0.1 + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.17 + '@tsconfig/node10@1.0.12': {} '@tsconfig/node12@1.0.11': {} @@ -25759,6 +26543,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/he@1.2.3': {} + '@types/http-assert@1.5.6': {} '@types/http-cache-semantics@4.2.0': {} @@ -25875,6 +26661,8 @@ snapshots: pg-protocol: 1.10.3 pg-types: 2.2.0 + '@types/pluralize@0.0.33': {} + '@types/pngjs@6.0.5': dependencies: '@types/node': 24.3.0 @@ -27799,6 +28587,8 @@ snapshots: capture-stack-trace@1.0.2: {} + case-anything@3.1.2: {} + ccount@2.0.1: {} chai@5.3.3: @@ -27836,6 +28626,8 @@ snapshots: chardet@2.1.1: {} + check-disk-space@3.4.0: {} + check-error@2.1.1: {} chevrotain-allstar@0.3.1(chevrotain@11.0.3): @@ -27902,6 +28694,8 @@ snapshots: cli-boxes@3.0.0: {} + cli-boxes@4.0.1: {} + cli-cursor@3.1.0: dependencies: restore-cursor: 3.1.0 @@ -27927,6 +28721,11 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-truncate@5.2.0: + dependencies: + slice-ansi: 8.0.0 + string-width: 8.2.2 + cli-width@3.0.0: {} cli-width@4.1.0: {} @@ -29188,17 +29987,19 @@ snapshots: electron-to-chromium@1.5.343: {} - elysia@1.3.8(exact-mirror@0.1.3(@sinclair/typebox@0.34.38))(file-type@21.3.4)(typescript@6.0.3): + elysia@1.3.8(exact-mirror@0.1.3(@sinclair/typebox@0.34.38))(file-type@22.0.1)(typescript@6.0.3): dependencies: cookie: 1.0.2 exact-mirror: 0.1.3(@sinclair/typebox@0.34.38) fast-decode-uri-component: 1.0.1 - file-type: 21.3.4 + file-type: 22.0.1 typescript: 6.0.3 optionalDependencies: '@sinclair/typebox': 0.34.38 openapi-types: 12.1.3 + emittery@1.2.1: {} + emmet@2.4.11: dependencies: '@emmetio/abbreviation': 2.3.3 @@ -29291,6 +30092,8 @@ snapshots: error-stack-parser-es@1.0.5: {} + error-stack-parser-es@2.0.1: {} + error-stack-parser@2.1.4: dependencies: stackframe: 1.3.4 @@ -29998,7 +30801,7 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-svelte@3.11.0(eslint@9.32.0(jiti@2.6.1))(svelte@5.38.3)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)): + eslint-plugin-svelte@3.11.0(eslint@9.32.0(jiti@2.6.1))(svelte@5.38.3)(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@24.3.0)(typescript@5.9.2)): dependencies: '@eslint-community/eslint-utils': 4.7.0(eslint@9.32.0(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.3 @@ -30007,7 +30810,7 @@ snapshots: globals: 16.3.0 known-css-properties: 0.37.0 postcss: 8.5.6 - postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)) + postcss-load-config: 3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@24.3.0)(typescript@5.9.2)) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.2 svelte-eslint-parser: 1.3.1(svelte@5.38.3) @@ -30550,6 +31353,15 @@ snapshots: transitivePeerDependencies: - supports-color + file-type@22.0.1: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + file-uri-to-path@1.0.0: {} fill-range@7.1.1: @@ -30884,6 +31696,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-tsconfig@4.14.2: + dependencies: + resolve-pkg-maps: 1.0.0 + get-tsconfig@5.0.0-beta.4: dependencies: resolve-pkg-maps: 1.0.0 @@ -31486,6 +32302,8 @@ snapshots: index-to-position@1.2.0: {} + inflation@2.1.0: {} + inflight@1.0.6: dependencies: once: 1.4.0 @@ -32151,6 +32969,8 @@ snapshots: jsonpointer@5.0.1: {} + jsonschema@1.5.0: {} + jsonwebtoken@9.0.3: dependencies: jws: 4.0.1 @@ -33405,13 +34225,13 @@ snapshots: neotraverse@0.6.18: {} - netlify-cli@26.2.0(@types/node@22.19.1)(db0@0.3.4(@electric-sql/pglite@0.3.16)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.3.16)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(ioredis@5.10.1)(picomatch@4.0.5)(rollup@4.60.2): + netlify-cli@26.2.0(@swc/core@1.15.47)(@types/node@22.19.1)(db0@0.3.4(@electric-sql/pglite@0.3.16)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.3.16)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(ioredis@5.10.1)(picomatch@4.0.5)(rollup@4.60.2): dependencies: '@fastify/static': 9.3.0 '@netlify/ai': 0.4.2 '@netlify/api': 14.0.19 '@netlify/blobs': 10.7.9(supports-color@10.2.2) - '@netlify/build': 35.15.0(@opentelemetry/api@1.9.1)(@types/node@22.19.1)(picomatch@4.0.5)(rollup@4.60.2) + '@netlify/build': 35.15.0(@opentelemetry/api@1.9.1)(@swc/core@1.15.47)(@types/node@22.19.1)(picomatch@4.0.5)(rollup@4.60.2) '@netlify/build-info': 10.5.1 '@netlify/config': 24.6.0 '@netlify/dev': 4.18.9(db0@0.3.4(@electric-sql/pglite@0.3.16)(better-sqlite3@12.9.0)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260511.1)(@electric-sql/pglite@0.3.16)(@neondatabase/serverless@1.1.0)(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.6.1)(better-sqlite3@12.9.0)(bun-types@1.3.3)(mysql2@3.22.3(@types/node@22.19.1))(pg@8.22.0)(postgres@3.4.7))(mysql2@3.22.3(@types/node@22.19.1)))(ioredis@5.10.1)(rollup@4.60.2) @@ -34467,8 +35287,6 @@ snapshots: registry-url: 6.0.1 semver: 7.8.0 - package-manager-detector@1.3.0: {} - package-manager-detector@1.6.0: {} pako@1.0.11: {} @@ -34515,6 +35333,11 @@ snapshots: es-module-lexer: 1.7.0 slashes: 3.0.12 + parse-imports@3.0.0: + dependencies: + es-module-lexer: 1.7.0 + slashes: 3.0.12 + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -34717,6 +35540,8 @@ snapshots: pvutils: 1.1.5 tslib: 2.8.1 + pluralize@8.0.0: {} + pngjs@3.4.0: {} pngjs@6.0.0: {} @@ -34781,21 +35606,21 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.5.6 - postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2)): + postcss-load-config@3.1.4(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@24.3.0)(typescript@5.9.2)): dependencies: lilconfig: 2.1.0 yaml: 1.10.2 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.2(@types/node@24.3.0)(typescript@5.9.2) + ts-node: 10.9.2(@swc/core@1.15.47)(@types/node@24.3.0)(typescript@5.9.2) - postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3)): + postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3)): dependencies: lilconfig: 3.1.3 yaml: 2.9.0 optionalDependencies: postcss: 8.5.6 - ts-node: 10.9.2(@types/node@22.19.1)(typescript@6.0.3) + ts-node: 10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3) postcss-merge-longhand@7.0.6(postcss@8.5.17): dependencies: @@ -35053,6 +35878,8 @@ snapshots: lodash: 4.18.1 renderkid: 2.0.7 + pretty-hrtime@1.0.3: {} + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -36274,6 +37101,8 @@ snapshots: speedometer@1.0.0: {} + split-lines@3.0.0: {} + split2@1.1.1: dependencies: through2: 2.0.5 @@ -36634,7 +37463,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3)): + tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -36653,7 +37482,7 @@ snapshots: postcss: 8.5.6 postcss-import: 15.1.0(postcss@8.5.6) postcss-js: 4.0.1(postcss@8.5.6) - postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3)) + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3)) postcss-nested: 6.2.0(postcss@8.5.6) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -36750,6 +37579,8 @@ snapshots: temporal-utils@1.0.1: {} + tempura@0.4.1: {} + terminal-link@4.0.0: dependencies: ansi-escapes: 7.3.0 @@ -36760,6 +37591,8 @@ snapshots: ansi-escapes: 7.3.0 supports-hyperlinks: 4.5.0 + terminal-size@4.0.1: {} + terracotta@1.1.0(solid-js@1.9.12): dependencies: solid-js: 1.9.12 @@ -36838,8 +37671,8 @@ snapshots: tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinyglobby@0.2.17: dependencies: @@ -36858,6 +37691,8 @@ snapshots: titleize@3.0.0: {} + tmp-cache@1.1.0: {} + tmp-promise@3.0.3: dependencies: tmp: 0.2.7 @@ -36958,7 +37793,12 @@ snapshots: '@ts-morph/common': 0.26.1 code-block-writer: 13.0.3 - ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3): + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + + ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -36975,8 +37815,10 @@ snapshots: typescript: 5.9.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.47 - ts-node@10.9.2(@types/node@22.19.1)(typescript@6.0.3): + ts-node@10.9.2(@swc/core@1.15.47)(@types/node@22.19.1)(typescript@6.0.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -36993,9 +37835,11 @@ snapshots: typescript: 6.0.3 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.47 optional: true - ts-node@10.9.2(@types/node@24.3.0)(typescript@5.9.2): + ts-node@10.9.2(@swc/core@1.15.47)(@types/node@24.3.0)(typescript@5.9.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.12 @@ -37012,6 +37856,8 @@ snapshots: typescript: 5.9.2 v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.15.47 optional: true tsconfig-paths@3.15.0: @@ -37021,7 +37867,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsdown@0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)): + tsdown@0.22.0(tsx@4.21.0)(typescript@6.0.3)(unrun@0.2.34): dependencies: ansis: 4.3.0 cac: 7.0.0 @@ -37214,6 +38060,8 @@ snapshots: uint8array-extras@1.4.0: {} + uint8array-extras@1.5.0: {} + ulid@3.0.2: {} ultrahtml@1.6.0: {} @@ -37405,7 +38253,7 @@ snapshots: dependencies: '@jridgewell/remapping': 2.3.5 acorn: 8.16.0 - picomatch: 4.0.4 + picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 unplugin@3.0.0: @@ -37817,8 +38665,8 @@ snapshots: vite@7.3.2(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 postcss: 8.5.19 rollup: 4.60.2 tinyglobby: 0.2.16 @@ -37834,8 +38682,8 @@ snapshots: vite@7.3.2(@types/node@24.3.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.9.0): dependencies: esbuild: 0.27.7 - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 postcss: 8.5.19 rollup: 4.60.2 tinyglobby: 0.2.16 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 2b9e0e470..2d87e6c84 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,4 +1,5 @@ packages: +- packages/adonisjs - packages/amqp - packages/astro - packages/backfill @@ -59,6 +60,8 @@ allowBuilds: workerd: true catalog: + "@adonisjs/assembler": ^8.0.0 + "@adonisjs/core": ^7.0.0 "@astrojs/netlify": ^6.6.5 "@cloudflare/workers-types": ^4.20260511.1 "@netlify/database": ^1.1.0