You've already forked AstralRinth
Merge tag 'v0.14.6' into beta
v0.14.6
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# Autogenerated files
|
||||
dist
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
CLAUDE.md
|
||||
@@ -0,0 +1,208 @@
|
||||
# @modrinth/api-client
|
||||
|
||||
Platform-agnostic API client for Modrinth's services. Works in Nuxt (SSR + CSR), Tauri (desktop app), and plain Node/browser environments.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Request Flow:
|
||||
Module Method → client.request() → Feature Chain (middleware) → Platform executeRequest()
|
||||
```
|
||||
|
||||
### Key Directories
|
||||
|
||||
- **`src/core/`** — base classes (`AbstractModrinthClient`, `AbstractModule`, `AbstractFeature`, etc.)
|
||||
- **`src/platform/`** — platform implementations (generic, nuxt, tauri, xhr-upload, websocket)
|
||||
- **`src/features/`** — middleware plugins (auth, retry, circuit-breaker, etc.)
|
||||
- **`src/modules/`** — API endpoint modules organized by service (`labrinth/`, `archon/`, `kyros/`, `iso3166/`)
|
||||
- **`src/types/`** — core type definitions (client config, request options, upload types, errors)
|
||||
|
||||
### Client Hierarchy
|
||||
|
||||
All platform clients extend `XHRUploadClient` → `AbstractModrinthClient`:
|
||||
|
||||
- **`GenericModrinthClient`** — uses `ofetch`, attaches WebSocket client to `archon.sockets`
|
||||
- **`NuxtModrinthClient`** — uses Nuxt's `$fetch`, SSR-aware, blocks `upload()` during SSR
|
||||
- **`TauriModrinthClient`** — uses `@tauri-apps/plugin-http`
|
||||
|
||||
### Module Access
|
||||
|
||||
Modules are lazy-loaded and accessed as a nested structure:
|
||||
|
||||
```ts
|
||||
client.labrinth.projects_v2
|
||||
client.labrinth.projects_v3
|
||||
client.labrinth.versions_v3
|
||||
client.labrinth.collections
|
||||
client.labrinth.billing_internal
|
||||
client.archon.servers_v0
|
||||
client.archon.servers_v1
|
||||
client.archon.backups_queue_v1
|
||||
client.archon.backups_v1
|
||||
client.archon.content_v0
|
||||
client.kyros.files_v0
|
||||
client.iso3166.data
|
||||
... etc.
|
||||
```
|
||||
|
||||
This structure is derived at runtime from the flat `MODULE_REGISTRY` in `modules/index.ts` via `buildModuleStructure()`, and the TypeScript types are inferred automatically via `InferredClientModules`.
|
||||
|
||||
## Critical: Always use `this.client.request()`
|
||||
|
||||
API modules **must** use `this.client.request()` (or `.upload`) for all HTTP calls — never `$fetch`, `fetch`, or any other HTTP library directly. The request method routes through the platform-specific implementation (Nuxt `$fetch`, Tauri HTTP plugin, etc.) and the feature middleware chain (auth, retry, circuit breaker). Using `$fetch` directly bypasses the platform layer and will fail in Tauri (CORS/sandboxing). The only exception is the `ISO3166Module` which is explicitly node-only.
|
||||
|
||||
For external APIs (non-Modrinth), pass the full base URL as the `api` field and set `skipAuth: true`:
|
||||
|
||||
```ts
|
||||
this.client.request<MyType>('/endpoint', {
|
||||
api: 'https://external-api.com',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: { data },
|
||||
skipAuth: true,
|
||||
})
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The client is provided to the component tree via DI (see the `dependency-injection` skill). Each app creates a platform-specific client and provides it at the root:
|
||||
|
||||
```ts
|
||||
// apps/frontend/src/app.vue (Nuxt)
|
||||
const client = new NuxtModrinthClient({ ... })
|
||||
provideModrinthClient(client)
|
||||
|
||||
// apps/app-frontend/src/App.vue (Tauri)
|
||||
const client = new TauriModrinthClient({ ... })
|
||||
provideModrinthClient(client)
|
||||
```
|
||||
|
||||
Components anywhere in the tree then inject it:
|
||||
|
||||
```ts
|
||||
const { labrinth, archon, kyros } = injectModrinthClient()
|
||||
|
||||
// Fetch data
|
||||
const project = await labrinth.projects_v3.get(projectId)
|
||||
|
||||
// Use with TanStack Query
|
||||
const { data } = useQuery({
|
||||
queryKey: ['project', projectId],
|
||||
queryFn: () => labrinth.projects_v3.get(projectId),
|
||||
})
|
||||
```
|
||||
|
||||
`provideModrinthClient` and `injectModrinthClient` are exported from `@modrinth/ui` (defined in `packages/ui/src/providers/api-client.ts`). The provider is typed as `AbstractModrinthClient`, so shared components in `packages/ui` work with any platform client.
|
||||
|
||||
## Types
|
||||
|
||||
Types must match 1:1 with how they are returned from the backend API they are fetching from. Do not reshape, rename, or omit fields — the types should be a direct representation of the API response.
|
||||
|
||||
Types are organized in namespaces that mirror the backend services:
|
||||
|
||||
```ts
|
||||
import type { Labrinth, Archon, Kyros, ISO3166 } from '@modrinth/api-client'
|
||||
|
||||
const project: Labrinth.Projects.v3.Project = ...
|
||||
const server: Archon.Servers.v0.Server = ...
|
||||
const auth: Archon.Websocket.v0.WSAuth = ...
|
||||
```
|
||||
|
||||
Each API has a `types.ts` in its module directory (`modules/labrinth/types.ts`, `modules/archon/types.ts`, etc.) using nested namespaces: `Namespace.Domain.Version.Type`.
|
||||
|
||||
## Features (Middleware)
|
||||
|
||||
Features wrap requests in a chain. Each feature can modify the request, retry, or short-circuit:
|
||||
|
||||
- **`AuthFeature`** — injects `Authorization: Bearer <token>`, supports async token providers
|
||||
- **`RetryFeature`** — exponential/linear/constant backoff, retries on 408/429/5xx and network errors
|
||||
- **`CircuitBreakerFeature`** — opens after N consecutive failures per endpoint, resets after timeout
|
||||
|
||||
## XHR Upload
|
||||
|
||||
File uploads use `XMLHttpRequest` for progress tracking (not available via `fetch`). The `upload()` method returns an `UploadHandle<T>`:
|
||||
|
||||
```ts
|
||||
interface UploadHandle<T> {
|
||||
promise: Promise<T>
|
||||
onProgress(callback: (progress: UploadProgress) => void): UploadHandle<T> // chainable
|
||||
cancel(): void
|
||||
}
|
||||
```
|
||||
|
||||
Supports two modes:
|
||||
|
||||
- **Single file** — `{ file: File | Blob }` sends with `Content-Type: application/octet-stream`
|
||||
- **FormData** — `{ formData: FormData }` for multipart uploads (browser/platform sets boundary)
|
||||
|
||||
Uploads go through the feature chain (auth, retry, etc.). Features detect uploads via `context.metadata.isUpload`.
|
||||
|
||||
### Usage Example (server file upload)
|
||||
|
||||
```ts
|
||||
const uploader = client.kyros.files_v0.uploadFile(path, file, {
|
||||
onProgress: ({ progress }) => {
|
||||
uploadProgress.value = Math.round(progress * 100)
|
||||
},
|
||||
})
|
||||
// Cancel if needed: uploader.cancel()
|
||||
await uploader.promise
|
||||
```
|
||||
|
||||
### Usage Example (version creation with FormData)
|
||||
|
||||
```ts
|
||||
const handle = client.labrinth.versions_v3.createVersion(draftVersion, files, projectType)
|
||||
handle.onProgress((progress) => {
|
||||
uploadProgress.value = progress
|
||||
})
|
||||
await handle.promise
|
||||
```
|
||||
|
||||
See `packages/ui/src/components/servers/files/upload/FileUploadDropdown.vue` and `apps/frontend/src/providers/version/manage-version-modal.ts` for real usage.
|
||||
|
||||
## WebSocket
|
||||
|
||||
WebSocket support is attached to `client.archon.sockets` (only on `GenericModrinthClient`). It provides event-based communication with Modrinth Hosting servers.
|
||||
|
||||
### Connection Flow
|
||||
|
||||
```
|
||||
client.archon.sockets.safeConnect(serverId)
|
||||
→ fetches JWT auth via archon.servers_v0.getWebSocketAuth()
|
||||
→ opens wss:// connection
|
||||
→ sends { event: 'auth', jwt: token }
|
||||
→ server responds with { event: 'auth-ok' }
|
||||
→ ready to receive events
|
||||
```
|
||||
|
||||
Auto-reconnects on unexpected disconnection with exponential backoff (base 1s, max 30s, up to 10 attempts).
|
||||
|
||||
### Subscribing to Events
|
||||
|
||||
```ts
|
||||
const unsub = client.archon.sockets.on(serverId, 'stats', (data) => {
|
||||
// data is typed as Archon.Websocket.v0.WSStatsEvent
|
||||
cpuUsage.value = data.cpu_percent
|
||||
})
|
||||
|
||||
// Clean up
|
||||
onUnmounted(() => {
|
||||
unsub()
|
||||
client.archon.sockets.disconnect(serverId)
|
||||
})
|
||||
```
|
||||
|
||||
Event types: `log`, `stats`, `power-state`, `uptime`, `backup-progress`, `installation-result`, `filesystem-ops`, `new-mod`, `auth-expiring`, `auth-incorrect`, `auth-ok`.
|
||||
|
||||
### Sending Commands
|
||||
|
||||
```ts
|
||||
client.archon.sockets.send(serverId, { event: 'command', cmd: '/say hello' })
|
||||
```
|
||||
|
||||
See `apps/frontend/src/pages/hosting/manage/[id].vue` for the full server panel WebSocket usage.
|
||||
|
||||
## Adding a New API Module
|
||||
|
||||
See the `api-module` skill (`.claude/skills/api-module/SKILL.md`) for step-by-step instructions.
|
||||
+159
-668
@@ -1,674 +1,165 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
||||
@@ -1,62 +1,63 @@
|
||||

|
||||
|
||||
# @modrinth/api-client
|
||||
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](LICENSE)
|
||||
[](LICENSE)
|
||||
|
||||
A flexible, type-safe API client for Modrinth's APIs (Labrinth, Kyros & Archon). Works across Node.js, browsers, Nuxt, and Tauri with a feature system for authentication, retries, circuit breaking and other custom processing of requests and responses.
|
||||
Platform-agnostic TypeScript client for Modrinth's API across Node.js, browsers, Nuxt, and Tauri.
|
||||
|
||||
**⚠️ We use this internally to power modrinth.com, Modrinth App, and Modrinth Hosting frontends. It may break without any notice, but you are welcome to use it.**
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pnpm add @modrinth/api-client
|
||||
# or
|
||||
npm install @modrinth/api-client
|
||||
# or
|
||||
yarn add @modrinth/api-client
|
||||
```
|
||||
|
||||
Tauri apps also need the optional peer dependency:
|
||||
|
||||
```bash
|
||||
pnpm add @modrinth/api-client @tauri-apps/plugin-http
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Plain JavaScript/Node.js
|
||||
### Generic Node.js or Browser Client
|
||||
|
||||
```typescript
|
||||
import { GenericModrinthClient, AuthFeature, ProjectV2 } from '@modrinth/api-client'
|
||||
```ts
|
||||
import { AuthFeature, GenericModrinthClient, type Labrinth } from '@modrinth/api-client'
|
||||
|
||||
const client = new GenericModrinthClient({
|
||||
userAgent: 'my-app/1.0.0',
|
||||
features: [new AuthFeature({ token: 'mrp_...' })],
|
||||
features: [new AuthFeature({ token: process.env.MODRINTH_TOKEN })],
|
||||
})
|
||||
|
||||
// Explicitly make a request using client.request
|
||||
const project: any = await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
const project: Labrinth.Projects.v2.Project = await client.labrinth.projects_v2.get('sodium')
|
||||
const members = await client.labrinth.projects_v3.getMembers(project.id)
|
||||
```
|
||||
|
||||
// Example for archon (Modrinth Hosting)
|
||||
const servers = await client.request('/servers?limit=10', { api: 'archon', version: 0 })
|
||||
You can still make direct requests through the same platform layer:
|
||||
|
||||
// Or use the provided wrappers for better type support.
|
||||
const project: ProjectV2 = await client.projects_v2.get('sodium')
|
||||
```ts
|
||||
const project = await client.request<Labrinth.Projects.v2.Project>('/project/sodium', {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
})
|
||||
```
|
||||
|
||||
### Nuxt
|
||||
|
||||
```typescript
|
||||
import { NuxtModrinthClient, AuthFeature, NuxtCircuitBreakerStorage } from '@modrinth/api-client'
|
||||
```ts
|
||||
import { AuthFeature, CircuitBreakerFeature, NuxtCircuitBreakerStorage, NuxtModrinthClient } from '@modrinth/api-client'
|
||||
|
||||
// Alternatively you can create a singleton of the client and provide it via DI.
|
||||
export const useModrinthClient = () => {
|
||||
export const useModrinthClient = async () => {
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
// example using the useAuth composable from our frontend, replace this with whatever you're using to store auth token
|
||||
const auth = await useAuth()
|
||||
|
||||
return new NuxtModrinthClient({
|
||||
userAgent: 'my-nuxt-app/1.0.0', // leave blank to use default user agent from fetch function
|
||||
userAgent: 'my-nuxt-app/1.0.0',
|
||||
rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
|
||||
features: [
|
||||
new AuthFeature({
|
||||
token: async () => auth.value.token,
|
||||
token: process.env.MODRINTH_TOKEN,
|
||||
}),
|
||||
new CircuitBreakerFeature({
|
||||
storage: new NuxtCircuitBreakerStorage(),
|
||||
@@ -64,116 +65,113 @@ export const useModrinthClient = () => {
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
const client = useModrinthClient()
|
||||
const project = await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
```
|
||||
|
||||
### Tauri
|
||||
|
||||
```typescript
|
||||
import { TauriModrinthClient, AuthFeature } from '@modrinth/api-client'
|
||||
```ts
|
||||
import { getVersion } from '@tauri-apps/api/app'
|
||||
import { AuthFeature, TauriModrinthClient } from '@modrinth/api-client'
|
||||
|
||||
const version = await getVersion()
|
||||
const client = new TauriModrinthClient({
|
||||
userAgent: `modrinth/theseus/${version} (support@modrinth.com)`,
|
||||
features: [new AuthFeature({ token: 'mrp_...' })],
|
||||
userAgent: async () => `modrinth/theseus/${await getVersion()} (support@modrinth.com)`,
|
||||
features: [new AuthFeature({ token: process.env.MODRINTH_TOKEN })],
|
||||
})
|
||||
|
||||
const project = await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
const project = await client.labrinth.projects_v2.get('sodium')
|
||||
```
|
||||
|
||||
### Overriding Base URLs
|
||||
## API Modules
|
||||
|
||||
By default, the client uses the production base URLs:
|
||||
Modules are available as nested properties on the client:
|
||||
|
||||
- `labrinthBaseUrl`: `https://api.modrinth.com/` (Labrinth API)
|
||||
- `archonBaseUrl`: `https://archon.modrinth.com/` (Archon/Servers API)
|
||||
```ts
|
||||
client.labrinth.projects_v2
|
||||
client.labrinth.projects_v3
|
||||
client.labrinth.versions_v3
|
||||
```
|
||||
|
||||
You can override these for staging environments or custom instances:
|
||||
Types are exported from the package root:
|
||||
|
||||
```typescript
|
||||
```ts
|
||||
import type { Labrinth } from '@modrinth/api-client'
|
||||
|
||||
const project: Labrinth.Projects.v3.Project = await client.labrinth.projects_v3.get('sodium')
|
||||
```
|
||||
|
||||
## Modrinth Hosting API Modules
|
||||
|
||||
- These modules are internal to Modrinth and are only supported inside the Modrinth Hosting panel in Modrinth App and on modrinth.com. They should not be expected to work in third-party clients today. We are discussing how to safely expose access to your own server through these APIs in the future.
|
||||
|
||||
## Base URLs
|
||||
|
||||
By default, the client uses Modrinth production services:
|
||||
|
||||
- `labrinthBaseUrl`: `https://api.modrinth.com`
|
||||
|
||||
Override them for staging or custom deployments:
|
||||
|
||||
```ts
|
||||
const client = new GenericModrinthClient({
|
||||
userAgent: 'my-app/1.0.0',
|
||||
labrinthBaseUrl: 'https://staging-api.modrinth.com/',
|
||||
archonBaseUrl: 'https://staging-archon.modrinth.com/',
|
||||
features: [new AuthFeature({ token: 'mrp_...' })],
|
||||
labrinthBaseUrl: 'https://staging-api.modrinth.com',
|
||||
})
|
||||
|
||||
// Now requests will use the staging URLs
|
||||
await client.request('/project/sodium', { api: 'labrinth', version: 2 })
|
||||
// -> https://staging-api.modrinth.com/v2/project/sodium
|
||||
```
|
||||
|
||||
You can also use custom URLs directly in requests:
|
||||
External APIs can be targeted per request by passing a full URL as `api` and disabling auth:
|
||||
|
||||
```typescript
|
||||
// One-off custom URL (useful for Kyros nodes or dynamic endpoints)
|
||||
await client.request('/some-endpoint', {
|
||||
api: 'https://eu-lim16.nodes.modrinth.com/',
|
||||
version: 0,
|
||||
```ts
|
||||
await client.request('/endpoint', {
|
||||
api: 'https://example.com',
|
||||
version: 1,
|
||||
skipAuth: true,
|
||||
})
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
### Authentication
|
||||
Features wrap requests before they reach the platform implementation:
|
||||
|
||||
Supports both static and dynamic tokens:
|
||||
```ts
|
||||
import { AuthFeature, CircuitBreakerFeature, RetryFeature } from '@modrinth/api-client'
|
||||
|
||||
```typescript
|
||||
// Static token
|
||||
new AuthFeature({ token: 'mrp_...' })
|
||||
|
||||
// Dynamic token (e.g., from auth state)
|
||||
const auth = await useAuth()
|
||||
new AuthFeature({
|
||||
token: async () => auth.value.token,
|
||||
const client = new GenericModrinthClient({
|
||||
features: [new AuthFeature({ token: async () => process.env.MODRINTH_TOKEN }), new RetryFeature({ maxAttempts: 3, backoffStrategy: 'exponential' }), new CircuitBreakerFeature({ maxFailures: 3, resetTimeout: 30_000 })],
|
||||
})
|
||||
```
|
||||
|
||||
### Retry
|
||||
Built-in features include authentication, node auth, retries, circuit breaking, panel version headers, and verbose logging.
|
||||
|
||||
Automatically retries failed requests with configurable backoff:
|
||||
## Uploads
|
||||
|
||||
```typescript
|
||||
new RetryFeature({
|
||||
maxAttempts: 3,
|
||||
backoffStrategy: 'exponential',
|
||||
initialDelay: 1000,
|
||||
maxDelay: 15000,
|
||||
Upload endpoints return an `UploadHandle<T>` with progress and cancellation support:
|
||||
|
||||
```ts
|
||||
const upload = client.kyros.files_v0.uploadFile(path, file)
|
||||
|
||||
upload.onProgress(({ progress }) => {
|
||||
console.log(Math.round(progress * 100))
|
||||
})
|
||||
|
||||
await upload.promise
|
||||
```
|
||||
|
||||
### Circuit Breaker
|
||||
Uploads use `XMLHttpRequest` for progress tracking and are only available in browser-capable contexts. `NuxtModrinthClient.upload()` throws during SSR.
|
||||
|
||||
Prevents cascade failures by opening circuits after repeated failures:
|
||||
## Third-Party API Typings
|
||||
|
||||
```typescript
|
||||
new CircuitBreakerFeature({
|
||||
maxFailures: 3,
|
||||
resetTimeout: 30000,
|
||||
failureStatusCodes: [500, 502, 503, 504],
|
||||
})
|
||||
- This package also includes some third-party API modules and typings used by Modrinth internals. They are not part of the stable public API surface and should be used at your own risk.
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
pnpm --filter @modrinth/api-client build
|
||||
pnpm --filter @modrinth/api-client lint
|
||||
# or pnpm prepr:frontend:lib in turborepo root.
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
This package is **self-documenting** through TypeScript types and JSDoc comments. Use your IDE's IntelliSense to explore available methods, classes, and configuration options.
|
||||
|
||||
For Modrinth API endpoints and routes, refer to the [Modrinth API Documentation](https://docs.modrinth.com).
|
||||
|
||||
## Contributing
|
||||
|
||||
- Modules are available in the `modules/<api>/...` folders.
|
||||
- When a module has different versions available, you should do it like so: `modules/labrinth/projects/v2.ts` etc.
|
||||
- Types for a module's requests should be made available in `modules/<api>/module/types.ts` or `.../types/v2.ts`.
|
||||
- You should expose these types in the `modules/types.ts` file.
|
||||
- When creating a new module, add it to the `modules/index.ts`'s `MODULE_REGISTRY` for it to become available in the api client class.
|
||||
|
||||
Dont forget to run `pnpm fix` before committing.
|
||||
When adding a module, add it to `src/modules/index.ts` so it is included in the typed client structure.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details.
|
||||
Licensed under LGPL-3.0. See [LICENSE](LICENSE).
|
||||
|
||||
@@ -1,2 +1,7 @@
|
||||
import config from '@modrinth/tooling-config/eslint/nuxt.mjs'
|
||||
export default config
|
||||
|
||||
export default config.append([
|
||||
{
|
||||
ignores: ['dist/'],
|
||||
},
|
||||
])
|
||||
|
||||
@@ -1,9 +1,41 @@
|
||||
{
|
||||
"name": "@modrinth/api-client",
|
||||
"version": "0.1.0",
|
||||
"version": "0.0.0",
|
||||
"description": "An API client for Modrinth's API for use in nuxt, tauri and plain node/browser environments.",
|
||||
"main": "./src/index.ts",
|
||||
"license": "LGPL-3.0-only",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/modrinth/code.git",
|
||||
"directory": "packages/api-client"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/modrinth/code/issues"
|
||||
},
|
||||
"homepage": "https://github.com/modrinth/code/tree/main/packages/api-client#readme",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "node --eval \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||
"build": "pnpm run clean && esbuild src/index.ts --bundle --format=esm --platform=neutral --target=es2020 --minify --legal-comments=none --outfile=dist/index.js --external:ofetch --external:mitt --external:@tauri-apps/plugin-http && tsc -p tsconfig.build.json",
|
||||
"prepare": "pnpm run build",
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"fix": "eslint . --fix && prettier --write ."
|
||||
},
|
||||
@@ -12,7 +44,10 @@
|
||||
"ofetch": "^1.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@modrinth/tooling-config": "workspace:*"
|
||||
"@modrinth/tooling-config": "workspace:*",
|
||||
"@tauri-apps/plugin-http": "^2.0.0",
|
||||
"esbuild": "0.27.2",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@tauri-apps/plugin-http": "^2.0.0"
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import type { InferredClientModules } from '../modules'
|
||||
import { buildModuleStructure } from '../modules'
|
||||
import type { ClientConfig } from '../types/client'
|
||||
import type { BaseUrlConfig, ClientConfig } from '../types/client'
|
||||
import type { RequestContext, RequestOptions } from '../types/request'
|
||||
import type { UploadMetadata, UploadProgress, UploadRequestOptions } from '../types/upload'
|
||||
import type { AbstractFeature } from './abstract-feature'
|
||||
import type { AbstractModule } from './abstract-module'
|
||||
import type { AbstractSyncClient } from './abstract-sync'
|
||||
import { AbstractUploadClient } from './abstract-upload-client'
|
||||
import type { AbstractWebSocketClient } from './abstract-websocket'
|
||||
import { ModrinthApiError, ModrinthServerError } from './errors'
|
||||
|
||||
type ArchonClientModules = Omit<InferredClientModules['archon'], 'backups_v1'> & {
|
||||
/** @deprecated Use `backups_queue_v1` for the Backups Queue API. */
|
||||
backups_v1: InferredClientModules['archon']['backups_v1']
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract base client for Modrinth APIs
|
||||
*/
|
||||
@@ -27,9 +33,16 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
private _moduleNamespaces: Map<string, Record<string, AbstractModule>> = new Map()
|
||||
|
||||
public readonly labrinth!: InferredClientModules['labrinth']
|
||||
public readonly archon!: InferredClientModules['archon'] & { sockets: AbstractWebSocketClient }
|
||||
public readonly archon!: ArchonClientModules & {
|
||||
sockets: AbstractWebSocketClient
|
||||
sync: AbstractSyncClient
|
||||
}
|
||||
public readonly kyros!: InferredClientModules['kyros']
|
||||
public readonly iso3166!: InferredClientModules['iso3166']
|
||||
public readonly mclogs!: InferredClientModules['mclogs']
|
||||
public readonly launchermeta!: InferredClientModules['launchermeta']
|
||||
public readonly paper!: InferredClientModules['paper']
|
||||
public readonly purpur!: InferredClientModules['purpur']
|
||||
|
||||
constructor(config: ClientConfig) {
|
||||
super()
|
||||
@@ -107,25 +120,28 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
async request<T>(path: string, options: RequestOptions): Promise<T> {
|
||||
let baseUrl: string
|
||||
if (options.api === 'labrinth') {
|
||||
baseUrl = this.config.labrinthBaseUrl!
|
||||
baseUrl = this.resolveBaseUrl(this.config.labrinthBaseUrl!)
|
||||
} else if (options.api === 'archon') {
|
||||
baseUrl = this.config.archonBaseUrl!
|
||||
baseUrl = this.resolveBaseUrl(this.config.archonBaseUrl!)
|
||||
} else {
|
||||
baseUrl = options.api
|
||||
}
|
||||
|
||||
const url = this.buildUrl(path, baseUrl, options.version)
|
||||
|
||||
const defaultHeaders = await this.buildDefaultHeaders()
|
||||
|
||||
// Merge options with defaults
|
||||
const mergedOptions: RequestOptions = {
|
||||
method: 'GET',
|
||||
timeout: this.config.timeout,
|
||||
...options,
|
||||
headers: {
|
||||
...this.buildDefaultHeaders(),
|
||||
...defaultHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
this.attachArchonSentryCaptureHeader(mergedOptions)
|
||||
|
||||
const headers = mergedOptions.headers
|
||||
if (headers && 'Content-Type' in headers && headers['Content-Type'] === '') {
|
||||
@@ -148,13 +164,55 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
}
|
||||
}
|
||||
|
||||
async stream(path: string, options: RequestOptions): Promise<ReadableStream<Uint8Array>> {
|
||||
let baseUrl: string
|
||||
if (options.api === 'labrinth') {
|
||||
baseUrl = this.resolveBaseUrl(this.config.labrinthBaseUrl!)
|
||||
} else if (options.api === 'archon') {
|
||||
baseUrl = this.resolveBaseUrl(this.config.archonBaseUrl!)
|
||||
} else {
|
||||
baseUrl = options.api
|
||||
}
|
||||
|
||||
const url = this.buildUrl(path, baseUrl, options.version)
|
||||
const defaultHeaders = await this.buildDefaultHeaders()
|
||||
const mergedOptions: RequestOptions = {
|
||||
method: 'GET',
|
||||
retry: false,
|
||||
circuitBreaker: false,
|
||||
...options,
|
||||
headers: {
|
||||
...defaultHeaders,
|
||||
Accept: 'text/event-stream',
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
this.attachArchonSentryCaptureHeader(mergedOptions)
|
||||
|
||||
const context = this.buildContext(url, path, mergedOptions)
|
||||
|
||||
try {
|
||||
return await this.executeFeatureChain<ReadableStream<Uint8Array>>(context, () =>
|
||||
this.executeStreamRequest(context.url, context.options),
|
||||
)
|
||||
} catch (error) {
|
||||
const apiError = this.normalizeError(error, context)
|
||||
await this.config.hooks?.onError?.(apiError, context)
|
||||
|
||||
throw apiError
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the feature chain and the actual request
|
||||
*
|
||||
* Features are executed in order, with each feature calling next() to continue.
|
||||
* The last "feature" in the chain is the actual request execution.
|
||||
*/
|
||||
protected async executeFeatureChain<T>(context: RequestContext): Promise<T> {
|
||||
protected async executeFeatureChain<T>(
|
||||
context: RequestContext,
|
||||
executeTerminal: () => Promise<T> = () => this.executeRequest<T>(context.url, context.options),
|
||||
): Promise<T> {
|
||||
// Filter to only features that should apply
|
||||
const applicableFeatures = this.features.filter((feature) => feature.shouldApply(context))
|
||||
|
||||
@@ -172,7 +230,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
} else {
|
||||
// We've reached the end of the chain, execute the actual request
|
||||
await this.config.hooks?.onRequest?.(context)
|
||||
return this.executeRequest<T>(context.url, context.options)
|
||||
return executeTerminal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,6 +289,10 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
return `${base}${versionPath}${cleanPath}`
|
||||
}
|
||||
|
||||
protected resolveBaseUrl(baseUrl: BaseUrlConfig): string {
|
||||
return typeof baseUrl === 'function' ? baseUrl() : baseUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the request context
|
||||
*/
|
||||
@@ -296,19 +358,40 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
* Subclasses can override this to add platform-specific headers
|
||||
* (e.g., Nuxt rate limit key)
|
||||
*/
|
||||
protected buildDefaultHeaders(): Record<string, string> {
|
||||
protected async buildDefaultHeaders(): Promise<Record<string, string>> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
...this.config.headers,
|
||||
}
|
||||
|
||||
if (this.config.userAgent) {
|
||||
headers['User-Agent'] = this.config.userAgent
|
||||
const userAgent = await this.resolveUserAgent()
|
||||
if (userAgent) {
|
||||
headers['User-Agent'] = userAgent
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private async resolveUserAgent(): Promise<string | undefined> {
|
||||
const userAgent = this.config.userAgent
|
||||
return typeof userAgent === 'function' ? await userAgent() : userAgent
|
||||
}
|
||||
|
||||
protected attachArchonSentryCaptureHeader(options: RequestOptions): void {
|
||||
if (options.api !== 'archon' || !options.headers || !this.shouldCaptureArchonRequests()) {
|
||||
return
|
||||
}
|
||||
|
||||
options.headers['modrinth-sentry-capture'] = '1'
|
||||
}
|
||||
|
||||
private shouldCaptureArchonRequests(): boolean {
|
||||
const archonSentryCapture = this.config.archonSentryCapture
|
||||
return typeof archonSentryCapture === 'function'
|
||||
? archonSentryCapture()
|
||||
: archonSentryCapture === true
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the actual HTTP request
|
||||
*
|
||||
@@ -321,6 +404,11 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
*/
|
||||
protected abstract executeRequest<T>(url: string, options: RequestOptions): Promise<T>
|
||||
|
||||
protected abstract executeStreamRequest(
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
): Promise<ReadableStream<Uint8Array>>
|
||||
|
||||
/**
|
||||
* Execute the actual XHR upload
|
||||
*
|
||||
@@ -379,7 +467,7 @@ export abstract class AbstractModrinthClient extends AbstractUploadClient {
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new GenericModrinthClient()
|
||||
* client.addFeature(new AuthFeature({ token: 'mrp_...' }))
|
||||
* client.addFeature(new AuthFeature({ token: async () => getOAuthToken() }))
|
||||
* client.addFeature(new RetryFeature({ maxAttempts: 3 }))
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
import type mitt from 'mitt'
|
||||
|
||||
import type { Archon } from '../modules/archon/types'
|
||||
import type { RequestOptions } from '../types/request'
|
||||
|
||||
export type SyncEventType = Archon.Sync.v1.SyncEvent['type']
|
||||
|
||||
export type SyncEventOfType<E extends SyncEventType> = Extract<
|
||||
Archon.Sync.v1.SyncEvent,
|
||||
{ type: E }
|
||||
>
|
||||
|
||||
export type SyncEventHandler<E extends Archon.Sync.v1.SyncEvent = Archon.Sync.v1.SyncEvent> = (
|
||||
event: E,
|
||||
) => void
|
||||
|
||||
export type SyncStatusState =
|
||||
| 'idle'
|
||||
| 'connecting'
|
||||
| 'connected'
|
||||
| 'reconnecting'
|
||||
| 'disconnected'
|
||||
| 'error'
|
||||
|
||||
export type SyncStatus = {
|
||||
state: SyncStatusState
|
||||
connected: boolean
|
||||
reconnecting: boolean
|
||||
reconnectAttempts: number
|
||||
retryDelay: number
|
||||
lastEventId?: string
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export type SyncStatusHandler = (status: SyncStatus) => void
|
||||
|
||||
export type SyncConnectOptions = {
|
||||
intent?: Archon.Sync.v1.SyncIntent
|
||||
force?: boolean
|
||||
}
|
||||
|
||||
export type SyncConnection = {
|
||||
serverId: string
|
||||
intent: Archon.Sync.v1.SyncIntent
|
||||
controller?: AbortController
|
||||
reconnectAttempts: number
|
||||
reconnectTimer?: ReturnType<typeof setTimeout>
|
||||
reconnectResolve?: () => void
|
||||
retryDelay: number
|
||||
lastEventId?: string
|
||||
stopped: boolean
|
||||
status: SyncStatusState
|
||||
error?: unknown
|
||||
}
|
||||
|
||||
export type SyncEmitterEvents = Record<string, unknown>
|
||||
|
||||
export abstract class AbstractSyncClient {
|
||||
protected connections = new Map<string, SyncConnection>()
|
||||
protected abstract emitter: ReturnType<typeof mitt<SyncEmitterEvents>>
|
||||
|
||||
constructor(
|
||||
protected client: {
|
||||
stream: (path: string, options: RequestOptions) => Promise<ReadableStream<Uint8Array>>
|
||||
},
|
||||
) {}
|
||||
|
||||
abstract safeConnectServer(serverId: string, options?: SyncConnectOptions): Promise<void>
|
||||
|
||||
abstract disconnect(serverId: string): void
|
||||
|
||||
abstract disconnectAll(): void
|
||||
|
||||
on<E extends SyncEventType>(
|
||||
serverId: string,
|
||||
eventType: E,
|
||||
handler: SyncEventHandler<SyncEventOfType<E>>,
|
||||
): () => void {
|
||||
const eventKey = this.getEventKey(serverId, eventType)
|
||||
const wrapped = handler as (event: unknown) => void
|
||||
|
||||
this.emitter.on(eventKey, wrapped)
|
||||
|
||||
return () => {
|
||||
this.emitter.off(eventKey, wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
onAny(serverId: string, handler: SyncEventHandler): () => void {
|
||||
const eventKey = this.getAnyEventKey(serverId)
|
||||
const wrapped = handler as (event: unknown) => void
|
||||
|
||||
this.emitter.on(eventKey, wrapped)
|
||||
|
||||
return () => {
|
||||
this.emitter.off(eventKey, wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
onStatus(serverId: string, handler: SyncStatusHandler): () => void {
|
||||
const eventKey = this.getStatusEventKey(serverId)
|
||||
const wrapped = handler as (event: unknown) => void
|
||||
|
||||
this.emitter.on(eventKey, wrapped)
|
||||
|
||||
return () => {
|
||||
this.emitter.off(eventKey, wrapped)
|
||||
}
|
||||
}
|
||||
|
||||
getStatus(serverId: string): SyncStatus | null {
|
||||
const connection = this.connections.get(serverId)
|
||||
if (!connection) return null
|
||||
|
||||
return this.connectionToStatus(connection)
|
||||
}
|
||||
|
||||
protected emitSyncEvent(serverId: string, event: Archon.Sync.v1.SyncEvent): void {
|
||||
this.emitter.emit(this.getEventKey(serverId, event.type), event)
|
||||
this.emitter.emit(this.getAnyEventKey(serverId), event)
|
||||
}
|
||||
|
||||
protected updateStatus(
|
||||
connection: SyncConnection,
|
||||
status: SyncStatusState,
|
||||
error?: unknown,
|
||||
): void {
|
||||
connection.status = status
|
||||
connection.error = error
|
||||
this.emitter.emit(
|
||||
this.getStatusEventKey(connection.serverId),
|
||||
this.connectionToStatus(connection),
|
||||
)
|
||||
}
|
||||
|
||||
protected clearListeners(serverId: string): void {
|
||||
this.emitter.all.forEach((_handlers, type) => {
|
||||
if (type.toString().startsWith(`${serverId}:`)) {
|
||||
this.emitter.all.delete(type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
protected connectionToStatus(connection: SyncConnection): SyncStatus {
|
||||
return {
|
||||
state: connection.status,
|
||||
connected: connection.status === 'connected',
|
||||
reconnecting: connection.status === 'reconnecting',
|
||||
reconnectAttempts: connection.reconnectAttempts,
|
||||
retryDelay: connection.retryDelay,
|
||||
lastEventId: connection.lastEventId,
|
||||
error: connection.error,
|
||||
}
|
||||
}
|
||||
|
||||
private getEventKey(serverId: string, eventType: string): string {
|
||||
return `${serverId}:${eventType}`
|
||||
}
|
||||
|
||||
private getAnyEventKey(serverId: string): string {
|
||||
return `${serverId}:*`
|
||||
}
|
||||
|
||||
private getStatusEventKey(serverId: string): string {
|
||||
return `${serverId}:__status`
|
||||
}
|
||||
}
|
||||
@@ -33,14 +33,8 @@ export interface AuthConfig extends FeatureConfig {
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Static token
|
||||
* const auth = new AuthFeature({
|
||||
* token: 'mrp_...'
|
||||
* })
|
||||
*
|
||||
* // Dynamic token (e.g., from auth state)
|
||||
* const auth = new AuthFeature({
|
||||
* token: async () => await getAuthToken()
|
||||
* token: async () => process.env.MODRINTH_TOKEN
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -8,6 +8,8 @@ import type { RequestContext } from '../types/request'
|
||||
export interface NodeAuth {
|
||||
/** Node instance URL (e.g., "node-xyz.modrinth.com/modrinth/v0/fs") */
|
||||
url: string
|
||||
/** Base URL without path suffix (e.g., "node-xyz.modrinth.com") — used when available */
|
||||
baseUrl?: string
|
||||
/** JWT token */
|
||||
token: string
|
||||
}
|
||||
@@ -105,7 +107,7 @@ export class NodeAuthFeature extends AbstractFeature {
|
||||
}
|
||||
|
||||
private applyAuth(context: RequestContext, auth: NodeAuth): void {
|
||||
const baseUrl = `https://${auth.url.replace('v0/fs', '')}`
|
||||
const baseUrl = `https://${auth.url.replace(/\/modrinth\/v\d+\/fs\/?$/, '')}`
|
||||
context.url = this.buildUrl(context.path, baseUrl, context.options.version)
|
||||
|
||||
context.options.headers = {
|
||||
|
||||
@@ -12,7 +12,7 @@ export class PanelVersionFeature extends AbstractFeature {
|
||||
return next()
|
||||
}
|
||||
|
||||
shouldApply(_: RequestContext): boolean {
|
||||
return true
|
||||
shouldApply(context: RequestContext): boolean {
|
||||
return context.options.api === 'labrinth' || context.options.api === 'archon'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,50 @@ export class VerboseLoggingFeature extends AbstractFeature {
|
||||
}
|
||||
return result
|
||||
} catch (error) {
|
||||
console.debug(`${prefix} ${context.url} FAILED`)
|
||||
const details = formatErrorDetails(error)
|
||||
console.debug(`${prefix} ${context.url} FAILED${details ? ` — ${details}` : ''}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatErrorDetails(error: unknown): string {
|
||||
if (!error || typeof error !== 'object') {
|
||||
return typeof error === 'string' ? error : ''
|
||||
}
|
||||
|
||||
const err = error as {
|
||||
status?: number
|
||||
statusCode?: number
|
||||
statusText?: string
|
||||
message?: string
|
||||
data?: unknown
|
||||
responseData?: unknown
|
||||
originalError?: unknown
|
||||
response?: { status?: number; statusText?: string; _data?: unknown }
|
||||
}
|
||||
|
||||
const status = err.status ?? err.statusCode ?? err.response?.status
|
||||
const statusText = err.statusText ?? err.response?.statusText
|
||||
const data = err.responseData ?? err.data ?? err.response?._data
|
||||
|
||||
const parts: string[] = []
|
||||
if (status !== undefined) {
|
||||
parts.push(statusText ? `${status} ${statusText}` : String(status))
|
||||
}
|
||||
if (data !== undefined) {
|
||||
parts.push(`body: ${safeStringify(data)}`)
|
||||
} else if (err.message) {
|
||||
parts.push(err.message)
|
||||
}
|
||||
return parts.join(' ')
|
||||
}
|
||||
|
||||
function safeStringify(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
try {
|
||||
return JSON.stringify(value)
|
||||
} catch {
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
export { AbstractModrinthClient } from './core/abstract-client'
|
||||
export { AbstractFeature, type FeatureConfig } from './core/abstract-feature'
|
||||
export {
|
||||
AbstractSyncClient,
|
||||
type SyncConnection,
|
||||
type SyncConnectOptions,
|
||||
type SyncEventHandler,
|
||||
type SyncEventOfType,
|
||||
type SyncEventType,
|
||||
type SyncStatus,
|
||||
type SyncStatusHandler,
|
||||
type SyncStatusState,
|
||||
} from './core/abstract-sync'
|
||||
export { AbstractUploadClient } from './core/abstract-upload-client'
|
||||
export {
|
||||
AbstractWebSocketClient,
|
||||
@@ -25,9 +36,18 @@ export * from './modules/types'
|
||||
export { GenericModrinthClient } from './platform/generic'
|
||||
export type { NuxtClientConfig } from './platform/nuxt'
|
||||
export { NuxtCircuitBreakerStorage, NuxtModrinthClient } from './platform/nuxt'
|
||||
export { GenericSyncClient } from './platform/sync-generic'
|
||||
export type { TauriClientConfig } from './platform/tauri'
|
||||
export { TauriModrinthClient } from './platform/tauri'
|
||||
export { XHRUploadClient } from './platform/xhr-upload-client'
|
||||
export { clearNodeAuthState, nodeAuthState, setNodeAuthState } from './state/node-auth'
|
||||
export * from './types'
|
||||
export { withJWTRetry } from './utils/jwt-retry'
|
||||
export {
|
||||
type ParsedSseEvent,
|
||||
type ParsedSseItem,
|
||||
type ParsedSseRetry,
|
||||
parseSyncEventData,
|
||||
SseParser,
|
||||
} from './utils/sse'
|
||||
export type { Override, RawDecimal } from './utils/types'
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonActionsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_actions_v1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get server action log entries.
|
||||
* GET /v1/servers/:server_id/action-log
|
||||
*/
|
||||
public async list(
|
||||
serverId: string,
|
||||
options: Archon.Actions.v1.ListActionLogOptions = {},
|
||||
): Promise<Archon.Actions.v1.ActionLogResponse> {
|
||||
const params: Record<string, string | number> = {}
|
||||
if (options.filter) params.filter = JSON.stringify(options.filter)
|
||||
if (options.limit !== undefined) params.limit = options.limit
|
||||
if (options.offset !== undefined) params.offset = options.offset
|
||||
if (options.order !== undefined) params.order = options.order
|
||||
if (options.min_datetime !== undefined) params.min_datetime = options.min_datetime
|
||||
if (options.max_datetime !== undefined) params.max_datetime = options.max_datetime
|
||||
|
||||
return this.client.request<Archon.Actions.v1.ActionLogResponse>(
|
||||
`/servers/${serverId}/action-log`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
params: Object.keys(params).length > 0 ? params : undefined,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonBackupsQueueV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_backups_queue_v1'
|
||||
}
|
||||
|
||||
/** GET /v1/servers/:server_id/worlds/:world_id/backups-queue */
|
||||
public async list(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
): Promise<Archon.BackupsQueue.v1.BackupsQueueResponse> {
|
||||
return this.client.request<Archon.BackupsQueue.v1.BackupsQueueResponse>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue`,
|
||||
{ api: 'archon', version: 1, method: 'GET' },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue */
|
||||
public async create(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.BackupsQueue.v1.BackupRequest,
|
||||
): Promise<Archon.BackupsQueue.v1.PostBackupQueueResponse> {
|
||||
return this.client.request<Archon.BackupsQueue.v1.PostBackupQueueResponse>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue`,
|
||||
{ api: 'archon', version: 1, method: 'POST', body: request },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/create/:operation_id/ack */
|
||||
public async ackCreate(serverId: string, worldId: string, operationId: number): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/history/create/${operationId}/ack`,
|
||||
{ api: 'archon', version: 1, method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/create/:operation_id/cancel */
|
||||
public async cancelCreate(serverId: string, worldId: string, operationId: number): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/history/create/${operationId}/cancel`,
|
||||
{ api: 'archon', version: 1, method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/restore/:operation_id/ack */
|
||||
public async ackRestore(serverId: string, worldId: string, operationId: number): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/history/restore/${operationId}/ack`,
|
||||
{ api: 'archon', version: 1, method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/history/restore/:operation_id/cancel */
|
||||
public async cancelRestore(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
operationId: number,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/history/restore/${operationId}/cancel`,
|
||||
{ api: 'archon', version: 1, method: 'POST' },
|
||||
)
|
||||
}
|
||||
|
||||
/** DELETE /v1/servers/:server_id/worlds/:world_id/backups-queue/:backup_id */
|
||||
public async delete(serverId: string, worldId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/${backupId}`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'DELETE',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/delete-many */
|
||||
public async deleteMany(serverId: string, worldId: string, backupIds: string[]): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/delete-many`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: { backup_ids: backupIds } satisfies Archon.BackupsQueue.v1.DeleteManyBackupRequest,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/:backup_id/restore */
|
||||
public async restore(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
backupId: string,
|
||||
request: Archon.BackupsQueue.v1.BackupRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/${backupId}/restore`,
|
||||
{ api: 'archon', version: 1, method: 'POST', body: request },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups-queue/:backup_id/retry */
|
||||
public async retry(serverId: string, worldId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups-queue/${backupId}/retry`,
|
||||
{ api: 'archon', version: 1, method: 'POST' },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonBackupsV0Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_backups_v0'
|
||||
}
|
||||
|
||||
/** GET /modrinth/v0/servers/:server_id/backups */
|
||||
public async list(serverId: string): Promise<Archon.Backups.v1.Backup[]> {
|
||||
return this.client.request<Archon.Backups.v1.Backup[]>(`/servers/${serverId}/backups`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/** GET /modrinth/v0/servers/:server_id/backups/:backup_id */
|
||||
public async get(serverId: string, backupId: string): Promise<Archon.Backups.v1.Backup> {
|
||||
return this.client.request<Archon.Backups.v1.Backup>(
|
||||
`/servers/${serverId}/backups/${backupId}`,
|
||||
{ api: 'archon', version: 'modrinth/v0', method: 'GET' },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /modrinth/v0/servers/:server_id/backups */
|
||||
public async create(
|
||||
serverId: string,
|
||||
request: Archon.Backups.v1.BackupRequest,
|
||||
): Promise<Archon.Backups.v1.PostBackupResponse> {
|
||||
return this.client.request<Archon.Backups.v1.PostBackupResponse>(
|
||||
`/servers/${serverId}/backups`,
|
||||
{ api: 'archon', version: 'modrinth/v0', method: 'POST', body: request },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/restore */
|
||||
public async restore(serverId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/restore`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/** DELETE /modrinth/v0/servers/:server_id/backups/:backup_id */
|
||||
public async delete(serverId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /modrinth/v0/servers/:server_id/backups/:backup_id/retry */
|
||||
public async retry(serverId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}/retry`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/** PATCH /modrinth/v0/servers/:server_id/backups/:backup_id */
|
||||
public async rename(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
request: Archon.Backups.v1.PatchBackup,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/backups/${backupId}`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'PATCH',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,101 +2,108 @@ import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
/**
|
||||
* Default world ID - Uuid::nil() which the backend treats as "first/active world"
|
||||
* See: apps/archon/src/routes/v1/servers/worlds/mod.rs - world_id_nullish()
|
||||
* TODO:
|
||||
* - Make sure world ID is being passed before we ship worlds.
|
||||
* - The schema will change when Backups v4 (routes stay as v1) so remember to do that.
|
||||
* @deprecated Use `client.archon.backups_queue_v1` (Backups Queue API) instead.
|
||||
*/
|
||||
const DEFAULT_WORLD_ID: string = '00000000-0000-0000-0000-000000000000' as const
|
||||
|
||||
export class ArchonBackupsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_backups_v1'
|
||||
}
|
||||
|
||||
/** GET /v1/:server_id/worlds/:world_id/backups */
|
||||
public async list(
|
||||
serverId: string,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<Archon.Backups.v1.Backup[]> {
|
||||
/**
|
||||
* @deprecated Use `client.archon.backups_queue_v1.list` instead.
|
||||
*/
|
||||
/** GET /v1/servers/:server_id/worlds/:world_id/backups */
|
||||
public async list(serverId: string, worldId: string): Promise<Archon.Backups.v1.Backup[]> {
|
||||
return this.client.request<Archon.Backups.v1.Backup[]>(
|
||||
`/${serverId}/worlds/${worldId}/backups`,
|
||||
`/servers/${serverId}/worlds/${worldId}/backups`,
|
||||
{ api: 'archon', version: 1, method: 'GET' },
|
||||
)
|
||||
}
|
||||
|
||||
/** GET /v1/:server_id/worlds/:world_id/backups/:backup_id */
|
||||
/**
|
||||
* @deprecated Use `client.archon.backups_queue_v1.list` instead.
|
||||
*/
|
||||
/** GET /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */
|
||||
public async get(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
backupId: string,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<Archon.Backups.v1.Backup> {
|
||||
return this.client.request<Archon.Backups.v1.Backup>(
|
||||
`/${serverId}/worlds/${worldId}/backups/${backupId}`,
|
||||
`/servers/${serverId}/worlds/${worldId}/backups/${backupId}`,
|
||||
{ api: 'archon', version: 1, method: 'GET' },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/backups */
|
||||
/**
|
||||
* @deprecated Use `client.archon.backups_queue_v1.create` instead.
|
||||
*/
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups */
|
||||
public async create(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Backups.v1.BackupRequest,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<Archon.Backups.v1.PostBackupResponse> {
|
||||
return this.client.request<Archon.Backups.v1.PostBackupResponse>(
|
||||
`/${serverId}/worlds/${worldId}/backups`,
|
||||
`/servers/${serverId}/worlds/${worldId}/backups`,
|
||||
{ api: 'archon', version: 1, method: 'POST', body: request },
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/restore */
|
||||
public async restore(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/restore`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
})
|
||||
/**
|
||||
* @deprecated Use `client.archon.backups_queue_v1.restore` instead.
|
||||
*/
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups/:backup_id/restore */
|
||||
public async restore(serverId: string, worldId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups/${backupId}/restore`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** DELETE /v1/:server_id/worlds/:world_id/backups/:backup_id */
|
||||
public async delete(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}`, {
|
||||
/**
|
||||
* @deprecated Use `client.archon.backups_queue_v1.delete` for backup deletion, or
|
||||
* `client.archon.backups_queue_v1.cancelCreate` / `cancelRestore` for active operations.
|
||||
*/
|
||||
/** DELETE /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */
|
||||
public async delete(serverId: string, worldId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/backups/${backupId}`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/backups/:backup_id/retry */
|
||||
public async retry(
|
||||
serverId: string,
|
||||
backupId: string,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}/retry`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
})
|
||||
/**
|
||||
* @deprecated Use `client.archon.backups_queue_v1.retry` instead.
|
||||
*/
|
||||
/** POST /v1/servers/:server_id/worlds/:world_id/backups/:backup_id/retry */
|
||||
public async retry(serverId: string, worldId: string, backupId: string): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/backups/${backupId}/retry`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** PATCH /v1/:server_id/worlds/:world_id/backups/:backup_id */
|
||||
/**
|
||||
* @deprecated Legacy backups only; no queue equivalent. Prefer renaming via other supported flows if available.
|
||||
*/
|
||||
/** PATCH /v1/servers/:server_id/worlds/:world_id/backups/:backup_id */
|
||||
public async rename(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
backupId: string,
|
||||
request: Archon.Backups.v1.PatchBackup,
|
||||
worldId: string = DEFAULT_WORLD_ID,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/${serverId}/worlds/${worldId}/backups/${backupId}`, {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/backups/${backupId}`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'PATCH',
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonContentV0Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_content_v0'
|
||||
}
|
||||
|
||||
/** GET /modrinth/v0/servers/:server_id/mods */
|
||||
public async list(serverId: string): Promise<Archon.Content.v0.Mod[]> {
|
||||
return this.client.request<Archon.Content.v0.Mod[]>(`/servers/${serverId}/mods`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /modrinth/v0/servers/:server_id/mods */
|
||||
public async install(
|
||||
serverId: string,
|
||||
request: Archon.Content.v0.InstallModRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/mods`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /modrinth/v0/servers/:server_id/deleteMod */
|
||||
public async delete(
|
||||
serverId: string,
|
||||
request: Archon.Content.v0.DeleteModRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/deleteMod`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /modrinth/v0/servers/:server_id/mods/update */
|
||||
public async update(
|
||||
serverId: string,
|
||||
request: Archon.Content.v0.UpdateModRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/mods/update`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonContentV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_content_v1'
|
||||
}
|
||||
|
||||
/** GET /v1/:server_id/worlds/:world_id/addons */
|
||||
public async getAddons(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
options?: {
|
||||
from_modpack?: boolean
|
||||
disabled?: boolean
|
||||
addons?: boolean
|
||||
updates?: boolean
|
||||
},
|
||||
): Promise<Archon.Content.v1.Addons> {
|
||||
const params = new URLSearchParams()
|
||||
if (options?.from_modpack !== undefined)
|
||||
params.set('from_modpack', String(options.from_modpack))
|
||||
if (options?.disabled !== undefined) params.set('disabled', String(options.disabled))
|
||||
if (options?.addons !== undefined) params.set('addons', String(options.addons))
|
||||
if (options?.updates !== undefined) params.set('updates', String(options.updates))
|
||||
const query = params.toString()
|
||||
|
||||
return this.client.request<Archon.Content.v1.Addons>(
|
||||
`/servers/${serverId}/worlds/${worldId}/addons${query ? `?${query}` : ''}`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons */
|
||||
public async addAddon(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Content.v1.AddAddonRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/install-many */
|
||||
public async addAddons(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
addons: Archon.Content.v1.AddAddonRequest[],
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/install-many`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: addons satisfies Archon.Content.v1.AddAddonsRequest,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/delete */
|
||||
public async deleteAddon(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Content.v1.RemoveAddonRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/delete`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/disable */
|
||||
public async disableAddon(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Content.v1.RemoveAddonRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/disable`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/enable */
|
||||
public async enableAddon(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Content.v1.RemoveAddonRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/enable`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/delete-many */
|
||||
public async deleteAddons(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
items: Archon.Content.v1.RemoveAddonRequest[],
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/delete-many`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: { items },
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/disable-many */
|
||||
public async disableAddons(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
items: Archon.Content.v1.RemoveAddonRequest[],
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/disable-many`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: { items },
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/enable-many */
|
||||
public async enableAddons(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
items: Archon.Content.v1.RemoveAddonRequest[],
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/enable-many`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: { items },
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/content */
|
||||
public async installContent(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Content.v1.InstallWorldContent,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/content`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/content/repair */
|
||||
public async repair(serverId: string, worldId: string): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/content/repair`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/content/unlink-modpack */
|
||||
public async unlinkModpack(serverId: string, worldId: string): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/content/unlink-modpack`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** GET /v1/:server_id/worlds/:world_id/addons/update?filename=... */
|
||||
public async getAddonUpdate(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
filename: string,
|
||||
): Promise<Archon.Content.v1.Addon> {
|
||||
return this.client.request<Archon.Content.v1.Addon>(
|
||||
`/servers/${serverId}/worlds/${worldId}/addons/update?filename=${encodeURIComponent(filename)}`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/update */
|
||||
public async updateAddon(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
request: Archon.Content.v1.UpdateAddonRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/update`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/addons/update-many */
|
||||
public async updateAddons(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
addons: Archon.Content.v1.UpdateAddonRequest[],
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(`/servers/${serverId}/worlds/${worldId}/addons/update-many`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: { addons },
|
||||
})
|
||||
}
|
||||
|
||||
/** GET /v1/:server_id/worlds/:world_id/content/modpack/update */
|
||||
public async getModpackUpdate(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
): Promise<Archon.Content.v1.ModpackFields> {
|
||||
return this.client.request<Archon.Content.v1.ModpackFields>(
|
||||
`/servers/${serverId}/worlds/${worldId}/content/modpack/update`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/content/modpack/update */
|
||||
public async updateModpack(serverId: string, worldId: string): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/content/modpack/update`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** GET /v1/:server_id/worlds/:world_id/content/update-game-version?game_version=... */
|
||||
public async getUpdateGameVersionPreview(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
gameVersion: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Archon.Content.v1.UpdateGameVersionPreview> {
|
||||
return this.client.request<Archon.Content.v1.UpdateGameVersionPreview>(
|
||||
`/servers/${serverId}/worlds/${worldId}/content/update-game-version?game_version=${encodeURIComponent(gameVersion)}`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
timeout: 1000 * 1000,
|
||||
signal,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** POST /v1/:server_id/worlds/:world_id/content/update-game-version?game_version=... */
|
||||
public async applyGameVersionUpdate(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
gameVersion: string,
|
||||
): Promise<void> {
|
||||
await this.client.request<void>(
|
||||
`/servers/${serverId}/worlds/${worldId}/content/update-game-version?game_version=${encodeURIComponent(gameVersion)}`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from './backups/v0'
|
||||
export * from './actions/v1'
|
||||
export * from './backups/v1'
|
||||
export * from './content/v0'
|
||||
export * from './backups-queue/v1'
|
||||
export * from './content/v1'
|
||||
export * from './properties/v1'
|
||||
export * from './servers/v0'
|
||||
export * from './servers/v1'
|
||||
export * from './types'
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonNodesInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_nodes_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get node hostnames and region summary for admin tooling.
|
||||
* GET /_internal/nodes/overview
|
||||
*/
|
||||
public async overview(): Promise<Archon.Nodes.Internal.Overview> {
|
||||
return this.client.request<Archon.Nodes.Internal.Overview>('/nodes/overview', {
|
||||
api: 'archon',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonNoticesV0Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_notices_v0'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all server notices.
|
||||
* GET /modrinth/v0/notices
|
||||
*/
|
||||
public async list(): Promise<Archon.Notices.v0.ListedNotice[]> {
|
||||
return this.client.request<Archon.Notices.v0.ListedNotice[]>('/notices', {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a server notice.
|
||||
* POST /modrinth/v0/notices
|
||||
*/
|
||||
public async create(
|
||||
request: Archon.Notices.v0.Announce,
|
||||
): Promise<Archon.Notices.v0.PostNoticeResponseBody> {
|
||||
return this.client.request<Archon.Notices.v0.PostNoticeResponseBody>('/notices', {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a server notice.
|
||||
* PATCH /modrinth/v0/notices/:id
|
||||
*/
|
||||
public async update(id: number, request: Archon.Notices.v0.AnnouncePatch): Promise<void> {
|
||||
await this.client.request(`/notices/${id}`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'PATCH',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a server notice.
|
||||
* DELETE /modrinth/v0/notices/:id
|
||||
*/
|
||||
public async delete(id: number): Promise<void> {
|
||||
await this.client.request(`/notices/${id}`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign a notice to a server or node.
|
||||
* PUT /modrinth/v0/notices/:id/assign?server=:serverId
|
||||
* PUT /modrinth/v0/notices/:id/assign?node=:nodeId
|
||||
*/
|
||||
public async assign(id: number, target: Archon.Notices.v0.AssignmentTarget): Promise<void> {
|
||||
await this.client.request(`/notices/${id}/assign`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'PUT',
|
||||
params: this.assignmentTargetToParams(target),
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Unassign a notice from a server or node.
|
||||
* PUT /modrinth/v0/notices/:id/unassign?server=:serverId
|
||||
* PUT /modrinth/v0/notices/:id/unassign?node=:nodeId
|
||||
*/
|
||||
public async unassign(id: number, target: Archon.Notices.v0.AssignmentTarget): Promise<void> {
|
||||
await this.client.request(`/notices/${id}/unassign`, {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'PUT',
|
||||
params: this.assignmentTargetToParams(target),
|
||||
})
|
||||
}
|
||||
|
||||
private assignmentTargetToParams(
|
||||
target: Archon.Notices.v0.AssignmentTarget,
|
||||
): Record<string, string> {
|
||||
if ('server' in target) {
|
||||
return { server: target.server }
|
||||
}
|
||||
|
||||
return { node: target.node }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonOptionsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_options_v1'
|
||||
}
|
||||
|
||||
/** GET /v1/servers/:server_id/worlds/:world_id/options/startup */
|
||||
public async getStartup(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
): Promise<Archon.Content.v1.RuntimeOptions> {
|
||||
return this.client.request<Archon.Content.v1.RuntimeOptions>(
|
||||
`/servers/${serverId}/worlds/${worldId}/options/startup`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** PATCH /v1/servers/:server_id/worlds/:world_id/options/startup */
|
||||
public async patchStartup(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
body: Archon.Content.v1.PatchRuntimeOptions,
|
||||
): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/worlds/${worldId}/options/startup`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonPropertiesV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_properties_v1'
|
||||
}
|
||||
|
||||
/** GET /v1/servers/:server_id/worlds/:world_id/properties */
|
||||
public async getProperties(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
): Promise<Archon.Content.v1.PropertiesFields> {
|
||||
return this.client.request<Archon.Content.v1.PropertiesFields>(
|
||||
`/servers/${serverId}/worlds/${worldId}/properties`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** PATCH /v1/servers/:server_id/worlds/:world_id/properties */
|
||||
public async patchProperties(
|
||||
serverId: string,
|
||||
worldId: string,
|
||||
body: Archon.Content.v1.PatchPropertiesFields,
|
||||
): Promise<Archon.Content.v1.PropertiesFields> {
|
||||
return this.client.request<Archon.Content.v1.PropertiesFields>(
|
||||
`/servers/${serverId}/worlds/${worldId}/properties`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'PATCH',
|
||||
body,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonServerUsersV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_server_users_v1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of users with access to a server
|
||||
* GET /v1/servers/:server_id/users
|
||||
*/
|
||||
public async list(serverId: string): Promise<Archon.ServerUsers.v1.ServerUser[]> {
|
||||
return this.client.request<Archon.ServerUsers.v1.ServerUser[]>(`/servers/${serverId}/users`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user to a server
|
||||
* POST /v1/servers/:server_id/users
|
||||
*/
|
||||
public async add(
|
||||
serverId: string,
|
||||
user: Archon.ServerUsers.v1.AddServerUserRequest,
|
||||
): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/users`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
body: user,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-send an invite to a pending server user.
|
||||
* POST /v1/servers/:server_id/users/:user_id/reinvite
|
||||
*/
|
||||
public async reinvite(
|
||||
serverId: string,
|
||||
userId: string,
|
||||
): Promise<Archon.ServerUsers.v1.ReinviteResponse> {
|
||||
return this.client.request<Archon.ServerUsers.v1.ReinviteResponse>(
|
||||
`/servers/${serverId}/users/${userId}/reinvite`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a user from a server
|
||||
* DELETE /v1/servers/:server_id/users/:user_id
|
||||
*/
|
||||
public async delete(serverId: string, userId: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/users/${userId}`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's server role
|
||||
* PATCH /v1/servers/:server_id/users/:user_id
|
||||
*/
|
||||
public async update(
|
||||
serverId: string,
|
||||
userId: string,
|
||||
role: Archon.ServerUsers.v1.AssignableServerUserRole,
|
||||
): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/users/${userId}`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify(role),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonServersV0Module extends AbstractModule {
|
||||
@@ -40,7 +41,7 @@ export class ArchonServersV0Module extends AbstractModule {
|
||||
|
||||
/**
|
||||
* Check stock availability for a region
|
||||
* POST /modrinth/v0/stock
|
||||
* POST /modrinth/v0/stock?region=:region
|
||||
*/
|
||||
public async checkStock(
|
||||
region: string,
|
||||
@@ -51,6 +52,23 @@ export class ArchonServersV0Module extends AbstractModule {
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check stock availability (without region filter)
|
||||
* POST /modrinth/v0/stock
|
||||
*/
|
||||
public async checkStockGlobal(
|
||||
request: Archon.Servers.v0.StockRequest,
|
||||
): Promise<Archon.Servers.v0.StockResponse> {
|
||||
return this.client.request<Archon.Servers.v0.StockResponse>('/stock', {
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,4 +112,210 @@ export class ArchonServersV0Module extends AbstractModule {
|
||||
body: { action },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstall a server with a new loader or modpack
|
||||
* POST /modrinth/v0/servers/:id/reinstall
|
||||
*/
|
||||
public async reinstall(
|
||||
serverId: string,
|
||||
request: Archon.Servers.v0.ReinstallRequest,
|
||||
hardReset: boolean = false,
|
||||
): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/reinstall`, {
|
||||
api: 'archon',
|
||||
method: 'POST',
|
||||
version: 'modrinth/v0',
|
||||
params: { hard: String(hardReset) },
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get authentication credentials for .mrpack file upload
|
||||
* GET /modrinth/v0/servers/:id/reinstallFromMrpack
|
||||
*/
|
||||
public async getReinstallMrpackAuth(
|
||||
serverId: string,
|
||||
): Promise<Archon.Servers.v0.MrpackReinstallAuth> {
|
||||
return this.client.request<Archon.Servers.v0.MrpackReinstallAuth>(
|
||||
`/servers/${serverId}/reinstallFromMrpack`,
|
||||
{
|
||||
api: 'archon',
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reinstall a server from a .mrpack file with progress tracking
|
||||
*
|
||||
* Two-step flow: fetches upload auth, then uploads the .mrpack file to the node.
|
||||
*
|
||||
* @param serverId - Server ID
|
||||
* @param file - .mrpack file to upload
|
||||
* @param hardReset - Whether to erase all server data
|
||||
* @param options - Optional progress callback
|
||||
* @returns Promise resolving to an UploadHandle with progress tracking and cancellation
|
||||
*/
|
||||
public async reinstallFromMrpack(
|
||||
serverId: string,
|
||||
file: File,
|
||||
hardReset: boolean = false,
|
||||
options?: {
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
},
|
||||
): Promise<UploadHandle<void>> {
|
||||
const auth = await this.getReinstallMrpackAuth(serverId)
|
||||
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
return this.client.upload<void>('', {
|
||||
api: `https://${auth.url}`,
|
||||
version: 'reinstallMrpackMultiparted',
|
||||
formData,
|
||||
params: { hard: String(hardReset) },
|
||||
headers: { Authorization: `Bearer ${auth.token}` },
|
||||
skipAuth: true,
|
||||
onProgress: options?.onProgress,
|
||||
retry: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a server's name
|
||||
* POST /modrinth/v0/servers/:id/name
|
||||
*/
|
||||
public async updateName(serverId: string, name: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/name`, {
|
||||
api: 'archon',
|
||||
method: 'POST',
|
||||
version: 'modrinth/v0',
|
||||
body: { name },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get allocations for a server
|
||||
* GET /modrinth/v0/servers/:id/allocations
|
||||
*/
|
||||
public async getAllocations(serverId: string): Promise<Archon.Servers.v0.Allocation[]> {
|
||||
return this.client.request<Archon.Servers.v0.Allocation[]>(`/servers/${serverId}/allocations`, {
|
||||
api: 'archon',
|
||||
method: 'GET',
|
||||
version: 'modrinth/v0',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reserve a new allocation for a server
|
||||
* POST /modrinth/v0/servers/:id/allocations?name=...
|
||||
*/
|
||||
public async reserveAllocation(
|
||||
serverId: string,
|
||||
name: string,
|
||||
): Promise<Archon.Servers.v0.Allocation> {
|
||||
return this.client.request<Archon.Servers.v0.Allocation>(`/servers/${serverId}/allocations`, {
|
||||
api: 'archon',
|
||||
method: 'POST',
|
||||
version: 'modrinth/v0',
|
||||
params: { name },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an allocation's name
|
||||
* PUT /modrinth/v0/servers/:id/allocations/:port?name=...
|
||||
*/
|
||||
public async updateAllocation(serverId: string, port: number, name: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/allocations/${port}`, {
|
||||
api: 'archon',
|
||||
method: 'PUT',
|
||||
version: 'modrinth/v0',
|
||||
params: { name },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an allocation
|
||||
* DELETE /modrinth/v0/servers/:id/allocations/:port
|
||||
*/
|
||||
public async deleteAllocation(serverId: string, port: number): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/allocations/${port}`, {
|
||||
api: 'archon',
|
||||
method: 'DELETE',
|
||||
version: 'modrinth/v0',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a subdomain is available
|
||||
* GET /modrinth/v0/subdomains/:subdomain/isavailable
|
||||
*/
|
||||
public async checkSubdomainAvailability(subdomain: string): Promise<{ available: boolean }> {
|
||||
return this.client.request<{ available: boolean }>(`/subdomains/${subdomain}/isavailable`, {
|
||||
api: 'archon',
|
||||
method: 'GET',
|
||||
version: 'modrinth/v0',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a server's subdomain
|
||||
* POST /modrinth/v0/servers/:id/subdomain
|
||||
*/
|
||||
public async changeSubdomain(serverId: string, subdomain: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/subdomain`, {
|
||||
api: 'archon',
|
||||
method: 'POST',
|
||||
version: 'modrinth/v0',
|
||||
body: { subdomain },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get startup configuration for a server
|
||||
* GET /modrinth/v0/servers/:id/startup
|
||||
*/
|
||||
public async getStartupConfig(serverId: string): Promise<Archon.Servers.v0.StartupConfig> {
|
||||
return this.client.request<Archon.Servers.v0.StartupConfig>(`/servers/${serverId}/startup`, {
|
||||
api: 'archon',
|
||||
method: 'GET',
|
||||
version: 'modrinth/v0',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update startup configuration for a server
|
||||
* POST /modrinth/v0/servers/:id/startup
|
||||
*/
|
||||
public async updateStartupConfig(
|
||||
serverId: string,
|
||||
config: {
|
||||
invocation: string | null
|
||||
jdk_version: string | null
|
||||
jdk_build: string | null
|
||||
},
|
||||
): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/startup`, {
|
||||
api: 'archon',
|
||||
method: 'POST',
|
||||
version: 'modrinth/v0',
|
||||
body: config,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss a server notice
|
||||
* POST /modrinth/v0/servers/:id/notices/:noticeId/dismiss
|
||||
*/
|
||||
public async dismissNotice(serverId: string, noticeId: number): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/notices/${noticeId}/dismiss`, {
|
||||
api: 'archon',
|
||||
method: 'POST',
|
||||
version: 'modrinth/v0',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,30 @@ export class ArchonServersV1Module extends AbstractModule {
|
||||
return 'archon_servers_v1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of servers for the authenticated user
|
||||
* GET /v1/servers
|
||||
*/
|
||||
public async list(): Promise<Archon.Servers.v1.ServerFull[]> {
|
||||
return this.client.request<Archon.Servers.v1.ServerFull[]>('/servers', {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full server details including worlds, backups, and content
|
||||
* GET /v1/servers/:server_id
|
||||
*/
|
||||
public async get(serverId: string): Promise<Archon.Servers.v1.ServerFull> {
|
||||
return this.client.request<Archon.Servers.v1.ServerFull>(`/servers/${serverId}`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available regions
|
||||
* GET /v1/regions
|
||||
@@ -15,6 +39,31 @@ export class ArchonServersV1Module extends AbstractModule {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* End the intro flow for a server
|
||||
* DELETE /v1/servers/:id/flows/intro
|
||||
*/
|
||||
public async endIntro(serverId: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/flows/intro`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset a world to onboarding
|
||||
* POST /v1/servers/:id/worlds/:wid/onboard
|
||||
*/
|
||||
public async resetToOnboarding(serverId: string, worldId: string): Promise<void> {
|
||||
await this.client.request(`/servers/${serverId}/worlds/${worldId}/onboard`, {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Archon } from '../types'
|
||||
|
||||
export class ArchonTransfersInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'archon_transfers_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule transfers for specific servers.
|
||||
* POST /_internal/transfers/schedule/servers
|
||||
*/
|
||||
public async scheduleServers(
|
||||
request: Archon.Transfers.Internal.ScheduleServerTransfersRequest,
|
||||
): Promise<Archon.Transfers.Internal.ScheduleTransfersResponse> {
|
||||
return this.client.request<Archon.Transfers.Internal.ScheduleTransfersResponse>(
|
||||
'/transfers/schedule/servers',
|
||||
{
|
||||
api: 'archon',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule transfers for all servers on specific nodes.
|
||||
* POST /_internal/transfers/schedule/nodes
|
||||
*/
|
||||
public async scheduleNodes(
|
||||
request: Archon.Transfers.Internal.ScheduleNodeTransfersRequest,
|
||||
): Promise<Archon.Transfers.Internal.ScheduleTransfersResponse> {
|
||||
return this.client.request<Archon.Transfers.Internal.ScheduleTransfersResponse>(
|
||||
'/transfers/schedule/nodes',
|
||||
{
|
||||
api: 'archon',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get transfer batch history.
|
||||
* GET /_internal/transfers/history
|
||||
*/
|
||||
public async history(
|
||||
options?: Archon.Transfers.Internal.TransferHistoryQuery,
|
||||
): Promise<Archon.Transfers.Internal.TransferHistoryResponse> {
|
||||
const params: Record<string, number> = {}
|
||||
if (options?.page !== undefined) params.page = options.page
|
||||
if (options?.page_size !== undefined) params.page_size = options.page_size
|
||||
|
||||
return this.client.request<Archon.Transfers.Internal.TransferHistoryResponse>(
|
||||
'/transfers/history',
|
||||
{
|
||||
api: 'archon',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
params,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel pending transfer batches.
|
||||
* POST /_internal/transfers/cancel
|
||||
*/
|
||||
public async cancel(
|
||||
request: Archon.Transfers.Internal.CancelTransfersRequest,
|
||||
): Promise<Archon.Transfers.Internal.CancelTransfersResponse> {
|
||||
return this.client.request<Archon.Transfers.Internal.CancelTransfersResponse>(
|
||||
'/transfers/cancel',
|
||||
{
|
||||
api: 'archon',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,19 +1,59 @@
|
||||
import type { AbstractModrinthClient } from '../core/abstract-client'
|
||||
import type { AbstractModule } from '../core/abstract-module'
|
||||
import { ArchonBackupsV0Module } from './archon/backups/v0'
|
||||
import { ArchonActionsV1Module } from './archon/actions/v1'
|
||||
import { ArchonBackupsV1Module } from './archon/backups/v1'
|
||||
import { ArchonContentV0Module } from './archon/content/v0'
|
||||
import { ArchonBackupsQueueV1Module } from './archon/backups-queue/v1'
|
||||
import { ArchonContentV1Module } from './archon/content/v1'
|
||||
import { ArchonNodesInternalModule } from './archon/nodes/internal'
|
||||
import { ArchonNoticesV0Module } from './archon/notices/v0'
|
||||
import { ArchonOptionsV1Module } from './archon/options/v1'
|
||||
import { ArchonPropertiesV1Module } from './archon/properties/v1'
|
||||
import { ArchonServerUsersV1Module } from './archon/server-users/v1'
|
||||
import { ArchonServersV0Module } from './archon/servers/v0'
|
||||
import { ArchonServersV1Module } from './archon/servers/v1'
|
||||
import { ArchonTransfersInternalModule } from './archon/transfers/internal'
|
||||
import { ISO3166Module } from './iso3166'
|
||||
import { KyrosContentV1Module } from './kyros/content/v1'
|
||||
import { KyrosFilesV0Module } from './kyros/files/v0'
|
||||
import { LabrinthVersionsV3Module } from './labrinth'
|
||||
import { KyrosLogsV1Module } from './kyros/logs/v1'
|
||||
import { KyrosUploadSessionsV1Module } from './kyros/upload-sessions/v1'
|
||||
import { LabrinthVersionsV2Module, LabrinthVersionsV3Module } from './labrinth'
|
||||
import { LabrinthAffiliateInternalModule } from './labrinth/affiliate/internal'
|
||||
import { LabrinthAnalyticsV3Module } from './labrinth/analytics/v3'
|
||||
import { LabrinthAuthInternalModule } from './labrinth/auth/internal'
|
||||
import { LabrinthAuthV2Module } from './labrinth/auth/v2'
|
||||
import { LabrinthBillingInternalModule } from './labrinth/billing/internal'
|
||||
import { LabrinthCampaignInternalModule } from './labrinth/campaign/internal'
|
||||
import { LabrinthCollectionsModule } from './labrinth/collections'
|
||||
import { LabrinthExternalProjectsInternalModule } from './labrinth/external-projects/internal'
|
||||
import { LabrinthFriendsV3Module } from './labrinth/friends/v3'
|
||||
import { LabrinthGlobalsInternalModule } from './labrinth/globals/internal'
|
||||
import { LabrinthLimitsV3Module } from './labrinth/limits/v3'
|
||||
import { LabrinthModerationInternalModule } from './labrinth/moderation/internal'
|
||||
import { LabrinthNotificationsV2Module } from './labrinth/notifications/v2'
|
||||
import { LabrinthOAuthInternalModule } from './labrinth/oauth/internal'
|
||||
import { LabrinthOrganizationsV3Module } from './labrinth/organizations/v3'
|
||||
import { LabrinthPatsV2Module } from './labrinth/pats/v2'
|
||||
import { LabrinthPayoutV3Module } from './labrinth/payout/v3'
|
||||
import { LabrinthPayoutsV3Module } from './labrinth/payouts/v3'
|
||||
import { LabrinthProjectsV2Module } from './labrinth/projects/v2'
|
||||
import { LabrinthProjectsV3Module } from './labrinth/projects/v3'
|
||||
import { LabrinthReportsV3Module } from './labrinth/reports/v3'
|
||||
import { LabrinthServerPingInternalModule } from './labrinth/server-ping/internal'
|
||||
import { LabrinthSessionsV2Module } from './labrinth/sessions/v2'
|
||||
import { LabrinthStateModule } from './labrinth/state'
|
||||
import { LabrinthTagsV2Module } from './labrinth/tags/v2'
|
||||
import { LabrinthTeamsV2Module } from './labrinth/teams/v2'
|
||||
import { LabrinthTeamsV3Module } from './labrinth/teams/v3'
|
||||
import { LabrinthTechReviewInternalModule } from './labrinth/tech-review/internal'
|
||||
import { LabrinthThreadsV3Module } from './labrinth/threads/v3'
|
||||
import { LabrinthUsersV2Module } from './labrinth/users/v2'
|
||||
import { LabrinthUsersV3Module } from './labrinth/users/v3'
|
||||
import { LauncherMetaManifestV0Module } from './launcher-meta/v0'
|
||||
import { MclogsInsightsV1Module } from './mclogs/insights/v1'
|
||||
import { MclogsLogsV1Module } from './mclogs/logs/v1'
|
||||
import { PaperVersionsV3Module } from './paper/v3'
|
||||
import { PurpurVersionsV2Module } from './purpur/v2'
|
||||
|
||||
type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
|
||||
|
||||
@@ -27,20 +67,61 @@ type ModuleConstructor = new (client: AbstractModrinthClient) => AbstractModule
|
||||
* TODO: Better way? Probably not
|
||||
*/
|
||||
export const MODULE_REGISTRY = {
|
||||
archon_backups_v0: ArchonBackupsV0Module,
|
||||
archon_actions_v1: ArchonActionsV1Module,
|
||||
archon_backups_queue_v1: ArchonBackupsQueueV1Module,
|
||||
archon_backups_v1: ArchonBackupsV1Module,
|
||||
archon_content_v0: ArchonContentV0Module,
|
||||
archon_content_v1: ArchonContentV1Module,
|
||||
archon_nodes_internal: ArchonNodesInternalModule,
|
||||
archon_notices_v0: ArchonNoticesV0Module,
|
||||
archon_options_v1: ArchonOptionsV1Module,
|
||||
archon_properties_v1: ArchonPropertiesV1Module,
|
||||
archon_server_users_v1: ArchonServerUsersV1Module,
|
||||
archon_servers_v0: ArchonServersV0Module,
|
||||
archon_servers_v1: ArchonServersV1Module,
|
||||
archon_transfers_internal: ArchonTransfersInternalModule,
|
||||
iso3166_data: ISO3166Module,
|
||||
mclogs_insights_v1: MclogsInsightsV1Module,
|
||||
mclogs_logs_v1: MclogsLogsV1Module,
|
||||
launchermeta_manifest_v0: LauncherMetaManifestV0Module,
|
||||
kyros_content_v1: KyrosContentV1Module,
|
||||
kyros_files_v0: KyrosFilesV0Module,
|
||||
kyros_logs_v1: KyrosLogsV1Module,
|
||||
kyros_upload_sessions_v1: KyrosUploadSessionsV1Module,
|
||||
labrinth_affiliate_internal: LabrinthAffiliateInternalModule,
|
||||
labrinth_analytics_v3: LabrinthAnalyticsV3Module,
|
||||
labrinth_auth_internal: LabrinthAuthInternalModule,
|
||||
labrinth_auth_v2: LabrinthAuthV2Module,
|
||||
labrinth_billing_internal: LabrinthBillingInternalModule,
|
||||
labrinth_campaign_internal: LabrinthCampaignInternalModule,
|
||||
labrinth_collections: LabrinthCollectionsModule,
|
||||
labrinth_external_projects_internal: LabrinthExternalProjectsInternalModule,
|
||||
labrinth_friends_v3: LabrinthFriendsV3Module,
|
||||
labrinth_globals_internal: LabrinthGlobalsInternalModule,
|
||||
labrinth_moderation_internal: LabrinthModerationInternalModule,
|
||||
labrinth_notifications_v2: LabrinthNotificationsV2Module,
|
||||
labrinth_oauth_internal: LabrinthOAuthInternalModule,
|
||||
labrinth_organizations_v3: LabrinthOrganizationsV3Module,
|
||||
labrinth_pats_v2: LabrinthPatsV2Module,
|
||||
labrinth_limits_v3: LabrinthLimitsV3Module,
|
||||
labrinth_payout_v3: LabrinthPayoutV3Module,
|
||||
labrinth_payouts_v3: LabrinthPayoutsV3Module,
|
||||
labrinth_projects_v2: LabrinthProjectsV2Module,
|
||||
labrinth_projects_v3: LabrinthProjectsV3Module,
|
||||
labrinth_reports_v3: LabrinthReportsV3Module,
|
||||
labrinth_server_ping_internal: LabrinthServerPingInternalModule,
|
||||
labrinth_sessions_v2: LabrinthSessionsV2Module,
|
||||
labrinth_state: LabrinthStateModule,
|
||||
labrinth_tags_v2: LabrinthTagsV2Module,
|
||||
labrinth_teams_v2: LabrinthTeamsV2Module,
|
||||
labrinth_teams_v3: LabrinthTeamsV3Module,
|
||||
labrinth_tech_review_internal: LabrinthTechReviewInternalModule,
|
||||
labrinth_threads_v3: LabrinthThreadsV3Module,
|
||||
labrinth_users_v2: LabrinthUsersV2Module,
|
||||
labrinth_users_v3: LabrinthUsersV3Module,
|
||||
labrinth_versions_v2: LabrinthVersionsV2Module,
|
||||
labrinth_versions_v3: LabrinthVersionsV3Module,
|
||||
paper_versions_v3: PaperVersionsV3Module,
|
||||
purpur_versions_v2: PurpurVersionsV2Module,
|
||||
} as const satisfies Record<string, ModuleConstructor>
|
||||
|
||||
export type ModuleID = keyof typeof MODULE_REGISTRY
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
||||
import type { Archon } from '../../archon/types'
|
||||
|
||||
export class KyrosContentV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_content_v1'
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload addon files to a world via multipart form data
|
||||
*
|
||||
* @param worldId - World UUID
|
||||
* @param files - Files to upload as addons
|
||||
* @param options - Optional progress callback
|
||||
* @returns UploadHandle with promise, onProgress, and cancel
|
||||
* @deprecated Use `kyros.upload_sessions_v1` so cancellation can remove staged addon files before finalize.
|
||||
*/
|
||||
public uploadAddonFile(
|
||||
worldId: string,
|
||||
files: (File | Blob)[],
|
||||
options?: {
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
},
|
||||
): UploadHandle<void> {
|
||||
const formData = new FormData()
|
||||
for (const file of files) {
|
||||
formData.append('file', file, file instanceof File ? file.name : 'file')
|
||||
}
|
||||
|
||||
return this.client.upload<void>(`/worlds/${worldId}/content/upload-addon-file`, {
|
||||
api: '',
|
||||
version: 'v1',
|
||||
formData,
|
||||
onProgress: options?.onProgress,
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/** POST /v1/worlds/:world_id/content/upload-modpack-file */
|
||||
public uploadModpackFile(
|
||||
worldId: string,
|
||||
file: File | Blob,
|
||||
properties: Archon.Content.v1.PropertiesFields,
|
||||
options?: {
|
||||
softOverride?: boolean
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
},
|
||||
): UploadHandle<void> {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file, file instanceof File ? file.name : 'file')
|
||||
formData.append('properties', JSON.stringify(properties))
|
||||
|
||||
return this.client.upload<void>(`/worlds/${worldId}/content/upload-modpack-file`, {
|
||||
api: '',
|
||||
version: 'v1',
|
||||
formData,
|
||||
params:
|
||||
options?.softOverride !== undefined
|
||||
? { soft_override: String(options.softOverride) }
|
||||
: undefined,
|
||||
onProgress: options?.onProgress,
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,19 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
||||
import type { Archon } from '../../archon/types'
|
||||
import type { Kyros } from '../types'
|
||||
|
||||
type NodeFsAuth = Pick<Archon.Servers.v0.JWTAuth, 'url' | 'token'>
|
||||
|
||||
export class KyrosFilesV0Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_files_v0'
|
||||
}
|
||||
|
||||
private getNodeBaseUrl(auth: NodeFsAuth): string {
|
||||
return `https://${auth.url.replace(/\/modrinth\/v\d+\/fs\/?$/, '')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* List directory contents with pagination
|
||||
*
|
||||
@@ -22,7 +29,7 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
): Promise<Kyros.Files.v0.DirectoryResponse> {
|
||||
return this.client.request<Kyros.Files.v0.DirectoryResponse>('/fs/list', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
params: { path, page, page_size: pageSize },
|
||||
useNodeAuth: true,
|
||||
@@ -38,7 +45,7 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
public async createFileOrFolder(path: string, type: 'file' | 'directory'): Promise<void> {
|
||||
return this.client.request<void>('/fs/create', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
params: { path, type },
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
@@ -55,13 +62,31 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
public async downloadFile(path: string): Promise<Blob> {
|
||||
return this.client.request<Blob>('/fs/download', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
params: { path },
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a file using explicit filesystem auth credentials.
|
||||
*
|
||||
* @param auth - Filesystem auth (url + token) from Archon
|
||||
* @param path - File path (e.g., "/server-icon.png")
|
||||
* @returns Promise resolving to file Blob
|
||||
*/
|
||||
public async downloadFileWithAuth(auth: NodeFsAuth, path: string): Promise<Blob> {
|
||||
return this.client.request<Blob>('/fs/download', {
|
||||
api: this.getNodeBaseUrl(auth),
|
||||
version: 'modrinth/v0',
|
||||
method: 'GET',
|
||||
params: { path },
|
||||
headers: { Authorization: `Bearer ${auth.token}` },
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to a server's filesystem with progress tracking
|
||||
*
|
||||
@@ -69,6 +94,7 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
* @param file - File to upload
|
||||
* @param options - Optional progress callback and feature overrides
|
||||
* @returns UploadHandle with promise, onProgress, and cancel
|
||||
* @deprecated Use `kyros.upload_sessions_v1` for bulk uploads so cancellation can remove staged files before finalize.
|
||||
*/
|
||||
public uploadFile(
|
||||
path: string,
|
||||
@@ -80,7 +106,7 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
): UploadHandle<void> {
|
||||
return this.client.upload<void>('/fs/create', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
file,
|
||||
params: { path, type: 'file' },
|
||||
onProgress: options?.onProgress,
|
||||
@@ -89,6 +115,36 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file using explicit filesystem auth credentials.
|
||||
*
|
||||
* @param auth - Filesystem auth (url + token) from Archon
|
||||
* @param path - Destination path (e.g., "/server-icon.png")
|
||||
* @param file - File to upload
|
||||
* @param options - Optional progress callback and feature overrides
|
||||
* @returns UploadHandle with promise, onProgress, and cancel
|
||||
*/
|
||||
public uploadFileWithAuth(
|
||||
auth: NodeFsAuth,
|
||||
path: string,
|
||||
file: File | Blob,
|
||||
options?: {
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
retry?: boolean | number
|
||||
},
|
||||
): UploadHandle<void> {
|
||||
return this.client.upload<void>('/fs/create', {
|
||||
api: this.getNodeBaseUrl(auth),
|
||||
version: 'modrinth/v0',
|
||||
file,
|
||||
params: { path, type: 'file' },
|
||||
headers: { Authorization: `Bearer ${auth.token}` },
|
||||
onProgress: options?.onProgress,
|
||||
retry: options?.retry,
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update file contents
|
||||
*
|
||||
@@ -100,7 +156,7 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
|
||||
return this.client.request<void>('/fs/update', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
method: 'PUT',
|
||||
params: { path },
|
||||
body: blob,
|
||||
@@ -118,7 +174,7 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
public async moveFileOrFolder(sourcePath: string, destPath: string): Promise<void> {
|
||||
return this.client.request<void>('/fs/move', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
method: 'POST',
|
||||
body: { source: sourcePath, destination: destPath },
|
||||
useNodeAuth: true,
|
||||
@@ -145,13 +201,35 @@ export class KyrosFilesV0Module extends AbstractModule {
|
||||
public async deleteFileOrFolder(path: string, recursive: boolean): Promise<void> {
|
||||
return this.client.request<void>('/fs/delete', {
|
||||
api: '',
|
||||
version: 'v0',
|
||||
version: 'modrinth/v0',
|
||||
method: 'DELETE',
|
||||
params: { path, recursive },
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file or folder using explicit filesystem auth credentials.
|
||||
*
|
||||
* @param auth - Filesystem auth (url + token) from Archon
|
||||
* @param path - Path to delete
|
||||
* @param recursive - If true, delete directory contents recursively
|
||||
*/
|
||||
public async deleteFileOrFolderWithAuth(
|
||||
auth: NodeFsAuth,
|
||||
path: string,
|
||||
recursive: boolean,
|
||||
): Promise<void> {
|
||||
return this.client.request<void>('/fs/delete', {
|
||||
api: this.getNodeBaseUrl(auth),
|
||||
version: 'modrinth/v0',
|
||||
method: 'DELETE',
|
||||
params: { path, recursive },
|
||||
headers: { Authorization: `Bearer ${auth.token}` },
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an archive file (zip, tar, etc.)
|
||||
*
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
|
||||
export class KyrosLogsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_logs_v1'
|
||||
}
|
||||
|
||||
/** POST /v1/logs/clear — clear the live logs buffer for the current server */
|
||||
public async clear(): Promise<void> {
|
||||
return this.client.request<void>('/logs/clear', {
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
useNodeAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,32 @@
|
||||
export namespace Kyros {
|
||||
export namespace UploadSessions {
|
||||
export namespace v1 {
|
||||
export type Scope = 'content' | 'files'
|
||||
export type UploadSessionStatus =
|
||||
| 'active'
|
||||
| 'uploading'
|
||||
| 'finalizing'
|
||||
| 'cancelled'
|
||||
| 'finalized'
|
||||
| 'expired'
|
||||
|
||||
export interface UploadSessionResponse {
|
||||
upload_id: string
|
||||
status: UploadSessionStatus
|
||||
created_at: number
|
||||
updated_at: number
|
||||
last_upload_at: number | null
|
||||
expires_at: number
|
||||
entry_count: number
|
||||
uploaded_byte_count: number
|
||||
}
|
||||
|
||||
export interface GetUploadSessionResponse {
|
||||
session: UploadSessionResponse | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Files {
|
||||
export namespace v0 {
|
||||
export interface DirectoryItem {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle, UploadProgress } from '../../../types/upload'
|
||||
import type { Kyros } from '../types'
|
||||
|
||||
export type UploadSessionFile = {
|
||||
file: File | Blob
|
||||
filename: string
|
||||
}
|
||||
|
||||
export class KyrosUploadSessionsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'kyros_upload_sessions_v1'
|
||||
}
|
||||
|
||||
public async create(
|
||||
scope: Kyros.UploadSessions.v1.Scope,
|
||||
worldId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async get(
|
||||
scope: Kyros.UploadSessions.v1.Scope,
|
||||
worldId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.GetUploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.GetUploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'GET',
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public uploadFiles(
|
||||
scope: Kyros.UploadSessions.v1.Scope,
|
||||
worldId: string,
|
||||
uploadId: string,
|
||||
files: UploadSessionFile[],
|
||||
options?: {
|
||||
onProgress?: (progress: UploadProgress) => void
|
||||
retry?: boolean | number
|
||||
},
|
||||
): UploadHandle<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
const formData = new FormData()
|
||||
for (const { file, filename } of files) {
|
||||
formData.append('file', file, filename)
|
||||
}
|
||||
|
||||
return this.client.upload<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}/files`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
formData,
|
||||
onProgress: options?.onProgress,
|
||||
retry: options?.retry,
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async finalize(
|
||||
scope: Kyros.UploadSessions.v1.Scope,
|
||||
worldId: string,
|
||||
uploadId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}/finalize`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'POST',
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async cancel(
|
||||
scope: Kyros.UploadSessions.v1.Scope,
|
||||
worldId: string,
|
||||
uploadId: string,
|
||||
): Promise<Kyros.UploadSessions.v1.UploadSessionResponse> {
|
||||
return this.client.request<Kyros.UploadSessions.v1.UploadSessionResponse>(
|
||||
`/worlds/${worldId}/files/upload-session/${uploadId}`,
|
||||
{
|
||||
api: '',
|
||||
version: 'v1',
|
||||
method: 'DELETE',
|
||||
useNodeAuth: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthAffiliateInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_affiliate_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all affiliate codes for the authenticated user (or all if admin)
|
||||
* GET /_internal/affiliate
|
||||
*/
|
||||
public async getAll(): Promise<Labrinth.Affiliate.Internal.AffiliateCode[]> {
|
||||
return this.client.request<Labrinth.Affiliate.Internal.AffiliateCode[]>('/affiliate', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new affiliate code
|
||||
* PUT /_internal/affiliate
|
||||
*/
|
||||
public async create(
|
||||
data: Labrinth.Affiliate.Internal.CreateRequest,
|
||||
): Promise<Labrinth.Affiliate.Internal.AffiliateCode> {
|
||||
return this.client.request<Labrinth.Affiliate.Internal.AffiliateCode>('/affiliate', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific affiliate code by ID
|
||||
* GET /_internal/affiliate/{id}
|
||||
*/
|
||||
public async get(id: string): Promise<Labrinth.Affiliate.Internal.AffiliateCode> {
|
||||
return this.client.request<Labrinth.Affiliate.Internal.AffiliateCode>(`/affiliate/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an affiliate code
|
||||
* DELETE /_internal/affiliate/{id}
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request<void>(`/affiliate/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an affiliate code's source name
|
||||
* PATCH /_internal/affiliate/{id}
|
||||
*/
|
||||
public async patch(id: string, data: Labrinth.Affiliate.Internal.PatchRequest): Promise<void> {
|
||||
return this.client.request<void>(`/affiliate/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthAnalyticsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_analytics_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch analytics data for the authenticated user's accessible projects
|
||||
* and affiliate codes.
|
||||
*
|
||||
* @param data - Analytics request body defining time range and requested metrics
|
||||
* @returns Promise resolving to the analytics response, with time slices in `metrics`
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const response = await client.labrinth.analytics_v3.fetch({
|
||||
* time_range: {
|
||||
* start: '2026-01-01T00:00:00Z',
|
||||
* end: '2026-02-01T00:00:00Z',
|
||||
* resolution: { slices: 31 },
|
||||
* },
|
||||
* project_ids: ['A1B2C3D4'],
|
||||
* return_metrics: {
|
||||
* project_views: { bucket_by: ['project_id'] },
|
||||
* },
|
||||
* })
|
||||
* const timeSlices = response.metrics
|
||||
* ```
|
||||
*/
|
||||
public async fetch(
|
||||
data: Labrinth.Analytics.v3.FetchRequest,
|
||||
): Promise<Labrinth.Analytics.v3.FetchResponse> {
|
||||
return this.client.request<Labrinth.Analytics.v3.FetchResponse>('/analytics', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
timeout: 100 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch available analytics filter facets for the authenticated user's
|
||||
* accessible projects.
|
||||
*
|
||||
* POST /v3/analytics/facets
|
||||
*/
|
||||
public async fetchFacets(
|
||||
data: Labrinth.Analytics.v3.FetchRequest,
|
||||
): Promise<Labrinth.Analytics.v3.FacetsResponse> {
|
||||
return this.client.request<Labrinth.Analytics.v3.FacetsResponse>('/analytics/facets', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
timeout: 100 * 1000,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all analytics events.
|
||||
* GET /v3/analytics-event
|
||||
*/
|
||||
public async getEvents(): Promise<Labrinth.Analytics.v3.AnalyticsEvent[]> {
|
||||
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent[]>('/analytics-event', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an analytics event.
|
||||
* POST /v3/analytics-event
|
||||
*/
|
||||
public async createEvent(
|
||||
data: Labrinth.Analytics.v3.AnalyticsEventUpsert,
|
||||
): Promise<Labrinth.Analytics.v3.AnalyticsEvent> {
|
||||
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent>('/analytics-event', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an analytics event.
|
||||
* PATCH /v3/analytics-event/{id}
|
||||
*/
|
||||
public async editEvent(
|
||||
id: Labrinth.Analytics.v3.AnalyticsEventId,
|
||||
data: Labrinth.Analytics.v3.AnalyticsEventUpsert,
|
||||
): Promise<Labrinth.Analytics.v3.AnalyticsEvent> {
|
||||
return this.client.request<Labrinth.Analytics.v3.AnalyticsEvent>(`/analytics-event/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an analytics event.
|
||||
* DELETE /v3/analytics-event/{id}
|
||||
*/
|
||||
public async deleteEvent(id: Labrinth.Analytics.v3.AnalyticsEventId): Promise<void> {
|
||||
return this.client.request<void>(`/analytics-event/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthAuthInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_auth_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user is subscribed to the newsletter
|
||||
*
|
||||
* @returns Promise resolving to the subscription status
|
||||
*/
|
||||
public async getNewsletterStatus(): Promise<Labrinth.Auth.Internal.SubscriptionStatus> {
|
||||
return this.client.request<Labrinth.Auth.Internal.SubscriptionStatus>('/auth/email/subscribe', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to the newsletter
|
||||
*/
|
||||
public async subscribeNewsletter(): Promise<void> {
|
||||
return this.client.request('/auth/email/subscribe', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a signed Discord community bot handoff URL
|
||||
*/
|
||||
public async createDiscordCommunityLink(): Promise<Labrinth.Auth.Internal.DiscordCommunityLinkResponse> {
|
||||
return this.client.request<Labrinth.Auth.Internal.DiscordCommunityLinkResponse>(
|
||||
'/auth/discord-community-link',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthAuthV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_auth_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in with a password
|
||||
*
|
||||
* Returns a session token on success, or a flow ID if 2FA is required.
|
||||
*
|
||||
* @param data - Login credentials and captcha challenge
|
||||
* @returns Promise resolving to a login response with session or flow
|
||||
*/
|
||||
public async login(data: Labrinth.Auth.v2.LoginRequest): Promise<Labrinth.Auth.v2.LoginResponse> {
|
||||
return this.client.request<Labrinth.Auth.v2.LoginResponse>(`/auth/login`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete a 2FA login flow
|
||||
*
|
||||
* @param data - The 2FA code and flow ID
|
||||
* @returns Promise resolving to a session response
|
||||
*/
|
||||
public async login2FA(
|
||||
data: Labrinth.Auth.v2.Login2FARequest,
|
||||
): Promise<Labrinth.Auth.v2.Login2FAResponse> {
|
||||
return this.client.request<Labrinth.Auth.v2.Login2FAResponse>(`/auth/login/2fa`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new account with a password
|
||||
*
|
||||
* @param data - Account creation data
|
||||
* @returns Promise resolving to a session response
|
||||
*/
|
||||
public async createAccount(
|
||||
data: Labrinth.Auth.v2.CreateAccountRequest,
|
||||
): Promise<Labrinth.Auth.v2.CreateAccountResponse> {
|
||||
return this.client.request<Labrinth.Auth.v2.CreateAccountResponse>(`/auth/create`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Begin a password reset flow by sending a recovery email
|
||||
*
|
||||
* @param data - The username/email and captcha challenge
|
||||
*/
|
||||
public async resetPasswordBegin(data: Labrinth.Auth.v2.ResetPasswordRequest): Promise<void> {
|
||||
return this.client.request(`/auth/password/reset`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a user's password (via reset flow or with old password)
|
||||
*
|
||||
* @param data - The password change data
|
||||
*/
|
||||
public async changePassword(data: Labrinth.Auth.v2.ChangePasswordRequest): Promise<void> {
|
||||
return this.client.request(`/auth/password`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthCampaignInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_campaign_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Pride 2026 campaign fundraising progress.
|
||||
* GET /_internal/campaign/pride-26
|
||||
*/
|
||||
public async getPride26(): Promise<Labrinth.Campaign.Internal.CampaignInfo> {
|
||||
return this.client.request<Labrinth.Campaign.Internal.CampaignInfo>('/campaign/pride-26', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthExternalProjectsInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_external_projects_internal'
|
||||
}
|
||||
|
||||
public async search(
|
||||
data: Labrinth.ExternalProjects.Internal.SearchRequest,
|
||||
): Promise<Labrinth.ExternalProjects.Internal.ExternalProject[]> {
|
||||
return this.client.request<Labrinth.ExternalProjects.Internal.ExternalProject[]>(
|
||||
'/moderation/external-license/search',
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async getBySha1(
|
||||
sha1: string,
|
||||
): Promise<Labrinth.ExternalProjects.Internal.ExternalProject> {
|
||||
return this.client.request<Labrinth.ExternalProjects.Internal.ExternalProject>(
|
||||
`/moderation/external-license/by-sha1/${sha1}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async update(
|
||||
id: number,
|
||||
data: Labrinth.ExternalProjects.Internal.UpdateLicenseRequest,
|
||||
): Promise<Labrinth.ExternalProjects.Internal.ExternalProject> {
|
||||
return this.client.request<Labrinth.ExternalProjects.Internal.ExternalProject>(
|
||||
`/moderation/external-license/${id}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthFriendsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_friends_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get friends and pending friend requests for the authenticated user
|
||||
*
|
||||
* @returns Promise resolving to friend relationships
|
||||
*/
|
||||
public async list(): Promise<Labrinth.Friends.v3.UserFriend[]> {
|
||||
return this.client.request<Labrinth.Friends.v3.UserFriend[]>('/friends', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Send or accept a friend request
|
||||
*
|
||||
* @param idOrUsername - The target user's ID or username
|
||||
*/
|
||||
public async add(idOrUsername: string): Promise<void> {
|
||||
return this.client.request(`/friend/${encodeURIComponent(idOrUsername)}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a friend or pending friend request
|
||||
*
|
||||
* @param idOrUsername - The target user's ID or username
|
||||
*/
|
||||
public async remove(idOrUsername: string): Promise<void> {
|
||||
return this.client.request(`/friend/${encodeURIComponent(idOrUsername)}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthGlobalsInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_globals_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get configured global non-secret variables for this backend instance
|
||||
*
|
||||
* @returns Promise resolving to the global configuration
|
||||
*/
|
||||
public async get(): Promise<Labrinth.Globals.Internal.Globals> {
|
||||
return this.client.request<Labrinth.Globals.Internal.Globals>(`/globals`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,28 @@
|
||||
export * from './analytics/v3'
|
||||
export * from './auth/internal'
|
||||
export * from './auth/v2'
|
||||
export * from './billing/internal'
|
||||
export * from './collections'
|
||||
export * from './external-projects/internal'
|
||||
export * from './friends/v3'
|
||||
export * from './globals/internal'
|
||||
export * from './limits/v3'
|
||||
export * from './moderation/internal'
|
||||
export * from './notifications/v2'
|
||||
export * from './oauth/internal'
|
||||
export * from './organizations/v3'
|
||||
export * from './pats/v2'
|
||||
export * from './payout/v3'
|
||||
export * from './payouts/v3'
|
||||
export * from './projects/v2'
|
||||
export * from './projects/v3'
|
||||
export * from './reports/v3'
|
||||
export * from './server-ping/internal'
|
||||
export * from './sessions/v2'
|
||||
export * from './state'
|
||||
export * from './tech-review/internal'
|
||||
export * from './threads/v3'
|
||||
export * from './users/v2'
|
||||
export * from './users/v3'
|
||||
export * from './versions/v2'
|
||||
export * from './versions/v3'
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module.js'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthLimitsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_limits_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project creation limits for the authenticated user.
|
||||
*/
|
||||
public async getProjectLimits(): Promise<Labrinth.Limits.v3.UserLimits> {
|
||||
return this.client.request<Labrinth.Limits.v3.UserLimits>('/limits/projects', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get organization creation limits for the authenticated user.
|
||||
*/
|
||||
public async getOrganizationLimits(): Promise<Labrinth.Limits.v3.UserLimits> {
|
||||
return this.client.request<Labrinth.Limits.v3.UserLimits>('/limits/organizations', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get collection creation limits for the authenticated user.
|
||||
*/
|
||||
public async getCollectionLimits(): Promise<Labrinth.Limits.v3.UserLimits> {
|
||||
return this.client.request<Labrinth.Limits.v3.UserLimits>('/limits/collections', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthModerationInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_moderation_internal'
|
||||
}
|
||||
|
||||
public async acquireLock(
|
||||
projectId: string,
|
||||
): Promise<Labrinth.Moderation.Internal.LockAcquireResponse> {
|
||||
return this.client.request<Labrinth.Moderation.Internal.LockAcquireResponse>(
|
||||
`/moderation/lock/${projectId}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async overrideLock(
|
||||
projectId: string,
|
||||
): Promise<Labrinth.Moderation.Internal.LockAcquireResponse> {
|
||||
return this.client.request<Labrinth.Moderation.Internal.LockAcquireResponse>(
|
||||
`/moderation/lock/${projectId}/override`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async releaseLock(
|
||||
projectId: string,
|
||||
): Promise<Labrinth.Moderation.Internal.ReleaseLockResponse> {
|
||||
return this.client.request<Labrinth.Moderation.Internal.ReleaseLockResponse>(
|
||||
`/moderation/lock/${projectId}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'DELETE',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
public async checkLock(
|
||||
projectId: string,
|
||||
): Promise<Labrinth.Moderation.Internal.LockStatusResponse> {
|
||||
return this.client.request<Labrinth.Moderation.Internal.LockStatusResponse>(
|
||||
`/moderation/lock/${projectId}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthNotificationsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_notifications_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all notifications for a user
|
||||
*
|
||||
* @param userId - The user's ID
|
||||
* @returns Promise resolving to the user's notifications
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const notifications = await client.labrinth.notifications_v2.getUserNotifications('user123')
|
||||
* ```
|
||||
*/
|
||||
public async getUserNotifications(
|
||||
userId: string,
|
||||
): Promise<Labrinth.Notifications.v2.Notification[]> {
|
||||
return this.client.request<Labrinth.Notifications.v2.Notification[]>(
|
||||
`/user/${userId}/notifications`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple notifications by their IDs
|
||||
*
|
||||
* @param ids - Array of notification IDs
|
||||
* @returns Promise resolving to an array of notifications
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const notifications = await client.labrinth.notifications_v2.getMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Notifications.v2.Notification[]> {
|
||||
return this.client.request<Labrinth.Notifications.v2.Notification[]>(
|
||||
`/notifications?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark a single notification as read
|
||||
*
|
||||
* @param id - Notification ID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.notifications_v2.markAsRead('notif123')
|
||||
* ```
|
||||
*/
|
||||
public async markAsRead(id: string): Promise<void> {
|
||||
return this.client.request(`/notification/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark multiple notifications as read
|
||||
*
|
||||
* @param ids - Array of notification IDs to mark as read
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.notifications_v2.markMultipleAsRead(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async markMultipleAsRead(ids: string[]): Promise<void> {
|
||||
return this.client.request(`/notifications`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
params: { ids: JSON.stringify([...new Set(ids)]) },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single notification
|
||||
*
|
||||
* @param id - Notification ID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.notifications_v2.delete('notif123')
|
||||
* ```
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request(`/notification/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple notifications
|
||||
*
|
||||
* @param ids - Array of notification IDs to delete
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.notifications_v2.deleteMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async deleteMultiple(ids: string[]): Promise<void> {
|
||||
return this.client.request(`/notifications`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
params: { ids: JSON.stringify([...new Set(ids)]) },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle } from '../../../types/upload'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthOAuthInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_oauth_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's OAuth applications
|
||||
*
|
||||
* @param userId - The user's ID
|
||||
* @returns Promise resolving to an array of the user's OAuth clients
|
||||
*/
|
||||
public async getUserApps(userId: string): Promise<Labrinth.OAuth.Internal.OAuthClient[]> {
|
||||
return this.client.request<Labrinth.OAuth.Internal.OAuthClient[]>(
|
||||
`/user/${userId}/oauth_apps`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single OAuth application by ID
|
||||
*
|
||||
* @param id - The OAuth client ID
|
||||
* @returns Promise resolving to the OAuth client
|
||||
*/
|
||||
public async getApp(id: string): Promise<Labrinth.OAuth.Internal.OAuthClient> {
|
||||
return this.client.request<Labrinth.OAuth.Internal.OAuthClient>(`/oauth/app/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple OAuth applications by their IDs
|
||||
*
|
||||
* @param ids - Array of OAuth client IDs
|
||||
* @returns Promise resolving to an array of OAuth clients
|
||||
*/
|
||||
public async getApps(ids: string[]): Promise<Labrinth.OAuth.Internal.OAuthClient[]> {
|
||||
return this.client.request<Labrinth.OAuth.Internal.OAuthClient[]>(
|
||||
`/oauth/apps?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new OAuth application
|
||||
*
|
||||
* @param data - The OAuth app creation data
|
||||
* @returns Promise resolving to the created OAuth client with its client secret
|
||||
*/
|
||||
public async createApp(
|
||||
data: Labrinth.OAuth.Internal.CreateOAuthAppRequest,
|
||||
): Promise<Labrinth.OAuth.Internal.OAuthClientCreationResult> {
|
||||
return this.client.request<Labrinth.OAuth.Internal.OAuthClientCreationResult>(`/oauth/app`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an existing OAuth application
|
||||
*
|
||||
* @param id - The OAuth client ID
|
||||
* @param data - The fields to update
|
||||
*/
|
||||
public async editApp(
|
||||
id: string,
|
||||
data: Labrinth.OAuth.Internal.EditOAuthAppRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/oauth/app/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an OAuth application
|
||||
*
|
||||
* @param id - The OAuth client ID
|
||||
*/
|
||||
public async deleteApp(id: string): Promise<void> {
|
||||
return this.client.request(`/oauth/app/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the icon for an OAuth application
|
||||
*
|
||||
* @param id - The OAuth client ID
|
||||
* @param file - The icon file
|
||||
* @param ext - The file extension (e.g. 'png', 'jpeg')
|
||||
* @returns UploadHandle for progress tracking and cancellation
|
||||
*/
|
||||
public uploadAppIcon(id: string, file: File | Blob, ext: string): UploadHandle<void> {
|
||||
return this.client.upload<void>(`/oauth/app/${id}/icon`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
file,
|
||||
params: { ext },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user's OAuth authorizations
|
||||
*
|
||||
* @returns Promise resolving to an array of OAuth client authorizations
|
||||
*/
|
||||
public async getAuthorizations(): Promise<Labrinth.OAuth.Internal.OAuthClientAuthorization[]> {
|
||||
return this.client.request<Labrinth.OAuth.Internal.OAuthClientAuthorization[]>(
|
||||
`/oauth/authorizations`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke an OAuth authorization for a client
|
||||
*
|
||||
* @param clientId - The OAuth client ID to revoke
|
||||
*/
|
||||
public async revokeAuthorization(clientId: string): Promise<void> {
|
||||
return this.client.request(`/oauth/authorizations`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'DELETE',
|
||||
params: { client_id: clientId },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize an OAuth authorization flow
|
||||
*
|
||||
* Returns either an OAuthClientAccessRequest (if user needs to approve)
|
||||
* or a redirect URL string (if already authorized).
|
||||
*
|
||||
* @param params - The OAuth query parameters
|
||||
* @returns Promise resolving to an access request object or redirect URL string
|
||||
*/
|
||||
public async authorize(params: {
|
||||
client_id: string
|
||||
redirect_uri: string
|
||||
scope: string
|
||||
state?: string
|
||||
}): Promise<Labrinth.OAuth.Internal.OAuthClientAccessRequest | string> {
|
||||
return this.client.request<Labrinth.OAuth.Internal.OAuthClientAccessRequest | string>(
|
||||
`/oauth/authorize`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
params: params as Record<string, string>,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Accept an OAuth authorization request
|
||||
*
|
||||
* @param data - The flow ID to accept
|
||||
* @returns Promise resolving to a redirect URL string
|
||||
*/
|
||||
public async accept(data: Labrinth.OAuth.Internal.AcceptRejectRequest): Promise<string> {
|
||||
return this.client.request<string>(`/oauth/accept`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject an OAuth authorization request
|
||||
*
|
||||
* @param data - The flow ID to reject
|
||||
* @returns Promise resolving to a redirect URL string
|
||||
*/
|
||||
public async reject(data: Labrinth.OAuth.Internal.AcceptRejectRequest): Promise<string> {
|
||||
return this.client.request<string>(`/oauth/reject`, {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthOrganizationsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_organizations_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an organization by ID or slug
|
||||
*
|
||||
* @param idOrSlug - Organization ID or slug
|
||||
* @returns Promise resolving to the organization data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const org = await client.labrinth.organizations_v3.get('my-org')
|
||||
* ```
|
||||
*/
|
||||
public async get(idOrSlug: string): Promise<Labrinth.Organizations.v3.Organization> {
|
||||
return this.client.request<Labrinth.Organizations.v3.Organization>(
|
||||
`/organization/${idOrSlug}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an organization's projects
|
||||
*
|
||||
* @param idOrSlug - Organization ID or slug
|
||||
* @returns Promise resolving to the organization's projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projects = await client.labrinth.organizations_v3.getProjects('my-org')
|
||||
* ```
|
||||
*/
|
||||
public async getProjects(idOrSlug: string): Promise<Labrinth.Projects.v3.Project[]> {
|
||||
return this.client.request<Labrinth.Projects.v3.Project[]>(
|
||||
`/organization/${idOrSlug}/projects`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple organizations by their IDs
|
||||
*
|
||||
* @param ids - Array of organization IDs
|
||||
* @returns Promise resolving to an array of organizations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const orgs = await client.labrinth.organizations_v3.getMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Organizations.v3.Organization[]> {
|
||||
return this.client.request<Labrinth.Organizations.v3.Organization[]>(
|
||||
`/organizations?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a project to an organization
|
||||
*
|
||||
* @param idOrSlug - Organization ID or slug
|
||||
* @param request - The project to add
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.organizations_v3.addProject('my-org', { project_id: 'AABBCCDD' })
|
||||
* ```
|
||||
*/
|
||||
public async addProject(
|
||||
idOrSlug: string,
|
||||
request: Labrinth.Organizations.v3.AddProjectRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/organization/${idOrSlug}/projects`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a project from an organization
|
||||
*
|
||||
* @param idOrSlug - Organization ID or slug
|
||||
* @param projectId - Project ID to remove
|
||||
* @param data - Request body containing the new_owner user ID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.organizations_v3.removeProject('my-org', 'proj123', { new_owner: 'user456' })
|
||||
* ```
|
||||
*/
|
||||
public async removeProject(
|
||||
idOrSlug: string,
|
||||
projectId: string,
|
||||
data: Labrinth.Organizations.v3.RemoveProjectRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/organization/${idOrSlug}/projects/${projectId}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthPatsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_pats_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all personal access tokens for the authenticated user
|
||||
*
|
||||
* @returns Promise resolving to an array of PATs
|
||||
*/
|
||||
public async list(): Promise<Labrinth.Pats.v2.PersonalAccessToken[]> {
|
||||
return this.client.request<Labrinth.Pats.v2.PersonalAccessToken[]>('/pat', {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new personal access token
|
||||
*
|
||||
* @param data - The PAT creation request data
|
||||
* @returns Promise resolving to the newly created PAT (includes access_token)
|
||||
*/
|
||||
public async create(
|
||||
data: Labrinth.Pats.v2.CreatePatRequest,
|
||||
): Promise<Labrinth.Pats.v2.PersonalAccessToken> {
|
||||
return this.client.request<Labrinth.Pats.v2.PersonalAccessToken>('/pat', {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify an existing personal access token
|
||||
*
|
||||
* @param id - The PAT ID
|
||||
* @param data - The fields to update
|
||||
*/
|
||||
public async modify(id: string, data: Labrinth.Pats.v2.ModifyPatRequest): Promise<void> {
|
||||
return this.client.request(`/pat/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a personal access token
|
||||
*
|
||||
* @param id - The PAT ID
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request(`/pat/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Override, RawDecimal } from '../../../utils/types'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
type RawPayoutBalance = Override<
|
||||
Labrinth.Payout.v3.PayoutBalance,
|
||||
{
|
||||
available: RawDecimal
|
||||
withdrawn_lifetime: RawDecimal
|
||||
withdrawn_ytd: RawDecimal
|
||||
pending: RawDecimal
|
||||
dates: Record<string, RawDecimal>
|
||||
}
|
||||
>
|
||||
|
||||
type RawTransactionItem =
|
||||
| Override<
|
||||
Extract<Labrinth.Payout.v3.TransactionItem, { type: 'withdrawal' }>,
|
||||
{
|
||||
amount: RawDecimal
|
||||
fee: RawDecimal | null
|
||||
}
|
||||
>
|
||||
| Override<
|
||||
Extract<Labrinth.Payout.v3.TransactionItem, { type: 'payout_available' }>,
|
||||
{
|
||||
amount: RawDecimal
|
||||
}
|
||||
>
|
||||
|
||||
export class LabrinthPayoutV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_payout_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's payout balance
|
||||
*
|
||||
* @returns Promise resolving to the user's payout balance
|
||||
*/
|
||||
public async getBalance(): Promise<Labrinth.Payout.v3.PayoutBalance> {
|
||||
const balance = await this.client.request<RawPayoutBalance>('/payout/balance', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
return {
|
||||
...balance,
|
||||
available: Number(balance.available),
|
||||
withdrawn_lifetime: Number(balance.withdrawn_lifetime),
|
||||
withdrawn_ytd: Number(balance.withdrawn_ytd),
|
||||
pending: Number(balance.pending),
|
||||
dates: Object.fromEntries(
|
||||
Object.entries(balance.dates).map(([date, amount]) => [date, Number(amount)]),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user's transaction history (withdrawals and payouts)
|
||||
*
|
||||
* @returns Promise resolving to an array of transaction items
|
||||
*/
|
||||
public async getHistory(): Promise<Labrinth.Payout.v3.TransactionItem[]> {
|
||||
const history = await this.client.request<RawTransactionItem[]>('/payout/history', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
return history.map((transaction) => {
|
||||
if (transaction.type === 'withdrawal') {
|
||||
return {
|
||||
...transaction,
|
||||
amount: Number(transaction.amount),
|
||||
fee: transaction.fee === null ? null : Number(transaction.fee),
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
amount: Number(transaction.amount),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available payout methods, optionally filtered by country
|
||||
*
|
||||
* @param country - Optional ISO country code to filter methods by supported countries
|
||||
* @returns Promise resolving to an array of payout methods
|
||||
*/
|
||||
public async getMethods(country?: string): Promise<Labrinth.Payout.v3.PayoutMethod[]> {
|
||||
return this.client.request<Labrinth.Payout.v3.PayoutMethod[]>('/payout/methods', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
params: country ? { country } : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel a pending payout
|
||||
*
|
||||
* @param id - The payout ID to cancel
|
||||
*/
|
||||
public async cancel(id: string): Promise<void> {
|
||||
return this.client.request<void>(`/payout/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module.js'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthPayoutsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_payouts_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get platform revenue data.
|
||||
*
|
||||
* @param params - Optional start/end date filters
|
||||
* @returns Promise resolving to platform revenue data
|
||||
*/
|
||||
public async getPlatformRevenue(params?: {
|
||||
start?: string
|
||||
end?: string
|
||||
}): Promise<Labrinth.Payouts.v3.RevenueResponse> {
|
||||
return this.client.request<Labrinth.Payouts.v3.RevenueResponse>('/payout/platform_revenue', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
params: params as Record<string, string>,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,23 @@ export class LabrinthProjectsV2Module extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a project slug or ID exists and return its canonical project ID.
|
||||
*
|
||||
* @param idOrSlug - Project ID or slug (e.g. `sodium` or `AANobbMI`)
|
||||
*/
|
||||
public async check(idOrSlug: string): Promise<Labrinth.Projects.v2.ProjectCheckResponse> {
|
||||
const encoded = encodeURIComponent(idOrSlug)
|
||||
return this.client.request<Labrinth.Projects.v2.ProjectCheckResponse>(
|
||||
`/project/${encoded}/check`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple projects by IDs
|
||||
*
|
||||
@@ -71,6 +88,7 @@ export class LabrinthProjectsV2Module extends AbstractModule {
|
||||
params: {
|
||||
...params,
|
||||
facets: params.facets ? JSON.stringify(params.facets) : undefined,
|
||||
new_filters: params.new_filters ?? undefined,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -135,4 +153,151 @@ export class LabrinthProjectsV2Module extends AbstractModule {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a gallery image for a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @param file - Image file to upload
|
||||
* @param options - Gallery image options
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v2.createGalleryImage('sodium', imageFile, {
|
||||
* featured: true,
|
||||
* title: 'Screenshot 1',
|
||||
* description: 'Main menu with Sodium enabled'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async createGalleryImage(
|
||||
id: string,
|
||||
file: Blob,
|
||||
options: {
|
||||
ext: string
|
||||
featured: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
ordering?: number
|
||||
},
|
||||
): Promise<void> {
|
||||
const params: Record<string, string> = {
|
||||
ext: options.ext,
|
||||
featured: String(options.featured),
|
||||
}
|
||||
if (options.title) params.title = options.title
|
||||
if (options.description) params.description = options.description
|
||||
if (options.ordering !== undefined) params.ordering = String(options.ordering)
|
||||
|
||||
return this.client.request(`/project/${id}/gallery`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
params,
|
||||
body: file,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a gallery image for a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @param url - URL of the existing gallery image to edit
|
||||
* @param options - Gallery image options to update
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v2.editGalleryImage('sodium', 'https://cdn.modrinth.com/...', {
|
||||
* featured: false,
|
||||
* title: 'Updated title'
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async editGalleryImage(
|
||||
id: string,
|
||||
url: string,
|
||||
options: {
|
||||
featured: boolean
|
||||
title?: string
|
||||
description?: string
|
||||
ordering?: number
|
||||
},
|
||||
): Promise<void> {
|
||||
const params: Record<string, string> = {
|
||||
url,
|
||||
featured: String(options.featured),
|
||||
}
|
||||
if (options.title) params.title = options.title
|
||||
if (options.description) params.description = options.description
|
||||
if (options.ordering !== undefined) params.ordering = String(options.ordering)
|
||||
|
||||
return this.client.request(`/project/${id}/gallery`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a gallery image from a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @param url - URL of the gallery image to delete
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v2.deleteGalleryImage('sodium', 'https://cdn.modrinth.com/...')
|
||||
* ```
|
||||
*/
|
||||
public async deleteGalleryImage(id: string, url: string): Promise<void> {
|
||||
return this.client.request(`/project/${id}/gallery`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
params: { url },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get random projects
|
||||
*
|
||||
* @param count - Number of random projects to return
|
||||
* @returns Promise resolving to an array of random projects
|
||||
*/
|
||||
public async getRandom(count: number): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return this.client.request<Labrinth.Projects.v2.Project[]>('/projects_random', {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
params: { count: String(count) },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk edit multiple projects at once
|
||||
*
|
||||
* @param ids - Array of project IDs to edit
|
||||
* @param data - Fields to update across all specified projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v2.bulkEdit(['id1', 'id2'], {
|
||||
* issues_url: 'https://github.com/issues',
|
||||
* source_url: null,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async bulkEdit(
|
||||
ids: string[],
|
||||
data: Labrinth.Projects.v2.BulkEditProjectRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/projects`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
params: { ids: JSON.stringify(ids) },
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,32 @@ export class LabrinthProjectsV3Module extends AbstractModule {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a project's dependencies (v3)
|
||||
*
|
||||
* Returns all projects and versions that are dependencies of this project's versions.
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @returns Promise resolving to dependency data with projects and versions
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const deps = await client.labrinth.projects_v3.getDependencies('sodium')
|
||||
* console.log(deps.projects) // Array of project objects
|
||||
* console.log(deps.versions) // Array of version objects
|
||||
* ```
|
||||
*/
|
||||
public async getDependencies(id: string): Promise<Labrinth.Projects.v3.ProjectDependencies> {
|
||||
return this.client.request<Labrinth.Projects.v3.ProjectDependencies>(
|
||||
`/project/${id}/dependencies`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple projects by IDs (v3)
|
||||
*
|
||||
@@ -103,4 +129,73 @@ export class LabrinthProjectsV3Module extends AbstractModule {
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
public async createServerProject(
|
||||
data: Labrinth.Projects.v3.CreateServerProjectRequest,
|
||||
): Promise<Labrinth.Projects.v3.Project> {
|
||||
return this.client.request<Labrinth.Projects.v3.Project>(`/project`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'PUT',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v3.deleteProject('my-project')
|
||||
* ```
|
||||
*/
|
||||
public async deleteProject(id: string): Promise<void> {
|
||||
return this.client.request(`/project/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the icon of a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
* @param file - Image file to upload
|
||||
* @param ext - File extension (e.g., 'png', 'jpeg', 'gif', 'webp')
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v3.changeIcon('sodium', imageFile, 'png')
|
||||
* ```
|
||||
*/
|
||||
public async changeIcon(id: string, file: Blob, ext: string): Promise<void> {
|
||||
return this.client.request(`/project/${id}/icon`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'PATCH',
|
||||
params: { ext },
|
||||
body: file,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the icon of a project
|
||||
*
|
||||
* @param id - Project ID or slug
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.projects_v3.deleteIcon('sodium')
|
||||
* ```
|
||||
*/
|
||||
public async deleteIcon(id: string): Promise<void> {
|
||||
return this.client.request(`/project/${id}/icon`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthReportsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_reports_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a report by ID
|
||||
*
|
||||
* @param id - Report ID
|
||||
* @returns Promise resolving to the report data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const report = await client.labrinth.reports_v3.get('abc123')
|
||||
* ```
|
||||
*/
|
||||
public async get(id: string): Promise<Labrinth.Reports.v3.Report> {
|
||||
return this.client.request<Labrinth.Reports.v3.Report>(`/report/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* List reports for the current user (or all reports if moderator)
|
||||
*
|
||||
* @param params - Optional query parameters for count, offset, and whether to show all reports
|
||||
* @returns Promise resolving to an array of reports
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const reports = await client.labrinth.reports_v3.list({ count: 100 })
|
||||
* ```
|
||||
*/
|
||||
public async list(
|
||||
params?: Labrinth.Reports.v3.ListReportsParams,
|
||||
): Promise<Labrinth.Reports.v3.Report[]> {
|
||||
const queryParams: Record<string, string> = {}
|
||||
if (params?.count != null) queryParams.count = String(params.count)
|
||||
if (params?.offset != null) queryParams.offset = String(params.offset)
|
||||
if (params?.all != null) queryParams.all = String(params.all)
|
||||
|
||||
return this.client.request<Labrinth.Reports.v3.Report[]>(`/report`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple reports by IDs
|
||||
*
|
||||
* @param ids - Array of report IDs
|
||||
* @returns Promise resolving to an array of reports
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const reports = await client.labrinth.reports_v3.getMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Reports.v3.Report[]> {
|
||||
return this.client.request<Labrinth.Reports.v3.Report[]>(
|
||||
`/reports?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new report
|
||||
*
|
||||
* @param data - Report creation data
|
||||
* @returns Promise resolving to the created report
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const report = await client.labrinth.reports_v3.create({
|
||||
* report_type: 'spam',
|
||||
* item_id: 'project123',
|
||||
* item_type: 'project',
|
||||
* body: 'This project is spam',
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async create(
|
||||
data: Labrinth.Reports.v3.CreateReportRequest,
|
||||
): Promise<Labrinth.Reports.v3.Report> {
|
||||
return this.client.request<Labrinth.Reports.v3.Report>(`/report`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a report
|
||||
*
|
||||
* @param id - Report ID
|
||||
* @param data - Report edit data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.reports_v3.edit('abc123', { closed: true })
|
||||
* ```
|
||||
*/
|
||||
public async edit(id: string, data: Labrinth.Reports.v3.EditReportRequest): Promise<void> {
|
||||
return this.client.request(`/report/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a report (moderator only)
|
||||
*
|
||||
* @param id - Report ID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.reports_v3.delete('abc123')
|
||||
* ```
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request(`/report/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthServerPingInternalModule extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_server_ping_internal'
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping a Minecraft Java server
|
||||
* POST /_internal/server-ping/minecraft-java
|
||||
*/
|
||||
public async pingMinecraftJava(
|
||||
request: Labrinth.ServerPing.Internal.MinecraftJavaPingRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request<void>('/server-ping/minecraft-java', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'POST',
|
||||
body: request,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthSessionsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_sessions_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* List all sessions for the authenticated user
|
||||
*
|
||||
* @returns Promise resolving to an array of sessions
|
||||
*/
|
||||
public async list(): Promise<Labrinth.Sessions.v2.Session[]> {
|
||||
return this.client.request<Labrinth.Sessions.v2.Session[]>('/session/list', {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete (revoke) a session
|
||||
*
|
||||
* @param id - The session ID
|
||||
*/
|
||||
public async delete(id: string): Promise<void> {
|
||||
return this.client.request(`/session/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ export class LabrinthStateModule extends AbstractModule {
|
||||
muralBankDetails,
|
||||
iso3166Data,
|
||||
payoutMethods,
|
||||
globals,
|
||||
] = await Promise.all([
|
||||
// Tag endpoints
|
||||
this.client
|
||||
@@ -126,6 +127,15 @@ export class LabrinthStateModule extends AbstractModule {
|
||||
method: 'GET',
|
||||
})
|
||||
.catch((err) => handleError(err, [], '/v3/payout/methods')),
|
||||
|
||||
// Global configuration
|
||||
this.client
|
||||
.request<{ tax_compliance_thresholds: Record<string, number> }>('/globals', {
|
||||
api: 'labrinth',
|
||||
version: 'internal',
|
||||
method: 'GET',
|
||||
})
|
||||
.catch((err) => handleError(err, null, '/_internal/globals')),
|
||||
])
|
||||
|
||||
const tremendousIdMap = Object.fromEntries(
|
||||
@@ -148,6 +158,7 @@ export class LabrinthStateModule extends AbstractModule {
|
||||
tremendousIdMap,
|
||||
countries: iso3166Data.countries,
|
||||
subdivisions: iso3166Data.subdivisions,
|
||||
taxComplianceThresholds: globals?.tax_compliance_thresholds,
|
||||
errors,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthTagsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_tags_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get license text by SPDX identifier
|
||||
*
|
||||
* @param licenseId - SPDX license identifier (e.g., 'MIT', 'Apache-2.0')
|
||||
* @returns Promise resolving to the license title and body text
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const license = await client.labrinth.tags_v2.getLicenseText('MIT')
|
||||
* console.log(license.title) // "MIT License"
|
||||
* console.log(license.body) // full license text
|
||||
* ```
|
||||
*/
|
||||
public async getLicenseText(licenseId: string): Promise<Labrinth.Tags.v2.LicenseText> {
|
||||
return this.client.request<Labrinth.Tags.v2.LicenseText>(`/tag/license/${licenseId}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthTeamsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_teams_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a member to a team
|
||||
*
|
||||
* @param teamId - Team ID
|
||||
* @param data - New member data including user_id
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.teams_v2.addMember('team123', { user_id: 'user456' })
|
||||
* ```
|
||||
*/
|
||||
public async addMember(
|
||||
teamId: string,
|
||||
data: Labrinth.Teams.v2.AddTeamMemberRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/team/${teamId}/members`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'POST',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a team member
|
||||
*
|
||||
* @param teamId - Team ID
|
||||
* @param userId - User ID of the member to edit
|
||||
* @param data - Member update data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.teams_v2.editMember('team123', 'user456', {
|
||||
* role: 'Developer',
|
||||
* permissions: 0b111,
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async editMember(
|
||||
teamId: string,
|
||||
userId: string,
|
||||
data: Labrinth.Teams.v2.EditTeamMemberRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/team/${teamId}/members/${userId}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from a team
|
||||
*
|
||||
* @param teamId - Team ID
|
||||
* @param userId - User ID of the member to remove
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.teams_v2.removeMember('team123', 'user456')
|
||||
* ```
|
||||
*/
|
||||
public async removeMember(teamId: string, userId: string): Promise<void> {
|
||||
return this.client.request(`/team/${teamId}/members/${userId}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfer team ownership to another member
|
||||
*
|
||||
* @param teamId - Team ID
|
||||
* @param data - Transfer data including the new owner's user_id
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.teams_v2.transferOwnership('team123', { user_id: 'user456' })
|
||||
* ```
|
||||
*/
|
||||
public async transferOwnership(
|
||||
teamId: string,
|
||||
data: Labrinth.Teams.v2.TransferOwnershipRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/team/${teamId}/owner`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthTeamsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_teams_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple teams by their IDs
|
||||
*
|
||||
* @param ids - Array of team IDs
|
||||
* @returns Promise resolving to an array of team member arrays (one per team)
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const teams = await client.labrinth.teams_v3.getMultiple(['team1', 'team2'])
|
||||
* // teams[0] = members of team1, teams[1] = members of team2
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Projects.v3.TeamMember[][]> {
|
||||
return this.client.request<Labrinth.Projects.v3.TeamMember[][]>(
|
||||
`/teams?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthThreadsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_threads_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a thread by ID (v3)
|
||||
*
|
||||
* @param id - Thread ID
|
||||
* @returns Promise resolving to the thread data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const thread = await client.labrinth.threads_v3.getThread('abc123')
|
||||
* console.log(thread.messages)
|
||||
* ```
|
||||
*/
|
||||
public async getThread(id: string): Promise<Labrinth.Threads.v3.Thread> {
|
||||
return this.client.request<Labrinth.Threads.v3.Thread>(`/thread/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple threads by IDs (v3)
|
||||
*
|
||||
* @param ids - Array of thread IDs
|
||||
* @returns Promise resolving to an array of threads
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const threads = await client.labrinth.threads_v3.getMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Threads.v3.Thread[]> {
|
||||
return this.client.request<Labrinth.Threads.v3.Thread[]>(
|
||||
`/threads?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message to a thread (v3)
|
||||
*
|
||||
* @param id - Thread ID
|
||||
* @param message - Message body to send
|
||||
* @returns Promise resolving when message is sent
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.threads_v3.sendMessage('abc123', {
|
||||
* body: { type: 'text', body: 'Hello!' }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
public async sendMessage(
|
||||
id: string,
|
||||
message: Labrinth.Threads.v3.SendMessageRequest,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/thread/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'POST',
|
||||
body: message,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a message from a thread (v3)
|
||||
*
|
||||
* @param messageId - Message ID
|
||||
* @returns Promise resolving when message is deleted
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.threads_v3.deleteMessage('msg123')
|
||||
* ```
|
||||
*/
|
||||
public async deleteMessage(messageId: string): Promise<void> {
|
||||
return this.client.request(`/message/${messageId}`, {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthUsersV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_users_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID or username
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to the user data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const user = await client.labrinth.users_v2.get('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async get(idOrUsername: string): Promise<Labrinth.Users.v2.User> {
|
||||
return this.client.request<Labrinth.Users.v2.User>(`/user/${idOrUsername}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple users by their IDs
|
||||
*
|
||||
* @param ids - Array of user IDs
|
||||
* @returns Promise resolving to an array of users
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const users = await client.labrinth.users_v2.getMultiple(['id1', 'id2'])
|
||||
* ```
|
||||
*/
|
||||
public async getMultiple(ids: string[]): Promise<Labrinth.Users.v2.User[]> {
|
||||
return this.client.request<Labrinth.Users.v2.User[]>(
|
||||
`/users?ids=${encodeURIComponent(JSON.stringify(ids))}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's projects
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of the user's projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projects = await client.labrinth.users_v2.getProjects('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getProjects(idOrUsername: string): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return this.client.request<Labrinth.Projects.v2.Project[]>(`/user/${idOrUsername}/projects`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's organizations
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of the user's organizations
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const orgs = await client.labrinth.users_v2.getOrganizations('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getOrganizations(
|
||||
idOrUsername: string,
|
||||
): Promise<Labrinth.Organizations.v3.Organization[]> {
|
||||
return this.client.request<Labrinth.Organizations.v3.Organization[]>(
|
||||
`/user/${idOrUsername}/organizations`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's collections
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of the user's collections
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const collections = await client.labrinth.users_v2.getCollections('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getCollections(idOrUsername: string): Promise<Labrinth.Collections.Collection[]> {
|
||||
return this.client.request<Labrinth.Collections.Collection[]>(
|
||||
`/user/${idOrUsername}/collections`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user's notifications
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of the user's notifications
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const notifications = await client.labrinth.users_v2.getNotifications('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getNotifications(
|
||||
idOrUsername: string,
|
||||
): Promise<Labrinth.Notifications.v2.Notification[]> {
|
||||
return this.client.request<Labrinth.Notifications.v2.Notification[]>(
|
||||
`/user/${idOrUsername}/notifications`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get projects a user follows
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to an array of followed projects
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const projects = await client.labrinth.users_v2.getFollowedProjects('my_user')
|
||||
* ```
|
||||
*/
|
||||
public async getFollowedProjects(idOrUsername: string): Promise<Labrinth.Projects.v2.Project[]> {
|
||||
return this.client.request<Labrinth.Projects.v2.Project[]>(`/user/${idOrUsername}/follows`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @param data - Fields to update
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.users_v2.patch('my_user', { role: 'admin' })
|
||||
* ```
|
||||
*/
|
||||
public async patch(
|
||||
idOrUsername: string,
|
||||
data: Partial<Pick<Labrinth.Users.v2.User, 'badges' | 'role'>>,
|
||||
): Promise<void> {
|
||||
return this.client.request(`/user/${idOrUsername}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'PATCH',
|
||||
body: data,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthUsersV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_users_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authenticated user.
|
||||
* GET /v3/user
|
||||
*/
|
||||
public async getAuthenticated(): Promise<Labrinth.Users.v3.User> {
|
||||
return this.client.request<Labrinth.Users.v3.User>('/user', {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user by ID or username
|
||||
*
|
||||
* @param idOrUsername - The user's ID or username
|
||||
* @returns Promise resolving to the user data
|
||||
*
|
||||
* GET /v3/user/{id}
|
||||
*/
|
||||
public async get(idOrUsername: string): Promise<Labrinth.Users.v3.User> {
|
||||
return this.client.request<Labrinth.Users.v3.User>(
|
||||
`/user/${encodeURIComponent(idOrUsername)}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Search users by username prefix.
|
||||
*
|
||||
* @param query - Username search query
|
||||
* @returns Promise resolving to compact user search results
|
||||
*
|
||||
* GET /v3/users/search?query=:query
|
||||
*/
|
||||
public async search(query: string): Promise<Labrinth.Users.v3.SearchUser[]> {
|
||||
return this.client.request<Labrinth.Users.v3.SearchUser[]>(
|
||||
`/users/search?query=${encodeURIComponent(query)}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all projects the authenticated user can access directly or through
|
||||
* their organizations.
|
||||
*
|
||||
* @param idOrUsername - User ID or username. Must be the authenticated user.
|
||||
*
|
||||
* GET /v3/user/{id}/all-projects
|
||||
*/
|
||||
public async getAllProjects(
|
||||
idOrUsername: string,
|
||||
): Promise<Labrinth.Users.v3.AllProjectsResponse> {
|
||||
return this.client.request<Labrinth.Users.v3.AllProjectsResponse>(
|
||||
`/user/${encodeURIComponent(idOrUsername)}/all-projects`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
export class LabrinthVersionsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_versions_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get versions for a project (v2)
|
||||
*
|
||||
* @param id - Project ID or slug (e.g., 'sodium' or 'AANobbMI')
|
||||
* @param options - Optional query parameters to filter versions
|
||||
* @returns Promise resolving to an array of v2 versions
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const versions = await client.labrinth.versions_v2.getProjectVersions('sodium')
|
||||
* const filteredVersions = await client.labrinth.versions_v2.getProjectVersions('sodium', {
|
||||
* game_versions: ['1.20.1'],
|
||||
* loaders: ['fabric'],
|
||||
* include_changelog: false
|
||||
* })
|
||||
* console.log(versions[0].version_number)
|
||||
* ```
|
||||
*/
|
||||
public async getProjectVersions(
|
||||
id: string,
|
||||
options?: Labrinth.Versions.v2.GetProjectVersionsParams,
|
||||
): Promise<Labrinth.Versions.v2.Version[]> {
|
||||
const params: Record<string, string> = {}
|
||||
if (options?.game_versions?.length) {
|
||||
params.game_versions = JSON.stringify(options.game_versions)
|
||||
}
|
||||
if (options?.loaders?.length) {
|
||||
params.loaders = JSON.stringify(options.loaders)
|
||||
}
|
||||
if (options?.include_changelog === false) {
|
||||
params.include_changelog = 'false'
|
||||
}
|
||||
if (options?.limit != null) {
|
||||
params.limit = String(options.limit)
|
||||
}
|
||||
if (options?.offset != null) {
|
||||
params.offset = String(options.offset)
|
||||
}
|
||||
|
||||
return this.client.request<Labrinth.Versions.v2.Version[]>(`/project/${id}/version`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
params: Object.keys(params).length > 0 ? params : undefined,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific version by ID (v2)
|
||||
*
|
||||
* @param id - Version ID
|
||||
* @returns Promise resolving to the v2 version data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const version = await client.labrinth.versions_v2.getVersion('DXtmvS8i')
|
||||
* console.log(version.version_number)
|
||||
* ```
|
||||
*/
|
||||
public async getVersion(id: string): Promise<Labrinth.Versions.v2.Version> {
|
||||
return this.client.request<Labrinth.Versions.v2.Version>(`/version/${id}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get multiple versions by IDs (v2)
|
||||
*
|
||||
* @param ids - Array of version IDs
|
||||
* @returns Promise resolving to an array of v2 versions
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const versions = await client.labrinth.versions_v2.getVersions(['DXtmvS8i', 'abc123'])
|
||||
* console.log(versions[0].version_number)
|
||||
* ```
|
||||
*/
|
||||
public async getVersions(ids: string[]): Promise<Labrinth.Versions.v2.Version[]> {
|
||||
return this.client.request<Labrinth.Versions.v2.Version[]>(`/versions`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
params: { ids: JSON.stringify(ids) },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a version from a project by version ID or number (v2)
|
||||
*
|
||||
* @param projectId - Project ID or slug
|
||||
* @param versionId - Version ID or version number
|
||||
* @returns Promise resolving to the v2 version data
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const version = await client.labrinth.versions_v2.getVersionFromIdOrNumber('sodium', 'DXtmvS8i')
|
||||
* const versionByNumber = await client.labrinth.versions_v2.getVersionFromIdOrNumber('sodium', '0.4.12')
|
||||
* ```
|
||||
*/
|
||||
public async getVersionFromIdOrNumber(
|
||||
projectId: string,
|
||||
versionId: string,
|
||||
): Promise<Labrinth.Versions.v2.Version> {
|
||||
return this.client.request<Labrinth.Versions.v2.Version>(
|
||||
`/project/${projectId}/version/${versionId}`,
|
||||
{
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'GET',
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a version by ID (v2)
|
||||
*
|
||||
* @param versionId - Version ID
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.labrinth.versions_v2.deleteVersion('DXtmvS8i')
|
||||
* ```
|
||||
*/
|
||||
public async deleteVersion(versionId: string): Promise<void> {
|
||||
return this.client.request(`/version/${versionId}`, {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
method: 'DELETE',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { UploadHandle } from '../../../types/upload'
|
||||
import type { Labrinth } from '../types'
|
||||
|
||||
const VERSION_UPLOAD_TIMEOUT_MS = 30 * 60 * 1000
|
||||
|
||||
export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'labrinth_versions_v3'
|
||||
@@ -35,8 +37,14 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
if (options?.loaders?.length) {
|
||||
params.loaders = JSON.stringify(options.loaders)
|
||||
}
|
||||
if (options?.include_changelog !== undefined) {
|
||||
params.include_changelog = options.include_changelog
|
||||
if (options?.include_changelog === false) {
|
||||
params.include_changelog = 'false'
|
||||
}
|
||||
if (options?.limit != null) {
|
||||
params.limit = String(options.limit)
|
||||
}
|
||||
if (options?.offset != null) {
|
||||
params.offset = String(options.offset)
|
||||
}
|
||||
|
||||
return this.client.request<Labrinth.Versions.v3.Version[]>(`/project/${id}/version`, {
|
||||
@@ -193,7 +201,7 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
api: 'labrinth',
|
||||
version: 3,
|
||||
formData,
|
||||
timeout: 60 * 5 * 1000,
|
||||
timeout: VERSION_UPLOAD_TIMEOUT_MS,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -278,7 +286,7 @@ export class LabrinthVersionsV3Module extends AbstractModule {
|
||||
api: 'labrinth',
|
||||
version: 2,
|
||||
formData,
|
||||
timeout: 60 * 5 * 1000,
|
||||
timeout: VERSION_UPLOAD_TIMEOUT_MS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
export namespace LauncherMeta {
|
||||
export namespace Manifest {
|
||||
export namespace v0 {
|
||||
export type LoaderVersion = {
|
||||
id: string
|
||||
stable: boolean
|
||||
}
|
||||
|
||||
export type GameVersionEntry = {
|
||||
id: string
|
||||
loaders: LoaderVersion[]
|
||||
}
|
||||
|
||||
export type Manifest = {
|
||||
gameVersions: GameVersionEntry[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { AbstractModule } from '../../core/abstract-module'
|
||||
import type { LauncherMeta } from './types'
|
||||
|
||||
export type { LauncherMeta } from './types'
|
||||
|
||||
const LAUNCHER_META_BASE_URL = 'https://launcher-meta.modrinth.com'
|
||||
|
||||
export class LauncherMetaManifestV0Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'launchermeta_manifest_v0'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the loader manifest for a given loader platform.
|
||||
*
|
||||
* launcher-meta refuses CORS preflights that ask for the `Content-Type`
|
||||
* header (returns 403), so we strip the default `Content-Type: application/json`
|
||||
* the abstract client sets — these are body-less GETs and don't need it.
|
||||
* Without this the browser preflight is rejected and the GET never fires.
|
||||
*
|
||||
* @param loader - Loader platform (fabric, forge, quilt, neo)
|
||||
*/
|
||||
public async getManifest(loader: string): Promise<LauncherMeta.Manifest.v0.Manifest> {
|
||||
return this.client.request<LauncherMeta.Manifest.v0.Manifest>('/manifest.json', {
|
||||
api: LAUNCHER_META_BASE_URL,
|
||||
version: `${loader}/v0`,
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
headers: { 'Content-Type': '' },
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Mclogs } from '../types'
|
||||
|
||||
export class MclogsInsightsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'mclogs_insights_v1'
|
||||
}
|
||||
|
||||
public async analyse(content: string): Promise<Mclogs.Insights.v1.InsightsResponse> {
|
||||
return this.client.request<Mclogs.Insights.v1.InsightsResponse>('/analyse', {
|
||||
api: 'https://api.mclo.gs',
|
||||
version: '1',
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ content }),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { AbstractModule } from '../../../core/abstract-module'
|
||||
import type { Mclogs } from '../types'
|
||||
|
||||
export class MclogsLogsV1Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'mclogs_logs_v1'
|
||||
}
|
||||
|
||||
public async create(content: string): Promise<Mclogs.Logs.v1.CreateResponse> {
|
||||
return this.client.request<Mclogs.Logs.v1.CreateResponse>('/log', {
|
||||
api: 'https://api.mclo.gs',
|
||||
version: '1',
|
||||
method: 'POST',
|
||||
body: new URLSearchParams({ content }),
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export namespace Mclogs {
|
||||
export namespace Insights {
|
||||
export namespace v1 {
|
||||
export type LogEntry = {
|
||||
level: number
|
||||
time: string | null
|
||||
prefix: string
|
||||
lines: Array<{ number: number; content: string }>
|
||||
}
|
||||
|
||||
export type Solution = {
|
||||
message: string
|
||||
}
|
||||
|
||||
export type Problem = {
|
||||
message: string
|
||||
counter: number
|
||||
entry: LogEntry
|
||||
solutions: Solution[]
|
||||
}
|
||||
|
||||
export type Information = {
|
||||
message: string
|
||||
counter: number
|
||||
label: string
|
||||
value: string
|
||||
entry: LogEntry
|
||||
}
|
||||
|
||||
export type Analysis = {
|
||||
problems: Problem[]
|
||||
information: Information[]
|
||||
}
|
||||
|
||||
export type InsightsResponse = {
|
||||
id: string
|
||||
name: string
|
||||
type: string
|
||||
version: string
|
||||
title: string
|
||||
analysis: Analysis
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace Logs {
|
||||
export namespace v1 {
|
||||
export type CreateResponse = {
|
||||
success: boolean
|
||||
id: string
|
||||
source: string | null
|
||||
created: number
|
||||
expires: number
|
||||
size: number
|
||||
lines: number
|
||||
errors: number
|
||||
url: string
|
||||
raw: string
|
||||
token: string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
export namespace Paper {
|
||||
export namespace Versions {
|
||||
export namespace v3 {
|
||||
export type Project = {
|
||||
project: { id: string; name: string }
|
||||
versions: Record<string, string[]>
|
||||
}
|
||||
|
||||
export type BuildChannel = 'STABLE' | 'BETA' | 'ALPHA'
|
||||
|
||||
export type Build = {
|
||||
id: number
|
||||
time: string
|
||||
channel: BuildChannel | string
|
||||
}
|
||||
|
||||
export type VersionBuilds = {
|
||||
builds: Build[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { AbstractModule } from '../../core/abstract-module'
|
||||
import type { Paper } from './types'
|
||||
|
||||
export type { Paper } from './types'
|
||||
|
||||
const PAPER_BASE_URL = 'https://fill.papermc.io'
|
||||
|
||||
export class PaperVersionsV3Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'paper_versions_v3'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Paper project info including all supported Minecraft versions.
|
||||
*/
|
||||
public async getProject(): Promise<Paper.Versions.v3.Project> {
|
||||
return this.client.request<Paper.Versions.v3.Project>('/projects/paper', {
|
||||
api: PAPER_BASE_URL,
|
||||
version: 'v3',
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Paper builds for a Minecraft version (includes channel per build).
|
||||
*
|
||||
* Fill (`fill.papermc.io`) returns a JSON array of builds at this path — not a `{ builds }`
|
||||
* wrapper like some other Paper API shapes — so we normalize to `VersionBuilds`.
|
||||
*
|
||||
* @param mcVersion - Minecraft version (e.g. "1.21.4")
|
||||
*/
|
||||
public async getBuilds(mcVersion: string): Promise<Paper.Versions.v3.VersionBuilds> {
|
||||
const builds = await this.client.request<Paper.Versions.v3.Build[]>(
|
||||
`/projects/paper/versions/${mcVersion}/builds`,
|
||||
{ api: PAPER_BASE_URL, version: 'v3', method: 'GET', skipAuth: true },
|
||||
)
|
||||
return { builds }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export namespace Purpur {
|
||||
export namespace Versions {
|
||||
export namespace v2 {
|
||||
export type Project = {
|
||||
project: string
|
||||
versions: string[]
|
||||
}
|
||||
|
||||
export type VersionBuilds = {
|
||||
builds: {
|
||||
all: string[]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { AbstractModule } from '../../core/abstract-module'
|
||||
import type { Purpur } from './types'
|
||||
|
||||
export type { Purpur } from './types'
|
||||
|
||||
const PURPUR_BASE_URL = 'https://api.purpurmc.org'
|
||||
|
||||
export class PurpurVersionsV2Module extends AbstractModule {
|
||||
public getModuleID(): string {
|
||||
return 'purpur_versions_v2'
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Purpur project info including all supported Minecraft versions.
|
||||
*/
|
||||
public async getProject(): Promise<Purpur.Versions.v2.Project> {
|
||||
return this.client.request<Purpur.Versions.v2.Project>('/purpur', {
|
||||
api: PURPUR_BASE_URL,
|
||||
version: 'v2',
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available Purpur builds for a Minecraft version.
|
||||
*
|
||||
* @param mcVersion - Minecraft version (e.g. "1.21.4")
|
||||
*/
|
||||
public async getBuilds(mcVersion: string): Promise<Purpur.Versions.v2.VersionBuilds> {
|
||||
return this.client.request<Purpur.Versions.v2.VersionBuilds>(`/purpur/${mcVersion}`, {
|
||||
api: PURPUR_BASE_URL,
|
||||
version: 'v2',
|
||||
method: 'GET',
|
||||
skipAuth: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -2,3 +2,7 @@ export * from './archon/types'
|
||||
export * from './iso3166/types'
|
||||
export * from './kyros/types'
|
||||
export * from './labrinth/types'
|
||||
export * from './launcher-meta/types'
|
||||
export * from './mclogs/types'
|
||||
export * from './paper/types'
|
||||
export * from './purpur/types'
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { $fetch, FetchError } from 'ofetch'
|
||||
|
||||
import type { ModrinthApiError } from '../core/errors'
|
||||
import { ModrinthApiError } from '../core/errors'
|
||||
import type { ClientConfig } from '../types/client'
|
||||
import type { RequestOptions } from '../types/request'
|
||||
import { appendRequestParams, parseResponseErrorData, toFetchBody } from '../utils/fetch'
|
||||
import { GenericSyncClient } from './sync-generic'
|
||||
import { GenericWebSocketClient } from './websocket-generic'
|
||||
import { XHRUploadClient } from './xhr-upload-client'
|
||||
|
||||
/**
|
||||
* Generic platform client using ofetch
|
||||
*
|
||||
* This client works in any JavaScript environment (Node.js, browser, workers).
|
||||
* This client works in any JavaScript environment (Node.js, browser, workers, etc).
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const client = new GenericModrinthClient({
|
||||
* userAgent: 'my-app/1.0.0',
|
||||
* features: [
|
||||
* new AuthFeature({ token: 'mrp_...' }),
|
||||
* new AuthFeature({ token: async () => getOAuthToken() }),
|
||||
* new RetryFeature({ maxAttempts: 3 })
|
||||
* ]
|
||||
* })
|
||||
@@ -34,6 +36,12 @@ export class GenericModrinthClient extends XHRUploadClient {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
Object.defineProperty(this.archon, 'sync', {
|
||||
value: new GenericSyncClient(this),
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
|
||||
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
|
||||
@@ -54,6 +62,38 @@ export class GenericModrinthClient extends XHRUploadClient {
|
||||
}
|
||||
}
|
||||
|
||||
protected async executeStreamRequest(
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
): Promise<ReadableStream<Uint8Array>> {
|
||||
try {
|
||||
const response = await fetch(appendRequestParams(url, options.params), {
|
||||
method: options.method ?? 'GET',
|
||||
headers: options.headers,
|
||||
body: toFetchBody(options.body),
|
||||
signal: options.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw this.createNormalizedError(
|
||||
new Error(`HTTP ${response.status}: ${response.statusText}`),
|
||||
response.status,
|
||||
await parseResponseErrorData(response),
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new ModrinthApiError('Streaming response has no readable body', {
|
||||
statusCode: response.status,
|
||||
})
|
||||
}
|
||||
|
||||
return response.body
|
||||
} catch (error) {
|
||||
throw this.normalizeError(error)
|
||||
}
|
||||
}
|
||||
|
||||
protected normalizeError(error: unknown): ModrinthApiError {
|
||||
if (error instanceof FetchError) {
|
||||
return this.createNormalizedError(error, error.response?.status, error.data)
|
||||
|
||||
@@ -5,6 +5,8 @@ import type { CircuitBreakerState, CircuitBreakerStorage } from '../features/cir
|
||||
import type { ClientConfig } from '../types/client'
|
||||
import type { RequestOptions } from '../types/request'
|
||||
import type { UploadHandle, UploadRequestOptions } from '../types/upload'
|
||||
import { appendRequestParams, parseResponseErrorData, toFetchBody } from '../utils/fetch'
|
||||
import { GenericSyncClient } from './sync-generic'
|
||||
import { GenericWebSocketClient } from './websocket-generic'
|
||||
import { XHRUploadClient } from './xhr-upload-client'
|
||||
|
||||
@@ -13,27 +15,32 @@ import { XHRUploadClient } from './xhr-upload-client'
|
||||
*
|
||||
* This provides cross-request persistence in SSR while also working in client-side.
|
||||
* State is shared between requests in the same Nuxt context.
|
||||
*
|
||||
* Note: useState must be called during initialization (in setup context) and cached,
|
||||
* as it won't work during async operations when the Nuxt context may be lost.
|
||||
*/
|
||||
export class NuxtCircuitBreakerStorage implements CircuitBreakerStorage {
|
||||
private getState(): Map<string, CircuitBreakerState> {
|
||||
private state: Map<string, CircuitBreakerState>
|
||||
|
||||
constructor() {
|
||||
// @ts-expect-error - useState is provided by Nuxt runtime
|
||||
const state = useState<Map<string, CircuitBreakerState>>(
|
||||
const stateRef = useState<Map<string, CircuitBreakerState>>(
|
||||
'circuit-breaker-state',
|
||||
() => new Map(),
|
||||
)
|
||||
return state.value
|
||||
this.state = stateRef.value
|
||||
}
|
||||
|
||||
get(key: string): CircuitBreakerState | undefined {
|
||||
return this.getState().get(key)
|
||||
return this.state.get(key)
|
||||
}
|
||||
|
||||
set(key: string, state: CircuitBreakerState): void {
|
||||
this.getState().set(key, state)
|
||||
this.state.set(key, state)
|
||||
}
|
||||
|
||||
clear(key: string): void {
|
||||
this.getState().delete(key)
|
||||
this.state.delete(key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,13 +68,17 @@ export interface NuxtClientConfig extends ClientConfig {
|
||||
* ```typescript
|
||||
* // In a Nuxt composable
|
||||
* const config = useRuntimeConfig()
|
||||
* const auth = await useAuth()
|
||||
*
|
||||
* const client = new NuxtModrinthClient({
|
||||
* userAgent: 'my-nuxt-app/1.0.0',
|
||||
* rateLimitKey: import.meta.server ? config.rateLimitKey : undefined,
|
||||
* features: [
|
||||
* new AuthFeature({ token: () => auth.value.token })
|
||||
* new AuthFeature({
|
||||
* token: async () => getOAuthToken()
|
||||
* }),
|
||||
* new CircuitBreakerFeature({
|
||||
* storage: new NuxtCircuitBreakerStorage()
|
||||
* })
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
@@ -88,6 +99,12 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
Object.defineProperty(this.archon, 'sync', {
|
||||
value: new GenericSyncClient(this),
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +157,7 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
|
||||
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
|
||||
try {
|
||||
// @ts-expect-error - $fetch is provided by Nuxt runtime
|
||||
// @ts-expect-error - $fetch is provided by Nuxt
|
||||
const response = await $fetch<T>(url, {
|
||||
method: options.method ?? 'GET',
|
||||
headers: options.headers,
|
||||
@@ -148,6 +165,8 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
params: options.params,
|
||||
timeout: options.timeout,
|
||||
signal: options.signal,
|
||||
// @ts-expect-error - import.meta is provided by Nuxt
|
||||
cache: import.meta.server ? undefined : 'no-store',
|
||||
})
|
||||
|
||||
return response
|
||||
@@ -156,6 +175,40 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
}
|
||||
}
|
||||
|
||||
protected async executeStreamRequest(
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
): Promise<ReadableStream<Uint8Array>> {
|
||||
try {
|
||||
const response = await fetch(appendRequestParams(url, options.params), {
|
||||
method: options.method ?? 'GET',
|
||||
headers: options.headers,
|
||||
body: toFetchBody(options.body),
|
||||
signal: options.signal,
|
||||
// @ts-expect-error - import.meta is provided by Nuxt
|
||||
cache: import.meta.server ? undefined : 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw this.createNormalizedError(
|
||||
new Error(`HTTP ${response.status}: ${response.statusText}`),
|
||||
response.status,
|
||||
await parseResponseErrorData(response),
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new ModrinthApiError('Streaming response has no readable body', {
|
||||
statusCode: response.status,
|
||||
})
|
||||
}
|
||||
|
||||
return response.body
|
||||
} catch (error) {
|
||||
throw this.normalizeError(error)
|
||||
}
|
||||
}
|
||||
|
||||
protected normalizeError(error: unknown): ModrinthApiError {
|
||||
if (error instanceof FetchError) {
|
||||
return this.createNormalizedError(error, error.response?.status, error.data)
|
||||
@@ -164,9 +217,9 @@ export class NuxtModrinthClient extends XHRUploadClient {
|
||||
return super.normalizeError(error)
|
||||
}
|
||||
|
||||
protected buildDefaultHeaders(): Record<string, string> {
|
||||
protected async buildDefaultHeaders(): Promise<Record<string, string>> {
|
||||
const headers: Record<string, string> = {
|
||||
...super.buildDefaultHeaders(),
|
||||
...(await super.buildDefaultHeaders()),
|
||||
}
|
||||
|
||||
// Use the resolved key (populated by resolveRateLimitKey in request())
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import mitt from 'mitt'
|
||||
|
||||
import {
|
||||
AbstractSyncClient,
|
||||
type SyncConnection,
|
||||
type SyncConnectOptions,
|
||||
type SyncEmitterEvents,
|
||||
} from '../core/abstract-sync'
|
||||
import type { Archon } from '../modules/archon/types'
|
||||
import { type ParsedSseItem, parseSyncEventData, SseParser } from '../utils/sse'
|
||||
|
||||
type StreamReadResult = 'closed' | 'protocol-reconnect'
|
||||
|
||||
const DEFAULT_RETRY_DELAY = 1000
|
||||
const MAX_RECONNECT_DELAY = 30000
|
||||
const JITTER_MS = 1000
|
||||
|
||||
export class GenericSyncClient extends AbstractSyncClient {
|
||||
protected emitter = mitt<SyncEmitterEvents>()
|
||||
|
||||
async safeConnectServer(serverId: string, options: SyncConnectOptions = {}): Promise<void> {
|
||||
const existing = this.connections.get(serverId)
|
||||
if (existing && !options.force && !existing.stopped && existing.status !== 'disconnected') {
|
||||
return
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
this.closeConnection(serverId)
|
||||
}
|
||||
|
||||
const connection: SyncConnection = {
|
||||
serverId,
|
||||
intent: options.intent ?? 'all',
|
||||
reconnectAttempts: 0,
|
||||
retryDelay: DEFAULT_RETRY_DELAY,
|
||||
stopped: false,
|
||||
status: 'idle',
|
||||
}
|
||||
|
||||
this.connections.set(serverId, connection)
|
||||
void this.runConnection(connection)
|
||||
}
|
||||
|
||||
disconnect(serverId: string): void {
|
||||
this.closeConnection(serverId)
|
||||
this.clearListeners(serverId)
|
||||
}
|
||||
|
||||
disconnectAll(): void {
|
||||
for (const serverId of this.connections.keys()) {
|
||||
this.disconnect(serverId)
|
||||
}
|
||||
}
|
||||
|
||||
private async runConnection(connection: SyncConnection): Promise<void> {
|
||||
while (!connection.stopped) {
|
||||
const hadConnected = connection.status === 'connected'
|
||||
this.updateStatus(connection, hadConnected ? 'reconnecting' : 'connecting')
|
||||
|
||||
const controller = new AbortController()
|
||||
connection.controller = controller
|
||||
|
||||
try {
|
||||
const stream = await this.client.stream('/sync', {
|
||||
api: 'archon',
|
||||
version: 1,
|
||||
method: 'GET',
|
||||
params: {
|
||||
scope: `server:${connection.serverId}`,
|
||||
intent: this.intentToParam(connection.intent),
|
||||
},
|
||||
headers: connection.lastEventId
|
||||
? {
|
||||
'Last-Event-Id': connection.lastEventId,
|
||||
}
|
||||
: undefined,
|
||||
signal: controller.signal,
|
||||
retry: false,
|
||||
circuitBreaker: false,
|
||||
})
|
||||
|
||||
if (connection.stopped) return
|
||||
|
||||
connection.reconnectAttempts = 0
|
||||
this.updateStatus(connection, 'connected')
|
||||
|
||||
const result = await this.consumeStream(connection, stream)
|
||||
connection.controller = undefined
|
||||
if (connection.stopped) return
|
||||
|
||||
if (result === 'protocol-reconnect') {
|
||||
connection.reconnectAttempts = 0
|
||||
continue
|
||||
}
|
||||
|
||||
await this.waitForReconnect(connection)
|
||||
} catch (error) {
|
||||
connection.controller = undefined
|
||||
if (connection.stopped || this.isAbortError(error)) return
|
||||
|
||||
connection.reconnectAttempts++
|
||||
this.updateStatus(connection, 'error', error)
|
||||
console.warn(`[Sync] Connection failed for server ${connection.serverId}:`, error)
|
||||
await this.waitForReconnect(connection)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async consumeStream(
|
||||
connection: SyncConnection,
|
||||
stream: ReadableStream<Uint8Array>,
|
||||
): Promise<StreamReadResult> {
|
||||
const reader = stream.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
const parser = new SseParser()
|
||||
|
||||
try {
|
||||
while (!connection.stopped) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
const result = this.processParsedItems(connection, parser.feed(chunk))
|
||||
if (result === 'protocol-reconnect') {
|
||||
await reader.cancel()
|
||||
connection.controller?.abort()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
const finalChunk = decoder.decode()
|
||||
const finalItems = finalChunk ? parser.feed(finalChunk) : []
|
||||
const result = this.processParsedItems(connection, [...finalItems, ...parser.end()])
|
||||
if (result === 'protocol-reconnect') {
|
||||
await reader.cancel()
|
||||
connection.controller?.abort()
|
||||
return result
|
||||
}
|
||||
} finally {
|
||||
reader.releaseLock()
|
||||
}
|
||||
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
private processParsedItems(connection: SyncConnection, items: ParsedSseItem[]): StreamReadResult {
|
||||
for (const item of items) {
|
||||
if (item.kind === 'retry') {
|
||||
connection.retryDelay = Math.min(item.retry, MAX_RECONNECT_DELAY)
|
||||
continue
|
||||
}
|
||||
|
||||
this.updateLastEventId(connection, item.id)
|
||||
|
||||
const event = parseSyncEventData(item.data)
|
||||
if (!event) {
|
||||
console.warn('[Sync] Dropping malformed SSE payload:', {
|
||||
serverId: connection.serverId,
|
||||
event: item.event,
|
||||
data: item.data,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
this.emitSyncEvent(connection.serverId, event)
|
||||
|
||||
if (event.type === 'protocol.reset' || event.type === 'protocol.invalid') {
|
||||
connection.lastEventId = undefined
|
||||
return 'protocol-reconnect'
|
||||
}
|
||||
}
|
||||
|
||||
return 'closed'
|
||||
}
|
||||
|
||||
private async waitForReconnect(connection: SyncConnection): Promise<void> {
|
||||
if (connection.stopped) return
|
||||
|
||||
this.updateStatus(connection, 'reconnecting')
|
||||
const delay = this.getReconnectDelay(connection)
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
connection.reconnectResolve = resolve
|
||||
connection.reconnectTimer = setTimeout(() => {
|
||||
connection.reconnectTimer = undefined
|
||||
connection.reconnectResolve = undefined
|
||||
resolve()
|
||||
}, delay)
|
||||
})
|
||||
}
|
||||
|
||||
private closeConnection(serverId: string): void {
|
||||
const connection = this.connections.get(serverId)
|
||||
if (!connection) return
|
||||
|
||||
connection.stopped = true
|
||||
connection.controller?.abort()
|
||||
|
||||
if (connection.reconnectTimer) {
|
||||
clearTimeout(connection.reconnectTimer)
|
||||
connection.reconnectTimer = undefined
|
||||
}
|
||||
connection.reconnectResolve?.()
|
||||
connection.reconnectResolve = undefined
|
||||
|
||||
this.updateStatus(connection, 'disconnected')
|
||||
this.connections.delete(serverId)
|
||||
}
|
||||
|
||||
private getReconnectDelay(connection: SyncConnection): number {
|
||||
const exponentialDelay =
|
||||
connection.retryDelay * Math.pow(2, Math.max(connection.reconnectAttempts - 1, 0))
|
||||
return Math.min(exponentialDelay, MAX_RECONNECT_DELAY) + Math.random() * JITTER_MS
|
||||
}
|
||||
|
||||
private updateLastEventId(connection: SyncConnection, id: string | undefined): void {
|
||||
if (id === undefined) return
|
||||
connection.lastEventId = id || undefined
|
||||
}
|
||||
|
||||
private intentToParam(intent: Archon.Sync.v1.SyncIntent): string {
|
||||
return Array.isArray(intent) ? intent.join(',') : intent
|
||||
}
|
||||
|
||||
private isAbortError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false
|
||||
return error.name === 'AbortError' || error.message.toLowerCase().includes('abort')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { ModrinthApiError } from '../core/errors'
|
||||
import type { ClientConfig } from '../types/client'
|
||||
import type { RequestOptions } from '../types/request'
|
||||
import { appendRequestParams, parseResponseErrorData, toFetchBody } from '../utils/fetch'
|
||||
import { GenericSyncClient } from './sync-generic'
|
||||
import { GenericWebSocketClient } from './websocket-generic'
|
||||
import { XHRUploadClient } from './xhr-upload-client'
|
||||
|
||||
@@ -27,11 +29,10 @@ interface HttpError extends Error {
|
||||
* ```typescript
|
||||
* import { getVersion } from '@tauri-apps/api/app'
|
||||
*
|
||||
* const version = await getVersion()
|
||||
* const client = new TauriModrinthClient({
|
||||
* userAgent: `modrinth/theseus/${version} (support@modrinth.com)`,
|
||||
* userAgent: async () => `modrinth/theseus/${await getVersion()} (support@modrinth.com)`,
|
||||
* features: [
|
||||
* new AuthFeature({ token: 'mrp_...' })
|
||||
* new AuthFeature({ token: async () => getOAuthToken() })
|
||||
* ]
|
||||
* })
|
||||
*
|
||||
@@ -50,6 +51,12 @@ export class TauriModrinthClient extends XHRUploadClient {
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
Object.defineProperty(this.archon, 'sync', {
|
||||
value: new GenericSyncClient(this),
|
||||
writable: false,
|
||||
enumerable: true,
|
||||
configurable: false,
|
||||
})
|
||||
}
|
||||
|
||||
protected async executeRequest<T>(url: string, options: RequestOptions): Promise<T> {
|
||||
@@ -58,20 +65,8 @@ export class TauriModrinthClient extends XHRUploadClient {
|
||||
// This allows the package to be used in non-Tauri environments
|
||||
const { fetch: tauriFetch } = await import('@tauri-apps/plugin-http')
|
||||
|
||||
let body: BodyInit | null | undefined = undefined
|
||||
if (options.body) {
|
||||
if (typeof options.body === 'object' && !(options.body instanceof FormData)) {
|
||||
body = JSON.stringify(options.body)
|
||||
} else {
|
||||
body = options.body as BodyInit
|
||||
}
|
||||
}
|
||||
|
||||
let fullUrl = url
|
||||
if (options.params) {
|
||||
const queryParams = new URLSearchParams(options.params as Record<string, string>).toString()
|
||||
fullUrl = `${url}?${queryParams}`
|
||||
}
|
||||
const body = toFetchBody(options.body)
|
||||
const fullUrl = appendRequestParams(url, options.params)
|
||||
|
||||
const response = await tauriFetch(fullUrl, {
|
||||
method: options.method ?? 'GET',
|
||||
@@ -95,8 +90,73 @@ export class TauriModrinthClient extends XHRUploadClient {
|
||||
throw error
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
return data as T
|
||||
// Handle binary downloads (e.g. kyros fs files) before JSON parsing.
|
||||
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
|
||||
if (fullUrl.includes('/fs/download')) {
|
||||
return (await response.blob()) as T
|
||||
}
|
||||
if (
|
||||
contentType.startsWith('image/') ||
|
||||
contentType.startsWith('audio/') ||
|
||||
contentType.startsWith('video/') ||
|
||||
contentType.includes('application/octet-stream')
|
||||
) {
|
||||
return (await response.blob()) as T
|
||||
}
|
||||
|
||||
if (response.status === 204 || response.status === 205) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
if (contentType.includes('application/json') || contentType.includes('+json')) {
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (!text) {
|
||||
return undefined as T
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(text) as T
|
||||
} catch {
|
||||
return text as T
|
||||
}
|
||||
} catch (error) {
|
||||
throw this.normalizeError(error)
|
||||
}
|
||||
}
|
||||
|
||||
protected async executeStreamRequest(
|
||||
url: string,
|
||||
options: RequestOptions,
|
||||
): Promise<ReadableStream<Uint8Array>> {
|
||||
try {
|
||||
const { fetch: tauriFetch } = await import('@tauri-apps/plugin-http')
|
||||
const response = await tauriFetch(appendRequestParams(url, options.params), {
|
||||
method: options.method ?? 'GET',
|
||||
headers: options.headers,
|
||||
body: toFetchBody(options.body),
|
||||
signal: options.signal,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw this.createNormalizedError(
|
||||
new Error(`HTTP ${response.status}: ${response.statusText}`),
|
||||
response.status,
|
||||
await parseResponseErrorData(response),
|
||||
)
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw this.createNormalizedError(
|
||||
new Error('Streaming response has no readable body'),
|
||||
response.status,
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
|
||||
return response.body
|
||||
} catch (error) {
|
||||
throw this.normalizeError(error)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
|
||||
async connect(serverId: string, auth: Archon.Websocket.v0.WSAuth): Promise<void> {
|
||||
if (this.connections.has(serverId)) {
|
||||
this.disconnect(serverId)
|
||||
this.closeConnection(serverId)
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -57,14 +57,30 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.debug(`[WebSocket] Closed for server ${serverId}:`, {
|
||||
code: event.code,
|
||||
reason: event.reason,
|
||||
wasClean: event.wasClean,
|
||||
})
|
||||
if (event.code !== NORMAL_CLOSURE) {
|
||||
this.scheduleReconnect(serverId, auth)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error(`[WebSocket] Error for server ${serverId}:`, error)
|
||||
reject(new Error(`WebSocket connection failed for server ${serverId}`))
|
||||
ws.onerror = (event) => {
|
||||
const url = ws.url
|
||||
const readyState = ws.readyState
|
||||
console.error(`[WebSocket] Error for server ${serverId}:`, {
|
||||
url,
|
||||
readyState,
|
||||
readyStateLabel: ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'][readyState],
|
||||
type: (event as Event).type,
|
||||
})
|
||||
reject(
|
||||
new Error(
|
||||
`WebSocket connection failed for server ${serverId} (readyState: ${readyState})`,
|
||||
),
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
@@ -73,6 +89,16 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
}
|
||||
|
||||
disconnect(serverId: string): void {
|
||||
this.closeConnection(serverId)
|
||||
|
||||
this.emitter.all.forEach((_handlers, type) => {
|
||||
if (type.toString().startsWith(`${serverId}:`)) {
|
||||
this.emitter.all.delete(type)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private closeConnection(serverId: string): void {
|
||||
const connection = this.connections.get(serverId)
|
||||
if (!connection) return
|
||||
|
||||
@@ -88,12 +114,6 @@ export class GenericWebSocketClient extends AbstractWebSocketClient {
|
||||
connection.socket.close(NORMAL_CLOSURE, 'Client disconnecting')
|
||||
}
|
||||
|
||||
this.emitter.all.forEach((_handlers, type) => {
|
||||
if (type.toString().startsWith(`${serverId}:`)) {
|
||||
this.emitter.all.delete(type)
|
||||
}
|
||||
})
|
||||
|
||||
this.connections.delete(serverId)
|
||||
}
|
||||
|
||||
|
||||
@@ -18,58 +18,66 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
|
||||
upload<T = void>(path: string, options: UploadRequestOptions): UploadHandle<T> {
|
||||
let baseUrl: string
|
||||
if (options.api === 'labrinth') {
|
||||
baseUrl = this.config.labrinthBaseUrl!
|
||||
baseUrl = this.resolveBaseUrl(this.config.labrinthBaseUrl!)
|
||||
} else if (options.api === 'archon') {
|
||||
baseUrl = this.config.archonBaseUrl!
|
||||
baseUrl = this.resolveBaseUrl(this.config.archonBaseUrl!)
|
||||
} else {
|
||||
baseUrl = options.api
|
||||
}
|
||||
|
||||
const url = this.buildUrl(path, baseUrl, options.version)
|
||||
|
||||
// For FormData uploads, don't set Content-Type (let browser set multipart boundary)
|
||||
// For file uploads, use application/octet-stream
|
||||
const isFormData = 'formData' in options && options.formData instanceof FormData
|
||||
const baseHeaders = this.buildDefaultHeaders()
|
||||
// Remove Content-Type for FormData so browser can set multipart/form-data with boundary
|
||||
if (isFormData) {
|
||||
delete baseHeaders['Content-Type']
|
||||
} else {
|
||||
baseHeaders['Content-Type'] = 'application/octet-stream'
|
||||
}
|
||||
|
||||
const mergedOptions: UploadRequestOptions = {
|
||||
retry: false, // default: don't retry uploads
|
||||
...options,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
|
||||
const context = this.buildUploadContext(url, path, mergedOptions)
|
||||
|
||||
const progressCallbacks: Array<(p: UploadProgress) => void> = []
|
||||
if (mergedOptions.onProgress) {
|
||||
progressCallbacks.push(mergedOptions.onProgress)
|
||||
if (options.onProgress) {
|
||||
progressCallbacks.push(options.onProgress)
|
||||
}
|
||||
const abortController = new AbortController()
|
||||
|
||||
if (mergedOptions.signal) {
|
||||
mergedOptions.signal.addEventListener('abort', () => abortController.abort())
|
||||
if (options.signal) {
|
||||
options.signal.addEventListener('abort', () => abortController.abort())
|
||||
}
|
||||
|
||||
let context: RequestContext | undefined
|
||||
const handle: UploadHandle<T> = {
|
||||
promise: this.executeUploadFeatureChain<T>(context, progressCallbacks, abortController)
|
||||
.then(async (result) => {
|
||||
await this.config.hooks?.onResponse?.(result, context)
|
||||
return result
|
||||
})
|
||||
.catch(async (error) => {
|
||||
const apiError = this.normalizeError(error, context)
|
||||
promise: (async () => {
|
||||
const isFormData = 'formData' in options && options.formData instanceof FormData
|
||||
const baseHeaders = await this.buildDefaultHeaders()
|
||||
if (isFormData) {
|
||||
delete baseHeaders['Content-Type']
|
||||
} else {
|
||||
baseHeaders['Content-Type'] = 'application/octet-stream'
|
||||
}
|
||||
|
||||
const mergedOptions: UploadRequestOptions = {
|
||||
retry: false,
|
||||
...options,
|
||||
headers: {
|
||||
...baseHeaders,
|
||||
...options.headers,
|
||||
},
|
||||
}
|
||||
this.attachArchonSentryCaptureHeader(mergedOptions)
|
||||
|
||||
const uploadContext = this.buildUploadContext(url, path, mergedOptions)
|
||||
context = uploadContext
|
||||
if (abortController.signal.aborted) {
|
||||
throw new ModrinthApiError('Upload cancelled')
|
||||
}
|
||||
|
||||
const result = await this.executeUploadFeatureChain<T>(
|
||||
uploadContext,
|
||||
progressCallbacks,
|
||||
abortController,
|
||||
)
|
||||
await this.config.hooks?.onResponse?.(result, uploadContext)
|
||||
return result
|
||||
})().catch(async (error) => {
|
||||
const apiError = this.normalizeError(error, context)
|
||||
if (context) {
|
||||
await this.config.hooks?.onError?.(apiError, context)
|
||||
throw apiError
|
||||
}),
|
||||
}
|
||||
throw apiError
|
||||
}),
|
||||
onProgress: (callback) => {
|
||||
progressCallbacks.push(callback)
|
||||
return handle
|
||||
@@ -114,6 +122,11 @@ export abstract class XHRUploadClient extends AbstractModrinthClient {
|
||||
|
||||
xhr.addEventListener('error', () => reject(new ModrinthApiError('Upload failed')))
|
||||
xhr.addEventListener('abort', () => reject(new ModrinthApiError('Upload cancelled')))
|
||||
xhr.addEventListener('timeout', () => reject(new ModrinthApiError('Upload timed out')))
|
||||
|
||||
if (context.options.timeout !== undefined) {
|
||||
xhr.timeout = context.options.timeout
|
||||
}
|
||||
|
||||
// build URL with params (unlike $fetch, XHR doesn't handle params automatically)
|
||||
let url = context.url
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { AbstractFeature } from '../core/abstract-feature'
|
||||
import type { RequestContext } from './request'
|
||||
|
||||
export type MaybePromise<T> = T | Promise<T>
|
||||
export type UserAgentProvider = string | (() => MaybePromise<string | undefined>)
|
||||
export type BaseUrlConfig = string | (() => string)
|
||||
|
||||
/**
|
||||
* Request lifecycle hooks
|
||||
*/
|
||||
@@ -26,23 +30,25 @@ export type RequestHooks = {
|
||||
*/
|
||||
export interface ClientConfig {
|
||||
/**
|
||||
* User agent string for requests
|
||||
* User agent string or provider for requests
|
||||
* Should identify your application (e.g., 'my-app/1.0.0')
|
||||
* If not provided, the platform's default user agent will be used
|
||||
*/
|
||||
userAgent?: string
|
||||
userAgent?: UserAgentProvider
|
||||
|
||||
/**
|
||||
* Base URL for Labrinth API (main Modrinth API)
|
||||
* @default 'https://api.modrinth.com'
|
||||
*/
|
||||
labrinthBaseUrl?: string
|
||||
labrinthBaseUrl?: BaseUrlConfig
|
||||
|
||||
/**
|
||||
* Base URL for Archon API (Modrinth Hosting API)
|
||||
* Can be a callback so apps can drive this from runtime feature flags.
|
||||
*
|
||||
* @default 'https://archon.modrinth.com'
|
||||
*/
|
||||
archonBaseUrl?: string
|
||||
archonBaseUrl?: BaseUrlConfig
|
||||
|
||||
/**
|
||||
* Default request timeout in milliseconds
|
||||
@@ -55,6 +61,14 @@ export interface ClientConfig {
|
||||
*/
|
||||
headers?: Record<string, string>
|
||||
|
||||
/**
|
||||
* Whether to attach `modrinth-sentry-capture: 1` to Archon requests.
|
||||
* Can be a callback so apps can drive this from runtime feature flags.
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
archonSentryCapture?: boolean | (() => boolean)
|
||||
|
||||
/**
|
||||
* Features to enable for this client
|
||||
* Features are applied in the order they appear in this array
|
||||
|
||||
@@ -7,8 +7,14 @@ export type {
|
||||
} from '../features/circuit-breaker'
|
||||
export type { BackoffStrategy, RetryConfig } from '../features/retry'
|
||||
export type { Archon } from '../modules/archon/types'
|
||||
export type { ClientConfig, RequestHooks } from './client'
|
||||
export type { BaseUrlConfig, ClientConfig, RequestHooks } from './client'
|
||||
export type { ApiErrorData, ModrinthErrorResponse } from './errors'
|
||||
export { isModrinthErrorResponse } from './errors'
|
||||
export type { HttpMethod, RequestContext, RequestOptions, ResponseData } from './request'
|
||||
export type { UploadHandle, UploadMetadata, UploadProgress, UploadRequestOptions } from './upload'
|
||||
export type {
|
||||
UploadHandle,
|
||||
UploadMetadata,
|
||||
UploadProgress,
|
||||
UploadRequestOptions,
|
||||
UploadState,
|
||||
} from './upload'
|
||||
|
||||
@@ -86,3 +86,16 @@ export interface UploadHandle<T> {
|
||||
/** Cancel the upload */
|
||||
cancel: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* State of a batch file upload operation
|
||||
*/
|
||||
export interface UploadState {
|
||||
isUploading: boolean
|
||||
currentFileName: string | null
|
||||
currentFileProgress: number
|
||||
uploadedBytes: number
|
||||
totalBytes: number
|
||||
completedFiles: number
|
||||
totalFiles: number
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import type { RequestOptions } from '../types/request'
|
||||
|
||||
export function appendRequestParams(url: string, params?: RequestOptions['params']): string {
|
||||
if (!params) return url
|
||||
|
||||
const filteredParams: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value !== undefined && value !== null) {
|
||||
filteredParams[key] = String(value)
|
||||
}
|
||||
}
|
||||
|
||||
const queryString = new URLSearchParams(filteredParams).toString()
|
||||
if (!queryString) return url
|
||||
|
||||
return `${url}${url.includes('?') ? '&' : '?'}${queryString}`
|
||||
}
|
||||
|
||||
export function toFetchBody(body: unknown): BodyInit | null | undefined {
|
||||
if (!body) return undefined
|
||||
|
||||
if (
|
||||
typeof body === 'object' &&
|
||||
!(body instanceof FormData) &&
|
||||
!(body instanceof URLSearchParams) &&
|
||||
!(body instanceof Blob) &&
|
||||
!(body instanceof ArrayBuffer) &&
|
||||
!ArrayBuffer.isView(body as ArrayBufferView)
|
||||
) {
|
||||
return JSON.stringify(body)
|
||||
}
|
||||
|
||||
return body as BodyInit
|
||||
}
|
||||
|
||||
export async function parseResponseErrorData(response: Response): Promise<unknown> {
|
||||
const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
|
||||
|
||||
try {
|
||||
if (contentType.includes('application/json') || contentType.includes('+json')) {
|
||||
return await response.json()
|
||||
}
|
||||
|
||||
const text = await response.text()
|
||||
if (!text) return undefined
|
||||
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import type { Archon } from '../modules/archon/types'
|
||||
|
||||
export type ParsedSseEvent = {
|
||||
kind: 'event'
|
||||
id?: string
|
||||
event?: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export type ParsedSseRetry = {
|
||||
kind: 'retry'
|
||||
retry: number
|
||||
}
|
||||
|
||||
export type ParsedSseItem = ParsedSseEvent | ParsedSseRetry
|
||||
|
||||
export class SseParser {
|
||||
private buffer = ''
|
||||
private eventName = ''
|
||||
private data = ''
|
||||
private id: string | undefined
|
||||
|
||||
feed(chunk: string): ParsedSseItem[] {
|
||||
this.buffer += chunk
|
||||
const items: ParsedSseItem[] = []
|
||||
|
||||
while (true) {
|
||||
const lineEnd = this.findLineEnd()
|
||||
if (!lineEnd) break
|
||||
|
||||
const { line, length } = lineEnd
|
||||
this.buffer = this.buffer.slice(length)
|
||||
this.processLine(line, items)
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
end(): ParsedSseItem[] {
|
||||
const items: ParsedSseItem[] = []
|
||||
|
||||
if (this.buffer.length > 0) {
|
||||
this.processLine(this.buffer.endsWith('\r') ? this.buffer.slice(0, -1) : this.buffer, items)
|
||||
this.buffer = ''
|
||||
}
|
||||
|
||||
this.dispatch(items)
|
||||
return items
|
||||
}
|
||||
|
||||
private findLineEnd(): { line: string; length: number } | null {
|
||||
const lf = this.buffer.indexOf('\n')
|
||||
const cr = this.buffer.indexOf('\r')
|
||||
|
||||
if (lf === -1 && cr === -1) return null
|
||||
|
||||
if (cr !== -1 && (lf === -1 || cr < lf)) {
|
||||
if (cr === this.buffer.length - 1) return null
|
||||
const length = this.buffer[cr + 1] === '\n' ? cr + 2 : cr + 1
|
||||
return {
|
||||
line: this.buffer.slice(0, cr),
|
||||
length,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
line: this.buffer.slice(0, lf),
|
||||
length: lf + 1,
|
||||
}
|
||||
}
|
||||
|
||||
private processLine(line: string, items: ParsedSseItem[]): void {
|
||||
if (line === '') {
|
||||
this.dispatch(items)
|
||||
return
|
||||
}
|
||||
|
||||
if (line.startsWith(':')) return
|
||||
|
||||
const colon = line.indexOf(':')
|
||||
const field = colon === -1 ? line : line.slice(0, colon)
|
||||
let value = colon === -1 ? '' : line.slice(colon + 1)
|
||||
if (value.startsWith(' ')) value = value.slice(1)
|
||||
|
||||
switch (field) {
|
||||
case 'event':
|
||||
this.eventName = value
|
||||
break
|
||||
case 'data':
|
||||
this.data += `${value}\n`
|
||||
break
|
||||
case 'id':
|
||||
this.id = value
|
||||
break
|
||||
case 'retry': {
|
||||
const retry = Number(value)
|
||||
if (Number.isInteger(retry) && retry >= 0) {
|
||||
items.push({ kind: 'retry', retry })
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private dispatch(items: ParsedSseItem[]): void {
|
||||
if (!this.data) {
|
||||
this.eventName = ''
|
||||
this.id = undefined
|
||||
return
|
||||
}
|
||||
|
||||
items.push({
|
||||
kind: 'event',
|
||||
id: this.id,
|
||||
event: this.eventName || undefined,
|
||||
data: this.data.endsWith('\n') ? this.data.slice(0, -1) : this.data,
|
||||
})
|
||||
|
||||
this.eventName = ''
|
||||
this.data = ''
|
||||
this.id = undefined
|
||||
}
|
||||
}
|
||||
|
||||
export function parseSyncEventData(data: string): Archon.Sync.v1.SyncEvent | null {
|
||||
let parsed: unknown
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(data)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
const event = parsed as { type?: unknown }
|
||||
if (typeof event.type !== 'string') return null
|
||||
|
||||
return parsed as Archon.Sync.v1.SyncEvent
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export type Override<T, R> = Omit<T, keyof R> & R
|
||||
export type RawDecimal = string | number
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"noEmit": false,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
MODRINTH_URL=http://localhost:3000/
|
||||
MODRINTH_API_BASE_URL=http://localhost:8000/
|
||||
MODRINTH_API_URL=http://127.0.0.1:8000/v2/
|
||||
MODRINTH_API_URL_V3=http://127.0.0.1:8000/v3/
|
||||
MODRINTH_SOCKET_URL=ws://127.0.0.1:8000/
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
MODRINTH_URL=https://modrinth.com/
|
||||
MODRINTH_API_BASE_URL=https://api.modrinth.com/
|
||||
MODRINTH_ARCHON_BASE_URL=https://archon.modrinth.com/
|
||||
MODRINTH_API_URL=https://api.modrinth.com/v2/
|
||||
MODRINTH_API_URL_V3=https://api.modrinth.com/v3/
|
||||
MODRINTH_SOCKET_URL=wss://api.modrinth.com/
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
MODRINTH_URL=https://modrinth.com/
|
||||
MODRINTH_API_BASE_URL=https://api.modrinth.com/
|
||||
MODRINTH_ARCHON_BASE_URL=https://staging-archon.modrinth.com/
|
||||
MODRINTH_API_URL=https://api.modrinth.com/v2/
|
||||
MODRINTH_API_URL_V3=https://api.modrinth.com/v3/
|
||||
MODRINTH_SOCKET_URL=wss://api.modrinth.com/
|
||||
MODRINTH_LAUNCHER_META_URL=https://launcher-meta.modrinth.com/
|
||||
|
||||
# SQLite database file used by sqlx for type checking. Uncomment this to a valid path
|
||||
# in your system and run `cargo sqlx database setup` to generate an empty database that
|
||||
# can be used for developing the app DB schema
|
||||
#DATABASE_URL=sqlite:///tmp/Modrinth/code/packages/app-lib/.sqlx/generated/state.db
|
||||
@@ -1,4 +1,6 @@
|
||||
MODRINTH_URL=https://staging.modrinth.com/
|
||||
MODRINTH_API_BASE_URL=https://staging-api.modrinth.com/
|
||||
MODRINTH_ARCHON_BASE_URL=https://staging-archon.modrinth.com/
|
||||
MODRINTH_API_URL=https://staging-api.modrinth.com/v2/
|
||||
MODRINTH_API_URL_V3=https://staging-api.modrinth.com/v3/
|
||||
MODRINTH_SOCKET_URL=wss://staging-api.modrinth.com/
|
||||
|
||||
@@ -5,13 +5,4 @@ java/build
|
||||
|
||||
# Migrations existing before Prettier formatted them shall always be ignored,
|
||||
# as any changes to them will break existing deployments
|
||||
migrations/20240711194701_init.sql
|
||||
migrations/20240813205023_drop-active-unique.sql
|
||||
migrations/20240930001852_disable-personalized-ads.sql
|
||||
migrations/20241222013857_feature-flags.sql
|
||||
migrations/20250318160526_protocol-versions.sql
|
||||
migrations/20250408181656_add-join-log.sql
|
||||
migrations/20250413162050_skin-selector.sql
|
||||
migrations/20250428171350_add-profile-feature-version.sql
|
||||
migrations/20250429150111_add-attached-world-data.sql
|
||||
migrations/20250514181748_skin_nametag_setting.sql
|
||||
migrations/**
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid = ? AND texture_key = ? AND variant = ? AND cape_id IS ?",
|
||||
"query": "DELETE FROM custom_minecraft_skins WHERE minecraft_user_uuid = ? AND texture_key = ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "faa8437519571b147b0135054847674be5035f385e0d85e759d4bbf9bca54f20"
|
||||
"hash": "08908e54884b79705500501389344f3dc52fc81d34b0e9a44f5b9bede487cfa6"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "UPDATE custom_minecraft_skins SET display_order = display_order + 1 WHERE minecraft_user_uuid = ? AND display_order >= ?",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "11875ae76d8a61099dff08e9792fd6f231ecffd45315ed2b76253d02bbd531ff"
|
||||
}
|
||||
Generated
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT display_order FROM custom_minecraft_skins WHERE minecraft_user_uuid = ? AND texture_key = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "display_order",
|
||||
"ordinal": 0,
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "18f04a0f6c262995b5f1eee10c2c5a396443ead9a9e295f6eea0986e40d65449"
|
||||
}
|
||||
Generated
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO profiles (\n path, install_stage, name, icon_path,\n game_version, mod_loader, mod_loader_version,\n groups,\n linked_project_id, linked_version_id, locked, preferred_update_channel,\n created, modified, last_played,\n submitted_time_played, recent_time_played,\n override_java_path, override_extra_launch_args, override_custom_env_vars,\n override_mc_memory_max, override_mc_force_fullscreen, override_mc_game_resolution_x, override_mc_game_resolution_y,\n override_hook_pre_launch, override_hook_wrapper, override_hook_post_exit,\n protocol_version, launcher_feature_version\n )\n VALUES (\n $1, $2, $3, $4,\n $5, $6, $7,\n jsonb($8),\n $9, $10, $11, $12,\n $13, $14, $15,\n $16, $17,\n $18, jsonb($19), jsonb($20),\n $21, $22, $23, $24,\n $25, $26, $27,\n $28, $29\n )\n ON CONFLICT (path) DO UPDATE SET\n install_stage = $2,\n name = $3,\n icon_path = $4,\n\n game_version = $5,\n mod_loader = $6,\n mod_loader_version = $7,\n\n groups = jsonb($8),\n\n linked_project_id = $9,\n linked_version_id = $10,\n locked = $11,\n preferred_update_channel = $12,\n\n created = $13,\n modified = $14,\n last_played = $15,\n\n submitted_time_played = $16,\n recent_time_played = $17,\n\n override_java_path = $18,\n override_extra_launch_args = jsonb($19),\n override_custom_env_vars = jsonb($20),\n override_mc_memory_max = $21,\n override_mc_force_fullscreen = $22,\n override_mc_game_resolution_x = $23,\n override_mc_game_resolution_y = $24,\n\n override_hook_pre_launch = $25,\n override_hook_wrapper = $26,\n override_hook_post_exit = $27,\n\n protocol_version = $28,\n launcher_feature_version = $29\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 29
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "22202dacf6b5bade90c909cb4a663a1388ef538e4b80680bce4b668b182b4f42"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user