* feat: rpc

* chore: update CODEOWNERS

* fix: linter rules

* chore: add rpc to workflows and readme

* chore: remove rpc tests folder

* chore: add package details from rpc repo

* docs: rpc docs readme

* chore: fix rpc license

* chore: remove vitest

* fix: rpc links

* build: dependencies

* docs: "id"

* chore: awkward doc spacing

* chore: lockfile

* chore: clean stuff

* fix: apply suggested changes

* chore: suggested changes

* chore: suggested changes

* chore: suggested changes

* fix: silly things

* chore: suggested changes

* chore: suggested changes

* chore: suggested changes

* chore: suggested changes

* suggested changes

* remove examples in favor of later guide additions

* add very basic guide for rpc

* meta config changes for guide

* guide changes

* suggested changes

* suggested changes

* Update packages/rpc/package.json

Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>

* Update packages/rpc/package.json

* fix: revert version bumps, use esm

* fix regression from suggested change

* suggested changes

* forgot to remove extra image

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: Qjuh <76154676+Qjuh@users.noreply.github.com>
This commit is contained in:
Micah Benac
2026-08-14 14:28:37 +00:00
committed by GitHub
co-authored by Qjuh Jiralite
parent 905445ec97
commit c9855614c4
43 changed files with 1911 additions and 10 deletions
+1
View File
@@ -28,6 +28,7 @@ pnpm-lock.yaml @discordjs/core
/packages/next/ @discordjs/core
/packages/proxy/ @discordjs/proxy
/packages/rest/ @discordjs/rest
/packages/rpc/ @discordjs/rpc
/packages/scripts/ @discordjs/scripts
/packages/structures/ @discordjs/structures
/packages/ui/ @discordjs/ui
@@ -23,6 +23,7 @@ body:
- next
- proxy
- rest
- rpc
- structures
- ui
- util
@@ -26,6 +26,7 @@ body:
- next
- proxy
- rest
- rpc
- structures
- ui
- util
+3
View File
@@ -37,6 +37,9 @@ packages:proxy:
packages:rest:
- "### Which (application|package|application or package) is this (bug
report|feature request) for\\?\\n\\nrest\\n"
packages:rpc:
- "### Which (application|package|application or package) is this (bug
report|feature request) for\\?\\n\\nrpc\\n"
packages:structures:
- "### Which (application|package|application or package) is this (bug
report|feature request) for\\?\\n\\nstructures\\n"
+5
View File
@@ -78,6 +78,11 @@ packages:rest:
- any-glob-to-any-file:
- packages/rest/*
- packages/rest/**/*
packages:rpc:
- changed-files:
- any-glob-to-any-file:
- packages/rpc/*
- packages/rpc/**/*
packages:structures:
- changed-files:
- any-glob-to-any-file:
+2
View File
@@ -80,6 +80,8 @@
color: fbca04
- name: packages:rest
color: fbca04
- name: packages:rpc
color: fbca04
- name: packages:structures
color: fbca04
- name: packages:ui
+1
View File
@@ -18,6 +18,7 @@ on:
- '@discordjs/next'
- '@discordjs/proxy'
- '@discordjs/rest'
- '@discordjs/rpc'
- '@discordjs/structures'
- '@discordjs/util'
- '@discordjs/voice'
+1 -1
View File
@@ -88,7 +88,7 @@ jobs:
- name: Build docs with main api-extractor
if: ${{ inputs.ref && inputs.ref != 'main' }}
run: |
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "structures" "util" "voice" "ws")
declare -a PACKAGES=("brokers" "builders" "collection" "core" "discord.js" "formatters" "next" "proxy" "rest" "rpc" "structures" "util" "voice" "ws")
for PACKAGE in "${PACKAGES[@]}"; do
if [ ! -d "packages/${PACKAGE}" ]; then
echo "::notice::${PACKAGE} does not exist on this ref. Skipping..."
+1
View File
@@ -23,6 +23,7 @@ on:
- '@discordjs/next'
- '@discordjs/proxy'
- '@discordjs/rest'
- '@discordjs/rpc'
- '@discordjs/structures'
- '@discordjs/util'
- '@discordjs/voice'
+2 -6
View File
@@ -35,6 +35,7 @@ This repository contains multiple packages with separate [releases][github-relea
- `@discordjs/formatters` ([source][formatters-source]) - A collection of functions for formatting strings
- `@discordjs/proxy` ([source][proxy-source]) - A wrapper around `@discordjs/rest` for running an HTTP proxy
- `@discordjs/rest` ([source][rest-source]) - A module for interacting with the Discord REST API
- `@discordjs/rpc` ([source][rpc-source]) - A simple RPC client for Discord
- `@discordjs/structures` ([source][structures-source]) - A wrapper around Discord's structures
- `@discordjs/util` ([source][util-source]) - A collection of utility functions
- `@discordjs/voice` ([source][voice-source]) - A module for interacting with the Discord Voice API
@@ -56,10 +57,6 @@ This repository contains multiple packages with separate [releases][github-relea
- [npm][npm]
- [Related libraries][related-libs]
### Extensions
- [RPC][rpc] ([source][rpc-source])
## Contributing
Please read through our [contribution guidelines][contributing] before starting a pull request. We welcome contributions of all kinds, not just code! If you're stuck for ideas, look for the [good first issue][good-first-issue] label on issues in the repository. If you have any questions about the project, feel free to ask them on [Discord][discord]. Before creating your own issue or pull request, always check to see if one already exists! Don't rush contributions, take your time and ensure you're doing it correctly.
@@ -79,8 +76,6 @@ If you don't understand something in the documentation, you are experiencing pro
[source]: https://github.com/discordjs/discord.js/tree/main/packages/discord.js
[npm]: https://www.npmjs.com/package/discord.js
[related-libs]: https://docs.discord.com/developers/developer-tools/community-resources#libraries
[rpc]: https://www.npmjs.com/package/discord-rpc
[rpc-source]: https://github.com/discordjs/RPC
[contributing]: https://github.com/discordjs/discord.js/blob/main/.github/CONTRIBUTING.md
[github-releases]: https://github.com/discordjs/discord.js/releases
[create-discord-bot-source]: https://github.com/discordjs/discord.js/tree/main/packages/create-discord-bot
@@ -91,6 +86,7 @@ If you don't understand something in the documentation, you are experiencing pro
[formatters-source]: https://github.com/discordjs/discord.js/tree/main/packages/formatters
[proxy-source]: https://github.com/discordjs/discord.js/tree/main/packages/proxy
[rest-source]: https://github.com/discordjs/discord.js/tree/main/packages/rest
[rpc-source]: https://github.com/discordjs/discord.js/tree/main/packages/rpc
[structures-source]: https://github.com/discordjs/discord.js/tree/main/packages/structures
[util-source]: https://github.com/discordjs/discord.js/tree/main/packages/util
[voice-source]: https://github.com/discordjs/discord.js/tree/main/packages/voice
Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

@@ -14,7 +14,7 @@ It's effortless to create one. The steps you need to take are as follows:
You should see a page like this:
![Successfully created application](./images/create-app.png)
![Successfully created application](../../common_images/create-app.png)
You can edit your application's name, description, and avatar here. Once you've done that, then congratulations—you're now the proud owner of a shiny new Discord bot! You're not entirely done, though.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

+1 -1
View File
@@ -1,3 +1,3 @@
{
"pages": ["legacy", "v15", "voice"]
"pages": ["legacy", "v15", "voice", "rpc"]
}
+25
View File
@@ -0,0 +1,25 @@
---
title: Application Setup
---
## Creating your RPC application
It's effortless to create one. The steps you need to take are as follows:
1. Open the [Discord developer portal](https://discord.com/developers/applications) and log into your account.
2. Click on the "New Application" button.
3. Enter a name and confirm the pop-up window by clicking the "Create" button.
You should see a page like this:
![Successfully created application](../common_images/create-app.png)
## Your app's client id and secret
After creating an app, you'll see a section like this under OAuth2:
![App Oauth2](./images/app-oauth2.png)
Here you will need the client id and the secret, which you'll need to click "Reset Secret" to obtain. These are necessary for the authentication/authorization of your RPC client.
Setting the Redirects is also important so your app can redirect a user after authorizing your app if you request for any scopes from them. As shown in the image, the example redirect points to an RPC client running locally on port 3000.
Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

+58
View File
@@ -0,0 +1,58 @@
---
title: Usage
---
### Legend
- `client` is a placeholder for the `RPCClient` object, such as `const client = new RPCClient({ scopes: [Oauth2Scopes.MessagesRead] });`.
### Authorization
Authorizing your client:
```ts
client.login({ clientId: env.CLIENT_ID, clientSecret: env.CLIENT_SECRET });
```
If the current user has already been authorized recently:
```ts
client.login({ clientId: env.CLIENT_ID, accessToken: env.ACCESS_TOKEN });
```
Optionally, if you aren't requesting for any scopes, you can exclude the client secret:
```ts
client.login({ clientId: env.CLIENT_ID });
```
### RPC Commands
Update a user's activity:
```ts
let updatesDoneSinceStart = 0;
setInterval(async () => {
await client.setActivity({
details: 'Doing silly things: ${updatesDoneSinceStart}',
state: 'Silly stuff :3',
name: 'Silly',
type: 1,
});
updatesDoneSinceStart++;
}, 15_000);
```
### RPC Events
Listen for messages on the current user's discord client:
<Callout>Listening for messages requires the scope `messages.read`</Callout>
```ts
client.on(RPCEvents.MessageCreate, (data: RPCMessageCreateDispatchData) =>
console.log(`${data.message.author.username} posted ${data.message.content}`),
);
```
+7
View File
@@ -0,0 +1,7 @@
{
"pages": ["app-setup", "index"],
"title": "RPC",
"description": "The discord.js RPC guide",
"icon": "Book",
"root": true
}
+2 -1
View File
@@ -12,7 +12,8 @@ export function middleware(request: NextRequest) {
if (
!request.nextUrl.pathname.startsWith('/legacy') &&
!request.nextUrl.pathname.startsWith('/voice') &&
!request.nextUrl.pathname.startsWith('/v15')
!request.nextUrl.pathname.startsWith('/v15') &&
!request.nextUrl.pathname.startsWith('/rpc')
) {
const newUrl = request.nextUrl.clone();
newUrl.pathname = `/legacy${newUrl.pathname}`;
+1
View File
@@ -9,6 +9,7 @@ export const PACKAGES = [
{ name: 'next' },
{ name: 'proxy' },
{ name: 'rest' },
{ name: 'rpc' },
{ name: 'structures' },
{ name: 'util' },
{ name: 'voice' },
@@ -88,6 +88,15 @@ runs:
flags: rest
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload RPC Coverage
if: ${{ hashFiles('packages/rpc/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
with:
files: ./packages/rpc/coverage/cobertura-coverage.xml
disable_search: true
flags: rpc
token: ${{ inputs.CODECOV_TOKEN }}
- name: Upload Structures Coverage
if: ${{ hashFiles('packages/structures/coverage/cobertura-coverage.xml') != '' }}
uses: codecov/codecov-action@v4
+6
View File
@@ -0,0 +1,6 @@
{
"name": "rpc",
"org": "discordjs",
"packagePath": "packages/rpc",
"identifierBase": false
}
+2
View File
@@ -0,0 +1,2 @@
/** @type {import('lint-staged').Config} */
module.exports = require('../../.lintstagedrc.json');
+7
View File
@@ -0,0 +1,7 @@
.turbo
coverage
dist
dist-docs
docs/docs.api.json
CHANGELOG.md
tsup.config.bundled*
+2
View File
@@ -0,0 +1,2 @@
/** @type {import('prettier').Config} */
module.exports = require('../../.prettierrc.json');
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 devsnek
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+6
View File
@@ -0,0 +1,6 @@
{
"extends": "../../api-extractor.json",
"docModel": {
"projectFolderUrl": "https://github.com/discordjs/discord.js/tree/main/packages/rpc"
}
}
+79
View File
@@ -0,0 +1,79 @@
[changelog]
header = """
# Changelog
All notable changes to this project will be documented in this file.\n
"""
body = """
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
{% if version %}\
# [{{ version | trim_start_matches(pat="v") }}]\
{% if previous %}\
{% if previous.version %}\
({{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }})\
{% else %}\
({{ self::remote_url() }}/tree/{{ version }})\
{% endif %}\
{% endif %} \
- ({{ timestamp | date(format="%Y-%m-%d") }})
{% else %}\
# [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
## {{ group | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}\
**{{commit.scope}}:** \
{% endif %}\
{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ self::remote_url() }}/commit/{{ commit.id }}))\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif %}\
{% if commit.breaking %}\
{% for footer in commit.footers %}\
{% if footer.breaking %}\
\n{% raw %} {% endraw %}- **{{ footer.token }}{{ footer.separator }}** {{ footer.value }}\
{% endif %}\
{% endfor %}\
{% endif %}\
{% endfor %}
{% endfor %}\
{% if github.contributors | filter(attribute="is_first_time", value=true) | length %}\
\n### New Contributors\n
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}\
* @{{ contributor.username }} made their first contribution in #{{ contributor.pr_number }}
{% endfor %}\
{% endif %}\n
"""
trim = true
footer = ""
[git]
conventional_commits = true
filter_unconventional = true
commit_parsers = [
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^docs", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^types", group = "Typings"},
{ message = ".*deprecated", body = ".*deprecated", group = "Deprecation"},
{ message = "^revert", skip = true},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore", skip = true},
{ message = "^ci", skip = true},
{ message = "^build", skip = true},
{ body = ".*security", group = "Security"},
]
filter_commits = true
protect_breaking_commits = true
tag_pattern = "@discordjs/rpc@[0-9]*"
ignore_tags = ""
topo_order = false
sort_commits = "newest"
[remote.github]
owner = "discordjs"
repo = "discord.js"
+1
View File
@@ -0,0 +1 @@
## [View the documentation here.](https://discord.js.org/docs/packages/rpc/main)
+82
View File
@@ -0,0 +1,82 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "@discordjs/rpc",
"version": "0.1.0",
"description": "A simple RPC client for Discord",
"scripts": {
"build": "tsc",
"build:docs": "tsc -p tsconfig.docs.json",
"lint": "prettier --check . && cross-env TIMING=1 eslint --format=pretty src",
"format": "prettier --write . && cross-env TIMING=1 eslint --fix --format=pretty src",
"fmt": "pnpm run format",
"docs": "pnpm run build:docs && api-extractor run --local --minify",
"prepack": "pnpm run build && pnpm run lint",
"changelog": "git cliff --prepend ./CHANGELOG.md -u -c ./cliff.toml -r ../../ --include-path 'packages/rpc/*'",
"release": "cliff-jumper"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"directories": {
"lib": "src"
},
"files": [
"dist"
],
"contributors": [
"Crawl <icrawltogo@gmail.com>",
"SpaceEEC <spaceeec@yahoo.com>",
"Vlad Frangu <me@vladfrangu.dev>",
"Aura Román <kyradiscord@gmail.com>",
"Micah Benac <micahbenac52@gmail.com>"
],
"license": "MIT",
"keywords": [
"discord",
"rpc",
"rich presence",
"remote procedural call"
],
"repository": {
"type": "git",
"url": "https://github.com/discordjs/discord.js.git",
"directory": "packages/rpc"
},
"bugs": {
"url": "https://github.com/discordjs/discord.js/issues"
},
"homepage": "https://discord.js.org",
"funding": "https://github.com/discordjs/discord.js?sponsor",
"dependencies": {
"@vladfrangu/async_event_emitter": "^2.4.6",
"discord-api-types": "^0.38.50"
},
"devDependencies": {
"@discordjs/api-extractor": "workspace:^",
"@favware/cliff-jumper": "^4.1.0",
"@types/node": "^22.17.2",
"cross-env": "^10.0.0",
"esbuild-plugin-version-injector": "^1.2.1",
"eslint": "^9.33.0",
"eslint-config-neon": "^0.2.7",
"eslint-formatter-compact": "^8.40.0",
"eslint-formatter-pretty": "^6.0.1",
"prettier": "^3.6.2",
"tsup": "^8.5.0",
"turbo": "^2.5.6",
"typescript": "~5.9.2"
},
"engines": {
"node": ">=24.17.0"
},
"publishConfig": {
"access": "public",
"provenance": true
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { RPCErrorCodes, RPCErrorDispatchData } from 'discord-api-types/v10';
export class RPCEventError extends Error {
public code?: RPCErrorCodes;
public data?: RPCErrorDispatchData;
public constructor(data: RPCErrorDispatchData | string) {
if (typeof data === 'string') {
super(data);
} else {
super(data.message);
this.code = data.code;
this.data = data;
}
this.name = 'RPCEventError';
}
}
+674
View File
@@ -0,0 +1,674 @@
import { randomUUID } from 'node:crypto';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
import type {
RESTPostOAuth2AccessTokenResult,
RESTPostOAuth2AccessTokenURLEncodedData,
RPCCertifiedDevice,
RPCCreateChannelInviteResultData,
RPCGetChannelResultData,
RPCGetChannelsResultData,
RPCGetGuildResultData,
RPCGetGuildsResultData,
RPCGetImageArgs,
RPCGetVoiceSettingsResultData,
RPCMessage,
RPCMessagePayload,
RPCOAuth2Application,
RPCSelectTextChannelArgs,
RPCSelectTextChannelResultData,
RPCSelectVoiceChannelArgs,
RPCSelectVoiceChannelResultData,
RPCSetActivityArgs,
RPCSetCertifiedDevicesResultData,
RPCSetUserVoiceSettingsArgs,
RPCSetUserVoiceSettingsResultData,
RPCSetVoiceSettingsArgs,
RPCUnsubscribeResultData,
APIUser,
OAuth2Scopes,
Snowflake,
RPCAuthorizeArgs,
RPCCommandSubscribePayload,
RPCSubscribeResultData,
} from 'discord-api-types/v10';
import { RPCEvents, RPCCommands, Routes, RouteBases } from 'discord-api-types/v10';
import { RPCEventError } from './RPCEventError.js';
import type {
EventAndArgsParameters,
MappedRPCCommandsArgs,
MappedRPCCommandsResultsData,
MappedRPCEventsDispatchData,
RPCCallableCommands,
} from './constants.js';
import { Events } from './constants.js';
import { IPCTransport } from './ipc.js';
import { getPid, isValidTimestamp } from './util.js';
export interface RPCClientOptions {
/**
* https://discord.com/developers/docs/topics/oauth2#authorization-code-grant
*
* for authorization requests
*/
prompt?: 'consent' | 'none';
/**
* https://discord.com/developers/docs/topics/oauth2#authorization-code-grant
*
* for authorization requests
*/
redirectUri?: string;
/**
* An array of scopes
*/
scopes?: OAuth2Scopes[];
/**
* the client username
*/
username?: string;
}
export interface RPCLoginOptions {
/**
* Access Token
*/
accessToken?: string;
/**
* Client id
*/
clientId: string;
/**
* Client Secret
*/
clientSecret?: string;
}
export interface RequestOptions {
signal?: AbortSignal;
}
/**
* The client for interacting with Discord RPC
*/
export class RPCClient extends AsyncEventEmitter<MappedRPCEventsDispatchData> {
public options: RPCClientOptions;
public accessToken: string;
public clientId: string;
public clientSecret: string;
public readyTimestamp: number | null;
public application: RPCOAuth2Application | null;
public user: APIUser | null;
/**
* The transport for the IPC
*/
public transport: IPCTransport;
/**
* Map of nonces being expected from the transport
*/
readonly #expectedNonces: Map<
string,
{ reject(this: void, reason?: unknown): void; resolve(this: void, value: unknown): void }
>;
public constructor(options: RPCClientOptions = {}) {
super();
this.options = options;
this.accessToken = '';
this.clientId = '';
this.clientSecret = '';
this.readyTimestamp = null;
this.application = null;
this.user = null;
this.transport = new IPCTransport(this);
this.transport.on('message', this.#onRpcMessage.bind(this));
this.#expectedNonces = new Map();
}
/**
* Returns the Date for when the client became ready
*/
public get readyAt() {
if (this.readyTimestamp === null) return null;
return new Date(this.readyTimestamp);
}
/**
* Returns true if the client is currently ready
*/
public get ready() {
return this.readyTimestamp !== null;
}
/**
* Search and connect to RPC
*/
public async connect(options?: RequestOptions) {
const { promise, resolve, reject } = Promise.withResolvers<RPCClient>();
if (this.ready) {
resolve(this);
return promise;
}
const signal = options?.signal ?? AbortSignal.timeout(10e3);
const onAbort: (_: Event) => void = (_: Event) => {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
this.removeListener(RPCEvents.Ready, onReady);
// eslint-disable-next-line @typescript-eslint/no-use-before-define
this.transport.removeListener('close', onClose);
const reason = signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
reject(reason);
};
const onReady = () => {
signal.removeEventListener('abort', onAbort);
this.readyTimestamp = Date.now();
resolve(this);
};
const onClose = (data: unknown) => {
signal.removeEventListener('abort', onAbort);
this.removeListener(RPCEvents.Ready, onReady);
for (const expNonce of this.#expectedNonces.values()) {
expNonce.reject(new RPCEventError('connection closed'));
}
this.#expectedNonces.clear();
this.readyTimestamp = null;
this.emit(Events.Disconnected);
reject(new RPCEventError(JSON.stringify(data)));
};
signal.addEventListener('abort', onAbort);
this.once(RPCEvents.Ready, onReady);
this.transport.once('close', onClose);
try {
await this.transport.connect();
} catch (error) {
this.readyTimestamp = null;
signal.removeEventListener('abort', onAbort);
this.removeListener(RPCEvents.Ready, onReady);
this.transport.removeListener('close', onClose);
reject(error);
}
return promise;
}
/**
* Performs authentication flow. Automatically calls Client#connect if needed.
*
* @example
* logging in with a client id and secret
* ```ts
* client.login({ clientId: '1234567', clientSecret: 'abcdef123' });
* ```
*/
public async login(
{ clientId, clientSecret, accessToken }: RPCLoginOptions,
options?: RequestOptions,
): Promise<RPCClient> {
if (!clientId) {
throw new Error('A client id must be provided to login');
}
this.clientId = clientId;
await this.connect(options);
if (!this.options.scopes) {
this.emit(Events.ApplicationReady);
return this;
}
if (accessToken) {
return this.authenticate(accessToken, options);
}
if (!clientSecret) {
throw new Error('A client secret must be provided for authorization if scopes are included');
}
this.clientSecret = clientSecret;
const authorizeArgs: RPCAuthorizeArgs = { client_id: this.clientId, scopes: this.options.scopes };
if (this.options.username) authorizeArgs.username = this.options.username;
return this.authenticate(await this.authorize(authorizeArgs, options), options);
}
/**
* Request
*
* @param cmd - Command
* @param args - Arguments
* @param evt - Event
*/
public async request<Cmd extends RPCCallableCommands = RPCCallableCommands>(
cmd: Cmd,
args?: MappedRPCCommandsArgs[Cmd],
options?: RequestOptions & { evt?: RPCEvents },
): Promise<MappedRPCCommandsResultsData[Cmd]>;
public async request(
cmd: RPCCommands.Subscribe | RPCCommands.Unsubscribe,
args: RPCCommandSubscribePayload['args'],
options: RequestOptions & { evt: RPCEvents },
): Promise<RPCSubscribeResultData | RPCUnsubscribeResultData>;
public async request<Cmd extends RPCCallableCommands = RPCCallableCommands>(
cmd: Cmd,
args: MappedRPCCommandsArgs[Cmd] = {} as MappedRPCCommandsArgs[Cmd],
options?: RequestOptions & { evt?: RPCEvents },
) {
const signal = options?.signal ?? AbortSignal.timeout(10e3);
signal.throwIfAborted();
const { promise, resolve, reject } = Promise.withResolvers<MappedRPCCommandsResultsData[Cmd]>();
const nonce = randomUUID();
const payload: { args: MappedRPCCommandsArgs[Cmd]; cmd: Cmd; evt?: RPCEvents; nonce: string } = {
cmd,
args,
nonce,
};
if (cmd === RPCCommands.Subscribe || cmd === RPCCommands.Unsubscribe) {
// eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain
payload.evt = options?.evt!;
}
this.transport.send(payload as RPCMessagePayload);
this.#expectedNonces.set(nonce, { resolve, reject });
const onAbort = (_: Event) => {
this.#expectedNonces.delete(nonce);
const reason = signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason));
reject(reason);
};
signal.addEventListener('abort', onAbort, { once: true });
try {
return await promise;
} finally {
signal.removeEventListener('abort', onAbort);
}
}
/**
* Message handler
*
* @param message - message
*/
#onRpcMessage(message: RPCMessage): void {
if (message.cmd === RPCCommands.Dispatch && message.evt === RPCEvents.Ready) {
if (message.data.user) {
this.user = message.data.user;
}
this.emit(RPCEvents.Ready, message.data);
} else if (message.cmd === RPCCommands.Dispatch) {
this.emit(message.evt, message.data);
} else {
if (!this.#expectedNonces.has(message.nonce)) {
return;
}
const { resolve, reject } = this.#expectedNonces.get(message.nonce)!;
if ('evt' in message && message.evt === RPCEvents.Error) {
reject(new RPCEventError(message.data));
} else {
resolve(message.data);
}
this.#expectedNonces.delete(message.nonce);
}
}
/**
* Authorize
*
* @param args - authorize args
*/
private async authorize(args: RPCAuthorizeArgs, options?: RequestOptions): Promise<string> {
const { code } = await this.request(RPCCommands.Authorize, args, options);
const jsonBody: RESTPostOAuth2AccessTokenURLEncodedData = {
client_id: this.clientId,
client_secret: this.clientSecret,
code,
grant_type: 'authorization_code',
};
if (this.options.redirectUri) {
jsonBody.redirect_uri = this.options.redirectUri;
}
const response = await fetch(`${RouteBases.api}${Routes.oauth2TokenExchange()}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(jsonBody),
...options,
});
if (!response.ok) {
throw new Error(response.statusText);
}
const data = (await response.json()) as RESTPostOAuth2AccessTokenResult;
if (!('access_token' in data)) {
throw new Error(JSON.stringify(response));
}
return data.access_token;
}
/**
* Authenticate
*
* @param accessToken - access token
*/
public async authenticate(accessToken: string, options?: RequestOptions): Promise<this> {
const { application, user } = await this.request(RPCCommands.Authenticate, { access_token: accessToken }, options);
this.accessToken = accessToken;
this.application = application;
this.user = user;
this.emit(Events.ApplicationReady);
return this;
}
/**
* Fetch a guild
*
* @param id - Guild id
* @param timeout - Timeout request
*/
public async getGuild(id: Snowflake, timeout: number, options?: RequestOptions): Promise<RPCGetGuildResultData> {
return this.request(RPCCommands.GetGuild, { guild_id: id, timeout }, options);
}
/**
* Fetch all guilds
*
* @param timeout - Timeout request
*/
public async getGuilds(timeout: number, options?: RequestOptions): Promise<RPCGetGuildsResultData> {
return this.request(RPCCommands.GetGuilds, { timeout }, options);
}
/**
* Get a channel
*
* @param id - Channel id
*/
public async getChannel(id: Snowflake, options?: RequestOptions): Promise<RPCGetChannelResultData> {
return this.request(RPCCommands.GetChannel, { channel_id: id }, options);
}
/**
* Get all channels
*
* @param id - Guild id
*/
public async getChannels(id: Snowflake, options?: RequestOptions): Promise<RPCGetChannelsResultData['channels']> {
const { channels } = await this.request(RPCCommands.GetChannels, { guild_id: id }, options);
return channels;
}
/**
* Create channel invite
*
* @param id - Channel id
*/
public async createChannelInvite(id: Snowflake, options?: RequestOptions): Promise<RPCCreateChannelInviteResultData> {
return this.request(RPCCommands.CreateChannelInvite, { channel_id: id }, options);
}
/**
* Tell discord which devices are certified
*
* @param devices - Certified devices to send to discord
*/
public async setCertifiedDevices(
devices: RPCCertifiedDevice[],
options?: RequestOptions,
): Promise<RPCSetCertifiedDevicesResultData> {
return this.request(
RPCCommands.SetCertifiedDevices,
{
devices,
},
options,
);
}
/**
* Set the voice settings for a user, by id
*
* @param id - Id of the user to set
* @param settings - Settings to set
*/
public async setUserVoiceSettings(
id: Snowflake,
settings: Omit<Partial<RPCSetUserVoiceSettingsArgs>, 'user_id'>,
options?: RequestOptions,
): Promise<RPCSetUserVoiceSettingsResultData> {
return this.request(
RPCCommands.SetUserVoiceSettings,
{
user_id: id,
...settings,
},
options,
);
}
/**
* Move the user to a voice channel
*
* @param id - Id of the voice channel
* @param args - select voice channel args
* @param options - request options
*/
public async selectVoiceChannel(
id: Snowflake,
args: Omit<RPCSelectVoiceChannelArgs, 'channel_id'> = {},
options?: RequestOptions,
): Promise<RPCSelectVoiceChannelResultData> {
const svcArgs: RPCSelectVoiceChannelArgs = { ...args, channel_id: id };
return this.request(RPCCommands.SelectVoiceChannel, svcArgs, options);
}
/**
* Move the user to a text channel
*
* @param id - Id of the text channel
* @param args - select text channel args
* @param options - request options
*/
public async selectTextChannel(
id: Snowflake,
args: Omit<RPCSelectTextChannelArgs, 'channel_id'> = {},
options?: RequestOptions,
): Promise<RPCSelectTextChannelResultData> {
const stcArgs: RPCSelectTextChannelArgs = { ...args, channel_id: id };
return this.request(RPCCommands.SelectTextChannel, stcArgs, options);
}
/**
* Get current voice settings
*
*/
public async getVoiceSettings(options?: RequestOptions): Promise<RPCGetVoiceSettingsResultData> {
return this.request(RPCCommands.GetVoiceSettings, {}, options);
}
/**
* Set current voice settings, overriding the current settings until this session disconnects.
* This also locks the settings for any other rpc sessions which may be connected.
*
* @param args - Settings
*/
public async setVoiceSettings(args: RPCSetVoiceSettingsArgs, options?: RequestOptions): Promise<unknown> {
return this.request(RPCCommands.SetVoiceSettings, args, options);
}
/**
* Sets the presence for the logged in user.
*
* @param activity - The rich presence to pass.
* @param pid - The application's process ID. Defaults to the executing process' PID.
* @remarks
* Clients may only update their activity 5 times per 20 seconds.
*/
public async setActivity(
activity: RPCSetActivityArgs['activity'] = {},
pid: number | null = getPid(),
options?: RequestOptions,
): Promise<unknown> {
activity.instance = Boolean(activity.instance);
if (activity.timestamps) {
if ('start' in activity.timestamps && isValidTimestamp(activity.timestamps.start)) {
throw new RangeError('timestamps.start must fit into a unix timestamp');
}
if ('end' in activity.timestamps && isValidTimestamp(activity.timestamps.end)) {
throw new RangeError('timestamps.end must fit into a unix timestamp');
}
}
return this.request(
RPCCommands.SetActivity,
{
pid: pid ?? 0,
activity,
},
options,
);
}
/**
* Clears the currently set presence, if any. This will hide the "Playing X" message
* displayed below the user's name.
*
* @param pid - The application's process ID. Defaults to the executing process' PID.
*/
public async clearActivity(pid: number | null = getPid(), options?: RequestOptions): Promise<unknown> {
return this.request(
RPCCommands.SetActivity,
{
pid: pid ?? 0,
},
options,
);
}
/**
* Invite a user to join the game the RPC user is currently playing
*
* @param userId - The id of the user to invite
*/
public async sendJoinInvite(userId: Snowflake, options?: RequestOptions): Promise<unknown> {
return this.request(
RPCCommands.SendActivityJoinInvite,
{
user_id: userId,
},
options,
);
}
/**
* Reject a join request from a user
*
* @param userId - The id of the user whose request you wish to reject
*/
public async closeJoinRequest(userId: Snowflake, options?: RequestOptions): Promise<unknown> {
return this.request(
RPCCommands.CloseActivityJoinRequest,
{
user_id: userId,
},
options,
);
}
/**
* requires `relationships.read` scope
*/
public async getRelationships(options?: RequestOptions) {
return this.request(RPCCommands.GetRelationships, {}, options);
}
/**
* Fetches a user
*/
public async getUser(id: Snowflake, options?: RequestOptions) {
return this.request(RPCCommands.GetUser, { id }, options);
}
/**
* Fetches a user's profile picture
*/
public async getImage(
{ id, format, size }: Omit<RPCGetImageArgs, 'type'>,
options?: RequestOptions,
): Promise<string> {
return (await this.request(RPCCommands.GetImage, { type: 'user', id, format, size }, options)).data_url;
}
/**
* Subscribe to an event
*
* @param event - Name of event e.g. `MESSAGE_CREATE`
* @param args - Args for event e.g. `{ channel_id: '1234' }`
*/
public async subscribe<Evt extends RPCEvents>(
...[event, args, options]: EventAndArgsParameters<Evt>
): Promise<{ unsubscribe(): Promise<RPCUnsubscribeResultData> }> {
await this.request(RPCCommands.Subscribe, args, { evt: event, ...options });
return {
unsubscribe: async () => this.request(RPCCommands.Unsubscribe, args, { evt: event, ...options }),
};
}
/**
* Unsubscribe from an event
*
* @param event - Name of event e.g. `MESSAGE_CREATE`
* @param args - Args for event e.g. `{ channel_id: '1234' }`
*/
public async unsubscribe<Evt extends RPCEvents>(
...[event, args, options]: EventAndArgsParameters<Evt>
): Promise<{ subscribe(): Promise<RPCSubscribeResultData> }> {
await this.request(RPCCommands.Unsubscribe, args, { evt: event, ...options });
return {
subscribe: async () => this.request(RPCCommands.Subscribe, args, { evt: event, ...options }),
};
}
/**
* Destroy the client
*/
public async destroy() {
await this.transport.close();
}
}
+340
View File
@@ -0,0 +1,340 @@
import type {
RPCAcceptActivityInviteArgs,
RPCAcceptActivityInviteResultData,
RPCActivityInviteDispatchData,
RPCActivityInviteUserArgs,
RPCActivityInviteUserResultData,
RPCActivityJoinDispatchData,
RPCActivityJoinRequestDispatchData,
RPCActivitySpectateDispatchData,
RPCAuthenticateArgs,
RPCAuthenticateResultData,
RPCAuthorizeArgs,
RPCAuthorizeResultData,
RPCBraintreePopupBridgeCallbackArgs,
RPCBraintreePopupBridgeCallbackResultData,
RPCBrowserHandoffArgs,
RPCBrowserHandoffResultData,
RPCChannelCreateDispatchData,
RPCCloseActivityJoinRequestArgs,
RPCCloseActivityJoinRequestResultData,
RPCCommandSubscribePayload,
RPCCommands,
RPCConnectionsCallbackArgs,
RPCConnectionsCallbackResultData,
RPCCreateChannelInviteArgs,
RPCCreateChannelInviteResultData,
RPCCurrentUserUpdateDispatchData,
RPCDeepLinkArgs,
RPCDeepLinkResultData,
RPCEntitlementCreateDispatchData,
RPCEntitlementDeleteDispatchData,
RPCErrorDispatchData,
RPCEvents,
RPCGameJoinDispatchData,
RPCGameSpectateDispatchData,
RPCGetApplicationTicketArgs,
RPCGetApplicationTicketResultData,
RPCGetChannelArgs,
RPCGetChannelResultData,
RPCGetChannelsArgs,
RPCGetChannelsResultData,
RPCGetEntitlementTicketArgs,
RPCGetEntitlementTicketResultData,
RPCGetEntitlementsArgs,
RPCGetEntitlementsResultData,
RPCGetGuildArgs,
RPCGetGuildResultData,
RPCGetGuildsArgs,
RPCGetGuildsResultData,
RPCGetImageArgs,
RPCGetImageResultData,
RPCGetNetworkingConfigArgs,
RPCGetNetworkingConfigResultData,
RPCGetRelationshipsArgs,
RPCGetRelationshipsResultData,
RPCGetSelectedVoiceChannelArgs,
RPCGetSelectedVoiceChannelResultData,
RPCGetSkusArgs,
RPCGetSkusResultData,
RPCGetUserArgs,
RPCGetUserResultData,
RPCGetVoiceSettingsArgs,
RPCGetVoiceSettingsResultData,
RPCGiftCodeBrowserArgs,
RPCGiftCodeBrowserResultData,
RPCGuildCreateDispatchData,
RPCGuildStatusDispatchData,
RPCGuildTemplateBrowserArgs,
RPCGuildTemplateBrowserResultData,
RPCInviteBrowserArgs,
RPCInviteBrowserResultData,
RPCMessageCreateDispatchData,
RPCMessageDeleteDispatchData,
RPCMessageUpdateDispatchData,
RPCNetworkingCreateTokenArgs,
RPCNetworkingCreateTokenResultData,
RPCNetworkingPeerMetricsArgs,
RPCNetworkingPeerMetricsResultData,
RPCNetworkingSystemMetricsArgs,
RPCNetworkingSystemMetricsResultData,
RPCNotificationCreateDispatchData,
RPCOpenOverlayActivityInviteArgs,
RPCOpenOverlayActivityInviteResultData,
RPCOpenOverlayGuildInviteArgs,
RPCOpenOverlayGuildInviteResultData,
RPCOpenOverlayVoiceSettingsArgs,
RPCOpenOverlayVoiceSettingsResultData,
RPCOverlayArgs,
RPCOverlayDispatchData,
RPCOverlayResultData,
RPCOverlayUpdateDispatchData,
RPCReadyDispatchData,
RPCRelationshipUpdateDispatchData,
RPCSelectTextChannelArgs,
RPCSelectTextChannelResultData,
RPCSelectVoiceChannelArgs,
RPCSelectVoiceChannelResultData,
RPCSendActivityJoinInviteArgs,
RPCSendActivityJoinInviteResultData,
RPCSetActivityArgs,
RPCSetActivityResultData,
RPCSetCertifiedDevicesArgs,
RPCSetCertifiedDevicesResultData,
RPCSetOverlayLockedArgs,
RPCSetOverlayLockedResultData,
RPCSetUserVoiceSettings2Args,
RPCSetUserVoiceSettings2ResultData,
RPCSetUserVoiceSettingsArgs,
RPCSetUserVoiceSettingsResultData,
RPCSetVoiceSettings2Args,
RPCSetVoiceSettings2ResultData,
RPCSetVoiceSettingsArgs,
RPCSetVoiceSettingsResultData,
RPCSpeakingStartDispatchData,
RPCSpeakingStopDispatchData,
RPCStartPurchaseArgs,
RPCStartPurchaseResultData,
RPCSubscribeActivityInviteArgs,
RPCSubscribeActivityJoinArgs,
RPCSubscribeActivityJoinRequestArgs,
RPCSubscribeActivitySpectateArgs,
RPCSubscribeChannelCreateArgs,
RPCSubscribeCurrentUserUpdateArgs,
RPCSubscribeEntitlementCreateArgs,
RPCSubscribeEntitlementDeleteArgs,
RPCSubscribeGameJoinArgs,
RPCSubscribeGameSpectateArgs,
RPCSubscribeGuildCreateArgs,
RPCSubscribeGuildStatusArgs,
RPCSubscribeMessageCreateArgs,
RPCSubscribeMessageDeleteArgs,
RPCSubscribeMessageUpdateArgs,
RPCSubscribeNotificationCreateArgs,
RPCSubscribeOverlayArgs,
RPCSubscribeOverlayUpdateArgs,
RPCSubscribeRelationshipUpdateArgs,
RPCSubscribeResultData,
RPCSubscribeSpeakingStartArgs,
RPCSubscribeSpeakingStopArgs,
RPCSubscribeVoiceChannelSelectArgs,
RPCSubscribeVoiceConnectionStatusArgs,
RPCSubscribeVoiceSettingsUpdate2Args,
RPCSubscribeVoiceSettingsUpdateArgs,
RPCSubscribeVoiceStateCreateArgs,
RPCSubscribeVoiceStateDeleteArgs,
RPCSubscribeVoiceStateUpdateArgs,
RPCUnsubscribeResultData,
RPCValidateApplicationArgs,
RPCValidateApplicationResultData,
RPCVoiceChannelSelectDispatchData,
RPCVoiceConnectionStatusDispatchData,
RPCVoiceSettingsUpdate2DispatchData,
RPCVoiceSettingsUpdateDispatchData,
RPCVoiceStateCreateDispatchData,
RPCVoiceStateDeleteDispatchData,
RPCVoiceStateUpdateDispatchData,
} from 'discord-api-types/v10';
import type { RequestOptions } from './client.js';
export enum Events {
ApplicationReady = 'APPLICATION_READY',
Disconnected = 'DISCONNECTED',
}
export interface MappedRPCCommandsResultsData {
[RPCCommands.Authorize]: RPCAuthorizeResultData;
[RPCCommands.Authenticate]: RPCAuthenticateResultData;
[RPCCommands.GetChannel]: RPCGetChannelResultData;
[RPCCommands.GetChannels]: RPCGetChannelsResultData;
[RPCCommands.GetGuild]: RPCGetGuildResultData;
[RPCCommands.GetGuilds]: RPCGetGuildsResultData;
[RPCCommands.GetUser]: RPCGetUserResultData;
[RPCCommands.GetVoiceSettings]: RPCGetVoiceSettingsResultData;
[RPCCommands.SelectTextChannel]: RPCSelectTextChannelResultData;
[RPCCommands.SelectVoiceChannel]: RPCSelectVoiceChannelResultData;
[RPCCommands.SetActivity]: RPCSetActivityResultData;
[RPCCommands.SetVoiceSettings]: RPCSetVoiceSettingsResultData;
[RPCCommands.Subscribe]: RPCSubscribeResultData;
[RPCCommands.Unsubscribe]: RPCUnsubscribeResultData;
[RPCCommands.AcceptActivityInvite]: RPCAcceptActivityInviteResultData;
[RPCCommands.ActivityInviteUser]: RPCActivityInviteUserResultData;
[RPCCommands.BraintreePopupBridgeCallback]: RPCBraintreePopupBridgeCallbackResultData;
[RPCCommands.BrowserHandoff]: RPCBrowserHandoffResultData;
[RPCCommands.CloseActivityJoinRequest]: RPCCloseActivityJoinRequestResultData;
[RPCCommands.ConnectionsCallback]: RPCConnectionsCallbackResultData;
[RPCCommands.CreateChannelInvite]: RPCCreateChannelInviteResultData;
[RPCCommands.DeepLink]: RPCDeepLinkResultData;
[RPCCommands.GetApplicationTicket]: RPCGetApplicationTicketResultData;
[RPCCommands.GetEntitlementTicket]: RPCGetEntitlementTicketResultData;
[RPCCommands.GetEntitlements]: RPCGetEntitlementsResultData;
[RPCCommands.GetImage]: RPCGetImageResultData;
[RPCCommands.GetNetworkingConfig]: RPCGetNetworkingConfigResultData;
[RPCCommands.GetRelationships]: RPCGetRelationshipsResultData;
[RPCCommands.GetSelectedVoiceChannel]: RPCGetSelectedVoiceChannelResultData;
[RPCCommands.GetSkus]: RPCGetSkusResultData;
[RPCCommands.GiftCodeBrowser]: RPCGiftCodeBrowserResultData;
[RPCCommands.GuildTemplateBrowser]: RPCGuildTemplateBrowserResultData;
[RPCCommands.InviteBrowser]: RPCInviteBrowserResultData;
[RPCCommands.NetworkingCreateToken]: RPCNetworkingCreateTokenResultData;
[RPCCommands.NetworkingPeerMetrics]: RPCNetworkingPeerMetricsResultData;
[RPCCommands.NetworkingSystemMetrics]: RPCNetworkingSystemMetricsResultData;
[RPCCommands.OpenOverlayActivityInvite]: RPCOpenOverlayActivityInviteResultData;
[RPCCommands.OpenOverlayGuildInvite]: RPCOpenOverlayGuildInviteResultData;
[RPCCommands.OpenOverlayVoiceSettings]: RPCOpenOverlayVoiceSettingsResultData;
[RPCCommands.Overlay]: RPCOverlayResultData;
[RPCCommands.SendActivityJoinInvite]: RPCSendActivityJoinInviteResultData;
[RPCCommands.SetCertifiedDevices]: RPCSetCertifiedDevicesResultData;
[RPCCommands.SetOverlayLocked]: RPCSetOverlayLockedResultData;
[RPCCommands.SetUserVoiceSettings]: RPCSetUserVoiceSettingsResultData;
[RPCCommands.SetUserVoiceSettings2]: RPCSetUserVoiceSettings2ResultData;
[RPCCommands.SetVoiceSettings2]: RPCSetVoiceSettings2ResultData;
[RPCCommands.StartPurchase]: RPCStartPurchaseResultData;
[RPCCommands.ValidateApplication]: RPCValidateApplicationResultData;
}
export interface MappedRPCCommandsArgs {
[RPCCommands.Authorize]: RPCAuthorizeArgs;
[RPCCommands.Authenticate]: RPCAuthenticateArgs;
[RPCCommands.GetChannel]: RPCGetChannelArgs;
[RPCCommands.GetChannels]: RPCGetChannelsArgs;
[RPCCommands.GetGuild]: RPCGetGuildArgs;
[RPCCommands.GetGuilds]: RPCGetGuildsArgs;
[RPCCommands.GetUser]: RPCGetUserArgs;
[RPCCommands.GetVoiceSettings]: RPCGetVoiceSettingsArgs;
[RPCCommands.SelectTextChannel]: RPCSelectTextChannelArgs;
[RPCCommands.SelectVoiceChannel]: RPCSelectVoiceChannelArgs;
[RPCCommands.SetActivity]: RPCSetActivityArgs;
[RPCCommands.SetVoiceSettings]: RPCSetVoiceSettingsArgs;
[RPCCommands.Subscribe]: RPCCommandSubscribePayload['args'];
[RPCCommands.Unsubscribe]: RPCCommandSubscribePayload['args'];
[RPCCommands.AcceptActivityInvite]: RPCAcceptActivityInviteArgs;
[RPCCommands.ActivityInviteUser]: RPCActivityInviteUserArgs;
[RPCCommands.BraintreePopupBridgeCallback]: RPCBraintreePopupBridgeCallbackArgs;
[RPCCommands.BrowserHandoff]: RPCBrowserHandoffArgs;
[RPCCommands.CloseActivityJoinRequest]: RPCCloseActivityJoinRequestArgs;
[RPCCommands.ConnectionsCallback]: RPCConnectionsCallbackArgs;
[RPCCommands.CreateChannelInvite]: RPCCreateChannelInviteArgs;
[RPCCommands.DeepLink]: RPCDeepLinkArgs;
[RPCCommands.GetApplicationTicket]: RPCGetApplicationTicketArgs;
[RPCCommands.GetEntitlementTicket]: RPCGetEntitlementTicketArgs;
[RPCCommands.GetEntitlements]: RPCGetEntitlementsArgs;
[RPCCommands.GetImage]: RPCGetImageArgs;
[RPCCommands.GetNetworkingConfig]: RPCGetNetworkingConfigArgs;
[RPCCommands.GetRelationships]: RPCGetRelationshipsArgs;
[RPCCommands.GetSelectedVoiceChannel]: RPCGetSelectedVoiceChannelArgs;
[RPCCommands.GetSkus]: RPCGetSkusArgs;
[RPCCommands.GiftCodeBrowser]: RPCGiftCodeBrowserArgs;
[RPCCommands.GuildTemplateBrowser]: RPCGuildTemplateBrowserArgs;
[RPCCommands.InviteBrowser]: RPCInviteBrowserArgs;
[RPCCommands.NetworkingCreateToken]: RPCNetworkingCreateTokenArgs;
[RPCCommands.NetworkingPeerMetrics]: RPCNetworkingPeerMetricsArgs;
[RPCCommands.NetworkingSystemMetrics]: RPCNetworkingSystemMetricsArgs;
[RPCCommands.OpenOverlayActivityInvite]: RPCOpenOverlayActivityInviteArgs;
[RPCCommands.OpenOverlayGuildInvite]: RPCOpenOverlayGuildInviteArgs;
[RPCCommands.OpenOverlayVoiceSettings]: RPCOpenOverlayVoiceSettingsArgs;
[RPCCommands.Overlay]: RPCOverlayArgs;
[RPCCommands.SendActivityJoinInvite]: RPCSendActivityJoinInviteArgs;
[RPCCommands.SetCertifiedDevices]: RPCSetCertifiedDevicesArgs;
[RPCCommands.SetOverlayLocked]: RPCSetOverlayLockedArgs;
[RPCCommands.SetUserVoiceSettings]: RPCSetUserVoiceSettingsArgs;
[RPCCommands.SetUserVoiceSettings2]: RPCSetUserVoiceSettings2Args;
[RPCCommands.SetVoiceSettings2]: RPCSetVoiceSettings2Args;
[RPCCommands.StartPurchase]: RPCStartPurchaseArgs;
[RPCCommands.ValidateApplication]: RPCValidateApplicationArgs;
}
export type RPCCallableCommands = Exclude<RPCCommands, RPCCommands.Dispatch>;
export interface MappedRPCSubscribeEventsArgs {
[RPCEvents.ActivityInvite]: RPCSubscribeActivityInviteArgs;
[RPCEvents.ActivityJoin]: RPCSubscribeActivityJoinArgs;
[RPCEvents.ActivityJoinRequest]: RPCSubscribeActivityJoinRequestArgs;
[RPCEvents.ActivitySpectate]: RPCSubscribeActivitySpectateArgs;
[RPCEvents.ChannelCreate]: RPCSubscribeChannelCreateArgs;
[RPCEvents.CurrentUserUpdate]: RPCSubscribeCurrentUserUpdateArgs;
[RPCEvents.EntitlementCreate]: RPCSubscribeEntitlementCreateArgs;
[RPCEvents.EntitlementDelete]: RPCSubscribeEntitlementDeleteArgs;
[RPCEvents.GameJoin]: RPCSubscribeGameJoinArgs;
[RPCEvents.GameSpectate]: RPCSubscribeGameSpectateArgs;
[RPCEvents.GuildCreate]: RPCSubscribeGuildCreateArgs;
[RPCEvents.GuildStatus]: RPCSubscribeGuildStatusArgs;
[RPCEvents.MessageCreate]: RPCSubscribeMessageCreateArgs;
[RPCEvents.MessageDelete]: RPCSubscribeMessageDeleteArgs;
[RPCEvents.MessageUpdate]: RPCSubscribeMessageUpdateArgs;
[RPCEvents.NotificationCreate]: RPCSubscribeNotificationCreateArgs;
[RPCEvents.Overlay]: RPCSubscribeOverlayArgs;
[RPCEvents.OverlayUpdate]: RPCSubscribeOverlayUpdateArgs;
[RPCEvents.RelationshipUpdate]: RPCSubscribeRelationshipUpdateArgs;
[RPCEvents.SpeakingStart]: RPCSubscribeSpeakingStartArgs;
[RPCEvents.SpeakingStop]: RPCSubscribeSpeakingStopArgs;
[RPCEvents.VoiceChannelSelect]: RPCSubscribeVoiceChannelSelectArgs;
[RPCEvents.VoiceConnectionStatus]: RPCSubscribeVoiceConnectionStatusArgs;
[RPCEvents.VoiceSettingsUpdate]: RPCSubscribeVoiceSettingsUpdateArgs;
[RPCEvents.VoiceSettingsUpdate2]: RPCSubscribeVoiceSettingsUpdate2Args;
[RPCEvents.VoiceStateCreate]: RPCSubscribeVoiceStateCreateArgs;
[RPCEvents.VoiceStateDelete]: RPCSubscribeVoiceStateDeleteArgs;
[RPCEvents.VoiceStateUpdate]: RPCSubscribeVoiceStateUpdateArgs;
}
export interface MappedRPCEventsDispatchData {
[RPCEvents.ActivityInvite]: [RPCActivityInviteDispatchData];
[RPCEvents.ActivityJoin]: [RPCActivityJoinDispatchData];
[RPCEvents.ActivityJoinRequest]: [RPCActivityJoinRequestDispatchData];
[RPCEvents.ActivitySpectate]: [RPCActivitySpectateDispatchData];
[RPCEvents.ChannelCreate]: [RPCChannelCreateDispatchData];
[RPCEvents.CurrentUserUpdate]: [RPCCurrentUserUpdateDispatchData];
[RPCEvents.EntitlementCreate]: [RPCEntitlementCreateDispatchData];
[RPCEvents.EntitlementDelete]: [RPCEntitlementDeleteDispatchData];
[RPCEvents.Error]: [RPCErrorDispatchData];
[RPCEvents.GameJoin]: [RPCGameJoinDispatchData];
[RPCEvents.GameSpectate]: [RPCGameSpectateDispatchData];
[RPCEvents.GuildCreate]: [RPCGuildCreateDispatchData];
[RPCEvents.GuildStatus]: [RPCGuildStatusDispatchData];
[RPCEvents.MessageCreate]: [RPCMessageCreateDispatchData];
[RPCEvents.MessageDelete]: [RPCMessageDeleteDispatchData];
[RPCEvents.MessageUpdate]: [RPCMessageUpdateDispatchData];
[RPCEvents.NotificationCreate]: [RPCNotificationCreateDispatchData];
[RPCEvents.Overlay]: [RPCOverlayDispatchData];
[RPCEvents.OverlayUpdate]: [RPCOverlayUpdateDispatchData];
[RPCEvents.Ready]: [RPCReadyDispatchData];
[RPCEvents.RelationshipUpdate]: [RPCRelationshipUpdateDispatchData];
[RPCEvents.SpeakingStart]: [RPCSpeakingStartDispatchData];
[RPCEvents.SpeakingStop]: [RPCSpeakingStopDispatchData];
[RPCEvents.VoiceChannelSelect]: [RPCVoiceChannelSelectDispatchData];
[RPCEvents.VoiceConnectionStatus]: [RPCVoiceConnectionStatusDispatchData];
[RPCEvents.VoiceSettingsUpdate]: [RPCVoiceSettingsUpdateDispatchData];
[RPCEvents.VoiceSettingsUpdate2]: [RPCVoiceSettingsUpdate2DispatchData];
[RPCEvents.VoiceStateCreate]: [RPCVoiceStateCreateDispatchData];
[RPCEvents.VoiceStateDelete]: [RPCVoiceStateDeleteDispatchData];
[RPCEvents.VoiceStateUpdate]: [RPCVoiceStateUpdateDispatchData];
}
export interface MappedEvents {
[Events.ApplicationReady]: [];
[Events.Disconnected]: [];
}
export type EventAndArgsParameters<Evt extends RPCEvents> = Evt extends RPCEvents.Error | RPCEvents.Ready
? never
: [Evt, MappedRPCSubscribeEventsArgs[Exclude<Evt, RPCEvents.Error | RPCEvents.Ready>], RequestOptions?];
+12
View File
@@ -0,0 +1,12 @@
export * from './client.js';
export * from './constants.js';
export * from './ipc.js';
export * from './RPCEventError.js';
export * from './util.js';
/**
* The {@link https://github.com/discordjs/discord.js/blob/main/packages/rpc#readme | @discordjs/rpc} version
* that you are currently using.
*/
// This needs to explicitly be `string` so it is not typed as a "const string" that gets injected by esbuild
export const version = '[VI]{{inject}}[/VI]' as string;
+224
View File
@@ -0,0 +1,224 @@
import { Buffer } from 'node:buffer';
import { randomUUID } from 'node:crypto';
import fs from 'node:fs/promises';
import { createConnection, Socket } from 'node:net';
import path from 'node:path';
import process from 'node:process';
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter';
import type { RPCMessagePayload } from 'discord-api-types/v10';
import type { RequestOptions, RPCClient } from './client.js';
enum OPCodes {
Handshake,
Frame,
Close,
Ping,
Pong,
}
interface HandshakePayload {
client_id: string;
v: number;
}
async function canOpenFile(path: string): Promise<boolean> {
try {
await fs.access(path);
return true;
} catch {
return false;
}
}
async function getIPCPath(id: number) {
if (process.platform === 'win32') {
return String.raw`\\?\pipe\discord-ipc-${id}`;
}
// Unix: check XDG_RUNTIME_DIR, TMPDIR, TMP, TEMP, then /tmp.
const prefix = process.env.XDG_RUNTIME_DIR ?? process.env.TMPDIR ?? '/tmp';
// Flatpak/Snap sandboxes nest the socket under app subdirs.
const bases = [prefix, path.join(prefix, 'app', 'com.discordapp.Discord'), path.join(prefix, 'snap.discord')];
for (const base of bases) {
const candidate = path.join(base, `discord-ipc-${id}`);
if (await canOpenFile(candidate)) return candidate;
}
return path.join(prefix, `discord-ipc-${id}`);
}
async function getIPC(id: number): Promise<Socket> {
const { promise, resolve, reject } = Promise.withResolvers<Socket>();
const path = await getIPCPath(id);
const onError = async () => reject(new Error('Could not connect'));
const socket = createConnection(path, () => {
socket.removeListener('error', onError);
resolve(socket);
});
socket.once('error', onError);
return promise;
}
export function encode(op: number, data: HandshakePayload | Record<string, never> | RPCMessagePayload | string) {
const stringifiedData = JSON.stringify(data);
const length = Buffer.byteLength(stringifiedData);
const packet = Buffer.allocUnsafe(8 + length);
packet.writeInt32LE(op, 0);
packet.writeInt32LE(length, 4);
packet.write(stringifiedData, 8, length);
return packet;
}
interface Working {
buffer: Buffer;
len: number;
op: number;
}
export class IPCTransport extends AsyncEventEmitter {
private socket: Socket;
private readonly working: Working;
public constructor(public readonly client: RPCClient) {
super();
this.socket = new Socket();
this.working = {
op: 0,
len: 0,
buffer: Buffer.from([]),
};
}
public async connect() {
for (let id = 0; id < 10; id++) {
try {
this.socket = await getIPC(id);
break;
} catch (error: unknown) {
if (id === 9) throw error;
}
}
this.socket.on('close', this.onClose.bind(this));
this.socket.on('error', this.onError.bind(this));
this.emit('open');
this.socket.write(
encode(OPCodes.Handshake, {
v: 1,
client_id: this.client.clientId,
}),
);
// paused for manual read management in this.decode()
this.socket.pause();
this.socket.on('readable', this.decode.bind(this));
}
private async decode() {
let packet: Buffer;
const chunks = [];
while ((packet = this.socket.read()) !== null) {
chunks.push(packet);
}
if (chunks.length === 0) return;
const packets = Buffer.concat([this.working.buffer, ...chunks]);
if (this.working.buffer.byteLength === 0) {
this.working.op = packets.readInt32LE(0);
this.working.len = packets.readInt32LE(4);
}
this.working.buffer = packets;
const { op, len } = this.working;
let data;
try {
data = JSON.parse(packets.subarray(8, len + 8).toString());
} catch {
return;
}
this.working.buffer = Buffer.from([]);
// Pong and Handshake is done from the client
switch (op) {
case OPCodes.Ping:
this.send(data, OPCodes.Pong);
break;
case OPCodes.Frame:
if (!data) {
return;
}
this.emit('message', data);
break;
case OPCodes.Close:
this.emit('close', data);
break;
default:
break;
}
}
public onClose(data: unknown) {
this.socket.removeAllListeners();
this.socket = new Socket();
this.working.buffer = Buffer.from([]);
const error = typeof data === 'boolean' ? new Error('socket closed') : data;
this.emit('close', error);
}
public onError(error: unknown) {
this.emit('error', error);
}
public send(data: string, op: OPCodes.Ping): void;
public send(data: RPCMessagePayload, op?: OPCodes.Frame | OPCodes.Pong): void;
public send(data: Record<string, never>, op: OPCodes.Close): void;
public send(data: Record<string, never> | RPCMessagePayload | string, op = OPCodes.Frame) {
this.socket.write(encode(op, data));
}
public async close(options?: RequestOptions) {
const { promise, resolve, reject } = Promise.withResolvers();
const signal = options?.signal ?? AbortSignal.timeout(10e3);
const onAbort = (_: Event) =>
reject(signal.reason instanceof Error ? signal.reason : new Error(String(signal.reason)));
this.once('close', (error) => {
signal.removeEventListener('abort', onAbort);
resolve(error);
});
signal.addEventListener('abort', onAbort);
this.send({}, OPCodes.Close);
this.socket.end();
return promise;
}
/**
* Manual ping method
*/
public ping() {
this.send(randomUUID(), OPCodes.Ping);
}
}
+29
View File
@@ -0,0 +1,29 @@
import { createRequire } from 'node:module';
export const register: () => Promise<boolean | unknown> = async () => {
const require = createRequire(import.meta.url);
try {
const { app } = require('electron');
return app.setAsDefaultProtocolClient.bind(app);
} catch {
try {
return require('register-scheme');
} catch {
return false;
}
}
};
export function getPid() {
// eslint-disable-next-line n/prefer-global/process
if (typeof globalThis.process !== 'undefined') {
// eslint-disable-next-line n/prefer-global/process
return process.pid;
}
return null;
}
export function isValidTimestamp(timestamp: number) {
return Number.isInteger(timestamp) && timestamp >= 0 && timestamp <= 2_147_483_647_000;
}
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.docs.json",
"compilerOptions": {
"outDir": "dist-docs"
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules"]
}
+22
View File
@@ -0,0 +1,22 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "./tsconfig.json",
"compilerOptions": {
"allowJs": true
},
"include": [
"*.ts",
"*.js",
"*.cjs",
"*.mjs",
"src/**/*.ts",
"src/**/*.js",
"src/**/*.cjs",
"src/**/*.mjs",
"bin",
"scripts",
"__tests__",
"__mocks__"
],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "../../tsconfig.json",
"include": ["src/**/*.ts", "src/**/*.js", "src/**/*.cjs", "src/**/*.mjs", "bin"],
"exclude": ["node_modules"]
}
+9
View File
@@ -0,0 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig.json",
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["__tests__/**/*.ts"],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
import { esbuildPluginVersionInjector } from 'esbuild-plugin-version-injector';
import { createTsupConfig } from '../../tsup.config.js';
export default createTsupConfig({
esbuildPlugins: [esbuildPluginVersionInjector()],
});
+1
View File
@@ -10,6 +10,7 @@ export const PACKAGES = [
'next',
'proxy',
'rest',
'rpc',
'structures',
'util',
'voice',
+232
View File
@@ -1529,6 +1529,55 @@ importers:
specifier: ^4.1.2
version: 4.1.2(@edge-runtime/vm@3.2.0)(@types/node@24.13.3)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
packages/rpc:
dependencies:
'@vladfrangu/async_event_emitter':
specifier: ^2.4.6
version: 2.4.7
discord-api-types:
specifier: ^0.38.50
version: 0.38.50
devDependencies:
'@discordjs/api-extractor':
specifier: workspace:^
version: link:../api-extractor
'@favware/cliff-jumper':
specifier: ^4.1.0
version: 4.1.0
'@types/node':
specifier: ^22.17.2
version: 22.19.15
cross-env:
specifier: ^10.0.0
version: 10.1.0
esbuild-plugin-version-injector:
specifier: ^1.2.1
version: 1.2.1
eslint:
specifier: ^9.33.0
version: 9.39.4(jiti@2.6.1)
eslint-config-neon:
specifier: ^0.2.7
version: 0.2.9(@typescript-eslint/types@8.57.2)(@typescript-eslint/utils@8.57.2(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)
eslint-formatter-compact:
specifier: ^8.40.0
version: 8.40.0
eslint-formatter-pretty:
specifier: ^6.0.1
version: 6.0.1
prettier:
specifier: ^3.6.2
version: 3.8.1
tsup:
specifier: ^8.5.0
version: 8.5.1(@microsoft/api-extractor@7.57.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)
turbo:
specifier: ^2.5.6
version: 2.8.21
typescript:
specifier: ~5.9.2
version: 5.9.3
packages/scripts:
dependencies:
'@actions/glob':
@@ -3590,6 +3639,11 @@ packages:
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
engines: {node: '>=14'}
'@favware/cliff-jumper@4.1.0':
resolution: {integrity: sha512-z8U6XecZr3F7nkq2d+dg+QXaHcBh+DugUuVQGwIEo2vpAPOFkZ0Xnz3YbnTu0zPlr9lDEWL/rbnGEr0lwCaGOA==}
engines: {node: '>=v18'}
hasBin: true
'@favware/cliff-jumper@6.0.0':
resolution: {integrity: sha512-9uXg/fGHFLh4AnG3HCtlrrrmDvUnmr5vrbs7H9pet3WlUCsGGGqeNT0bFb8LG0M0GatYUi9RM/F60p1yn2ndEA==}
engines: {node: '>=v18'}
@@ -5929,6 +5983,10 @@ packages:
resolution: {integrity: sha512-bFyGYHFT3TpOf5Sg2P+zY2ad0t5IA2epc5HtewlghhL7MYvbZvxtKsdaNaMwAdNObBx7hpiQm5OcOhyzEwQvbQ==}
engines: {node: '>=v16.0.0', npm: '>=8.0.0'}
'@sapphire/utilities@3.17.0':
resolution: {integrity: sha512-bbqjnKKrCgyT0C3+KOjIBzE6z6/v764EFuV7Uxw9WeIdkqGhwepl6tuNEzgiek/JdtEcODaPeX0K1aCt53yCcA==}
engines: {node: '>=v14.0.0'}
'@sapphire/utilities@3.18.1':
resolution: {integrity: sha512-zyEyQOQb2/t2mKRmu8T+M4r1Ulb+54BjwDS5pfzf6abGzTAcUg4VDWjHeKX7p3IgiZTcpN4Ij77b9k+K1KV4Lg==}
engines: {node: '>=v14.0.0'}
@@ -6730,6 +6788,9 @@ packages:
'@types/eslint@7.29.0':
resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==}
'@types/eslint@8.56.12':
resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==}
'@types/eslint@9.6.1':
resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==}
@@ -7488,6 +7549,10 @@ packages:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-escapes@6.2.1:
resolution: {integrity: sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==}
engines: {node: '>=14.16'}
ansi-escapes@7.3.0:
resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==}
engines: {node: '>=18'}
@@ -8900,6 +8965,10 @@ packages:
eslint: ^8.0.0
typescript: '>=4.0.0'
eslint-formatter-compact@8.40.0:
resolution: {integrity: sha512-cwGUs113TgmTQXecx5kfRjB7m0y2wkDLSadPTE2pK6M/wO4N8PjmUaoWOFNCP9MHgsiZwgqd5bZFnDCnszC56Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-formatter-compact@9.0.1:
resolution: {integrity: sha512-mBAti2tb403dQGMyilQTYHU80stem3N7jdtKW+tmn5gj3JNF7ki0rgCZtJFw4iMayTH862FTUIqCdp70ug0S0Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -8908,6 +8977,10 @@ packages:
resolution: {integrity: sha512-IsUTtGxF1hrH6lMWiSl1WbGaiP01eT6kzywdY1U+zLc0MP+nwEnUiS9UI8IaOTUhTeQJLlCEWIbXINBH4YJbBQ==}
engines: {node: '>=10'}
eslint-formatter-pretty@6.0.1:
resolution: {integrity: sha512-znAUcXmBthdIUmlnRkPSxz3zSJHFUhfHF/nJPcCMVKg/mOa4yUie2Olqg1Ghbi5JJRBZVU3rIgzWSObvIspxMA==}
engines: {node: '>=18'}
eslint-formatter-pretty@7.0.0:
resolution: {integrity: sha512-1CaE7Pnce8Csy+tlTEbFC2q5qgT5cJo2a0UkEOds+Y5+mI1nX3DApIhcBP8EPwV8TgTpLlzOfw8mcBJBAs3Y9Q==}
engines: {node: '>=18'}
@@ -10203,6 +10276,10 @@ packages:
resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==}
engines: {node: '>=10'}
is-unicode-supported@1.3.0:
resolution: {integrity: sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==}
engines: {node: '>=12'}
is-unicode-supported@2.1.0:
resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==}
engines: {node: '>=18'}
@@ -10617,6 +10694,10 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
log-symbols@6.0.0:
resolution: {integrity: sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==}
engines: {node: '>=18'}
log-symbols@7.0.1:
resolution: {integrity: sha512-ja1E3yCr9i/0hmBVaM0bfwDjnGy8I/s6PP4DFp+yP+a+mrHO4Rm7DtmnqROTUkHIkqffC84YY7AeqX6oFk0WFg==}
engines: {node: '>=18'}
@@ -12662,6 +12743,10 @@ packages:
resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
engines: {node: '>=8'}
supports-hyperlinks@3.2.0:
resolution: {integrity: sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==}
engines: {node: '>=14.18'}
supports-hyperlinks@4.4.0:
resolution: {integrity: sha512-UKbpT93hN5Nr9go5UY7bopIB9YQlMz9nm/ct4IXt/irb5YRkn9WaqrOBJGZ5Pwvsd5FQzSVeYlGdXoCAPQZrPg==}
engines: {node: '>=20'}
@@ -15523,6 +15608,23 @@ snapshots:
'@fastify/busboy@2.1.1': {}
'@favware/cliff-jumper@4.1.0':
dependencies:
'@favware/colorette-spinner': 1.0.1
'@octokit/auth-token': 5.1.2
'@octokit/core': 6.1.6
'@octokit/plugin-retry': 7.2.1(@octokit/core@6.1.6)
'@sapphire/result': 2.8.0
'@sapphire/utilities': 3.17.0
colorette: 2.0.20
commander: 12.1.0
conventional-recommended-bump: 10.0.0
execa: 9.6.1
git-cliff: 2.12.0
js-yaml: 4.1.1
semver: 7.7.4
smol-toml: 1.6.1
'@favware/cliff-jumper@6.0.0':
dependencies:
'@favware/colorette-spinner': 1.0.1
@@ -15994,6 +16096,15 @@ snapshots:
dependencies:
langium: 4.2.1
'@microsoft/api-extractor-model@7.33.4(@types/node@22.19.15)':
dependencies:
'@microsoft/tsdoc': 0.16.0
'@microsoft/tsdoc-config': 0.18.1
'@rushstack/node-core-library': 5.20.3(@types/node@22.19.15)
transitivePeerDependencies:
- '@types/node'
optional: true
'@microsoft/api-extractor-model@7.33.4(@types/node@24.13.3)':
dependencies:
'@microsoft/tsdoc': 0.16.0
@@ -16002,6 +16113,26 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
'@microsoft/api-extractor@7.57.7(@types/node@22.19.15)':
dependencies:
'@microsoft/api-extractor-model': 7.33.4(@types/node@22.19.15)
'@microsoft/tsdoc': 0.16.0
'@microsoft/tsdoc-config': 0.18.1
'@rushstack/node-core-library': 5.20.3(@types/node@22.19.15)
'@rushstack/rig-package': 0.7.2
'@rushstack/terminal': 0.22.3(@types/node@22.19.15)
'@rushstack/ts-command-line': 5.3.3(@types/node@22.19.15)
diff: 8.0.4
lodash: 4.17.23
minimatch: 10.2.3
resolve: 1.22.11
semver: 7.5.4
source-map: 0.6.1
typescript: 5.8.2
transitivePeerDependencies:
- '@types/node'
optional: true
'@microsoft/api-extractor@7.57.7(@types/node@24.13.3)':
dependencies:
'@microsoft/api-extractor-model': 7.33.4(@types/node@24.13.3)
@@ -18072,6 +18203,20 @@ snapshots:
'@rollup/rollup-win32-x64-msvc@4.60.0':
optional: true
'@rushstack/node-core-library@5.20.3(@types/node@22.19.15)':
dependencies:
ajv: 8.18.0
ajv-draft-04: 1.0.0(ajv@8.18.0)
ajv-formats: 3.0.1
fs-extra: 11.3.4
import-lazy: 4.0.0
jju: 1.4.0
resolve: 1.22.11
semver: 7.5.4
optionalDependencies:
'@types/node': 22.19.15
optional: true
'@rushstack/node-core-library@5.20.3(@types/node@24.13.3)':
dependencies:
ajv: 8.18.0
@@ -18098,6 +18243,11 @@ snapshots:
optionalDependencies:
'@types/node': 24.13.3
'@rushstack/problem-matcher@0.2.1(@types/node@22.19.15)':
optionalDependencies:
'@types/node': 22.19.15
optional: true
'@rushstack/problem-matcher@0.2.1(@types/node@24.13.3)':
optionalDependencies:
'@types/node': 24.13.3
@@ -18107,6 +18257,15 @@ snapshots:
resolve: 1.22.11
strip-json-comments: 3.1.1
'@rushstack/terminal@0.22.3(@types/node@22.19.15)':
dependencies:
'@rushstack/node-core-library': 5.20.3(@types/node@22.19.15)
'@rushstack/problem-matcher': 0.2.1(@types/node@22.19.15)
supports-color: 8.1.1
optionalDependencies:
'@types/node': 22.19.15
optional: true
'@rushstack/terminal@0.22.3(@types/node@24.13.3)':
dependencies:
'@rushstack/node-core-library': 5.20.3(@types/node@24.13.3)
@@ -18123,6 +18282,16 @@ snapshots:
optionalDependencies:
'@types/node': 24.13.3
'@rushstack/ts-command-line@5.3.3(@types/node@22.19.15)':
dependencies:
'@rushstack/terminal': 0.22.3(@types/node@22.19.15)
'@types/argparse': 1.0.38
argparse: 1.0.10
string-argv: 0.3.2
transitivePeerDependencies:
- '@types/node'
optional: true
'@rushstack/ts-command-line@5.3.3(@types/node@24.13.3)':
dependencies:
'@rushstack/terminal': 0.22.3(@types/node@24.13.3)
@@ -18161,6 +18330,8 @@ snapshots:
tslib: 2.8.1
typescript: 5.4.5
'@sapphire/utilities@3.17.0': {}
'@sapphire/utilities@3.18.1': {}
'@sapphire/utilities@3.18.2': {}
@@ -19097,6 +19268,11 @@ snapshots:
'@types/estree': 1.0.8
'@types/json-schema': 7.0.15
'@types/eslint@8.56.12':
dependencies:
'@types/estree': 1.0.8
'@types/json-schema': 7.0.15
'@types/eslint@9.6.1':
dependencies:
'@types/estree': 1.0.8
@@ -20169,6 +20345,8 @@ snapshots:
dependencies:
type-fest: 0.21.3
ansi-escapes@6.2.1: {}
ansi-escapes@7.3.0:
dependencies:
environment: 1.1.0
@@ -21873,6 +22051,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-formatter-compact@8.40.0: {}
eslint-formatter-compact@9.0.1: {}
eslint-formatter-pretty@4.1.0:
@@ -21886,6 +22066,17 @@ snapshots:
string-width: 4.2.3
supports-hyperlinks: 2.3.0
eslint-formatter-pretty@6.0.1:
dependencies:
'@types/eslint': 8.56.12
ansi-escapes: 6.2.1
chalk: 5.6.2
eslint-rule-docs: 1.1.235
log-symbols: 6.0.0
plur: 5.1.0
string-width: 7.2.0
supports-hyperlinks: 3.2.0
eslint-formatter-pretty@7.0.0:
dependencies:
'@types/eslint': 9.6.1
@@ -23523,6 +23714,8 @@ snapshots:
is-unicode-supported@0.1.0: {}
is-unicode-supported@1.3.0: {}
is-unicode-supported@2.1.0: {}
is-weakmap@2.0.2: {}
@@ -23902,6 +24095,11 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
log-symbols@6.0.0:
dependencies:
chalk: 5.6.2
is-unicode-supported: 1.3.0
log-symbols@7.0.1:
dependencies:
is-unicode-supported: 2.1.0
@@ -26517,6 +26715,11 @@ snapshots:
has-flag: 4.0.0
supports-color: 7.2.0
supports-hyperlinks@3.2.0:
dependencies:
has-flag: 4.0.0
supports-color: 7.2.0
supports-hyperlinks@4.4.0:
dependencies:
has-flag: 5.0.1
@@ -26705,6 +26908,35 @@ snapshots:
tslib@2.8.1: {}
tsup@8.5.1(@microsoft/api-extractor@7.57.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.4)
cac: 6.7.14
chokidar: 4.0.3
consola: 3.4.2
debug: 4.4.3
esbuild: 0.27.4
fix-dts-default-cjs-exports: 1.0.1
joycon: 3.1.1
picocolors: 1.1.1
postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3)
resolve-from: 5.0.0
rollup: 4.60.0
source-map: 0.7.6
sucrase: 3.35.1
tinyexec: 0.3.2
tinyglobby: 0.2.15
tree-kill: 1.2.2
optionalDependencies:
'@microsoft/api-extractor': 7.57.7(@types/node@22.19.15)
postcss: 8.5.8
typescript: 5.9.3
transitivePeerDependencies:
- jiti
- supports-color
- tsx
- yaml
tsup@8.5.1(@microsoft/api-extractor@7.57.7(@types/node@24.13.3))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3):
dependencies:
bundle-require: 5.1.0(esbuild@0.27.4)