@addozhang/dsh-discord 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/cordis.patch.yml +14 -0
- package/lib/client/DiscordSettingsCard.d.ts +6 -0
- package/lib/client/PluginCard.d.ts +43 -0
- package/lib/client/card-controller.d.ts +22 -0
- package/lib/client/card-form.d.ts +111 -0
- package/lib/client/fields.d.ts +50 -0
- package/lib/client/index.d.ts +12 -0
- package/lib/client/nav-icon.d.ts +21 -0
- package/lib/client/settings-model.d.ts +49 -0
- package/lib/client/slot-contract.d.ts +57 -0
- package/lib/client/snapshot-store.d.ts +9 -0
- package/lib/client.js +1622 -0
- package/lib/compose.d.ts +158 -0
- package/lib/compose.js +227 -0
- package/lib/credential.d.ts +20 -0
- package/lib/credential.js +24 -0
- package/lib/discord/commands.d.ts +45 -0
- package/lib/discord/commands.js +87 -0
- package/lib/discord/components.d.ts +29 -0
- package/lib/discord/components.js +57 -0
- package/lib/discord/delivery.d.ts +21 -0
- package/lib/discord/delivery.js +31 -0
- package/lib/discord/interaction-lifecycle.d.ts +42 -0
- package/lib/discord/interaction-lifecycle.js +78 -0
- package/lib/discord/rest.d.ts +74 -0
- package/lib/discord/rest.js +149 -0
- package/lib/discord/selector.d.ts +39 -0
- package/lib/discord/selector.js +70 -0
- package/lib/discord/thread-port.d.ts +37 -0
- package/lib/discord/thread-port.js +58 -0
- package/lib/dsh/api-proxy-face.d.ts +253 -0
- package/lib/dsh/api-proxy-face.js +370 -0
- package/lib/features/adapter-status.d.ts +111 -0
- package/lib/features/adapter-status.js +103 -0
- package/lib/features/approval-expiry.d.ts +27 -0
- package/lib/features/approval-expiry.js +36 -0
- package/lib/features/approval-routing.d.ts +50 -0
- package/lib/features/approval-routing.js +57 -0
- package/lib/features/approval-store.d.ts +86 -0
- package/lib/features/approval-store.js +119 -0
- package/lib/features/approval-view.d.ts +47 -0
- package/lib/features/approval-view.js +48 -0
- package/lib/features/ask-wiring.d.ts +74 -0
- package/lib/features/ask-wiring.js +151 -0
- package/lib/features/host-status.d.ts +41 -0
- package/lib/features/host-status.js +24 -0
- package/lib/features/image-collection.d.ts +56 -0
- package/lib/features/image-collection.js +79 -0
- package/lib/features/image-download.d.ts +48 -0
- package/lib/features/image-download.js +77 -0
- package/lib/features/image-submission.d.ts +67 -0
- package/lib/features/image-submission.js +57 -0
- package/lib/features/interaction-router.d.ts +84 -0
- package/lib/features/interaction-router.js +655 -0
- package/lib/features/model-control.d.ts +79 -0
- package/lib/features/model-control.js +51 -0
- package/lib/features/preset-control.d.ts +84 -0
- package/lib/features/preset-control.js +43 -0
- package/lib/features/project-bind.d.ts +69 -0
- package/lib/features/project-bind.js +57 -0
- package/lib/features/project-info.d.ts +25 -0
- package/lib/features/project-info.js +22 -0
- package/lib/features/project-list.d.ts +79 -0
- package/lib/features/project-list.js +93 -0
- package/lib/features/prompt-submission.d.ts +53 -0
- package/lib/features/prompt-submission.js +47 -0
- package/lib/features/question-expiry.d.ts +53 -0
- package/lib/features/question-expiry.js +56 -0
- package/lib/features/question-routing.d.ts +108 -0
- package/lib/features/question-routing.js +162 -0
- package/lib/features/question-store.d.ts +124 -0
- package/lib/features/question-store.js +186 -0
- package/lib/features/question-view.d.ts +58 -0
- package/lib/features/question-view.js +81 -0
- package/lib/features/queue-control.d.ts +63 -0
- package/lib/features/queue-control.js +31 -0
- package/lib/features/reconcile-bindings.d.ts +75 -0
- package/lib/features/reconcile-bindings.js +54 -0
- package/lib/features/reconcile-deliveries.d.ts +29 -0
- package/lib/features/reconcile-deliveries.js +28 -0
- package/lib/features/reconcile-events.d.ts +49 -0
- package/lib/features/reconcile-events.js +50 -0
- package/lib/features/reconcile-intents.d.ts +32 -0
- package/lib/features/reconcile-intents.js +32 -0
- package/lib/features/reconcile-interactions.d.ts +59 -0
- package/lib/features/reconcile-interactions.js +54 -0
- package/lib/features/session-adopt.d.ts +89 -0
- package/lib/features/session-adopt.js +77 -0
- package/lib/features/session-creation.d.ts +49 -0
- package/lib/features/session-creation.js +43 -0
- package/lib/features/session-mainline.d.ts +75 -0
- package/lib/features/session-mainline.js +96 -0
- package/lib/features/session-resume.d.ts +44 -0
- package/lib/features/session-resume.js +40 -0
- package/lib/features/skill-run.d.ts +48 -0
- package/lib/features/skill-run.js +25 -0
- package/lib/features/steer-control.d.ts +37 -0
- package/lib/features/steer-control.js +28 -0
- package/lib/features/stop-control.d.ts +38 -0
- package/lib/features/stop-control.js +31 -0
- package/lib/features/task-admission.d.ts +29 -0
- package/lib/features/task-admission.js +21 -0
- package/lib/features/thread-continuation.d.ts +36 -0
- package/lib/features/thread-continuation.js +18 -0
- package/lib/features/thread-creation.d.ts +81 -0
- package/lib/features/thread-creation.js +81 -0
- package/lib/features/thread-routing.d.ts +49 -0
- package/lib/features/thread-routing.js +34 -0
- package/lib/features/turn-ownership.d.ts +39 -0
- package/lib/features/turn-ownership.js +40 -0
- package/lib/features/unbound-mention.d.ts +49 -0
- package/lib/features/unbound-mention.js +31 -0
- package/lib/features/workspace-channel.d.ts +41 -0
- package/lib/features/workspace-channel.js +47 -0
- package/lib/gateway/gateway.d.ts +50 -0
- package/lib/gateway/gateway.js +229 -0
- package/lib/gateway/inbound.d.ts +85 -0
- package/lib/gateway/inbound.js +186 -0
- package/lib/gateway/ingress.d.ts +15 -0
- package/lib/gateway/ingress.js +22 -0
- package/lib/i18n.d.ts +92 -0
- package/lib/i18n.js +180 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +869 -0
- package/lib/lifecycle.d.ts +44 -0
- package/lib/lifecycle.js +84 -0
- package/lib/policy/authorization.d.ts +41 -0
- package/lib/policy/authorization.js +64 -0
- package/lib/policy/disclosure.d.ts +93 -0
- package/lib/policy/disclosure.js +153 -0
- package/lib/policy/guard.d.ts +31 -0
- package/lib/policy/guard.js +46 -0
- package/lib/policy/suppress.d.ts +13 -0
- package/lib/policy/suppress.js +20 -0
- package/lib/settings-namespace.d.ts +7 -0
- package/lib/settings-namespace.js +6 -0
- package/lib/settings.d.ts +37 -0
- package/lib/settings.js +102 -0
- package/lib/startup.d.ts +16 -0
- package/lib/startup.js +60 -0
- package/lib/state/bindings.d.ts +46 -0
- package/lib/state/bindings.js +55 -0
- package/lib/state/channel-bindings.d.ts +37 -0
- package/lib/state/channel-bindings.js +43 -0
- package/lib/state/domain.d.ts +82 -0
- package/lib/state/domain.js +86 -0
- package/lib/state/effect-machine.d.ts +37 -0
- package/lib/state/effect-machine.js +52 -0
- package/lib/state/fail-closed.d.ts +44 -0
- package/lib/state/fail-closed.js +57 -0
- package/lib/state/intents.d.ts +51 -0
- package/lib/state/intents.js +73 -0
- package/lib/state/records.d.ts +37 -0
- package/lib/state/records.js +30 -0
- package/lib/state/retention.d.ts +74 -0
- package/lib/state/retention.js +68 -0
- package/lib/state/session-owners.d.ts +50 -0
- package/lib/state/session-owners.js +50 -0
- package/lib/stream/finalizer.d.ts +49 -0
- package/lib/stream/finalizer.js +67 -0
- package/lib/stream/icons.d.ts +15 -0
- package/lib/stream/icons.js +36 -0
- package/lib/stream/live.d.ts +140 -0
- package/lib/stream/live.js +515 -0
- package/lib/stream/markdown.d.ts +24 -0
- package/lib/stream/markdown.js +95 -0
- package/lib/stream/outbound.d.ts +27 -0
- package/lib/stream/outbound.js +25 -0
- package/lib/stream/render-fence.d.ts +27 -0
- package/lib/stream/render-fence.js +37 -0
- package/lib/stream/render-model.d.ts +48 -0
- package/lib/stream/render-model.js +68 -0
- package/lib/stream/splitter.d.ts +8 -0
- package/lib/stream/splitter.js +58 -0
- package/lib/stream/tool-view.d.ts +40 -0
- package/lib/stream/tool-view.js +39 -0
- package/lib/stream/typing.d.ts +22 -0
- package/lib/stream/typing.js +58 -0
- package/lib/stream/update-scheduler.d.ts +20 -0
- package/lib/stream/update-scheduler.js +65 -0
- package/package.json +86 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter's single cancellation root. Every timer, listener, and
|
|
3
|
+
* abortable resource the plugin opens registers here, so Cordis-owned
|
|
4
|
+
* disposal tears the whole runtime down in one effect and no callback can
|
|
5
|
+
* ever run after the plugin unloads.
|
|
6
|
+
*/
|
|
7
|
+
export type Disposer = () => void;
|
|
8
|
+
/** Minimal event target face the root's listener helper needs. */
|
|
9
|
+
export interface ListenerTarget {
|
|
10
|
+
addEventListener(name: string, handler: () => void): unknown;
|
|
11
|
+
removeEventListener(name: string, handler: () => void): unknown;
|
|
12
|
+
}
|
|
13
|
+
export declare class CancellationRoot {
|
|
14
|
+
private disposedState;
|
|
15
|
+
private readonly pendingTimers;
|
|
16
|
+
private readonly disposables;
|
|
17
|
+
private readonly controller;
|
|
18
|
+
/** The abort signal async work (streams, fetches) must observe. */
|
|
19
|
+
get signal(): AbortSignal;
|
|
20
|
+
/** Whether this root has been torn down; disposed roots refuse new work. */
|
|
21
|
+
get disposed(): boolean;
|
|
22
|
+
/** Run `callback` every `ms` until disposal; the timer never fires after. */
|
|
23
|
+
setInterval(callback: () => void, ms: number): void;
|
|
24
|
+
/** Run `callback` once after `ms` unless disposal happens first. */
|
|
25
|
+
setTimeout(callback: () => void, ms: number): void;
|
|
26
|
+
/** Observe `name` on `target` until disposal removes the listener. */
|
|
27
|
+
listen(target: ListenerTarget, name: string, handler: () => void): void;
|
|
28
|
+
/** Register one disposer running at disposal (reverse registration order). */
|
|
29
|
+
register(disposer: Disposer): void;
|
|
30
|
+
/**
|
|
31
|
+
* Tear everything down: clear timers, run disposers in reverse order,
|
|
32
|
+
* abort the signal, and refuse all future work. Idempotent.
|
|
33
|
+
*/
|
|
34
|
+
dispose(): void;
|
|
35
|
+
private assertActive;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Install the plugin's cancellation root on its own Cordis fiber: the
|
|
39
|
+
* returned root lives exactly as long as the plugin and is disposed through
|
|
40
|
+
* the fiber's effect teardown.
|
|
41
|
+
*/
|
|
42
|
+
export declare function installCancellationRoot(ctx: {
|
|
43
|
+
effect(execute: () => () => void, label?: string): unknown;
|
|
44
|
+
}): CancellationRoot;
|
package/lib/lifecycle.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter's single cancellation root. Every timer, listener, and
|
|
3
|
+
* abortable resource the plugin opens registers here, so Cordis-owned
|
|
4
|
+
* disposal tears the whole runtime down in one effect and no callback can
|
|
5
|
+
* ever run after the plugin unloads.
|
|
6
|
+
*/
|
|
7
|
+
export class CancellationRoot {
|
|
8
|
+
disposedState = false;
|
|
9
|
+
pendingTimers = new Set();
|
|
10
|
+
disposables = [];
|
|
11
|
+
controller = new AbortController();
|
|
12
|
+
/** The abort signal async work (streams, fetches) must observe. */
|
|
13
|
+
get signal() {
|
|
14
|
+
return this.controller.signal;
|
|
15
|
+
}
|
|
16
|
+
/** Whether this root has been torn down; disposed roots refuse new work. */
|
|
17
|
+
get disposed() {
|
|
18
|
+
return this.disposedState;
|
|
19
|
+
}
|
|
20
|
+
/** Run `callback` every `ms` until disposal; the timer never fires after. */
|
|
21
|
+
setInterval(callback, ms) {
|
|
22
|
+
this.assertActive();
|
|
23
|
+
const timer = setInterval(() => { callback(); }, ms);
|
|
24
|
+
this.pendingTimers.add(timer);
|
|
25
|
+
}
|
|
26
|
+
/** Run `callback` once after `ms` unless disposal happens first. */
|
|
27
|
+
setTimeout(callback, ms) {
|
|
28
|
+
this.assertActive();
|
|
29
|
+
const timer = setTimeout(() => {
|
|
30
|
+
this.pendingTimers.delete(timer);
|
|
31
|
+
callback();
|
|
32
|
+
}, ms);
|
|
33
|
+
this.pendingTimers.add(timer);
|
|
34
|
+
}
|
|
35
|
+
/** Observe `name` on `target` until disposal removes the listener. */
|
|
36
|
+
listen(target, name, handler) {
|
|
37
|
+
this.assertActive();
|
|
38
|
+
target.addEventListener(name, handler);
|
|
39
|
+
this.disposables.push(() => { target.removeEventListener(name, handler); });
|
|
40
|
+
}
|
|
41
|
+
/** Register one disposer running at disposal (reverse registration order). */
|
|
42
|
+
register(disposer) {
|
|
43
|
+
this.assertActive();
|
|
44
|
+
this.disposables.push(disposer);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Tear everything down: clear timers, run disposers in reverse order,
|
|
48
|
+
* abort the signal, and refuse all future work. Idempotent.
|
|
49
|
+
*/
|
|
50
|
+
dispose() {
|
|
51
|
+
if (this.disposedState)
|
|
52
|
+
return;
|
|
53
|
+
this.disposedState = true;
|
|
54
|
+
for (const timer of this.pendingTimers)
|
|
55
|
+
clearTimeout(timer);
|
|
56
|
+
this.pendingTimers.clear();
|
|
57
|
+
for (const disposer of [...this.disposables].reverse()) {
|
|
58
|
+
try {
|
|
59
|
+
disposer();
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// A failing disposer must not stop the remaining teardown; the root
|
|
63
|
+
// is already marked disposed, so nothing can re-register.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
this.disposables.length = 0;
|
|
67
|
+
this.controller.abort();
|
|
68
|
+
}
|
|
69
|
+
assertActive() {
|
|
70
|
+
if (this.disposedState) {
|
|
71
|
+
throw new TypeError('dsh-discord cancellation root is disposed; refusing new work');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Install the plugin's cancellation root on its own Cordis fiber: the
|
|
77
|
+
* returned root lives exactly as long as the plugin and is disposed through
|
|
78
|
+
* the fiber's effect teardown.
|
|
79
|
+
*/
|
|
80
|
+
export function installCancellationRoot(ctx) {
|
|
81
|
+
const root = new CancellationRoot();
|
|
82
|
+
ctx.effect(() => () => { root.dispose(); }, 'dsh-discord cancellation root');
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure authorization evaluator. Two nested boundaries:
|
|
3
|
+
*
|
|
4
|
+
* 1. The Guild allowlist is the outer tenant boundary — no ownership,
|
|
5
|
+
* permission, or role ever bypasses it.
|
|
6
|
+
* 2. Inside an allowed Guild, deny-user/deny-role rules beat every allow
|
|
7
|
+
* rule: member grants, configured administrator grants, Discord-native
|
|
8
|
+
* administrator authority (owner / Administrator / Manage Guild), and the
|
|
9
|
+
* global Host-operator list alike. Bots other than the adapter itself are
|
|
10
|
+
* refused outright.
|
|
11
|
+
*
|
|
12
|
+
* The decision ranks so callers can require a minimum level: a
|
|
13
|
+
* host-operator satisfies member and workspace-administrator requirements.
|
|
14
|
+
*/
|
|
15
|
+
import type { DiscordSettings } from '../settings.js';
|
|
16
|
+
/** The policy slice the evaluator reads (the adapter's settings document). */
|
|
17
|
+
export type PolicyTable = Pick<DiscordSettings, 'allowedGuildIds' | 'memberUserIds' | 'memberRoleIds' | 'administratorUserIds' | 'administratorRoleIds' | 'deniedUserIds' | 'deniedRoleIds' | 'hostOperatorUserIds'>;
|
|
18
|
+
/** The normalized actor facts the evaluator consumes (precomputed upstream). */
|
|
19
|
+
export interface AuthorizationInput {
|
|
20
|
+
guildId: string;
|
|
21
|
+
userId: string;
|
|
22
|
+
/** Role ids the wire attached to the member (callers include @everyone if desired). */
|
|
23
|
+
roleIds: readonly string[];
|
|
24
|
+
/** Discord permission bitmask as the wire carries it (string or number). */
|
|
25
|
+
memberPermissions?: string | number | undefined;
|
|
26
|
+
isGuildOwner?: boolean | undefined;
|
|
27
|
+
/** Any bot besides the adapter is unauthorized; the adapter self-filters earlier. */
|
|
28
|
+
isBot?: boolean | undefined;
|
|
29
|
+
}
|
|
30
|
+
/** Access levels in ascending authority order. */
|
|
31
|
+
export type AccessLevel = 'member' | 'workspace-administrator' | 'host-operator';
|
|
32
|
+
export type AccessDecision = {
|
|
33
|
+
allowed: true;
|
|
34
|
+
level: AccessLevel;
|
|
35
|
+
} | {
|
|
36
|
+
allowed: false;
|
|
37
|
+
reason: 'guild-not-allowed' | 'bot-not-authorized' | 'denied' | 'no-grant';
|
|
38
|
+
};
|
|
39
|
+
/** Whether an allowed decision satisfies a required minimum level. */
|
|
40
|
+
export declare function levelAtLeast(level: AccessLevel, required: AccessLevel): boolean;
|
|
41
|
+
export declare function evaluateAuthorization(policy: PolicyTable, actor: AuthorizationInput): AccessDecision;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pure authorization evaluator. Two nested boundaries:
|
|
3
|
+
*
|
|
4
|
+
* 1. The Guild allowlist is the outer tenant boundary — no ownership,
|
|
5
|
+
* permission, or role ever bypasses it.
|
|
6
|
+
* 2. Inside an allowed Guild, deny-user/deny-role rules beat every allow
|
|
7
|
+
* rule: member grants, configured administrator grants, Discord-native
|
|
8
|
+
* administrator authority (owner / Administrator / Manage Guild), and the
|
|
9
|
+
* global Host-operator list alike. Bots other than the adapter itself are
|
|
10
|
+
* refused outright.
|
|
11
|
+
*
|
|
12
|
+
* The decision ranks so callers can require a minimum level: a
|
|
13
|
+
* host-operator satisfies member and workspace-administrator requirements.
|
|
14
|
+
*/
|
|
15
|
+
const DISCORD_ADMINISTRATOR = 1n << 3n;
|
|
16
|
+
const DISCORD_MANAGE_GUILD = 1n << 5n;
|
|
17
|
+
function hasPermission(permissions, flag) {
|
|
18
|
+
if (permissions === undefined)
|
|
19
|
+
return false;
|
|
20
|
+
try {
|
|
21
|
+
const mask = typeof permissions === 'string' ? BigInt(permissions) : BigInt(permissions);
|
|
22
|
+
return (mask & flag) === flag;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Whether an allowed decision satisfies a required minimum level. */
|
|
29
|
+
export function levelAtLeast(level, required) {
|
|
30
|
+
const order = ['member', 'workspace-administrator', 'host-operator'];
|
|
31
|
+
return order.indexOf(level) >= order.indexOf(required);
|
|
32
|
+
}
|
|
33
|
+
export function evaluateAuthorization(policy, actor) {
|
|
34
|
+
// 1. Outer boundary: the explicit Guild allowlist. Nothing bypasses it.
|
|
35
|
+
if (!policy.allowedGuildIds.includes(actor.guildId)) {
|
|
36
|
+
return { allowed: false, reason: 'guild-not-allowed' };
|
|
37
|
+
}
|
|
38
|
+
// 2. Bots other than the adapter are never authorized.
|
|
39
|
+
if (actor.isBot === true) {
|
|
40
|
+
return { allowed: false, reason: 'bot-not-authorized' };
|
|
41
|
+
}
|
|
42
|
+
// 3. Deny rules take precedence over every allow rule at every level.
|
|
43
|
+
if (policy.deniedUserIds.includes(actor.userId) || actor.roleIds.some(role => policy.deniedRoleIds.includes(role))) {
|
|
44
|
+
return { allowed: false, reason: 'denied' };
|
|
45
|
+
}
|
|
46
|
+
// 4. Host-operator: the global allowlist.
|
|
47
|
+
if (policy.hostOperatorUserIds.includes(actor.userId)) {
|
|
48
|
+
return { allowed: true, level: 'host-operator' };
|
|
49
|
+
}
|
|
50
|
+
// 5. Workspace administrators: configured ids plus Discord-native authority.
|
|
51
|
+
const configuredAdmin = policy.administratorUserIds.includes(actor.userId)
|
|
52
|
+
|| actor.roleIds.some(role => policy.administratorRoleIds.includes(role));
|
|
53
|
+
const nativeAdmin = actor.isGuildOwner === true
|
|
54
|
+
|| hasPermission(actor.memberPermissions, DISCORD_ADMINISTRATOR)
|
|
55
|
+
|| hasPermission(actor.memberPermissions, DISCORD_MANAGE_GUILD);
|
|
56
|
+
if (configuredAdmin || nativeAdmin) {
|
|
57
|
+
return { allowed: true, level: 'workspace-administrator' };
|
|
58
|
+
}
|
|
59
|
+
// 6. Ordinary members: explicit user or role grants only.
|
|
60
|
+
if (policy.memberUserIds.includes(actor.userId) || actor.roleIds.some(role => policy.memberRoleIds.includes(role))) {
|
|
61
|
+
return { allowed: true, level: 'member' };
|
|
62
|
+
}
|
|
63
|
+
return { allowed: false, reason: 'no-grant' };
|
|
64
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Least-disclosure labels and output policy (amended design §3, 16.1).
|
|
3
|
+
* Workspace lists show titles disambiguated by short opaque id suffixes.
|
|
4
|
+
* Paths are not treated as sensitive for this self-hosted, trusted-Guild
|
|
5
|
+
* product: the canonical path renders in the ephemeral detail response and
|
|
6
|
+
* an abbreviated form may appear in autocomplete labels — but a path never
|
|
7
|
+
* reaches channel metadata or public (non-ephemeral) messages. User-controlled
|
|
8
|
+
* text is sanitized before it can forge Discord mentions or overflow
|
|
9
|
+
* component limits, and every outbound message carries
|
|
10
|
+
* `allowed_mentions: { parse: [] }` plus byte-wise syntax neutralization so
|
|
11
|
+
* assistant, tool, title, and error content can never ping anyone.
|
|
12
|
+
*/
|
|
13
|
+
/** A workspace as the adapter's listing surfaces see it. */
|
|
14
|
+
export interface WorkspaceEntry {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
/** Canonical path; rendered in ephemeral details, abbreviated in autocomplete labels. */
|
|
18
|
+
path?: string | undefined;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* path abbreviation (16.1): the user's home directory collapses
|
|
22
|
+
* to a `~` prefix — `/Users/addo/Workspaces/x` → `~/Workspaces/x` — so
|
|
23
|
+
* autocomplete labels stay short without hiding the directory structure.
|
|
24
|
+
* The home path must end at a segment boundary: `/Users/addoish/x` is NOT under `/Users/addo`. Display-only;
|
|
25
|
+
* never persisted to Discord metadata.
|
|
26
|
+
*/
|
|
27
|
+
export declare function abbreviatePath(fullPath: string, home?: string): string;
|
|
28
|
+
/** One rendered row of a Workspace list. */
|
|
29
|
+
export interface WorkspaceLabel {
|
|
30
|
+
id: string;
|
|
31
|
+
title: string;
|
|
32
|
+
/** Discord-renderable label carrying no path. */
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Discord's SUPPRESS_NOTIFICATIONS message flag (1 << 12): the message will
|
|
37
|
+
* not trigger push/desktop notifications. This flag says nothing about
|
|
38
|
+
* mentions — ping prevention is the `allowed_mentions` request field below,
|
|
39
|
+
* and the syntax neutralization in policy/suppress.ts is the third layer.
|
|
40
|
+
*/
|
|
41
|
+
export declare const DISCORD_SUPPRESS_NOTIFICATIONS_FLAG: number;
|
|
42
|
+
/**
|
|
43
|
+
* Discord's request field that disables every mention parse category: with
|
|
44
|
+
* `parse: []` no `@everyone`, user, role, or channel mention in the content
|
|
45
|
+
* pings, regardless of how the content renders. Every message create/edit
|
|
46
|
+
* the adapter sends carries this.
|
|
47
|
+
*/
|
|
48
|
+
export declare const ALLOWED_MENTIONS_NONE: {
|
|
49
|
+
parse: string[];
|
|
50
|
+
};
|
|
51
|
+
/** Discord's ephemeral message flag (only the invoker sees the message). */
|
|
52
|
+
export declare const DISCORD_EPHEMERAL_FLAG: number;
|
|
53
|
+
/** Ephemeral + silent: the standard adapter followup flags. */
|
|
54
|
+
export declare const OUTBOUND_EPHEMERAL_FLAGS: number;
|
|
55
|
+
/**
|
|
56
|
+
* The comparison key for Discord channel/thread names: Discord slugs names
|
|
57
|
+
* (lowercase, whitespace runs collapse to `-`), so a rename-dedupe check
|
|
58
|
+
* must compare both sides slugified or every restart burns a rename.
|
|
59
|
+
*/
|
|
60
|
+
export declare function discordChannelNameKey(text: string): string;
|
|
61
|
+
/**
|
|
62
|
+
* Sanitize user-controlled text for safe rendering as a label or title:
|
|
63
|
+
* control characters and line breaks collapse, mention syntax breaks, and
|
|
64
|
+
* Discord's component length ceiling applies.
|
|
65
|
+
*/
|
|
66
|
+
export declare function safeTitle(text: string, maxLength?: number): string;
|
|
67
|
+
/**
|
|
68
|
+
* Label a Workspace list: unique titles stay bare; duplicate titles gain a
|
|
69
|
+
* short id suffix so every label stays unambiguous without exposing paths.
|
|
70
|
+
* Escalating suffix groups grow together (4 → 8 → … chars, then the full id)
|
|
71
|
+
* and stay deterministic.
|
|
72
|
+
*/
|
|
73
|
+
export declare function workspaceLabels(entries: readonly WorkspaceEntry[]): WorkspaceLabel[];
|
|
74
|
+
/** The opaque value a select option encodes for choosing this workspace. */
|
|
75
|
+
export declare function workspaceReference(id: string): string;
|
|
76
|
+
/** The inverse of workspaceReference; `undefined` for any other string. */
|
|
77
|
+
export declare function parseWorkspaceReference(reference: string): string | undefined;
|
|
78
|
+
export interface WorkspaceDetail {
|
|
79
|
+
id: string;
|
|
80
|
+
title: string;
|
|
81
|
+
label: string;
|
|
82
|
+
/** Canonical path; rendered when the caller is an authorized member. */
|
|
83
|
+
path?: string | undefined;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Project one workspace for display. `includePath` is for the ephemeral
|
|
87
|
+
* detail response (any authorized member per amended design §3); list
|
|
88
|
+
* surfaces always pass false, so a path never reaches channel metadata or
|
|
89
|
+
* public messages.
|
|
90
|
+
*/
|
|
91
|
+
export declare function describeWorkspace(entry: WorkspaceEntry, options: {
|
|
92
|
+
includePath: boolean;
|
|
93
|
+
}): WorkspaceDetail;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Least-disclosure labels and output policy (amended design §3, 16.1).
|
|
3
|
+
* Workspace lists show titles disambiguated by short opaque id suffixes.
|
|
4
|
+
* Paths are not treated as sensitive for this self-hosted, trusted-Guild
|
|
5
|
+
* product: the canonical path renders in the ephemeral detail response and
|
|
6
|
+
* an abbreviated form may appear in autocomplete labels — but a path never
|
|
7
|
+
* reaches channel metadata or public (non-ephemeral) messages. User-controlled
|
|
8
|
+
* text is sanitized before it can forge Discord mentions or overflow
|
|
9
|
+
* component limits, and every outbound message carries
|
|
10
|
+
* `allowed_mentions: { parse: [] }` plus byte-wise syntax neutralization so
|
|
11
|
+
* assistant, tool, title, and error content can never ping anyone.
|
|
12
|
+
*/
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { suppressMentionSyntax } from './suppress.js';
|
|
15
|
+
/**
|
|
16
|
+
* path abbreviation (16.1): the user's home directory collapses
|
|
17
|
+
* to a `~` prefix — `/Users/addo/Workspaces/x` → `~/Workspaces/x` — so
|
|
18
|
+
* autocomplete labels stay short without hiding the directory structure.
|
|
19
|
+
* The home path must end at a segment boundary: `/Users/addoish/x` is NOT under `/Users/addo`. Display-only;
|
|
20
|
+
* never persisted to Discord metadata.
|
|
21
|
+
*/
|
|
22
|
+
export function abbreviatePath(fullPath, home = homedir()) {
|
|
23
|
+
if (fullPath === home)
|
|
24
|
+
return '~';
|
|
25
|
+
if (fullPath.startsWith(`${home}/`))
|
|
26
|
+
return `~${fullPath.slice(home.length)}`;
|
|
27
|
+
return fullPath;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Discord's SUPPRESS_NOTIFICATIONS message flag (1 << 12): the message will
|
|
31
|
+
* not trigger push/desktop notifications. This flag says nothing about
|
|
32
|
+
* mentions — ping prevention is the `allowed_mentions` request field below,
|
|
33
|
+
* and the syntax neutralization in policy/suppress.ts is the third layer.
|
|
34
|
+
*/
|
|
35
|
+
export const DISCORD_SUPPRESS_NOTIFICATIONS_FLAG = 1 << 12;
|
|
36
|
+
/**
|
|
37
|
+
* Discord's request field that disables every mention parse category: with
|
|
38
|
+
* `parse: []` no `@everyone`, user, role, or channel mention in the content
|
|
39
|
+
* pings, regardless of how the content renders. Every message create/edit
|
|
40
|
+
* the adapter sends carries this.
|
|
41
|
+
*/
|
|
42
|
+
export const ALLOWED_MENTIONS_NONE = { parse: [] };
|
|
43
|
+
/** Discord's ephemeral message flag (only the invoker sees the message). */
|
|
44
|
+
export const DISCORD_EPHEMERAL_FLAG = 1 << 6;
|
|
45
|
+
/** Ephemeral + silent: the standard adapter followup flags. */
|
|
46
|
+
export const OUTBOUND_EPHEMERAL_FLAGS = DISCORD_EPHEMERAL_FLAG | DISCORD_SUPPRESS_NOTIFICATIONS_FLAG;
|
|
47
|
+
/**
|
|
48
|
+
* The comparison key for Discord channel/thread names: Discord slugs names
|
|
49
|
+
* (lowercase, whitespace runs collapse to `-`), so a rename-dedupe check
|
|
50
|
+
* must compare both sides slugified or every restart burns a rename.
|
|
51
|
+
*/
|
|
52
|
+
export function discordChannelNameKey(text) {
|
|
53
|
+
return text.trim().toLowerCase().replace(/\s+/gu, '-');
|
|
54
|
+
}
|
|
55
|
+
/** Component label ceiling imposed by Discord. */
|
|
56
|
+
const DISCORD_LABEL_MAX = 100;
|
|
57
|
+
/** Length of the id tail used to disambiguate duplicate titles. */
|
|
58
|
+
const DISAMBIGUATOR_TAIL = 4;
|
|
59
|
+
function stripControlCharacters(text) {
|
|
60
|
+
let out = '';
|
|
61
|
+
for (const char of text) {
|
|
62
|
+
const code = char.charCodeAt(0);
|
|
63
|
+
if (code < 9 || (code > 13 && code < 32) || code === 127)
|
|
64
|
+
continue;
|
|
65
|
+
out += char;
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
function sanitizePart(text) {
|
|
70
|
+
return suppressMentionSyntax(stripControlCharacters(text)
|
|
71
|
+
.replace(/[\r\n]+/gu, ' ')
|
|
72
|
+
.replace(/ {2,}/gu, ' ')
|
|
73
|
+
.trim());
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Sanitize user-controlled text for safe rendering as a label or title:
|
|
77
|
+
* control characters and line breaks collapse, mention syntax breaks, and
|
|
78
|
+
* Discord's component length ceiling applies.
|
|
79
|
+
*/
|
|
80
|
+
export function safeTitle(text, maxLength = DISCORD_LABEL_MAX) {
|
|
81
|
+
const cleaned = sanitizePart(text);
|
|
82
|
+
return cleaned.length <= maxLength ? cleaned : cleaned.slice(0, maxLength);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Label a Workspace list: unique titles stay bare; duplicate titles gain a
|
|
86
|
+
* short id suffix so every label stays unambiguous without exposing paths.
|
|
87
|
+
* Escalating suffix groups grow together (4 → 8 → … chars, then the full id)
|
|
88
|
+
* and stay deterministic.
|
|
89
|
+
*/
|
|
90
|
+
export function workspaceLabels(entries) {
|
|
91
|
+
const groups = new Map();
|
|
92
|
+
entries.forEach((entry, index) => {
|
|
93
|
+
const title = safeTitle(entry.title);
|
|
94
|
+
const group = groups.get(title) ?? [];
|
|
95
|
+
group.push(index);
|
|
96
|
+
groups.set(title, group);
|
|
97
|
+
});
|
|
98
|
+
const labels = new Array(entries.length);
|
|
99
|
+
for (const [title, indices] of groups) {
|
|
100
|
+
const single = indices[0];
|
|
101
|
+
if (indices.length === 1 && single !== undefined) {
|
|
102
|
+
labels[single] = title;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (let tailLength = DISAMBIGUATOR_TAIL;; tailLength += DISAMBIGUATOR_TAIL) {
|
|
106
|
+
const tails = indices.map((index) => {
|
|
107
|
+
const id = entries[index]?.id ?? '';
|
|
108
|
+
return id.slice(-Math.min(tailLength, id.length));
|
|
109
|
+
});
|
|
110
|
+
const unique = new Set(tails).size === indices.length;
|
|
111
|
+
const atFullLength = indices.every(index => (entries[index]?.id.length ?? 0) <= tailLength);
|
|
112
|
+
if (unique || atFullLength) {
|
|
113
|
+
indices.forEach((index, position) => {
|
|
114
|
+
const tail = tails[position] ?? '';
|
|
115
|
+
labels[index] = `${title} (${tail})`;
|
|
116
|
+
});
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return entries.map((entry, index) => ({
|
|
122
|
+
id: entry.id,
|
|
123
|
+
title: safeTitle(entry.title),
|
|
124
|
+
label: labels[index] ?? safeTitle(entry.title),
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
/** The opaque value a select option encodes for choosing this workspace. */
|
|
128
|
+
export function workspaceReference(id) {
|
|
129
|
+
return `ws:${id}`;
|
|
130
|
+
}
|
|
131
|
+
/** The inverse of workspaceReference; `undefined` for any other string. */
|
|
132
|
+
export function parseWorkspaceReference(reference) {
|
|
133
|
+
if (!reference.startsWith('ws:'))
|
|
134
|
+
return undefined;
|
|
135
|
+
const id = reference.slice(3);
|
|
136
|
+
return id === '' ? undefined : id;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Project one workspace for display. `includePath` is for the ephemeral
|
|
140
|
+
* detail response (any authorized member per amended design §3); list
|
|
141
|
+
* surfaces always pass false, so a path never reaches channel metadata or
|
|
142
|
+
* public messages.
|
|
143
|
+
*/
|
|
144
|
+
export function describeWorkspace(entry, options) {
|
|
145
|
+
const [labeled] = workspaceLabels([entry]);
|
|
146
|
+
const label = labeled?.label ?? safeTitle(entry.title);
|
|
147
|
+
return {
|
|
148
|
+
id: entry.id,
|
|
149
|
+
title: safeTitle(entry.title),
|
|
150
|
+
label,
|
|
151
|
+
...(options.includePath && entry.path !== undefined ? { path: entry.path } : {}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authorization middleware installed at the single normalized ingress
|
|
3
|
+
* boundary. Every event kind — messages, slash commands, autocomplete,
|
|
4
|
+
* buttons, selects, and modals — flows through this one gate, so no DSH
|
|
5
|
+
* access can precede authorization by construction: the business callback
|
|
6
|
+
* only fires with a positive, ranked decision attached. Ordinary messages
|
|
7
|
+
* deny silently; interactions deny with an explicit result the transport
|
|
8
|
+
* layer turns into an ephemeral reply.
|
|
9
|
+
*/
|
|
10
|
+
import { type AccessDecision, type PolicyTable } from './authorization.js';
|
|
11
|
+
import { type NormalizedInboundEvent } from '../gateway/ingress.js';
|
|
12
|
+
import type { DiscordSnowflake, GatewayDispatch } from '../gateway/inbound.js';
|
|
13
|
+
export interface AuthorizedIngressOptions {
|
|
14
|
+
selfUserId: DiscordSnowflake;
|
|
15
|
+
/** Live policy reader so settings updates apply without re-installing. */
|
|
16
|
+
policy: () => PolicyTable;
|
|
17
|
+
/** The business boundary; only invoked with a positive decision. */
|
|
18
|
+
onEvent(event: NormalizedInboundEvent, decision: AccessDecision): void;
|
|
19
|
+
}
|
|
20
|
+
export type AuthorizedGateResult = {
|
|
21
|
+
accepted: true;
|
|
22
|
+
event: NormalizedInboundEvent;
|
|
23
|
+
decision: AccessDecision;
|
|
24
|
+
} | {
|
|
25
|
+
accepted: false;
|
|
26
|
+
reason: string;
|
|
27
|
+
response: 'silent' | 'ephemeral-denial';
|
|
28
|
+
};
|
|
29
|
+
export declare function createAuthorizedIngress(options: AuthorizedIngressOptions): {
|
|
30
|
+
accept(dispatch: GatewayDispatch): AuthorizedGateResult;
|
|
31
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The authorization middleware installed at the single normalized ingress
|
|
3
|
+
* boundary. Every event kind — messages, slash commands, autocomplete,
|
|
4
|
+
* buttons, selects, and modals — flows through this one gate, so no DSH
|
|
5
|
+
* access can precede authorization by construction: the business callback
|
|
6
|
+
* only fires with a positive, ranked decision attached. Ordinary messages
|
|
7
|
+
* deny silently; interactions deny with an explicit result the transport
|
|
8
|
+
* layer turns into an ephemeral reply.
|
|
9
|
+
*/
|
|
10
|
+
import { evaluateAuthorization } from './authorization.js';
|
|
11
|
+
import { normalizeInbound } from '../gateway/ingress.js';
|
|
12
|
+
export function createAuthorizedIngress(options) {
|
|
13
|
+
const policyAt = () => options.policy();
|
|
14
|
+
function accept(dispatch) {
|
|
15
|
+
// 1. Normalization: malformed frames, DMs, bots, self-messages die here.
|
|
16
|
+
const result = normalizeInbound(dispatch, options.selfUserId);
|
|
17
|
+
if (!result.accepted) {
|
|
18
|
+
return { accepted: false, reason: result.reason, response: 'silent' };
|
|
19
|
+
}
|
|
20
|
+
// 2. Outer boundary: the Guild allowlist.
|
|
21
|
+
const event = result.event;
|
|
22
|
+
if (!policyAt().allowedGuildIds.includes(event.guildId)) {
|
|
23
|
+
return { accepted: false, reason: 'guild-not-allowed', response: 'silent' };
|
|
24
|
+
}
|
|
25
|
+
// 3. Inner boundary: deny-first member/administrator/operator evaluation.
|
|
26
|
+
const isMessage = event.kind === 'message';
|
|
27
|
+
const decision = evaluateAuthorization(policyAt(), {
|
|
28
|
+
guildId: event.guildId,
|
|
29
|
+
userId: isMessage ? event.authorId : event.actorId,
|
|
30
|
+
roleIds: event.roleIds,
|
|
31
|
+
memberPermissions: event.memberPermissions,
|
|
32
|
+
...(isMessage ? {} : { isBot: event.isBot }),
|
|
33
|
+
});
|
|
34
|
+
if (!decision.allowed) {
|
|
35
|
+
return {
|
|
36
|
+
accepted: false,
|
|
37
|
+
reason: decision.reason,
|
|
38
|
+
// Interactions must answer (ephemeral denial); messages ignore.
|
|
39
|
+
response: isMessage ? 'silent' : 'ephemeral-denial',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
options.onEvent(event, decision);
|
|
43
|
+
return { accepted: true, event, decision };
|
|
44
|
+
}
|
|
45
|
+
return { accept };
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention-syntax neutralization for outbound text. Discord renders
|
|
3
|
+
* `<@id>`, `<@!id>`, `<#id>`, `<@&id>`, `@everyone`, and `@here` pings; the
|
|
4
|
+
* adapter's `allowed_mentions: { parse: [] }` request field stops them from
|
|
5
|
+
* notifying, but titles and labels can flow into surfaces the field does not
|
|
6
|
+
* cover (component labels, thread names, markdown contexts), so the syntax
|
|
7
|
+
* itself is broken with a zero-width space the renderer treats as plain text.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Break every Discord mention form inside `text` so no renderer — with or
|
|
11
|
+
* without the suppression flag — can resolve it into a ping.
|
|
12
|
+
*/
|
|
13
|
+
export declare function suppressMentionSyntax(text: string): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mention-syntax neutralization for outbound text. Discord renders
|
|
3
|
+
* `<@id>`, `<@!id>`, `<#id>`, `<@&id>`, `@everyone`, and `@here` pings; the
|
|
4
|
+
* adapter's `allowed_mentions: { parse: [] }` request field stops them from
|
|
5
|
+
* notifying, but titles and labels can flow into surfaces the field does not
|
|
6
|
+
* cover (component labels, thread names, markdown contexts), so the syntax
|
|
7
|
+
* itself is broken with a zero-width space the renderer treats as plain text.
|
|
8
|
+
*/
|
|
9
|
+
const ZERO_WIDTH = '\u200b';
|
|
10
|
+
/**
|
|
11
|
+
* Break every Discord mention form inside `text` so no renderer — with or
|
|
12
|
+
* without the suppression flag — can resolve it into a ping.
|
|
13
|
+
*/
|
|
14
|
+
export function suppressMentionSyntax(text) {
|
|
15
|
+
return text
|
|
16
|
+
.replace(/<@([!&]?)/gu, `<@$1${ZERO_WIDTH}`)
|
|
17
|
+
.replace(/<#/gu, `<#${ZERO_WIDTH}`)
|
|
18
|
+
.replaceAll('@everyone', `@${ZERO_WIDTH}everyone`)
|
|
19
|
+
.replaceAll('@here', `@${ZERO_WIDTH}here`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** The plugin's settings namespace, shared by the Host registration and the
|
|
2
|
+
* browser card. Declared locally (not imported from `dsh-settings`) so the
|
|
3
|
+
* client bundle stays free of host-package value imports.
|
|
4
|
+
*/
|
|
5
|
+
import type { SettingsNamespace } from '@deepseek-ai/dsh-settings';
|
|
6
|
+
/** The branded settings namespace, matching the Host plugin's registration. */
|
|
7
|
+
export declare const DISCORD_SETTINGS_NAMESPACE: SettingsNamespace;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** The plugin's settings namespace, shared by the Host registration and the
|
|
2
|
+
* browser card. Declared locally (not imported from `dsh-settings`) so the
|
|
3
|
+
* client bundle stays free of host-package value imports.
|
|
4
|
+
*/
|
|
5
|
+
/** The branded settings namespace, matching the Host plugin's registration. */
|
|
6
|
+
export const DISCORD_SETTINGS_NAMESPACE = 'dsh-discord';
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import z from '@deepseek-ai/schemastery';
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
export { DISCORD_SETTINGS_NAMESPACE } from './settings-namespace.js';
|
|
4
|
+
export type DiscordVerbosity = 'text-only' | 'essential-tools' | 'full-tools';
|
|
5
|
+
/** Card language preference: follow the DSH locale, or pin Chinese/English. */
|
|
6
|
+
export type Language = 'auto' | 'zh' | 'en';
|
|
7
|
+
export interface DiscordSettings {
|
|
8
|
+
enabled: boolean;
|
|
9
|
+
allowedGuildIds: string[];
|
|
10
|
+
memberUserIds: string[];
|
|
11
|
+
memberRoleIds: string[];
|
|
12
|
+
administratorUserIds: string[];
|
|
13
|
+
administratorRoleIds: string[];
|
|
14
|
+
deniedUserIds: string[];
|
|
15
|
+
deniedRoleIds: string[];
|
|
16
|
+
hostOperatorUserIds: string[];
|
|
17
|
+
defaultVerbosity: DiscordVerbosity;
|
|
18
|
+
/** Discord-visible copy language for adapter messages (16.25). */
|
|
19
|
+
language: Language;
|
|
20
|
+
streamUpdateIntervalMs: number;
|
|
21
|
+
typingIntervalMs: number;
|
|
22
|
+
approvalTimeoutMs: number;
|
|
23
|
+
questionTimeoutMs: number;
|
|
24
|
+
/** Task-thread auto-archive; Discord supports exactly these four values. */
|
|
25
|
+
threadAutoArchiveMinutes: ThreadAutoArchiveMinutes;
|
|
26
|
+
}
|
|
27
|
+
/** The archive durations Discord's API accepts (minutes). */
|
|
28
|
+
export type ThreadAutoArchiveMinutes = 60 | 1440 | 4320 | 10080;
|
|
29
|
+
export declare const THREAD_AUTO_ARCHIVE_OPTIONS: readonly ThreadAutoArchiveMinutes[];
|
|
30
|
+
export declare const DEFAULT_DISCORD_SETTINGS: DiscordSettings;
|
|
31
|
+
export declare const DiscordSettingsSchema: z<DiscordSettings>;
|
|
32
|
+
export declare function normalizeDiscordSettings(input: DiscordSettings): DiscordSettings;
|
|
33
|
+
export declare function validateDiscordSettings(input: DiscordSettings): void;
|
|
34
|
+
export interface DiscordSettingsSource {
|
|
35
|
+
get(): DiscordSettings;
|
|
36
|
+
}
|
|
37
|
+
export declare function installDiscordSettings(ctx: Context, entry: DiscordSettings, onChange: (value: DiscordSettings) => void): void;
|