@fastagent-sh/fastagent 0.27.0 → 0.29.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/README.md +4 -3
- package/dist/authoring.d.ts +14 -7
- package/dist/authoring.js +11 -7
- package/dist/channels/feishu/feishu-api.d.ts +1 -0
- package/dist/channels/feishu/feishu-api.js +4 -0
- package/dist/channels/feishu/register-app.d.ts +2 -0
- package/dist/channels/feishu/register-app.js +1 -1
- package/dist/channels/lark/credentials-page.d.ts +25 -0
- package/dist/channels/lark/credentials-page.js +206 -0
- package/dist/channels/lark/onboard.d.ts +18 -2
- package/dist/channels/lark/onboard.js +36 -6
- package/dist/channels/slack/setup-server.js +75 -30
- package/dist/channels/wait-health.js +1 -1
- package/dist/cli/add-feishu.d.ts +0 -9
- package/dist/cli/add-feishu.js +62 -38
- package/dist/cli/commands/add.js +5 -12
- package/dist/cli/commands/content.js +1 -1
- package/dist/cli/commands/deploy.js +1 -8
- package/dist/cli/commands/info.js +4 -0
- package/dist/cli/commands/invoke.js +5 -3
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +156 -0
- package/dist/cli/content-view.d.ts +1 -1
- package/dist/cli/content-view.js +2 -2
- package/dist/cli/program.js +65 -9
- package/dist/content/declare.d.ts +1 -6
- package/dist/content/declare.js +13 -18
- package/dist/content/resolve.d.ts +0 -2
- package/dist/content/resolve.js +2 -4
- package/dist/content/source.d.ts +0 -1
- package/dist/content/source.js +0 -1
- package/dist/deploy/agentcore/destroy.js +1 -1
- package/dist/deploy/build-context.js +4 -0
- package/dist/deploy/container.js +1 -2
- package/dist/deploy/docker/run.d.ts +16 -3
- package/dist/deploy/docker/run.js +54 -1
- package/dist/deploy/fly/run.d.ts +0 -14
- package/dist/deploy/fly/run.js +4 -5
- package/dist/deploy/preflight.js +6 -14
- package/dist/dev-supervisor.js +3 -2
- package/dist/harnesses/pi-durable/mcp.d.ts +3 -0
- package/dist/harnesses/pi-durable/mcp.js +34 -0
- package/dist/harnesses/pi-durable/open.d.ts +3 -4
- package/dist/harnesses/pi-durable/open.js +13 -10
- package/dist/harnesses/pi-durable/serve.d.ts +5 -1
- package/dist/harnesses/pi-durable/serve.js +47 -17
- package/dist/harnesses/pi-durable/text.js +7 -13
- package/dist/http/contract.d.ts +3 -3
- package/dist/index.d.ts +9 -1
- package/dist/index.js +6 -1
- package/dist/locked-file.d.ts +2 -2
- package/dist/locked-file.js +8 -3
- package/dist/mcp/auth.d.ts +22 -0
- package/dist/mcp/auth.js +180 -0
- package/dist/mcp/client.d.ts +15 -0
- package/dist/mcp/client.js +170 -0
- package/dist/mcp/config.d.ts +34 -0
- package/dist/mcp/config.js +142 -0
- package/dist/mcp/grants.d.ts +4 -0
- package/dist/mcp/grants.js +132 -0
- package/dist/mcp/host.d.ts +16 -0
- package/dist/mcp/host.js +120 -0
- package/dist/mcp/network.d.ts +12 -0
- package/dist/mcp/network.js +150 -0
- package/dist/mcp/sign-in.d.ts +2 -0
- package/dist/mcp/sign-in.js +204 -0
- package/dist/mcp-api.d.ts +93 -0
- package/dist/mcp-api.js +10 -0
- package/dist/model-ref.d.ts +3 -0
- package/dist/model-ref.js +7 -0
- package/dist/setup-page.d.ts +51 -0
- package/dist/setup-page.js +149 -0
- package/dist/value-file.d.ts +1 -0
- package/dist/value-file.js +15 -16
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -152,9 +152,10 @@ The package exports the supported surface only, from one entry per kind of user,
|
|
|
152
152
|
| The Agent API | `Agent`, `AgentError` ([Agent API](docs/SPEC.md)) | Stable within its version |
|
|
153
153
|
| Directory → agent → service | `openAgent`, `LocalAgent`, `serve()`'s `Service` (`fetch`, `listen`) | Usable now, may tighten before 1.0 |
|
|
154
154
|
| Over HTTP | `connectAgent` (`/client`), the `/v1` routes ([Agent API](docs/SPEC.md) §9) | Stable within its version |
|
|
155
|
-
| A host embedding agents | `createAgent`, `CredentialStore`, `credentialFile`, `signIn`, `signInOptions`, `providerAuth` ([embedding](docs/embedding.md#6-a-desktop-host)) | Usable now, may tighten before 1.0 |
|
|
155
|
+
| A host embedding agents | `createAgent`, `addContent`, `removeContent`, `listContent`, `CredentialStore`, `credentialFile`, `signIn`, `signInOptions`, `providerAuth` ([embedding](docs/embedding.md#6-a-desktop-host)) | Usable now, may tighten before 1.0 |
|
|
156
|
+
| MCP tools and authorization | `inspectMcp`, `signInMcp`, `mcpGrantFile`, `McpGrantStore` ([MCP servers](docs/mcp.md)) | Usable now, may tighten before 1.0 |
|
|
156
157
|
| Authoring | `defineAgent`, `defineChannel`, `defineSchedule`, `needs`, `sessionIdOf`, `readBodyCapped`, `text`, `secretEquals` | Usable now, may tighten before 1.0 |
|
|
157
|
-
| Not exported | The assembly's parts (`router`, `createScheduler`),
|
|
158
|
+
| Not exported | The assembly's parts (`router`, `createScheduler`), the environment's edit (`writeEnvironment`), the model environment, harness internals | No compatibility promise |
|
|
158
159
|
|
|
159
160
|
A tool or an extension is pi-durable's own (`defineTool`, `defineExtension` from `@earendil-works/pi-durable`), and a
|
|
160
161
|
custom provider is pi-ai's (`createProvider` from `@earendil-works/pi-ai`): `init` adds both packages to the agent.
|
|
@@ -189,7 +190,7 @@ The Agent API leaves room for capabilities that are not complete product feature
|
|
|
189
190
|
- **Scale-out**: one process holds an instance's storage today; spreading one instance over several processes is future work.
|
|
190
191
|
- **Sandboxed execution** — the coding tools, channels, and author-written code reach the local process directly; a sandbox around the complete agent process is future work.
|
|
191
192
|
- **Observability export** — leveled logs exist today; an OpenTelemetry exporter does not.
|
|
192
|
-
- **More channels
|
|
193
|
+
- **More channels** — community channels can use the channel kit. External tools can also use [MCP servers](docs/mcp.md).
|
|
193
194
|
- **More deploy targets** — local Docker, Fly, Railway, and AWS Bedrock AgentCore ship today; the generated container is the portable path for other hosts.
|
|
194
195
|
|
|
195
196
|
See [Contributing](https://github.com/fastagent-sh/fastagent/blob/main/CONTRIBUTING.md) if one of these is the problem you want to work on.
|
package/dist/authoring.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export declare class ContentNameError extends Error {
|
|
|
10
10
|
name: string;
|
|
11
11
|
}
|
|
12
12
|
export interface InitAgentOptions {
|
|
13
|
-
/** What the agent works on
|
|
13
|
+
/** What the agent works on: repositories into `context.json`, this machine's directories into
|
|
14
14
|
* `context.local.json`. */
|
|
15
15
|
content?: ContentAddition[];
|
|
16
16
|
/**
|
|
@@ -36,26 +36,33 @@ export interface CreatedAgent {
|
|
|
36
36
|
* Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
|
|
37
37
|
* repository whose first commit is what was written, since the agent changes itself and version control is how its
|
|
38
38
|
* author goes back to a version that worked. It installs no dependencies; the agent opens without them.
|
|
39
|
+
*
|
|
40
|
+
* `content` gives it any number of entries, each a directory of this machine or a GitHub repository. They are all
|
|
41
|
+
* checked before anything is written, and one that is refused leaves no directory. Two entries may bind the same
|
|
42
|
+
* directory, or one inside the other; the agent then reaches it by both names.
|
|
39
43
|
*/
|
|
40
|
-
export declare function createAgent(dir: string
|
|
44
|
+
export declare function createAgent(dir: string, options?: {
|
|
45
|
+
content?: ContentAddition[];
|
|
46
|
+
}): Promise<CreatedAgent & {
|
|
47
|
+
content: ResolvedContent[];
|
|
48
|
+
}>;
|
|
41
49
|
/**
|
|
42
|
-
* {@link createAgent} with what `fastagent init` adds:
|
|
50
|
+
* {@link createAgent} with what `fastagent init` adds: the install. Every content entry is checked before
|
|
43
51
|
* anything is written, and the scaffold is removed again when writing the content fails.
|
|
44
52
|
*/
|
|
45
53
|
export declare function initAgent(dir: string, options?: InitAgentOptions): Promise<CreatedAgent & {
|
|
46
54
|
content: ResolvedContent[];
|
|
47
55
|
}>;
|
|
48
56
|
/**
|
|
49
|
-
* The
|
|
50
|
-
*
|
|
57
|
+
* The on-disk declaration resolved for this machine, not an open agent's startup snapshot. Missing local directories
|
|
58
|
+
* are listed separately; an agent opened here is not told of them.
|
|
51
59
|
*/
|
|
52
60
|
export declare function listContent(agentDir: string): Promise<{
|
|
53
61
|
content: ResolvedContent[];
|
|
54
62
|
absent: BoundContent[];
|
|
55
63
|
}>;
|
|
56
64
|
/**
|
|
57
|
-
*
|
|
58
|
-
* user about what was left on this machine.
|
|
65
|
+
* The edited declaration resolved for this machine; an already-open agent keeps its startup snapshot until reopened.
|
|
59
66
|
*/
|
|
60
67
|
export interface ContentEdit {
|
|
61
68
|
name: string;
|
package/dist/authoring.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Creating an agent and editing its content: `fastagent init` and `fastagent content` are thin wrappers over these (and
|
|
3
3
|
* `fastagent env` writes its declaration through {@link writeEnvironment}), so the rules live here, not in a command.
|
|
4
4
|
* Nothing here prints or exits; every refusal is thrown with the message the CLI shows. The package exports
|
|
5
|
-
* {@link createAgent}
|
|
5
|
+
* {@link createAgent} and the content edits ({@link addContent}, {@link removeContent}, {@link listContent}), which
|
|
6
|
+
* give an agent its own entries, without an alias prefix; the environment's edit is not exported.
|
|
6
7
|
*/
|
|
7
8
|
import { existsSync, lstatSync, readFileSync, statSync } from "node:fs";
|
|
8
9
|
import { rm } from "node:fs/promises";
|
|
@@ -28,13 +29,16 @@ export class ContentNameError extends Error {
|
|
|
28
29
|
* Create an agent in `dir` as `fastagent init` does, which must be new or empty: the whole scaffold, and a git
|
|
29
30
|
* repository whose first commit is what was written, since the agent changes itself and version control is how its
|
|
30
31
|
* author goes back to a version that worked. It installs no dependencies; the agent opens without them.
|
|
32
|
+
*
|
|
33
|
+
* `content` gives it any number of entries, each a directory of this machine or a GitHub repository. They are all
|
|
34
|
+
* checked before anything is written, and one that is refused leaves no directory. Two entries may bind the same
|
|
35
|
+
* directory, or one inside the other; the agent then reaches it by both names.
|
|
31
36
|
*/
|
|
32
|
-
export async function createAgent(dir) {
|
|
33
|
-
|
|
34
|
-
return created;
|
|
37
|
+
export async function createAgent(dir, options = {}) {
|
|
38
|
+
return initAgent(dir, options.content === undefined ? {} : { content: options.content });
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
|
-
* {@link createAgent} with what `fastagent init` adds:
|
|
41
|
+
* {@link createAgent} with what `fastagent init` adds: the install. Every content entry is checked before
|
|
38
42
|
* anything is written, and the scaffold is removed again when writing the content fails.
|
|
39
43
|
*/
|
|
40
44
|
export async function initAgent(dir, options = {}) {
|
|
@@ -69,8 +73,8 @@ export async function initAgent(dir, options = {}) {
|
|
|
69
73
|
return { dir: agentDir, created, content, repository: await initRepository(agentDir) };
|
|
70
74
|
}
|
|
71
75
|
/**
|
|
72
|
-
* The
|
|
73
|
-
*
|
|
76
|
+
* The on-disk declaration resolved for this machine, not an open agent's startup snapshot. Missing local directories
|
|
77
|
+
* are listed separately; an agent opened here is not told of them.
|
|
74
78
|
*/
|
|
75
79
|
export async function listContent(agentDir) {
|
|
76
80
|
const dir = resolveAgentDir(agentDir);
|
|
@@ -19,6 +19,7 @@ export interface DownloadedFile {
|
|
|
19
19
|
* streaming once when it sees this. A plain code check on the typed error, exported instead of the
|
|
20
20
|
* class so no caller can construct/throw one. */
|
|
21
21
|
export declare function isCardStreamingClosed(e: unknown): boolean;
|
|
22
|
+
export declare function isFeishuCredentialsRefused(e: unknown): boolean;
|
|
22
23
|
/** Whether the platform origin has no application-config route at all. Onboarding uses this narrow
|
|
23
24
|
* signal to fall back to a manual token/mode setup; auth/scope/network failures must remain visible. */
|
|
24
25
|
export declare function isFeishuConfigApiMissing(e: unknown): boolean;
|
|
@@ -61,6 +61,10 @@ class FeishuApiError extends Error {
|
|
|
61
61
|
export function isCardStreamingClosed(e) {
|
|
62
62
|
return e instanceof FeishuApiError && (e.code === 200850 || e.code === 300309);
|
|
63
63
|
}
|
|
64
|
+
// Lark's credential endpoint returns code 10003 for invalid app credentials; other errors remain fatal.
|
|
65
|
+
export function isFeishuCredentialsRefused(e) {
|
|
66
|
+
return e instanceof FeishuApiError && e.call === "tenant_access_token" && e.status === 200 && e.code === 10003;
|
|
67
|
+
}
|
|
64
68
|
/** Whether the platform origin has no application-config route at all. Onboarding uses this narrow
|
|
65
69
|
* signal to fall back to a manual token/mode setup; auth/scope/network failures must remain visible. */
|
|
66
70
|
export function isFeishuConfigApiMissing(e) {
|
|
@@ -24,6 +24,8 @@ interface FeishuAppAddons {
|
|
|
24
24
|
items?: string[];
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
/** JSON → gzip → base64url — the platform's fixed addons encoding. */
|
|
28
|
+
export declare function encodeAddons(addons: FeishuAppAddons): string;
|
|
27
29
|
export interface RegisterFeishuAppOptions {
|
|
28
30
|
/** Pre-filled app name shown on the confirm page (`{user}` expands to the scanning user's name). */
|
|
29
31
|
name?: string;
|
|
@@ -13,7 +13,7 @@ const ENDPOINT = "/oauth/v1/app/registration";
|
|
|
13
13
|
/** Per-attempt timeout for one registration POST — small form/JSON round-trips. */
|
|
14
14
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
15
15
|
/** JSON → gzip → base64url — the platform's fixed addons encoding. */
|
|
16
|
-
function encodeAddons(addons) {
|
|
16
|
+
export function encodeAddons(addons) {
|
|
17
17
|
return gzipSync(Buffer.from(JSON.stringify(addons), "utf8"))
|
|
18
18
|
.toString("base64")
|
|
19
19
|
.replace(/\+/g, "-")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface LarkCredentials {
|
|
2
|
+
appId: string;
|
|
3
|
+
appSecret: string;
|
|
4
|
+
}
|
|
5
|
+
export declare class LarkCredentialsRefused extends Error {
|
|
6
|
+
readonly fix: string | undefined;
|
|
7
|
+
readonly why: string | undefined;
|
|
8
|
+
constructor(headline: string, options?: {
|
|
9
|
+
fix?: string;
|
|
10
|
+
why?: string;
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
export interface LarkCredentialsPage<T = LarkCredentials> {
|
|
14
|
+
url: string;
|
|
15
|
+
credentials: Promise<T>;
|
|
16
|
+
close(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
export declare function startLarkCredentialsPage(input: {
|
|
19
|
+
consoleUrl: string;
|
|
20
|
+
check(credentials: LarkCredentials): Promise<void>;
|
|
21
|
+
}): Promise<LarkCredentialsPage>;
|
|
22
|
+
export declare function startLarkVerificationPage(input: {
|
|
23
|
+
consoleUrl: string;
|
|
24
|
+
save(token: string): Promise<void>;
|
|
25
|
+
}): Promise<LarkCredentialsPage<string>>;
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/** Loopback-only, single-use forms for Lark setup secrets; values never pass through the agent's conversation. */
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import { field, formCard, openButton, privacy, prose, refusal, sendPage, setupRequestUrl, statusCard, step, submit, } from "../../setup-page.js";
|
|
5
|
+
export class LarkCredentialsRefused extends Error {
|
|
6
|
+
fix;
|
|
7
|
+
why;
|
|
8
|
+
constructor(headline, options = {}) {
|
|
9
|
+
super(headline);
|
|
10
|
+
this.name = "LarkCredentialsRefused";
|
|
11
|
+
this.fix = options.fix;
|
|
12
|
+
this.why = options.why;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function readBody(request) {
|
|
16
|
+
return new Promise((resolve) => {
|
|
17
|
+
let body = "";
|
|
18
|
+
let tooLarge = false;
|
|
19
|
+
request.on("data", (chunk) => {
|
|
20
|
+
if (tooLarge)
|
|
21
|
+
return;
|
|
22
|
+
body += String(chunk);
|
|
23
|
+
if (Buffer.byteLength(body) > 16 * 1024)
|
|
24
|
+
tooLarge = true;
|
|
25
|
+
});
|
|
26
|
+
request.on("end", () => resolve(tooLarge ? undefined : body));
|
|
27
|
+
request.on("error", () => resolve(undefined));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function startLarkCredentialsPage(input) {
|
|
31
|
+
return startPage({
|
|
32
|
+
heading: "Lark credentials verified",
|
|
33
|
+
form: (pasted, problem) => formCard("Connect Lark", [
|
|
34
|
+
step(1, "Create the app in Lark", prose("It opens with the permissions and event your agent needs already set.") +
|
|
35
|
+
openButton("Open Lark", input.consoleUrl)),
|
|
36
|
+
step(2, "Paste its credentials", (problem
|
|
37
|
+
? refusal(problem.message, problem.fix, problem.why)
|
|
38
|
+
: prose("Find them under <b>Credentials & Basic Info</b>.")) +
|
|
39
|
+
field({
|
|
40
|
+
id: "app_id",
|
|
41
|
+
label: "App ID",
|
|
42
|
+
placeholder: "cli_…",
|
|
43
|
+
value: pasted.get("app_id") ?? "",
|
|
44
|
+
invalid: !!problem,
|
|
45
|
+
}) +
|
|
46
|
+
field({ id: "app_secret", label: "App Secret", secret: true, invalid: !!problem, autofocus: !!problem }) +
|
|
47
|
+
submit("Save and check") +
|
|
48
|
+
privacy()),
|
|
49
|
+
].join("")),
|
|
50
|
+
accept: async (pasted) => {
|
|
51
|
+
const pair = { appId: (pasted.get("app_id") ?? "").trim(), appSecret: (pasted.get("app_secret") ?? "").trim() };
|
|
52
|
+
if (!pair.appId || !pair.appSecret) {
|
|
53
|
+
throw new LarkCredentialsRefused("Both values are needed.", {
|
|
54
|
+
fix: "Copy the App ID and the App Secret from Credentials & Basic Info.",
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
await input.check(pair);
|
|
58
|
+
return pair;
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
export function startLarkVerificationPage(input) {
|
|
63
|
+
return startPage({
|
|
64
|
+
heading: "Verification Token saved",
|
|
65
|
+
form: (_pasted, problem) => formCard("Finish Lark webhook setup", [
|
|
66
|
+
step(1, "Copy the Verification Token", prose("Find it under <b>Events & Callbacks</b> → Encryption Strategy. Choose webhook mode when setting the Request URL.") + openButton("Open Lark", input.consoleUrl)),
|
|
67
|
+
step(2, "Paste the token here", (problem ? refusal(problem.message, problem.fix) : "") +
|
|
68
|
+
field({
|
|
69
|
+
id: "verification_token",
|
|
70
|
+
label: "Verification Token",
|
|
71
|
+
secret: true,
|
|
72
|
+
invalid: !!problem,
|
|
73
|
+
autofocus: !!problem,
|
|
74
|
+
}) +
|
|
75
|
+
submit("Save token") +
|
|
76
|
+
privacy()),
|
|
77
|
+
].join("")),
|
|
78
|
+
accept: async (pasted) => {
|
|
79
|
+
const token = (pasted.get("verification_token") ?? "").trim();
|
|
80
|
+
if (!token)
|
|
81
|
+
throw new LarkCredentialsRefused("Verification Token is required.");
|
|
82
|
+
await input.save(token);
|
|
83
|
+
return token;
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
// A browser can leave during validation, before either response event can be subscribed to.
|
|
88
|
+
function afterResponse(response, complete) {
|
|
89
|
+
if (response.destroyed || response.writableFinished)
|
|
90
|
+
complete();
|
|
91
|
+
else
|
|
92
|
+
response.once("close", complete);
|
|
93
|
+
}
|
|
94
|
+
async function startPage(input) {
|
|
95
|
+
const path = `/lark/credentials/${randomBytes(24).toString("hex")}`;
|
|
96
|
+
const { promise: credentials, resolve, reject } = Promise.withResolvers();
|
|
97
|
+
let checking = false;
|
|
98
|
+
let used = false;
|
|
99
|
+
let port = 0;
|
|
100
|
+
const server = createServer(async (request, response) => {
|
|
101
|
+
const url = setupRequestUrl(request, response);
|
|
102
|
+
if (url === undefined)
|
|
103
|
+
return;
|
|
104
|
+
const host = request.headers.host ?? "";
|
|
105
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`) {
|
|
106
|
+
response.writeHead(421).end("misdirected request\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (url.pathname !== path || (request.method !== "GET" && request.method !== "POST")) {
|
|
110
|
+
response.writeHead(404).end("not found\n");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const expired = () => sendPage(response, 404, {
|
|
114
|
+
title: "FastAgent setup",
|
|
115
|
+
card: statusCard({
|
|
116
|
+
state: "failed",
|
|
117
|
+
heading: "This page is no longer in use",
|
|
118
|
+
next: "Go back to your coding agent.",
|
|
119
|
+
}),
|
|
120
|
+
});
|
|
121
|
+
if (used) {
|
|
122
|
+
expired();
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (request.method === "GET") {
|
|
126
|
+
sendPage(response, 200, { title: "Connect Lark — FastAgent", card: input.form(new URLSearchParams()) });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const site = request.headers["sec-fetch-site"];
|
|
130
|
+
if (site !== undefined && site !== "same-origin") {
|
|
131
|
+
response.writeHead(403).end("forbidden\n");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const body = await readBody(request);
|
|
135
|
+
if (body === undefined) {
|
|
136
|
+
response.writeHead(413).end("payload too large\n");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (used) {
|
|
140
|
+
expired();
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (checking) {
|
|
144
|
+
response.writeHead(409).end("another submission is being checked\n");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
checking = true;
|
|
148
|
+
const pasted = new URLSearchParams(body);
|
|
149
|
+
let accepted;
|
|
150
|
+
try {
|
|
151
|
+
accepted = await input.accept(pasted);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
checking = false;
|
|
155
|
+
// Only explicit input refusals stay on the form. Infrastructure and persistence errors fail the ticket.
|
|
156
|
+
if (error instanceof LarkCredentialsRefused) {
|
|
157
|
+
sendPage(response, 200, { title: "Connect Lark — FastAgent", card: input.form(pasted, error) });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
used = true;
|
|
161
|
+
afterResponse(response, () => reject(error));
|
|
162
|
+
sendPage(response, 200, {
|
|
163
|
+
title: "Setup stopped — FastAgent",
|
|
164
|
+
card: statusCard({
|
|
165
|
+
state: "failed",
|
|
166
|
+
heading: "Setup stopped",
|
|
167
|
+
lede: error instanceof Error ? error.message : String(error),
|
|
168
|
+
next: "Go back to your coding agent.",
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
used = true;
|
|
174
|
+
afterResponse(response, () => resolve(accepted));
|
|
175
|
+
sendPage(response, 200, {
|
|
176
|
+
title: `${input.heading} — FastAgent`,
|
|
177
|
+
card: statusCard({
|
|
178
|
+
state: "done",
|
|
179
|
+
heading: input.heading,
|
|
180
|
+
lede: "Your coding agent will say if anything is left to do in Lark.",
|
|
181
|
+
next: "Go back to your coding agent.",
|
|
182
|
+
}),
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
await new Promise((resolve, reject) => {
|
|
186
|
+
server.once("error", reject);
|
|
187
|
+
server.listen(0, "127.0.0.1", () => {
|
|
188
|
+
server.off("error", reject);
|
|
189
|
+
resolve();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
const address = server.address();
|
|
193
|
+
if (!address || typeof address === "string") {
|
|
194
|
+
server.close();
|
|
195
|
+
throw new Error("the Lark credentials page did not receive a local TCP port");
|
|
196
|
+
}
|
|
197
|
+
port = address.port;
|
|
198
|
+
return {
|
|
199
|
+
url: `http://127.0.0.1:${port}${path}`,
|
|
200
|
+
credentials,
|
|
201
|
+
close: () => new Promise((resolve) => {
|
|
202
|
+
server.close(() => resolve());
|
|
203
|
+
server.closeAllConnections();
|
|
204
|
+
}),
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type FeishuSubscriptionMode } from "../feishu/setup-mode.ts";
|
|
2
2
|
/** Guided Lark-international onboarding. */
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Lark's one-click launcher, where the person creates the app. The launcher is unbound (the bound confirm page is broken
|
|
5
|
+
* on the international cloud), but it merges the `addons` query parameter onto the app it creates, as Feishu's does: the
|
|
6
|
+
* scopes and the event the channel needs arrive with the app, so nothing is ticked in the console afterwards. A tenant
|
|
7
|
+
* may still withhold a scope; the permission check that follows creation names it.
|
|
8
|
+
*/
|
|
9
|
+
export declare function larkConsoleUrl(ingress: FeishuSubscriptionMode): string;
|
|
4
10
|
/** Directly where the fallback token lives (and where the successful automatic mode change is visible). */
|
|
5
11
|
export declare function larkEventSecurityUrl(appId: string): string;
|
|
6
12
|
export interface LarkOnboardIO {
|
|
@@ -10,6 +16,15 @@ export interface LarkOnboardIO {
|
|
|
10
16
|
prompt(message: string, opts?: {
|
|
11
17
|
hidden?: boolean;
|
|
12
18
|
}): Promise<string | undefined>;
|
|
19
|
+
verificationToken?(consoleUrl: string): Promise<string>;
|
|
20
|
+
collect?(input: {
|
|
21
|
+
/** Lark's launcher, with what the channel needs set on the app it creates. */
|
|
22
|
+
consoleUrl: string;
|
|
23
|
+
check(appId: string, appSecret: string): Promise<void>;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
appId: string;
|
|
26
|
+
appSecret: string;
|
|
27
|
+
}>;
|
|
13
28
|
}
|
|
14
29
|
interface LarkBootstrapResult {
|
|
15
30
|
/** Challenge-captured token: the PATCH also switched Subscription mode to webhook. */
|
|
@@ -22,6 +37,7 @@ export interface LarkOnboardOptions {
|
|
|
22
37
|
existing?: Readonly<Record<string, string | undefined>>;
|
|
23
38
|
ingress?: FeishuSubscriptionMode;
|
|
24
39
|
verifyCredentials(appId: string, appSecret: string): Promise<void>;
|
|
40
|
+
keep?(appId: string, appSecret: string): Promise<void>;
|
|
25
41
|
bootstrapWebhook?(appId: string, appSecret: string): Promise<LarkBootstrapResult>;
|
|
26
42
|
/** The command that continues an interrupted setup: the one that started it (`add lark`, `deploy <host> --run`). */
|
|
27
43
|
rerun?: string;
|
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { encodeAddons } from "../feishu/register-app.js";
|
|
2
|
+
import { feishuAppAddons } from "../feishu/setup-mode.js";
|
|
1
3
|
/** Guided Lark-international onboarding. */
|
|
2
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Lark's one-click launcher, where the person creates the app. The launcher is unbound (the bound confirm page is broken
|
|
6
|
+
* on the international cloud), but it merges the `addons` query parameter onto the app it creates, as Feishu's does: the
|
|
7
|
+
* scopes and the event the channel needs arrive with the app, so nothing is ticked in the console afterwards. A tenant
|
|
8
|
+
* may still withhold a scope; the permission check that follows creation names it.
|
|
9
|
+
*/
|
|
10
|
+
export function larkConsoleUrl(ingress) {
|
|
11
|
+
const url = new URL("https://open.larksuite.com/page/launcher");
|
|
12
|
+
url.searchParams.set("from", "backend_oneclick");
|
|
13
|
+
url.searchParams.set("addons", encodeAddons(feishuAppAddons(ingress)));
|
|
14
|
+
return url.toString();
|
|
15
|
+
}
|
|
3
16
|
/** Directly where the fallback token lives (and where the successful automatic mode change is visible). */
|
|
4
17
|
export function larkEventSecurityUrl(appId) {
|
|
5
18
|
return `https://open.larksuite.com/app/${encodeURIComponent(appId)}/event?tab=safe`;
|
|
@@ -17,15 +30,30 @@ export async function onboardLarkApp(io, opts) {
|
|
|
17
30
|
const existingSecret = opts.existing?.LARK_APP_SECRET?.trim();
|
|
18
31
|
const reuseExistingApp = Boolean(existingId && existingSecret);
|
|
19
32
|
// Credentials are one App-scoped set.
|
|
33
|
+
// No ingress named is the webhook path below (a deploy's production app).
|
|
34
|
+
const consoleUrl = larkConsoleUrl(opts.ingress ?? "webhook");
|
|
35
|
+
let appId;
|
|
36
|
+
let appSecret;
|
|
37
|
+
const verify = async (appId, appSecret) => {
|
|
38
|
+
await opts.verifyCredentials(appId, appSecret);
|
|
39
|
+
if (!reuseExistingApp)
|
|
40
|
+
await opts.keep?.(appId, appSecret);
|
|
41
|
+
};
|
|
20
42
|
if (reuseExistingApp) {
|
|
21
43
|
io.note(`Reusing Lark app ${existingId}.`);
|
|
44
|
+
appId = required(existingId, "LARK_APP_ID", rerun);
|
|
45
|
+
appSecret = required(existingSecret, "LARK_APP_SECRET", rerun);
|
|
46
|
+
await verify(appId, appSecret);
|
|
47
|
+
}
|
|
48
|
+
else if (io.collect) {
|
|
49
|
+
({ appId, appSecret } = await io.collect({ consoleUrl, check: verify }));
|
|
22
50
|
}
|
|
23
51
|
else {
|
|
24
|
-
io.openUrl(
|
|
52
|
+
io.openUrl(consoleUrl, "create the app on Lark's one-click launcher; LARK_APP_ID and LARK_APP_SECRET are under Credentials & Basic Info");
|
|
53
|
+
appId = required(await io.prompt("LARK_APP_ID (Credentials & Basic Info)", { hidden: false }), "LARK_APP_ID", rerun);
|
|
54
|
+
appSecret = required(await io.prompt("LARK_APP_SECRET (Credentials & Basic Info)", { hidden: true }), "LARK_APP_SECRET", rerun);
|
|
55
|
+
await verify(appId, appSecret);
|
|
25
56
|
}
|
|
26
|
-
const appId = required(reuseExistingApp ? existingId : await io.prompt("LARK_APP_ID (Credentials & Basic Info)", { hidden: false }), "LARK_APP_ID", rerun);
|
|
27
|
-
const appSecret = required(reuseExistingApp ? existingSecret : await io.prompt("LARK_APP_SECRET (Credentials & Basic Info)", { hidden: true }), "LARK_APP_SECRET", rerun);
|
|
28
|
-
await opts.verifyCredentials(appId, appSecret);
|
|
29
57
|
const eventSecurityUrl = larkEventSecurityUrl(appId);
|
|
30
58
|
io.note("App ID / Secret verified.");
|
|
31
59
|
if (opts.ingress === "websocket") {
|
|
@@ -47,7 +75,9 @@ export async function onboardLarkApp(io, opts) {
|
|
|
47
75
|
}
|
|
48
76
|
const verificationToken = required(bootstrap.token ||
|
|
49
77
|
(reuseExistingApp ? opts.existing?.LARK_VERIFICATION_TOKEN?.trim() : undefined) ||
|
|
50
|
-
(
|
|
78
|
+
(io.verificationToken
|
|
79
|
+
? await io.verificationToken(eventSecurityUrl)
|
|
80
|
+
: await io.prompt("LARK_VERIFICATION_TOKEN (Events & Callbacks → Encryption Strategy)", { hidden: true })), "LARK_VERIFICATION_TOKEN", rerun);
|
|
51
81
|
return {
|
|
52
82
|
LARK_APP_ID: appId,
|
|
53
83
|
LARK_APP_SECRET: appSecret,
|