@zoowork-ai/sdk 0.4.0 → 0.4.2

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/CHANGELOG.md CHANGED
@@ -3,6 +3,46 @@
3
3
  All notable changes to `@zoowork-ai/sdk` (formerly `@zooclaw-agents/sdk`). Dates are the
4
4
  day the behaviour was verified, not the day it was written.
5
5
 
6
+ ## 0.4.2 — 2026-08-25
7
+
8
+ ### Documentation
9
+
10
+ - **`addChannel` is idempotent, not an upsert — 0.3.4 said the opposite.** Re-posting an
11
+ identical body for the same `platform` + `account` replays the binding you already have and
12
+ answers `201` again; that same pair with a **different** `config` answers
13
+ `409 channel.conflict`. Rotating credentials therefore means `removeChannel` and then a
14
+ fresh `addChannel` — a plain re-add fails. The earlier note came from re-posting the same
15
+ body, where a replay cannot be told apart from an overwrite; the conflict path was measured
16
+ on staging 2026-08-25. No signature or behaviour change.
17
+
18
+ ## 0.4.1 — 2026-08-25
19
+
20
+ ### Internal
21
+
22
+ - **Trailing slashes are stripped from the base URL by a scan rather than `/\/+$/`.** Same
23
+ output for every input; the regex retried at every start position on a long run of
24
+ slashes, which CodeQL flags as polynomial. Nothing hostile reaches it — the input is the
25
+ caller's own base URL — so this closes an alert rather than a vulnerability.
26
+
27
+ ### Documentation
28
+
29
+ - **`account` on `addChannel` / `startFeishuSetup` now documents what it actually is.** It
30
+ names a binding and is part of its identity — `updateChannel` and `removeChannel` look a
31
+ binding up by `platform` + `account`, and nothing renames one. The four constraints, all
32
+ staging-verified 2026-08-25:
33
+ - the name is unique per USER across every agent, not per agent;
34
+ - `'default'` is usually taken already by a binding the app made, which this API will not
35
+ adopt — it answers `409 channel.conflict`;
36
+ - the format is `^[a-z0-9][a-z0-9_-]{0,63}$` plus three reserved words, and nothing is
37
+ normalized for you;
38
+ - this SDK cannot pre-check a name, because `listChannels` is scoped to one agent while the
39
+ constraint spans the whole account.
40
+ - **`startFeishuSetup` warns that a name clash surfaces after the scan.** Approving the QR
41
+ registers a new app in the Feishu workspace before the binding is written, so a clash costs
42
+ a scan and leaves that app behind; retrying under the same name repeats both.
43
+
44
+ No runtime change — comments only.
45
+
6
46
  ## 0.4.0 — 2026-08-25
7
47
 
8
48
  ### Changed (breaking)
package/dist/client.d.ts CHANGED
@@ -275,7 +275,29 @@ export type ChannelPlatform = 'feishu' | 'slack' | 'wecom';
275
275
  export interface AddChannelInput {
276
276
  /** See {@link ChannelPlatform}. Typed loosely so a newly supported platform needs no SDK release. */
277
277
  platform: ChannelPlatform | (string & {});
278
- /** Server default: `'default'`. */
278
+ /**
279
+ * Names this binding. It is part of the record's identity, not a setting: `updateChannel`
280
+ * and `removeChannel` find a binding by `platform` + `account`, and nothing renames one
281
+ * afterwards — you would remove it and bind again.
282
+ *
283
+ * Four things to know before you pick a value, all staging-verified 2026-08-25:
284
+ *
285
+ * - **The name is unique per USER, across every agent** — one active row per
286
+ * (owner, platform, account). Binding `feishu`/`default` on one agent takes that name away
287
+ * from all your other agents.
288
+ * - **`'default'` is very likely taken already** if the same login ever bound this platform in
289
+ * the app. That binding was not made through this API, so the server refuses to adopt it and
290
+ * answers `409 channel.conflict`.
291
+ * - **Format is `^[a-z0-9][a-z0-9_-]{0,63}$`**, plus three reserved words (`__proto__`,
292
+ * `prototype`, `constructor`). Anything else is `400`. Nothing is normalized for you, so a
293
+ * display name with capitals, spaces or non-ASCII characters is rejected rather than fixed.
294
+ * - **This SDK cannot pre-check the name for you.** {@link ZooworkClient.listChannels} is scoped
295
+ * to one agent while the constraint spans your whole account, so a name another agent holds is
296
+ * invisible here. Track your own names.
297
+ *
298
+ * Use whatever naming scheme your system already has. With no scheme of your own, the agent id
299
+ * works as-is: it matches the pattern and is unique per agent by construction.
300
+ */
279
301
  account?: string;
280
302
  display_name?: string;
281
303
  /** Server default: `'open'`. `'pairing'` is rejected with `400 channel.pairing_unsupported`. */
@@ -296,7 +318,7 @@ export interface AddChannelInput {
296
318
  config?: Record<string, unknown>;
297
319
  }
298
320
  export interface UpdateChannelInput {
299
- /** Which platform account to touch. Server default: `'default'`. */
321
+ /** Which binding to touch — see {@link AddChannelInput.account}. Server default: `'default'`. */
300
322
  account?: string;
301
323
  dm_policy?: string;
302
324
  group_policy?: string;
@@ -305,7 +327,29 @@ export interface UpdateChannelInput {
305
327
  export interface FeishuSetupInput {
306
328
  /** `'feishu'` (default) or `'lark'` — the international brand of the same platform. */
307
329
  brand?: 'feishu' | 'lark';
308
- /** Server default: `'default'`. */
330
+ /**
331
+ * Names this binding. It is part of the record's identity, not a setting: `updateChannel`
332
+ * and `removeChannel` find a binding by `platform` + `account`, and nothing renames one
333
+ * afterwards — you would remove it and bind again.
334
+ *
335
+ * Four things to know before you pick a value, all staging-verified 2026-08-25:
336
+ *
337
+ * - **The name is unique per USER, across every agent** — one active row per
338
+ * (owner, platform, account). Binding `feishu`/`default` on one agent takes that name away
339
+ * from all your other agents.
340
+ * - **`'default'` is very likely taken already** if the same login ever bound this platform in
341
+ * the app. That binding was not made through this API, so the server refuses to adopt it and
342
+ * answers `409 channel.conflict`.
343
+ * - **Format is `^[a-z0-9][a-z0-9_-]{0,63}$`**, plus three reserved words (`__proto__`,
344
+ * `prototype`, `constructor`). Anything else is `400`. Nothing is normalized for you, so a
345
+ * display name with capitals, spaces or non-ASCII characters is rejected rather than fixed.
346
+ * - **This SDK cannot pre-check the name for you.** {@link ZooworkClient.listChannels} is scoped
347
+ * to one agent while the constraint spans your whole account, so a name another agent holds is
348
+ * invisible here. Track your own names.
349
+ *
350
+ * Use whatever naming scheme your system already has. With no scheme of your own, the agent id
351
+ * works as-is: it matches the pattern and is unique per agent by construction.
352
+ */
309
353
  account?: string;
310
354
  /** Server default: `'open'`. */
311
355
  dm_policy?: string;
@@ -1012,8 +1056,12 @@ export interface ZooworkClient {
1012
1056
  * path for Slack and WeCom; Feishu also has the QR flow. See {@link ChannelPlatform} for what
1013
1057
  * binds and what does not.
1014
1058
  *
1015
- * **It is an upsert, not a create.** Binding the same `platform` + `account` twice answers
1016
- * `201` again and overwrites the first binding rather than conflicting.
1059
+ * **It is idempotent, not an upsert.** Re-posting an identical body for the same
1060
+ * `platform` + `account` answers `201` again and replays the binding you already have — it
1061
+ * neither creates a second channel nor overwrites the first. That same pair with a DIFFERENT
1062
+ * `config` answers `409 channel.conflict`, so rotating credentials means
1063
+ * {@link removeChannel} and then a fresh `addChannel`; a plain re-add fails
1064
+ * (staging-verified 2026-08-25).
1017
1065
  *
1018
1066
  * ⚠️ **201 means STORED, not WORKING.** Credentials are not validated at bind time: a channel
1019
1067
  * created from deliberately bogus credentials still answered 201 with `health: 'unknown'`,
@@ -1049,6 +1097,12 @@ export interface ZooworkClient {
1049
1097
  * Observed defaults: `expires_in: 600`, `poll_interval: 5`. `brand` picks the real host —
1050
1098
  * `'feishu'` answers an `open.feishu.cn` URI, `'lark'` an `open.larksuite.com` one, so the
1051
1099
  * brand has to match the workspace the person will approve it in.
1100
+ *
1101
+ * Pick `account` before you show the QR. Approving the scan registers a NEW app in that
1102
+ * Feishu workspace, and only then does the binding get written — so a name clash surfaces as
1103
+ * `409 channel.conflict` AFTER someone has scanned, with the freshly registered app left
1104
+ * behind in their workspace. Retrying under the same name repeats both. See
1105
+ * {@link AddChannelInput.account} for how names are scoped.
1052
1106
  */
1053
1107
  startFeishuSetup(agentId: string, input?: FeishuSetupInput): Promise<FeishuSetupSession>;
1054
1108
  /**
package/dist/client.js CHANGED
@@ -27,6 +27,18 @@ function readEnv(name) {
27
27
  const value = proc?.env?.[name];
28
28
  return value === undefined || value === '' ? undefined : value;
29
29
  }
30
+ /**
31
+ * Trailing `/` removed by scanning, not by `/\/+$/`. That regex retries at every start
32
+ * position on a long run of slashes, which is quadratic and which CodeQL flags. Nothing
33
+ * hostile can reach it — the input is the caller's own base URL — but one pass is simpler
34
+ * than arguing with the scanner about it.
35
+ */
36
+ function stripTrailingSlashes(url) {
37
+ let end = url.length;
38
+ while (end > 0 && url.charCodeAt(end - 1) === 47 /* '/' */)
39
+ end--;
40
+ return end === url.length ? url : url.slice(0, end);
41
+ }
30
42
  export class ZooworkError extends Error {
31
43
  status;
32
44
  /**
@@ -64,7 +76,7 @@ export class ZooworkError extends Error {
64
76
  */
65
77
  export function createZooworkClient(cfg = {}) {
66
78
  const doFetch = cfg.fetch ?? ((input, init) => fetch(input, init));
67
- const base = (cfg.baseUrl ?? readEnv('ZOOWORK_BASE_URL') ?? DEFAULT_BASE_URL).replace(/\/+$/, '');
79
+ const base = stripTrailingSlashes(cfg.baseUrl ?? readEnv('ZOOWORK_BASE_URL') ?? DEFAULT_BASE_URL);
68
80
  const auth = cfg.auth ??
69
81
  (cfg.apiKey !== undefined
70
82
  ? { apiKey: cfg.apiKey }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoowork-ai/sdk",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "TypeScript SDK for the ZooWork Managed Agents API (Developer Preview)",
5
5
  "keywords": [
6
6
  "zoowork",