@pellux/goodvibes-tui 0.25.0 → 0.26.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/CHANGELOG.md +5 -3
- package/README.md +1 -1
- package/docs/foundation-artifacts/operator-contract.json +2419 -1040
- package/package.json +2 -2
- package/src/daemon/{calendar → handlers/calendar}/caldav-client.ts +28 -24
- package/src/daemon/handlers/calendar/index.ts +316 -0
- package/src/daemon/handlers/context.ts +29 -0
- package/src/daemon/handlers/contracts.ts +77 -0
- package/src/daemon/{channels → handlers}/drafts/draft-store.ts +114 -50
- package/src/daemon/handlers/drafts/index.ts +17 -0
- package/src/daemon/handlers/drafts/register.ts +331 -0
- package/src/daemon/handlers/email/config.ts +164 -0
- package/src/daemon/handlers/email/index.ts +43 -0
- package/src/daemon/handlers/email/read-handlers.ts +80 -0
- package/src/daemon/handlers/email/runtime.ts +140 -0
- package/src/daemon/handlers/email/validation.ts +147 -0
- package/src/daemon/handlers/email/write-handlers.ts +133 -0
- package/src/daemon/handlers/errors.ts +18 -0
- package/src/daemon/{channels → handlers}/inbox/cursor-store.ts +58 -66
- package/src/daemon/handlers/inbox/index.ts +210 -0
- package/src/daemon/{channels → handlers}/inbox/mapping.ts +8 -6
- package/src/daemon/{channels → handlers}/inbox/poller.ts +4 -4
- package/src/daemon/{channels → handlers}/inbox/provider-adapter.ts +14 -10
- package/src/daemon/{channels → handlers}/inbox/providers/discord.ts +8 -10
- package/src/daemon/{channels → handlers}/inbox/providers/imap-client.ts +1 -1
- package/src/daemon/{channels → handlers}/inbox/providers/route-util.ts +2 -2
- package/src/daemon/{channels → handlers}/inbox/providers/slack.ts +9 -11
- package/src/daemon/handlers/index.ts +107 -0
- package/src/daemon/handlers/register.ts +161 -0
- package/src/daemon/{remote → handlers/remote}/backends/cloud-terminal.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/docker.ts +2 -3
- package/src/daemon/handlers/remote/backends/index.ts +40 -0
- package/src/daemon/{remote → handlers/remote}/backends/process-runner.ts +16 -40
- package/src/daemon/{remote → handlers/remote}/backends/ssh.ts +8 -3
- package/src/daemon/{remote → handlers/remote}/backends/types.ts +29 -3
- package/src/daemon/{remote → handlers/remote}/dispatcher.ts +27 -6
- package/src/daemon/handlers/remote/index.ts +119 -0
- package/src/daemon/{remote → handlers/remote}/peer-registry.ts +47 -11
- package/src/daemon/handlers/remote/service.ts +191 -0
- package/src/daemon/{channels → handlers}/routing/inbox-bridge.ts +33 -20
- package/src/daemon/handlers/routing/index.ts +261 -0
- package/src/daemon/{channels → handlers}/routing/route-store.ts +57 -16
- package/src/daemon/{channels → handlers}/routing/routing-resolver.ts +1 -1
- package/src/daemon/{operator → handlers}/sqlite-store.ts +5 -5
- package/src/daemon/handlers/triage/index.ts +57 -0
- package/src/daemon/handlers/triage/integration.ts +212 -0
- package/src/daemon/{triage → handlers/triage}/pipeline.ts +58 -70
- package/src/daemon/{triage → handlers/triage}/scorer.ts +21 -21
- package/src/daemon/handlers/triage/tagger/discord.ts +186 -0
- package/src/daemon/handlers/triage/tagger/imap.ts +383 -0
- package/src/daemon/handlers/triage/tagger/index.ts +184 -0
- package/src/daemon/handlers/triage/tagger/shared.ts +70 -0
- package/src/daemon/handlers/triage/tagger/slack.ts +69 -0
- package/src/daemon/handlers/triage/types.ts +50 -0
- package/src/runtime/services.ts +48 -35
- package/src/version.ts +1 -1
- package/src/daemon/calendar/index.ts +0 -52
- package/src/daemon/calendar/register.ts +0 -527
- package/src/daemon/channels/drafts/index.ts +0 -22
- package/src/daemon/channels/drafts/register.ts +0 -449
- package/src/daemon/channels/inbox/index.ts +0 -58
- package/src/daemon/channels/inbox/register.ts +0 -247
- package/src/daemon/channels/routing/index.ts +0 -39
- package/src/daemon/channels/routing/register.ts +0 -296
- package/src/daemon/email/index.ts +0 -68
- package/src/daemon/email/register.ts +0 -715
- package/src/daemon/operator/index.ts +0 -43
- package/src/daemon/operator/register-helper.ts +0 -150
- package/src/daemon/operator/surfaces.ts +0 -137
- package/src/daemon/operator/types.ts +0 -207
- package/src/daemon/remote/backends/index.ts +0 -34
- package/src/daemon/remote/index.ts +0 -74
- package/src/daemon/remote/register.ts +0 -411
- package/src/daemon/triage/index.ts +0 -59
- package/src/daemon/triage/integration.ts +0 -179
- package/src/daemon/triage/register.ts +0 -231
- package/src/daemon/triage/tagger.ts +0 -777
- /package/src/daemon/{calendar → handlers/calendar}/ics.ts +0 -0
- /package/src/daemon/{operator/credential-store.ts → handlers/credentials.ts} +0 -0
- /package/src/daemon/{email → handlers/email}/imap-connector.ts +0 -0
- /package/src/daemon/{email → handlers/email}/imap-parsing.ts +0 -0
- /package/src/daemon/{email → handlers/email}/smtp-connector.ts +0 -0
- /package/src/daemon/{channels → handlers}/inbox/providers/email.ts +0 -0
- /package/src/daemon/{remote → handlers/remote}/backends/local-process.ts +0 -0
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Surface register: `channels.inbox.list`.
|
|
3
|
-
//
|
|
4
|
-
// register(ctx) =>
|
|
5
|
-
// 1. registers built-in provider adapter factories (slack/discord/email)
|
|
6
|
-
// 2. constructs adapters with a daemon credential store
|
|
7
|
-
// 3. opens the inbox cursor store and seeds an initial poll
|
|
8
|
-
// 4. starts the per-provider polling loops
|
|
9
|
-
// 5. publishes the read-only `channels.inbox.list` operator method
|
|
10
|
-
// 6. returns an Unregister that stops the poller, closes the store, and
|
|
11
|
-
// removes the method.
|
|
12
|
-
//
|
|
13
|
-
// `channels.inbox.list` is READ-ONLY (no confirm). It reads the persisted feed
|
|
14
|
-
// (filtered by providers/limit/since) and reports per-provider state from the
|
|
15
|
-
// live poller status. nextSince advances monotonically (max receivedAt in the
|
|
16
|
-
// returned window, never below the requested `since`).
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
createDaemonCredentialStore,
|
|
21
|
-
declareOperatorMethod,
|
|
22
|
-
} from '../../operator/index.ts';
|
|
23
|
-
import type {
|
|
24
|
-
OperatorContext,
|
|
25
|
-
OperatorInvocation,
|
|
26
|
-
SurfaceRegister,
|
|
27
|
-
Unregister,
|
|
28
|
-
} from '../../operator/index.ts';
|
|
29
|
-
import {
|
|
30
|
-
buildAdapters,
|
|
31
|
-
registerAdapterFactory,
|
|
32
|
-
type AdapterContext,
|
|
33
|
-
type InboundChannelItem,
|
|
34
|
-
type RouteResolver,
|
|
35
|
-
} from './provider-adapter.ts';
|
|
36
|
-
import { InboxCursorStore } from './cursor-store.ts';
|
|
37
|
-
import { InboundPoller, type ProviderStatus } from './poller.ts';
|
|
38
|
-
import { createSlackAdapter, SLACK_PROVIDER_ID } from './providers/slack.ts';
|
|
39
|
-
import { createDiscordAdapter, DISCORD_PROVIDER_ID } from './providers/discord.ts';
|
|
40
|
-
import { createEmailAdapter, EMAIL_PROVIDER_ID } from './providers/email.ts';
|
|
41
|
-
|
|
42
|
-
export const INBOX_LIST_METHOD_ID = 'channels.inbox.list';
|
|
43
|
-
export const INBOX_LIST_SCOPES = ['channels:inbox:read'];
|
|
44
|
-
const DEFAULT_LIMIT = 50;
|
|
45
|
-
const MAX_LIMIT = 500;
|
|
46
|
-
|
|
47
|
-
export interface InboxListInput {
|
|
48
|
-
providers?: string[];
|
|
49
|
-
limit?: number;
|
|
50
|
-
since?: number;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export interface InboxProviderReport {
|
|
54
|
-
id: string;
|
|
55
|
-
state: ProviderStatus['state'];
|
|
56
|
-
itemCount: number;
|
|
57
|
-
error?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface InboxListOutput {
|
|
61
|
-
items: InboundChannelItem[];
|
|
62
|
-
nextSince: number;
|
|
63
|
-
providers: InboxProviderReport[];
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface RegisterInboxOptions {
|
|
67
|
-
/** Route resolver injected by the routing surface (optional, best-effort). */
|
|
68
|
-
resolveRouteId?: RouteResolver;
|
|
69
|
-
/** Override the cursor-store filename (tests). */
|
|
70
|
-
storeFileName?: string;
|
|
71
|
-
/** Skip the initial seed poll (tests that drive polling manually). */
|
|
72
|
-
skipInitialPoll?: boolean;
|
|
73
|
-
/** Replace the default built-in adapter set (tests). */
|
|
74
|
-
registerBuiltins?: boolean;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function registerBuiltinAdapters(): void {
|
|
78
|
-
registerAdapterFactory(SLACK_PROVIDER_ID, (ctx) => createSlackAdapter(ctx));
|
|
79
|
-
registerAdapterFactory(DISCORD_PROVIDER_ID, (ctx) => createDiscordAdapter(ctx));
|
|
80
|
-
registerAdapterFactory(EMAIL_PROVIDER_ID, (ctx) => createEmailAdapter(ctx));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const INBOX_LIST_OUTPUT_SCHEMA: Record<string, unknown> = {
|
|
84
|
-
type: 'object',
|
|
85
|
-
required: ['items', 'nextSince', 'providers'],
|
|
86
|
-
properties: {
|
|
87
|
-
items: {
|
|
88
|
-
type: 'array',
|
|
89
|
-
items: {
|
|
90
|
-
type: 'object',
|
|
91
|
-
required: [
|
|
92
|
-
'id', 'provider', 'kind', 'fromDigest',
|
|
93
|
-
'subjectPreview', 'bodyPreview', 'receivedAt', 'unread',
|
|
94
|
-
],
|
|
95
|
-
properties: {
|
|
96
|
-
id: { type: 'string' },
|
|
97
|
-
provider: { type: 'string' },
|
|
98
|
-
kind: { type: 'string', enum: ['dm', 'thread', 'mention', 'reaction'] },
|
|
99
|
-
fromDigest: { type: 'string' },
|
|
100
|
-
subjectPreview: { type: 'string', maxLength: 200 },
|
|
101
|
-
bodyPreview: { type: 'string', maxLength: 500 },
|
|
102
|
-
routeId: { type: 'string' },
|
|
103
|
-
receivedAt: { type: 'number' },
|
|
104
|
-
unread: { type: 'boolean' },
|
|
105
|
-
triageScore: { type: 'number' },
|
|
106
|
-
triageTags: { type: 'array', items: { type: 'string' } },
|
|
107
|
-
},
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
nextSince: { type: 'number' },
|
|
111
|
-
providers: {
|
|
112
|
-
type: 'array',
|
|
113
|
-
items: {
|
|
114
|
-
type: 'object',
|
|
115
|
-
required: ['id', 'state', 'itemCount'],
|
|
116
|
-
properties: {
|
|
117
|
-
id: { type: 'string' },
|
|
118
|
-
state: { type: 'string', enum: ['ready', 'unavailable', 'empty'] },
|
|
119
|
-
itemCount: { type: 'number' },
|
|
120
|
-
error: { type: 'string' },
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
},
|
|
124
|
-
},
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const INBOX_LIST_INPUT_SCHEMA: Record<string, unknown> = {
|
|
128
|
-
type: 'object',
|
|
129
|
-
properties: {
|
|
130
|
-
providers: { type: 'array', items: { type: 'string' } },
|
|
131
|
-
limit: { type: 'number', minimum: 1, maximum: MAX_LIMIT },
|
|
132
|
-
since: { type: 'number', minimum: 0 },
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
function normalizeInput(body: unknown): { providers?: string[]; limit: number; since?: number } {
|
|
137
|
-
const input = (body ?? {}) as InboxListInput;
|
|
138
|
-
const providers = Array.isArray(input.providers)
|
|
139
|
-
? input.providers.filter((p): p is string => typeof p === 'string' && p.length > 0)
|
|
140
|
-
: undefined;
|
|
141
|
-
let limit = typeof input.limit === 'number' && Number.isFinite(input.limit)
|
|
142
|
-
? Math.floor(input.limit)
|
|
143
|
-
: DEFAULT_LIMIT;
|
|
144
|
-
limit = Math.min(Math.max(1, limit), MAX_LIMIT);
|
|
145
|
-
const since = typeof input.since === 'number' && Number.isFinite(input.since) && input.since >= 0
|
|
146
|
-
? Math.floor(input.since)
|
|
147
|
-
: undefined;
|
|
148
|
-
return {
|
|
149
|
-
...(providers && providers.length > 0 ? { providers } : {}),
|
|
150
|
-
limit,
|
|
151
|
-
...(since !== undefined ? { since } : {}),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Register the inbox surface. Returns an Unregister that tears down the poller,
|
|
157
|
-
* closes the store, and removes the operator method.
|
|
158
|
-
*/
|
|
159
|
-
export function registerInboxMethods(
|
|
160
|
-
ctx: OperatorContext,
|
|
161
|
-
options: RegisterInboxOptions = {},
|
|
162
|
-
): Unregister {
|
|
163
|
-
if (options.registerBuiltins !== false) {
|
|
164
|
-
registerBuiltinAdapters();
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
const credentials = createDaemonCredentialStore(ctx.secrets);
|
|
168
|
-
const adapterContext: AdapterContext = {
|
|
169
|
-
credentials,
|
|
170
|
-
logger: ctx.logger,
|
|
171
|
-
...(options.resolveRouteId ? { resolveRouteId: options.resolveRouteId } : {}),
|
|
172
|
-
};
|
|
173
|
-
const adapters = buildAdapters(adapterContext);
|
|
174
|
-
const store = new InboxCursorStore(ctx.workingDirectory, options.storeFileName);
|
|
175
|
-
const poller = new InboundPoller({ adapters, store, logger: ctx.logger });
|
|
176
|
-
|
|
177
|
-
// Async bootstrap: init store, seed one poll, start loops. Failures are
|
|
178
|
-
// logged but never thrown out of register() — the method still serves the
|
|
179
|
-
// (possibly empty) persisted feed and reports provider states.
|
|
180
|
-
const ready: Promise<void> = (async () => {
|
|
181
|
-
await store.init();
|
|
182
|
-
if (!options.skipInitialPoll) {
|
|
183
|
-
await poller.pollOnce();
|
|
184
|
-
}
|
|
185
|
-
poller.start();
|
|
186
|
-
})().catch((error: unknown) => {
|
|
187
|
-
ctx.logger.error('inbox surface bootstrap failed', {
|
|
188
|
-
error: error instanceof Error ? error.message : String(error),
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
const handler = async (
|
|
193
|
-
invocation: OperatorInvocation<InboxListInput>,
|
|
194
|
-
): Promise<InboxListOutput> => {
|
|
195
|
-
await ready;
|
|
196
|
-
const { providers, limit, since } = normalizeInput(invocation.body);
|
|
197
|
-
const items = store.listItems({ ...(providers ? { providers } : {}), ...(since !== undefined ? { since } : {}), limit });
|
|
198
|
-
const maxReceived = store.maxReceivedAt(providers);
|
|
199
|
-
const nextSince = Math.max(since ?? 0, maxReceived);
|
|
200
|
-
const statuses = poller.snapshotStatuses(providers);
|
|
201
|
-
const reports: InboxProviderReport[] = statuses.map((status) => ({
|
|
202
|
-
id: status.id,
|
|
203
|
-
state: status.state,
|
|
204
|
-
itemCount: status.itemCount,
|
|
205
|
-
...(status.error ? { error: status.error } : {}),
|
|
206
|
-
}));
|
|
207
|
-
return { items, nextSince, providers: reports };
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
const unregisterMethod = declareOperatorMethod<InboxListInput, InboxListOutput>(
|
|
211
|
-
ctx,
|
|
212
|
-
{
|
|
213
|
-
id: INBOX_LIST_METHOD_ID,
|
|
214
|
-
title: 'List inbound channel items',
|
|
215
|
-
description:
|
|
216
|
-
'Aggregated, deduplicated inbound feed (DMs/threads/mentions) across '
|
|
217
|
-
+ 'configured providers. Read-only; advances a monotonic nextSince cursor.',
|
|
218
|
-
category: 'channels',
|
|
219
|
-
source: 'daemon',
|
|
220
|
-
access: 'operator',
|
|
221
|
-
transport: ['ws', 'internal'],
|
|
222
|
-
scopes: INBOX_LIST_SCOPES,
|
|
223
|
-
effect: 'read-only',
|
|
224
|
-
confirm: false,
|
|
225
|
-
inputSchema: INBOX_LIST_INPUT_SCHEMA,
|
|
226
|
-
outputSchema: INBOX_LIST_OUTPUT_SCHEMA,
|
|
227
|
-
},
|
|
228
|
-
handler,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
return () => {
|
|
232
|
-
try {
|
|
233
|
-
unregisterMethod();
|
|
234
|
-
} finally {
|
|
235
|
-
poller.stop();
|
|
236
|
-
void store.close();
|
|
237
|
-
}
|
|
238
|
-
};
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* SurfaceRegister contract entry point. Integration wires the inbox surface by
|
|
243
|
-
* calling this from its surface bootstrap (the same mechanism every other
|
|
244
|
-
* daemon surface uses, e.g. routing/triage), retaining the returned Unregister
|
|
245
|
-
* for teardown. This is the production caller of `registerInboxMethods`.
|
|
246
|
-
*/
|
|
247
|
-
export const register: SurfaceRegister = (ctx) => registerInboxMethods(ctx);
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// Channel-to-Profile Routing Control Plane — surface barrel.
|
|
2
|
-
//
|
|
3
|
-
// Integration wires this surface by calling `register(ctx)` (or
|
|
4
|
-
// `registerRoutingMethods(ctx)` when it needs the returned store/resolver).
|
|
5
|
-
// The inbox surface reuses `resolveProfile` / `createRoutingResolver` for
|
|
6
|
-
// identical online/offline routing resolution.
|
|
7
|
-
|
|
8
|
-
export { register, registerRoutingMethods } from './register.ts';
|
|
9
|
-
export type {
|
|
10
|
-
RoutingRegistration,
|
|
11
|
-
RoutingListInput,
|
|
12
|
-
RoutingListOutput,
|
|
13
|
-
RoutingAssignInput,
|
|
14
|
-
RoutingAssignOutput,
|
|
15
|
-
RoutingDeleteInput,
|
|
16
|
-
RoutingDeleteOutput,
|
|
17
|
-
} from './register.ts';
|
|
18
|
-
|
|
19
|
-
export {
|
|
20
|
-
RouteStore,
|
|
21
|
-
parseChannelId,
|
|
22
|
-
buildChannelId,
|
|
23
|
-
} from './route-store.ts';
|
|
24
|
-
export type {
|
|
25
|
-
RoutingChannelRoute,
|
|
26
|
-
ParsedChannelId,
|
|
27
|
-
RouteUpsertInput,
|
|
28
|
-
RouteUpsertResult,
|
|
29
|
-
RouteListFilter,
|
|
30
|
-
} from './route-store.ts';
|
|
31
|
-
|
|
32
|
-
export {
|
|
33
|
-
resolveProfile,
|
|
34
|
-
createRoutingResolver,
|
|
35
|
-
WILDCARD_SURFACE,
|
|
36
|
-
} from './routing-resolver.ts';
|
|
37
|
-
export type { RoutingResolver } from './routing-resolver.ts';
|
|
38
|
-
|
|
39
|
-
export { createInboxRouteResolver } from './inbox-bridge.ts';
|
|
@@ -1,296 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
declareOperatorMethods,
|
|
3
|
-
OperatorError,
|
|
4
|
-
type OperatorContext,
|
|
5
|
-
type OperatorHandler,
|
|
6
|
-
type OperatorMethodDescriptor,
|
|
7
|
-
type SurfaceRegister,
|
|
8
|
-
type Unregister,
|
|
9
|
-
} from '../../operator/index.ts';
|
|
10
|
-
import { RouteStore, type RoutingChannelRoute } from './route-store.ts';
|
|
11
|
-
import { createRoutingResolver, type RoutingResolver } from './routing-resolver.ts';
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Wire contracts (request/response bodies) for the channels.routing.* methods.
|
|
15
|
-
//
|
|
16
|
-
// These interfaces and the JSON Schemas below mirror the handoff's literal
|
|
17
|
-
// Input/Output blocks exactly. Per the handoff's "Confirmation / Effect
|
|
18
|
-
// Semantics" note, `channels.routing.assign` and `channels.routing.delete` are
|
|
19
|
-
// control-plane mutations that additionally require confirmation. That
|
|
20
|
-
// confirmation is NOT part of the documented Input body shape: it is a
|
|
21
|
-
// framework confirm-guard envelope flag (`confirm: true` on the raw request
|
|
22
|
-
// body, plus `explicitUserRequest` in the call metadata) enforced by
|
|
23
|
-
// `assertConfirmed` inside `declareOperatorMethods` when `descriptor.confirm`
|
|
24
|
-
// is set. We therefore keep `confirm` out of the advertised contract here and
|
|
25
|
-
// rely on the framework guard, so the wire Input stays a faithful match for the
|
|
26
|
-
// handoff rather than a strict superset.
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
export interface RoutingListInput {
|
|
30
|
-
profileId?: string;
|
|
31
|
-
surfaceKind?: string;
|
|
32
|
-
}
|
|
33
|
-
export interface RoutingListOutput {
|
|
34
|
-
routes: RoutingChannelRoute[];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface RoutingAssignInput {
|
|
38
|
-
channelId: string;
|
|
39
|
-
profileId: string;
|
|
40
|
-
label?: string;
|
|
41
|
-
}
|
|
42
|
-
export interface RoutingAssignOutput {
|
|
43
|
-
assignmentId: string;
|
|
44
|
-
channelId: string;
|
|
45
|
-
profileId: string;
|
|
46
|
-
created: boolean;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface RoutingDeleteInput {
|
|
50
|
-
assignmentId: string;
|
|
51
|
-
}
|
|
52
|
-
export interface RoutingDeleteOutput {
|
|
53
|
-
deleted: boolean;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function asObject(body: unknown): Record<string, unknown> {
|
|
57
|
-
return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function optionalString(value: unknown): string | undefined {
|
|
61
|
-
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
// JSON Schemas (advertised on the catalog descriptors).
|
|
66
|
-
// ---------------------------------------------------------------------------
|
|
67
|
-
|
|
68
|
-
const CHANNEL_ROUTE_SCHEMA: Record<string, unknown> = {
|
|
69
|
-
type: 'object',
|
|
70
|
-
required: ['assignmentId', 'channelId', 'surfaceKind', 'profileId', 'createdAt', 'updatedAt'],
|
|
71
|
-
properties: {
|
|
72
|
-
assignmentId: { type: 'string' },
|
|
73
|
-
channelId: { type: 'string' },
|
|
74
|
-
surfaceKind: { type: 'string' },
|
|
75
|
-
routeId: { type: 'string' },
|
|
76
|
-
profileId: { type: 'string' },
|
|
77
|
-
label: { type: 'string' },
|
|
78
|
-
createdAt: { type: 'string' },
|
|
79
|
-
updatedAt: { type: 'string' },
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
const LIST_INPUT_SCHEMA: Record<string, unknown> = {
|
|
84
|
-
type: 'object',
|
|
85
|
-
properties: {
|
|
86
|
-
profileId: { type: 'string' },
|
|
87
|
-
surfaceKind: { type: 'string' },
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
const LIST_OUTPUT_SCHEMA: Record<string, unknown> = {
|
|
92
|
-
type: 'object',
|
|
93
|
-
required: ['routes'],
|
|
94
|
-
properties: {
|
|
95
|
-
routes: { type: 'array', items: CHANNEL_ROUTE_SCHEMA },
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const ASSIGN_INPUT_SCHEMA: Record<string, unknown> = {
|
|
100
|
-
type: 'object',
|
|
101
|
-
required: ['channelId', 'profileId'],
|
|
102
|
-
properties: {
|
|
103
|
-
channelId: { type: 'string' },
|
|
104
|
-
profileId: { type: 'string' },
|
|
105
|
-
label: { type: 'string' },
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
|
|
109
|
-
const ASSIGN_OUTPUT_SCHEMA: Record<string, unknown> = {
|
|
110
|
-
type: 'object',
|
|
111
|
-
required: ['assignmentId', 'channelId', 'profileId', 'created'],
|
|
112
|
-
properties: {
|
|
113
|
-
assignmentId: { type: 'string' },
|
|
114
|
-
channelId: { type: 'string' },
|
|
115
|
-
profileId: { type: 'string' },
|
|
116
|
-
created: { type: 'boolean' },
|
|
117
|
-
},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
const DELETE_INPUT_SCHEMA: Record<string, unknown> = {
|
|
121
|
-
type: 'object',
|
|
122
|
-
required: ['assignmentId'],
|
|
123
|
-
properties: {
|
|
124
|
-
assignmentId: { type: 'string' },
|
|
125
|
-
},
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const DELETE_OUTPUT_SCHEMA: Record<string, unknown> = {
|
|
129
|
-
type: 'object',
|
|
130
|
-
required: ['deleted'],
|
|
131
|
-
properties: {
|
|
132
|
-
deleted: { type: 'boolean' },
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
// ---------------------------------------------------------------------------
|
|
137
|
-
// Registration
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Handle returned by {@link registerRoutingMethods}: tears down the catalog
|
|
142
|
-
* methods and closes the underlying SQLite store. Also exposes the resolver so
|
|
143
|
-
* integration / the inbox surface can reuse routing resolution without going
|
|
144
|
-
* through the catalog.
|
|
145
|
-
*/
|
|
146
|
-
export interface RoutingRegistration extends Unregister {
|
|
147
|
-
(): void;
|
|
148
|
-
readonly store: RouteStore;
|
|
149
|
-
readonly resolver: RoutingResolver;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Register the channel-to-profile routing control-plane methods against the
|
|
154
|
-
* operator catalog:
|
|
155
|
-
* - channels.routing.list (read-only)
|
|
156
|
-
* - channels.routing.assign (confirmed local-state mutation)
|
|
157
|
-
* - channels.routing.delete (confirmed local-state mutation)
|
|
158
|
-
*
|
|
159
|
-
* The store is lazily initialized on first method invocation (the SurfaceRegister
|
|
160
|
-
* contract is synchronous), so registration never blocks on disk I/O.
|
|
161
|
-
*/
|
|
162
|
-
export function registerRoutingMethods(ctx: OperatorContext): RoutingRegistration {
|
|
163
|
-
const store = new RouteStore({ workingDirectory: ctx.workingDirectory });
|
|
164
|
-
const resolver = createRoutingResolver(store);
|
|
165
|
-
|
|
166
|
-
let initPromise: Promise<void> | null = null;
|
|
167
|
-
const ensureInit = async (): Promise<void> => {
|
|
168
|
-
if (!initPromise) {
|
|
169
|
-
initPromise = store.init().catch((error) => {
|
|
170
|
-
// Allow a later invocation to retry initialization.
|
|
171
|
-
initPromise = null;
|
|
172
|
-
throw error;
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
await initPromise;
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const listHandler: OperatorHandler<RoutingListInput, RoutingListOutput> = async (input) => {
|
|
179
|
-
await ensureInit();
|
|
180
|
-
const body = asObject(input.body);
|
|
181
|
-
const routes = store.list({
|
|
182
|
-
profileId: optionalString(body.profileId),
|
|
183
|
-
surfaceKind: optionalString(body.surfaceKind),
|
|
184
|
-
});
|
|
185
|
-
return { routes };
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const assignHandler: OperatorHandler<RoutingAssignInput, RoutingAssignOutput> = async (input) => {
|
|
189
|
-
await ensureInit();
|
|
190
|
-
const body = asObject(input.body);
|
|
191
|
-
const channelId = optionalString(body.channelId);
|
|
192
|
-
const profileId = optionalString(body.profileId);
|
|
193
|
-
if (channelId === undefined) {
|
|
194
|
-
throw new OperatorError('channelId is required', 'ROUTING_INVALID_CHANNEL_ID', 400);
|
|
195
|
-
}
|
|
196
|
-
if (profileId === undefined) {
|
|
197
|
-
throw new OperatorError('profileId is required', 'ROUTING_INVALID_PROFILE_ID', 400);
|
|
198
|
-
}
|
|
199
|
-
const { route, created } = await store.upsert({
|
|
200
|
-
channelId,
|
|
201
|
-
profileId,
|
|
202
|
-
label: optionalString(body.label),
|
|
203
|
-
});
|
|
204
|
-
ctx.logger.info('channels.routing.assign', {
|
|
205
|
-
assignmentId: route.assignmentId,
|
|
206
|
-
channelId: route.channelId,
|
|
207
|
-
profileId: route.profileId,
|
|
208
|
-
created,
|
|
209
|
-
principalId: input.context.principalId,
|
|
210
|
-
});
|
|
211
|
-
return {
|
|
212
|
-
assignmentId: route.assignmentId,
|
|
213
|
-
channelId: route.channelId,
|
|
214
|
-
profileId: route.profileId,
|
|
215
|
-
created,
|
|
216
|
-
};
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const deleteHandler: OperatorHandler<RoutingDeleteInput, RoutingDeleteOutput> = async (input) => {
|
|
220
|
-
await ensureInit();
|
|
221
|
-
const body = asObject(input.body);
|
|
222
|
-
const assignmentId = optionalString(body.assignmentId);
|
|
223
|
-
if (assignmentId === undefined) {
|
|
224
|
-
throw new OperatorError('assignmentId is required', 'ROUTING_INVALID_ASSIGNMENT_ID', 400);
|
|
225
|
-
}
|
|
226
|
-
const deleted = await store.delete(assignmentId);
|
|
227
|
-
ctx.logger.info('channels.routing.delete', {
|
|
228
|
-
assignmentId,
|
|
229
|
-
deleted,
|
|
230
|
-
principalId: input.context.principalId,
|
|
231
|
-
});
|
|
232
|
-
return { deleted };
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const listDescriptor: OperatorMethodDescriptor = {
|
|
236
|
-
id: 'channels.routing.list',
|
|
237
|
-
title: 'List channel routes',
|
|
238
|
-
description: 'List channel-to-profile routing assignments, optionally filtered by profile or surface.',
|
|
239
|
-
category: 'channels',
|
|
240
|
-
source: 'daemon',
|
|
241
|
-
access: 'authenticated',
|
|
242
|
-
transport: ['ws', 'internal'],
|
|
243
|
-
scopes: ['channels:routing:read'],
|
|
244
|
-
effect: 'read-only',
|
|
245
|
-
inputSchema: LIST_INPUT_SCHEMA,
|
|
246
|
-
outputSchema: LIST_OUTPUT_SCHEMA,
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
const assignDescriptor: OperatorMethodDescriptor = {
|
|
250
|
-
id: 'channels.routing.assign',
|
|
251
|
-
title: 'Assign channel to profile',
|
|
252
|
-
description: 'Create or update the profile that handles inbound messages on a channel.',
|
|
253
|
-
category: 'channels',
|
|
254
|
-
source: 'daemon',
|
|
255
|
-
access: 'authenticated',
|
|
256
|
-
transport: ['ws', 'internal'],
|
|
257
|
-
scopes: ['channels:routing:write'],
|
|
258
|
-
effect: 'local-state-mutation',
|
|
259
|
-
confirm: true,
|
|
260
|
-
inputSchema: ASSIGN_INPUT_SCHEMA,
|
|
261
|
-
outputSchema: ASSIGN_OUTPUT_SCHEMA,
|
|
262
|
-
};
|
|
263
|
-
|
|
264
|
-
const deleteDescriptor: OperatorMethodDescriptor = {
|
|
265
|
-
id: 'channels.routing.delete',
|
|
266
|
-
title: 'Delete channel route',
|
|
267
|
-
description: 'Remove a channel-to-profile routing assignment by assignmentId.',
|
|
268
|
-
category: 'channels',
|
|
269
|
-
source: 'daemon',
|
|
270
|
-
access: 'authenticated',
|
|
271
|
-
transport: ['ws', 'internal'],
|
|
272
|
-
scopes: ['channels:routing:write'],
|
|
273
|
-
effect: 'local-state-mutation',
|
|
274
|
-
confirm: true,
|
|
275
|
-
inputSchema: DELETE_INPUT_SCHEMA,
|
|
276
|
-
outputSchema: DELETE_OUTPUT_SCHEMA,
|
|
277
|
-
};
|
|
278
|
-
|
|
279
|
-
const unregister = declareOperatorMethods(ctx, [
|
|
280
|
-
{ descriptor: listDescriptor, handler: listHandler as OperatorHandler<unknown, unknown> },
|
|
281
|
-
{ descriptor: assignDescriptor, handler: assignHandler as OperatorHandler<unknown, unknown> },
|
|
282
|
-
{ descriptor: deleteDescriptor, handler: deleteHandler as OperatorHandler<unknown, unknown> },
|
|
283
|
-
]);
|
|
284
|
-
|
|
285
|
-
// Build the callable teardown handle, then attach the store/resolver as
|
|
286
|
-
// read-only properties so callers (integration, the inbox surface) can reuse
|
|
287
|
-
// routing resolution without going through the catalog.
|
|
288
|
-
const teardown = (): void => {
|
|
289
|
-
unregister();
|
|
290
|
-
store.close();
|
|
291
|
-
};
|
|
292
|
-
return Object.assign(teardown, { store, resolver }) as RoutingRegistration;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/** SurfaceRegister contract entry point (integration calls this). */
|
|
296
|
-
export const register: SurfaceRegister = (ctx) => registerRoutingMethods(ctx);
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Email operator-method surface barrel.
|
|
3
|
-
//
|
|
4
|
-
// Integration wires this surface by calling registerEmailMethods(ctx) once and
|
|
5
|
-
// retaining the returned Unregister. Connectors and pure helpers are re-exported
|
|
6
|
-
// for daemon-internal reuse and unit testing.
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
export { registerEmailMethods, resolveEmailSettings } from './register.ts';
|
|
10
|
-
export type {
|
|
11
|
-
ResolvedEmailSettings,
|
|
12
|
-
EmailMethodsOptions,
|
|
13
|
-
ImapClient,
|
|
14
|
-
SmtpClient,
|
|
15
|
-
} from './register.ts';
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
ImapConnector,
|
|
19
|
-
ImapError,
|
|
20
|
-
toImapSearchDate,
|
|
21
|
-
quoteImapString,
|
|
22
|
-
parseSearchUids,
|
|
23
|
-
parseAppendUid,
|
|
24
|
-
parseEnvelope,
|
|
25
|
-
parseFetchSummaries,
|
|
26
|
-
parseFullMessage,
|
|
27
|
-
parseAddressList,
|
|
28
|
-
parseMimeMessage,
|
|
29
|
-
splitHeadersBody,
|
|
30
|
-
tokenizeParen,
|
|
31
|
-
extractParenValue,
|
|
32
|
-
extractLiteralFor,
|
|
33
|
-
unescapeImapString,
|
|
34
|
-
decodeTransferEncoding,
|
|
35
|
-
decodeQuotedPrintable,
|
|
36
|
-
decodeMimeWords,
|
|
37
|
-
stripHtml,
|
|
38
|
-
collapseWhitespace,
|
|
39
|
-
} from './imap-connector.ts';
|
|
40
|
-
export type {
|
|
41
|
-
ImapConnectionSettings,
|
|
42
|
-
ImapEnvelopeSummary,
|
|
43
|
-
ImapFullMessage,
|
|
44
|
-
ImapAttachmentSummary,
|
|
45
|
-
ImapListOptions,
|
|
46
|
-
ImapAppendResult,
|
|
47
|
-
ParsedMime,
|
|
48
|
-
} from './imap-connector.ts';
|
|
49
|
-
|
|
50
|
-
export {
|
|
51
|
-
SmtpConnector,
|
|
52
|
-
SmtpError,
|
|
53
|
-
extractCompleteReply,
|
|
54
|
-
extractAddress,
|
|
55
|
-
parseRecipients,
|
|
56
|
-
dotStuff,
|
|
57
|
-
generateMessageId,
|
|
58
|
-
buildRfc5322Message,
|
|
59
|
-
encodeHeaderValue,
|
|
60
|
-
formatRfc2822Date,
|
|
61
|
-
encodeQuotedPrintable,
|
|
62
|
-
} from './smtp-connector.ts';
|
|
63
|
-
export type {
|
|
64
|
-
SmtpConnectionSettings,
|
|
65
|
-
SmtpMessage,
|
|
66
|
-
SmtpSendResult,
|
|
67
|
-
Rfc5322Parts,
|
|
68
|
-
} from './smtp-connector.ts';
|