diff --git a/.agents/skills/add-to-fedify-init/SKILL.md b/.agents/skills/add-to-fedify-init/SKILL.md new file mode 100644 index 000000000..33dcfb0a9 --- /dev/null +++ b/.agents/skills/add-to-fedify-init/SKILL.md @@ -0,0 +1,129 @@ +--- +name: add-to-fedify-init +description: >- + This skill is used to add an integration package to the @fedify/init + package so that users can select the new framework via the `fedify init` + command, and to test it with `mise test:init`. +argument-hint: "Provide the name of the web framework to register in @fedify/init." +--- + + + +Adding an integration package to `@fedify/init` +=============================================== + +Follow these steps in order to register the integration package in +`@fedify/init` and verify it works. + +1. Add to `@fedify/init` +2. Test with `mise test:init` +3. Lint, format, and final checks + + +Add to `@fedify/init` +--------------------- + +Add the new package to the `@fedify/init` package so users can select the +new framework via the `fedify init` command. Follow these steps. + +Steps may require code modifications not explicitly listed. For example, +if the new package needs specific configuration, utility functions in +`packages/init/src/webframeworks/utils.ts` may need updating. Make +modifications consistent with the existing code style and context. + +### Write the `WebFrameworkDescription` object + +Create a `packages/init/src/webframeworks/framework.ts` file and write the +`WebFrameworkDescription` object, referring to . Check +the specifications in the comments in `packages/init/src/types.ts` for +details. + +### Add to the `WEB_FRAMEWORK` array + +Add the new framework name to the end of the `WEB_FRAMEWORK` array in +`packages/init/src/const.ts`. + +~~~~ typescript +export const WEB_FRAMEWORK = [ + // ... other frameworks + "framework", // Fill with the framework name +]; +~~~~ + +### Add to the `webFrameworks` object + +Add the new `WebFrameworkDescription` object in alphabetical order to the +`webFrameworks` object in `packages/init/src/webframeworks/mod.ts`. + +~~~~ typescript +// packages/init/src/webframeworks/mod.ts + +// ... other imports +import framework from "./framework.ts"; // Fill with the framework name + +const webFrameworks: Record = { + // ... other frameworks + framework, // Fill with the framework name +}; +~~~~ + +### Add templates in `packages/init/src/templates/framework/` + +If additional files need to be generated, add template files under the +`packages/init/src/templates/framework/` directory. Template files must +end with the `.tpl` extension appended to their base name. Then, in +`packages/init/src/webframeworks/framework.ts`, load the templates using +the `readTemplate` function defined in `packages/init/src/lib.ts` and add +them to the `WebFrameworkDescription.init().files` object. + + +Test with `mise test:init` +-------------------------- + +Run `mise test:init` to verify that the new package is generated and runs +correctly. If a test fails, the output and error file paths are printed; +read them to diagnose the issue. + +Running `mise test:init` without arguments tests all option combinations +and can take a very long time. Use appropriate options to narrow the test +scope. + +Immediately remove test paths after completing the tests and analyzing any +resulting errors. + +At a minimum, test the following three combinations. + + - `mise test:init -w framework -m in-process -k in-memory --no-dry-run`: + Tests the new framework with the in-memory KV store and in-process message + queue, which are the most basic options. This combination verify that the + newly created package can be used without issues by minimizing dependencies + on other environments. + - `mise test:init -w framework`: Tests all package manager, KV store, + and message queue combinations with the framework selected. If a + required database is not installed or running, this combinations are + useless. Therefore, if the test output indicates that the databases are + not running, don't use this combination ever again for the session. + Instead, use the previous one or the next one. + - `mise test:init -m in-process -k in-memory --no-dry-run`: Fixes the + KV store and message queue and tests all web framework and package + manager combinations. This test is mandatory if you modified logic + beyond just writing the `WebFrameworkDescription` object. + +For details on options, run `mise test:init --help`. + +Some frameworks or combinations may be untestable. Analyze the test +results; if there are impossible combinations, identify the reason and add +the combination and reason as a key-value pair to the +`BANNED_LOOKUP_REASONS` object in +`packages/init/src/test/lookup.ts`. + + +Lint, format, and final checks +------------------------------ + +Add keywords related to the framework in `.hongdown.toml` and `cspell.json` in +root path. + +After implementation, run `mise run fmt && mise check`. +If there are lint or format errors, fix them and run the command again until +there are no errors. diff --git a/.agents/skills/add-to-fedify-init/init/framework.ts b/.agents/skills/add-to-fedify-init/init/framework.ts new file mode 100644 index 000000000..b61358331 --- /dev/null +++ b/.agents/skills/add-to-fedify-init/init/framework.ts @@ -0,0 +1,58 @@ +// packages/init/src/webframeworks/프레임워크.ts +// The import paths are written based on the files in +// `packages/init/src/webframeworks/` where the actual files must exist, +// so do not modify them unless necessary. + +import deps from "../json/deps.json" with { type: "json" }; +import type { WebFrameworkDescription } from "../types.ts"; +import { defaultDenoDependencies, defaultDevDependencies } from "./const.ts"; +import { getInstruction } from "./utils.ts"; + +const frameworkDescription: WebFrameworkDescription = { + label: "프레임워크", // Fill 프레임워크 with the official framework name + packageManagers: [ + // List the package managers that support this framework, + // the list should be a subset of `PACKAGE_MANAGER` from `../const.ts`. + // If the framework is compatible with all package managers, + // you can just use `packageManagers: PACKAGE_MANAGER`. + ], + defaultPort: 0, // Fill in the default port of the framework + init: ({ + // Destructure necessary parameters from the argument + packageManager: pm, + }) => ({ + command: [ + // Optional shell command to run before scaffolding e.g., `create-next-app`. + // Split the command into an array of command and arguments, + // e.g., `["npx", "create-next-app@latest"]`. + ], + dependencies: pm === "deno" + ? { + // Use `deps.json` for version numbers, + // e.g., `"@fedify/프레임워크": deps["@fedify/프레임워크"]`. + ...defaultDenoDependencies, + } + : { + // Use `deps.json` for version numbers, + // e.g., `"@fedify/프레임워크": deps["@fedify/프레임워크"]`. + }, + devDependencies: { + // Use `deps.json` for version numbers, + // e.g., `"@fedify/프레임워크": deps["@fedify/프레임워크"]`. + ...defaultDevDependencies, + }, + federationFile: "**/federation.ts", + loggingFile: "**/logging.ts", + tasks: { + // If `command` create a project with `tasks` in `deno.json` (or `script`s in + // `package.json`) to run application, this could be unnecessary. + // In the tasks of the finally generated application, at least include + // a `dev` task to run the development server. `dev` task is used by + // `mise test:init` to run tests against the generated project. + // For Node.js/Bun, `lint: "eslint ."` is needed. + }, + instruction: getInstruction(pm, 0 /* Replace with default port */), + }), +}; + +export default frameworkDescription; diff --git a/.agents/skills/create-example-app-with-integration/ARCHITECTURE.md b/.agents/skills/create-example-app-with-integration/ARCHITECTURE.md new file mode 100644 index 000000000..92a4d646b --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/ARCHITECTURE.md @@ -0,0 +1,191 @@ + + +Fedify example architecture +=========================== + +This document defines the shared architecture for Fedify example applications. +Every example should follow these conventions regardless of the web framework +used, so that learners can compare examples and transfer knowledge between them. + + +Middleware integration +---------------------- + +Every Fedify framework adapter exposes a middleware or hook function that +intercepts incoming requests. Register this middleware at the top level of +the server so that it runs before any application routes. + +The middleware inspects the `Accept` and `Content-Type` headers. Requests +carrying ActivityPub media types (`application/activity+json`, +`application/ld+json`, etc.) or targeting well-known federation endpoints +are forwarded to the `Federation` instance. All other requests fall through +to the application's own routes. + +The specific API differs, but the role is identical: delegate federation +traffic to Fedify, let everything else pass through. + + +Reverse proxy support +--------------------- + +If needed, wrap the middleware (or the request handler it receives) with +`getXForwardedRequest` from the `x-forwarded-fetch` package. This rewrites +the request URL to respect `X-Forwarded-Host` and related headers, which is +required when the server runs behind a tunneling tool or reverse proxy during +local development. Apply this wrapping at the same level as the Fedify +middleware registration, before any routing logic executes. + + +Routing +------- + +### `GET /` + +The main page. Contains the following sections: + +**Search** + +A text input for searching fediverse accounts by handle. The client +debounces input with a 300ms delay, then sends a `GET` request with the +handle as a URL query parameter (e.g. `/?q=@user@example.com`). The server +resolves the handle using Fedify's `lookupObject` and returns the result. +The result shows: profile image, display name, handle, and a follow button. +If the local actor already follows the target, show an unfollow button +instead. + +**User info** + +Displays the local actor's profile. Because this is a demo there is exactly +one actor, `@demo`. + + - Profile image: `/demo-profile.png` + - Name: `"Fedify Demo"` + - Handle: `@demo` + - Summary: `"This is a Fedify Demo account."` + +**Following** + +Lists accounts the local actor follows. Shows the total count and, for +each account: profile image, display name, handle, and an unfollow button. + +**Followers** + +Lists accounts that follow the local actor. Shows the total count and, for +each account: profile image, display name, and handle. + +The following and followers sections update in real time via SSE (see below). + +**Compose** + +A text area and a submit button for writing a new post. On submission the +server creates a `Note`, stores it in `postStore`, wraps it in a `Create` +activity, and sends it to followers. If sending fails, the post is removed +from the store. + +**Posts** + +Lists all posts by the local actor in reverse chronological order. Each +entry shows the post content, published timestamp, and a link to the +single post detail page (`/users/{identifier}/posts/{id}`). + +### `GET /users/{identifier}` + +Actor profile page. Shares its path with the Fedify actor dispatcher. +When a federation peer requests this URL with an ActivityPub media type, the +middleware handles it. Otherwise the request falls through to this route, +which renders an HTML page showing: + + - Profile image + - Name + - Handle + - Summary + - Following count + - Followers count + +### `GET /users/{identifier}/posts/{id}` + +Single post detail page. Shares its path with the Fedify `Note` object +dispatcher. Same content-negotiation fallback as the actor profile: the +middleware serves ActivityPub JSON to federation peers, and this route +renders HTML for browsers. Shows: + + - Author profile (same layout as the actor profile page) + - Post content + - Published timestamp + +### `POST /post` + +Accepts post content from the compose form, creates a `Note`, stores it in +`postStore`, wraps it in a `Create` activity, and sends it to followers. +If sending fails, the post is removed from the store. Redirects back to +`/` on completion. + +### `POST /follow` + +Accepts a target actor URI, sends a `Follow` activity from the local actor, +and stores the relationship locally. + +### `POST /unfollow` + +Accepts a target actor URI, sends an `Undo(Follow)` activity, and removes +the relationship locally. + +### `GET /events` + +SSE endpoint. See the SSE section below. + + +Server-sent events +------------------ + +The `/events` endpoint keeps an open SSE connection to the client. +When the following or followers list changes (a follow is accepted, a +remote follow arrives, an unfollow occurs, etc.), the server pushes an +event so the page can update without a full reload. + +The server maintains a set of active SSE connections. Whenever the +follower or following store is mutated — inside inbox listeners or after a +local follow/unfollow request — it broadcasts an event to every open +connection. + +The client listens on an `EventSource` and replaces the relevant DOM +section with the received data. + + +Server-side data access +----------------------- + +Use Fedify's `RequestContext` to bridge between the framework routing layer +and the federation layer. Obtain a context by calling +`federation.createContext(request, contextData)` inside a route handler. +Through this context, routes can look up actors, resolve object URIs, and +invoke `sendActivity` without coupling to Fedify internals. + +Avoid accessing the data stores directly from route handlers when a +`RequestContext` method exists for the same purpose. This keeps the +routing layer thin and ensures that Fedify's internal bookkeeping (key +resolution, URI canonicalization, etc.) is applied consistently. + + +Federation +---------- + +Use `src/federation.ts`. + + +Storing +------- + +Use `src/store.ts` and the provided in-memory stores. + + +View rendering +-------------- + +See `DESIGN.md`. + + +Logging +------- + +Use `@logtape/logtape` and `src/logging.ts`. diff --git a/.agents/skills/create-example-app-with-integration/DESIGN.md b/.agents/skills/create-example-app-with-integration/DESIGN.md new file mode 100644 index 000000000..db197c63c --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/DESIGN.md @@ -0,0 +1,295 @@ +Fedify example design system +============================ + +Visual theme & atmosphere +------------------------- + +Clean, functional, and developer-friendly. The aesthetic is minimal and +modern: a neutral canvas with a single bold gradient accent for profile +sections. The overall feel is “documentation site meets social app” — +sparse enough to read comfortably, colorful enough to feel alive. + +Dark and light themes are mandatory. The system detects +`prefers-color-scheme` and switches automatically; there is no manual +toggle. + +**Key Characteristics:** + + - Neutral canvas that inverts cleanly between light and dark + - Single gradient accent (purple) reserved for the profile header and + primary actions + - Card-based content layout with subtle shadows + - Monospace typesetting for handles and federation addresses + - No framework-specific branding in UI chrome — only the demo profile + and Fedify logo + + +Color palette & roles +--------------------- + +### Surface & background + +Two CSS custom properties control the entire theme inversion. +`--background` is `#ffffff` in light mode and `#0a0a0a` in dark mode. +`--foreground` is `#171717` in light mode and `#ededed` in dark mode. +All other surface colors derive from these two tokens through `rgba()` +or `color-mix()`. + +### Accent & brand + +Link text and input focus rings use `#3b82f6`. The focus ring shadow is +`rgba(59, 130, 246, 0.3)`. The profile gradient is +`linear-gradient(135deg, #667eea, #764ba2)` and applies exclusively to +the profile header background and the primary action button. + +### Neutral & semantic + +Handle badges and follower items use `#f3f4f6` background with `#000` +text. Card borders use `rgba(0, 0, 0, 0.05)` for subtle dividers and +`rgba(0, 0, 0, 0.1)` for post cards and forms. Textarea borders are +`rgba(0, 0, 0, 0.2)`. Post avatar rings are `#e5e7eb`. Text on +gradient backgrounds is always `white`. + +### Shadow system + +Four elevation levels. Elevation 1 (`0 2px 8px rgba(0,0,0,0.05)`) for +post cards and forms. Elevation 2 (`0 4px 20px rgba(0,0,0,0.08)`) for +info cards and detail cards. Elevation 3 +(`0 8px 32px rgba(0,0,0,0.1)`) for the profile header. Hover lift +(`0 8px 24px rgba(0,0,0,0.1)`) for card hover states. + + +Typography rules +---------------- + +### Font family + +Body text uses the system font stack: +`-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif`. +No custom fonts or font loading. Monospace (`monospace`) is reserved for +federation handles and follower addresses. + +### Hierarchy + +Base font size is `16px`. Line height for readable content is `1.625`. + + - **User name** (profile header): `2.25rem`, weight `700` + - **Section title** (card headings, posts title): `1.5rem`, + weight `600`—`700` + - **User handle** (profile header): `1.25rem`, weight `500` + - **Body large** (bio, post detail content): `1.125rem`, weight `400`, + line-height `1.625` + - **Body** (post content, forms): `1rem`, weight `400`, + line-height `1.625` + - **Label** (info labels, metadata, timestamps): `0.875rem`, + weight `600` + +### Principles + +Weight `600`—`700` for headings; `400`—`500` for body and subtext. +Opacity (`0.6`—`0.9`) creates text hierarchy on gradient backgrounds +instead of color changes. + + +Component stylings +------------------ + +### Profile header + +Background is the profile gradient. Text is `white`. Padding `2rem`, +gap `2rem` between avatar and info. Border radius `1rem`. Shadow is +elevation 3. Layout is `flex row`, collapsing to `column` on mobile. + +### Avatar + +Profile header avatar is `7.5rem` square, `border-radius: 50%`, with a +`4px solid rgba(255,255,255,0.2)` border. Post card avatars are `3rem` +with a `2px solid #e5e7eb` border. Post detail avatars are `4rem` with +the same border. + +### Cards + +Background is `var(--background)`. Border is +`1px solid rgba(0,0,0,0.1)`. Border radius is `0.75rem`. Shadow is +elevation 1 or 2 depending on context. Post cards gain +`translateY(-2px)` and hover-lift shadow on hover. Transitions are +`transform 0.2s, box-shadow 0.2s`. + +### Search input + +Full-width text input with `border-radius: 0.5rem` and +`padding: 0.75rem`. Border is `1px solid rgba(0,0,0,0.2)`. On focus +the border becomes `#3b82f6` with a `0 0 0 2px` focus ring shadow. +Placeholder text reads “Search by handle (e.g. @user@example.com)”. + +### Search result + +Inline card below the search input. Shows a row with the target's +avatar (`3rem`, rounded), display name, handle (monospace badge), and an +action button (follow or unfollow depending on current state). Appears +only when a result is available; hidden otherwise. + +### Compose form + +Textarea is full width with `border-radius: 0.5rem` and +`padding: 0.75rem`. On focus the border becomes `#3b82f6` with a +`0 0 0 2px` focus ring shadow. Font inherits body font family. Resize +is vertical only. A submit button sits below the textarea, right-aligned. + +### Buttons (primary) + +Background is the profile gradient. Text is `white`. +Padding `0.5rem 1.5rem`. Border radius `0.5rem`. Font weight `600`. +On hover the button lifts `translateY(-1px)` and the shadow intensifies. +Transition matches cards. + +### Buttons (danger) + +Used for unfollow actions. Background is `#ef4444`. Text is `white`. +Same padding, radius, weight, and hover behavior as primary buttons. + +### Back link + +Background is `color-mix(in srgb, var(--foreground) 10%, transparent)`. +Padding `0.5rem 1rem`. Border radius `0.5rem`. Hover increases the +foreground mix to 15%. + +### Fedify badge + +Background `#7dd3fc`, text `white`, height `1.5rem`, +border radius `0.375rem`. A `::before` pseudo-element renders a 16x16 +Fedify logo icon. + + +Layout principles +----------------- + +### Spacing + +Base unit is `1rem` (16px). Common spacing values are `0.25rem`, +`0.5rem`, `0.75rem`, `1rem`, `1.5rem`, and `2rem`. Section gaps range +from `1.5rem` to `2rem`. + +### Containers + +Profile, posts, and post detail containers max out at `56rem` with +`2rem` padding. The home page container maxes at `780px` with `1rem` +padding. Profile container stretches to `min-height: 100vh`. + +### Grid + +All grids are single-column. Post grid gap is `1.5rem`. Info grid gap +is `1rem`. Home grid gap is `1rem`. Profile content grid gap is `2rem`. + +### Whitespace + +Generous vertical breathing room between sections. Card internal +padding is `1.5rem`—`2rem`. Dividers are bottom borders on list items +(`1px solid rgba(0,0,0,0.05)`); the last item has no border. + + +Responsive behavior +------------------- + +Single breakpoint at `768px`. + +Below the breakpoint: + + - Profile header switches to `flex-direction: column`, + `align-items: center`, `text-align: center` + - User name shrinks from `2.25rem` to `1.875rem` + - Container padding reduces from `2rem` to `1rem` + - Post detail card padding: `2rem` to `1.5rem` + - Author avatar: `4rem` to `3.5rem` + - Author name: `1.5rem` to `1.25rem` + - Post detail content: `1.125rem` to `1rem` + - Info items stack vertically with `0.25rem` gap + +No hover-dependent information. Hover effects add visual polish only. +Tap targets meet at least `44px` effective height through padding. + + +Do's and don'ts +--------------- + +### Do + + - Use CSS custom properties (`--background`, `--foreground`) for + theme-dependent values. + - Detect theme via `prefers-color-scheme`; apply class (`light`/`dark`) + on `` at runtime. + - Keep all layout in a single static CSS file under *public/*. + - Use `rgba()` or `color-mix()` for transparent overlays rather than + hard-coded gray values. + - Render handles and federation addresses in monospace with a light gray + badge background. + - Provide *demo-profile.png* and *fedify-logo.svg* in *public/*. + - Maintain the gradient accent exclusively for profile headers and + primary action buttons. + +### Don't + + - Don't add a CSS framework (Tailwind, Bootstrap). The example must + stay dependency-free on the styling side so that any framework + integration can adopt it. + - Don't introduce custom fonts or font loading. + - Don't use JavaScript for layout or styling beyond the dark-mode class + toggle. + - Don't create multiple themes or color schemes beyond light/dark. + - Don't use the gradient accent on secondary elements (back links, info + cards, text). + - Don't add animations beyond the card hover lift and button press + feedback. + + +Static assets +------------- + +All visual assets live in *public/* and are served at the site root: + + - *style.css* — Complete stylesheet + - *theme.js* — Dark/light class toggle script + - *demo-profile.png* — Demo actor avatar + - *fedify-logo.svg* — Fedify logo for badge and branding + +### Following / followers list + +Each list is a vertical stack of rows. Each row contains an avatar +(`3rem`, rounded), display name, and handle (monospace badge). Following +rows additionally include an unfollow button (danger style). A count +label sits above each list (e.g. “Following (3)”). When the list is +empty, show a single line of muted text. Both lists update in real time +via SSE without page reload. + + +Page structure +-------------- + +Every example must implement these pages. See *ARCHITECTURE.md* for the +full routing specification. + +### Home (`/`) + +Top to bottom: + +1. **Search** — text input with debounced lookup; result card appears + inline below +2. **User info** — profile header (gradient, avatar, name, handle, + bio) +3. **Following** — count + account list with unfollow buttons +4. **Followers** — count + account list +5. **Compose** — textarea + submit button +6. **Posts** — reverse-chronological post cards, each linking to the + detail page + +### Actor profile (`/users/{identifier}`) + +Profile header (gradient, avatar, name, handle, bio) followed by +following count and followers count. Content-negotiated: serves HTML to +browsers and ActivityPub JSON to federation peers. + +### Post detail (`/users/{identifier}/posts/{id}`) + +Back link to home, then author profile section (same layout as the actor +profile page), then the post content and a formatted timestamp. +Content-negotiated like the actor profile. diff --git a/.agents/skills/create-example-app-with-integration/SKILL.md b/.agents/skills/create-example-app-with-integration/SKILL.md new file mode 100644 index 000000000..4ec3a185d --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/SKILL.md @@ -0,0 +1,154 @@ +--- +name: create-example-app-with-integration +description: >- + This skill is used to create an example application for a web framework + integration package and to test it with `mise test:examples`. +argument-hint: "Provide the name of the web framework to create an example for." +--- + + + +Creating an example for an integration package +============================================== + +Follow these steps in order to create the example application and verify +it works. + +1. Set up the example project +2. Implement the example app +3. Test the example with `mise test:examples` +4. Lint, format, and final checks + + +Reference documents +------------------- + +Two reference documents describe what the example must do and how it must +look. Both are references only — do not create these files in the actual +generated example app. + +### + +Defines the example's functional behavior. Consult it for: + + - **Middleware integration**: How to register the Fedify middleware so it + intercepts ActivityPub requests before application routes. + - **Reverse proxy support**: When and how to apply + `getXForwardedRequest` from `x-forwarded-fetch`. + - **Routing**: The complete list of routes (`GET /`, `GET /users/…`, + `POST /post`, `POST /follow`, `POST /unfollow`, `GET /events`, etc.) + with their expected request/response behavior. + - **Server-sent events**: How the `/events` endpoint keeps an open SSE + connection and broadcasts changes to the client. + - **Server-side data access**: How to use Fedify's `RequestContext` to + bridge between the framework routing layer and the federation layer. + - **Federation** and **Storing**: Which source files to set up + (`src/federation.ts`, `src/store.ts`) and the template files they are + based on (, ). + - **Logging**: How to use `@logtape/logtape` and `src/logging.ts`. + +### + +Defines the example's visual presentation. Consult it for: + + - **Visual theme & atmosphere**: Light/dark theme with + `prefers-color-scheme` detection. + - **Color palette & roles**: Surface, accent, neutral, and shadow tokens. + - **Typography rules**: Font family, size hierarchy, and weight + principles. + - **Component stylings**: Profile header, avatar, cards, search input, + compose form, buttons, back link, and Fedify badge. + - **Layout principles**: Spacing, containers, grid, and whitespace. + - **Responsive behavior**: Single breakpoint at `768px` and mobile + adaptations. + - **Static assets**: Files to serve from `public/` (). + - **Page structure**: Detailed layout of the home page, actor profile + page, and post detail page. + + +Set up the example project +-------------------------- + +Create an `examples/framework/` app and write an example for the new +package. Unless the framework itself prevents it, support both Deno and +Node.js environments. If Deno is supported, add a *deno.json* based on +; if Node.js is supported, add *package.json* based on + and *tsdown.config.ts*. Depending on the supported +environments, add the example path to the `workspace` field in +the root *deno.json* and to the `packages` field in +*pnpm-workspace.yaml*. + +If the framework is backend-only and needs a frontend framework, and there +is no natural pairing like solidstart-solid, use Hono. + +Copy the template files from as-is and modify as needed. + +If the framework does not have a prescribed entry point, use `src/main.ts` +as the application entry point. Define and export the framework app in +`src/app.ts`, then import and run it from the entry file. Import +`src/logging.ts` in the entry file to initialize `@logtape/logtape`. +When logging is needed, use the `getLogger` function from `@logtape/logtape` +to create a logger. + + +Implement the example app +------------------------- + +Follow the specifications in and to +implement the example. In particular: + + - Register the Fedify middleware in `src/app.ts` per the “Middleware + integration” and “Reverse proxy support” sections of + . + - Set up federation logic in `src/federation.ts` based on + . Set up in-memory stores in `src/store.ts` + based on . + - Implement all routes listed in the “Routing” section of + , using `RequestContext` as described in the + “Server-side data access” section. + - Render HTML pages according to . Serve static assets from + the `public/` directory (copy from ). + - Implement the SSE endpoint per the “Server-sent events” section of + . + + +Test the example with `mise test:examples` +------------------------------------------ + +Register the new example in `examples/test-examples/mod.ts`. Read the +comments above the example registry arrays in that file to determine +which array is appropriate and what fields are required. Follow the +patterns of existing entries. + +Before running the tests, ensure that the tunneling service is usable. +The tests use the tunneling service `pinggy.io` to make the example app +accessible to the test suite. If the tunneling service is not usable, +the tests may never finish or may fail due to a connection error. + +While developing the example, run only the new example to iterate +quickly: + +~~~~ bash +mise test:examples framework +~~~~ + +where `framework` is the `name` field of the registered entry. Pass +`--debug` for verbose output if the test fails. + +After the example is complete, run the full suite once to confirm nothing +is broken: + +~~~~ bash +mise test:examples +~~~~ + + +Lint, format, and final checks +------------------------------ + +Add keywords related to the framework in `.hongdown.toml` and `cspell.json` in +root path. + +After implementation, run `mise run fmt && mise check`. +If there are lint or format errors, fix them and run the command again until +there are no errors. diff --git a/.agents/skills/create-example-app-with-integration/example/README.md b/.agents/skills/create-example-app-with-integration/example/README.md new file mode 100644 index 000000000..459617717 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/README.md @@ -0,0 +1,56 @@ + + + + +프레임워크 example application +============================== + +A comprehensive example of building a federated server application using +[Fedify] with [프레임워크]. This example demonstrates how to create an +ActivityPub-compatible federated social media server that can interact with +other federated platforms like Mastodon, Pleroma, and other ActivityPub +implementations using the Fedify and [프레임워크]. + +[Fedify]: https://fedify.dev +[프레임워크]: https://프레임.워크/ + + +Running the example +------------------- + + + +~~~~ sh +# For Deno +deno task dev + +# For pnpm(Node.js) +pnpm dev +~~~~ + + +Communicate with other federated servers +---------------------------------------- + + + +1. Tunnel your local server to the internet using `fedify tunnel` + + ~~~~ sh + fedify tunnel 0000 + ~~~~ + +2. Open the tunneled URL in your browser and check that the server is running + properly. + +3. Search your handle and follow from other federated servers such as Mastodon + or Misskey. + + > [!NOTE] + > [ActivityPub Academy] is a great resource to learn how to interact + > with other federated servers using ActivityPub protocol. + +[ActivityPub Academy]: https://www.activitypub.academy/ diff --git a/.agents/skills/create-example-app-with-integration/example/deno.jsonc b/.agents/skills/create-example-app-with-integration/example/deno.jsonc new file mode 100644 index 000000000..6aa8b8a2c --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/deno.jsonc @@ -0,0 +1,13 @@ +{ + "imports": { + // Add imports required for the framework you are integrating with. + // If packages are already added in the workspace, + // you don't need to add import maps for here. + }, + "tasks": { + // `dev` task STRONGLY RECOMMENDED for `mise test:examples`. + // Other tasks can be added as needed. + // Follow the convention of the framework you are integrating with. + "dev": "deno run --watch -A src/main.ts" + } +} diff --git a/.agents/skills/create-example-app-with-integration/example/package.jsonc b/.agents/skills/create-example-app-with-integration/example/package.jsonc new file mode 100644 index 000000000..2aed45824 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/package.jsonc @@ -0,0 +1,29 @@ +{ + // Fill 프레임워크 with the name of the framework you want to integrate with + "name": "프레임워크-example", + "version": "0.0.1", + "private": true, + "type": "module", + "description": "Fedify app with 프레임워크 integration", + "scripts": { + // `dev` script STRONGLY RECOMMENDED for `mise test:examples`. + // Other scripts can be added as needed. + // Follow the convention of the framework you are integrating with. + "dev": "" + }, + "dependencies": { + // Add packages required for the 프레임워크 integration here + // If packages are already added in the workspace, + // you can reference them with "catalog:". + // Check `pnpm-workspace.yaml` for more packages in the workspace. + "@fedify/fedify": "workspace:^", + "@fedify/프레임워크": "workspace:^", + "@fedify/vocab": "workspace:^", + "@logtape/logtape": "catalog:" + }, + "devDependencies": { + "typescript": "catalog:", + "@types/node": "catalog:" + // Add dev dependencies required for the 프레임워크 integration here + } +} diff --git a/.agents/skills/create-example-app-with-integration/example/public/demo-profile.png b/.agents/skills/create-example-app-with-integration/example/public/demo-profile.png new file mode 100644 index 000000000..d91cf5956 Binary files /dev/null and b/.agents/skills/create-example-app-with-integration/example/public/demo-profile.png differ diff --git a/.agents/skills/create-example-app-with-integration/example/public/fedify-logo.svg b/.agents/skills/create-example-app-with-integration/example/public/fedify-logo.svg new file mode 100644 index 000000000..ba4a7e371 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/public/fedify-logo.svg @@ -0,0 +1,206 @@ + + + + + + + + Fedify + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fedify + + + + diff --git a/.agents/skills/create-example-app-with-integration/example/public/style.css b/.agents/skills/create-example-app-with-integration/example/public/style.css new file mode 100644 index 000000000..3d8b3e6e5 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/public/style.css @@ -0,0 +1,504 @@ +:root { + --background: #ffffff; + --foreground: #171717; +} + +@media (prefers-color-scheme: dark) { + :root { + --background: #0a0a0a; + --foreground: #ededed; + } +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: var(--background); + color: var(--foreground); + font-size: 16px; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, + Oxygen, Ubuntu, Cantarell, sans-serif; +} + +a { + color: #3b82f6; + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +/* Profile Header */ +.profile-header { + display: flex; + gap: 2rem; + padding: 2rem; + margin-bottom: 2rem; + border-radius: 1rem; + color: white; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); +} + +.avatar-section { + flex-shrink: 0; +} + +.avatar { + width: 7.5rem; + height: 7.5rem; + border-radius: 50%; + object-fit: cover; + border: 4px solid rgba(255, 255, 255, 0.2); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.user-info { + display: flex; + flex-direction: column; + justify-content: center; + flex: 1; +} + +.user-name { + font-size: 2.25rem; + font-weight: bold; + margin: 0 0 0.5rem 0; + text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.user-handle { + font-size: 1.25rem; + font-weight: 500; + margin-bottom: 1rem; + opacity: 0.9; +} + +.user-bio { + font-size: 1.125rem; + line-height: 1.625; + opacity: 0.95; + margin: 0; +} + +/* Profile Container & Content */ +.profile-container { + max-width: 56rem; + margin: 0 auto; + display: flex; + flex-direction: column; + justify-content: center; + align-content: center; + padding: 2rem; + min-height: 100vh; +} + +.profile-content { + display: grid; + gap: 2rem; +} + +.info-card { + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.05); + background: var(--background); + color: var(--foreground); + padding: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.info-card h3 { + font-size: 1.5rem; + font-weight: 600; + margin: 0 0 1.5rem 0; +} + +.info-grid { + display: grid; + gap: 1rem; +} + +.info-item { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid rgba(0, 0, 0, 0.05); +} + +.info-item:last-child { + border-bottom: none; +} + +.info-label { + font-size: 0.875rem; + font-weight: 600; + color: color-mix(in srgb, var(--foreground) 60%, transparent); +} + +.fedify-anchor { + display: inline-flex; + align-items: center; + gap: 0.25rem; + height: 1.5rem; + padding: 0.125rem 0.25rem; + border-radius: 0.375rem; + background: #7dd3fc; + color: white; + font-weight: 500; + text-decoration: none; +} + +.fedify-anchor::before { + content: ""; + display: inline-block; + width: 16px; + height: 16px; + background-image: url("/fedify-logo.svg"); + background-size: 16px 16px; + vertical-align: middle; + margin-bottom: 0.125rem; +} + +/* Post Form */ +.post-form { + max-width: 56rem; + margin: 2rem auto; + padding: 1.5rem; + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--background); + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); +} + +.form-group { + margin-bottom: 1rem; +} + +.form-label { + display: block; + font-size: 1.125rem; + font-weight: 600; + margin-bottom: 0.5rem; + color: var(--foreground); +} + +.form-textarea { + width: 100%; + resize: vertical; + border-radius: 0.5rem; + border: 1px solid rgba(0, 0, 0, 0.2); + padding: 0.75rem; + font-size: 1rem; + background: var(--background); + color: var(--foreground); + transition: border-color 0.2s, box-shadow 0.2s; + font-family: inherit; +} + +.form-textarea:focus { + outline: none; + border-color: #3b82f6; + box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3); +} + +.post-button { + padding: 0.5rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-size: 1rem; + font-weight: 600; + color: white; + cursor: pointer; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; +} + +.post-button:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Posts Container & Grid */ +.posts-container { + max-width: 56rem; + margin: 0 auto; + padding: 0 2rem; +} + +.posts-title { + font-size: 1.5rem; + font-weight: bold; + margin-bottom: 1.5rem; + color: var(--foreground); +} + +.posts-grid { + display: grid; + gap: 1.5rem; +} + +.post-card { + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--background); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + transition: transform 0.2s, box-shadow 0.2s; +} + +.post-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); +} + +.post-link { + display: block; + padding: 1.5rem; + text-decoration: none; + color: inherit; +} +.post-link:hover { + text-decoration: none; +} + +.post-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1rem; +} + +.post-avatar { + width: 3rem; + height: 3rem; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e5e7eb; +} + +.post-user-info { + flex: 1; +} + +.post-user-name { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 0.25rem 0; + color: var(--foreground); +} + +.post-user-handle { + font-size: 0.875rem; + opacity: 0.7; + color: var(--foreground); + margin: 0; +} + +.post-content { + font-size: 1rem; + line-height: 1.625; + color: var(--foreground); +} + +.post-content p { + margin: 0; +} + +/* Post Detail */ +.post-detail-container { + max-width: 56rem; + margin: 0 auto; + padding: 2rem; +} + +.post-detail-card { + border-radius: 0.75rem; + border: 1px solid rgba(0, 0, 0, 0.1); + background: var(--background); + padding: 2rem; + margin-bottom: 2rem; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); +} + +.post-detail-author { + display: flex; + align-items: flex-start; + gap: 1rem; + padding-bottom: 1.5rem; + margin-bottom: 1.5rem; + border-bottom: 1px solid rgba(0, 0, 0, 0.1); + text-decoration: none; + color: inherit; +} +.post-detail-author:hover { + text-decoration: none; +} + +.author-avatar { + width: 4rem; + height: 4rem; + border-radius: 50%; + object-fit: cover; + border: 2px solid #e5e7eb; +} + +.author-info { + flex: 1; +} + +.author-name { + font-size: 1.5rem; + font-weight: bold; + margin: 0 0 0.25rem 0; + color: var(--foreground); +} + +.author-handle { + font-size: 1rem; + font-weight: 500; + opacity: 0.7; + margin: 0 0 0.5rem 0; + color: var(--foreground); +} + +.post-timestamp { + font-size: 0.875rem; + opacity: 0.6; + color: var(--foreground); +} + +.post-detail-content { + padding: 1.5rem 0; + font-size: 1.125rem; + line-height: 1.625; + color: var(--foreground); +} + +.post-detail-content p { + margin: 0; +} + +.back-link { + display: inline-block; + margin-bottom: 1.5rem; + padding: 0.5rem 1rem; + border-radius: 0.5rem; + font-weight: 500; + color: var(--foreground); + background: color-mix(in srgb, var(--foreground) 10%, transparent); + text-decoration: none; + transition: background 0.2s; +} + +.back-link:hover { + background: color-mix(in srgb, var(--foreground) 15%, transparent); + text-decoration: none; +} + +/* Home Page */ +.home-container { + max-width: 780px; + margin: 2rem auto; + display: grid; + gap: 1rem; + padding: 1rem; +} + +.home-logo { + display: block; + width: 8rem; + height: 8rem; + margin: 0 auto; +} + +.home-banner { + display: flex; + flex-wrap: wrap; + justify-content: center; + font-family: monospace; + line-height: 1.2; + white-space: pre; +} + +.home-handle { + font-family: monospace; + background: #f3f4f6; + color: #000; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + user-select: all; +} + +.follower-item { + font-family: monospace; + background: #f3f4f6; + color: #000; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; +} + +.follower-list { + display: flex; + flex-direction: column; + gap: 0.25rem; + width: max-content; + list-style: none; +} + +/* Responsive */ +@media (max-width: 768px) { + .profile-container { + padding: 1rem; + } + + .profile-header { + flex-direction: column; + align-items: center; + gap: 1rem; + text-align: center; + } + + .user-name { + font-size: 1.875rem; + } + + .info-item { + flex-direction: column; + align-items: flex-start; + gap: 0.25rem; + } + + .posts-container { + padding: 0 1rem; + } + + .post-form { + padding: 1rem; + } + + .post-detail-container { + padding: 1rem; + } + + .post-detail-card { + padding: 1.5rem; + } + + .author-avatar { + width: 3.5rem; + height: 3.5rem; + } + + .author-name { + font-size: 1.25rem; + } + + .post-detail-content { + font-size: 1rem; + } +} diff --git a/.agents/skills/create-example-app-with-integration/example/public/theme.js b/.agents/skills/create-example-app-with-integration/example/public/theme.js new file mode 100644 index 000000000..8d521d625 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/public/theme.js @@ -0,0 +1,7 @@ +"use strict"; +var mq = window.matchMedia("(prefers-color-scheme: dark)"); +document.body.classList.add(mq.matches ? "dark" : "light"); +mq.addEventListener("change", function (e) { + document.body.classList.remove("light", "dark"); + document.body.classList.add(e.matches ? "dark" : "light"); +}); diff --git a/.agents/skills/create-example-app-with-integration/example/src/federation.ts b/.agents/skills/create-example-app-with-integration/example/src/federation.ts new file mode 100644 index 000000000..898a7c91b --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/src/federation.ts @@ -0,0 +1,164 @@ +import { + createFederation, + generateCryptoKeyPair, + InProcessMessageQueue, + MemoryKvStore, +} from "@fedify/fedify"; +import { + Accept, + Endpoints, + Follow, + Image, + Note, + Person, + PUBLIC_COLLECTION, + type Recipient, + Undo, +} from "@fedify/vocab"; +import { keyPairsStore, postStore, relationStore } from "./store.ts"; + +const federation = createFederation({ + kv: new MemoryKvStore(), + queue: new InProcessMessageQueue(), +}); + +const IDENTIFIER = "demo"; + +federation + .setActorDispatcher( + "/users/{identifier}", + async (context, identifier) => { + if (identifier != IDENTIFIER) { + return null; + } + const keyPairs = await context.getActorKeyPairs(identifier); + return new Person({ + id: context.getActorUri(identifier), + name: "Fedify Demo", + summary: "This is a Fedify Demo account.", + preferredUsername: identifier, + icon: new Image({ url: new URL("/demo-profile.png", context.url) }), + url: new URL("/", context.url), + inbox: context.getInboxUri(identifier), + endpoints: new Endpoints({ sharedInbox: context.getInboxUri() }), + publicKey: keyPairs[0].cryptographicKey, + assertionMethods: keyPairs.map((keyPair) => keyPair.multikey), + }); + }, + ) + .setKeyPairsDispatcher(async (_, identifier) => { + if (identifier != IDENTIFIER) { + return []; + } + const keyPairs = keyPairsStore.get(identifier); + if (keyPairs) { + return keyPairs; + } + const { privateKey, publicKey } = await generateCryptoKeyPair(); + keyPairsStore.set(identifier, [{ privateKey, publicKey }]); + return [{ privateKey, publicKey }]; + }); + +federation + .setInboxListeners("/users/{identifier}/inbox", "/inbox") + .on(Follow, async (context, follow) => { + if ( + follow.id == null || + follow.actorId == null || + follow.objectId == null + ) { + return; + } + const result = context.parseUri(follow.objectId); + if (result?.type !== "actor" || result.identifier !== IDENTIFIER) { + return; + } + const follower = await follow.getActor(context) as Person; + if (!follower?.id) { + throw new Error("follower is null"); + } + await context.sendActivity( + { identifier: result.identifier }, + follower, + new Accept({ + id: new URL( + `#accepts/${follower.id.href}`, + context.getActorUri(IDENTIFIER), + ), + actor: follow.objectId, + object: follow, + }), + ); + relationStore.set(follower.id.href, follower); + }) + .on(Undo, async (context, undo) => { + const activity = await undo.getObject(context); + if (activity instanceof Follow) { + if (activity.id == null) { + return; + } + if (undo.actorId == null) { + return; + } + relationStore.delete(undo.actorId.href); + } else { + console.debug(undo); + } + }); + +federation.setObjectDispatcher( + Note, + "/users/{identifier}/posts/{id}", + (ctx, values) => { + const id = ctx.getObjectUri(Note, values); + const post = postStore.get(id); + if (post == null) return null; + return new Note({ + id, + attribution: ctx.getActorUri(values.identifier), + to: PUBLIC_COLLECTION, + cc: ctx.getFollowersUri(values.identifier), + content: post.content, + mediaType: "text/html", + published: post.published, + url: id, + }); + }, +); + +federation + .setFollowersDispatcher( + "/users/{identifier}/followers", + () => { + const followers = Array.from(relationStore.values()); + const items: Recipient[] = followers.map((f) => ({ + id: f.id, + inboxId: f.inboxId, + endpoints: f.endpoints, + })); + return { items }; + }, + ); + +federation.setNodeInfoDispatcher("/nodeinfo/2.1", (ctx) => { + return { + software: { + /** + * Fill `` with the actual framework name. + * Lowercase, digits, and hyphens only. + */ + name: "fedify-", + version: "0.0.1", + homepage: new URL(ctx.canonicalOrigin), + }, + protocols: ["activitypub"], + usage: { + // Usage statistics is hard-coded here for demonstration purposes. + // You should replace these with real statistics: + users: { total: 1, activeHalfyear: 1, activeMonth: 1 }, + localPosts: postStore.getAll().length, + }, + }; +}); + +export default federation; diff --git a/.agents/skills/create-example-app-with-integration/example/src/logging.ts b/.agents/skills/create-example-app-with-integration/example/src/logging.ts new file mode 100644 index 000000000..3b43c28c6 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/src/logging.ts @@ -0,0 +1,23 @@ +import { configure, getConsoleSink } from "@logtape/logtape"; +import { AsyncLocalStorage } from "node:async_hooks"; + +await configure({ + contextLocalStorage: new AsyncLocalStorage(), + sinks: { + console: getConsoleSink(), + }, + filters: {}, + loggers: [ + { + category: ["default", "example"], + lowestLevel: "debug", + sinks: ["console"], + }, + { category: "fedify", lowestLevel: "info", sinks: ["console"] }, + { + category: ["logtape", "meta"], + lowestLevel: "warning", + sinks: ["console"], + }, + ], +}); diff --git a/.agents/skills/create-example-app-with-integration/example/src/main.ts b/.agents/skills/create-example-app-with-integration/example/src/main.ts new file mode 100644 index 000000000..54324dcb0 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/src/main.ts @@ -0,0 +1,2 @@ +import "./logging.ts"; +import "./app.ts"; diff --git a/.agents/skills/create-example-app-with-integration/example/src/store.ts b/.agents/skills/create-example-app-with-integration/example/src/store.ts new file mode 100644 index 000000000..507531494 --- /dev/null +++ b/.agents/skills/create-example-app-with-integration/example/src/store.ts @@ -0,0 +1,45 @@ +import { Note, Person } from "@fedify/vocab"; + +declare global { + var keyPairsStore: Map>; + var relationStore: Map; + var postStore: PostStore; +} + +class PostStore { + #map: Map = new Map(); + #timeline: URL[] = []; + constructor() {} + append(posts: Note[]) { + posts.filter((p) => p.id && !this.#map.has(p.id.toString())) + .forEach((p) => { + this.#map.set(p.id!.toString(), p); + this.#timeline.push(p.id!); + }); + } + get(id: URL) { + return this.#map.get(id.toString()); + } + getAll() { + return this.#timeline.toReversed() + .map((id) => id.toString()) + .map((id) => this.#map.get(id)!) + .filter((p) => p); + } + delete(id: URL) { + const existed = this.#map.delete(id.toString()); + if (existed) { + this.#timeline = this.#timeline.filter((i) => i.href !== id.href); + } + } +} + +const keyPairsStore = globalThis.keyPairsStore ?? new Map(); +const relationStore = globalThis.relationStore ?? new Map(); +const postStore = globalThis.postStore ?? new PostStore(); + +// this is just a hack for the demo +// never do this in production, use safe and secure storage +globalThis.keyPairsStore = keyPairsStore; +globalThis.relationStore = relationStore; +globalThis.postStore = postStore; diff --git a/.agents/skills/create-integration-package/SKILL.md b/.agents/skills/create-integration-package/SKILL.md new file mode 100644 index 000000000..8aa3cec5e --- /dev/null +++ b/.agents/skills/create-integration-package/SKILL.md @@ -0,0 +1,180 @@ +--- +name: create-integration-package +description: >- + This skill is utilized when creating a web framework integration package. + After examining the given framework, a feasibility assessment is conducted + regarding the creation of an integration package. + If implementation is feasible, the package is generated; + if it is not possible, the rationale is provided to the user. +argument-hint: "Provide the name of the web framework you want to integrate with." +--- + + + +Adding an integration package to a web framework +================================================ + +Follow these steps in order to implement the integration package. + +1. Research the web framework +2. Implement the package +3. Lint, format, and final checks + + +Research the web framework +-------------------------- + +Research the web framework for which the integration package will be +implemented. Fedify operates as middleware via +[`Federation.fetch`](../../../packages/fedify/src/federation/federation.ts). +The critical question is whether the given framework can act as a server +framework and supports adding middleware. Search for and investigate +whether the relevant functionality is available. Assess feasibility based +on the research. If research indicates implementation is not possible, +explain the reasons in detail to the user and stop. If feasible, proceed +to create the package. Even during package creation, it may turn out to be +infeasible. In that case as well, explain the reasons in detail to the +user and stop. + + +Implement the package +--------------------- + +**Prioritize usability above all else.** The most important goal is that +the package integrates smoothly with the framework so users do not +experience friction when connecting it. + +Create the package directory inside the `packages/` directory. For example, if +the framework is named “framework”, create the directory `packages/framework/`. + +Unless there are significant hurdles, please set up the package to publish +on both JSR and NPM. + +Copy the template files from into the directory you created. Then, +implement the package according to the framework. Since the comments in the +template are instructions for the developer to follow, please remove them once +the implementation is complete. + +Add additional definitions as appropriate based on context. Aside from the +main integration function and the `ContextDataFactory` type, keep module +exports to a minimum to avoid confusing users. + +### Request flow + +When a request arrives, the integration middleware calls +`federation.fetch()`. If Fedify has a route for the path and the client's +`Accept` header includes an ActivityPub media type such as +`application/activity+json`, Fedify generates and returns the JSON-LD +response directly. Framework-side routing does not execute. + +### Request conversion + +Some frameworks define and use their own `Request` type internally instead +of the Web API `Request`. If the target framework does so, write +conversion functions within the integration package to translate between +the Web API `Request` and the framework's native `Request`. + +### 406 not acceptable + +The final failure 406 response uses this form: + +~~~~ typescript +new Response("Not acceptable", { + status: 406, + headers: { + "Content-Type": "text/plain", + Vary: "Accept", + }, +}); +~~~~ + +### Function naming conventions + +A consistent naming convention for the main function has not yet been +established, but there is an [open naming convention issue]. If the issue +has been resolved by the time this skill is executed, update this section. +As a temporary convention, respect conventions of the framework : name it +`fedifyMiddleware` if the official documentation calls it as middleware, or +`fedifyHandler` if it's called a handler. + +[open naming convention issue]: https://github.com/fedify-dev/fedify/issues/657 + +### Non-source files + +#### README.md + +The package README.md must include the following: + + - Package description + - Supported framework versions, if only specific versions are supported + - Installation instructions + - Usage instructions (with example code) + +#### `deno.json` + +A *deno.json* is required to publish to JSR. + +#### `package.json` + +A *package.json* is required to publish to npm. + +#### `tsdown.config.ts` + +A *tsdown.config.ts* is required for the build in Node.js and Bun +environments. + +### Other updates + +Refer to the “Adding a new package” section in *CONTRIBUTING.md* and +perform the required updates. Record the package addition in *CHANGES.md*. + +### Tests + +You can test the integration using `mise test:init`, which will be explained +later, but write unit tests as well if possible. Import the `test` function +from `@fedify/fixture` to write runtime-agnostic tests that work across +Deno, Node.js, and Bun. Name test files with the `*.test.ts` convention +(e.g., `src/mod.test.ts`). + +> **Warning**: `@fedify/fixture` is a **private** workspace package and +> must never be imported from published (non-test) source files. Only +> import it in `*.test.ts` files. + +### Implementation checklist + +1. Create the *packages/framework/* directory +2. Write *src/mod.ts*: + - Export the main integration middleware/handler function + - Implement `federation.fetch()` invocation with + `onNotFound`/`onNotAcceptable` + - Export the `ContextDataFactory` type + - Write conversion functions if the framework does not natively support + Web API `Request`/`Response` +3. Write *README.md* +4. Write *deno.json* (if publishing to JSR is intended) +5. Write *package.json* (if publishing to npm is intended) +6. Write *tsdown.config.ts* (if Node.js and Bun are supported) +7. Write tests if possible +8. Perform remaining updates per the “Adding a new package” section in + *CONTRIBUTING.md* +9. Record changes in *CHANGES.md* + + +Lint, format, and final checks +------------------------------ + +Add keywords related to the framework in `.hongdown.toml` and `cspell.json` in +root path. Especially, the package name `@fedify/framework` should be added to +the `.hongdown.toml`. + +After implementation, run `mise run fmt && mise check`. +If there are lint or format errors, fix them and run the command again until +there are no errors. + + +Next steps +---------- + +If there are no particular issues, continue by using the +`add-to-fedify-init` and `create-example-app-with-integration` skills to +complete the remaining implementation. diff --git a/.agents/skills/create-integration-package/package/README.md b/.agents/skills/create-integration-package/package/README.md new file mode 100644 index 000000000..71b6999bf --- /dev/null +++ b/.agents/skills/create-integration-package/package/README.md @@ -0,0 +1,81 @@ + + + + +@fedify/프레임워크: Integrate Fedify with 프레임워크 +==================================================== + + + +[![JSR][JSR badge]][JSR] + + + +[![npm][npm badge]][npm] + + + +[![Matrix][Matrix badge]][Matrix] +[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social] + +This package provides a simple way to integrate [Fedify] with [프레임워크]. + +[JSR badge]: https://jsr.io/badges/@fedify/프레임워크 +[JSR]: https://jsr.io/@fedify/프레임워크 +[npm badge]: https://img.shields.io/npm/v/@fedify/프레임워크?logo=npm +[npm]: https://www.npmjs.com/package/@fedify/프레임워크 +[Matrix badge]: https://img.shields.io/matrix/fedify%3Amatrix.org +[Matrix]: https://matrix.to/#/#fedify:matrix.org +[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg +[@fedify@hollo.social]: https://hollo.social/@fedify +[Fedify]: https://fedify.dev/ +[프레임워크]: https://프레임.워크/ + + +Installation +------------ + + + +~~~~ bash +deno add jsr:@fedify/프레임워크 # If JSR is supported (Have `deno.json(c)`) +deno add npm:@fedify/프레임워크 # If JSR is not supported (No `deno.json(c)`) +# or +npm add @fedify/프레임워크 +# or +pnpm add @fedify/프레임워크 +# or +yarn add @fedify/프레임워크 +# or +bun add @fedify/프레임워크 +~~~~ + + +Usage +----- + +First, create your `Federation` instance in a server utility file, +e.g., *src/federation.ts*: + +~~~~ typescript +import { createFederation, MemoryKvStore } from "@fedify/fedify"; + +const federation = createFederation({ + kv: new MemoryKvStore(), +}); + +// ... configure your federation ... + +export default federation; +~~~~ + +Then, add Fedify middleware to your server: + +~~~~ typescript +import fedifyMiddleware from "@fedify/프레임워크"; +import federation from "./federation.ts"; + +const fedify = fedifyMiddleware(federation); + +app.use(fedify); +~~~~ diff --git a/.agents/skills/create-integration-package/package/deno.jsonc b/.agents/skills/create-integration-package/package/deno.jsonc new file mode 100644 index 000000000..1042c3470 --- /dev/null +++ b/.agents/skills/create-integration-package/package/deno.jsonc @@ -0,0 +1,28 @@ +{ + "name": "@fedify/프레임워크", // Replace `프레임워크` with the framework name in lowercase + "version": "*.*.*", // Sync with packages/fedify/deno.json + "license": "MIT", + "exports": { + ".": "./src/mod.ts" + }, + "imports": { + // Add Fedify and framework dependencies here. + // Use JSR packages in Deno, and alias to the npm name when they differ. + // Example: + // "hono": "jsr:@hono/hono@^4" + }, + "exclude": [ + "dist", + "node_modules" + ], + "publish": { + "exclude": [ + "**/*.test.ts", // If there are test files + "tsdown.config.ts" + ] + }, + "tasks": { + "check": "deno fmt --check && deno lint && deno check src/*.ts", + "test": "deno test" // Add if --allow-* for permissions is needed + } +} diff --git a/.agents/skills/create-integration-package/package/package.jsonc b/.agents/skills/create-integration-package/package/package.jsonc new file mode 100644 index 000000000..7882492ae --- /dev/null +++ b/.agents/skills/create-integration-package/package/package.jsonc @@ -0,0 +1,66 @@ +{ + "name": "@fedify/프레임워크", // Fill 프레임워크 with the framework name in lowercase + "version": "*.*.*", // Sync with packages/fedify/package.json + "description": "Integration Package for Fedify with 프레임워크", // Fill 프레임워크 with the framework name + "keywords": [ + "Fedify", + "Federation", + // Add relevant keywords for the framework + ], + "author": { // Fill in author information + "name": "", + "email": "", + "url": "", + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fedify-dev/fedify.git", + "directory": "packages/프레임워크" // Fill 프레임워크 with the framework name + }, + "homepage": "https://fedify.dev/", + "license": "MIT", + "bugs": { + "url": "https://github.com/fedify-dev/fedify/issues" + }, + "funding": [ + "https://opencollective.com/fedify", + "https://github.com/sponsors/dahlia" + ], + "type": "module", + "main": "./dist/mod.cjs", + "module": "./dist/mod.js", + "types": "./dist/mod.d.ts", + "exports": { + ".": { + "types": { + "import": "./dist/mod.d.ts", + "require": "./dist/mod.d.cts", + "default": "./dist/mod.d.ts" + }, + "import": "./dist/mod.js", + "require": "./dist/mod.cjs", + "default": "./dist/mod.js" + }, + "./package.json": "./package.json" + }, + "files": [ + "dist/", + "package.json" + ], + "peerDependencies": { + "@fedify/fedify": "workspace:^", + // Add relevant peer dependencies for the framework + }, + "devDependencies": { + "@types/node": "catalog:", + "tsdown": "catalog:", + "typescript": "catalog:", + // Add other relevant peer dependencies for the framework + }, + "scripts": { + "build:self": "tsdown", + "build": "pnpm --filter @fedify/프레임워크... run build:self", + "prepack": "pnpm build", + "prepublish": "pnpm build" + } +} diff --git a/.agents/skills/create-integration-package/package/src/mod.ts b/.agents/skills/create-integration-package/package/src/mod.ts new file mode 100644 index 000000000..9b7ea0ed4 --- /dev/null +++ b/.agents/skills/create-integration-package/package/src/mod.ts @@ -0,0 +1,17 @@ +import { Federation } from "@fedify/fedify"; +import type { FrameworkMiddlewareHandler } from "프레임워크"; + +// `FrameworkContext` could be unnecessary. +// Remove it if the framework's middleware handler does not provide a context object. + +export type ContextDataFactory = ( + context: FrameworkContext, +) => TContextData | Promise; + +export default function fedifyMiddleware( + federation: Federation, + contextDataFactory: ContextDataFactory = + (() => void 0 as TContextData), +): FrameworkMiddlewareHandler { + // Implement handler or middleware +} diff --git a/.agents/skills/create-integration-package/package/tsdown.config.ts b/.agents/skills/create-integration-package/package/tsdown.config.ts new file mode 100644 index 000000000..54e25f61b --- /dev/null +++ b/.agents/skills/create-integration-package/package/tsdown.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "tsdown"; + +export default defineConfig({ + entry: ["src/mod.ts"], + format: ["esm", "cjs"], + platform: "node", + outExtensions({ format }) { + return { + js: format === "cjs" ? ".cjs" : ".js", + dts: format === "cjs" ? ".d.cts" : ".d.ts", + }; + }, +}); diff --git a/AGENTS.md b/AGENTS.md index b23d7d2b9..88ed134fc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -158,11 +158,14 @@ Common tasks ### Implementing framework integrations -1. Create a new package in *packages/* directory for new integrations -2. Follow pattern from existing integration packages (*packages/hono/*, - *packages/sveltekit/*) -3. Use standard request/response interfaces for compatibility -4. Consider creating example applications in *examples/* that demonstrate usage +A detailed step-by-step guide is available across three skills: + + - *.agents/skills/create-integration-package/SKILL.md*: Researching the + framework and creating the integration package. + - *.agents/skills/add-to-fedify-init/SKILL.md*: Adding the package to + `fedify init` and testing with `mise test:init`. + - *.agents/skills/create-example-app-with-integration/SKILL.md*: Writing an + example application and testing with `mise test:examples`. ### Creating database adapters diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8b73c580e..e41f9bbad 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -208,6 +208,15 @@ When adding a new package to the monorepo, the following files must be updated: - If using pnpm catalog for dependency management: Add to `catalog` in *pnpm-workspace.yaml*. +### Adding a web framework integration + +A step-by-step guide for implementing a web framework integration package is +available in *.agents/skills/create-integration-package/SKILL.md*. Although +the file is primarily designed for AI coding agents, the instructions are +written so that human contributors can also read and follow them. The guide +covers the entire workflow from researching the framework through creating +the package, adding it to `fedify init`, testing, and writing an example. + ### Dependency management Fedify uses two package managers: diff --git a/deno.lock b/deno.lock index cd78d61f8..4949e9240 100644 --- a/deno.lock +++ b/deno.lock @@ -57,6 +57,7 @@ "jsr:@std/semver@^1.0.6": "1.0.8", "jsr:@std/testing@0.224": "0.224.0", "jsr:@std/uuid@^1.0.9": "1.1.0", + "jsr:@std/yaml@^1.0.8": "1.0.12", "jsr:@valibot/valibot@^1.2.0": "1.2.0", "npm:@alinea/suite@~0.6.3": "0.6.3", "npm:@astrojs/node@^10.0.3": "10.0.4_astro@5.18.1__@types+node@24.12.0__ioredis@5.10.1__tsx@4.21.0__typescript@6.0.2__yaml@2.8.3_@types+node@24.12.0_ioredis@5.10.1_tsx@4.21.0_typescript@6.0.2_yaml@2.8.3", @@ -398,6 +399,9 @@ "jsr:@std/bytes" ] }, + "@std/yaml@1.0.12": { + "integrity": "7deabca4545bcedd07c5f69ea53acea71b8b4c67562f224e17b90d75944cb20c" + }, "@valibot/valibot@1.2.0": { "integrity": "61c118a4d027ed55912caf381c78f0a178f335f46ad0c4bcb136498dc1ef2285" } diff --git a/examples/test-examples/mod.ts b/examples/test-examples/mod.ts index 0ed78181f..d83dac360 100644 --- a/examples/test-examples/mod.ts +++ b/examples/test-examples/mod.ts @@ -126,6 +126,25 @@ type TestResult = | { name: string; status: "skip"; reason: string }; // ─── Example Registry ───────────────────────────────────────────────────────── +// +// Every example directory under examples/ must be registered in exactly one of +// the arrays below. The test runner scans the examples/ directory and reports +// any unregistered directories as warnings. +// +// - SERVER_EXAMPLES – Long-running HTTP servers. The runner starts the +// server, opens a tunnel, and verifies federation via +// `fedify lookup`. Most integration framework examples +// belong here. +// - SCRIPT_EXAMPLES – Standalone scripts (no server). The runner executes +// the command and checks the exit code. +// - MULTI_HANDLE_EXAMPLES – Scripts that accept an ActivityPub handle as +// their last argument. Multiple handles are tried in +// order; the test passes if any exits with code 0. +// - SKIPPED_EXAMPLES – Examples that cannot be tested automatically. +// Provide a reason string explaining why. +// +// See the interface definitions above for the full set of fields each entry +// accepts. const SERVER_EXAMPLES: ServerExample[] = [ {