@krischoichoi/channel-lark 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 (62) hide show
  1. package/README.md +114 -0
  2. package/lib/adapter.d.ts +128 -0
  3. package/lib/adapter.d.ts.map +1 -0
  4. package/lib/adapter.js +347 -0
  5. package/lib/adapter.js.map +1 -0
  6. package/lib/auth/device-authorization.d.ts +12 -0
  7. package/lib/auth/device-authorization.d.ts.map +1 -0
  8. package/lib/auth/device-authorization.js +169 -0
  9. package/lib/auth/device-authorization.js.map +1 -0
  10. package/lib/card.d.ts +124 -0
  11. package/lib/card.d.ts.map +1 -0
  12. package/lib/card.js +347 -0
  13. package/lib/card.js.map +1 -0
  14. package/lib/config.d.ts +103 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +60 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/definition.d.ts +74 -0
  19. package/lib/definition.d.ts.map +1 -0
  20. package/lib/definition.js +190 -0
  21. package/lib/definition.js.map +1 -0
  22. package/lib/inbound.d.ts +41 -0
  23. package/lib/inbound.d.ts.map +1 -0
  24. package/lib/inbound.js +102 -0
  25. package/lib/inbound.js.map +1 -0
  26. package/lib/index.d.ts +58 -0
  27. package/lib/index.d.ts.map +1 -0
  28. package/lib/index.js +104 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/lark-sdk-upstream.d.ts +128 -0
  31. package/lib/lark-sdk-upstream.d.ts.map +1 -0
  32. package/lib/lark-sdk-upstream.js +339 -0
  33. package/lib/lark-sdk-upstream.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 +36 -0
  37. package/lib/manifest.js.map +1 -0
  38. package/lib/mapper.d.ts +47 -0
  39. package/lib/mapper.d.ts.map +1 -0
  40. package/lib/mapper.js +223 -0
  41. package/lib/mapper.js.map +1 -0
  42. package/lib/media-hydrator.d.ts +55 -0
  43. package/lib/media-hydrator.d.ts.map +1 -0
  44. package/lib/media-hydrator.js +172 -0
  45. package/lib/media-hydrator.js.map +1 -0
  46. package/lib/openapi-outbound.d.ts +216 -0
  47. package/lib/openapi-outbound.d.ts.map +1 -0
  48. package/lib/openapi-outbound.js +384 -0
  49. package/lib/openapi-outbound.js.map +1 -0
  50. package/lib/outbound.d.ts +21 -0
  51. package/lib/outbound.d.ts.map +1 -0
  52. package/lib/outbound.js +93 -0
  53. package/lib/outbound.js.map +1 -0
  54. package/lib/upstream/media-port.d.ts +154 -0
  55. package/lib/upstream/media-port.d.ts.map +1 -0
  56. package/lib/upstream/media-port.js +168 -0
  57. package/lib/upstream/media-port.js.map +1 -0
  58. package/lib/upstream.d.ts +119 -0
  59. package/lib/upstream.d.ts.map +1 -0
  60. package/lib/upstream.js +2 -0
  61. package/lib/upstream.js.map +1 -0
  62. package/package.json +46 -0
package/lib/index.d.ts ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @krischoichoi/channel-lark — Lark / Feishu channel adapter for DeepSeek Harness.
3
+ *
4
+ * Maps the Lark platform to the stable Channel Contract. The upstream is the
5
+ * OFFICIAL `@larksuiteoapi/node-sdk` only:
6
+ * - inbound — WSClient + EventDispatcher (`im.message.receive_v1` +
7
+ * `card.action.trigger`);
8
+ * - outbound — the official OpenAPI client (`im.v1.message.create` / `patch`,
9
+ * media uploads, CardKit 2.0 card entities and native streaming).
10
+ * The AppId is a plain config string (`upstream.appId`); the AppSecret is
11
+ * resolved via `ctx.credentials` (`DSH_CHANNEL_LARK_MAIN_APP_SECRET`) — only
12
+ * the reference name ever lives in config. The secret value is never logged.
13
+ * There is no self-hosted gateway mode, transport injection, plaintext secret
14
+ * field, or runtime migration path.
15
+ *
16
+ * Lifecycle: when the Channel Control Plane (`ctx.channelControl`) is present,
17
+ * apply() registers a `ChannelDefinition` ('lark'); the control plane decides
18
+ * when to instantiate/mount the adapter (headless auto-start). When it is
19
+ * absent (standalone / older harness), apply() falls back to resolving SDK
20
+ * credentials and mounting directly — never throwing when a channel is merely
21
+ * unconfigured.
22
+ *
23
+ * Streaming is `edit` (CardKit 2.0 native streaming card): create card entity
24
+ * → send card reference → stream element content → close streaming at end.
25
+ * Threads are preserved (`conversation.threadId`) so Harness sessions isolate
26
+ * per thread. Auth is connection-state driven — the driver owns platform
27
+ * credentials (never logged).
28
+ */
29
+ import { type Context } from '@deepseek-ai/cordis';
30
+ import type { LarkConfig, LarkConfigLive } from './config.js';
31
+ import { Config, LARK_APP_SECRET_REF } from './config.js';
32
+ import { type LarkAdapterDeps } from './adapter.js';
33
+ export declare const name = "channel-lark";
34
+ export declare const inject: string[];
35
+ export { Config, LARK_APP_SECRET_REF };
36
+ export { createLarkDefinition, type LarkCredentialSeam, type CreateLarkDefinitionOptions } from './definition.js';
37
+ export { beginLarkDeviceAuthorization, pollLarkDeviceAuthorization, type LarkDeviceAuthorizationOptions, } from './auth/device-authorization.js';
38
+ export { LarkAdapter, resolveDomain, type LarkAdapterDeps } from './adapter.js';
39
+ export { LarkCardReply, truncateSummary, splitForRollover, type LarkCardStatus, type LarkCardUpdate } from './card.js';
40
+ export { InboundProcessor } from './inbound.js';
41
+ export { OutboundSender } from './outbound.js';
42
+ export { type LarkOutbound, type LarkUpstream, type LarkFileRef, type LarkMediaRef, type LarkStreamingCardRef, } from './upstream.js';
43
+ export { LarkOpenApiOutbound, receiveIdType, cardContent, interactiveCardContent, streamingCardJson, STREAM_MARKDOWN_ELEMENT_ID, type LarkOpenApiClient, type LarkOpenApiOutboundOptions, type LarkCreateMessagePayload, type LarkCreateMessageResult, type LarkReceiveIdType, type LarkPatchMessagePayload, type LarkCreateImagePayload, type LarkCreateImageResult, fileTypeFromName, type LarkFileType, type LarkCreateFilePayload, type LarkCreateFileResult, type LarkApiResponse, type LarkCardkitCardCreatePayload, type LarkCardkitCardCreateResult, type LarkCardElementContentPayload, type LarkCardSettingsPayload, } from './openapi-outbound.js';
44
+ export { LarkSdkUpstream, mapSdkMessageEvent, mapSdkCardAction, MESSAGE_EVENT_KEY, CARD_ACTION_EVENT_KEY, type LarkSdkClient, type LarkSdkDispatcher, type LarkSdkUpstreamOptions, type LarkMessageEventData, } from './lark-sdk-upstream.js';
45
+ export { LarkOpenApiMediaPort, type LarkMediaClient, type LarkMediaPort, type LarkMediaPortOptions, type LarkResourceType, type LarkMessageResourceGetPayload, type LarkMessageResourceResult, type LarkImageGetPayload, type LarkImageCreatePayload, type LarkImageCreateResult, type LarkFileCreatePayload, type LarkFileCreateResult, } from './upstream/media-port.js';
46
+ export { MediaHydrator, ImageHydrator, classifyIngressFailure, type MediaHydratorOptions, type ImageHydratorOptions, } from './media-hydrator.js';
47
+ export { mapInbound, mapInteraction, toTextPayload, dedupKey, simpleHash, type LarkTextPayload, } from './mapper.js';
48
+ export { manifest, type LarkManifest } from './manifest.js';
49
+ export type { LarkConfigLive } from './config.js';
50
+ /**
51
+ * Entry plugin apply. Accepts both the activation-time live shape (volatile
52
+ * fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
53
+ * snapshot (tests / programmatic mounting); plain fields pass through
54
+ * `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
55
+ * the Loader boundary via the attached `Config` schema.
56
+ */
57
+ export declare function apply(ctx: Context, rawConfig: LarkConfig | LarkConfigLive, deps?: LarkAdapterDeps): void;
58
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/lib/index.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @krischoichoi/channel-lark — Lark / Feishu channel adapter for DeepSeek Harness.
3
+ *
4
+ * Maps the Lark platform to the stable Channel Contract. The upstream is the
5
+ * OFFICIAL `@larksuiteoapi/node-sdk` only:
6
+ * - inbound — WSClient + EventDispatcher (`im.message.receive_v1` +
7
+ * `card.action.trigger`);
8
+ * - outbound — the official OpenAPI client (`im.v1.message.create` / `patch`,
9
+ * media uploads, CardKit 2.0 card entities and native streaming).
10
+ * The AppId is a plain config string (`upstream.appId`); the AppSecret is
11
+ * resolved via `ctx.credentials` (`DSH_CHANNEL_LARK_MAIN_APP_SECRET`) — only
12
+ * the reference name ever lives in config. The secret value is never logged.
13
+ * There is no self-hosted gateway mode, transport injection, plaintext secret
14
+ * field, or runtime migration path.
15
+ *
16
+ * Lifecycle: when the Channel Control Plane (`ctx.channelControl`) is present,
17
+ * apply() registers a `ChannelDefinition` ('lark'); the control plane decides
18
+ * when to instantiate/mount the adapter (headless auto-start). When it is
19
+ * absent (standalone / older harness), apply() falls back to resolving SDK
20
+ * credentials and mounting directly — never throwing when a channel is merely
21
+ * unconfigured.
22
+ *
23
+ * Streaming is `edit` (CardKit 2.0 native streaming card): create card entity
24
+ * → send card reference → stream element content → close streaming at end.
25
+ * Threads are preserved (`conversation.threadId`) so Harness sessions isolate
26
+ * per thread. Auth is connection-state driven — the driver owns platform
27
+ * credentials (never logged).
28
+ */
29
+ import {} from '@deepseek-ai/cordis';
30
+ import { credentialRef } from '@deepseek-ai/dsh-credentials';
31
+ import { mountChannelAdapter, resolveVolatileConfig, resolveVolatileValue } from '@krischoichoi/channel-core';
32
+ import { Config, LARK_APP_SECRET_REF } from './config.js';
33
+ import { LarkAdapter } from './adapter.js';
34
+ import { createLarkDefinition } from './definition.js';
35
+ export const name = 'channel-lark';
36
+ export const inject = ['channels', 'credentials'];
37
+ export { Config, LARK_APP_SECRET_REF };
38
+ export { createLarkDefinition } from './definition.js';
39
+ export { beginLarkDeviceAuthorization, pollLarkDeviceAuthorization, } from './auth/device-authorization.js';
40
+ export { LarkAdapter, resolveDomain } from './adapter.js';
41
+ export { LarkCardReply, truncateSummary, splitForRollover } from './card.js';
42
+ export { InboundProcessor } from './inbound.js';
43
+ export { OutboundSender } from './outbound.js';
44
+ export {} from './upstream.js';
45
+ export { LarkOpenApiOutbound, receiveIdType, cardContent, interactiveCardContent, streamingCardJson, STREAM_MARKDOWN_ELEMENT_ID, fileTypeFromName, } from './openapi-outbound.js';
46
+ export { LarkSdkUpstream, mapSdkMessageEvent, mapSdkCardAction, MESSAGE_EVENT_KEY, CARD_ACTION_EVENT_KEY, } from './lark-sdk-upstream.js';
47
+ export { LarkOpenApiMediaPort, } from './upstream/media-port.js';
48
+ export { MediaHydrator, ImageHydrator, classifyIngressFailure, } from './media-hydrator.js';
49
+ export { mapInbound, mapInteraction, toTextPayload, dedupKey, simpleHash, } from './mapper.js';
50
+ export { manifest } from './manifest.js';
51
+ /**
52
+ * Entry plugin apply. Accepts both the activation-time live shape (volatile
53
+ * fields resolve to `Volatile<T>` handles under Harness 0.2.0) and a plain
54
+ * snapshot (tests / programmatic mounting); plain fields pass through
55
+ * `resolveVolatileConfig` untouched. The shape is validated by Schemastery at
56
+ * the Loader boundary via the attached `Config` schema.
57
+ */
58
+ export function apply(ctx, rawConfig, deps = {}) {
59
+ const config = resolveVolatileConfig(rawConfig);
60
+ // Adapt the CredentialProvider to the structural seam expected by the
61
+ // definition (credentialRef branding is applied here, once).
62
+ const seam = {
63
+ resolve: (ref) => ctx.credentials.resolve(credentialRef(ref)),
64
+ describe: (ref) => ctx.credentials.describe(credentialRef(ref)),
65
+ set: (ref, value) => ctx.credentials.set(credentialRef(ref), value),
66
+ };
67
+ const control = ctx.get('channelControl');
68
+ if (control) {
69
+ // Control plane present: register the definition EVEN when disabled — the
70
+ // plane owns adapter instantiation + headless auto-start, and a disabled
71
+ // definition must stay visible so the Web control plane can re-enable it
72
+ // later.
73
+ const settings = ctx.get('settings');
74
+ control.definitions.register(createLarkDefinition({
75
+ config,
76
+ deps,
77
+ credentials: seam,
78
+ persistSetup: (patch) => settings?.update('channels-lark', patch) ?? Promise.resolve(),
79
+ persistEnabled: (enabled) => settings?.update('channels-lark', { enabled }) ?? Promise.resolve(),
80
+ }));
81
+ return;
82
+ }
83
+ // Legacy fallback (standalone, no control plane): mount directly. An
84
+ // unconfigured adapter must NOT throw — log a warning and stay idle. In this
85
+ // mode there is no directory/control surface, so the config `enabled` gate
86
+ // still applies.
87
+ if (!resolveVolatileValue(config.enabled))
88
+ return;
89
+ ctx.effect(async () => {
90
+ const appId = config.upstream.appId;
91
+ const appSecretRef = config.upstream.appSecretRef ?? LARK_APP_SECRET_REF;
92
+ const appSecret = (await ctx.credentials.resolve(credentialRef(appSecretRef)))?.value;
93
+ if (!appId || !appSecret) {
94
+ ctx.logger('channel-lark').warn(`[channel-lark] not configured (missing appId or appSecret ref "${appSecretRef}"); adapter not mounted`);
95
+ return () => { };
96
+ }
97
+ const adapter = new LarkAdapter(config, { ...deps, appId, appSecret });
98
+ mountChannelAdapter(ctx, adapter, (signal) => ctx.channels.createAdapterContext({ channelId: 'lark', signal }));
99
+ // The mount owns the adapter lifecycle; this outer effect only scopes the
100
+ // async credential resolution, so its disposer is a no-op.
101
+ return () => { };
102
+ });
103
+ }
104
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import { z } from 'zod';
2
+ import type { LarkFileRef, LarkMediaRef, LarkOutbound, LarkUpstream } from './upstream.js';
3
+ /** Event type key for inbound message delivery (v1 event). */
4
+ export declare const MESSAGE_EVENT_KEY = "im.message.receive_v1";
5
+ /** Official event name for Feishu interactive-card button presses. */
6
+ export declare const CARD_ACTION_EVENT_KEY = "card.action.trigger";
7
+ /**
8
+ * Minimal structural dispatcher surface consumed by the driver. The real SDK
9
+ * `EventDispatcher` satisfies it (register / invoke); the WS client invokes
10
+ * `invoke(data, { needCheck: false })` for every inbound event frame.
11
+ */
12
+ export interface LarkSdkDispatcher {
13
+ /** Register handlers keyed by event type (e.g. 'im.message.receive_v1'). */
14
+ register(handles: Record<string, (...args: any[]) => unknown>): unknown;
15
+ /** Dispatch one raw event envelope to the registered handler. */
16
+ invoke(data: unknown, params?: {
17
+ needCheck?: boolean;
18
+ }): Promise<unknown>;
19
+ }
20
+ /**
21
+ * Minimal structural WS client surface consumed by the driver. The real SDK
22
+ * `WSClient` satisfies it (start / close); tests inject a fake without a
23
+ * WebSocket.
24
+ */
25
+ export interface LarkSdkClient {
26
+ /** Open the WS long-connection and start dispatching events. */
27
+ start(params: {
28
+ eventDispatcher: LarkSdkDispatcher;
29
+ }): Promise<void>;
30
+ /** Close the WS long-connection (sync in the real SDK). */
31
+ close(params?: {
32
+ force?: boolean;
33
+ }): void;
34
+ }
35
+ export interface LarkSdkUpstreamOptions {
36
+ /** The WS long-connection client (real WSClient or injected fake). */
37
+ client: LarkSdkClient;
38
+ /**
39
+ * Official OpenAPI outbound delegate (`LarkOpenApiOutbound` in production;
40
+ * any `LarkOutbound` fake in tests).
41
+ */
42
+ outbound: LarkOutbound;
43
+ /** Official OpenAPI chat lookup; absence deliberately fails card actions closed. */
44
+ resolveChatType?: (conversationId: string) => Promise<'p2p' | 'group' | undefined>;
45
+ /** Invoked after the WS connection is established (connection state). */
46
+ onConnected?: () => void;
47
+ }
48
+ declare const larkMessageEventDataSchema: z.ZodObject<{
49
+ event_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
50
+ event_type: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
51
+ token: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
52
+ create_time: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
53
+ sender: z.ZodOptional<z.ZodObject<{
54
+ sender_id: z.ZodOptional<z.ZodObject<{
55
+ union_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
56
+ user_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
57
+ open_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
58
+ }, z.core.$loose>>;
59
+ sender_type: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
60
+ tenant_key: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
61
+ }, z.core.$loose>>;
62
+ message: z.ZodOptional<z.ZodObject<{
63
+ message_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
64
+ root_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
65
+ parent_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
66
+ thread_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
67
+ create_time: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
68
+ chat_id: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
69
+ chat_type: z.ZodEnum<{
70
+ p2p: "p2p";
71
+ group: "group";
72
+ }>;
73
+ message_type: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
74
+ content: z.ZodPipe<z.ZodOptional<z.ZodNullable<z.ZodString>>, z.ZodTransform<string | undefined, string | null | undefined>>;
75
+ }, z.core.$loose>>;
76
+ }, z.core.$loose>;
77
+ export type LarkMessageEventData = z.infer<typeof larkMessageEventDataSchema>;
78
+ /**
79
+ * Map one parsed v1 message event into the canonical raw envelope consumed by
80
+ * the inbound mapper (`{ type, msgId, eventId, senderId, conversationId,
81
+ * chatType, threadId, content, ... }`). Returns `undefined` when the event
82
+ * carries no message body.
83
+ */
84
+ export declare function mapSdkMessageEvent(input: unknown): Record<string, unknown> | undefined;
85
+ /**
86
+ * Convert the official SDK-normalized card action into the canonical
87
+ * interaction envelope. The platform callback itself is never hand-parsed —
88
+ * it is first trust-boundary validated with zod and then normalized by the
89
+ * official SDK's `normalizeCardAction()`.
90
+ */
91
+ export declare function mapSdkCardAction(input: unknown, resolveChatType: ((conversationId: string) => Promise<'p2p' | 'group' | undefined>) | undefined): Promise<Record<string, unknown> | undefined>;
92
+ /** SDK-mode implementation of `LarkUpstream` (inbound via the SDK). */
93
+ export declare class LarkSdkUpstream implements LarkUpstream {
94
+ private readonly options;
95
+ /**
96
+ * Real SDK dispatcher: the registered handler is invoked for every
97
+ * `im.message.receive_v1` frame the WS client delivers. Reused across
98
+ * reconnects (the handler is registered once per driver instance).
99
+ */
100
+ private readonly dispatcher;
101
+ private registered;
102
+ private onMessage?;
103
+ constructor(options: LarkSdkUpstreamOptions);
104
+ /**
105
+ * Connect the WS long-connection, route inbound message events into the
106
+ * canonical raw shape, and keep the connection open until `signal` aborts.
107
+ */
108
+ receive(signal: AbortSignal, onMessage: (raw: unknown) => void): Promise<void>;
109
+ private registerOnce;
110
+ sendText(to: string, text: string): Promise<unknown>;
111
+ sendMedia(to: string, media: LarkMediaRef): Promise<unknown>;
112
+ sendFile(to: string, file: LarkFileRef): Promise<unknown>;
113
+ sendInteractive(to: string, text: string, actions: import('@krischoichoi/channel-core').OutboundActionRow[]): Promise<unknown>;
114
+ updateInteractive(cardId: string, text: string, actions: import('@krischoichoi/channel-core').OutboundActionRow[]): Promise<unknown>;
115
+ createCardEntity(cardJson: string): Promise<{
116
+ cardId: string;
117
+ }>;
118
+ sendCardEntity(conversationId: string, cardId: string): Promise<{
119
+ messageId: string;
120
+ }>;
121
+ updateCardElementContent(cardId: string, elementId: string, content: string, sequence: number, uuid: string): Promise<unknown>;
122
+ finishStreamingCard(cardId: string, sequence: number, summary: string): Promise<unknown>;
123
+ startTyping(messageId: string): Promise<void>;
124
+ stopTyping(messageId: string): Promise<void>;
125
+ getChatType(conversationId: string): Promise<'p2p' | 'group' | undefined>;
126
+ }
127
+ export {};
128
+ //# sourceMappingURL=lark-sdk-upstream.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lark-sdk-upstream.d.ts","sourceRoot":"","sources":["../src/lark-sdk-upstream.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Lark SDK upstream driver (official `@larksuiteoapi/node-sdk`).
3
+ *
4
+ * This driver is the ONLY inbound implementation: robot messages arrive over
5
+ * the official SDK's WebSocket long-connection (`WSClient` +
6
+ * `EventDispatcher`, event `im.message.receive_v1`) and interactive-card
7
+ * button presses arrive over `card.action.trigger`. Both are mapped into the
8
+ * canonical raw envelope the adapter's mapper consumes (`{ type, msgId,
9
+ * eventId, senderId, conversationId, chatType, threadId, ... }`) — the mapper
10
+ * is SDK-event-driven, there is no gateway raw shape anywhere.
11
+ *
12
+ * Thread replies are preserved: the SDK's `message.thread_id` / `root_id` /
13
+ * `parent_id` map into the canonical `threadId` the mapper uses to build the
14
+ * thread-scoped SessionBinding.
15
+ *
16
+ * OUTBOUND is delegated to the injected `LarkOutbound` — the official OpenAPI
17
+ * driver (`LarkOpenApiOutbound`) in production, or an injected fake in tests.
18
+ * This bounded split keeps the inbound and outbound legs independently
19
+ * swappable and fully offline-testable.
20
+ *
21
+ * Credentials never appear in this module (the client is built elsewhere from
22
+ * config) and are never logged. Live verification against a real Lark app
23
+ * (AppId/AppSecret) is a manual step — the offline tests inject a fake WS
24
+ * client and drive a real `EventDispatcher` with v1 event envelopes.
25
+ */
26
+ import { EventDispatcher, LoggerLevel, normalizeCardAction } from '@larksuiteoapi/node-sdk';
27
+ import { ChannelError } from '@krischoichoi/channel-core';
28
+ import { z } from 'zod';
29
+ /** Event type key for inbound message delivery (v1 event). */
30
+ export const MESSAGE_EVENT_KEY = 'im.message.receive_v1';
31
+ /** Official event name for Feishu interactive-card button presses. */
32
+ export const CARD_ACTION_EVENT_KEY = 'card.action.trigger';
33
+ /**
34
+ * Parsed v1 message event payload — the flat shape the `EventDispatcher`
35
+ * delivers to the `im.message.receive_v1` handler (header + event fields
36
+ * merged, per the SDK's RequestHandle.parse).
37
+ */
38
+ const optionalStringSchema = z.string().nullish().transform((value) => value ?? undefined);
39
+ const larkMessageEventDataSchema = z.object({
40
+ event_id: optionalStringSchema,
41
+ event_type: optionalStringSchema,
42
+ token: optionalStringSchema,
43
+ create_time: optionalStringSchema,
44
+ sender: z.object({
45
+ sender_id: z.object({
46
+ union_id: optionalStringSchema,
47
+ user_id: optionalStringSchema,
48
+ open_id: optionalStringSchema,
49
+ }).passthrough().optional(),
50
+ sender_type: optionalStringSchema,
51
+ tenant_key: optionalStringSchema,
52
+ }).passthrough().optional(),
53
+ message: z.object({
54
+ message_id: optionalStringSchema,
55
+ root_id: optionalStringSchema,
56
+ parent_id: optionalStringSchema,
57
+ thread_id: optionalStringSchema,
58
+ create_time: optionalStringSchema,
59
+ chat_id: optionalStringSchema,
60
+ chat_type: z.enum(['p2p', 'group']),
61
+ message_type: optionalStringSchema,
62
+ /** JSON-encoded message body per the v1 schema, e.g. '{"text":"hi"}'. */
63
+ content: optionalStringSchema,
64
+ }).passthrough().optional(),
65
+ }).passthrough();
66
+ const actionValueSchema = z.object({ actionId: z.string().trim().min(1) });
67
+ // Trust-boundary validation only. Protocol normalization stays in the
68
+ // official SDK's normalizeCardAction() implementation below.
69
+ const officialCardActionInputSchema = z.object({
70
+ event_id: z.string().optional(),
71
+ context: z.object({
72
+ open_message_id: z.string().optional(),
73
+ open_chat_id: z.string().optional(),
74
+ }).optional(),
75
+ open_message_id: z.string().optional(),
76
+ open_chat_id: z.string().optional(),
77
+ operator: z.object({
78
+ open_id: z.string().optional(),
79
+ user_id: z.string().optional(),
80
+ name: z.string().optional(),
81
+ }).optional(),
82
+ action: z.object({
83
+ value: z.unknown().optional(),
84
+ tag: z.string().optional(),
85
+ name: z.string().optional(),
86
+ option: z.string().optional(),
87
+ }).optional(),
88
+ }).passthrough();
89
+ /**
90
+ * Map one parsed v1 message event into the canonical raw envelope consumed by
91
+ * the inbound mapper (`{ type, msgId, eventId, senderId, conversationId,
92
+ * chatType, threadId, content, ... }`). Returns `undefined` when the event
93
+ * carries no message body.
94
+ */
95
+ export function mapSdkMessageEvent(input) {
96
+ const parsed = larkMessageEventDataSchema.safeParse(input);
97
+ if (!parsed.success) {
98
+ throw new ChannelError('CHANNEL_ERROR', `lark message event payload is invalid: ${zodIssueSummary(parsed.error)}`);
99
+ }
100
+ const data = parsed.data;
101
+ const message = data.message;
102
+ if (!message || typeof message !== 'object')
103
+ return undefined;
104
+ const raw = {
105
+ type: message.message_type,
106
+ msgId: message.message_id,
107
+ eventId: data.event_id,
108
+ senderId: senderIdOf(data.sender),
109
+ conversationId: message.chat_id,
110
+ // Platform chat kind ('p2p' | 'group') is authoritative for ACL identity;
111
+ // Feishu uses oc_ chat ids for both direct and group conversations.
112
+ chatType: message.chat_type,
113
+ };
114
+ // Thread preservation: the mapper keys Harness sessions by
115
+ // conversation.threadId, so replies inside a thread must carry the thread
116
+ // root. thread_id (when present) is canonical; root_id identifies the
117
+ // thread root; parent_id is the direct parent — a fallback for plain
118
+ // replies without a thread.
119
+ const threadId = message.thread_id ?? message.root_id ?? message.parent_id;
120
+ if (threadId)
121
+ raw.threadId = threadId;
122
+ const content = parseContent(message.content);
123
+ switch (message.message_type) {
124
+ case 'text':
125
+ raw.content = content.text;
126
+ break;
127
+ case 'image':
128
+ // SDK image bodies carry an image_key (not a URL); the mapper surfaces
129
+ // it as an opaque resourceRef resolved later by the media port.
130
+ raw.picUrl = content.image_key;
131
+ break;
132
+ case 'audio':
133
+ raw.mediaUrl = content.file_key;
134
+ raw.durationMs = content.duration;
135
+ break;
136
+ case 'media':
137
+ // Feishu video messages use message_type 'media'; the mapper knows 'video'.
138
+ raw.type = 'video';
139
+ raw.mediaUrl = content.file_key;
140
+ raw.durationMs = content.duration;
141
+ break;
142
+ case 'file':
143
+ raw.mediaUrl = content.file_key;
144
+ raw.title = content.file_name;
145
+ break;
146
+ case 'post':
147
+ raw.content = postText(content);
148
+ break;
149
+ default:
150
+ // Keep the SDK message_type; the mapper reports unknown types as
151
+ // unsupported (honest rather than guessing).
152
+ break;
153
+ }
154
+ return raw;
155
+ }
156
+ /**
157
+ * Convert the official SDK-normalized card action into the canonical
158
+ * interaction envelope. The platform callback itself is never hand-parsed —
159
+ * it is first trust-boundary validated with zod and then normalized by the
160
+ * official SDK's `normalizeCardAction()`.
161
+ */
162
+ export async function mapSdkCardAction(input, resolveChatType) {
163
+ const parsed = officialCardActionInputSchema.safeParse(input);
164
+ if (!parsed.success)
165
+ return undefined;
166
+ const event = normalizeCardAction(parsed.data);
167
+ if (!event)
168
+ return undefined;
169
+ const value = actionValueSchema.safeParse(event.action.value);
170
+ if (!value.success)
171
+ return undefined;
172
+ const chatType = await resolveChatType?.(event.chatId);
173
+ if (!chatType)
174
+ return undefined;
175
+ return {
176
+ type: 'interaction',
177
+ msgId: event.messageId,
178
+ // EventDispatcher preserves the official header event_id in its flat
179
+ // payload. Use it when available so a later deliberate press of the same
180
+ // button is not collapsed with a transport retry of an earlier press.
181
+ eventId: parsed.data.event_id ?? `${event.messageId}:${event.operator.openId}:${value.data.actionId}`,
182
+ senderId: event.operator.openId,
183
+ conversationId: event.chatId,
184
+ chatType,
185
+ interactionId: event.messageId,
186
+ action: value.data.actionId,
187
+ value: event.action.value,
188
+ };
189
+ }
190
+ /** SDK-mode implementation of `LarkUpstream` (inbound via the SDK). */
191
+ export class LarkSdkUpstream {
192
+ options;
193
+ /**
194
+ * Real SDK dispatcher: the registered handler is invoked for every
195
+ * `im.message.receive_v1` frame the WS client delivers. Reused across
196
+ * reconnects (the handler is registered once per driver instance).
197
+ */
198
+ dispatcher;
199
+ registered = false;
200
+ onMessage;
201
+ constructor(options) {
202
+ this.options = options;
203
+ // No verification token needed: the WS long-connection dispatches with
204
+ // needCheck: false (tokens/encrypt are webhook-only concerns).
205
+ this.dispatcher = new EventDispatcher({ loggerLevel: LoggerLevel.fatal });
206
+ }
207
+ /**
208
+ * Connect the WS long-connection, route inbound message events into the
209
+ * canonical raw shape, and keep the connection open until `signal` aborts.
210
+ */
211
+ async receive(signal, onMessage) {
212
+ this.onMessage = onMessage;
213
+ this.registerOnce();
214
+ try {
215
+ await this.options.client.start({ eventDispatcher: this.dispatcher });
216
+ }
217
+ catch (error) {
218
+ // Abort-driven teardown exits gracefully; other failures propagate to
219
+ // the adapter, which owns reconnect/backoff.
220
+ if (signal.aborted)
221
+ return;
222
+ throw error;
223
+ }
224
+ if (signal.aborted) {
225
+ this.options.client.close();
226
+ return;
227
+ }
228
+ this.options.onConnected?.();
229
+ await waitForAbort(signal);
230
+ this.options.client.close();
231
+ }
232
+ registerOnce() {
233
+ if (this.registered)
234
+ return;
235
+ this.dispatcher.register({
236
+ [MESSAGE_EVENT_KEY]: (data) => {
237
+ if (!this.onMessage)
238
+ return undefined;
239
+ const raw = mapSdkMessageEvent(data);
240
+ if (raw !== undefined)
241
+ this.onMessage(raw);
242
+ return undefined;
243
+ },
244
+ [CARD_ACTION_EVENT_KEY]: async (data) => {
245
+ if (!this.onMessage)
246
+ return undefined;
247
+ const raw = await mapSdkCardAction(data, this.options.resolveChatType);
248
+ if (raw !== undefined)
249
+ this.onMessage(raw);
250
+ return undefined;
251
+ },
252
+ });
253
+ this.registered = true;
254
+ }
255
+ sendText(to, text) {
256
+ return this.options.outbound.sendText(to, text);
257
+ }
258
+ sendMedia(to, media) {
259
+ return this.options.outbound.sendMedia(to, media);
260
+ }
261
+ sendFile(to, file) {
262
+ return this.options.outbound.sendFile(to, file);
263
+ }
264
+ sendInteractive(to, text, actions) {
265
+ return this.options.outbound.sendInteractive(to, text, actions);
266
+ }
267
+ updateInteractive(cardId, text, actions) {
268
+ return this.options.outbound.updateInteractive(cardId, text, actions);
269
+ }
270
+ createCardEntity(cardJson) {
271
+ return this.options.outbound.createCardEntity(cardJson);
272
+ }
273
+ sendCardEntity(conversationId, cardId) {
274
+ return this.options.outbound.sendCardEntity(conversationId, cardId);
275
+ }
276
+ updateCardElementContent(cardId, elementId, content, sequence, uuid) {
277
+ return this.options.outbound.updateCardElementContent(cardId, elementId, content, sequence, uuid);
278
+ }
279
+ finishStreamingCard(cardId, sequence, summary) {
280
+ return this.options.outbound.finishStreamingCard(cardId, sequence, summary);
281
+ }
282
+ startTyping(messageId) {
283
+ return this.options.outbound.startTyping?.(messageId) ?? Promise.resolve();
284
+ }
285
+ stopTyping(messageId) {
286
+ return this.options.outbound.stopTyping?.(messageId) ?? Promise.resolve();
287
+ }
288
+ getChatType(conversationId) {
289
+ return this.options.outbound.getChatType?.(conversationId) ?? Promise.resolve(undefined);
290
+ }
291
+ }
292
+ /** Open id first (the id Harness sessions key on), then union/user ids. */
293
+ function senderIdOf(sender) {
294
+ const id = sender?.sender_id;
295
+ return id?.open_id ?? id?.union_id ?? id?.user_id;
296
+ }
297
+ function parseContent(content) {
298
+ if (!content)
299
+ return {};
300
+ try {
301
+ const parsed = z.record(z.string(), z.unknown()).safeParse(JSON.parse(content));
302
+ return parsed.success ? parsed.data : {};
303
+ }
304
+ catch {
305
+ return {};
306
+ }
307
+ }
308
+ function zodIssueSummary(error) {
309
+ return error.issues
310
+ .slice(0, 3)
311
+ .map((issue) => `${issue.path.join('.') || '<root>'}: ${issue.message}`)
312
+ .join('; ');
313
+ }
314
+ /** Best-effort plain-text extraction from a rich-text 'post' body. */
315
+ function postText(content) {
316
+ const rows = content.content;
317
+ if (!Array.isArray(rows))
318
+ return '';
319
+ const segments = [];
320
+ for (const row of rows) {
321
+ if (!Array.isArray(row))
322
+ continue;
323
+ for (const node of row) {
324
+ const item = node;
325
+ if (item && typeof item === 'object' && typeof item.text === 'string') {
326
+ segments.push(item.text);
327
+ }
328
+ }
329
+ }
330
+ return segments.join('');
331
+ }
332
+ function waitForAbort(signal) {
333
+ if (signal.aborted)
334
+ return Promise.resolve();
335
+ return new Promise((resolve) => {
336
+ signal.addEventListener('abort', () => resolve(), { once: true });
337
+ });
338
+ }
339
+ //# sourceMappingURL=lark-sdk-upstream.js.map