@qoniai/qoni 0.7.0 → 0.8.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/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 - Unreleased
4
+
5
+ ### Added
6
+
7
+ - `qoni.genauth.delegateAgent(input)` returns the grant directly (`grant.token`),
8
+ or an interactive authorization request, through the existing signed endpoint.
9
+ - `agentKey` is an alias for `agent`. Existing root and `qoni.qoni` entries keep
10
+ their `QoniResponse` envelopes. Conflicting Agent labels are rejected;
11
+ the existing top-level `userId` precedence is unchanged.
12
+
13
+ ### Fixed
14
+
15
+ - Expiry strings such as `"15m"` are converted to integer seconds; valid
16
+ durations are 60–86400 seconds, matching the GenAuth DTO.
17
+ - Unsupported `deniedScopes` is rejected rather than forwarding an unenforced
18
+ deny policy. Legacy `mailbox.*`, `web.search`, and `web.extract` scopes are rejected without mapping to product scopes.
19
+
20
+ ### Changed
21
+
22
+ - Expiry validation now happens locally before signing. Invalid formats and
23
+ values outside 60–86400 seconds throw `QoniValidationError`; numeric seconds
24
+ strings are sent as integers.
25
+
3
26
  ## 0.7.0 - 2026-10-02
4
27
 
5
28
  ### Changed
@@ -136,8 +159,8 @@ compatible fixes.
136
159
 
137
160
  ### Changed
138
161
 
139
- - The default Qoni host is now `https://dashboard.qoni.ai` (was
140
- `https://anima.eazo.ai/dashboard`). Callers that never passed an explicit `host`
162
+ - The default Qoni host is now `https://dashboard.qoni.ai`.
163
+ Callers that never passed an explicit `host`
141
164
  reach a different control plane after upgrading. Note that the new default has
142
165
  no `/dashboard` path segment — pass `host` explicitly for private or local
143
166
  deployments.
package/README.md CHANGED
@@ -251,7 +251,7 @@ Silent product calls receive `token: delegation.data.token`. If that token is an
251
251
 
252
252
  - **Silent** (`mode: "silent"`, the default) — acts for one end user, so a user
253
253
  is **required**: pass `user: { id: <genauthUserId> }` (preferred) or the
254
- deprecated top-level `userId`. Omitting it throws `QoniValidationError`
254
+ top-level `userId`. Omitting it throws `QoniValidationError`
255
255
  locally before any request. Authorize with the `products` sugar and/or
256
256
  explicit `scopes`.
257
257
  - **Interactive** (`mode: "interactive"`) — pass `redirectUri`, `state`,
@@ -306,7 +306,35 @@ await qoni.gumem.recall({
306
306
  });
307
307
  ```
308
308
 
309
- `completeDelegateToken` is the recommended server callback helper. `delegateAgent` and `completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
309
+ `completeDelegateToken` is the recommended server callback helper. Root `qoni.delegateAgent` and `qoni.completeDelegateAgent` remain as deprecated compatibility names, but the current callback contract is `{ grantId, code, state }`; old `{ code, state }` completion is not supported.
310
+
311
+ ## Original delegation style (0.8.0, unreleased)
312
+
313
+ ```ts
314
+ const grant = await qoni.genauth.delegateAgent({
315
+ userId: process.env.QONI_USER_ID!, // The current user's GenAuth ID
316
+ agentKey: "email-assistant", // Agent audit label; alias for agent
317
+ scopes: [
318
+ "webagent.do_anything:read", // Read status, events, and artifacts
319
+ "webagent.do_anything:manage", // Start/cancel tasks and respond to interactions
320
+ ],
321
+ expiresIn: "15m", // Converted to 900 seconds before signing
322
+ });
323
+ console.log(grant.grantId, grant.auditId);
324
+ ```
325
+
326
+ This entry returns the grant directly; root `delegateToken` still returns
327
+ `QoniResponse`, with the token in `response.data.token`. Interactive mode returns
328
+ the authorization request directly; use `completeDelegateToken()` in the server
329
+ callback after consent. `agentKey` aliases `agent`; both labels must agree when
330
+ supplied together. Top-level `userId` retains precedence over `user.id`.
331
+ Expiry accepts integer seconds or whole-number
332
+ `s/m/h/d` durations, between 60 and 86400 seconds. Until published, use the 0.8.0
333
+ source build for these additions.
334
+
335
+ Legacy `mailbox.*`, `web.search`, and `web.extract` scopes, and `deniedScopes`, are unsupported and rejected.
336
+ They are never expanded into broader product permissions. Product scopes do not
337
+ prohibit email sending or deletion; a prompt is not an enforced deny policy.
310
338
 
311
339
  ## Choosing Scopes
312
340
 
package/README.zh-CN.md CHANGED
@@ -227,7 +227,7 @@ silent 产品能力方法传入 `token: delegation.data.token`。如果这个 to
227
227
 
228
228
  **入参形态。** `delegateToken` 用 `mode` 判别:
229
229
 
230
- - **Silent**(`mode: "silent"`,默认)——代表某个终端用户,所以 user **必填**:传 `user: { id: <genauthUserId> }`(推荐)或已废弃的顶层 `userId`。不传会在请求发出前本地抛 `QoniValidationError`。授权用 `products` 语法糖和/或显式 `scopes`。
230
+ - **Silent**(`mode: "silent"`,默认)——代表某个终端用户,所以 user **必填**:传 `user: { id: <genauthUserId> }`(推荐)或顶层 `userId`。不传会在请求发出前本地抛 `QoniValidationError`。授权用 `products` 语法糖和/或显式 `scopes`。
231
231
  - **Interactive**(`mode: "interactive"`)——传 `redirectUri`、`state`、`agent`、`scopes`,**不传** user(Qoni Console 在授权时解析登录)。返回 `data.authorizationUrl` 而非 token;在服务端用 `completeDelegateToken({ grantId, code, state })` 完成。
232
232
 
233
233
  `agent` 可选,默认 `"sdk"`。
@@ -277,7 +277,32 @@ await qoni.gumem.recall({
277
277
  });
278
278
  ```
279
279
 
280
- `completeDelegateToken` 是推荐的服务端 callback helper。`delegateAgent` 和 `completeDelegateAgent` 仍保留为 deprecated 兼容命名,但当前 callback 契约是 `{ grantId, code, state }`;旧的 `{ code, state }` complete 签名不再兼容。
280
+ `completeDelegateToken` 是推荐的服务端 callback helper。顶层 `qoni.delegateAgent` 和 `qoni.completeDelegateAgent` 仍保留为 deprecated 兼容命名,但当前 callback 契约是 `{ grantId, code, state }`;旧的 `{ code, state }` complete 签名不再兼容。
281
+
282
+ ## 原始委托写法(0.8.0,尚未发布)
283
+
284
+ ```ts
285
+ const grant = await qoni.genauth.delegateAgent({
286
+ userId: process.env.QONI_USER_ID!, // 当前用户的 GenAuth ID
287
+ agentKey: "email-assistant", // Agent 审计标签,兼容 agent 参数
288
+ scopes: [
289
+ "webagent.do_anything:read", // 读取任务状态、事件和产物
290
+ "webagent.do_anything:manage", // 启动、取消任务和提交交互响应
291
+ ],
292
+ expiresIn: "15m", // 签名前转换为 900 秒
293
+ });
294
+ console.log(grant.grantId, grant.auditId);
295
+ ```
296
+
297
+ 此入口直接返回 grant;顶层 `delegateToken` 仍返回 `QoniResponse`,token 在
298
+ `response.data.token` 中。交互式模式直接返回授权请求;用户同意后,在服务端回调
299
+ 用 `completeDelegateToken()` 兑换 token。`agentKey`、`userId` 分别兼容 `agent`、
300
+ `user.id`;同时提供两个 Agent 标签时必须一致,顶层 `userId` 保持原有的优先级。
301
+ 有效期支持整数秒或整数 `s/m/h/d` 时长,
302
+ 范围为 60–86400 秒。这些新增能力在发布前需要使用 0.8.0 源码构建。
303
+
304
+ 旧版 `mailbox.*`、`web.search`、`web.extract` scope 和 `deniedScopes` 不受支持,SDK 会拒绝,不会将它们
305
+ 扩展成更宽的产品权限。产品 scope 不限制发送、删除邮件;任务描述不能代替禁止策略。
281
306
 
282
307
  ## Scope 怎么选
283
308
 
package/dist/index.cjs CHANGED
@@ -1612,7 +1612,14 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1612
1612
 
1613
1613
  // src/genauth.ts
1614
1614
  function createGenAuthNamespace(transport, options = {}) {
1615
+ async function delegateAgent(input) {
1616
+ if (!options.delegateTokenFor) {
1617
+ throw new QoniValidationError("GenAuth delegateAgent requires a Qoni delegation provider");
1618
+ }
1619
+ return (await options.delegateTokenFor(input)).data;
1620
+ }
1615
1621
  return {
1622
+ delegateAgent,
1616
1623
  userInfo: (input) => transport.genauthJson("GET", "/oidc/me", input.accessToken),
1617
1624
  jwks: () => transport.genauthJson("GET", "/oidc/.well-known/jwks.json"),
1618
1625
  discovery: () => transport.genauthJson("GET", "/oidc/.well-known/openid-configuration"),
@@ -2248,7 +2255,8 @@ var Qoni = class {
2248
2255
  webAgentSSE: (...args) => this.webAgentSSE(...args)
2249
2256
  };
2250
2257
  this.genauth = createGenAuthNamespace(transport, {
2251
- adminTokenFor: () => this.genauthAdminTokenFor()
2258
+ adminTokenFor: () => this.genauthAdminTokenFor(),
2259
+ delegateTokenFor: (input) => this.delegateToken(input)
2252
2260
  });
2253
2261
  this.qoni = createQoniNamespace(
2254
2262
  transport,
@@ -2961,19 +2969,43 @@ function toRecord(value) {
2961
2969
  }
2962
2970
  function normalizeDelegateTokenInput(input) {
2963
2971
  const body = toRecord(input);
2972
+ if (input.deniedScopes !== void 0) {
2973
+ throw new QoniValidationError(
2974
+ "delegateToken: deniedScopes is not supported by the delegation service. Do not rely on this field to prevent mailbox or browser actions."
2975
+ );
2976
+ }
2977
+ if (input.agent !== void 0 && input.agentKey !== void 0 && input.agent !== input.agentKey) {
2978
+ throw new QoniValidationError("delegateToken: agent and agentKey must identify the same Agent");
2979
+ }
2964
2980
  if (!body.userId && isJsonObject(input.user) && typeof input.user.id === "string") {
2965
2981
  body.userId = input.user.id;
2966
2982
  }
2967
2983
  if (input.mode !== "interactive" && !body.userId) {
2968
2984
  throw new QoniValidationError(
2969
- 'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the deprecated top-level `userId`). Interactive mode (`mode: "interactive"`) does not need one.'
2985
+ 'delegateToken: silent mode requires a user \u2014 pass `user: { id: <genauthUserId> }` (or the top-level `userId`). Interactive mode (`mode: "interactive"`) does not need one.'
2970
2986
  );
2971
2987
  }
2972
- body.agent = input.agent ?? "sdk";
2988
+ body.agent = input.agent ?? input.agentKey ?? "sdk";
2989
+ delete body.agentKey;
2990
+ if (input.expiresIn !== void 0) body.expiresIn = delegationDurationSeconds(input.expiresIn);
2973
2991
  body.scopes = resolveDelegationScopes(input.scopes, input.products);
2974
2992
  delete body.products;
2975
2993
  return body;
2976
2994
  }
2995
+ function delegationDurationSeconds(value) {
2996
+ let seconds = value;
2997
+ if (typeof value === "string") {
2998
+ const duration = /^(\d+)(s|m|h|d)?$/.exec(value);
2999
+ const units = { s: 1, m: 60, h: 3600, d: 86400 };
3000
+ seconds = duration ? Number(duration[1]) * units[duration[2] ?? "s"] : NaN;
3001
+ }
3002
+ if (typeof seconds !== "number" || !Number.isInteger(seconds) || seconds < 60 || seconds > 86400) {
3003
+ throw new QoniValidationError(
3004
+ 'delegateToken: expiresIn must be 60\u201386400 integer seconds or a whole-number duration such as "15m"'
3005
+ );
3006
+ }
3007
+ return seconds;
3008
+ }
2977
3009
  function resolveDelegationScopes(scopes, products) {
2978
3010
  const resolved = /* @__PURE__ */ new Set();
2979
3011
  for (const product of products ?? []) {
@@ -2997,6 +3029,11 @@ function resolveDelegationScopes(scopes, products) {
2997
3029
  return [...resolved];
2998
3030
  }
2999
3031
  function validateScopeFormat(scope) {
3032
+ if (scope.startsWith("mailbox.") || scope === "web.search" || scope === "web.extract") {
3033
+ throw new QoniValidationError(
3034
+ `delegateToken: legacy action scope "${scope}" has no enforced mapping in this delegation service. It will not be expanded into broader product permissions.`
3035
+ );
3036
+ }
3000
3037
  if (KNOWN_SCOPES.includes(scope)) return;
3001
3038
  const prefixed = KNOWN_SCOPES.find((known) => known.endsWith(`.${scope}`) || known === `webagent.${scope}`);
3002
3039
  if (prefixed) {