Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/nuke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ on:
description: "Eas"
type: boolean
default: false
modrinth:
description: "Modrinth"
type: boolean
default: false

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/pr-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
if: contains(github.event.pull_request.labels.*.name, 'force-ci')
run: |
set -euo pipefail
all='["core","aws","cloudflare","gcp","neon","planetscale","prisma-postgres","stripe","supabase","posthog","axiom","azure","kubernetes","coinbase","mongodb-atlas","fly-io","turso","typesense","workos","expo-eas"]'
all='["core","aws","cloudflare","gcp","neon","planetscale","prisma-postgres","stripe","supabase","posthog","axiom","azure","kubernetes","coinbase","mongodb-atlas","fly-io","turso","typesense","workos","expo-eas","modrinth"]'
echo "packages=${all}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v4
Expand Down Expand Up @@ -102,6 +102,9 @@ jobs:
expo-eas:
- 'packages/expo-eas/**'
- 'packages/core/**'
modrinth:
- 'packages/modrinth/**'
- 'packages/core/**'

# ── Compute tags once so every matrix job + the comment use the same set. ─
tags:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ env:
packages/typesense/package.json
packages/workos/package.json
packages/expo-eas/package.json
packages/modrinth/package.json
bun.lock
CHANGELOG.md

Expand Down
25 changes: 25 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
typesense: ${{ steps.force.outputs.all || steps.changes.outputs.typesense }}
workos: ${{ steps.force.outputs.all || steps.changes.outputs.workos }}
expo-eas: ${{ steps.force.outputs.all || steps.changes.outputs.expo-eas }}
modrinth: ${{ steps.force.outputs.all || steps.changes.outputs.modrinth }}
steps:
- id: force
if: contains(github.event.pull_request.labels.*.name, 'force-ci')
Expand Down Expand Up @@ -110,6 +111,9 @@ jobs:
expo-eas:
- 'packages/expo-eas/**'
- 'packages/core/**'
modrinth:
- 'packages/modrinth/**'
- 'packages/core/**'

ci-core:
needs: detect-changes
Expand Down Expand Up @@ -494,3 +498,24 @@ jobs:
working-directory: packages/expo-eas
env:
EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}

ci-modrinth:
needs: detect-changes
if: needs.detect-changes.outputs.modrinth == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- run: bun install
- run: bun run build
working-directory: packages/core
- run: bun run check
working-directory: packages/modrinth
- run: bun run test
working-directory: packages/modrinth
env:
MODRINTH_API_BASE_URL: ${{ secrets.MODRINTH_API_BASE_URL }}
MODRINTH_API_KEY: ${{ secrets.MODRINTH_API_KEY }}
MODRINTH_USER_AGENT: ${{ secrets.MODRINTH_USER_AGENT }}
20 changes: 20 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 79 additions & 0 deletions packages/modrinth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# @distilled.cloud/modrinth

Effect-native Modrinth SDK generated from the [Modrinth OpenAPI specification](https://docs.modrinth.com/openapi.yaml). Browse, create, and manage projects, versions, users, teams, threads, and notifications on the Modrinth platform with exhaustive error typing.

## Installation

```bash
npm install @distilled.cloud/modrinth effect
```

## Quick Start

```typescript
import { Effect, Layer } from "effect";
import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient";
import { searchProjects } from "@distilled.cloud/modrinth/Operations";
import { CredentialsFromEnv } from "@distilled.cloud/modrinth";

const program = Effect.gen(function* () {
const results = yield* searchProjects({
query: "fabric api",
limit: 10,
});
return results.hits;
});

const ModrinthLive = Layer.mergeAll(FetchHttpClient.layer, CredentialsFromEnv);

program.pipe(Effect.provide(ModrinthLive), Effect.runPromise);
```

## Configuration

```bash
# Optional — required only for endpoints that create/modify data or access
# private resources. Most read endpoints work without a token.
MODRINTH_API_KEY=mrp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# Strongly recommended — Modrinth requires a uniquely-identifying User-Agent
# header and may throttle traffic that only identifies the HTTP client.
# Format: <github_username>/<project_name>/<version> (<contact>)
MODRINTH_USER_AGENT="my-org/my-app/1.0.0 (contact@example.com)"

# Optional — defaults to https://api.modrinth.com/v2.
# Use https://staging-api.modrinth.com/v2 to target the staging environment.
MODRINTH_API_BASE_URL=https://api.modrinth.com/v2
```

Personal access tokens are generated in [Modrinth account settings](https://modrinth.com/settings/account). Tokens are sent verbatim in the `Authorization` header (no `Bearer ` prefix) and are scoped — see the [scopes list](https://github.com/modrinth/labrinth/blob/master/src/models/pats.rs) for details.

## Error Handling

```typescript
import { getProject } from "@distilled.cloud/modrinth/Operations";
import { Effect } from "effect";

getProject({ "id|slug": "missing-project" }).pipe(
Effect.catchTags({
NotFound: () => Effect.succeed(null),
Unauthorized: (e) => Effect.fail(new Error(`Auth: ${e.message}`)),
UnknownModrinthError: (e) => Effect.fail(new Error(`Unknown: ${e.message}`)),
}),
);
```

## Services

- **Projects** — `searchProjects`, `getProject`, `getProjects`, `createProject`, `modifyProject`, `deleteProject`, `randomProjects`, `checkProjectValidity`, `changeProjectIcon`, `deleteProjectIcon`, `addGalleryImage`, `modifyGalleryImage`, `deleteGalleryImage`, `getDependencies`, `followProject`, `unfollowProject`, `scheduleProject`
- **Versions** — `getProjectVersions`, `getVersion`, `getVersions`, `createVersion`, `modifyVersion`, `deleteVersion`, `scheduleVersion`, `getVersionFromIdOrNumber`, `addFilesToVersion`, `versionFromHash`, `versionsFromHashes`, `deleteFileFromHash`, `getLatestVersionFromHash`, `getLatestVersionsFromHashes`
- **Users** — `getUser`, `getUsers`, `getUserFromAuth`, `modifyUser`, `changeUserIcon`, `deleteUserIcon`, `getUserProjects`, `getFollowedProjects`, `getPayoutHistory`, `withdrawPayout`
- **Notifications** — `getUserNotifications`, `getNotification`, `getNotifications`, `readNotification`, `readNotifications`, `deleteNotification`, `deleteNotifications`
- **Teams** — `getProjectTeamMembers`, `getTeamMembers`, `getTeams`, `addTeamMember`, `joinTeam`, `modifyTeamMember`, `deleteTeamMember`, `transferTeamOwnership`
- **Threads & Reports** — `getThread`, `getThreads`, `sendThreadMessage`, `deleteThreadMessage`, `submitReport`, `getReport`, `getReports`, `getOpenReports`, `modifyReport`
- **Tags** — `categoryList`, `loaderList`, `versionList`, `licenseText`, `donationPlatformList`, `reportTypeList`, `projectTypeList`, `sideTypeList`
- **Misc** — `forgeUpdates`, `statistics`

## License

MIT
89 changes: 89 additions & 0 deletions packages/modrinth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
{
"name": "@distilled.cloud/modrinth",
"version": "0.2.0-alpha",
"repository": {
"type": "git",
"url": "https://github.com/alchemy-run/distilled",
"directory": "packages/modrinth"
},
"type": "module",
"sideEffects": false,
"module": "src/index.ts",
"files": [
"lib",
"src"
],
"exports": {
".": {
"types": "./lib/index.d.ts",
"bun": "./src/index.ts",
"default": "./lib/index.js"
},
"./Category": {
"types": "./lib/category.d.ts",
"bun": "./src/category.ts",
"default": "./lib/category.js"
},
"./Client": {
"types": "./lib/client.d.ts",
"bun": "./src/client.ts",
"default": "./lib/client.js"
},
"./Credentials": {
"types": "./lib/credentials.d.ts",
"bun": "./src/credentials.ts",
"default": "./lib/credentials.js"
},
"./Errors": {
"types": "./lib/errors.d.ts",
"bun": "./src/errors.ts",
"default": "./lib/errors.js"
},
"./Operations": {
"types": "./lib/operations/index.d.ts",
"bun": "./src/operations/index.ts",
"default": "./lib/operations/index.js"
},
"./Retry": {
"types": "./lib/retry.d.ts",
"bun": "./src/retry.ts",
"default": "./lib/retry.js"
},
"./Sensitive": {
"types": "./lib/sensitive.d.ts",
"bun": "./src/sensitive.ts",
"default": "./lib/sensitive.js"
},
"./Traits": {
"types": "./lib/traits.d.ts",
"bun": "./src/traits.ts",
"default": "./lib/traits.js"
}
},
"scripts": {
"typecheck": "tsgo",
"build": "tsgo -b",
"fmt": "oxfmt --write src",
"lint": "oxlint --fix src",
"check": "tsgo && oxlint src && oxfmt --check src",
"test": "bunx vitest run test --passWithNoTests",
"publish:npm": "bun run build && bun publish --access public",
"generate": "bun run scripts/generate.ts && oxlint --fix src && oxfmt --write src && oxfmt --write src",
"specs:fetch": "echo 'No specs configured'",
"specs:update": "echo 'No specs configured'"
},
"dependencies": {
"@distilled.cloud/core": "workspace:*",
"effect": "catalog:"
},
"devDependencies": {
"@types/bun": "catalog:",
"@types/node": "catalog:",
"dotenv": "catalog:",
"vitest": "catalog:",
"yaml": "catalog:"
},
"peerDependencies": {
"effect": "catalog:"
}
}
113 changes: 113 additions & 0 deletions packages/modrinth/patches/001-add-error-responses.patch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
{
"description": "Add 400 BadRequest responses to operations that return them in practice but don't document them. Discovered via live API probing — Modrinth returns 400 with `{ error: 'invalid_input' | 'json_error', description }` for malformed base62 IDs (path params) and malformed JSON arrays (query params), but the spec omits these responses on the corresponding operations.",
"patches": [
{
"op": "add",
"path": "/paths/~1version~1{id}/get/responses/400",
"value": {
"description": "Path parameter could not be parsed (e.g. invalid base62 encoding)",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1versions/get/responses/400",
"value": {
"description": "Query parameter `ids` could not be parsed as a JSON array of strings",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1users/get/responses/400",
"value": {
"description": "Query parameter `ids` could not be parsed as a JSON array of strings",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1teams/get/responses/400",
"value": {
"description": "Query parameter `ids` could not be parsed as a JSON array of strings",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1team~1{id}~1members/get/responses/400",
"value": {
"description": "Path parameter could not be parsed (e.g. invalid base62 encoding)",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1report~1{id}/get/responses/400",
"value": {
"description": "Path parameter could not be parsed (e.g. invalid base62 encoding)",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1reports/get/responses/400",
"value": {
"description": "Query parameter `ids` missing or could not be parsed as a JSON array of strings",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1notification~1{id}/get/responses/400",
"value": {
"description": "Path parameter could not be parsed (e.g. invalid base62 encoding)",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
},
{
"op": "add",
"path": "/paths/~1thread~1{id}/get/responses/400",
"value": {
"description": "Path parameter could not be parsed (e.g. invalid base62 encoding)",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/InvalidInputError" }
}
}
}
}
]
}
Loading