@hitl-sdk/adapter-line 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +167 -0
- package/dist/adapter.d.ts +23 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/client.d.ts +29 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/constants.d.ts +19 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/destination.d.ts +16 -0
- package/dist/destination.d.ts.map +1 -0
- package/dist/external-id.d.ts +6 -0
- package/dist/external-id.d.ts.map +1 -0
- package/dist/feedback.d.ts +31 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/fields.d.ts +8 -0
- package/dist/fields.d.ts.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +718 -0
- package/dist/postback.d.ts +12 -0
- package/dist/postback.d.ts.map +1 -0
- package/dist/render.d.ts +10 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/webhook.d.ts +22 -0
- package/dist/webhook.d.ts.map +1 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Unbounded Pioneering
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# @hitl-sdk/adapter-line
|
|
2
|
+
|
|
3
|
+
A hitl channel adapter for the [LINE Messaging API](https://developers.line.biz/en/docs/messaging-api/). Deliver approvals as Flex Messages with postback buttons; resolve through `hitl.inbox` via a LINE webhook. Text and multi-field feedback uses a LIFF form.
|
|
4
|
+
|
|
5
|
+
For Slack, Teams, Discord, and other Chat SDK platforms, use [`@hitl-sdk/adapter-chat-sdk`](../adapter-chat-sdk/README.md) instead.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @hitl-sdk/adapter-line @line/bot-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`@line/bot-sdk` is a peer dependency.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Hitl } from "@hitl-sdk/hitl";
|
|
19
|
+
import { LineBotClient } from "@line/bot-sdk";
|
|
20
|
+
import { createLineAdapter } from "@hitl-sdk/adapter-line";
|
|
21
|
+
import { workflowResolver } from "@hitl-sdk/resolver-workflow-sdk";
|
|
22
|
+
|
|
23
|
+
const client = LineBotClient.fromChannelAccessToken({
|
|
24
|
+
channelAccessToken: process.env.LINE_CHANNEL_ACCESS_TOKEN!,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const hitl = new Hitl({
|
|
28
|
+
resolver: workflowResolver(),
|
|
29
|
+
adapters: [
|
|
30
|
+
createLineAdapter({
|
|
31
|
+
id: "line-approvals",
|
|
32
|
+
client,
|
|
33
|
+
defaultChannel: "user:Uxxxxxxxx",
|
|
34
|
+
inbox: () => hitl.inbox,
|
|
35
|
+
// Required when actions use text/textarea or multiple feedback fields:
|
|
36
|
+
liffId: process.env.LINE_LIFF_ID,
|
|
37
|
+
feedbackSecret: process.env.LINE_FEEDBACK_SECRET,
|
|
38
|
+
}),
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Express
|
|
44
|
+
|
|
45
|
+
Use `line.middleware` for signature validation and `hitl.handler` for the internal API + LIFF feedback. Branch postbacks with `parsePostback`:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import express from "express";
|
|
49
|
+
import { middleware } from "@line/bot-sdk";
|
|
50
|
+
import { handlePostbackEvent, parsePostback } from "@hitl-sdk/adapter-line";
|
|
51
|
+
|
|
52
|
+
const app = express();
|
|
53
|
+
|
|
54
|
+
// Workflow internal API + LIFF feedback (GET/POST)
|
|
55
|
+
app.all("/.well-known/hitl/v1/*", (req, res) => {
|
|
56
|
+
void hitl.handler(req, res);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Messaging API webhook
|
|
60
|
+
app.post(
|
|
61
|
+
"/webhook",
|
|
62
|
+
middleware({ channelSecret: process.env.LINE_CHANNEL_SECRET! }),
|
|
63
|
+
async (req, res) => {
|
|
64
|
+
res.sendStatus(200);
|
|
65
|
+
|
|
66
|
+
for (const event of req.body.events ?? []) {
|
|
67
|
+
if (event.type === "postback") {
|
|
68
|
+
if (parsePostback(event.postback.data)) {
|
|
69
|
+
await handlePostbackEvent(event, { client, inbox: hitl.inbox });
|
|
70
|
+
} else {
|
|
71
|
+
await handleMyPostback(event);
|
|
72
|
+
}
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (event.type === "message") {
|
|
77
|
+
await handleMyMessage(event);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
app.listen(3000);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
`parsePostback` returns a payload only for hitl Flex buttons. Run this after `line.middleware` (signature already validated). Keep the webhook URL you already registered in LINE Console. Do not apply `express.json()` on `/webhook` before `line.middleware`.
|
|
87
|
+
|
|
88
|
+
Omit the `else` branch when you only need hitl approvals.
|
|
89
|
+
|
|
90
|
+
### Fetch-based (Next.js, Hono)
|
|
91
|
+
|
|
92
|
+
When you are not on Express, use `createLineWebhookHandler`. It validates `x-line-signature` and processes hitl postbacks from a raw `Request`.
|
|
93
|
+
|
|
94
|
+
Mount the hitl internal API + LIFF feedback route as well:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
// Next.js App Router
|
|
98
|
+
// app/.well-known/hitl/v1/[[...path]]/route.ts
|
|
99
|
+
export const { GET, POST } = hitl.routeHandlers;
|
|
100
|
+
|
|
101
|
+
// app/api/webhooks/line/route.ts
|
|
102
|
+
import { createLineWebhookHandler } from "@hitl-sdk/adapter-line";
|
|
103
|
+
import { hitl, client } from "@/lib/hitl";
|
|
104
|
+
|
|
105
|
+
export const POST = createLineWebhookHandler({
|
|
106
|
+
channelSecret: process.env.LINE_CHANNEL_SECRET!,
|
|
107
|
+
client,
|
|
108
|
+
inbox: () => hitl.inbox,
|
|
109
|
+
onFallbackEvent: async (event) => {
|
|
110
|
+
if (event.type === "postback") {
|
|
111
|
+
await handleMyPostback(event);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (event.type === "message") {
|
|
115
|
+
await handleMyMessage(event);
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`onFallbackEvent` receives everything hitl does not handle (non-postback events and custom postbacks). Omit it when you only need hitl approvals. Register the webhook route URL in LINE Console.
|
|
122
|
+
|
|
123
|
+
### LIFF setup (text / textarea / multi-field actions)
|
|
124
|
+
|
|
125
|
+
When `feedbackSecret` is set, the adapter serves LIFF feedback at:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
/.well-known/hitl/v1/channels/line/feedback
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Create a **dedicated LIFF app** for hitl feedback (recommended when you already use LIFF for something else). Set its **Endpoint URL** in LINE Developers Console to:
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
https://{your-domain}/.well-known/hitl/v1/channels/line/feedback
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Pass that app's LIFF ID to `createLineAdapter({ liffId })`.
|
|
138
|
+
|
|
139
|
+
### Routing keys
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
await waitForHuman({
|
|
143
|
+
channel: "line-approvals:user:U456",
|
|
144
|
+
message: "Approve?",
|
|
145
|
+
actions,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
await waitForHuman({ channel: "line-approvals", message: "...", actions }); // uses defaultChannel
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Destination format: `user:Uxxx`, `group:Cxxx`, or `room:Rxxx`.
|
|
152
|
+
|
|
153
|
+
### Feedback fields
|
|
154
|
+
|
|
155
|
+
| Field kinds | UX |
|
|
156
|
+
|---|---|
|
|
157
|
+
| None | Postback button resolves immediately |
|
|
158
|
+
| Single `select` or `confirm` | Second Flex message with option buttons |
|
|
159
|
+
| `text`, `textarea`, or multiple fields | LIFF form (`liffId` + `feedbackSecret` on the adapter) |
|
|
160
|
+
|
|
161
|
+
`TimelineAnchor.externalRef` uses `destination#messageId` (e.g. `user:U123#msg-abc`).
|
|
162
|
+
|
|
163
|
+
### Outcome updates
|
|
164
|
+
|
|
165
|
+
LINE cannot edit sent messages. After resolve, the adapter pushes a follow-up text message with the outcome (same graceful behavior as other adapters after a process restart).
|
|
166
|
+
|
|
167
|
+
See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full flow.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HitlAdapter } from "@hitl-sdk/hitl/adapter";
|
|
2
|
+
import type { HitlInbox } from "@hitl-sdk/hitl/state";
|
|
3
|
+
import { type LineMessagingClient } from "./client.js";
|
|
4
|
+
export interface LineAdapterOptions {
|
|
5
|
+
/** Adapter id, the routing key prefix used by `waitForHuman({ channel })`. */
|
|
6
|
+
id: string;
|
|
7
|
+
/** LINE Messaging API client (`LineBotClient.fromChannelAccessToken(...)`). */
|
|
8
|
+
client: LineMessagingClient;
|
|
9
|
+
/** Default destination when the routing key is adapter id only, e.g. `user:U123`. */
|
|
10
|
+
defaultChannel?: string;
|
|
11
|
+
/**
|
|
12
|
+
* The hitl inbox, resolved lazily. `new Hitl()` needs the adapters before
|
|
13
|
+
* the inbox exists, so pass `() => hitl.inbox`.
|
|
14
|
+
*/
|
|
15
|
+
inbox: () => HitlInbox;
|
|
16
|
+
/** LIFF app id for actions that need text/textarea or multi-field feedback. */
|
|
17
|
+
liffId?: string;
|
|
18
|
+
/** HMAC secret for signed LIFF feedback tokens. Required when `liffId` is set. */
|
|
19
|
+
feedbackSecret?: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function createLineAdapter(options: LineAdapterOptions): HitlAdapter;
|
|
22
|
+
export type { LineMessagingClient };
|
|
23
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,WAAW,EAAgB,MAAM,wBAAwB,CAAC;AACnG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAqB,KAAK,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAQ1E,MAAM,WAAW,kBAAkB;IACjC,8EAA8E;IAC9E,EAAE,EAAE,MAAM,CAAC;IACX,+EAA+E;IAC/E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;OAGG;IACH,KAAK,EAAE,MAAM,SAAS,CAAC;IACvB,+EAA+E;IAC/E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAeD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAgF1E;AAED,YAAY,EAAE,mBAAmB,EAAE,CAAC"}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { messagingApi } from "@line/bot-sdk";
|
|
2
|
+
type Message = messagingApi.Message;
|
|
3
|
+
/** Minimal LINE client surface used by the adapter. */
|
|
4
|
+
export interface LineMessagingClient {
|
|
5
|
+
pushMessage(args: {
|
|
6
|
+
to: string;
|
|
7
|
+
messages: Message[];
|
|
8
|
+
}): Promise<{
|
|
9
|
+
sentMessages?: Array<{
|
|
10
|
+
id?: string;
|
|
11
|
+
}>;
|
|
12
|
+
}>;
|
|
13
|
+
replyMessage(args: {
|
|
14
|
+
replyToken: string;
|
|
15
|
+
messages: Message[];
|
|
16
|
+
}): Promise<unknown>;
|
|
17
|
+
getProfile(userId: string): Promise<{
|
|
18
|
+
displayName?: string;
|
|
19
|
+
userId?: string;
|
|
20
|
+
}>;
|
|
21
|
+
}
|
|
22
|
+
export declare function toLineClient(client: {
|
|
23
|
+
pushMessage: LineMessagingClient["pushMessage"];
|
|
24
|
+
replyMessage: LineMessagingClient["replyMessage"];
|
|
25
|
+
getProfile: LineMessagingClient["getProfile"];
|
|
26
|
+
}): LineMessagingClient;
|
|
27
|
+
export declare function pushToDestination(client: LineMessagingClient, destinationRef: string, messages: Message[]): Promise<string>;
|
|
28
|
+
export {};
|
|
29
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,KAAK,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC;AAEpC,uDAAuD;AACvD,MAAM,WAAW,mBAAmB;IAClC,WAAW,CAAC,IAAI,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,YAAY,CAAC,EAAE,KAAK,CAAC;YAAE,EAAE,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;IAC3G,YAAY,CAAC,IAAI,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClF,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChF;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE;IACnC,WAAW,EAAE,mBAAmB,CAAC,aAAa,CAAC,CAAC;IAChD,YAAY,EAAE,mBAAmB,CAAC,cAAc,CAAC,CAAC;IAClD,UAAU,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;CAC/C,GAAG,mBAAmB,CAEtB;AAID,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,mBAAmB,EAC3B,cAAc,EAAE,MAAM,EACtB,QAAQ,EAAE,OAAO,EAAE,GAClB,OAAO,CAAC,MAAM,CAAC,CAQjB"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** HTTP channel key under `/.well-known/hitl/v1/channels/{channelKey}/`. */
|
|
2
|
+
export declare const LINE_CHANNEL_KEY = "line";
|
|
3
|
+
/** Fixed LIFF feedback path. Must stay aligned with `CHANNELS_BASE_PATH` in `@hitl-sdk/hitl/client`. */
|
|
4
|
+
export declare const LINE_FEEDBACK_PATH = "/.well-known/hitl/v1/channels/line/feedback";
|
|
5
|
+
/** Postback payload kinds carried in LINE postback `data` (max 300 chars). */
|
|
6
|
+
export declare const POSTBACK_KIND_ACTION = "a";
|
|
7
|
+
export declare const POSTBACK_KIND_FIELD_VALUE = "v";
|
|
8
|
+
export declare const CONFIRM_YES = "yes";
|
|
9
|
+
export declare const CONFIRM_NO = "no";
|
|
10
|
+
export interface PostbackPayload {
|
|
11
|
+
k: typeof POSTBACK_KIND_ACTION | typeof POSTBACK_KIND_FIELD_VALUE;
|
|
12
|
+
r: string;
|
|
13
|
+
a: string;
|
|
14
|
+
f?: string;
|
|
15
|
+
v?: string;
|
|
16
|
+
}
|
|
17
|
+
export declare function encodePostback(payload: PostbackPayload): string;
|
|
18
|
+
export declare function parsePostback(data: string): PostbackPayload | undefined;
|
|
19
|
+
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,eAAO,MAAM,gBAAgB,SAAS,CAAC;AAEvC,wGAAwG;AACxG,eAAO,MAAM,kBAAkB,gDAAgD,CAAC;AAEhF,8EAA8E;AAE9E,eAAO,MAAM,oBAAoB,MAAM,CAAC;AACxC,eAAO,MAAM,yBAAyB,MAAM,CAAC;AAE7C,eAAO,MAAM,WAAW,QAAQ,CAAC;AACjC,eAAO,MAAM,UAAU,OAAO,CAAC;AAE/B,MAAM,WAAW,eAAe;IAC9B,CAAC,EAAE,OAAO,oBAAoB,GAAG,OAAO,yBAAyB,CAAC;IAClE,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,CAAC,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAE/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAcvE"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type LineDestinationKind = "user" | "group" | "room";
|
|
2
|
+
export interface LineDestination {
|
|
3
|
+
kind: LineDestinationKind;
|
|
4
|
+
/** Full routing ref, e.g. `user:U123`. */
|
|
5
|
+
ref: string;
|
|
6
|
+
/** Push API `to` value (id without kind prefix). */
|
|
7
|
+
to: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function parseDestination(dest: string): LineDestination;
|
|
10
|
+
export declare function destinationFromSource(source: {
|
|
11
|
+
type?: string;
|
|
12
|
+
userId?: string;
|
|
13
|
+
groupId?: string;
|
|
14
|
+
roomId?: string;
|
|
15
|
+
}): string | undefined;
|
|
16
|
+
//# sourceMappingURL=destination.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destination.d.ts","sourceRoot":"","sources":["../src/destination.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,mBAAmB,GAAG,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC;AAE5D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,mBAAmB,CAAC;IAC1B,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAC;CACZ;AAID,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAc9D;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE;IAC5C,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,MAAM,GAAG,SAAS,CAWrB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-id.d.ts","sourceRoot":"","sources":["../src/external-id.ts"],"names":[],"mappings":"AAEA,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAE/E;AAED,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAS/F"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type HitlField } from "@hitl-sdk/hitl/adapter";
|
|
2
|
+
import type { HitlInbox } from "@hitl-sdk/hitl/state";
|
|
3
|
+
export interface FeedbackTokenPayload {
|
|
4
|
+
requestId: string;
|
|
5
|
+
actionId: string;
|
|
6
|
+
exp: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function signFeedbackToken(opts: {
|
|
9
|
+
requestId: string;
|
|
10
|
+
actionId: string;
|
|
11
|
+
secret: string;
|
|
12
|
+
ttlSeconds?: number;
|
|
13
|
+
}): string;
|
|
14
|
+
export declare function verifyFeedbackToken(token: string, secret: string): FeedbackTokenPayload | undefined;
|
|
15
|
+
export declare function buildLiffUri(liffId: string, token: string): string;
|
|
16
|
+
export declare function buildFeedbackFormHtml(opts: {
|
|
17
|
+
fields: Record<string, HitlField>;
|
|
18
|
+
token: string;
|
|
19
|
+
submitUrl: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
}): string;
|
|
22
|
+
export interface LineFeedbackHandlerOptions {
|
|
23
|
+
inbox: () => HitlInbox;
|
|
24
|
+
secret: string;
|
|
25
|
+
}
|
|
26
|
+
export declare function createLineFeedbackHandler(options: LineFeedbackHandlerOptions): (request: Request) => Promise<Response>;
|
|
27
|
+
export declare function createLineFeedbackFormHandler(options: {
|
|
28
|
+
secret: string;
|
|
29
|
+
inbox: () => HitlInbox;
|
|
30
|
+
}): (request: Request) => Promise<Response>;
|
|
31
|
+
//# sourceMappingURL=feedback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"feedback.d.ts","sourceRoot":"","sources":["../src/feedback.ts"],"names":[],"mappings":"AACA,OAAO,EAA+C,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACrG,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAItD,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;CACb;AAgBD,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GACjF,MAAM,CAKR;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS,CAiBnG;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAElE;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAC1C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GAAG,MAAM,CAoDT;AAmCD,MAAM,WAAW,0BAA0B;IACzC,KAAK,EAAE,MAAM,SAAS,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,0BAA0B,GAClC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAwDzC;AAED,wBAAgB,6BAA6B,CAAC,OAAO,EAAE;IACrD,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,SAAS,CAAC;CACxB,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CA4B1C"}
|
package/dist/fields.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { HitlField } from "@hitl-sdk/hitl/adapter";
|
|
2
|
+
export declare function needsFeedbackStep(fields: Record<string, HitlField>): boolean;
|
|
3
|
+
/** Inline Flex can collect a single select or confirm field without LIFF. */
|
|
4
|
+
export declare function canUseInlineFlex(fields: Record<string, HitlField>): boolean;
|
|
5
|
+
export declare function needsLiff(fields: Record<string, HitlField>): boolean;
|
|
6
|
+
export declare function parseFieldValue(field: HitlField, raw: string): unknown;
|
|
7
|
+
export declare function parsePostbackFeedbacks(fields: Record<string, HitlField>, fieldKey: string, rawValue: string): Record<string, unknown>;
|
|
8
|
+
//# sourceMappingURL=fields.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fields.d.ts","sourceRoot":"","sources":["../src/fields.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAGxD,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,OAAO,CAE5E;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,OAAO,CAK3E;AAED,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,OAAO,CAEpE;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAQtE;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,EACjC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIzB"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createLineAdapter, type LineAdapterOptions, type LineMessagingClient } from "./adapter.js";
|
|
2
|
+
export { createLineWebhookHandler, handleLineWebhookBody, handleLineWebhookEvents, LineWebhookError } from "./webhook.js";
|
|
3
|
+
export type { LineWebhookEventsOptions, LineWebhookHandlerOptions } from "./webhook.js";
|
|
4
|
+
export { createLineFeedbackHandler, createLineFeedbackFormHandler, signFeedbackToken, verifyFeedbackToken, buildLiffUri, buildFeedbackFormHtml, type LineFeedbackHandlerOptions, type FeedbackTokenPayload, } from "./feedback.js";
|
|
5
|
+
export { encodeExternalId, decodeExternalId } from "./external-id.js";
|
|
6
|
+
export { parseDestination, destinationFromSource, type LineDestination } from "./destination.js";
|
|
7
|
+
export { LINE_CHANNEL_KEY, LINE_FEEDBACK_PATH, POSTBACK_KIND_ACTION, POSTBACK_KIND_FIELD_VALUE, encodePostback, parsePostback, } from "./constants.js";
|
|
8
|
+
export { handlePostbackEvent } from "./postback.js";
|
|
9
|
+
export { needsFeedbackStep, canUseInlineFlex, needsLiff, } from "./fields.js";
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAC1H,YAAY,EAAE,wBAAwB,EAAE,yBAAyB,EAAE,MAAM,cAAc,CAAC;AACxF,OAAO,EACL,yBAAyB,EACzB,6BAA6B,EAC7B,iBAAiB,EACjB,mBAAmB,EACnB,YAAY,EACZ,qBAAqB,EACrB,KAAK,0BAA0B,EAC/B,KAAK,oBAAoB,GAC1B,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACjG,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,oBAAoB,EACpB,yBAAyB,EACzB,cAAc,EACd,aAAa,GACd,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,SAAS,GACV,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
// src/feedback.ts
|
|
2
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { actionById, actionFields, validateFeedbacks } from "@hitl-sdk/hitl/adapter";
|
|
4
|
+
|
|
5
|
+
// src/constants.ts
|
|
6
|
+
var LINE_CHANNEL_KEY = "line";
|
|
7
|
+
var LINE_FEEDBACK_PATH = "/.well-known/hitl/v1/channels/line/feedback";
|
|
8
|
+
var POSTBACK_KIND_ACTION = "a";
|
|
9
|
+
var POSTBACK_KIND_FIELD_VALUE = "v";
|
|
10
|
+
var CONFIRM_YES = "yes";
|
|
11
|
+
var CONFIRM_NO = "no";
|
|
12
|
+
function encodePostback(payload) {
|
|
13
|
+
return JSON.stringify(payload);
|
|
14
|
+
}
|
|
15
|
+
function parsePostback(data) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(data);
|
|
18
|
+
if (parsed.k !== POSTBACK_KIND_ACTION && parsed.k !== POSTBACK_KIND_FIELD_VALUE || typeof parsed.r !== "string" || typeof parsed.a !== "string") {
|
|
19
|
+
return void 0;
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
} catch {
|
|
23
|
+
return void 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// src/fields.ts
|
|
28
|
+
function needsFeedbackStep(fields) {
|
|
29
|
+
return Object.keys(fields).length > 0;
|
|
30
|
+
}
|
|
31
|
+
function canUseInlineFlex(fields) {
|
|
32
|
+
const entries = Object.entries(fields);
|
|
33
|
+
if (entries.length !== 1) return false;
|
|
34
|
+
const field = entries[0][1];
|
|
35
|
+
return field.kind === "select" || field.kind === "confirm";
|
|
36
|
+
}
|
|
37
|
+
function needsLiff(fields) {
|
|
38
|
+
return needsFeedbackStep(fields) && !canUseInlineFlex(fields);
|
|
39
|
+
}
|
|
40
|
+
function parseFieldValue(field, raw) {
|
|
41
|
+
if (field.kind === "confirm") {
|
|
42
|
+
const normalized = raw.trim().toLowerCase();
|
|
43
|
+
if (normalized === CONFIRM_YES || normalized === "true") return true;
|
|
44
|
+
if (normalized === CONFIRM_NO || normalized === "false") return false;
|
|
45
|
+
return raw;
|
|
46
|
+
}
|
|
47
|
+
return raw;
|
|
48
|
+
}
|
|
49
|
+
function parsePostbackFeedbacks(fields, fieldKey, rawValue) {
|
|
50
|
+
const field = fields[fieldKey];
|
|
51
|
+
if (!field) return {};
|
|
52
|
+
return { [fieldKey]: parseFieldValue(field, rawValue) };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/feedback.ts
|
|
56
|
+
var TOKEN_SEP = ".";
|
|
57
|
+
function base64UrlEncode(data) {
|
|
58
|
+
return Buffer.from(data, "utf8").toString("base64url");
|
|
59
|
+
}
|
|
60
|
+
function base64UrlDecode(data) {
|
|
61
|
+
return Buffer.from(data, "base64url").toString("utf8");
|
|
62
|
+
}
|
|
63
|
+
function signPayload(payload, secret) {
|
|
64
|
+
return createHmac("sha256", secret).update(payload).digest("base64url");
|
|
65
|
+
}
|
|
66
|
+
function signFeedbackToken(opts) {
|
|
67
|
+
const exp = Math.floor(Date.now() / 1e3) + (opts.ttlSeconds ?? 3600);
|
|
68
|
+
const body = base64UrlEncode(JSON.stringify({ requestId: opts.requestId, actionId: opts.actionId, exp }));
|
|
69
|
+
const sig = signPayload(body, opts.secret);
|
|
70
|
+
return `${body}${TOKEN_SEP}${sig}`;
|
|
71
|
+
}
|
|
72
|
+
function verifyFeedbackToken(token, secret) {
|
|
73
|
+
const sep = token.lastIndexOf(TOKEN_SEP);
|
|
74
|
+
if (sep === -1) return void 0;
|
|
75
|
+
const body = token.slice(0, sep);
|
|
76
|
+
const sig = token.slice(sep + 1);
|
|
77
|
+
const expected = signPayload(body, secret);
|
|
78
|
+
const a = Buffer.from(sig);
|
|
79
|
+
const b = Buffer.from(expected);
|
|
80
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) return void 0;
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(base64UrlDecode(body));
|
|
83
|
+
if (typeof parsed.requestId !== "string" || typeof parsed.actionId !== "string") return void 0;
|
|
84
|
+
if (typeof parsed.exp !== "number" || parsed.exp < Math.floor(Date.now() / 1e3)) return void 0;
|
|
85
|
+
return parsed;
|
|
86
|
+
} catch {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function buildLiffUri(liffId, token) {
|
|
91
|
+
return `https://liff.line.me/${liffId}?token=${encodeURIComponent(token)}`;
|
|
92
|
+
}
|
|
93
|
+
function buildFeedbackFormHtml(opts) {
|
|
94
|
+
const { fields, token, submitUrl, title = "Submit feedback" } = opts;
|
|
95
|
+
const inputs = Object.entries(fields).map(([key, field]) => renderFieldInput(key, field)).join("\n");
|
|
96
|
+
return `<!DOCTYPE html>
|
|
97
|
+
<html lang="en">
|
|
98
|
+
<head>
|
|
99
|
+
<meta charset="utf-8" />
|
|
100
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
101
|
+
<title>${escapeHtml(title)}</title>
|
|
102
|
+
<style>
|
|
103
|
+
body { font-family: system-ui, sans-serif; margin: 1rem; }
|
|
104
|
+
label { display: block; margin: 0.75rem 0 0.25rem; font-weight: 600; }
|
|
105
|
+
input, textarea, select { width: 100%; box-sizing: border-box; padding: 0.5rem; }
|
|
106
|
+
button { margin-top: 1rem; padding: 0.75rem 1rem; width: 100%; }
|
|
107
|
+
</style>
|
|
108
|
+
<script src="https://static.line-scdn.net/liff/edge/2/sdk.js"></script>
|
|
109
|
+
</head>
|
|
110
|
+
<body>
|
|
111
|
+
<h1>${escapeHtml(title)}</h1>
|
|
112
|
+
<form id="form">
|
|
113
|
+
${inputs}
|
|
114
|
+
<button type="submit">Submit</button>
|
|
115
|
+
</form>
|
|
116
|
+
<script>
|
|
117
|
+
const token = ${JSON.stringify(token)};
|
|
118
|
+
const submitUrl = ${JSON.stringify(submitUrl)};
|
|
119
|
+
async function init() {
|
|
120
|
+
await liff.init({});
|
|
121
|
+
}
|
|
122
|
+
document.getElementById("form").addEventListener("submit", async (e) => {
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
const form = e.target;
|
|
125
|
+
const data = new FormData(form);
|
|
126
|
+
const feedbacks = Object.fromEntries(data.entries());
|
|
127
|
+
const res = await fetch(submitUrl, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers: { "Content-Type": "application/json" },
|
|
130
|
+
body: JSON.stringify({ token, feedbacks }),
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
alert("Submit failed");
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (liff.isInClient()) await liff.closeWindow();
|
|
137
|
+
});
|
|
138
|
+
init().catch(console.error);
|
|
139
|
+
</script>
|
|
140
|
+
</body>
|
|
141
|
+
</html>`;
|
|
142
|
+
}
|
|
143
|
+
function renderFieldInput(key, field) {
|
|
144
|
+
const id = escapeHtml(key);
|
|
145
|
+
const label = escapeHtml(field.label);
|
|
146
|
+
switch (field.kind) {
|
|
147
|
+
case "text":
|
|
148
|
+
return `<label for="${id}">${label}</label><input id="${id}" name="${id}" type="text" value="${escapeHtml(field.default ?? "")}" />`;
|
|
149
|
+
case "textarea":
|
|
150
|
+
return `<label for="${id}">${label}</label><textarea id="${id}" name="${id}">${escapeHtml(field.default ?? "")}</textarea>`;
|
|
151
|
+
case "select": {
|
|
152
|
+
const options = field.options.map(
|
|
153
|
+
(o) => `<option value="${escapeHtml(o)}"${field.default === o ? " selected" : ""}>${escapeHtml(o)}</option>`
|
|
154
|
+
).join("");
|
|
155
|
+
return `<label for="${id}">${label}</label><select id="${id}" name="${id}">${options}</select>`;
|
|
156
|
+
}
|
|
157
|
+
case "confirm": {
|
|
158
|
+
const yes = field.default === true ? " selected" : "";
|
|
159
|
+
const no = field.default === false ? " selected" : "";
|
|
160
|
+
return `<label for="${id}">${label}</label><select id="${id}" name="${id}"><option value="yes"${yes}>Yes</option><option value="no"${no}>No</option></select>`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function escapeHtml(value) {
|
|
165
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """);
|
|
166
|
+
}
|
|
167
|
+
function createLineFeedbackHandler(options) {
|
|
168
|
+
return async (request) => {
|
|
169
|
+
if (request.method !== "POST") {
|
|
170
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
171
|
+
}
|
|
172
|
+
let body;
|
|
173
|
+
try {
|
|
174
|
+
body = await request.json();
|
|
175
|
+
} catch {
|
|
176
|
+
return new Response("Invalid JSON", { status: 400 });
|
|
177
|
+
}
|
|
178
|
+
if (!body.token || !body.feedbacks) {
|
|
179
|
+
return new Response("Missing token or feedbacks", { status: 400 });
|
|
180
|
+
}
|
|
181
|
+
const parsed = verifyFeedbackToken(body.token, options.secret);
|
|
182
|
+
if (!parsed) {
|
|
183
|
+
return new Response("Invalid or expired token", { status: 401 });
|
|
184
|
+
}
|
|
185
|
+
const inbox = options.inbox();
|
|
186
|
+
const record = await inbox.get(parsed.requestId);
|
|
187
|
+
if (!record) {
|
|
188
|
+
return new Response("Request not found", { status: 404 });
|
|
189
|
+
}
|
|
190
|
+
const def = actionById(record.actions, parsed.actionId);
|
|
191
|
+
if (!def) {
|
|
192
|
+
return new Response("Unknown action", { status: 400 });
|
|
193
|
+
}
|
|
194
|
+
const fields = actionFields(def);
|
|
195
|
+
const coerced = {};
|
|
196
|
+
for (const [key, field] of Object.entries(fields)) {
|
|
197
|
+
const raw = body.feedbacks[key];
|
|
198
|
+
if (raw === void 0) continue;
|
|
199
|
+
coerced[key] = typeof raw === "string" ? parseFieldValue(field, raw) : raw;
|
|
200
|
+
}
|
|
201
|
+
let feedbacks;
|
|
202
|
+
try {
|
|
203
|
+
feedbacks = validateFeedbacks(fields, coerced);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
const message = err instanceof Error ? err.message : "Validation failed";
|
|
206
|
+
return new Response(message, { status: 400 });
|
|
207
|
+
}
|
|
208
|
+
await inbox.resolve(parsed.requestId, {
|
|
209
|
+
actionId: parsed.actionId,
|
|
210
|
+
feedbacks
|
|
211
|
+
});
|
|
212
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
213
|
+
status: 200,
|
|
214
|
+
headers: { "Content-Type": "application/json" }
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function createLineFeedbackFormHandler(options) {
|
|
219
|
+
return async (request) => {
|
|
220
|
+
const url = new URL(request.url);
|
|
221
|
+
const token = url.searchParams.get("token");
|
|
222
|
+
if (!token) {
|
|
223
|
+
return new Response("Missing token", { status: 400 });
|
|
224
|
+
}
|
|
225
|
+
const parsed = verifyFeedbackToken(token, options.secret);
|
|
226
|
+
if (!parsed) {
|
|
227
|
+
return new Response("Invalid or expired token", { status: 401 });
|
|
228
|
+
}
|
|
229
|
+
const record = await options.inbox().get(parsed.requestId);
|
|
230
|
+
if (!record) {
|
|
231
|
+
return new Response("Request not found", { status: 404 });
|
|
232
|
+
}
|
|
233
|
+
const def = actionById(record.actions, parsed.actionId);
|
|
234
|
+
if (!def) {
|
|
235
|
+
return new Response("Unknown action", { status: 400 });
|
|
236
|
+
}
|
|
237
|
+
const fields = actionFields(def);
|
|
238
|
+
const html = buildFeedbackFormHtml({
|
|
239
|
+
fields,
|
|
240
|
+
token,
|
|
241
|
+
submitUrl: LINE_FEEDBACK_PATH,
|
|
242
|
+
title: def.label ?? def.id
|
|
243
|
+
});
|
|
244
|
+
return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8" } });
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/destination.ts
|
|
249
|
+
var KINDS = ["user", "group", "room"];
|
|
250
|
+
function parseDestination(dest) {
|
|
251
|
+
const colon = dest.indexOf(":");
|
|
252
|
+
if (colon === -1) {
|
|
253
|
+
throw new Error(`Invalid LINE destination "${dest}"; expected user:, group:, or room: prefix.`);
|
|
254
|
+
}
|
|
255
|
+
const kind = dest.slice(0, colon);
|
|
256
|
+
if (!KINDS.includes(kind)) {
|
|
257
|
+
throw new Error(`Invalid LINE destination "${dest}"; expected user:, group:, or room: prefix.`);
|
|
258
|
+
}
|
|
259
|
+
const to = dest.slice(colon + 1);
|
|
260
|
+
if (!to) {
|
|
261
|
+
throw new Error(`Invalid LINE destination "${dest}"; missing id after prefix.`);
|
|
262
|
+
}
|
|
263
|
+
return { kind, ref: dest, to };
|
|
264
|
+
}
|
|
265
|
+
function destinationFromSource(source) {
|
|
266
|
+
switch (source.type) {
|
|
267
|
+
case "user":
|
|
268
|
+
return source.userId ? `user:${source.userId}` : void 0;
|
|
269
|
+
case "group":
|
|
270
|
+
return source.groupId ? `group:${source.groupId}` : void 0;
|
|
271
|
+
case "room":
|
|
272
|
+
return source.roomId ? `room:${source.roomId}` : void 0;
|
|
273
|
+
default:
|
|
274
|
+
return void 0;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// src/client.ts
|
|
279
|
+
async function pushToDestination(client, destinationRef, messages) {
|
|
280
|
+
const { to } = parseDestination(destinationRef);
|
|
281
|
+
const response = await client.pushMessage({ to, messages });
|
|
282
|
+
const id = response.sentMessages?.[0]?.id;
|
|
283
|
+
if (!id) {
|
|
284
|
+
throw new Error("LINE pushMessage did not return a sent message id.");
|
|
285
|
+
}
|
|
286
|
+
return id;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/external-id.ts
|
|
290
|
+
var SEPARATOR = "#";
|
|
291
|
+
function encodeExternalId(destination, messageId) {
|
|
292
|
+
return `${destination}${SEPARATOR}${messageId}`;
|
|
293
|
+
}
|
|
294
|
+
function decodeExternalId(externalId) {
|
|
295
|
+
const at = externalId.indexOf(SEPARATOR);
|
|
296
|
+
if (at === -1) {
|
|
297
|
+
throw new Error(`Invalid LINE externalId: ${externalId}`);
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
destination: externalId.slice(0, at),
|
|
301
|
+
messageId: externalId.slice(at + 1)
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/adapter.ts
|
|
306
|
+
import { actionFields as actionFields3 } from "@hitl-sdk/hitl/adapter";
|
|
307
|
+
|
|
308
|
+
// src/render.ts
|
|
309
|
+
import {
|
|
310
|
+
actionById as actionById2,
|
|
311
|
+
actionFields as actionFields2,
|
|
312
|
+
effectiveActionLabel,
|
|
313
|
+
effectiveStyle
|
|
314
|
+
} from "@hitl-sdk/hitl/adapter";
|
|
315
|
+
function flexText(text, opts) {
|
|
316
|
+
return {
|
|
317
|
+
type: "text",
|
|
318
|
+
text,
|
|
319
|
+
wrap: opts?.wrap ?? true,
|
|
320
|
+
...opts?.size ? { size: opts.size } : {},
|
|
321
|
+
...opts?.color ? { color: opts.color } : {}
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
function flexButton(label, action, style) {
|
|
325
|
+
return {
|
|
326
|
+
type: "button",
|
|
327
|
+
action,
|
|
328
|
+
style: style ?? "secondary",
|
|
329
|
+
height: "sm"
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
function buttonStyle(def) {
|
|
333
|
+
const style = effectiveStyle(def);
|
|
334
|
+
if (style === "primary") return "primary";
|
|
335
|
+
if (style === "danger") return "secondary";
|
|
336
|
+
return "link";
|
|
337
|
+
}
|
|
338
|
+
function actionButton(requestId, def, liffUri) {
|
|
339
|
+
const fields = actionFields2(def);
|
|
340
|
+
if (needsLiff(fields)) {
|
|
341
|
+
if (!liffUri) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`LINE action "${def.id}" requires LIFF for feedback fields; set liffId on createLineAdapter.`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return flexButton(
|
|
347
|
+
effectiveActionLabel(def),
|
|
348
|
+
{ type: "uri", label: effectiveActionLabel(def), uri: liffUri },
|
|
349
|
+
buttonStyle(def)
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
return flexButton(
|
|
353
|
+
effectiveActionLabel(def),
|
|
354
|
+
{
|
|
355
|
+
type: "postback",
|
|
356
|
+
label: effectiveActionLabel(def),
|
|
357
|
+
data: encodePostback({ k: POSTBACK_KIND_ACTION, r: requestId, a: def.id }),
|
|
358
|
+
displayText: effectiveActionLabel(def)
|
|
359
|
+
},
|
|
360
|
+
buttonStyle(def)
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
function buttonRow(contents) {
|
|
364
|
+
return { type: "box", layout: "vertical", spacing: "sm", contents };
|
|
365
|
+
}
|
|
366
|
+
function approvalBubble(request, liffUriFor) {
|
|
367
|
+
const buttons = request.actions.map(
|
|
368
|
+
(def) => actionButton(request.id, def, liffUriFor?.(def.id))
|
|
369
|
+
);
|
|
370
|
+
return {
|
|
371
|
+
type: "bubble",
|
|
372
|
+
body: {
|
|
373
|
+
type: "box",
|
|
374
|
+
layout: "vertical",
|
|
375
|
+
spacing: "md",
|
|
376
|
+
contents: [flexText(request.message), buttonRow(buttons)]
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
function buildApprovalFlex(request, liffUriFor) {
|
|
381
|
+
return {
|
|
382
|
+
type: "flex",
|
|
383
|
+
altText: request.message.slice(0, 400),
|
|
384
|
+
contents: approvalBubble(request, liffUriFor)
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
function buildFieldStepFlex(requestId, actionId, fieldKey, field) {
|
|
388
|
+
const def = { id: actionId };
|
|
389
|
+
const title = field.kind === "select" || field.kind === "confirm" ? field.label : field.label;
|
|
390
|
+
let buttons;
|
|
391
|
+
if (field.kind === "confirm") {
|
|
392
|
+
buttons = [
|
|
393
|
+
flexButton("Yes", {
|
|
394
|
+
type: "postback",
|
|
395
|
+
label: "Yes",
|
|
396
|
+
data: encodePostback({
|
|
397
|
+
k: POSTBACK_KIND_FIELD_VALUE,
|
|
398
|
+
r: requestId,
|
|
399
|
+
a: actionId,
|
|
400
|
+
f: fieldKey,
|
|
401
|
+
v: CONFIRM_YES
|
|
402
|
+
}),
|
|
403
|
+
displayText: "Yes"
|
|
404
|
+
}),
|
|
405
|
+
flexButton("No", {
|
|
406
|
+
type: "postback",
|
|
407
|
+
label: "No",
|
|
408
|
+
data: encodePostback({
|
|
409
|
+
k: POSTBACK_KIND_FIELD_VALUE,
|
|
410
|
+
r: requestId,
|
|
411
|
+
a: actionId,
|
|
412
|
+
f: fieldKey,
|
|
413
|
+
v: CONFIRM_NO
|
|
414
|
+
}),
|
|
415
|
+
displayText: "No"
|
|
416
|
+
})
|
|
417
|
+
];
|
|
418
|
+
} else if (field.kind === "select") {
|
|
419
|
+
buttons = field.options.map(
|
|
420
|
+
(option) => flexButton(
|
|
421
|
+
option,
|
|
422
|
+
{
|
|
423
|
+
type: "postback",
|
|
424
|
+
label: option,
|
|
425
|
+
data: encodePostback({
|
|
426
|
+
k: POSTBACK_KIND_FIELD_VALUE,
|
|
427
|
+
r: requestId,
|
|
428
|
+
a: actionId,
|
|
429
|
+
f: fieldKey,
|
|
430
|
+
v: option
|
|
431
|
+
}),
|
|
432
|
+
displayText: option
|
|
433
|
+
},
|
|
434
|
+
"link"
|
|
435
|
+
)
|
|
436
|
+
);
|
|
437
|
+
} else {
|
|
438
|
+
buttons = [];
|
|
439
|
+
}
|
|
440
|
+
void def;
|
|
441
|
+
return {
|
|
442
|
+
type: "flex",
|
|
443
|
+
altText: title.slice(0, 400),
|
|
444
|
+
contents: {
|
|
445
|
+
type: "bubble",
|
|
446
|
+
body: {
|
|
447
|
+
type: "box",
|
|
448
|
+
layout: "vertical",
|
|
449
|
+
spacing: "md",
|
|
450
|
+
contents: [flexText(title), buttonRow(buttons)]
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function buildOutcomeText(message, result) {
|
|
456
|
+
return `${message}
|
|
457
|
+
|
|
458
|
+
${outcomeLine(result)}`;
|
|
459
|
+
}
|
|
460
|
+
function outcomeLine(result) {
|
|
461
|
+
const by = "by" in result && result.by?.name ? ` by ${result.by.name}` : "";
|
|
462
|
+
switch (result.type) {
|
|
463
|
+
case "RESOLVED": {
|
|
464
|
+
const edited = result.edited ? " with edits" : "";
|
|
465
|
+
const reason = result.actionId === "deny" && result.feedbacks && typeof result.feedbacks === "object" && "reason" in result.feedbacks && typeof result.feedbacks.reason === "string" ? ` \u2014 ${result.feedbacks.reason}` : "";
|
|
466
|
+
const label = result.actionId === "approve" ? `Approved${edited}` : result.actionId === "deny" ? `Denied${reason}` : `${result.actionId}${edited}`;
|
|
467
|
+
return `${label}${by}`;
|
|
468
|
+
}
|
|
469
|
+
case "TIMED_OUT":
|
|
470
|
+
return "Timed out";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function fieldStepForAction(requestId, actions, actionId) {
|
|
474
|
+
const def = actionById2(actions, actionId);
|
|
475
|
+
if (!def) return void 0;
|
|
476
|
+
const fields = actionFields2(def);
|
|
477
|
+
if (!needsFeedbackStep(fields) || !canUseInlineFlex(fields)) return void 0;
|
|
478
|
+
const [fieldKey, field] = Object.entries(fields)[0];
|
|
479
|
+
return buildFieldStepFlex(requestId, actionId, fieldKey, field);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/adapter.ts
|
|
483
|
+
function resolveDestination(request, defaultChannel) {
|
|
484
|
+
const dest = request.destination ?? defaultChannel;
|
|
485
|
+
if (dest === void 0) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
"LINE adapter has no defaultChannel; pass channel as adapter_id:destination (e.g. line-approvals:user:U123)."
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
return dest;
|
|
491
|
+
}
|
|
492
|
+
function createLineAdapter(options) {
|
|
493
|
+
const { client, defaultChannel, liffId, feedbackSecret } = options;
|
|
494
|
+
const sent = /* @__PURE__ */ new Map();
|
|
495
|
+
function liffUriFor(requestId, actionId) {
|
|
496
|
+
if (!liffId || !feedbackSecret) return void 0;
|
|
497
|
+
const token = signFeedbackToken({ requestId, actionId, secret: feedbackSecret });
|
|
498
|
+
return buildLiffUri(liffId, token);
|
|
499
|
+
}
|
|
500
|
+
function assertLiffConfigured(request) {
|
|
501
|
+
for (const def of request.actions) {
|
|
502
|
+
if (needsLiff(actionFields3(def)) && (!liffId || !feedbackSecret)) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`LINE action "${def.id}" requires LIFF feedback fields; set liffId and feedbackSecret on createLineAdapter.`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
const adapter = {
|
|
510
|
+
id: options.id,
|
|
511
|
+
...defaultChannel !== void 0 ? { defaultChannel } : {},
|
|
512
|
+
async send(request) {
|
|
513
|
+
assertLiffConfigured(request);
|
|
514
|
+
const dest = resolveDestination(request, defaultChannel);
|
|
515
|
+
parseDestination(dest);
|
|
516
|
+
const message = buildApprovalFlex(request, (actionId) => liffUriFor(request.id, actionId));
|
|
517
|
+
const messageId = await pushToDestination(client, dest, [message]);
|
|
518
|
+
const externalId = encodeExternalId(dest, messageId);
|
|
519
|
+
sent.set(externalId, { destination: dest, message: request.message });
|
|
520
|
+
return { externalId };
|
|
521
|
+
},
|
|
522
|
+
async update(externalId, result) {
|
|
523
|
+
const entry = sent.get(externalId);
|
|
524
|
+
let destination;
|
|
525
|
+
let message;
|
|
526
|
+
if (entry) {
|
|
527
|
+
destination = entry.destination;
|
|
528
|
+
message = entry.message;
|
|
529
|
+
} else {
|
|
530
|
+
try {
|
|
531
|
+
destination = decodeExternalId(externalId).destination;
|
|
532
|
+
} catch {
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
message = "Approval request";
|
|
536
|
+
}
|
|
537
|
+
const text = buildOutcomeText(message, result);
|
|
538
|
+
await pushToDestination(client, destination, [{ type: "text", text }]);
|
|
539
|
+
sent.delete(externalId);
|
|
540
|
+
},
|
|
541
|
+
async notify(notification) {
|
|
542
|
+
const dest = resolveDestination(notification, defaultChannel);
|
|
543
|
+
const messageId = await pushToDestination(client, dest, [
|
|
544
|
+
{ type: "text", text: notification.message }
|
|
545
|
+
]);
|
|
546
|
+
return { externalId: encodeExternalId(dest, messageId) };
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
if (feedbackSecret) {
|
|
550
|
+
const feedbackOptions = { secret: feedbackSecret, inbox: options.inbox };
|
|
551
|
+
const handleFeedbackGet = createLineFeedbackFormHandler(feedbackOptions);
|
|
552
|
+
const handleFeedbackPost = createLineFeedbackHandler(feedbackOptions);
|
|
553
|
+
adapter.channelKey = LINE_CHANNEL_KEY;
|
|
554
|
+
adapter.fetch = async (req) => {
|
|
555
|
+
if (new URL(req.url).pathname !== LINE_FEEDBACK_PATH) {
|
|
556
|
+
return new Response("Not Found", { status: 404 });
|
|
557
|
+
}
|
|
558
|
+
if (req.method === "GET") return handleFeedbackGet(req);
|
|
559
|
+
if (req.method === "POST") return handleFeedbackPost(req);
|
|
560
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
return adapter;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/webhook.ts
|
|
567
|
+
import { validateSignature } from "@line/bot-sdk";
|
|
568
|
+
|
|
569
|
+
// src/postback.ts
|
|
570
|
+
import {
|
|
571
|
+
actionById as actionById3,
|
|
572
|
+
actionFields as actionFields4
|
|
573
|
+
} from "@hitl-sdk/hitl/adapter";
|
|
574
|
+
import { validateFeedbacks as validateFeedbacks2 } from "@hitl-sdk/hitl/adapter";
|
|
575
|
+
async function handlePostbackEvent(event, options) {
|
|
576
|
+
const payload = parsePostback(event.postback.data);
|
|
577
|
+
if (!payload) return;
|
|
578
|
+
const by = await reviewerFromEvent(event, options.client);
|
|
579
|
+
const { inbox, client } = options;
|
|
580
|
+
if (payload.k === POSTBACK_KIND_ACTION) {
|
|
581
|
+
await handleActionPostback(payload.r, payload.a, event, { client, inbox, by });
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (payload.k === POSTBACK_KIND_FIELD_VALUE) {
|
|
585
|
+
await handleFieldValuePostback(payload, { inbox, by });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async function handleActionPostback(requestId, actionId, event, options) {
|
|
589
|
+
const { inbox, client, by } = options;
|
|
590
|
+
const record = await inbox.get(requestId);
|
|
591
|
+
const actions = record?.actions ?? [{ id: "approve" }];
|
|
592
|
+
const def = actionById3(actions, actionId);
|
|
593
|
+
if (!def) return;
|
|
594
|
+
const fields = actionFields4(def);
|
|
595
|
+
if (needsFeedbackStep(fields)) {
|
|
596
|
+
if (!canUseInlineFlex(fields)) return;
|
|
597
|
+
const fieldStep = fieldStepForAction(requestId, actions, actionId);
|
|
598
|
+
if (!fieldStep) return;
|
|
599
|
+
const dest = destinationFromSource(event.source ?? {});
|
|
600
|
+
if (!dest) return;
|
|
601
|
+
if (event.replyToken) {
|
|
602
|
+
await client.replyMessage({ replyToken: event.replyToken, messages: [fieldStep] });
|
|
603
|
+
} else {
|
|
604
|
+
await pushToDestination(client, dest, [fieldStep]);
|
|
605
|
+
}
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
await inbox.resolve(requestId, { actionId, by });
|
|
609
|
+
}
|
|
610
|
+
async function handleFieldValuePostback(payload, options) {
|
|
611
|
+
const { inbox, by } = options;
|
|
612
|
+
const { r: requestId, a: actionId, f: fieldKey, v: rawValue } = payload;
|
|
613
|
+
if (!fieldKey || rawValue === void 0) return;
|
|
614
|
+
const record = await inbox.get(requestId);
|
|
615
|
+
const actions = record?.actions ?? [{ id: "approve" }];
|
|
616
|
+
const def = actionById3(actions, actionId);
|
|
617
|
+
if (!def) return;
|
|
618
|
+
const fields = actionFields4(def);
|
|
619
|
+
const rawFeedbacks = parsePostbackFeedbacks(fields, fieldKey, rawValue);
|
|
620
|
+
const feedbacks = validateFeedbacks2(fields, rawFeedbacks);
|
|
621
|
+
await inbox.resolve(requestId, {
|
|
622
|
+
actionId,
|
|
623
|
+
feedbacks,
|
|
624
|
+
by
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
async function reviewerFromEvent(event, client) {
|
|
628
|
+
const userId = event.source?.type === "user" ? event.source.userId : event.source?.type === "group" ? event.source.userId : event.source?.type === "room" ? event.source.userId : void 0;
|
|
629
|
+
if (!userId) return void 0;
|
|
630
|
+
try {
|
|
631
|
+
const profile = await client.getProfile(userId);
|
|
632
|
+
return { id: userId, name: profile.displayName };
|
|
633
|
+
} catch {
|
|
634
|
+
return { id: userId };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// src/webhook.ts
|
|
639
|
+
async function handleLineWebhookEvents(events, options) {
|
|
640
|
+
for (const event of events) {
|
|
641
|
+
if (event.type === "postback") {
|
|
642
|
+
const postback = event;
|
|
643
|
+
if (parsePostback(postback.postback.data)) {
|
|
644
|
+
await handlePostbackEvent(postback, options);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
await options.onFallbackEvent?.(event);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
async function handleLineWebhookBody(body, signature, options) {
|
|
652
|
+
if (!signature || !validateSignature(body, options.channelSecret, signature)) {
|
|
653
|
+
throw new LineWebhookError("Invalid signature", 401);
|
|
654
|
+
}
|
|
655
|
+
let payload;
|
|
656
|
+
try {
|
|
657
|
+
payload = JSON.parse(body);
|
|
658
|
+
} catch {
|
|
659
|
+
throw new LineWebhookError("Invalid JSON", 400);
|
|
660
|
+
}
|
|
661
|
+
await handleLineWebhookEvents(payload.events ?? [], {
|
|
662
|
+
client: options.client,
|
|
663
|
+
inbox: options.inbox(),
|
|
664
|
+
onFallbackEvent: options.onFallbackEvent
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
var LineWebhookError = class extends Error {
|
|
668
|
+
constructor(message, status) {
|
|
669
|
+
super(message);
|
|
670
|
+
this.status = status;
|
|
671
|
+
this.name = "LineWebhookError";
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
function createLineWebhookHandler(options) {
|
|
675
|
+
return async (request) => {
|
|
676
|
+
if (request.method !== "POST") {
|
|
677
|
+
return new Response("Method Not Allowed", { status: 405 });
|
|
678
|
+
}
|
|
679
|
+
const body = await request.text();
|
|
680
|
+
const signature = request.headers.get("x-line-signature");
|
|
681
|
+
try {
|
|
682
|
+
await handleLineWebhookBody(body, signature, options);
|
|
683
|
+
return new Response("OK", { status: 200 });
|
|
684
|
+
} catch (err) {
|
|
685
|
+
if (err instanceof LineWebhookError) {
|
|
686
|
+
return new Response(err.message, { status: err.status });
|
|
687
|
+
}
|
|
688
|
+
throw err;
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
export {
|
|
693
|
+
LINE_CHANNEL_KEY,
|
|
694
|
+
LINE_FEEDBACK_PATH,
|
|
695
|
+
LineWebhookError,
|
|
696
|
+
POSTBACK_KIND_ACTION,
|
|
697
|
+
POSTBACK_KIND_FIELD_VALUE,
|
|
698
|
+
buildFeedbackFormHtml,
|
|
699
|
+
buildLiffUri,
|
|
700
|
+
canUseInlineFlex,
|
|
701
|
+
createLineAdapter,
|
|
702
|
+
createLineFeedbackFormHandler,
|
|
703
|
+
createLineFeedbackHandler,
|
|
704
|
+
createLineWebhookHandler,
|
|
705
|
+
decodeExternalId,
|
|
706
|
+
destinationFromSource,
|
|
707
|
+
encodeExternalId,
|
|
708
|
+
encodePostback,
|
|
709
|
+
handleLineWebhookBody,
|
|
710
|
+
handleLineWebhookEvents,
|
|
711
|
+
handlePostbackEvent,
|
|
712
|
+
needsFeedbackStep,
|
|
713
|
+
needsLiff,
|
|
714
|
+
parseDestination,
|
|
715
|
+
parsePostback,
|
|
716
|
+
signFeedbackToken,
|
|
717
|
+
verifyFeedbackToken
|
|
718
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HitlInbox } from "@hitl-sdk/hitl/state";
|
|
2
|
+
import type { webhook } from "@line/bot-sdk";
|
|
3
|
+
import type { LineMessagingClient } from "./client.js";
|
|
4
|
+
import { buildFieldStepFlex } from "./render.js";
|
|
5
|
+
export interface PostbackHandlerOptions {
|
|
6
|
+
client: LineMessagingClient;
|
|
7
|
+
inbox: HitlInbox;
|
|
8
|
+
}
|
|
9
|
+
type PostbackEvent = webhook.PostbackEvent;
|
|
10
|
+
export declare function handlePostbackEvent(event: PostbackEvent, options: PostbackHandlerOptions): Promise<void>;
|
|
11
|
+
export { buildFieldStepFlex };
|
|
12
|
+
//# sourceMappingURL=postback.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postback.d.ts","sourceRoot":"","sources":["../src/postback.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AASvD,OAAO,EAAE,kBAAkB,EAAsB,MAAM,aAAa,CAAC;AAIrE,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,mBAAmB,CAAC;IAC5B,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,KAAK,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC;AAE3C,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAef;AA6ED,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HumanRequest, HumanResult, HitlField } from "@hitl-sdk/hitl/adapter";
|
|
2
|
+
import type { messagingApi } from "@line/bot-sdk";
|
|
3
|
+
type FlexMessage = messagingApi.FlexMessage;
|
|
4
|
+
export declare function buildApprovalFlex(request: HumanRequest, liffUriFor?: (actionId: string) => string | undefined): FlexMessage;
|
|
5
|
+
export declare function buildFieldStepFlex(requestId: string, actionId: string, fieldKey: string, field: HitlField): FlexMessage;
|
|
6
|
+
export declare function buildOutcomeText(message: string, result: HumanResult): string;
|
|
7
|
+
export declare function outcomeLine(result: HumanResult): string;
|
|
8
|
+
export declare function fieldStepForAction(requestId: string, actions: HumanRequest["actions"], actionId: string): FlexMessage | undefined;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,SAAS,EAAkB,MAAM,wBAAwB,CAAC;AAOnG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAUlD,KAAK,WAAW,GAAG,YAAY,CAAC,WAAW,CAAC;AAqF5C,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,YAAY,EACrB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,GACpD,WAAW,CAMb;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,SAAS,GACf,WAAW,CAsEb;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAE7E;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAwBvD;AAED,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,QAAQ,EAAE,MAAM,GACf,WAAW,GAAG,SAAS,CAOzB"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { webhook } from "@line/bot-sdk";
|
|
2
|
+
import type { HitlInbox } from "@hitl-sdk/hitl/state";
|
|
3
|
+
import type { LineMessagingClient } from "./client.js";
|
|
4
|
+
import { type PostbackHandlerOptions } from "./postback.js";
|
|
5
|
+
export interface LineWebhookHandlerOptions {
|
|
6
|
+
channelSecret: string;
|
|
7
|
+
client: LineMessagingClient;
|
|
8
|
+
inbox: () => HitlInbox;
|
|
9
|
+
/** Non-hitl events and postbacks that fail `parsePostback`. Mirrors Express `else` branches. */
|
|
10
|
+
onFallbackEvent?: (event: webhook.Event) => void | Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
export type LineWebhookEventsOptions = PostbackHandlerOptions & Pick<LineWebhookHandlerOptions, "onFallbackEvent">;
|
|
13
|
+
/** Hitl postbacks first; remaining events go to `onFallbackEvent` when provided. */
|
|
14
|
+
export declare function handleLineWebhookEvents(events: readonly webhook.Event[], options: LineWebhookEventsOptions): Promise<void>;
|
|
15
|
+
export declare function handleLineWebhookBody(body: string, signature: string | null, options: LineWebhookHandlerOptions): Promise<void>;
|
|
16
|
+
export declare class LineWebhookError extends Error {
|
|
17
|
+
readonly status: number;
|
|
18
|
+
constructor(message: string, status: number);
|
|
19
|
+
}
|
|
20
|
+
/** Fetch-compatible webhook handler for Next.js / Hono route handlers. */
|
|
21
|
+
export declare function createLineWebhookHandler(options: LineWebhookHandlerOptions): (request: Request) => Promise<Response>;
|
|
22
|
+
//# sourceMappingURL=webhook.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook.d.ts","sourceRoot":"","sources":["../src/webhook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,OAAO,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAEvD,OAAO,EAAuB,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAKjF,MAAM,WAAW,yBAAyB;IACxC,aAAa,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,KAAK,EAAE,MAAM,SAAS,CAAC;IACvB,gGAAgG;IAChG,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClE;AAED,MAAM,MAAM,wBAAwB,GAAG,sBAAsB,GAAG,IAAI,CAAC,yBAAyB,EAAE,iBAAiB,CAAC,CAAC;AAEnH,oFAAoF;AACpF,wBAAsB,uBAAuB,CAC3C,MAAM,EAAE,SAAS,OAAO,CAAC,KAAK,EAAE,EAChC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,IAAI,CAAC,CAWf;AAED,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,IAAI,EACxB,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED,qBAAa,gBAAiB,SAAQ,KAAK;IAGvC,QAAQ,CAAC,MAAM,EAAE,MAAM;gBADvB,OAAO,EAAE,MAAM,EACN,MAAM,EAAE,MAAM;CAK1B;AAED,0EAA0E;AAC1E,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,yBAAyB,GACjC,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAiBzC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hitl-sdk/adapter-line",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "LINE Messaging API channel plugin for hitl: deliver approvals via Flex Messages and postback webhooks.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/u17g/hitl.git"
|
|
9
|
+
},
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"@hitl-sdk/hitl": "1.0.0"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@line/bot-sdk": "^9.0.0 || ^10.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@line/bot-sdk": "^10.0.0",
|
|
32
|
+
"@types/node": "^25.9.3",
|
|
33
|
+
"typescript": "^5.8.3",
|
|
34
|
+
"vitest": "^3.2.4"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "node ../../scripts/build-lib.mjs",
|
|
38
|
+
"test": "vitest run",
|
|
39
|
+
"typecheck": "tsc --noEmit"
|
|
40
|
+
}
|
|
41
|
+
}
|