@sokel-dev/plugin-sdk 0.3.0 → 0.5.3

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 CHANGED
@@ -1,7 +1,11 @@
1
1
  # Sokel Plugin SDK — Node.js / TypeScript
2
2
 
3
- 用 TypeScript 写 [Sokel](https://github.com/sokel-dev) 插件。契约写在 `sokel.yaml` 里(语言中立),
4
- `sokel-gen` 把它生成成 TS 接口与类型化的注册口;SDK 负责注册、传输、凭证、文件、心跳与重连。
3
+ [简体中文](README.zh-CN.md)
4
+
5
+ Write [Sokel](https://github.com/sokel-dev) plugins in TypeScript. The contract lives in a
6
+ language-neutral `manifest.yml`; `sokel-gen` turns it into TypeScript interfaces and typed
7
+ registration functions, and the SDK handles registration, transport, credentials, files, heartbeats
8
+ and reconnects.
5
9
 
6
10
  ```ts
7
11
  onIssuesList(p, async (ctx, in_) => {
@@ -10,72 +14,83 @@ onIssuesList(p, async (ctx, in_) => {
10
14
  });
11
15
  ```
12
16
 
13
- `in_.project` 拼错是编译错误,不是线上的一次失败调用——代码里没有任何 `any`。
17
+ A typo in `in_.project` is a compile error, not a failed call in production — there is no `any`
18
+ anywhere in your code.
14
19
 
15
- ## 装
20
+ ## Install
16
21
 
17
22
  ```bash
18
23
  npm install @sokel-dev/plugin-sdk
19
- go install github.com/sokel-dev/sokel-plugin-sdk/cmd/sokel-gen@latest # 生成器
24
+ go install github.com/sokel-dev/sokel-plugin-sdk/cmd/sokel-gen@latest # the generator
20
25
  ```
21
26
 
22
- `sokel-gen` 是个单文件二进制(Go 写的),只在**生成时**用到;跑插件时不需要它。
27
+ `sokel-gen` is a single binary (written in Go) used **only at generation time**; running a plugin
28
+ does not need it.
23
29
 
24
- ## 四步
30
+ ## Four steps
25
31
 
26
32
  ```bash
27
33
  sokel-gen init -lang ts ./my-plugin
28
- cd my-plugin
29
- npm install
30
- sokel-gen generate . # sokel.yaml → src/sokel.gen.ts
34
+ cd my-plugin && npm install
35
+ sokel-gen generate . # manifest.yml → src/sokel.gen.ts
31
36
  npm run build && npm start
32
37
  ```
33
38
 
34
- 1. **声明** —— `sokel.yaml`:操作、事件、凭证、认证方式。格式见 [docs/manifest.md](../docs/manifest.md)。
35
- 2. **生成** —— `sokel-gen generate .` 产出 `src/sokel.gen.ts`:每个操作一对 `XxxIn` / `XxxOut` 接口
36
- 和一个 `onXxx(p, fn)`;每个事件一个 payload 接口和一个 `triggerXxx(ctx, eventId, payload)`。
37
- 3. **实现** —— handler 签名完全具体,返回值可以是对象也可以是 Promise。
38
- 4. **连接** —— `await p.run()`。插件**出站**连平台:无入站端口、无公网 IP、无防火墙洞。
39
+ 1. **Declare** — `manifest.yml`: operations, events, credentials, authentication. Format:
40
+ [docs/manifest.md](../docs/manifest.md), or run `sokel-gen docs`.
41
+ 2. **Generate** — `sokel-gen generate .` writes `src/sokel.gen.ts`: an `XxxIn` / `XxxOut` interface
42
+ pair and an `onXxx(p, fn)` per operation; a payload interface and a
43
+ `triggerXxx(ctx, eventId, payload)` per event.
44
+ 3. **Implement** — handler signatures are fully concrete; return a value or a promise.
45
+ 4. **Connect** — `await p.run()`. A plugin **dials out**: no inbound port, no public IP, no firewall
46
+ hole.
39
47
 
40
- ## 为什么不是 zod
48
+ ## Why not zod
41
49
 
42
- zod schema 是运行时对象:用它声明契约意味着「契约只有跑起来才知道」,而且每种语言都得
43
- 自己解释一遍那套 DSL。声明留在 `sokel.yaml`,TS 这边只要类型——类型在编译期,运行时零开销。
50
+ A zod schema is a runtime object, so declaring the contract in zod means the contract only exists
51
+ once the process is running — and every language would have to interpret that DSL for itself. The
52
+ declaration stays in `manifest.yml`; TypeScript takes only the types, which is what TypeScript is good
53
+ at: compile-time checks, zero runtime cost.
44
54
 
45
- ## 能力一览
55
+ ## What you can do
46
56
 
47
- | 要做的事 | 怎么写 |
57
+ | Task | How |
48
58
  |---|---|
49
- | 读凭证 | `ctx.credentialAs<Credential>()` |
50
- | 取入参文件的字节 | `await ctx.fetch(in_.file)` |
51
- | 产出文件 | `await ctx.upload(name, mime, bytes)` → 放进出参 |
52
- | 流式产出 | `out.text(...)` 逐帧给人看,`out.vars({...})` 给下游 |
53
- | 推事件 | `await triggerMessage(ctx, eventId, {...})` |
54
- | 常驻事件源 | `p.registerSource(id, label, fn)`,循环里判 `ctx.stopped` |
55
- | 平台代收 webhook | `p.registerWebhook(fn)`,返回 `ok()` / `text(401, "...")` |
56
- | 协作式认证 | `p.registerAuth({ start, poll, submit })` |
57
- | 会话型凭证刷新 | `await ctx.updateCredential({ session: "…" })` |
58
- | 自报运行态 | `ctx.reportStatus("auth_required", "…")` |
59
-
60
- ## 配置
61
-
62
- SDK 读 `SOKEL_` 前缀的环境变量:
63
-
64
- | 变量 | 必填 | 含义 |
59
+ | Read credentials | `ctx.credentialAs<Credential>()` |
60
+ | Read an input file's bytes | `await ctx.fetch(in_.file)` |
61
+ | Produce a file | `await ctx.upload(name, mime, bytes)`, or `await ctx.uploadFile(path)` for large files |
62
+ | Stream output | `out.text(...)` frame by frame for humans, `out.vars({...})` for downstream nodes |
63
+ | Push an event | `await triggerMessage(ctx, eventId, {...})` |
64
+ | Long-running event source | `p.registerSource(id, label, fn)`; loop while `!ctx.stopped` |
65
+ | Handle a platform-relayed webhook | `p.registerWebhook(fn)`, return `ok()` / `text(401, "...")` |
66
+ | Collaborative authentication | `p.registerAuth({ start, poll, submit })` |
67
+ | Refresh a session credential | `await ctx.updateCredential({ session: "…" })` |
68
+ | Report runtime state | `ctx.reportStatus("auth_required", "…")` |
69
+
70
+ `uploadFile(path)` streams from disk: memory stays at one chunk (1 MiB) regardless of file size.
71
+ Anything above a few hundred megabytes should use it — `upload(bytes)` reads the whole file into
72
+ memory first, and the symptom of that is a container mysteriously killed by the OOM reaper.
73
+
74
+ ## Configuration
75
+
76
+ The SDK reads `SOKEL_`-prefixed environment variables:
77
+
78
+ | Variable | Required | Meaning |
65
79
  |---|---|---|
66
- | `SOKEL_ENDPOINT` | 是 | `nats://broker:4222`,或 `https://` 平台地址(经 `/connect-info` 发现 broker) |
67
- | `SOKEL_TOKEN` | 是 | 接入组 token(`skp_…`),平台据此认「插件 + 工作空间」 |
68
- | `SOKEL_NATS_TOKEN` | 否 | broker 的传输层鉴权 |
69
- | `SOKEL_NATS_CA` | 否 | `tls://` broker 的自定义 CA |
70
- | `SOKEL_INSTANCE_ID` | 否 | 固定副本身份(默认按 token 指纹落盘复用) |
71
- | `SOKEL_REGION` | 否 | 副本的地域标注 |
80
+ | `SOKEL_ENDPOINT` | yes | `nats://broker:4222`, or an `https://` platform URL to discover the broker from |
81
+ | `SOKEL_TOKEN` | yes | Access-group token (`skp_…`) identifying plugin + workspace |
82
+ | `SOKEL_NATS_TOKEN` | no | Broker-level auth |
83
+ | `SOKEL_NATS_CA` | no | Custom CA bundle for `tls://` brokers |
84
+ | `SOKEL_INSTANCE_ID` | no | Pin a replica identity (otherwise derived from the token and cached on disk) |
85
+ | `SOKEL_REGION` | no | Region label for the replica |
72
86
 
73
- 凭证从不由插件存储:平台随每次调用把解析后的字段下发下来。
87
+ Credentials are never stored by the plugin: the platform injects the resolved fields with each call.
74
88
 
75
- ## 例子
89
+ ## Example
76
90
 
77
- [`examples/kitchen-sink`](../examples/kitchen-sink) 覆盖了全部形态——每种字段、文件、流式、
78
- 事件、webhook、协作式认证各一份,Node 与 Python 实现的是同一份声明。
91
+ [`examples/kitchen-sink`](../examples/kitchen-sink) covers every shape — each field type, files,
92
+ streaming, events, webhooks, collaborative auth — and the Node and Python implementations share one
93
+ declaration.
79
94
 
80
95
  ```bash
81
96
  cd examples/kitchen-sink/node
@@ -83,7 +98,7 @@ npm install && npm run build
83
98
  SOKEL_ENDPOINT=nats://localhost:4222 SOKEL_TOKEN=skp_xxx npm start
84
99
  ```
85
100
 
86
- ## 开发本 SDK
101
+ ## Developing this SDK
87
102
 
88
103
  ```bash
89
104
  pnpm install
@@ -0,0 +1,95 @@
1
+ # Sokel Plugin SDK — Node.js / TypeScript
2
+
3
+ 用 TypeScript 写 [Sokel](https://github.com/sokel-dev) 插件。契约写在 `manifest.yml` 里(语言中立),
4
+ `sokel-gen` 把它生成成 TS 接口与类型化的注册口;SDK 负责注册、传输、凭证、文件、心跳与重连。
5
+
6
+ ```ts
7
+ onIssuesList(p, async (ctx, in_) => {
8
+ const issues = await client.listIssues(in_.project, in_.state);
9
+ return { issues, count: issues.length };
10
+ });
11
+ ```
12
+
13
+ `in_.project` 拼错是编译错误,不是线上的一次失败调用——代码里没有任何 `any`。
14
+
15
+ ## 装
16
+
17
+ ```bash
18
+ npm install @sokel-dev/plugin-sdk
19
+ go install github.com/sokel-dev/sokel-plugin-sdk/cmd/sokel-gen@latest # 生成器
20
+ ```
21
+
22
+ `sokel-gen` 是个单文件二进制(Go 写的),只在**生成时**用到;跑插件时不需要它。
23
+
24
+ ## 四步
25
+
26
+ ```bash
27
+ sokel-gen init -lang ts ./my-plugin
28
+ cd my-plugin
29
+ npm install
30
+ sokel-gen generate . # manifest.yml → src/sokel.gen.ts
31
+ npm run build && npm start
32
+ ```
33
+
34
+ 1. **声明** —— `manifest.yml`:操作、事件、凭证、认证方式。格式见 [docs/manifest.md](../docs/manifest.md)。
35
+ 2. **生成** —— `sokel-gen generate .` 产出 `src/sokel.gen.ts`:每个操作一对 `XxxIn` / `XxxOut` 接口
36
+ 和一个 `onXxx(p, fn)`;每个事件一个 payload 接口和一个 `triggerXxx(ctx, eventId, payload)`。
37
+ 3. **实现** —— handler 签名完全具体,返回值可以是对象也可以是 Promise。
38
+ 4. **连接** —— `await p.run()`。插件**出站**连平台:无入站端口、无公网 IP、无防火墙洞。
39
+
40
+ ## 为什么不是 zod
41
+
42
+ zod schema 是运行时对象:用它声明契约意味着「契约只有跑起来才知道」,而且每种语言都得
43
+ 自己解释一遍那套 DSL。声明留在 `manifest.yml`,TS 这边只要类型——类型在编译期,运行时零开销。
44
+
45
+ ## 能力一览
46
+
47
+ | 要做的事 | 怎么写 |
48
+ |---|---|
49
+ | 读凭证 | `ctx.credentialAs<Credential>()` |
50
+ | 取入参文件的字节 | `await ctx.fetch(in_.file)` |
51
+ | 产出文件 | `await ctx.upload(name, mime, bytes)` → 放进出参 |
52
+ | 流式产出 | `out.text(...)` 逐帧给人看,`out.vars({...})` 给下游 |
53
+ | 推事件 | `await triggerMessage(ctx, eventId, {...})` |
54
+ | 常驻事件源 | `p.registerSource(id, label, fn)`,循环里判 `ctx.stopped` |
55
+ | 平台代收 webhook | `p.registerWebhook(fn)`,返回 `ok()` / `text(401, "...")` |
56
+ | 协作式认证 | `p.registerAuth({ start, poll, submit })` |
57
+ | 会话型凭证刷新 | `await ctx.updateCredential({ session: "…" })` |
58
+ | 自报运行态 | `ctx.reportStatus("auth_required", "…")` |
59
+
60
+ ## 配置
61
+
62
+ SDK 读 `SOKEL_` 前缀的环境变量:
63
+
64
+ | 变量 | 必填 | 含义 |
65
+ |---|---|---|
66
+ | `SOKEL_ENDPOINT` | 是 | `nats://broker:4222`,或 `https://` 平台地址(经 `/connect-info` 发现 broker) |
67
+ | `SOKEL_TOKEN` | 是 | 接入组 token(`skp_…`),平台据此认「插件 + 工作空间」 |
68
+ | `SOKEL_NATS_TOKEN` | 否 | broker 的传输层鉴权 |
69
+ | `SOKEL_NATS_CA` | 否 | `tls://` broker 的自定义 CA |
70
+ | `SOKEL_INSTANCE_ID` | 否 | 固定副本身份(默认按 token 指纹落盘复用) |
71
+ | `SOKEL_REGION` | 否 | 副本的地域标注 |
72
+
73
+ 凭证从不由插件存储:平台随每次调用把解析后的字段下发下来。
74
+
75
+ ## 例子
76
+
77
+ [`examples/kitchen-sink`](../examples/kitchen-sink) 覆盖了全部形态——每种字段、文件、流式、
78
+ 事件、webhook、协作式认证各一份,Node 与 Python 实现的是同一份声明。
79
+
80
+ ```bash
81
+ cd examples/kitchen-sink/node
82
+ npm install && npm run build
83
+ SOKEL_ENDPOINT=nats://localhost:4222 SOKEL_TOKEN=skp_xxx npm start
84
+ ```
85
+
86
+ ## 开发本 SDK
87
+
88
+ ```bash
89
+ pnpm install
90
+ pnpm test # tsc + node --test
91
+ ```
92
+
93
+ ## License
94
+
95
+ Apache-2.0.
@@ -1,27 +1,31 @@
1
1
  /**
2
- * 协作式凭证认证:有些凭证没法让人填——扫码、验证码回填、OAuth 同意页。
2
+ * Collaborative credential authentication: some credentials cannot be typed in by hand — a QR scan,
3
+ * a verification code, an OAuth consent page.
3
4
  *
4
- * 面板点「登录」→ start 拿挑战 →(扫码 / 回填)→ 2s 轮询 poll → confirmed。
5
+ * the panel's "log in" button -> start returns a challenge -> (scan / type it back)
6
+ * -> poll every 2s -> confirmed
5
7
  *
6
- * 形态写在 sokel.yaml 的 credential.auth 里(声明式),处理器挂在保留操作 id
7
- * auth.start / auth.poll / auth.submit 上——**不要**自己注册叫 auth_start 的业务操作:
8
- * 那三个名字从来不是保留字,任何插件的同名业务操作都会让面板的按钮凭空出现。
8
+ * The shape is declared in manifest.yml under credential.auth; the handlers hang off the reserved
9
+ * operation ids auth.start / auth.poll / auth.submit. **Do not** register a business operation named
10
+ * auth_start: those three names were never reserved, so any plugin with an operation of that name
11
+ * made the panel's button appear out of nowhere.
9
12
  */
10
13
  import type { Ctx } from "./runtime.js";
11
14
  export declare const PENDING = "pending";
12
15
  export declare const SCANNED = "scanned";
13
16
  export declare const CONFIRMED = "confirmed";
14
17
  export declare const EXPIRED = "expired";
15
- /** start 交出的挑战。面板按 kind 渲染:qr 画二维码,input 显示 prompt 与输入框。 */
18
+ /** What start hands back. The panel renders by kind: qr draws a code, input shows the prompt. */
16
19
  export interface AuthChallenge {
17
20
  authId?: string;
18
21
  kind?: "qr" | "input";
19
- /** data-uri 形态的二维码图片。 */
22
+ /** The QR image as a data-uri. */
20
23
  qrImage?: string;
21
24
  prompt?: string;
22
25
  expiresIn?: number;
23
26
  }
24
- /** poll 的结果。session 只在 confirmed 时带上——中途带出去等于让平台反复覆写凭证行。 */
27
+ /** The result of poll. Carry `session` only once confirmed — handing it over earlier makes the
28
+ * platform rewrite the credential row again and again. */
25
29
  export interface AuthState {
26
30
  status: string;
27
31
  session?: Record<string, unknown>;
package/dist/src/auth.js CHANGED
@@ -1,12 +1,5 @@
1
- /**
2
- * 协作式凭证认证:有些凭证没法让人填——扫码、验证码回填、OAuth 同意页。
3
- *
4
- * 面板点「登录」→ start 拿挑战 →(扫码 / 回填)→ 2s 轮询 poll → confirmed。
5
- *
6
- * 形态写在 sokel.yaml 的 credential.auth 里(声明式),处理器挂在保留操作 id
7
- * auth.start / auth.poll / auth.submit 上——**不要**自己注册叫 auth_start 的业务操作:
8
- * 那三个名字从来不是保留字,任何插件的同名业务操作都会让面板的按钮凭空出现。
9
- */
1
+ // Copyright 2026 The Sokel Authors
2
+ // SPDX-License-Identifier: Apache-2.0
10
3
  export const PENDING = "pending";
11
4
  export const SCANNED = "scanned";
12
5
  export const CONFIRMED = "confirmed";
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAIH,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AACrC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC"}
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAiBtC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC;AACjC,MAAM,CAAC,MAAM,SAAS,GAAG,WAAW,CAAC;AACrC,MAAM,CAAC,MAAM,OAAO,GAAG,SAAS,CAAC"}
@@ -1,10 +1,11 @@
1
1
  /**
2
- * 契约的运行时视图(线协议 §5)。
2
+ * The runtime view of a contract (wire protocol §5).
3
3
  *
4
- * 契约本身是**数据**:`sokel-gen` 从 sokel.yaml 生成一份 CONTRACT 常量,运行时只是查它、上报它。
5
- * 所以这里不重新定义一套 Field 校验器——那会变成契约的第二份定义,而两份定义迟早会漂。
4
+ * A contract is **data**: `sokel-gen` renders a CONTRACT constant from manifest.yml, and the runtime
5
+ * only looks things up in it and reports it. So there is no second Field validator here — that
6
+ * would be a second definition of the contract, and two definitions drift.
6
7
  */
7
- /** 一个入/出参字段(协议 §5 的 Field)。这里只声明形状,不做校验——校验在 sokel-gen。 */
8
+ /** One input/output field (protocol §5's Field). Shape only; validation lives in sokel-gen. */
8
9
  export interface Field {
9
10
  name: string;
10
11
  label?: string;
@@ -36,6 +37,11 @@ export interface OperationSpec {
36
37
  stream?: boolean;
37
38
  internal?: boolean;
38
39
  timeoutSec?: number;
40
+ /** Which platform capability slot this operation fills ("rowstore.query" comes from
41
+ * `implements:` in manifest.yml). Absent on ordinary operations. It is reported as-is; the
42
+ * platform routes on it. Missing it here made every generated shell that declares `implements:`
43
+ * fail to compile. */
44
+ capability?: string;
39
45
  inputs: Field[];
40
46
  outputs: Field[];
41
47
  }
@@ -49,9 +55,12 @@ export interface AuthFlowSpec {
49
55
  kind: "qr" | "input" | "oauth";
50
56
  steps?: string[];
51
57
  }
52
- /** 生成物 CONTRACT 的形状。键名与注册载荷(协议 §3)同名,直接上报,不做转换。 */
58
+ /** The shape of the generated CONTRACT. Keys match the registration payload (protocol §3)
59
+ * verbatim, so they are reported as-is with no translation step. */
53
60
  export interface ContractData {
54
61
  name?: string;
62
+ /** Publisher identity: `<org>/<name>` is how the plugin is addressed in the marketplace. */
63
+ org?: string;
55
64
  label?: string;
56
65
  desc?: string;
57
66
  version?: string;
@@ -65,16 +74,24 @@ export interface ContractData {
65
74
  scopes?: string[];
66
75
  };
67
76
  capabilities?: Record<string, boolean>;
77
+ /** Translations of the human-facing strings, keyed by locale then by the source string
78
+ * (`{"zh-CN": {"Row query": "按行查询"}}`). Generated from `locales/<lang>.json` next to the
79
+ * manifest; the platform renders whichever locale the viewer is in. */
80
+ locales?: Record<string, Record<string, string>>;
68
81
  doc?: string;
69
82
  doc_url?: string;
70
83
  }
71
- /** 保留操作 id(认证流)。带点号,业务 id 产生不出来(业务 id 限定 ^[a-z][a-z0-9_]*$)。 */
84
+ /** Reserved operation ids (the auth flow).
85
+ *
86
+ * A **plain** business id cannot contain a dot (it must match ^[a-z][a-z0-9_]*$), but a capability
87
+ * slot's id does — `implements:` produces "rowstore.query" — so "has a dot" no longer means
88
+ * "reserved". Compare against these constants rather than looking for a dot. */
72
89
  export declare const OP_AUTH_START = "auth.start";
73
90
  export declare const OP_AUTH_POLL = "auth.poll";
74
91
  export declare const OP_AUTH_SUBMIT = "auth.submit";
75
- /** 平台代收 webhook 的特殊操作名(复用调用帧,见协议 §7b)。 */
92
+ /** The special operation name for platform-relayed webhooks (it reuses the call frame, §7b). */
76
93
  export declare const OP_WEBHOOK = "__webhook__";
77
- /** 能力位:注册了 webhook 处理器就是支持,不靠作者手动声明。 */
94
+ /** Capability bit: registering a webhook handler *is* the declaration; the author does not repeat it. */
78
95
  export declare const CAP_WEBHOOK = "webhook";
79
96
  export declare class Contract {
80
97
  readonly data: ContractData;
@@ -83,6 +100,7 @@ export declare class Contract {
83
100
  operation(id: string): OperationSpec | undefined;
84
101
  isStream(id: string): boolean;
85
102
  eventIds(): string[];
86
- /** 契约部分的注册载荷。空值一律省略(协议:新字段一律 optional)。 */
103
+ /** The contract half of the registration payload. Empty values are omitted (the protocol makes
104
+ * every new field optional). */
87
105
  payload(): Record<string, unknown>;
88
106
  }
@@ -1,16 +1,16 @@
1
- /**
2
- * 契约的运行时视图(线协议 §5)。
1
+ // Copyright 2026 The Sokel Authors
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /** Reserved operation ids (the auth flow).
3
4
  *
4
- * 契约本身是**数据**:`sokel-gen` 从 sokel.yaml 生成一份 CONTRACT 常量,运行时只是查它、上报它。
5
- * 所以这里不重新定义一套 Field 校验器——那会变成契约的第二份定义,而两份定义迟早会漂。
6
- */
7
- /** 保留操作 id(认证流)。带点号,业务 id 产生不出来(业务 id 限定 ^[a-z][a-z0-9_]*$)。 */
5
+ * A **plain** business id cannot contain a dot (it must match ^[a-z][a-z0-9_]*$), but a capability
6
+ * slot's id does — `implements:` produces "rowstore.query" — so "has a dot" no longer means
7
+ * "reserved". Compare against these constants rather than looking for a dot. */
8
8
  export const OP_AUTH_START = "auth.start";
9
9
  export const OP_AUTH_POLL = "auth.poll";
10
10
  export const OP_AUTH_SUBMIT = "auth.submit";
11
- /** 平台代收 webhook 的特殊操作名(复用调用帧,见协议 §7b)。 */
11
+ /** The special operation name for platform-relayed webhooks (it reuses the call frame, §7b). */
12
12
  export const OP_WEBHOOK = "__webhook__";
13
- /** 能力位:注册了 webhook 处理器就是支持,不靠作者手动声明。 */
13
+ /** Capability bit: registering a webhook handler *is* the declaration; the author does not repeat it. */
14
14
  export const CAP_WEBHOOK = "webhook";
15
15
  export class Contract {
16
16
  data;
@@ -29,7 +29,8 @@ export class Contract {
29
29
  eventIds() {
30
30
  return (this.data.events ?? []).map((e) => e.id);
31
31
  }
32
- /** 契约部分的注册载荷。空值一律省略(协议:新字段一律 optional)。 */
32
+ /** The contract half of the registration payload. Empty values are omitted (the protocol makes
33
+ * every new field optional). */
33
34
  payload() {
34
35
  const out = { operations: this.operations() };
35
36
  const d = this.data;
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/contract.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AA4DH,gEAAgE;AAChE,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;AAC1C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAC;AAE5C,0CAA0C;AAC1C,MAAM,CAAC,MAAM,UAAU,GAAG,aAAa,CAAC;AAExC,wCAAwC;AACxC,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC,MAAM,OAAO,QAAQ;IACE;IAArB,YAAqB,IAAkB;QAAlB,SAAI,GAAJ,IAAI,CAAc;IAAG,CAAC;IAE3C,UAAU;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;IACpC,CAAC;IAED,SAAS,CAAC,EAAU;QAClB,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,QAAQ,CAAC,EAAU;QACjB,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,CAAC;IAED,QAAQ;QACN,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC;IAED,2CAA2C;IAC3C,OAAO;QACL,MAAM,GAAG,GAA4B,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;QACvE,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACpB,IAAI,CAAC,CAAC,iBAAiB,EAAE,MAAM;YAAE,GAAG,CAAC,iBAAiB,GAAG,CAAC,CAAC,iBAAiB,CAAC;QAC7E,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM;YAAE,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QAC5C,IAAI,CAAC,CAAC,aAAa,EAAE,MAAM;YAAE,GAAG,CAAC,aAAa,GAAG,CAAC,CAAC,aAAa,CAAC;QACjE,IAAI,CAAC,CAAC,SAAS;YAAE,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;QAC7C,IAAI,CAAC,CAAC,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;QACjC,IAAI,CAAC,CAAC,YAAY,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM;YAAE,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;QAC5F,IAAI,CAAC,CAAC,GAAG;YAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QAC3B,IAAI,CAAC,CAAC,OAAO;YAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QACvC,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/contract.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAgFtC;;;;gFAIgF;AAChF,MAAM,CAAC,MAAM,aAAa,GAAG,YAAY,CAAC;AAC1C,MAAM,CAAC,MAAM,YAAY,GAAG,WAAW,CAAC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,aAAa,CAAC;AAE5C,gGAAgG;AAChG,MAAM,CAAC,MAAM,UAAU,GAAG,aAAa,CAAC;AAExC,yGAAyG;AACzG,MAAM,CAAC,MAAM,WAAW,GAAG,SAAS,CAAC;AAErC,MAAM,OAAO,QAAQ;IACE;IAArB,YAAqB,IAAkB;QAAlB,SAAI,GAAJ,IAAI,CAAc;IAAG,CAAC;IAE3C,UAAU;QACR,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,EAAE,CAAC;IACpC,CAAC;IAED,SAAS,CAAC,EAAU;QAClB,OAAO,IAAI,CAAC,UAAU,EAAE,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;IACtD,CAAC;IAED,QAAQ,CAAC,EAAU;QACjB,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,CAAC;IAED,QAAQ;QACN,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IACnD,CAAC;IAED;oCACgC;IAChC,OAAO;QACL,MAAM,GAAG,GAA4B,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC;QACvE,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;QACpB,IAAI,CAAC,CAAC,iBAAiB,EAAE,MAAM;YAAE,GAAG,CAAC,iBAAiB,GAAG,CAAC,CAAC,iBAAiB,CAAC;QAC7E,IAAI,CAAC,CAAC,MAAM,EAAE,MAAM;YAAE,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;QAC5C,IAAI,CAAC,CAAC,aAAa,EAAE,MAAM;YAAE,GAAG,CAAC,aAAa,GAAG,CAAC,CAAC,aAAa,CAAC;QACjE,IAAI,CAAC,CAAC,SAAS;YAAE,GAAG,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,CAAC;QAC7C,IAAI,CAAC,CAAC,KAAK;YAAE,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC;QACjC,IAAI,CAAC,CAAC,YAAY,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,MAAM;YAAE,GAAG,CAAC,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;QAC5F,IAAI,CAAC,CAAC,GAAG;YAAE,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QAC3B,IAAI,CAAC,CAAC,OAAO;YAAE,GAAG,CAAC,OAAO,GAAG,CAAC,CAAC,OAAO,CAAC;QACvC,OAAO,GAAG,CAAC;IACb,CAAC;CACF"}
package/dist/src/env.d.ts CHANGED
@@ -1,8 +1,3 @@
1
- /**
2
- * 插件侧环境变量的统一读法:把 SOKEL_ 前缀收在一处(对齐 Go 侧 pluginenv)。
3
- *
4
- * 没有第二个前缀的兼容层——认第二个前缀省下的是一次重新部署,换来的是一个没人敢摘的包袱。
5
- */
6
- /** 读 SOKEL_<name>。name 不带前缀,如 env("TOKEN")。 */
1
+ /** Read SOKEL_<name>. `name` carries no prefix, e.g. env("TOKEN"). */
7
2
  export declare function env(name: string): string;
8
3
  export declare function envOr(name: string, fallback: string): string;
package/dist/src/env.js CHANGED
@@ -1,10 +1,13 @@
1
+ // Copyright 2026 The Sokel Authors
2
+ // SPDX-License-Identifier: Apache-2.0
1
3
  /**
2
- * 插件侧环境变量的统一读法:把 SOKEL_ 前缀收在一处(对齐 Go 侧 pluginenv)。
4
+ * One place to read the plugin's environment variables (mirrors pluginenv on the Go side).
3
5
  *
4
- * 没有第二个前缀的兼容层——认第二个前缀省下的是一次重新部署,换来的是一个没人敢摘的包袱。
6
+ * There is no compatibility layer for a second prefix: accepting one saves a single redeploy and
7
+ * buys a piece of history nobody dares remove.
5
8
  */
6
9
  const PREFIX = "SOKEL_";
7
- /** 读 SOKEL_<name>。name 不带前缀,如 env("TOKEN")。 */
10
+ /** Read SOKEL_<name>. `name` carries no prefix, e.g. env("TOKEN"). */
8
11
  export function env(name) {
9
12
  return (process.env[PREFIX + name] ?? "").trim();
10
13
  }
@@ -1 +1 @@
1
- {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/env.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,MAAM,GAAG,QAAQ,CAAC;AAExB,+CAA+C;AAC/C,MAAM,UAAU,GAAG,CAAC,IAAY;IAC9B,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,IAAY,EAAE,QAAgB;IAClD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;AAC/B,CAAC"}
1
+ {"version":3,"file":"env.js","sourceRoot":"","sources":["../../src/env.ts"],"names":[],"mappings":"AAAA,mCAAmC;AACnC,sCAAsC;AAEtC;;;;;GAKG;AAEH,MAAM,MAAM,GAAG,QAAQ,CAAC;AAExB,sEAAsE;AACtE,MAAM,UAAU,GAAG,CAAC,IAAY;IAC9B,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACnD,CAAC;AAED,MAAM,UAAU,KAAK,CAAC,IAAY,EAAE,QAAgB;IAClD,OAAO,GAAG,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC;AAC/B,CAAC"}
@@ -1,20 +1,22 @@
1
1
  /**
2
- * 事件源:插件主动把外部事件推给平台起 workflow(协议 §7)。
2
+ * Event sources: the plugin pushes external events to the platform to start workflows (protocol §7).
3
3
  *
4
- * 与操作的区别:操作是 request/reply(平台调插件),事件是 fire-and-forget(插件推平台)。
4
+ * How this differs from operations: an operation is request/reply (the platform calls the plugin);
5
+ * an event is fire-and-forget (the plugin pushes to the platform).
5
6
  *
6
- * 多 bot 单实例(协议 v1.3):平台每次注册/心跳下发「分配给本副本的凭证子集」,
7
- * supervisor 按它 reconcile —— 每个凭证一套源实例,凭证被移除就取消,字段变了就重启。
7
+ * Many bots, one replica (protocol v1.3): every registration and heartbeat returns "the subset of
8
+ * credentials assigned to this replica", and the supervisor reconciles against it — one source
9
+ * instance per credential, cancelled when the credential goes away, restarted when its fields change.
8
10
  */
9
11
  import type { FileRuntime, SokelFile } from "./runtime.js";
10
12
  export declare const TRIGGER_SUBJECT = "sokel.trigger";
11
13
  export declare const CREDENTIAL_UPDATE_SUBJECT = "sokel.credential.update";
12
- /** 注册回包 credentials 列表项 —— 分配给本副本的一个 bot 身份。 */
14
+ /** One entry of the registration reply's credentials list — a bot identity assigned here. */
13
15
  export declare class CredEntry {
14
16
  readonly id: string;
15
17
  readonly fields: Record<string, string>;
16
18
  constructor(id?: string, fields?: Record<string, string>);
17
- /** 字段的稳定签名:reconcile 据此判定「字段变更 → 重启该源实例」。 */
19
+ /** A stable signature of the fields; reconcile uses it to decide "fields changed -> restart". */
18
20
  sig(): string;
19
21
  }
20
22
  export interface SourceState {
@@ -24,29 +26,32 @@ export interface SourceState {
24
26
  error?: string;
25
27
  since: string;
26
28
  }
27
- /** 源实例运行态(源 × 凭证)。随注册/心跳上报,面板据此展示每个 bot。 */
29
+ /** Runtime state per source × credential. Reported with each registration/heartbeat so the panel
30
+ * can show every bot. */
28
31
  export declare class StateBoard {
29
32
  private readonly now;
30
33
  private readonly m;
31
34
  constructor(now?: () => string);
32
35
  set(sourceId: string, credId: string, status: string, error?: string): void;
33
36
  /**
34
- * 只在该实例仍是 running 时改写。
37
+ * Overwrite only while the instance is still `running`.
35
38
  *
36
- * 源自报过状态(如 auth_required)之后正常返回,收尾时不该把那句话盖掉——
37
- * 盖掉之后面板上只剩一个「已退出」,而「为什么退出」正是要看的那一半。
39
+ * A source that reported its own status (auth_required, say) and then returned normally should not
40
+ * have that sentence overwritten on the way out: all the panel would show is "exited", and *why*
41
+ * it exited is the half that matters.
38
42
  */
39
43
  setIfRunning(sourceId: string, credId: string, status: string, error?: string): void;
40
44
  removeCred(credId: string): void;
41
45
  snapshot(): SourceState[];
42
46
  }
43
47
  export type Publish = (subject: string, data: Uint8Array) => void | Promise<void>;
44
- /** 常驻事件源 / webhook 的上下文:推事件、读凭证、回写凭证、上传附件、自报状态。 */
48
+ /** The context for a long-running source or a webhook: push events, read and write back
49
+ * credentials, upload attachments, report state. */
45
50
  export declare class SourceCtx {
46
51
  readonly credential: Record<string, string>;
47
52
  readonly credentialId: string;
48
53
  readonly sourceId: string;
49
- /** stopped:该源实例被 reconcile 停止时置真。长轮询循环 while (!ctx.stopped)。 */
54
+ /** Set to true when reconcile stops this instance. Long-poll loops run `while (!ctx.stopped)`. */
50
55
  stopped: boolean;
51
56
  private readonly token;
52
57
  private readonly publish;
@@ -63,33 +68,36 @@ export declare class SourceCtx {
63
68
  board?: StateBoard;
64
69
  files?: FileRuntime;
65
70
  });
66
- /** 凭证按类型化形状读出(与操作侧 Ctx.credentialAs 同义)。 */
71
+ /** Read the credential into a typed shape (same as Ctx.credentialAs on the operation side). */
67
72
  credentialAs<T extends object>(): Partial<T>;
68
73
  /**
69
- * 推一条事件(fire-and-forget)。
74
+ * Push one event (fire-and-forget).
70
75
  *
71
- * event 必须是已声明的事件 id —— 拼错在这里当场报错,而不是变成一条平台侧无人认领的
72
- * 消息(那种失败没有任何症状:插件日志正常,工作流就是不起)。
73
- * eventId 是幂等键,平台按 (plugin, event, eventId) 去重。
76
+ * `event` must be a declared event id: a typo fails here rather than turning into a message nobody
77
+ * on the platform claims. That failure mode has no symptoms — the plugin log looks fine and the
78
+ * workflow simply never starts. `eventId` is the idempotency key; the platform deduplicates on
79
+ * (plugin, event, eventId).
74
80
  */
75
81
  trigger(event: string, eventId: string, payload: unknown): Promise<void>;
76
82
  /**
77
- * 把 patch 回写到本实例绑定的平台凭证(会话型凭证运行中刷新用)。
78
- * 平台是唯一凭证存储方,插件本地从不落地凭证。
83
+ * Write a patch back to the credential bound to this instance (how a session-style credential
84
+ * refreshes itself while running).
85
+ * The platform is the only store for credentials; a plugin never persists them locally.
79
86
  */
80
87
  updateCredential(patch: Record<string, string>): Promise<void>;
81
- /** 自报运行态(如 session 失效 → auth_required),随心跳上报,面板亮「待登录」。 */
88
+ /** Report state (an expired session becomes auth_required); it rides the heartbeat and lights up
89
+ * "needs login" in the panel. */
82
90
  reportStatus(status: string, msg?: string): void;
83
91
  upload(name: string, mime: string, data: Uint8Array): Promise<SokelFile>;
84
92
  fetch(f: SokelFile): Promise<Uint8Array>;
85
93
  }
86
- /** 一个常驻事件源。fn 在 SDK 起的任务里跑,内部用 ctx.trigger 推事件。 */
94
+ /** A long-running event source. The SDK runs fn in its own task; inside, ctx.trigger pushes. */
87
95
  export interface Source {
88
96
  id: string;
89
97
  label: string;
90
98
  fn: (ctx: SourceCtx) => Promise<void>;
91
99
  }
92
- /** per-credential 源实例监督器:按平台下发的凭证集合起停/重启。 */
100
+ /** Per-credential supervisor: starts, stops and restarts instances to match the assigned set. */
93
101
  export declare class SourceSupervisor {
94
102
  private readonly start;
95
103
  private readonly running;
@@ -97,5 +105,6 @@ export declare class SourceSupervisor {
97
105
  reconcile(desired: CredEntry[]): void;
98
106
  stopAll(): void;
99
107
  }
100
- /** 空(无凭证插件)→ 一个空凭证裸实例,与有凭证时同一条代码路径。 */
108
+ /** Empty (a plugin with no credentials) becomes one bare instance, so both cases take the same
109
+ * code path. */
101
110
  export declare function desiredSourceCreds(creds: CredEntry[]): CredEntry[];