@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
package/lib/compose.d.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter composition root (review C1). Assembles the runtime the
|
|
3
|
+
* module-level features could not wire themselves into: bot token → Gateway
|
|
4
|
+
* → authorized ingress → business router → Discord REST / DSH apiProxy,
|
|
5
|
+
* plus the settings-card status feed. Everything injected is a port;
|
|
6
|
+
* everything started here is torn down by the returned disposer.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed by construction: without a resolvable bot token nothing
|
|
9
|
+
* connects, and the status reports exactly that instead of pretending.
|
|
10
|
+
*/
|
|
11
|
+
import { type GatewaySocketFactory } from './gateway/gateway.js';
|
|
12
|
+
import type { AccessDecision, PolicyTable } from './policy/authorization.js';
|
|
13
|
+
import type { NormalizedInboundEvent } from './gateway/ingress.js';
|
|
14
|
+
import { type ComponentRegistry } from './discord/components.js';
|
|
15
|
+
import type { AdapterStatusTracker } from './features/adapter-status.js';
|
|
16
|
+
import type { ApprovalStore } from './features/approval-store.js';
|
|
17
|
+
import type { QuestionStore } from './features/question-store.js';
|
|
18
|
+
/** Discord Gateway wss endpoint (Milestone 1: fixed v10 surface). */
|
|
19
|
+
export declare const GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
20
|
+
/**
|
|
21
|
+
* The session mainline (src/features/session-mainline.ts): the orchestrator
|
|
22
|
+
* behind both message paths — bound-channel mentions create thread/session,
|
|
23
|
+
* adapter-owned thread messages queue continuations.
|
|
24
|
+
*/
|
|
25
|
+
export interface SessionMainlinePort {
|
|
26
|
+
admitMention(request: {
|
|
27
|
+
applicationId: string;
|
|
28
|
+
guildId: string;
|
|
29
|
+
channelId: string;
|
|
30
|
+
messageId: string;
|
|
31
|
+
authorId: string;
|
|
32
|
+
workspaceId: string;
|
|
33
|
+
prompt: string;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
outcome: 'admitted';
|
|
36
|
+
threadId: string;
|
|
37
|
+
sessionId: string;
|
|
38
|
+
} | {
|
|
39
|
+
outcome: 'thread-conflict';
|
|
40
|
+
} | {
|
|
41
|
+
outcome: 'thread-failed';
|
|
42
|
+
} | {
|
|
43
|
+
outcome: 'session-rejected';
|
|
44
|
+
} | {
|
|
45
|
+
outcome: 'session-unknown';
|
|
46
|
+
} | {
|
|
47
|
+
outcome: 'prompt-rejected';
|
|
48
|
+
} | {
|
|
49
|
+
outcome: 'prompt-unknown';
|
|
50
|
+
}>;
|
|
51
|
+
continueInThread(request: {
|
|
52
|
+
applicationId: string;
|
|
53
|
+
guildId: string;
|
|
54
|
+
threadId: string;
|
|
55
|
+
sessionId: string;
|
|
56
|
+
messageId: string;
|
|
57
|
+
prompt: string;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
outcome: 'queued';
|
|
60
|
+
} | {
|
|
61
|
+
outcome: 'already-submitted';
|
|
62
|
+
} | {
|
|
63
|
+
outcome: 'conflict';
|
|
64
|
+
} | {
|
|
65
|
+
outcome: 'rejected';
|
|
66
|
+
} | {
|
|
67
|
+
outcome: 'unknown';
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
export interface BindingsProbe {
|
|
71
|
+
/** Workspace id bound to the channel, when one exists. */
|
|
72
|
+
workspaceForChannel(guildId: string, channelId: string): string | undefined;
|
|
73
|
+
/** Session id owning the thread (writable binding), when one exists. */
|
|
74
|
+
sessionForThread(guildId: string, threadId: string): string | undefined;
|
|
75
|
+
}
|
|
76
|
+
export interface CompositionDeps {
|
|
77
|
+
tokenProvider: () => Promise<string | undefined>;
|
|
78
|
+
socketFactory: GatewaySocketFactory;
|
|
79
|
+
policy: () => PolicyTable;
|
|
80
|
+
/** Resolves the bot's own user id (Discord GET /users/@me) before start. */
|
|
81
|
+
selfUserIdProvider: () => Promise<string>;
|
|
82
|
+
/** Gateway intent bitmask (Milestone 1 fixed set). */
|
|
83
|
+
intents: number;
|
|
84
|
+
/**
|
|
85
|
+
* Gateway endpoint override (twin-E2E seam): defaults to the production
|
|
86
|
+
* wss URL. Must expose the real Gateway wire protocol, not a fake.
|
|
87
|
+
*/
|
|
88
|
+
gatewayUrl?: string;
|
|
89
|
+
/** Live accessor for the adapter's Discord application id (binding keys). */
|
|
90
|
+
applicationId: () => string;
|
|
91
|
+
mainline: SessionMainlinePort;
|
|
92
|
+
bindings: BindingsProbe;
|
|
93
|
+
approvals: ApprovalStore;
|
|
94
|
+
questions: QuestionStore;
|
|
95
|
+
status: AdapterStatusTracker;
|
|
96
|
+
/**
|
|
97
|
+
* Interaction (commands/components/modals) routing, wired by the Host
|
|
98
|
+
* composition where the typed apiProxy respond face lives.
|
|
99
|
+
*/
|
|
100
|
+
routeInteraction?: (event: NormalizedInboundEvent, interactionToken?: string) => void | Promise<void>;
|
|
101
|
+
/**
|
|
102
|
+
* Public (non-ephemeral — ordinary messages have no ephemeral channel)
|
|
103
|
+
* bind affordance for an authorized mention in an unbound channel
|
|
104
|
+
* (session-control spec, "New task in an unbound channel"). Absent, the
|
|
105
|
+
* mention is ignored as before.
|
|
106
|
+
*/
|
|
107
|
+
unboundNotice?: (request: {
|
|
108
|
+
guildId: string;
|
|
109
|
+
channelId: string;
|
|
110
|
+
actorId: string;
|
|
111
|
+
audience: 'administrator' | 'member';
|
|
112
|
+
}) => void | Promise<void>;
|
|
113
|
+
/** Idempotently ensure the category + control channel exist in a guild. */
|
|
114
|
+
ensureGuildChannels?: (guildId: string) => Promise<void>;
|
|
115
|
+
/**
|
|
116
|
+
* Runs once per READY after channel provisioning: the reconciliation
|
|
117
|
+
* sweep's trigger (startup mapping verification precedes accepting
|
|
118
|
+
* writes for those mappings — reconciliation spec).
|
|
119
|
+
*/
|
|
120
|
+
onReady?: () => void | Promise<void>;
|
|
121
|
+
/** Allowlist snapshot used to provision channels on READY. */
|
|
122
|
+
allowedGuildIds?: readonly string[] | undefined;
|
|
123
|
+
/**
|
|
124
|
+
* Registry override so the composition root shares ONE registry instance
|
|
125
|
+
* across the runtime and every feature handler (question controls,
|
|
126
|
+
* approval buttons, bind confirms).
|
|
127
|
+
*/
|
|
128
|
+
registry?: ComponentRegistry;
|
|
129
|
+
logger?: {
|
|
130
|
+
warn(event: string, detail?: unknown): void;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export interface DiscordAdapterRuntime {
|
|
134
|
+
registry: ComponentRegistry;
|
|
135
|
+
approvals: ApprovalStore;
|
|
136
|
+
questions: QuestionStore;
|
|
137
|
+
started: boolean;
|
|
138
|
+
/** Why the runtime did not start (no token ⇒ fail-closed offline). */
|
|
139
|
+
startError?: 'missing-token' | undefined;
|
|
140
|
+
/**
|
|
141
|
+
* Re-run the start chain after a late credential write (the card's
|
|
142
|
+
* Connect button). Idempotent: an already-running gateway is replaced.
|
|
143
|
+
*/
|
|
144
|
+
connect(): void;
|
|
145
|
+
/**
|
|
146
|
+
* Operator-initiated offline. The credential stays stored; the start chain
|
|
147
|
+
* is blocked until the next connect().
|
|
148
|
+
*/
|
|
149
|
+
disconnect(): void;
|
|
150
|
+
dispose(): void;
|
|
151
|
+
}
|
|
152
|
+
/** Business routing over one normalized, already-authorized event. */
|
|
153
|
+
export declare function routeEvent(deps: CompositionDeps, event: NormalizedInboundEvent, decision: AccessDecision): void;
|
|
154
|
+
/**
|
|
155
|
+
* Start the composed adapter runtime. The returned runtime is live after the
|
|
156
|
+
* token resolves; `started` flips once the Gateway has been spawned.
|
|
157
|
+
*/
|
|
158
|
+
export declare function startDiscordAdapter(deps: CompositionDeps): DiscordAdapterRuntime;
|
package/lib/compose.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter composition root (review C1). Assembles the runtime the
|
|
3
|
+
* module-level features could not wire themselves into: bot token → Gateway
|
|
4
|
+
* → authorized ingress → business router → Discord REST / DSH apiProxy,
|
|
5
|
+
* plus the settings-card status feed. Everything injected is a port;
|
|
6
|
+
* everything started here is torn down by the returned disposer.
|
|
7
|
+
*
|
|
8
|
+
* Fail-closed by construction: without a resolvable bot token nothing
|
|
9
|
+
* connects, and the status reports exactly that instead of pretending.
|
|
10
|
+
*/
|
|
11
|
+
import { startGateway } from './gateway/gateway.js';
|
|
12
|
+
import { createAuthorizedIngress } from './policy/guard.js';
|
|
13
|
+
import { planUnboundMention } from './features/unbound-mention.js';
|
|
14
|
+
import { createComponentRegistry } from './discord/components.js';
|
|
15
|
+
/** Discord Gateway wss endpoint (Milestone 1: fixed v10 surface). */
|
|
16
|
+
export const GATEWAY_URL = 'wss://gateway.discord.gg/?v=10&encoding=json';
|
|
17
|
+
/** Business routing over one normalized, already-authorized event. */
|
|
18
|
+
export function routeEvent(deps, event, decision) {
|
|
19
|
+
if (event.kind === 'message') {
|
|
20
|
+
const prompt = event.content.trim();
|
|
21
|
+
// Adapter-owned thread: ordinary continuation, no mention required.
|
|
22
|
+
const sessionId = deps.bindings.sessionForThread(event.guildId, event.channelId);
|
|
23
|
+
if (sessionId !== undefined) {
|
|
24
|
+
if (prompt === '')
|
|
25
|
+
return;
|
|
26
|
+
const request = {
|
|
27
|
+
applicationId: deps.applicationId(),
|
|
28
|
+
guildId: event.guildId,
|
|
29
|
+
threadId: event.channelId,
|
|
30
|
+
sessionId,
|
|
31
|
+
messageId: event.messageId,
|
|
32
|
+
prompt,
|
|
33
|
+
};
|
|
34
|
+
void deps.mainline.continueInThread(request)
|
|
35
|
+
.then((result) => {
|
|
36
|
+
if (result.outcome === 'rejected' || result.outcome === 'unknown') {
|
|
37
|
+
deps.logger?.warn('discord_continuation_not_queued', { ...result, messageId: event.messageId });
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
.catch((cause) => {
|
|
41
|
+
deps.logger?.warn('discord_continuation_failed', { messageId: event.messageId, cause: String(cause) });
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
// Mention-gated prompt flow: bindings decide admit vs ignore.
|
|
46
|
+
const workspaceId = deps.bindings.workspaceForChannel(event.guildId, event.channelId);
|
|
47
|
+
if (!event.mentionedBot)
|
|
48
|
+
return;
|
|
49
|
+
if (workspaceId === undefined) {
|
|
50
|
+
// Unbound channel: one public bind affordance for authorized members,
|
|
51
|
+
// silent for everyone the guard already refused (spec-as-written,
|
|
52
|
+
// task 16.5). No DSH call ever follows.
|
|
53
|
+
const plan = planUnboundMention({ decision, isBound: false });
|
|
54
|
+
if (plan.outcome === 'bind-affordance') {
|
|
55
|
+
void Promise.resolve(deps.unboundNotice?.({
|
|
56
|
+
guildId: event.guildId,
|
|
57
|
+
channelId: event.channelId,
|
|
58
|
+
actorId: event.authorId,
|
|
59
|
+
audience: plan.audience,
|
|
60
|
+
})).catch((cause) => {
|
|
61
|
+
deps.logger?.warn('discord_unbound_notice_failed', { cause: String(cause) });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (prompt === '')
|
|
67
|
+
return;
|
|
68
|
+
void deps.mainline.admitMention({
|
|
69
|
+
applicationId: deps.applicationId(),
|
|
70
|
+
guildId: event.guildId,
|
|
71
|
+
channelId: event.channelId,
|
|
72
|
+
messageId: event.messageId,
|
|
73
|
+
authorId: event.authorId,
|
|
74
|
+
workspaceId,
|
|
75
|
+
prompt,
|
|
76
|
+
})
|
|
77
|
+
.then((result) => {
|
|
78
|
+
if (result.outcome !== 'admitted') {
|
|
79
|
+
deps.logger?.warn('discord_mention_not_admitted', { result, messageId: event.messageId });
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.catch((cause) => {
|
|
83
|
+
deps.logger?.warn('discord_mention_admit_failed', { messageId: event.messageId, cause: String(cause) });
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
// Interactions are dispatched ONLY from handleDispatch (which owns the
|
|
88
|
+
// interaction token); no token-less forwarding here (double-call bug).
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Start the composed adapter runtime. The returned runtime is live after the
|
|
92
|
+
* token resolves; `started` flips once the Gateway has been spawned.
|
|
93
|
+
*/
|
|
94
|
+
export function startDiscordAdapter(deps) {
|
|
95
|
+
const registry = deps.registry ?? createComponentRegistry();
|
|
96
|
+
const status = deps.status;
|
|
97
|
+
let gateway;
|
|
98
|
+
let started = false;
|
|
99
|
+
let startError;
|
|
100
|
+
let ingress;
|
|
101
|
+
// provisioning style: one category + one control channel per
|
|
102
|
+
// allowed guild, created idempotently on the first READY.
|
|
103
|
+
const provisioned = new Set();
|
|
104
|
+
function handleDispatch(dispatch) {
|
|
105
|
+
if (dispatch.t === 'READY')
|
|
106
|
+
status.setGateway('connected');
|
|
107
|
+
const accepted = ingress?.accept(dispatch);
|
|
108
|
+
if (accepted?.accepted === false && (dispatch.t === 'INTERACTION_CREATE' || dispatch.t === 'MESSAGE_CREATE')) {
|
|
109
|
+
// A user-driven dispatch that dies at the gate must not vanish: from
|
|
110
|
+
// the user's side the click "does nothing", so the gate reason is on
|
|
111
|
+
// the record. Unrelated dispatch noise stays unlogged.
|
|
112
|
+
deps.logger?.warn('discord_dispatch_rejected', { type: dispatch.t, reason: accepted.reason });
|
|
113
|
+
}
|
|
114
|
+
if (accepted?.accepted === true && accepted.event !== undefined && accepted.event.kind === 'interaction') {
|
|
115
|
+
const d = dispatch.d;
|
|
116
|
+
const token = typeof d?.['token'] === 'string' ? d['token'] : undefined;
|
|
117
|
+
void Promise.resolve(deps.routeInteraction?.(accepted.event, token)).catch((cause) => {
|
|
118
|
+
console.error('[dsh-discord] interaction handler failed:', cause);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (dispatch.t === 'READY' && deps.ensureGuildChannels !== undefined) {
|
|
122
|
+
for (const guildId of deps.allowedGuildIds ?? []) {
|
|
123
|
+
if (provisioned.has(guildId))
|
|
124
|
+
continue;
|
|
125
|
+
provisioned.add(guildId);
|
|
126
|
+
deps.ensureGuildChannels(guildId).catch((cause) => {
|
|
127
|
+
deps.logger?.warn('discord_channel_provision_failed', { guildId, cause: String(cause) });
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (dispatch.t === 'READY' && deps.onReady !== undefined) {
|
|
132
|
+
void Promise.resolve(deps.onReady()).catch((cause) => {
|
|
133
|
+
deps.logger?.warn('discord_ready_reconcile_failed', String(cause));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Set while the operator explicitly disconnected; blocks auto start. */
|
|
138
|
+
let stoppedByUser = false;
|
|
139
|
+
/**
|
|
140
|
+
* Start-chain generation. Bumped by connect()/disconnect() so an in-flight
|
|
141
|
+
* chain (suspended at its token/identity awaits) dies quietly instead of
|
|
142
|
+
* spawning a second, unownable gateway after the operator acted.
|
|
143
|
+
*/
|
|
144
|
+
let startSeq = 0;
|
|
145
|
+
async function runStartChain() {
|
|
146
|
+
const seq = ++startSeq;
|
|
147
|
+
const stale = () => stoppedByUser || seq !== startSeq;
|
|
148
|
+
if (stale())
|
|
149
|
+
return;
|
|
150
|
+
const token = await deps.tokenProvider();
|
|
151
|
+
if (stale())
|
|
152
|
+
return;
|
|
153
|
+
if (token === undefined || token === '') {
|
|
154
|
+
started = false;
|
|
155
|
+
startError = 'missing-token';
|
|
156
|
+
status.setCredential({ configured: false });
|
|
157
|
+
status.setGateway('disconnected');
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
status.setCredential({ configured: true });
|
|
161
|
+
const selfUserId = await deps.selfUserIdProvider();
|
|
162
|
+
if (stale())
|
|
163
|
+
return;
|
|
164
|
+
ingress = createAuthorizedIngress({
|
|
165
|
+
selfUserId,
|
|
166
|
+
policy: deps.policy,
|
|
167
|
+
onEvent: (event, decision) => { routeEvent(deps, event, decision); },
|
|
168
|
+
});
|
|
169
|
+
gateway = startGateway({
|
|
170
|
+
url: deps.gatewayUrl ?? GATEWAY_URL,
|
|
171
|
+
tokenProvider: () => deps.tokenProvider().then((value) => {
|
|
172
|
+
if (value === undefined)
|
|
173
|
+
throw new TypeError('discord bot token unavailable');
|
|
174
|
+
return value;
|
|
175
|
+
}),
|
|
176
|
+
intents: deps.intents,
|
|
177
|
+
socketFactory: deps.socketFactory,
|
|
178
|
+
onDispatch: handleDispatch,
|
|
179
|
+
onTerminalClose: (code) => {
|
|
180
|
+
status.setGateway({ kind: 'terminal-close', code });
|
|
181
|
+
},
|
|
182
|
+
onBackoffScheduled: () => {
|
|
183
|
+
status.setGateway('connecting');
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
started = true;
|
|
187
|
+
status.setGateway('connecting');
|
|
188
|
+
}
|
|
189
|
+
void runStartChain().catch((cause) => {
|
|
190
|
+
deps.logger?.warn('discord_adapter_start_failed', String(cause));
|
|
191
|
+
status.setGateway('disconnected');
|
|
192
|
+
});
|
|
193
|
+
return {
|
|
194
|
+
registry,
|
|
195
|
+
approvals: deps.approvals,
|
|
196
|
+
questions: deps.questions,
|
|
197
|
+
get started() { return started; },
|
|
198
|
+
get startError() { return startError; },
|
|
199
|
+
connect() {
|
|
200
|
+
stoppedByUser = false;
|
|
201
|
+
startError = undefined;
|
|
202
|
+
startSeq += 1;
|
|
203
|
+
gateway?.dispose();
|
|
204
|
+
gateway = undefined;
|
|
205
|
+
started = false;
|
|
206
|
+
void runStartChain().catch((cause) => {
|
|
207
|
+
deps.logger?.warn('discord_adapter_connect_failed', String(cause));
|
|
208
|
+
status.setGateway('disconnected');
|
|
209
|
+
});
|
|
210
|
+
},
|
|
211
|
+
disconnect() {
|
|
212
|
+
// Operator-initiated offline: the credential stays in the store so the
|
|
213
|
+
// next Connect (possibly with a replaced token) needs no re-entry.
|
|
214
|
+
stoppedByUser = true;
|
|
215
|
+
startSeq += 1;
|
|
216
|
+
gateway?.dispose();
|
|
217
|
+
gateway = undefined;
|
|
218
|
+
started = false;
|
|
219
|
+
startError = undefined;
|
|
220
|
+
status.setGateway('disconnected');
|
|
221
|
+
},
|
|
222
|
+
dispose() {
|
|
223
|
+
startSeq += 1;
|
|
224
|
+
gateway?.dispose();
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const DISCORD_BOT_TOKEN_REF: import("@deepseek-ai/dsh-credentials").CredentialRef;
|
|
2
|
+
export interface DiscordCredentialProvider {
|
|
3
|
+
describe(ref: typeof DISCORD_BOT_TOKEN_REF): Promise<{
|
|
4
|
+
configured: boolean;
|
|
5
|
+
source?: string;
|
|
6
|
+
writable: boolean;
|
|
7
|
+
}>;
|
|
8
|
+
resolve(ref: typeof DISCORD_BOT_TOKEN_REF): Promise<{
|
|
9
|
+
value: string;
|
|
10
|
+
source: string;
|
|
11
|
+
} | undefined>;
|
|
12
|
+
/** Durably store the secret in the provider-managed writable source. */
|
|
13
|
+
set(ref: typeof DISCORD_BOT_TOKEN_REF, value: string): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
export declare function describeDiscordCredential(provider: Pick<DiscordCredentialProvider, 'describe' | 'resolve'>): Promise<{
|
|
16
|
+
configured: boolean;
|
|
17
|
+
source?: string;
|
|
18
|
+
writable: boolean;
|
|
19
|
+
}>;
|
|
20
|
+
export declare function resolveDiscordBotToken(provider: Pick<DiscordCredentialProvider, 'resolve'>): Promise<string | undefined>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { credentialRef } from '@deepseek-ai/dsh-credentials';
|
|
2
|
+
export const DISCORD_BOT_TOKEN_REF = credentialRef('DSH_DISCORD_BOT_TOKEN');
|
|
3
|
+
export async function describeDiscordCredential(provider) {
|
|
4
|
+
const result = await provider.describe(DISCORD_BOT_TOKEN_REF);
|
|
5
|
+
if (result.configured) {
|
|
6
|
+
return {
|
|
7
|
+
configured: true,
|
|
8
|
+
...(result.source === undefined ? {} : { source: result.source }),
|
|
9
|
+
writable: result.writable,
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
// The Host's describe() does not report env-sourced values, while the
|
|
13
|
+
// gateway's resolve() accepts them. Resolve is what connectivity actually
|
|
14
|
+
// uses, so probe it before claiming the credential is absent — otherwise
|
|
15
|
+
// the card shows "not configured" on a connected adapter.
|
|
16
|
+
const resolved = await provider.resolve(DISCORD_BOT_TOKEN_REF).catch(() => undefined);
|
|
17
|
+
if (resolved !== undefined) {
|
|
18
|
+
return { configured: true, source: resolved.source, writable: false };
|
|
19
|
+
}
|
|
20
|
+
return { configured: false, writable: result.writable };
|
|
21
|
+
}
|
|
22
|
+
export async function resolveDiscordBotToken(provider) {
|
|
23
|
+
return (await provider.resolve(DISCORD_BOT_TOKEN_REF))?.value;
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Milestone 1 Discord command contract (design.md §13). One declarative
|
|
3
|
+
* table feeds both the adapter's own routing metadata and the Discord
|
|
4
|
+
* registration payload, so the wire shape cannot drift from the contract:
|
|
5
|
+
* nine fixed commands, guild-only, ephemeral responses, no Discord-level
|
|
6
|
+
* permission gates (adapter RBAC decides at interaction time, deny-first).
|
|
7
|
+
*/
|
|
8
|
+
/** A command argument as the contract table declares it. */
|
|
9
|
+
export interface CommandArgument {
|
|
10
|
+
name: string;
|
|
11
|
+
required: boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Discord sends a type-4 autocomplete interaction while the user types;
|
|
14
|
+
* the adapter answers with live choices (live-candidates pattern).
|
|
15
|
+
*/
|
|
16
|
+
autocomplete?: boolean;
|
|
17
|
+
}
|
|
18
|
+
/** A subcommand of a top-level command. */
|
|
19
|
+
export interface CommandSubcommand {
|
|
20
|
+
name: string;
|
|
21
|
+
options?: CommandArgument[];
|
|
22
|
+
}
|
|
23
|
+
/** One fixed top-level command. */
|
|
24
|
+
export interface AdapterCommand {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
subcommands?: CommandSubcommand[];
|
|
28
|
+
/** Direct options for leaf commands (no subcommands). */
|
|
29
|
+
options?: CommandArgument[];
|
|
30
|
+
/** Milestone 1: guild-only; DMs are unsupported. */
|
|
31
|
+
guildOnly: true;
|
|
32
|
+
/** Milestone 1: every response is ephemeral. */
|
|
33
|
+
responseVisibility: 'ephemeral';
|
|
34
|
+
}
|
|
35
|
+
/** The complete Milestone 1 command set, in registration order. */
|
|
36
|
+
export declare const MILESTONE_ONE_COMMANDS: readonly AdapterCommand[];
|
|
37
|
+
/**
|
|
38
|
+
* Build the bulk-overwrite registration payload for
|
|
39
|
+
* `PUT /applications/{application.id}/commands`.
|
|
40
|
+
* `default_member_permissions: '0'` keeps every command visible: the
|
|
41
|
+
* adapter's own deny-first RBAC authorizes at interaction time, because
|
|
42
|
+
* Discord roles cannot express Workspace-administrator or Host-operator
|
|
43
|
+
* authority.
|
|
44
|
+
*/
|
|
45
|
+
export declare function buildCommandRegistrations(): Array<Record<string, unknown>>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Milestone 1 Discord command contract (design.md §13). One declarative
|
|
3
|
+
* table feeds both the adapter's own routing metadata and the Discord
|
|
4
|
+
* registration payload, so the wire shape cannot drift from the contract:
|
|
5
|
+
* nine fixed commands, guild-only, ephemeral responses, no Discord-level
|
|
6
|
+
* permission gates (adapter RBAC decides at interaction time, deny-first).
|
|
7
|
+
*/
|
|
8
|
+
function leaf(name, description, options) {
|
|
9
|
+
return { name, description, ...(options === undefined ? {} : { options }), guildOnly: true, responseVisibility: 'ephemeral' };
|
|
10
|
+
}
|
|
11
|
+
function grouped(name, description, subcommands) {
|
|
12
|
+
return { name, description, subcommands, guildOnly: true, responseVisibility: 'ephemeral' };
|
|
13
|
+
}
|
|
14
|
+
/** The complete Milestone 1 command set, in registration order. */
|
|
15
|
+
export const MILESTONE_ONE_COMMANDS = [
|
|
16
|
+
grouped('project', 'Bind this channel to a DSH workspace', [
|
|
17
|
+
{ name: 'list', options: [{ name: 'query', required: false, autocomplete: true }] },
|
|
18
|
+
{ name: 'bind', options: [{ name: 'workspace', required: true, autocomplete: true }] },
|
|
19
|
+
{ name: 'info' },
|
|
20
|
+
]),
|
|
21
|
+
grouped('queue', 'Inspect the session inbox queue', [
|
|
22
|
+
{ name: 'list' },
|
|
23
|
+
{ name: 'remove', options: [{ name: 'item', required: true }] },
|
|
24
|
+
]),
|
|
25
|
+
leaf('steer', 'Steer the running turn of this thread', [{ name: 'prompt', required: true }]),
|
|
26
|
+
leaf('stop', 'Cancel the running turn of this thread'),
|
|
27
|
+
grouped('model', 'Show or select the session model', [
|
|
28
|
+
{ name: 'show' },
|
|
29
|
+
{ name: 'select', options: [{ name: 'model', required: true }, { name: 'reasoning', required: false }] },
|
|
30
|
+
]),
|
|
31
|
+
grouped('preset', 'Show, select, or reset this channel\'s agent preset', [
|
|
32
|
+
{ name: 'show' },
|
|
33
|
+
{ name: 'select', options: [{ name: 'preset', required: true }] },
|
|
34
|
+
{ name: 'reset' },
|
|
35
|
+
]),
|
|
36
|
+
grouped('skill', 'Run a DSH skill through the session queue', [
|
|
37
|
+
{ name: 'run', options: [{ name: 'skill', required: true }, { name: 'input', required: false }] },
|
|
38
|
+
]),
|
|
39
|
+
grouped('host', 'Show the connected DSH host status', [
|
|
40
|
+
{ name: 'status' },
|
|
41
|
+
]),
|
|
42
|
+
grouped('guild', 'Guild-scoped adapter operations', [
|
|
43
|
+
{ name: 'forget' },
|
|
44
|
+
]),
|
|
45
|
+
];
|
|
46
|
+
const DISCORD_APPLICATION_COMMAND = 1;
|
|
47
|
+
const DISCORD_SUBCOMMAND = 1;
|
|
48
|
+
const DISCORD_STRING_OPTION = 3;
|
|
49
|
+
/** 0 = interaction context "guild" (Telegram-style contexts list; no DMs). */
|
|
50
|
+
const DISCORD_GUILD_CONTEXT = 0;
|
|
51
|
+
function argumentToWire(option) {
|
|
52
|
+
return {
|
|
53
|
+
type: DISCORD_STRING_OPTION,
|
|
54
|
+
name: option.name,
|
|
55
|
+
description: option.name,
|
|
56
|
+
required: option.required,
|
|
57
|
+
...(option.autocomplete === true ? { autocomplete: true } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Build the bulk-overwrite registration payload for
|
|
62
|
+
* `PUT /applications/{application.id}/commands`.
|
|
63
|
+
* `default_member_permissions: '0'` keeps every command visible: the
|
|
64
|
+
* adapter's own deny-first RBAC authorizes at interaction time, because
|
|
65
|
+
* Discord roles cannot express Workspace-administrator or Host-operator
|
|
66
|
+
* authority.
|
|
67
|
+
*/
|
|
68
|
+
export function buildCommandRegistrations() {
|
|
69
|
+
return MILESTONE_ONE_COMMANDS.map((command) => {
|
|
70
|
+
const options = command.subcommands !== undefined
|
|
71
|
+
? command.subcommands.map(sub => ({
|
|
72
|
+
type: DISCORD_SUBCOMMAND,
|
|
73
|
+
name: sub.name,
|
|
74
|
+
description: `${command.name} ${sub.name}`,
|
|
75
|
+
...(sub.options === undefined ? {} : { options: sub.options.map(argumentToWire) }),
|
|
76
|
+
}))
|
|
77
|
+
: command.options?.map(argumentToWire);
|
|
78
|
+
return {
|
|
79
|
+
type: DISCORD_APPLICATION_COMMAND,
|
|
80
|
+
name: command.name,
|
|
81
|
+
description: command.description,
|
|
82
|
+
contexts: [DISCORD_GUILD_CONTEXT],
|
|
83
|
+
default_member_permissions: '0',
|
|
84
|
+
...(options === undefined || options.length === 0 ? {} : { options }),
|
|
85
|
+
};
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque component-id correlation for Discord components and modals. The
|
|
3
|
+
* custom_id Discord echoes back carries none of the adapter's DSH identifiers
|
|
4
|
+
* — only a registry key — so session, approval, question, and request data
|
|
5
|
+
* never appear on the Discord wire. Resolution returns the exact pending
|
|
6
|
+
* context idempotently until its expiry; actor and business validation stay
|
|
7
|
+
* with the callers, who own what they register.
|
|
8
|
+
*/
|
|
9
|
+
/** Arbitrary JSON-safe correlation context. */
|
|
10
|
+
export type ComponentContext = Record<string, unknown>;
|
|
11
|
+
export interface RegistryOptions {
|
|
12
|
+
/** Injectable opaque-id source for deterministic tests. */
|
|
13
|
+
idFactory?: () => string;
|
|
14
|
+
/** custom_id prefix; Discord allows 100 characters total. */
|
|
15
|
+
prefix?: string;
|
|
16
|
+
}
|
|
17
|
+
export type ComponentResolution = {
|
|
18
|
+
found: true;
|
|
19
|
+
context: ComponentContext;
|
|
20
|
+
} | {
|
|
21
|
+
found: false;
|
|
22
|
+
};
|
|
23
|
+
/** The registry face shared by renderers (register) and click routing (resolve). */
|
|
24
|
+
export type ComponentRegistry = ReturnType<typeof createComponentRegistry>;
|
|
25
|
+
export declare function createComponentRegistry(options?: RegistryOptions): {
|
|
26
|
+
register(context: ComponentContext): string;
|
|
27
|
+
resolve(customId: string, atMs: number): ComponentResolution;
|
|
28
|
+
purgeExpired(atMs: number): number;
|
|
29
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opaque component-id correlation for Discord components and modals. The
|
|
3
|
+
* custom_id Discord echoes back carries none of the adapter's DSH identifiers
|
|
4
|
+
* — only a registry key — so session, approval, question, and request data
|
|
5
|
+
* never appear on the Discord wire. Resolution returns the exact pending
|
|
6
|
+
* context idempotently until its expiry; actor and business validation stay
|
|
7
|
+
* with the callers, who own what they register.
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_PREFIX = 'dc';
|
|
10
|
+
const DISCORD_CUSTOM_ID_MAX = 100;
|
|
11
|
+
export function createComponentRegistry(options = {}) {
|
|
12
|
+
const prefix = options.prefix ?? DEFAULT_PREFIX;
|
|
13
|
+
const nextId = options.idFactory ?? (() => {
|
|
14
|
+
// UnGuessable by default: custom_ids are bearer tokens for interaction
|
|
15
|
+
// routing, so the fallback must not be enumerable. Web crypto is global
|
|
16
|
+
// in Node 22+ and every browser target.
|
|
17
|
+
return globalThis.crypto.randomUUID();
|
|
18
|
+
});
|
|
19
|
+
const entries = new Map();
|
|
20
|
+
function register(context) {
|
|
21
|
+
let customId = `${prefix}:${nextId()}`;
|
|
22
|
+
while (entries.has(customId))
|
|
23
|
+
customId = `${prefix}:${nextId()}`;
|
|
24
|
+
if (customId.length > DISCORD_CUSTOM_ID_MAX) {
|
|
25
|
+
throw new TypeError(`custom_id exceeds Discord's ${String(DISCORD_CUSTOM_ID_MAX)}-character limit`);
|
|
26
|
+
}
|
|
27
|
+
const expiresAtMs = context['expiresAtMs'];
|
|
28
|
+
if (typeof expiresAtMs !== 'number' || !Number.isFinite(expiresAtMs)) {
|
|
29
|
+
throw new TypeError('component context requires a numeric expiresAtMs');
|
|
30
|
+
}
|
|
31
|
+
entries.set(customId, { context, expiresAtMs });
|
|
32
|
+
return customId;
|
|
33
|
+
}
|
|
34
|
+
function resolve(customId, atMs) {
|
|
35
|
+
const entry = entries.get(customId);
|
|
36
|
+
if (entry === undefined)
|
|
37
|
+
return { found: false };
|
|
38
|
+
if (atMs >= entry.expiresAtMs) {
|
|
39
|
+
// Lazy expiry: the first observation past the deadline retires the
|
|
40
|
+
// entry, so a stale clock can never resurrect a consumed control.
|
|
41
|
+
entries.delete(customId);
|
|
42
|
+
return { found: false };
|
|
43
|
+
}
|
|
44
|
+
return { found: true, context: entry.context };
|
|
45
|
+
}
|
|
46
|
+
function purgeExpired(atMs) {
|
|
47
|
+
let purged = 0;
|
|
48
|
+
for (const [customId, entry] of entries) {
|
|
49
|
+
if (atMs >= entry.expiresAtMs) {
|
|
50
|
+
entries.delete(customId);
|
|
51
|
+
purged += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return purged;
|
|
55
|
+
}
|
|
56
|
+
return { register, resolve, purgeExpired };
|
|
57
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-route delivery queues with nonce delivery identity. Writes to one
|
|
3
|
+
* Discord route (a channel, an interaction token) serialize FIFO so edits can
|
|
4
|
+
* never reorder terminal output; distinct routes proceed concurrently. A
|
|
5
|
+
* nonce identifies one in-process delivery claim and cannot be reused while
|
|
6
|
+
* known, which turns a double-send race into a loud local failure instead of
|
|
7
|
+
* duplicate Discord output.
|
|
8
|
+
*/
|
|
9
|
+
/** A unique in-process delivery identity; the durable layer binds it to intent records. */
|
|
10
|
+
export interface DeliveryIdentity {
|
|
11
|
+
nonce: string;
|
|
12
|
+
}
|
|
13
|
+
export interface RouteQueues {
|
|
14
|
+
/**
|
|
15
|
+
* Run `op` serialized after every unsettled op previously enqueued on the
|
|
16
|
+
* same route. Rejects when `identity.nonce` collides with an op that has
|
|
17
|
+
* not settled yet.
|
|
18
|
+
*/
|
|
19
|
+
enqueue<T>(route: string, op: () => Promise<T>, identity: DeliveryIdentity): Promise<T>;
|
|
20
|
+
}
|
|
21
|
+
export declare function createRouteQueues(): RouteQueues;
|