@qoniai/qoni 0.5.0 → 0.5.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
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.2 - 2026-09-23
4
+
5
+ ### Fixed
6
+
7
+ - Keep permission errors concise instead of appending every known scope.
8
+ - Explain how to enable a permission policy when an AccessKey has no allowed
9
+ scopes. Preserve the backend error code and request ID for diagnosis.
10
+
11
+ ## 0.5.1 - 2026-09-23
12
+
13
+ ### Fixed
14
+
15
+ - Recheck access-key scope policy at token exchange before every SDK product
16
+ operation, including repeated calls with the same delegation token. Removing
17
+ a scope now blocks the next SDK operation instead of leaving a cached product
18
+ token usable until expiry.
19
+
3
20
  ## 0.5.0 - 2026-09-17
4
21
 
5
22
  ### Added
package/README.md CHANGED
@@ -727,7 +727,7 @@ All error classes are exported and can be matched with `instanceof`:
727
727
  | --- | --- | --- |
728
728
  | `QoniValidationError` | Local input check failed before any request (bad scope string, silent delegation missing a user). | Fix the call — the message states the correct form. |
729
729
  | `QoniAuthError` | AK/SK signing rejected by Qoni (bad/rotated credentials). | Check `accessKey` / `secretKey`. |
730
- | `QoniPermissionDeniedError` | The delegation token lacks a required scope (403). | Re-`delegateToken` with the missing scope. |
730
+ | `QoniPermissionDeniedError` | The AccessKey or delegation token lacks a required scope (403). | Check the AccessKey permission policy and request the missing scope with `delegateToken`. |
731
731
  | `QoniTokenExpiredError` | The delegation / product token expired. | Mint a fresh token and retry. |
732
732
  | `QoniDelegationRequiredError` | A product call was made without `token`, or with a raw token the gateway won't accept directly. | Pass `delegation.data.token` to the product call. |
733
733
  | `QoniRateLimitError` | Rate limited (429). | Back off and retry (`retryable === true`). |
@@ -777,6 +777,7 @@ Common first-run errors:
777
777
  | --- | --- | --- |
778
778
  | `agent must be a string` | The online delegation API currently expects `agent` to be a string id. | Pass `agent: "memory-agent"` rather than an object. |
779
779
  | `eak.delegation.user_not_bound` | The `userId` is not in the GenAuth userpool bound to this Qoni credential. | Use a real user id from `currentUser` or from the same bound userpool. |
780
+ | `eak.delegation.no_allowed_scopes` | The AccessKey has no permission scopes enabled. | In Qoni Console, open its Permission scope settings and enable the policy required by this call. |
780
781
  | `eak.genauth.userpool_binding_missing` | The AK/SK is not bound to a GenAuth userpool, so `genauth.users.*` cannot obtain a management token. | Bind the Qoni credential to the target GenAuth userpool before calling user management APIs. |
781
782
  | `eak.genauth.userpool_owner_missing` | The bound GenAuth userpool has no resolvable owner user for management-token signing. | Fix the GenAuth userpool owner data or binding. |
782
783
  | `eak.token_exchange.upstream_failed` with `unauthorized_client` or `grant_type is not enabled` | Delegation succeeded, but the managed delegation app is missing token-exchange grant support or has drifted from the tenant binding. | Repair the managed delegation app binding before calling GUMem or WebAgent product capabilities. |
@@ -788,6 +789,9 @@ Common first-run errors:
788
789
  - Keep Qoni AK/SK on trusted servers only.
789
790
  - Do not expose delegation tokens to untrusted clients.
790
791
  - Request the smallest useful scope set for each Agent action.
792
+ - Product methods exchange the delegation token on every call, so a change to
793
+ the access key's allowed scopes is checked before the next SDK operation.
794
+ Product tokens already obtained outside the SDK remain valid until expiry.
791
795
  - Prefer `interactive` mode for browser control, site login, long-running monitors, and sensitive artifacts.
792
796
  - Store `auditId`, `requestId`, and `traceId` with product logs when later investigation matters.
793
797
  - Rotate AK/SK through Qoni Console if a credential might have been exposed.
package/README.zh-CN.md CHANGED
@@ -657,7 +657,7 @@ type QoniEvent<T = unknown> = {
657
657
  | --- | --- | --- |
658
658
  | `QoniValidationError` | 请求发出前的本地校验失败(scope 字符串格式错、silent 委托缺 user)。 | 按消息修正调用——消息里给出正确写法。 |
659
659
  | `QoniAuthError` | AK/SK 签名被 Qoni 拒绝(凭据错误/已轮换)。 | 检查 `accessKey` / `secretKey`。 |
660
- | `QoniPermissionDeniedError` | delegation token 缺所需 scope(403)。 | 带上缺失 scope 重新 `delegateToken`。 |
660
+ | `QoniPermissionDeniedError` | AccessKey 或 delegation token 缺少所需 scope(403)。 | 检查 AccessKey 权限策略,并在 `delegateToken` 中申请所需 scope。 |
661
661
  | `QoniTokenExpiredError` | delegation / 产品 token 过期。 | 重新 mint token 再试。 |
662
662
  | `QoniDelegationRequiredError` | 产品调用没带 `token`,或带的是网关不接受的裸 token。 | 把 `delegation.data.token` 传给产品调用。 |
663
663
  | `QoniRateLimitError` | 被限流(429)。 | 退避后重试(`retryable === true`)。 |
@@ -706,6 +706,7 @@ try {
706
706
  | --- | --- | --- |
707
707
  | `agent must be a string` | 当前线上 delegation API 期望 `agent` 是字符串 ID。 | 使用 `agent: "memory-agent"`,不要传对象。 |
708
708
  | `eak.delegation.user_not_bound` | `userId` 不属于该 Qoni credential 绑定的 GenAuth userpool。 | 使用 `currentUser` 返回的真实用户 ID,或确认用户来自同一个绑定 userpool。 |
709
+ | `eak.delegation.no_allowed_scopes` | AccessKey 没有启用任何权限 scope。 | 在 Qoni Console 的 Permission scope 设置中启用本次调用所需的策略。 |
709
710
  | `eak.genauth.userpool_binding_missing` | AK/SK 没有绑定 GenAuth userpool,`genauth.users.*` 无法换取 management token。 | 先把 Qoni credential 绑定到目标 GenAuth userpool。 |
710
711
  | `eak.genauth.userpool_owner_missing` | 绑定的 GenAuth userpool 找不到可用于签发 management token 的 owner 用户。 | 修复 GenAuth userpool owner 数据或绑定。 |
711
712
  | `eak.token_exchange.upstream_failed` 且包含 `unauthorized_client` 或 `grant_type is not enabled` | Delegation 已成功,但 managed delegation app 缺少 token-exchange grant,或租户绑定发生漂移。 | 先修复 managed delegation app 绑定,再调用 GUMem 或 WebAgent 产品能力。 |
@@ -717,6 +718,8 @@ try {
717
718
  - AK/SK 只保存在可信服务端。
718
719
  - 不要把 delegation token 暴露给不可信客户端。
719
720
  - 每次 Agent 行动只申请必要 scope。
721
+ - SDK 的产品方法每次调用前都会重新兑换 delegation token,因此 AccessKey 的 scope
722
+ 变更会在下一次 SDK 调用前重新校验。已经在 SDK 外取得的产品 token 仍会持续有效至过期。
720
723
  - 浏览器接管、站点登录、长期监控、敏感产物访问优先使用 `interactive`。
721
724
  - 在业务日志中记录 `auditId`、`requestId`、`traceId`,方便后续审计。
722
725
  - 如果怀疑 AK/SK 泄露,应在 Qoni Console 中轮换密钥。
package/dist/index.cjs CHANGED
@@ -104,81 +104,6 @@ function buildSignedHeaders(credentials, method, pathname, payload) {
104
104
  return headers;
105
105
  }
106
106
 
107
- // src/scopes.ts
108
- var QoniScopes = {
109
- GUMEM_SESSION_CREATE: "gumem.session:create",
110
- GUMEM_MESSAGE_WRITE: "gumem.message:write",
111
- GUMEM_MEMORY_READ: "gumem.memory:read",
112
- GUMEM_MEMORY_WRITE: "gumem.memory:write",
113
- GUMEM_MEMORY_DELETE: "gumem.memory:delete",
114
- GUMEM_SEARCH_RUN: "gumem.search:run",
115
- GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
116
- GUMEM_RESOURCE_WRITE: "gumem.resource:write",
117
- GUMEM_ACTION_WRITE: "gumem.action:write",
118
- GUMEM_ACTION_READ: "gumem.action:read",
119
- GUMEM_PROFILE_READ: "gumem.profile:read",
120
- // WebAgent products: exactly two verbs per product — `read` (observe) and
121
- // `manage` (anything that changes execution state). See
122
- // wa/docs/qoni-sdk-public-surface.md §3.
123
- DO_ANYTHING_READ: "webagent.do_anything:read",
124
- DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
125
- WEB_SEARCH_READ: "webagent.web_search:read",
126
- WEB_SEARCH_MANAGE: "webagent.web_search:manage",
127
- DEEP_RESEARCH_READ: "webagent.deep_research:read",
128
- DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
129
- TRACK_READ: "webagent.track:read",
130
- TRACK_MANAGE: "webagent.track:manage"
131
- };
132
- var QoniProductScopes = {
133
- doAnything: [
134
- QoniScopes.DO_ANYTHING_READ,
135
- QoniScopes.DO_ANYTHING_MANAGE
136
- ],
137
- webSearch: [
138
- QoniScopes.WEB_SEARCH_READ,
139
- QoniScopes.WEB_SEARCH_MANAGE
140
- ],
141
- deepResearch: [
142
- QoniScopes.DEEP_RESEARCH_READ,
143
- QoniScopes.DEEP_RESEARCH_MANAGE
144
- ],
145
- track: [
146
- QoniScopes.TRACK_READ,
147
- QoniScopes.TRACK_MANAGE
148
- ]
149
- };
150
- var KNOWN_SCOPES = Object.values(QoniScopes);
151
- var QoniScopeBundles = {
152
- GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
153
- GUMEM_SESSION_RECALL: [
154
- QoniScopes.GUMEM_MEMORY_READ,
155
- QoniScopes.GUMEM_MEMORY_WRITE,
156
- QoniScopes.GUMEM_MESSAGE_WRITE
157
- ],
158
- GUMEM_WRITE: [
159
- QoniScopes.GUMEM_SESSION_CREATE,
160
- QoniScopes.GUMEM_MESSAGE_WRITE,
161
- QoniScopes.GUMEM_ACTION_WRITE
162
- ],
163
- // One bundle per WebAgent product = [read, manage].
164
- DO_ANYTHING: [
165
- QoniScopes.DO_ANYTHING_READ,
166
- QoniScopes.DO_ANYTHING_MANAGE
167
- ],
168
- WEB_SEARCH: [
169
- QoniScopes.WEB_SEARCH_READ,
170
- QoniScopes.WEB_SEARCH_MANAGE
171
- ],
172
- DEEP_RESEARCH: [
173
- QoniScopes.DEEP_RESEARCH_READ,
174
- QoniScopes.DEEP_RESEARCH_MANAGE
175
- ],
176
- TRACK: [
177
- QoniScopes.TRACK_READ,
178
- QoniScopes.TRACK_MANAGE
179
- ]
180
- };
181
-
182
107
  // src/types.ts
183
108
  function isJsonObject(value) {
184
109
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
@@ -262,7 +187,7 @@ function errorFromPayload(status, payload, fallbackMessage) {
262
187
  const source = detailObject || (isJsonObject(payload) ? payload : {});
263
188
  const code = stringField(source, "code") || stringField(source, "apiCode") || statusCodeToCode(status);
264
189
  const rawMessage = stringField(source, "message") || stringField(source, "detail") || fallbackMessage;
265
- const message = appendOnboardingHint(
190
+ const message = code === "eak.delegation.no_allowed_scopes" ? "This AccessKey has no permission scopes. In Qoni Console, open its Permission scope settings, enable the policy needed for this call, then retry." : appendOnboardingHint(
266
191
  appendDeniedScopes(appendUpstreamDescription(rawMessage, source), source),
267
192
  code
268
193
  );
@@ -279,7 +204,7 @@ function errorFromPayload(status, payload, fallbackMessage) {
279
204
  if (isDelegationRequired(code)) return new QoniDelegationRequiredError(message, options);
280
205
  if (status === 401) return new QoniAuthError(message, options);
281
206
  if (status === 403 || isPermissionDenied(code)) {
282
- return new QoniPermissionDeniedError(appendKnownScopes(message), options);
207
+ return new QoniPermissionDeniedError(message, options);
283
208
  }
284
209
  if (status === 400 || status === 422) return new QoniValidationError(message, options);
285
210
  if (status === 429) return new QoniRateLimitError(message, options);
@@ -314,17 +239,14 @@ function appendUpstreamDescription(message, source) {
314
239
  }
315
240
  var ONBOARDING_HINTS = {
316
241
  "eak.delegation.user_not_bound": "Hint: the userId is not in the GenAuth userpool bound to this credential. Resolve a real user with qoni.resolveAnyBoundUser() (demo/smoke) or qoni.currentUser({ accessToken }).",
317
- "eak.genauth.userpool_binding_missing": "Hint: bind the Qoni credential to a GenAuth userpool before calling genauth.users.*."
242
+ "eak.genauth.userpool_binding_missing": "Hint: bind the Qoni credential to a GenAuth userpool before calling genauth.users.*.",
243
+ "eak.delegation.scope_not_allowed": "Hint: enable the denied scopes in this AccessKey's Permission scope settings in Qoni Console."
318
244
  };
319
245
  function appendOnboardingHint(message, code) {
320
246
  const hint = ONBOARDING_HINTS[code];
321
247
  if (!hint || message.includes(hint)) return message;
322
248
  return `${message} ${hint}`;
323
249
  }
324
- function appendKnownScopes(message) {
325
- if (message.includes("Known scopes:")) return message;
326
- return `${message} (Known scopes: ${KNOWN_SCOPES.join(", ")})`;
327
- }
328
250
  function statusCodeToCode(status) {
329
251
  if (status === 401) return "auth.failed";
330
252
  if (status === 403) return "permission_denied";
@@ -1488,6 +1410,81 @@ function createQoniNamespace(_transport, delegateToken, completeDelegateToken) {
1488
1410
  };
1489
1411
  }
1490
1412
 
1413
+ // src/scopes.ts
1414
+ var QoniScopes = {
1415
+ GUMEM_SESSION_CREATE: "gumem.session:create",
1416
+ GUMEM_MESSAGE_WRITE: "gumem.message:write",
1417
+ GUMEM_MEMORY_READ: "gumem.memory:read",
1418
+ GUMEM_MEMORY_WRITE: "gumem.memory:write",
1419
+ GUMEM_MEMORY_DELETE: "gumem.memory:delete",
1420
+ GUMEM_SEARCH_RUN: "gumem.search:run",
1421
+ GUMEM_ADMIN_MANAGE: "gumem.admin:manage",
1422
+ GUMEM_RESOURCE_WRITE: "gumem.resource:write",
1423
+ GUMEM_ACTION_WRITE: "gumem.action:write",
1424
+ GUMEM_ACTION_READ: "gumem.action:read",
1425
+ GUMEM_PROFILE_READ: "gumem.profile:read",
1426
+ // WebAgent products: exactly two verbs per product — `read` (observe) and
1427
+ // `manage` (anything that changes execution state). See
1428
+ // wa/docs/qoni-sdk-public-surface.md §3.
1429
+ DO_ANYTHING_READ: "webagent.do_anything:read",
1430
+ DO_ANYTHING_MANAGE: "webagent.do_anything:manage",
1431
+ WEB_SEARCH_READ: "webagent.web_search:read",
1432
+ WEB_SEARCH_MANAGE: "webagent.web_search:manage",
1433
+ DEEP_RESEARCH_READ: "webagent.deep_research:read",
1434
+ DEEP_RESEARCH_MANAGE: "webagent.deep_research:manage",
1435
+ TRACK_READ: "webagent.track:read",
1436
+ TRACK_MANAGE: "webagent.track:manage"
1437
+ };
1438
+ var QoniProductScopes = {
1439
+ doAnything: [
1440
+ QoniScopes.DO_ANYTHING_READ,
1441
+ QoniScopes.DO_ANYTHING_MANAGE
1442
+ ],
1443
+ webSearch: [
1444
+ QoniScopes.WEB_SEARCH_READ,
1445
+ QoniScopes.WEB_SEARCH_MANAGE
1446
+ ],
1447
+ deepResearch: [
1448
+ QoniScopes.DEEP_RESEARCH_READ,
1449
+ QoniScopes.DEEP_RESEARCH_MANAGE
1450
+ ],
1451
+ track: [
1452
+ QoniScopes.TRACK_READ,
1453
+ QoniScopes.TRACK_MANAGE
1454
+ ]
1455
+ };
1456
+ var KNOWN_SCOPES = Object.values(QoniScopes);
1457
+ var QoniScopeBundles = {
1458
+ GUMEM_READONLY: [QoniScopes.GUMEM_MEMORY_READ, QoniScopes.GUMEM_PROFILE_READ],
1459
+ GUMEM_SESSION_RECALL: [
1460
+ QoniScopes.GUMEM_MEMORY_READ,
1461
+ QoniScopes.GUMEM_MEMORY_WRITE,
1462
+ QoniScopes.GUMEM_MESSAGE_WRITE
1463
+ ],
1464
+ GUMEM_WRITE: [
1465
+ QoniScopes.GUMEM_SESSION_CREATE,
1466
+ QoniScopes.GUMEM_MESSAGE_WRITE,
1467
+ QoniScopes.GUMEM_ACTION_WRITE
1468
+ ],
1469
+ // One bundle per WebAgent product = [read, manage].
1470
+ DO_ANYTHING: [
1471
+ QoniScopes.DO_ANYTHING_READ,
1472
+ QoniScopes.DO_ANYTHING_MANAGE
1473
+ ],
1474
+ WEB_SEARCH: [
1475
+ QoniScopes.WEB_SEARCH_READ,
1476
+ QoniScopes.WEB_SEARCH_MANAGE
1477
+ ],
1478
+ DEEP_RESEARCH: [
1479
+ QoniScopes.DEEP_RESEARCH_READ,
1480
+ QoniScopes.DEEP_RESEARCH_MANAGE
1481
+ ],
1482
+ TRACK: [
1483
+ QoniScopes.TRACK_READ,
1484
+ QoniScopes.TRACK_MANAGE
1485
+ ]
1486
+ };
1487
+
1491
1488
  // src/genauth.ts
1492
1489
  function createGenAuthNamespace(transport, options = {}) {
1493
1490
  return {
@@ -2114,7 +2111,6 @@ var Qoni = class {
2114
2111
  secretKey;
2115
2112
  host;
2116
2113
  runtimeConfigPromise;
2117
- productTokenCache = /* @__PURE__ */ new Map();
2118
2114
  genAuthAdminTokenCache;
2119
2115
  gumem;
2120
2116
  genauth;
@@ -2585,9 +2581,6 @@ var Qoni = class {
2585
2581
  const runtimeToken = requiredRuntimeToken(token);
2586
2582
  if (!isQoniDelegationToken(runtimeToken)) return runtimeToken;
2587
2583
  const scopes = [...requiredScopes || []].sort();
2588
- const cacheKey = [service, runtimeToken, scopes.join(" ")].join(":");
2589
- const cached = this.productTokenCache.get(cacheKey);
2590
- if (cached && cached.expiresAt > Date.now() + 3e4) return cached.token;
2591
2584
  const response = await this.signedPost("/api/v3/eak/token-exchange", {
2592
2585
  subjectToken: runtimeToken,
2593
2586
  resource: service === "gumem" ? "gumem" : "webagent",
@@ -2602,10 +2595,6 @@ var Qoni = class {
2602
2595
  "Qoni product token exchange response did not include an access token"
2603
2596
  );
2604
2597
  }
2605
- this.productTokenCache.set(cacheKey, {
2606
- token: accessToken,
2607
- expiresAt: Date.now() + Math.max(0, readExpiresIn(payload) ?? 3600) * 1e3
2608
- });
2609
2598
  return accessToken;
2610
2599
  }
2611
2600
  async genauthAdminTokenFor() {