@krischoichoi/channel-dingtalk 0.6.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.
Files changed (70) hide show
  1. package/README.md +139 -0
  2. package/lib/adapter.d.ts +123 -0
  3. package/lib/adapter.d.ts.map +1 -0
  4. package/lib/adapter.js +355 -0
  5. package/lib/adapter.js.map +1 -0
  6. package/lib/ai-card.d.ts +64 -0
  7. package/lib/ai-card.d.ts.map +1 -0
  8. package/lib/ai-card.js +150 -0
  9. package/lib/ai-card.js.map +1 -0
  10. package/lib/auth/device-registration.d.ts +18 -0
  11. package/lib/auth/device-registration.d.ts.map +1 -0
  12. package/lib/auth/device-registration.js +147 -0
  13. package/lib/auth/device-registration.js.map +1 -0
  14. package/lib/config.d.ts +119 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +75 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/definition.d.ts +53 -0
  19. package/lib/definition.d.ts.map +1 -0
  20. package/lib/definition.js +227 -0
  21. package/lib/definition.js.map +1 -0
  22. package/lib/image-hydrator.d.ts +213 -0
  23. package/lib/image-hydrator.d.ts.map +1 -0
  24. package/lib/image-hydrator.js +244 -0
  25. package/lib/image-hydrator.js.map +1 -0
  26. package/lib/inbound.d.ts +38 -0
  27. package/lib/inbound.d.ts.map +1 -0
  28. package/lib/inbound.js +158 -0
  29. package/lib/inbound.js.map +1 -0
  30. package/lib/index.d.ts +49 -0
  31. package/lib/index.d.ts.map +1 -0
  32. package/lib/index.js +123 -0
  33. package/lib/index.js.map +1 -0
  34. package/lib/manifest.d.ts +23 -0
  35. package/lib/manifest.d.ts.map +1 -0
  36. package/lib/manifest.js +34 -0
  37. package/lib/manifest.js.map +1 -0
  38. package/lib/mapper.d.ts +36 -0
  39. package/lib/mapper.d.ts.map +1 -0
  40. package/lib/mapper.js +175 -0
  41. package/lib/mapper.js.map +1 -0
  42. package/lib/media-mime.d.ts +14 -0
  43. package/lib/media-mime.d.ts.map +1 -0
  44. package/lib/media-mime.js +32 -0
  45. package/lib/media-mime.js.map +1 -0
  46. package/lib/official-upstream.d.ts +143 -0
  47. package/lib/official-upstream.d.ts.map +1 -0
  48. package/lib/official-upstream.js +425 -0
  49. package/lib/official-upstream.js.map +1 -0
  50. package/lib/openapi-port.d.ts +190 -0
  51. package/lib/openapi-port.d.ts.map +1 -0
  52. package/lib/openapi-port.js +2 -0
  53. package/lib/openapi-port.js.map +1 -0
  54. package/lib/outbound.d.ts +69 -0
  55. package/lib/outbound.d.ts.map +1 -0
  56. package/lib/outbound.js +148 -0
  57. package/lib/outbound.js.map +1 -0
  58. package/lib/stream-upstream.d.ts +77 -0
  59. package/lib/stream-upstream.d.ts.map +1 -0
  60. package/lib/stream-upstream.js +303 -0
  61. package/lib/stream-upstream.js.map +1 -0
  62. package/lib/transport.d.ts +37 -0
  63. package/lib/transport.d.ts.map +1 -0
  64. package/lib/transport.js +73 -0
  65. package/lib/transport.js.map +1 -0
  66. package/lib/upstream.d.ts +60 -0
  67. package/lib/upstream.d.ts.map +1 -0
  68. package/lib/upstream.js +61 -0
  69. package/lib/upstream.js.map +1 -0
  70. package/package.json +46 -0
@@ -0,0 +1,244 @@
1
+ import { applyHydrationResult, SecureRemoteMediaFetcher, toIngressFailureCode, } from '@krischoichoi/channel-core';
2
+ /** Default hard byte cap for a downloaded image (20 MiB). */
3
+ export const IMAGE_MAX_BYTES = 20 * 1024 * 1024;
4
+ /** Default read-idle timeout (no body chunk for N ms -> fail). */
5
+ export const IMAGE_IDLE_TIMEOUT_MS = 15_000;
6
+ function classifyLocator(url) {
7
+ if (url === undefined || url === '')
8
+ return 'none';
9
+ return /^https?:\/\//i.test(url) ? 'http-url' : 'opaque';
10
+ }
11
+ /**
12
+ * Identify the http(s)-fetchable image parts in a message content. Opaque
13
+ * (non-http) locators are surfaced separately so the ingestion policy can
14
+ * route them to `resourceRef`.
15
+ */
16
+ export function findFetchableImages(parts) {
17
+ const out = [];
18
+ parts.forEach((part, index) => {
19
+ if (part.type !== 'image')
20
+ return;
21
+ const kind = classifyLocator(part.url);
22
+ if (kind === 'none')
23
+ return;
24
+ out.push({ kind, index, locator: part.url });
25
+ });
26
+ return out;
27
+ }
28
+ /**
29
+ * Hydrate image parts in place: mutate `parts` so every image with a genuine
30
+ * http(s) URL carries `localData` + `mimeType` (preferring the fetcher MIME
31
+ * type) on success, and `ingressFailure` on failure. Opaque locators are
32
+ * moved to `resourceRef` and left unresolved (OpenAPI milestone).
33
+ *
34
+ * This never throws and never blocks text delivery — any image failure only
35
+ * annotates the part.
36
+ */
37
+ export async function hydrateImages(parts, options = {}) {
38
+ const fetcher = options.secureFetch ?? new SecureRemoteMediaFetcher();
39
+ const resolver = options.resolveMedia;
40
+ const maxBytes = options.maxBytes ?? IMAGE_MAX_BYTES;
41
+ const idleTimeoutMs = options.idleTimeoutMs ?? IMAGE_IDLE_TIMEOUT_MS;
42
+ for (const part of parts) {
43
+ if (part.type !== 'image')
44
+ continue;
45
+ // Opaque handle: either the mapper already placed it in resourceRef, or a
46
+ // non-http url needs moving there first. Resolve via the DingTalk OpenAPI
47
+ // port (official downloadCode flow) when a resolver is wired — never via a
48
+ // generic fetch.
49
+ let opaque = part.resourceRef;
50
+ if (!opaque) {
51
+ const kind = classifyLocator(part.url);
52
+ if (kind === 'none')
53
+ continue;
54
+ if (kind === 'opaque') {
55
+ opaque = part.url;
56
+ part.resourceRef = opaque;
57
+ delete part.url;
58
+ }
59
+ }
60
+ if (opaque) {
61
+ if (!resolver)
62
+ continue;
63
+ try {
64
+ const resolved = await resolver.resolveMedia(opaque, {
65
+ signal: options.signal,
66
+ name: part.alt,
67
+ downloadCode: options.downloadContext?.downloadCode,
68
+ robotCode: options.downloadContext?.robotCode,
69
+ });
70
+ applyResolvedImage(part, resolved);
71
+ }
72
+ catch (error) {
73
+ part.ingressFailure = toIngressFailureCode(error);
74
+ options.onFailure?.(error, part);
75
+ }
76
+ continue;
77
+ }
78
+ const url = part.url;
79
+ try {
80
+ const result = await fetcher.fetchBounded(url, {
81
+ maxBytes,
82
+ idleTimeoutMs,
83
+ signal: options.signal,
84
+ });
85
+ // Prefer the fetcher declared MIME type; fall back to any existing hint.
86
+ part.localData = result.data;
87
+ if (result.mimeType)
88
+ part.mimeType = result.mimeType;
89
+ if (part.size === undefined)
90
+ part.size = result.data.byteLength;
91
+ }
92
+ catch (error) {
93
+ // Keep the locator and annotate the failure; never throw into the caller.
94
+ part.ingressFailure = toIngressFailureCode(error);
95
+ options.onFailure?.(error, part);
96
+ }
97
+ }
98
+ }
99
+ /** Default hard byte cap for a downloaded generic file (50 MiB). */
100
+ export const FILE_MAX_BYTES = 50 * 1024 * 1024;
101
+ /**
102
+ * Hydrate generic file parts in place:
103
+ * - http(s) url -> SecureRemoteMediaFetcher -> `localData` + `mimeType` + `size`
104
+ * - opaque mediaId -> move to `resourceRef` -> `resolveMedia` -> `localData`
105
+ * On failure the original locator is kept and a stable `ingressFailure` is
106
+ * stamped — this never throws and never blocks text delivery.
107
+ */
108
+ export async function hydrateFiles(parts, options = {}) {
109
+ const fetcher = options.secureFetch ?? new SecureRemoteMediaFetcher();
110
+ const resolver = options.resolveMedia;
111
+ const maxBytes = options.maxBytes ?? FILE_MAX_BYTES;
112
+ const idleTimeoutMs = options.idleTimeoutMs ?? IMAGE_IDLE_TIMEOUT_MS;
113
+ for (const part of parts) {
114
+ if (part.type !== 'file')
115
+ continue;
116
+ const file = part;
117
+ // Opaque handle: either the mapper already placed it in resourceRef, or a
118
+ // non-http url needs moving there first. Resolve via the port (official
119
+ // downloadCode flow) when a resolver is wired — never via a generic fetch.
120
+ let opaque = file.resourceRef;
121
+ if (!opaque) {
122
+ const kind = classifyPartLocator(file.url);
123
+ if (kind === 'none')
124
+ continue;
125
+ if (kind === 'opaque') {
126
+ opaque = file.url;
127
+ file.resourceRef = opaque;
128
+ delete file.url;
129
+ }
130
+ }
131
+ if (opaque) {
132
+ if (resolver) {
133
+ try {
134
+ const resolved = await resolver.resolveMedia(opaque, {
135
+ signal: options.signal,
136
+ name: file.name,
137
+ downloadCode: options.downloadContext?.downloadCode,
138
+ robotCode: options.downloadContext?.robotCode,
139
+ });
140
+ applyResolved(file, resolved);
141
+ }
142
+ catch (error) {
143
+ file.ingressFailure = toIngressFailureCode(error);
144
+ }
145
+ }
146
+ continue;
147
+ }
148
+ const url = file.url;
149
+ try {
150
+ const result = await fetcher.fetchBounded(url, { maxBytes, idleTimeoutMs, signal: options.signal });
151
+ file.localData = result.data;
152
+ if (result.mimeType)
153
+ file.mimeType = result.mimeType;
154
+ if (file.size === undefined)
155
+ file.size = result.data.byteLength;
156
+ }
157
+ catch (error) {
158
+ file.ingressFailure = toIngressFailureCode(error);
159
+ }
160
+ }
161
+ }
162
+ function classifyPartLocator(url) {
163
+ if (url === undefined || url === '')
164
+ return 'none';
165
+ return /^https?:\/\//i.test(url) ? 'http-url' : 'opaque';
166
+ }
167
+ /** Default hard byte cap for a downloaded audio/video resource (100 MiB). */
168
+ export const MEDIA_MAX_BYTES = 100 * 1024 * 1024;
169
+ /**
170
+ * Hydrate audio/video parts in place. Audio/video follow the
171
+ * exact same URL-or-opaque resolution as image/file:
172
+ * - http(s) url -> SecureRemoteMediaFetcher -> `localData` + `mimeType` + `size`
173
+ * - opaque handle -> move to `resourceRef` -> `resolveMedia` (official
174
+ * `messageFiles/download` seam) -> `localData`
175
+ * The apply step delegates to core `applyHydrationResult`, so success clears
176
+ * `ingressFailure`, failures write ONLY a stable `ingressFailure` (never
177
+ * fabricating bytes), and this never throws / never blocks text delivery.
178
+ */
179
+ export async function hydrateMedia(parts, options = {}) {
180
+ const fetcher = options.secureFetch ?? new SecureRemoteMediaFetcher();
181
+ const resolver = options.resolveMedia;
182
+ const maxBytes = options.maxBytes ?? MEDIA_MAX_BYTES;
183
+ const idleTimeoutMs = options.idleTimeoutMs ?? IMAGE_IDLE_TIMEOUT_MS;
184
+ for (const part of parts) {
185
+ if (part.type !== 'audio' && part.type !== 'video')
186
+ continue;
187
+ const media = part;
188
+ // Opaque handle: either the mapper already placed it in resourceRef, or a
189
+ // non-http url needs moving there first. Resolve via the port (official
190
+ // downloadCode flow) when a resolver is wired — never via a generic fetch.
191
+ let opaque = media.resourceRef;
192
+ if (!opaque) {
193
+ const kind = classifyPartLocator(media.url);
194
+ if (kind === 'none')
195
+ continue;
196
+ if (kind === 'opaque') {
197
+ opaque = media.url;
198
+ media.resourceRef = opaque;
199
+ delete media.url;
200
+ }
201
+ }
202
+ if (opaque) {
203
+ if (!resolver)
204
+ continue;
205
+ await applyHydrationResult(media, () => resolver.resolveMedia(opaque, {
206
+ signal: options.signal,
207
+ name: media.name,
208
+ downloadCode: options.downloadContext?.downloadCode,
209
+ robotCode: options.downloadContext?.robotCode,
210
+ }), { maxBytes, signal: options.signal });
211
+ continue;
212
+ }
213
+ const url = media.url;
214
+ await applyHydrationResult(media, async () => {
215
+ const result = await fetcher.fetchBounded(url, { maxBytes, idleTimeoutMs, signal: options.signal });
216
+ return { data: result.data, mimeType: result.mimeType };
217
+ }, { maxBytes, signal: options.signal });
218
+ }
219
+ }
220
+ /** Apply a successful resolution to a file part. */
221
+ function applyResolved(part, resolved) {
222
+ part.localData = resolved.data;
223
+ if (resolved.mimeType)
224
+ part.mimeType = resolved.mimeType;
225
+ if (resolved.size !== undefined)
226
+ part.size = resolved.size;
227
+ if (part.size === undefined)
228
+ part.size = resolved.data.byteLength;
229
+ }
230
+ /** Apply a successful resolution to an image part. */
231
+ function applyResolvedImage(part, resolved) {
232
+ part.localData = resolved.data;
233
+ if (resolved.mimeType)
234
+ part.mimeType = resolved.mimeType;
235
+ if (resolved.size !== undefined)
236
+ part.size = resolved.size;
237
+ if (part.size === undefined)
238
+ part.size = resolved.data.byteLength;
239
+ }
240
+ /** Convenience factory returning a real secure fetcher bound to global fetch. */
241
+ export function defaultSecureFetcher() {
242
+ return new SecureRemoteMediaFetcher();
243
+ }
244
+ //# sourceMappingURL=image-hydrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-hydrator.js","sourceRoot":"","sources":["../src/image-hydrator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Inbound processing: dedup window + structured mapping + media hydration (image/file/audio/video) + emit.
3
+ */
4
+ import type { ChannelAdapterContext } from '@krischoichoi/channel-core';
5
+ import { type DingTalkInboundMeta } from './mapper.js';
6
+ import { type RemoteMediaFetchLike } from './image-hydrator.js';
7
+ import type { MediaResolverLike } from './openapi-port.js';
8
+ export interface InboundProcessorOptions {
9
+ ctx: ChannelAdapterContext;
10
+ meta: DingTalkInboundMeta;
11
+ dedupEnabled: boolean;
12
+ dedupWindowMs: number;
13
+ /** Injectable clock (tests). */
14
+ now?: () => number;
15
+ /**
16
+ * Secure remote media fetcher used to hydrate inbound image URLs into
17
+ * `localData`. Injectable for offline tests; defaults to a real
18
+ * `SecureRemoteMediaFetcher` bound to the global fetch.
19
+ */
20
+ secureFetch?: RemoteMediaFetchLike;
21
+ /**
22
+ * DingTalk OpenAPI media resolver used to turn opaque file mediaIds into
23
+ * trusted bytes during file ingress. Injectable for offline
24
+ * tests; when absent, opaque file handles are deferred to `resourceRef`.
25
+ */
26
+ resolveMedia?: MediaResolverLike;
27
+ }
28
+ export declare class InboundProcessor {
29
+ private readonly options;
30
+ private readonly now;
31
+ /** dedup key -> last-seen timestamp, pruned on every handle. */
32
+ private readonly seen;
33
+ constructor(options: InboundProcessorOptions);
34
+ /** Process one raw payload from the upstream; dedup, hydrate, then emit. */
35
+ handle(raw: unknown): Promise<void>;
36
+ private prune;
37
+ }
38
+ //# sourceMappingURL=inbound.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAgC,MAAM,4BAA4B,CAAC;AAEtG,OAAO,EAAwC,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAwD7F,OAAO,EAA6C,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3G,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,WAAW,uBAAuB;IACtC,GAAG,EAAE,qBAAqB,CAAC;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,gCAAgC;IAChC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB;;;;OAIG;IACH,WAAW,CAAC,EAAE,oBAAoB,CAAC;IACnC;;;;OAIG;IACH,YAAY,CAAC,EAAE,iBAAiB,CAAC;CAClC;AAED,qBAAa,gBAAgB;IAKf,OAAO,CAAC,QAAQ,CAAC,OAAO;IAJpC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,gEAAgE;IAChE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;gBAErB,OAAO,EAAE,uBAAuB;IAI7D,4EAA4E;IACtE,MAAM,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAkFzC,OAAO,CAAC,KAAK;CAKd"}
package/lib/inbound.js ADDED
@@ -0,0 +1,158 @@
1
+ import { z } from 'zod';
2
+ import { dedupKey, mapInbound, mapInteraction } from './mapper.js';
3
+ const downloadContextSchema = z.object({
4
+ type: z.string().optional(),
5
+ picDownloadCode: z.string().min(1).optional(),
6
+ downloadCode: z.string().min(1).optional(),
7
+ robotCode: z.string().min(1).optional(),
8
+ });
9
+ /** Compact per-part summary for inbound message logs (debug diagnostics). */
10
+ function summarizeParts(parts) {
11
+ return parts.map((part) => {
12
+ switch (part.type) {
13
+ case 'text':
14
+ return { type: 'text', text: part.text.slice(0, 80) };
15
+ case 'image':
16
+ return {
17
+ type: 'image',
18
+ hasUrl: Boolean(part.url),
19
+ resourceRef: part.resourceRef ? '[redacted]' : undefined,
20
+ mimeType: part.mimeType,
21
+ localDataBytes: part.localData?.byteLength,
22
+ ingressFailure: part.ingressFailure,
23
+ };
24
+ case 'file':
25
+ return {
26
+ type: 'file',
27
+ name: part.name,
28
+ size: part.size,
29
+ mimeType: part.mimeType,
30
+ localDataBytes: part.localData?.byteLength,
31
+ ingressFailure: part.ingressFailure,
32
+ };
33
+ case 'audio':
34
+ return {
35
+ type: 'audio',
36
+ durationMs: part.durationMs,
37
+ mimeType: part.mimeType,
38
+ size: part.size,
39
+ localDataBytes: part.localData?.byteLength,
40
+ ingressFailure: part.ingressFailure,
41
+ };
42
+ case 'video':
43
+ return {
44
+ type: 'video',
45
+ durationMs: part.durationMs,
46
+ mimeType: part.mimeType,
47
+ size: part.size,
48
+ localDataBytes: part.localData?.byteLength,
49
+ ingressFailure: part.ingressFailure,
50
+ };
51
+ default:
52
+ return { type: part.type };
53
+ }
54
+ });
55
+ }
56
+ import { hydrateFiles, hydrateImages, hydrateMedia } from './image-hydrator.js';
57
+ export class InboundProcessor {
58
+ options;
59
+ now;
60
+ /** dedup key -> last-seen timestamp, pruned on every handle. */
61
+ seen = new Map();
62
+ constructor(options) {
63
+ this.options = options;
64
+ this.now = options.now ?? Date.now;
65
+ }
66
+ /** Process one raw payload from the upstream; dedup, hydrate, then emit. */
67
+ async handle(raw) {
68
+ const key = dedupKey(raw);
69
+ if (this.options.dedupEnabled) {
70
+ const now = this.now();
71
+ const last = this.seen.get(key);
72
+ if (last !== undefined && now - last < this.options.dedupWindowMs) {
73
+ this.options.ctx.logger.debug(`[channel-dingtalk] dropped duplicate message '${key}'`);
74
+ return;
75
+ }
76
+ this.seen.set(key, now);
77
+ this.prune(now);
78
+ }
79
+ const kind = z.object({ type: z.string().optional() }).passthrough().safeParse(raw);
80
+ if (kind.success && kind.data.type === 'interaction') {
81
+ const interaction = mapInteraction(raw, this.options.meta);
82
+ this.options.ctx.logger.info('[channel-dingtalk] inbound interaction', {
83
+ interactionId: interaction.interactionId,
84
+ conversationId: interaction.conversation.id,
85
+ senderId: interaction.sender.id,
86
+ action: interaction.action,
87
+ });
88
+ await this.options.ctx.emit(interaction);
89
+ return;
90
+ }
91
+ const event = mapInbound(raw, this.options.meta);
92
+ // Per-message download context (official robot schema): the callback's
93
+ // downloadCode + robotCode are transient upstream state the official
94
+ // /v1.0/robot/messageFiles/download API needs. Read from the raw payload
95
+ // and passed to hydration — never persisted onto core parts.
96
+ const parsedDownloadContext = downloadContextSchema.safeParse(raw);
97
+ const rawValue = parsedDownloadContext.success ? parsedDownloadContext.data : undefined;
98
+ const downloadContext = {
99
+ downloadCode: rawValue?.picDownloadCode ?? rawValue?.downloadCode,
100
+ robotCode: rawValue?.robotCode,
101
+ };
102
+ // Hydrate image parts (never throws / never blocks text delivery). The
103
+ // adapter context signal lets the owning scope abort in-flight downloads.
104
+ await hydrateImages(event.message.content, {
105
+ secureFetch: this.options.secureFetch,
106
+ resolveMedia: this.options.resolveMedia,
107
+ downloadContext,
108
+ signal: this.options.ctx.signal,
109
+ onFailure: (error, part) => {
110
+ this.options.ctx.logger.warn('[channel-dingtalk] inbound image hydration failed', {
111
+ hasResourceRef: part.type === 'image' && Boolean(part.resourceRef),
112
+ hasUrl: part.type === 'image' && Boolean(part.url),
113
+ error: summarizeError(error),
114
+ });
115
+ },
116
+ });
117
+ // Hydrate generic file parts: genuine http(s) urls via the secure fetcher;
118
+ // opaque mediaIds via the DingTalk OpenAPI media resolver.
119
+ await hydrateFiles(event.message.content, {
120
+ secureFetch: this.options.secureFetch,
121
+ resolveMedia: this.options.resolveMedia,
122
+ downloadContext,
123
+ signal: this.options.ctx.signal,
124
+ });
125
+ // Hydrate audio/video parts: audio/video follow the exact
126
+ // same URL-or-opaque resolution as image/file — genuine http(s) urls via
127
+ // the secure fetcher, opaque downloadCodes via the official
128
+ // messageFiles/download seam (the connector oracle downloads voice/video
129
+ // bytes through the identical downloadCode -> downloadUrl -> raw bytes
130
+ // flow). Failures only annotate the part; text still delivers.
131
+ await hydrateMedia(event.message.content, {
132
+ secureFetch: this.options.secureFetch,
133
+ resolveMedia: this.options.resolveMedia,
134
+ downloadContext,
135
+ signal: this.options.ctx.signal,
136
+ });
137
+ // Inbound message log: expose hydration state without platform locators.
138
+ // Signed URLs and download codes must never enter logs at any level.
139
+ this.options.ctx.logger.info(`[channel-dingtalk] inbound message ${event.message.id} from ${event.sender.id} in ${event.conversation.id}`, {
140
+ msgtype: rawValue?.type,
141
+ parts: summarizeParts(event.message.content),
142
+ });
143
+ await this.options.ctx.emit(event);
144
+ }
145
+ prune(now) {
146
+ for (const [key, ts] of this.seen) {
147
+ if (now - ts >= this.options.dedupWindowMs)
148
+ this.seen.delete(key);
149
+ }
150
+ }
151
+ }
152
+ function summarizeError(error) {
153
+ if (!(error instanceof Error))
154
+ return { name: 'UnknownError' };
155
+ const code = 'code' in error && typeof error.code === 'string' ? error.code : undefined;
156
+ return { name: error.name, ...(code !== undefined ? { code } : {}) };
157
+ }
158
+ //# sourceMappingURL=inbound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inbound.js","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @krischoichoi/channel-dingtalk — DingTalk channel adapter for DeepSeek Harness.
3
+ *
4
+ * Maps the DingTalk platform to the stable Channel Contract. Two upstream
5
+ * drivers implement `DingTalkUpstream` behind `config.upstream.mode`:
6
+ * - 'sdk' — inbound via the official `dingtalk-stream` SDK (WebSocket
7
+ * stream mode); outbound via `sessionWebhook` and the AI Card OpenAPI.
8
+ * - 'gateway' — self-hosted HTTP gateway long-poll driver (legacy).
9
+ *
10
+ * Credential model: the DingTalk AppSecret never lives in config. Config only
11
+ * carries its credential reference (`upstream.clientSecretRef`, default
12
+ * `DSH_CHANNEL_DINGTALK_MAIN_CLIENT_SECRET`); the value is resolved via
13
+ * `ctx.credentials` and injected as `deps.clientSecret`. A legacy plaintext
14
+ * `upstream.clientSecret` is migrated to the credentials seam ONCE on startup
15
+ * and deleted. Streaming is `edit` (AI Card): create card → update with
16
+ * content → finalize → failure card. Auth is connection-state driven — the
17
+ * driver owns platform credentials (never logged).
18
+ */
19
+ import { type Context } from '@deepseek-ai/cordis';
20
+ import { Config, DINGTALK_CLIENT_SECRET_REF } from './config.js';
21
+ import type { DingTalkConfig, DingTalkConfigLive } from './config.js';
22
+ import { type DingTalkAdapterDeps } from './adapter.js';
23
+ export declare const name = "channel-dingtalk";
24
+ export declare const inject: string[];
25
+ export { Config, DINGTALK_CLIENT_SECRET_REF };
26
+ export type { DingTalkConfig, DingTalkConfigLive } from './config.js';
27
+ export { createDingTalkDefinition } from './definition.js';
28
+ export { beginDingTalkDeviceAuth, pollDingTalkDeviceAuth, type DingTalkDeviceAuthOptions, type DingTalkDevicePollResult, } from './auth/device-registration.js';
29
+ export { DingTalkAdapter, type DingTalkAdapterDeps } from './adapter.js';
30
+ export { DingTalkCardReply, type DingTalkCardStatus, type DingTalkCardUpdate } from './ai-card.js';
31
+ export { InboundProcessor } from './inbound.js';
32
+ export { IMAGE_IDLE_TIMEOUT_MS, IMAGE_MAX_BYTES, FILE_MAX_BYTES, MEDIA_MAX_BYTES, defaultSecureFetcher, findFetchableImages, hydrateFiles, hydrateImages, hydrateMedia, type BinaryIngressFailureCode, type HydrateFilesOptions, type HydrateImagesOptions, type HydrateMediaOptions, type RemoteMediaFetchLike, type ResolvedImageLocator, } from './image-hydrator.js';
33
+ export { OutboundSender, type OutboundSenderOptions, } from './outbound.js';
34
+ export { HttpDingTalkUpstream, type CardCreateResult, type DingTalkUpstream } from './upstream.js';
35
+ export { DingTalkOfficialUpstream, DingTalkOpenApiPortImpl, OFFICIAL_BASIS, type DingTalkOfficialUpstreamOptions, } from './official-upstream.js';
36
+ export { type AccessTokenResult, type DingTalkOpenApiCredentials, type DingTalkOpenApiPort, type MediaResolverLike, type MediaSendInput, type MediaUploadInput, type MediaUploadResult, type OutboxCapabilities, type ProactiveTextInput, type ResolvedMedia, type RobotMessageSendResult, } from './openapi-port.js';
37
+ export { DingTalkStreamUpstream, ackRobotMessage, toCardInteractionRaw, toGatewayRaw, type DingTalkStreamClient, type DingTalkStreamMessage, type DingTalkStreamUpstreamOptions, } from './stream-upstream.js';
38
+ export { FetchTransport, type HttpTransport } from './transport.js';
39
+ export { mapInbound, toTextPayload, dedupKey, simpleHash, type DingTalkTextPayload, } from './mapper.js';
40
+ export { manifest, type DingTalkManifest } from './manifest.js';
41
+ /**
42
+ * Entry plugin apply. Accepts both the activation-time live shape (volatile
43
+ * fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
44
+ * snapshot (tests / programmatic mounting); plain fields pass through
45
+ * `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
46
+ * the Loader boundary via the attached `Config` schema.
47
+ */
48
+ export declare function apply(ctx: Context, rawConfig: DingTalkConfig | DingTalkConfigLive, deps?: DingTalkAdapterDeps): void;
49
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAInD,OAAO,EAAE,MAAM,EAAE,0BAA0B,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAA0B,MAAM,aAAa,CAAC;AAC9F,OAAO,EAAmB,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAGzE,eAAO,MAAM,IAAI,qBAAqB,CAAC;AACvC,eAAO,MAAM,MAAM,EAAE,MAAM,EAAgC,CAAC;AAE5D,OAAO,EAAE,MAAM,EAAE,0BAA0B,EAAE,CAAC;AAC9C,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtE,OAAO,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAC3D,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,KAAK,yBAAyB,EAC9B,KAAK,wBAAwB,GAC9B,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,eAAe,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,qBAAqB,EACrB,eAAe,EACf,cAAc,EACd,eAAe,EACf,oBAAoB,EACpB,mBAAmB,EACnB,YAAY,EACZ,aAAa,EACb,YAAY,EACZ,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,GAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,cAAc,EACd,KAAK,qBAAqB,GAC3B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,oBAAoB,EAAE,KAAK,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACnG,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,cAAc,EACd,KAAK,+BAA+B,GACrC,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,KAAK,iBAAiB,EACtB,KAAK,0BAA0B,EAC/B,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,cAAc,EACnB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,sBAAsB,GAC5B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,sBAAsB,EACtB,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,6BAA6B,GACnC,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,cAAc,EAAE,KAAK,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EACL,UAAU,EACV,aAAa,EACb,QAAQ,EACR,UAAU,EACV,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,QAAQ,EAAE,KAAK,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAoBhE;;;;;;GAMG;AACH,wBAAgB,KAAK,CACnB,GAAG,EAAE,OAAO,EACZ,SAAS,EAAE,cAAc,GAAG,kBAAkB,EAC9C,IAAI,GAAE,mBAAwB,GAC7B,IAAI,CAoFN"}
package/lib/index.js ADDED
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @krischoichoi/channel-dingtalk — DingTalk channel adapter for DeepSeek Harness.
3
+ *
4
+ * Maps the DingTalk platform to the stable Channel Contract. Two upstream
5
+ * drivers implement `DingTalkUpstream` behind `config.upstream.mode`:
6
+ * - 'sdk' — inbound via the official `dingtalk-stream` SDK (WebSocket
7
+ * stream mode); outbound via `sessionWebhook` and the AI Card OpenAPI.
8
+ * - 'gateway' — self-hosted HTTP gateway long-poll driver (legacy).
9
+ *
10
+ * Credential model: the DingTalk AppSecret never lives in config. Config only
11
+ * carries its credential reference (`upstream.clientSecretRef`, default
12
+ * `DSH_CHANNEL_DINGTALK_MAIN_CLIENT_SECRET`); the value is resolved via
13
+ * `ctx.credentials` and injected as `deps.clientSecret`. A legacy plaintext
14
+ * `upstream.clientSecret` is migrated to the credentials seam ONCE on startup
15
+ * and deleted. Streaming is `edit` (AI Card): create card → update with
16
+ * content → finalize → failure card. Auth is connection-state driven — the
17
+ * driver owns platform credentials (never logged).
18
+ */
19
+ import {} from '@deepseek-ai/cordis';
20
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
21
+ import { mountChannelAdapter, resolveVolatileConfig, resolveVolatileValue } from '@krischoichoi/channel-core';
22
+ import { Config, DINGTALK_CLIENT_SECRET_REF } from './config.js';
23
+ import { DingTalkAdapter } from './adapter.js';
24
+ import { createDingTalkDefinition } from './definition.js';
25
+ export const name = 'channel-dingtalk';
26
+ export const inject = ['channels', 'credentials'];
27
+ export { Config, DINGTALK_CLIENT_SECRET_REF };
28
+ export { createDingTalkDefinition } from './definition.js';
29
+ export { beginDingTalkDeviceAuth, pollDingTalkDeviceAuth, } from './auth/device-registration.js';
30
+ export { DingTalkAdapter } from './adapter.js';
31
+ export { DingTalkCardReply } from './ai-card.js';
32
+ export { InboundProcessor } from './inbound.js';
33
+ export { IMAGE_IDLE_TIMEOUT_MS, IMAGE_MAX_BYTES, FILE_MAX_BYTES, MEDIA_MAX_BYTES, defaultSecureFetcher, findFetchableImages, hydrateFiles, hydrateImages, hydrateMedia, } from './image-hydrator.js';
34
+ export { OutboundSender, } from './outbound.js';
35
+ export { HttpDingTalkUpstream } from './upstream.js';
36
+ export { DingTalkOfficialUpstream, DingTalkOpenApiPortImpl, OFFICIAL_BASIS, } from './official-upstream.js';
37
+ export {} from './openapi-port.js';
38
+ export { DingTalkStreamUpstream, ackRobotMessage, toCardInteractionRaw, toGatewayRaw, } from './stream-upstream.js';
39
+ export { FetchTransport } from './transport.js';
40
+ export { mapInbound, toTextPayload, dedupKey, simpleHash, } from './mapper.js';
41
+ export { manifest } from './manifest.js';
42
+ /**
43
+ * Entry plugin apply. Accepts both the activation-time live shape (volatile
44
+ * fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
45
+ * snapshot (tests / programmatic mounting); plain fields pass through
46
+ * `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
47
+ * the Loader boundary via the attached `Config` schema.
48
+ */
49
+ export function apply(ctx, rawConfig, deps = {}) {
50
+ const config = resolveVolatileConfig(rawConfig);
51
+ // --- One-time legacy plaintext -> credential-reference migration (§52 Task 4).
52
+ // The value is written to the credentials seam exactly once, then the
53
+ // plaintext is deleted from the in-memory config. Never double-written.
54
+ const legacySecret = config.upstream.clientSecret;
55
+ const ref = config.upstream.clientSecretRef ?? DINGTALK_CLIENT_SECRET_REF;
56
+ if (typeof legacySecret === 'string' && legacySecret.length > 0) {
57
+ void ctx.credentials.set(credentialRef(ref), legacySecret);
58
+ // Strip the plaintext from BOTH the unwrapped snapshot and the caller's
59
+ // raw config so it is never read or written again. Schemastery 3.18
60
+ // freezes parsed sub-objects, so swap in a fresh shallow copy; the raw
61
+ // config top level may be frozen too (host-provided), hence the guard.
62
+ const stripped = { ...config.upstream, clientSecret: undefined };
63
+ config.upstream = stripped;
64
+ try {
65
+ rawConfig.upstream = stripped;
66
+ }
67
+ catch {
68
+ // Frozen live config: the swap above already sanitized our snapshot.
69
+ }
70
+ ctx.logger('channel-dingtalk').info('migrated legacy dingtalk clientSecret to credentials');
71
+ }
72
+ const control = ctx.get('channelControl');
73
+ if (control) {
74
+ // The control plane owns lifecycle: register EVEN when disabled so the Web
75
+ // control plane can re-enable the channel later (doc §19/§20); runtime
76
+ // auto-starts.
77
+ const settings = ctx.get('settings');
78
+ control.definitions.register(createDingTalkDefinition({
79
+ config,
80
+ deps,
81
+ credentials: ctx.credentials,
82
+ persistSetup: (patch) => settings?.update('channels-dingtalk', patch) ?? Promise.resolve(),
83
+ persistEnabled: (enabled) => settings?.update('channels-dingtalk', { enabled }) ?? Promise.resolve(),
84
+ onAuthCompleted: control.runtime
85
+ ? async () => {
86
+ try {
87
+ await control.runtime.restart('dingtalk', config.accountId);
88
+ }
89
+ catch (error) {
90
+ // Provider authorization is complete even if adapter startup
91
+ // needs attention; keep the public auth session terminal and
92
+ // expose the startup failure through runtime status.
93
+ ctx.logger('channel-dingtalk').error('dingtalk credentials saved but runtime restart failed', error);
94
+ }
95
+ }
96
+ : undefined,
97
+ }));
98
+ return;
99
+ }
100
+ // --- Legacy standalone mount (channel-control absent). No directory/control
101
+ // surface exists to re-enable a disabled channel, so the config `enabled`
102
+ // gate still applies (doc §20).
103
+ if (!resolveVolatileValue(config.enabled))
104
+ return;
105
+ const credentialsCtx = ctx;
106
+ ctx.effect(async () => {
107
+ // SDK mode requires a resolved credential; gateway mode owns its own.
108
+ let clientSecret;
109
+ if (config.upstream.mode === 'sdk') {
110
+ const resolved = await credentialsCtx.credentials.resolve(credentialRef(ref));
111
+ clientSecret = resolved?.value;
112
+ if (!clientSecret) {
113
+ ctx.logger('channel-dingtalk').warn(`dingtalk upstream mode "sdk" credential "${ref}" is not configured; not mounting`);
114
+ return () => { };
115
+ }
116
+ }
117
+ mountChannelAdapter(ctx, new DingTalkAdapter(config, clientSecret ? { ...deps, clientSecret } : deps), (signal) => ctx.channels.createAdapterContext({ channelId: 'dingtalk', signal }));
118
+ // The mount owns the adapter lifecycle; this outer effect only scopes the
119
+ // async credential resolution, so its disposer is a no-op.
120
+ return () => { };
121
+ });
122
+ }
123
+ //# sourceMappingURL=index.js.map