@qoniai/qoni 0.8.0 → 0.9.1

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.
@@ -0,0 +1,285 @@
1
+ ---
2
+ title: GenAuth 管理 SDK
3
+ description: 初始化 SDK,管理用户、用户组、组织、角色、权限、应用和自定义字段。
4
+ status: draft
5
+ sdkCandidate: contracts/genauth-management.json
6
+ lastReviewed: 2026-10-12
7
+ pageClass: qoni-sdk-management
8
+ ---
9
+
10
+ ## 初始化
11
+
12
+ ```ts
13
+ import { Qoni } from '@qoniai/qoni'
14
+
15
+ const qoni = new Qoni({
16
+ accessKey: process.env.QONI_ACCESS_KEY!,
17
+ secretKey: process.env.QONI_SECRET_KEY!,
18
+ })
19
+ ```
20
+
21
+ 使用绑定目标 GenAuth 用户池的 Qoni 访问密钥,在服务端调用。
22
+
23
+ | 参数 | 类型 | 必填 | 说明 |
24
+ | --- | --- | --- | --- |
25
+ | `accessKey` | `string` | 是 | Qoni 访问密钥 |
26
+ | `secretKey` | `string` | 是 | Qoni 访问密钥 Secret |
27
+ | `host` | `string` | 否 | 私有部署的 Qoni 服务地址 |
28
+ | `genauthHost` | `string` | 否 | 指定 GenAuth 服务地址 |
29
+ | `timeoutMs` | `number` | 否 | 请求超时,单位毫秒,默认 `30000` |
30
+ | `fetch` | `typeof fetch` | 否 | 自定义 HTTP transport |
31
+
32
+ ## 用户管理
33
+
34
+ ```ts
35
+ const { data: user } = await qoni.genauth.users.create({
36
+ username: 'sdk-example-user',
37
+ name: 'Example user',
38
+ })
39
+
40
+ const { data: userDetails } = await qoni.genauth.users.get({ userId: user.userId })
41
+ await qoni.genauth.users.update({ userId: user.userId, nickname: 'Alice' })
42
+ const { data: userBatch } = await qoni.genauth.users.getBatch({ userIds: [user.userId] })
43
+ ```
44
+
45
+ | 方法 | 功能 | 主要参数 | `data` |
46
+ | --- | --- | --- | --- |
47
+ | `users.create` | 创建用户 | username / email / phone;name、password 等可选 | 用户对象 |
48
+ | `users.get` | 读取用户 | userId | 用户对象 |
49
+ | `users.getBatch` | 批量读取用户 | userIds: string[] | 用户数组 |
50
+ | `users.update` | 更新用户 | userId 与需要更新的字段 | 更新后的用户 |
51
+ | `users.createBatch` | 批量创建用户 | list: 用户参数数组 | 用户数组 |
52
+ | `users.deleteBatch` | 批量删除用户 | userIds: string[] | 删除结果 |
53
+
54
+ 创建时至少设置用户名、邮箱或手机号。删除在职用户前,先按业务流程通过 `users.resign({ userId })` 设置离职状态。
55
+
56
+ ## 用户查询与分页
57
+
58
+ ```ts
59
+ const { data: userPage } = await qoni.genauth.users.list({
60
+ page: 1,
61
+ limit: 20,
62
+ keywords: 'alice',
63
+ options: { fuzzySearchOn: ['username', 'email'] },
64
+ })
65
+ console.log(userPage.list, userPage.totalCount)
66
+
67
+ for await (const item of qoni.genauth.users.iterate({ limit: 50 })) {
68
+ console.log(item.userId)
69
+ }
70
+ ```
71
+
72
+ | 方法 | 功能 | 主要参数 | `data` |
73
+ | --- | --- | --- | --- |
74
+ | `users.list` | 分页查询用户 | page、limit、keywords、options | { list, totalCount } |
75
+ | `users.iterate` | 逐个遍历用户 | page 默认 1,limit 默认 50、范围 1–50 | `AsyncIterable<User>` |
76
+
77
+ `page` 从 1 开始。`keywords` 用于关键字筛选,`options.fuzzySearchOn` 指定匹配字段。无需筛选时省略这两个参数。`iterate` 中使用 `break` 停止遍历。
78
+
79
+ ## 用户组
80
+
81
+ ```ts
82
+ const { data: group } = await qoni.genauth.groups.create({
83
+ code: 'engineering',
84
+ name: 'Engineering',
85
+ })
86
+ await qoni.genauth.groups.addMembers({ code: group.code, userIds: [user.userId] })
87
+ const { data: groupPage } = await qoni.genauth.groups.list({ page: 1, limit: 20 })
88
+ ```
89
+
90
+ | 方法 | 功能 | 主要参数 | `data` |
91
+ | --- | --- | --- | --- |
92
+ | `groups.create` | 创建用户组 | code、name | 用户组对象 |
93
+ | `groups.get` | 读取用户组 | code | 用户组对象 |
94
+ | `groups.list` | 查询用户组 | page、limit | { list, totalCount } |
95
+ | `groups.update` | 更新用户组 | code 与更新字段 | 用户组对象 |
96
+ | `groups.addMembers` | 添加成员 | code、userIds | 操作结果 |
97
+
98
+ ## 组织与部门
99
+
100
+ ```ts
101
+ await qoni.genauth.organizations.create({
102
+ organizationCode: 'company',
103
+ organizationName: 'Example company',
104
+ })
105
+ const { data: department } = await qoni.genauth.departments.create({
106
+ organizationCode: 'company',
107
+ parentDepartmentId: 'root',
108
+ name: 'Engineering',
109
+ code: 'engineering',
110
+ })
111
+ await qoni.genauth.departments.addMembers({
112
+ organizationCode: 'company',
113
+ departmentId: department.departmentId,
114
+ userIds: [user.userId],
115
+ })
116
+ ```
117
+
118
+ | 方法 | 功能 | 主要参数 | `data` |
119
+ | --- | --- | --- | --- |
120
+ | `organizations.create` | 创建组织 | organizationCode、organizationName | 组织对象 |
121
+ | `organizations.get` | 读取组织 | organizationCode | 组织对象 |
122
+ | `organizations.list` | 查询组织 | page、limit | 组织列表 |
123
+ | `organizations.update` | 更新组织 | organizationCode 与更新字段 | 组织对象 |
124
+ | `departments.create` | 创建部门 | organizationCode、parentDepartmentId、name | 部门对象 |
125
+ | `departments.get` | 读取部门 | organizationCode、departmentId 或 departmentCode | 部门对象 |
126
+ | `departments.update` | 更新部门 | organizationCode、departmentId 与更新字段 | 部门对象 |
127
+ | `departments.addMembers` | 添加部门成员 | organizationCode、departmentId、userIds | 操作结果 |
128
+
129
+ 根部门的 `parentDepartmentId` 使用 `root`;其他部门使用实际父部门 ID。
130
+
131
+ ## 角色、资源与授权
132
+
133
+ ```ts
134
+ await qoni.genauth.namespaces.create({ code: 'project', name: 'Project' })
135
+ await qoni.genauth.roles.create({ namespace: 'project', code: 'reader', name: 'Reader' })
136
+ await qoni.genauth.roles.assign({
137
+ namespace: 'project',
138
+ code: 'reader',
139
+ targets: [{ targetType: 'USER', targetIdentifier: user.userId }],
140
+ })
141
+ await qoni.genauth.resources.create({
142
+ namespace: 'project', code: 'documents', type: 'API',
143
+ apiIdentifier: '/documents',
144
+ actions: [{ name: 'documents:read', description: 'Read documents' }],
145
+ })
146
+ await qoni.genauth.permissions.authorize({
147
+ namespace: 'project',
148
+ list: [{
149
+ targetType: 'ROLE', targetIdentifiers: ['reader'],
150
+ resources: [{ code: 'documents:*', resourceType: 'API', actions: ['documents:read'] }],
151
+ }],
152
+ })
153
+ const { data: permission } = await qoni.genauth.permissions.isAllowed({
154
+ namespace: 'project', userId: user.userId,
155
+ resource: 'documents:123', action: 'documents:read',
156
+ })
157
+ console.log(permission.allowed)
158
+ ```
159
+
160
+ | 方法 | 功能 | 主要参数 | `data` |
161
+ | --- | --- | --- | --- |
162
+ | `namespaces.create` | 创建权限空间 | code、name | 权限空间对象 |
163
+ | `roles.create` | 创建角色 | namespace、code、name | 角色对象 |
164
+ | `roles.list` | 查询角色 | namespace、page、limit | { list, totalCount } |
165
+ | `roles.assign` | 分配角色 | namespace、code、targets | 操作结果 |
166
+ | `resources.create` | 定义资源及动作 | namespace、code、type、actions;API 资源使用 apiIdentifier | 资源对象 |
167
+ | `permissions.authorize` | 授予资源权限 | namespace、list | 操作结果 |
168
+ | `permissions.isAllowed` | 判断用户权限 | namespace、userId、resource、action | { allowed } |
169
+
170
+ 角色、资源和授权使用同一 `namespace`。创建资源用 `type`,授权资源项用 `resourceType`。
171
+
172
+ ## 应用管理
173
+
174
+ ```ts
175
+ const { data: application } = await qoni.genauth.applications.create({
176
+ appName: 'Example app',
177
+ appIdentifier: 'example-app',
178
+ appType: 'web',
179
+ defaultProtocol: 'oidc',
180
+ redirectUris: ['https://app.example.com/callback'],
181
+ })
182
+ const { data: appDetails } = await qoni.genauth.applications.get({ appId: application.appId })
183
+ const { data: appPage } = await qoni.genauth.applications.list({ page: 1, limit: 20 })
184
+ ```
185
+
186
+ | 方法 | 功能 | 主要参数 | `data` |
187
+ | --- | --- | --- | --- |
188
+ | `applications.create` | 创建应用 | appName;自建应用传 appIdentifier,可设置 appType、defaultProtocol、redirectUris | 应用创建结果 |
189
+ | `applications.get` | 读取应用配置 | appId | 应用对象 |
190
+ | `applications.list` | 分页查询应用 | page、limit、keywords | 应用列表 |
191
+ | `applications.update` | 更新应用配置 | appId 与更新字段 | 应用对象 |
192
+ | `applications.delete` | 删除应用 | appId | 操作结果 |
193
+
194
+ ## 自定义字段
195
+
196
+ ```ts
197
+ await qoni.genauth.customFields.set({
198
+ list: [{ targetType: 'USER', key: 'costCenter', label: 'Cost center', dataType: 'STRING' }],
199
+ })
200
+ await qoni.genauth.customFields.setData({
201
+ targetType: 'USER', targetIdentifier: user.userId,
202
+ list: [{ key: 'costCenter', value: 'engineering' }],
203
+ })
204
+ const { data: fieldValues } = await qoni.genauth.customFields.getData({
205
+ targetType: 'USER', targetIdentifier: user.userId,
206
+ })
207
+ ```
208
+
209
+ | 方法 | 功能 | 主要参数 | `data` |
210
+ | --- | --- | --- | --- |
211
+ | `customFields.list` | 查询字段定义 | targetType,默认 USER | 字段定义列表 |
212
+ | `customFields.set` | 创建或更新字段定义 | list: { targetType, key, label, dataType }[] | 操作结果 |
213
+ | `customFields.setData` | 设置对象字段值 | targetType、targetIdentifier、list | 操作结果 |
214
+ | `customFields.getData` | 读取对象字段值 | targetType、targetIdentifier | 字段数据 |
215
+
216
+ 先定义字段,再写入字段值。已有字段的 `dataType` 不能修改;角色字段需要传所属 `namespace`。
217
+
218
+ ## 其他管理功能
219
+
220
+ | 入口 | 功能 |
221
+ | --- | --- |
222
+ | `tenants` | 租户与租户成员 |
223
+ | `identityProviders` | 外部身份源与连接配置 |
224
+ | `dataPermissions / policies` | 数据资源、策略及数据授权 |
225
+ | `auditLogs / sessions` | 管理审计与用户会话 |
226
+ | `security / whitelist / riskLists` | MFA、安全设置及访问名单 |
227
+ | `webhooks / events` | Webhook 配置与事件 |
228
+ | `sync / pipelines` | 账号同步及认证流程 |
229
+ | `accessKeys` | 访问密钥管理 |
230
+ | `positions / dynamicGroups` | 岗位与动态用户组 |
231
+ | `devices / deviceRules` | 设备及设备规则 |
232
+ | `publicAccounts / invitations` | 公共账号及邀请 |
233
+ | `approvals / ldap` | 审批与 LDAP 配置 |
234
+ | `metadata / email / billing` | 元数据、邮件及计费配置 |
235
+
236
+ 以上入口均位于 `qoni.genauth` 下。具体方法、参数与返回类型见 [管理方法参考](./genauth-management-api)。
237
+
238
+ ## 返回值与请求取消
239
+
240
+ ```ts
241
+ const controller = new AbortController()
242
+ const { data: rolePage, meta } = await qoni.genauth.roles.list(
243
+ { namespace: 'project', page: 1, limit: 20 },
244
+ { signal: controller.signal },
245
+ )
246
+ console.log(rolePage.list, meta.requestId)
247
+ ```
248
+
249
+ 方法返回 `Promise<QoniResponse<T>>`:`data` 为业务结果,`meta.requestId` 为请求标识。第二参数传 `{ signal }`,调用 `controller.abort()` 取消请求。`users.iterate` 返回 `AsyncIterable`。
250
+
251
+ ## 使用已有管理 Token
252
+
253
+ ```ts
254
+ const { data: roleList } = await qoni.genauth.roles.list({
255
+ namespace: 'project',
256
+ adminToken: process.env.GENAUTH_MANAGEMENT_TOKEN!,
257
+ userPoolId: process.env.GENAUTH_USER_POOL_ID!,
258
+ })
259
+ ```
260
+
261
+ `adminToken` 与 `userPoolId` 必须同时传入。显式传入的 Token 过期后由调用方更新。
262
+
263
+ ## 错误处理
264
+
265
+ ```ts
266
+ import { QoniError } from '@qoniai/qoni'
267
+
268
+ try {
269
+ await qoni.genauth.users.get({ userId: '<user-id>' })
270
+ } catch (error) {
271
+ if (error instanceof QoniError) {
272
+ console.error(error.code, error.message, error.requestId)
273
+ }
274
+ throw error
275
+ }
276
+ ```
277
+
278
+ | 错误 | 处理方式 |
279
+ | --- | --- |
280
+ | `QoniValidationError` | 检查必填参数与字段类型 |
281
+ | `401` / `403` | 检查凭据、用户池绑定及管理权限 |
282
+ | `404` | 检查对象 ID 与所属用户池 |
283
+ | `409` | 处理对象状态冲突或标识重复 |
284
+ | `QoniTimeoutError` | 重试写操作前先查询原操作结果 |
285
+ | 其他 `QoniError` | 查看 `code`、`message`、`requestId`、`body` |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qoniai/qoni",
3
- "version": "0.8.0",
4
- "description": "Unified Node.js SDK for Qoni Agent delegation, GUMem memory, and WebAgent automation.",
3
+ "version": "0.9.1",
4
+ "description": "Unified Node.js SDK for Qoni Agent delegation, GenAuth management, GUMem memory, and WebAgent automation.",
5
5
  "homepage": "https://github.com/QONIAI/qoni-sdk-node#readme",
6
6
  "bugs": {
7
7
  "url": "https://github.com/QONIAI/qoni-sdk-node/issues"
@@ -19,7 +19,10 @@
19
19
  "sdk",
20
20
  "delegation",
21
21
  "gumem",
22
- "webagent"
22
+ "webagent",
23
+ "genauth",
24
+ "authing",
25
+ "management"
23
26
  ],
24
27
  "main": "./dist/index.cjs",
25
28
  "module": "./dist/index.js",
@@ -46,7 +49,7 @@
46
49
  "gen:event-types": "node scripts/gen-event-types.mjs",
47
50
  "typecheck": "tsc --noEmit",
48
51
  "test": "pnpm build && pnpm check:brand && pnpm test:mock && pnpm test:package",
49
- "test:mock": "vitest run --coverage=false tests/qoni.test.ts tests/semantic-surface.test.ts tests/open-source-readiness.test.ts tests/complete-coverage.test.ts tests/event-catalog.test.ts tests/interactions.test.ts tests/contract-surface.test.ts tests/sse-abort.test.ts tests/do-anything-evidence.test.ts tests/query-matrix.test.ts tests/live-evidence.test.ts tests/site-login-settle-track.test.ts",
52
+ "test:mock": "vitest run --coverage=false tests/qoni.test.ts tests/semantic-surface.test.ts tests/open-source-readiness.test.ts tests/complete-coverage.test.ts tests/event-catalog.test.ts tests/interactions.test.ts tests/contract-surface.test.ts tests/sse-abort.test.ts tests/do-anything-evidence.test.ts tests/query-matrix.test.ts tests/live-evidence.test.ts tests/site-login-settle-track.test.ts tests/abstract-interactions.test.ts tests/genauth-management.test.ts tests/genauth-communication.test.ts tests/genauth-hosted-login.test.ts tests/personal-agent-page.test.ts tests/agent-provisioning.test.ts tests/formal-agent-silent.test.ts tests/formal-agent-wildcard.test.ts tests/delegation-normalizer-r3.test.ts tests/gray-track-contract.test.ts",
50
53
  "test:e2e": "QONI_LIVE_E2E=1 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=120000 tests/live-e2e/*.test.ts",
51
54
  "test:e2e:do-anything": "QONI_LIVE_E2E=1 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=120000 tests/live-e2e/do-anything.test.ts",
52
55
  "test:e2e:do-anything:release": "QONI_LIVE_E2E=1 QONI_LIVE_ALLOW_ENV_CONSTRAINTS=0 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=120000 tests/live-e2e/do-anything.test.ts",
@@ -58,7 +61,13 @@
58
61
  "smoke:auth-gum:live": "pnpm build && QONI_SMOKE_MODE=live node scripts/qoni-auth-gum-smoke.mjs --live",
59
62
  "pack:dry-run": "pnpm build && pnpm pack --dry-run",
60
63
  "prepack": "pnpm build",
61
- "prepare": "tsup"
64
+ "prepare": "tsup",
65
+ "gen:genauth-management": "node scripts/gen-genauth-management.mjs",
66
+ "test:e2e:genauth": "QONI_LIVE_E2E=1 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=30000 tests/live-e2e/genauth-management.test.ts",
67
+ "gen:genauth-communication": "node scripts/gen-genauth-communication.mjs",
68
+ "check:genauth-communication": "node scripts/gen-genauth-communication.mjs --check",
69
+ "test:e2e:communication": "QONI_LIVE_E2E=1 vitest run --coverage=false --fileParallelism=false --maxWorkers=1 --testTimeout=30000 tests/live-e2e/genauth-communication.test.ts",
70
+ "check:genauth-communication-examples": "node scripts/check-genauth-communication-examples.mjs"
62
71
  },
63
72
  "publishConfig": {
64
73
  "access": "public"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: qoni-sdk
3
- description: Use when building, testing, debugging, or documenting Node.js integrations with @qoniai/qoni, the Qoni constructor, AK/SK credentials, delegateToken, GenAuth currentUser/userInfo/introspection/user management, Qoni runtime-config discovery, GUMem, WebAgent, Web Search, Do Anything, Deep Research, Track, OBO product token exchange, or local .genauth.localhost SDK flows.
3
+ description: Use when building, testing, debugging, or documenting Node.js integrations with @qoniai/qoni, the Qoni constructor, AK/SK credentials, delegateToken, GenAuth currentUser/userInfo/introspection/Authing v3 management, Qoni runtime-config discovery, GUMem, WebAgent, Web Search, Do Anything, Deep Research, Track, OBO product token exchange, or local .genauth.localhost SDK flows.
4
4
  ---
5
5
 
6
6
  # Qoni SDK
@@ -8,14 +8,14 @@ description: Use when building, testing, debugging, or documenting Node.js integ
8
8
  > **Requires `@qoniai/qoni` ≥ 0.4.0.** This guide documents the semantic-layer
9
9
  > surface: `run()`/`create()` return handles (`RunHandle` / `MonitorHandle`),
10
10
  > events are normalized (`event.type`, camelCase `data`, decoded screenshots),
11
- > and the 1:1 wire methods live under `qoni.<product>.api.*`. If the consumer
11
+ > and supported 1:1 wire methods live under `qoni.<product>.api.*`. If the consumer
12
12
  > resolves an older version from npm, install this repo's local build instead
13
13
  > (`npm install <path-to-qoni-sdk-node>` or a `pnpm pack` tarball) — otherwise
14
14
  > the code below will not match the installed SDK.
15
15
 
16
16
  ## Core Contract
17
17
 
18
- Use `@qoniai/qoni` from trusted server-side Node.js code. Keep AK/SK on the server and create one reusable `Qoni`. For silent runtime product calls, resolve a real GenAuth user id from the credential-bound userpool, call `delegateToken`, then pass `delegation.data.token` explicitly to every product capability call. For interactive runtime authorization, call `delegateToken({ mode: "interactive", redirectUri, state, agent, scopes })` from the server without exposing AK/SK; redirect the user to `authorizationUrl`, then handle the business callback server-side with `completeDelegateToken({ grantId, code, state })` to receive the token. For GenAuth user management, call `qoni.genauth.users.*`; the SDK exchanges AK/SK for a standard GenAuth management token internally and does not require `QONI_USER_ID`.
18
+ Use `@qoniai/qoni` from trusted server-side Node.js code. Keep AK/SK on the server and create one reusable `Qoni`. For silent runtime product calls, resolve a real GenAuth user id from the credential-bound userpool, call `delegateToken`, then pass `delegation.data.token` explicitly to every product capability call. For interactive runtime authorization, call `delegateToken({ mode: "interactive", redirectUri, state, agent, scopes })` from the server without exposing AK/SK; redirect the user to `authorizationUrl`, then handle the business callback server-side with `completeDelegateToken({ grantId, code, state })` to receive the token. For GenAuth management, use `qoni.genauth` domain namespaces (such as `users`, `roles`, `groups`, `organizations`, `applications`) or the Authing-name migration entry `qoni.genauth.management`; the SDK exchanges AK/SK for a standard GenAuth management token internally and does not require `QONI_USER_ID`.
19
19
 
20
20
  ```ts
21
21
  import { Qoni, QoniScopes } from "@qoniai/qoni";
@@ -66,12 +66,12 @@ const run = await qoni.doAnything.run({
66
66
  });
67
67
  const result = await run.wait({
68
68
  onScreenshot: (img, i) => saveImage(`step-${i}.jpg`, img.bytes),
69
- onInputRequest: (req) => openForUser(req.liveUrl),
69
+ onInteraction: (handle) => handleInteraction(handle), // see "Do Anything Interactions"
70
70
  });
71
71
  console.log(result.output);
72
72
  ```
73
73
 
74
- For explicit authorization, the Qoni Console/BFF owns GenAuth login and grant completion before it redirects back to the business `redirectUri`. The redirect only carries one-time callback fields such as `code`, `state`, and `grant_state`/`grantId`; never design an API that exposes a delegation token to browser code.
74
+ For explicit authorization, the Qoni Console/BFF owns GenAuth login and grant completion before it redirects back to the business `redirectUri`. The redirect only carries one-time callback fields: `code`, `grant_state`, and your business `state` when you supplied one. It does not carry `grantId`; load the `grantId` you saved by `grant_state`. Never design an API that exposes a delegation token to browser code.
75
75
 
76
76
  ```ts
77
77
  const grant = await qoni.delegateToken({
@@ -82,12 +82,17 @@ const grant = await qoni.delegateToken({
82
82
  scopes: [QoniScopes.GUMEM_MEMORY_READ],
83
83
  });
84
84
 
85
+ // The callback carries code, state (your business state) and grant_state, but no grantId.
86
+ await savePendingGrant(grant.data.grantState, { grantId: grant.data.grantId, businessState: "business-csrf-state" });
85
87
  redirectUserTo(grant.data.authorizationUrl);
86
88
 
89
+ // Callback handler: look up by grant_state, check the business state, then exchange.
90
+ const pending = await loadPendingGrant(callback.grant_state);
91
+ if (!pending || callback.state !== pending.businessState) throw new Error("Invalid delegation callback");
87
92
  const completed = await qoni.completeDelegateToken({
88
- grantId,
89
- code,
90
- state,
93
+ grantId: pending.grantId,
94
+ code: callback.code,
95
+ state: callback.grant_state,
91
96
  });
92
97
  ```
93
98
 
@@ -148,7 +153,7 @@ const run = await qoni.doAnything.run({
148
153
  run.id;
149
154
  run.sessionRef; // pass back to run({ session })
150
155
  await run.status(); // refresh current state
151
- await run.respond(requestId, response?); // HITL answer; omit response to skip
156
+ run.interactionHandle(event.data); // wrap an interaction event; answer via its methods
152
157
  await run.cancel(reason?); // idempotent — safe on a finished run
153
158
 
154
159
  const result = await run.wait({
@@ -156,7 +161,7 @@ const result = await run.wait({
156
161
  onEvent: (e) => console.log(e.type),
157
162
  onWireEvent: (e) => appendRawEvidence(e), // every SSE frame before semantic filtering
158
163
  onReconnect: (info) => logReconnect(info.attempt, info.lastEventId),
159
- onInputRequest: (req) => openForUser(req.liveUrl),
164
+ onInteraction: (handle) => handleInteraction(handle), // typed InteractionHandle
160
165
  onScreenshot: (img, i) => saveImage(`step-${i}.jpg`, img.bytes),
161
166
  });
162
167
  // result: { runId, status: "succeeded"|"failed"|"canceled", output?, artifacts,
@@ -210,18 +215,133 @@ for await (const event of run.events()) {
210
215
 
211
216
  `qoni.doAnything.api.events({ token, sessionId, runId, signal?, lastEventId?, onlyTopLevel? })` yields raw `{ id?, event, data }` wire events: the type is a raw string on `event.event` (e.g. `"run.status_changed"`), the payload is `event.data.data` (snake_case), and `event.data.task_id` identifies the run. `run.status_changed` payloads carry the new status in `.to` (NOT `.status`). Terminal detection is `event.event === "run.completed"`. Wire names are internal and may evolve — prefer the high-level `run.events()` + `event.type` instead.
212
217
 
213
- ## Do Anything Login Wall (human handoff)
218
+ ## Track and deprecated snapshot reads
219
+
220
+ Use `track.create({ token, prompt, schedule, title? })` with an explicit
221
+ `{ kind: "interval", intervalSeconds: N }` (integer 600–2592000 seconds) or
222
+ `{ kind: "daily", at: "HH:MM", tz: "Area/City" }`. Daily starts 0–30 minutes
223
+ before the target. Prompt maps to instructions; include URLs there. No default
224
+ schedule is inferred, and arbitrary cron is rejected with a daily alternative.
225
+
226
+ The historical `MonitorHandle` name remains. `get()` normalizes the real Track
227
+ record: known schedule shapes convert without write-bound/Intl revalidation;
228
+ unknown/incomplete wire schedules stay verbatim. Input validation remains strict.
229
+ `pause()`/`resume()` PATCH status; pause keeps any started check running.
230
+ `refine()` accepts title/schedule only and cannot change instructions.
231
+ `runNow()` returns `{ trackId, sessionId, runId }` (202 acceptance); it can
232
+ surface 409 task_in_progress. `runs({ limit?, offset? })` slices the newest 50
233
+ checks locally; `run(id)` searches that exact window and reports its range on
234
+ not_found. Keep real status/outcome/headline/answer/failure, never invent
235
+ extracted/diffs/snapshotId. `delete()` soft-deletes without cancelling a running task.
236
+
237
+ Create starts a first_look immediately. Capture the original initial lastTaskId,
238
+ wait for that exact check to terminate, then call runNow once; never retry 409
239
+ implicitly or substitute another check. Checks keep `running/done/failed/expired/
240
+ canceled`; success is done, distinct from RunResult.status succeeded. A missing
241
+ check in the recent window is local not_found without an HTTP status; a missing
242
+ Track is backend 404 track_not_found.
243
+
244
+ The example uses the reusable qoni client and delegated token from Core Contract:
214
245
 
215
- A web task often hits a page only the end user can log in to. The agent parks and emits an **`inputRequest`** event. The live-browser URL arrives on the request payload (`event.data.liveUrl`; live-url changes also stream as `progress` events with `data.liveUrl`). The client's ONLY job is to surface that URL so the human can sign in — login detection is the BACKEND's job; it re-probes and resumes the run automatically.
246
+ ```ts
247
+ import type { MonitorSchedule } from "@qoniai/qoni";
248
+
249
+ const monitor = await qoni.track.create({
250
+ token,
251
+ prompt: "Watch https://example.com/policy and report changes.",
252
+ title: "Policy changes",
253
+ schedule: { kind: "interval", intervalSeconds: 3600 },
254
+ }); // create immediately starts a first_look check
255
+ const initial = await monitor.get();
256
+ const firstRunId = initial.lastTaskId; // capture this original ID once
257
+ if (typeof firstRunId !== "string" || !firstRunId) throw new Error("Missing initial lastTaskId");
258
+ const sameMonitor = await qoni.track.attach(monitor.id, { token });
259
+
260
+ async function waitForCheck(runId: string) {
261
+ const deadline = Date.now() + 300_000;
262
+ do {
263
+ const check = await monitor.run(runId); // exact ID in the recent 50 checks
264
+ if (["done", "failed", "expired", "canceled"].includes(String(check.status))) {
265
+ if (check.status !== "done") throw new Error(`Check ${runId} ${String(check.status)}: ${String(check.failure ?? "")}`);
266
+ return check;
267
+ }
268
+ await new Promise(resolve => setTimeout(resolve, Math.min(1000, Math.max(0, deadline - Date.now()))));
269
+ } while (Date.now() < deadline);
270
+ throw new Error(`Check ${runId} timed out`);
271
+ }
272
+
273
+ try {
274
+ await monitor.pause(); // pause future scheduling; the first look continues
275
+ await waitForCheck(firstRunId);
276
+ await monitor.refine({ schedule: initial.schedule as MonitorSchedule }); // our known, validated input roundtrips
277
+ await monitor.refine({ schedule: { kind: "daily", at: "09:00", tz: "Asia/Singapore" } });
278
+ const task = await monitor.runNow(); // one explicit manual check while scheduling is paused
279
+ const check = await waitForCheck(task.runId); // 202 acceptance is not completion
280
+ const checks = await monitor.runs({ limit: 10, offset: 0 });
281
+ await monitor.resume();
282
+ } finally {
283
+ await monitor.delete(); // soft-delete; any in-flight check continues
284
+ }
285
+ ```
216
286
 
217
- - Open `liveUrl` ONCE in a real browser when you see `inputRequest`. Do NOT poll, do NOT call `respond`, do NOT wait on the window closing.
218
- - Keep iterating (or stay inside `wait()`); the run progresses past the wall and reaches its terminal event on its own once the user is signed in.
219
- - The first login is unavoidable; later runs reuse persisted login state and never reach this wall.
287
+ Wire API mapping (all supported routes use `/track/tracks`, not `/monitors`):
288
+
289
+ | Method | Contract |
290
+ | --- | --- |
291
+ | createMonitor / getMonitor | POST / GET Track; create uses instructions/title and wire schedule every_seconds or daily_at/tz. |
292
+ | updateMonitor | PATCH title/instructions/schedule/status; raw instructions is supported, high-level refine is stricter. |
293
+ | runNow / deleteMonitor | POST run_now returns track_id/session_id/run_id; DELETE returns empty 204. |
294
+ | listRuns / getRun | GET checks, local pagination/exact lookup within the recent 50. |
295
+ | api.events / api.intervene / MonitorHandle.events / interactionHandle | Deprecated; local QoniUnsupportedError, zero HTTP and zero token exchange. No Track scope authorizes DoAnything actions. |
296
+
297
+ Legacy targetUrls/extractionSchema/triggerDsl/stopConditionDsl/notifyChannel
298
+ (including snake_case) and url are unsupported local errors, never folded into
299
+ instructions. Cron and legacy event/option types remain exported, deprecated.
300
+ Invalid or missing input schedules raise QoniValidationError before HTTP.
301
+ Optional undefined keys keep their original omission behavior; defined
302
+ unsupported values (including null) are still local errors. Interval tz, when
303
+ specified, is an invalid schedule and raises QoniValidationError.
304
+
305
+ `doAnything.api.readArtifacts` and DoAnythingSnapshot/SnapshotImage/SnapshotAction
306
+ are deprecated: no backend snapshot route/source exists. The method raises
307
+ local QoniUnsupportedError (`unsupported.operation`), zero HTTP. Read actual
308
+ files with `qoni.doAnything.artifacts({ token, runId })` / api.listArtifacts and download via
309
+ artifact.content()/api.artifactDownloadUrl. For images, enable
310
+ capture.screenshots and consume screenshot events. A text artifact is not a
311
+ snapshot. Deprecated unsupported negatives never count as positive capability coverage.
312
+
313
+ ## Do Anything Interactions (human handoff)
314
+
315
+ When a run needs the user it pauses and emits one `interaction` event per request (`event.data` is a typed `Interaction`; `wait({ onInteraction })` hands you an `InteractionHandle`). Interactions are typed by what the user must do, never by business: `site_login`, `take_control`, `ask_user`, `confirmation`, plus the system `wait`. `switch (handle.interaction.type)` narrows `payload` per branch.
316
+
317
+ - Act only on `status === "pending"` and dedupe by `interaction.id`: creation, status changes and reconnect replays all call back again with the same `id` and `type`. Updates with real content replace the card (an `active` take-over carries a new `liveUrl`); only empty settling shells keep the original `payload`.
318
+ - Interactions arrive one at a time; the run resumes once the current one is handled.
319
+ - `site_login`: `openLogin()` → show `liveUrl` → after the user signs in, `confirmSignedIn()` (it saves the login once, then the agent rechecks). Do NOT call the deprecated `login.confirm()` first. A `QoniError` coded `site_login.save_failed` or `site_login.session_replaced` is retryable: call `confirmSignedIn()` again. Delegate `webagent.site_login:request` and `webagent.site_login:confirm`.
320
+ - `ask_user`: `answer(value)` with the type `payload.answerType` names (`text` string, `number`, `boolean`, `single_choice` option value, `multiple_choice` array of option values), or `skip()`. The wire's `clarification` arrives as `ask_user`; never switch on `"clarification"`.
321
+ - `take_control`: `connectControl()` only when `handle.can("connect_control")`; it resolves to `{ liveUrl, expiresAt }` (show that URL, else `payload.liveUrl`) → `releaseControl()`. `refreshControl()` returns a fresh URL.
322
+ - `confirmation`: `confirm()` or `reject()` after showing `payload.summary`.
323
+ - Only call methods `handle.can(kind)` reports; an undeclared action throws.
220
324
 
221
325
  ```ts
222
326
  const result = await run.wait({
223
- onInputRequest: (req) => {
224
- if (req.liveUrl) openBrowserForUser(String(req.liveUrl)); // pop once; the backend resumes by itself
327
+ onInteraction: async (handle) => {
328
+ const request = handle.interaction;
329
+ if (request.status !== "pending" || handled.has(request.id)) return;
330
+ handled.add(request.id);
331
+ switch (request.type) {
332
+ case "site_login": {
333
+ const login = await handle.openLogin();
334
+ await showToUser(login.liveUrl); // resolves when the user says "I'm signed in"
335
+ await handle.confirmSignedIn();
336
+ break;
337
+ }
338
+ case "ask_user":
339
+ await handle.answer(await askUser(request.payload));
340
+ break;
341
+ case "confirmation":
342
+ await handle.confirm();
343
+ break;
344
+ }
225
345
  },
226
346
  });
227
347
  ```
@@ -243,12 +363,9 @@ pending → running → (running ⇄ awaiting_input)* → succeeded | failed | c
243
363
  `terminal_reason` / `is_task_successful`).
244
364
 
245
365
  **Client action is event-driven, not status-driven.** The only thing that needs
246
- you is the `inputRequest` event (surface `liveUrl` so a human can log in /
247
- confirm — see Login Wall). Everything else, including login walls, the backend
248
- re-probes and resumes on its own. A run flapping `running ⇄ awaiting_input`
249
- (e.g. `waiting_reason=confirmation_required` during internal pacing) is
250
- expected — do NOT poll or call `respond`. Only call `respond` to answer an
251
- actual `inputRequest`.
366
+ you is an `interaction` event (see Do Anything Interactions). A run flapping
367
+ `running ⇄ awaiting_input` (e.g. `waiting_reason=confirmation_required` during
368
+ internal pacing) is expected — do NOT poll; act only on pending interactions.
252
369
 
253
370
  ### Parent & sub-tasks
254
371
 
@@ -322,3 +439,19 @@ For skill or docs changes that teach integration, also validate from a fresh con
322
439
  5. Run the consumer project's typecheck and test command.
323
440
 
324
441
  Prefer mocked fetch-based tests for normal verification. Use real GenAuth/GUMem/WebAgent services only for explicit smoke tests, and never hard-code real secrets in repository files.
442
+
443
+ ## Authing v3 management additions
444
+
445
+ The SDK provides 591 typed JSON management methods in 35 namespaces, aligned with the official Authing Node SDK 4.0.2 (496 JSON methods) plus GenAuth extensions and compatibility names. Read `docs/genauth-management.md` and `docs/genauth-management-api.md` for exact methods, aliases, inputs and deployment-dependent rules. Preserve the Qoni management-token exchange; do not use a delegated runtime token for management calls.
446
+
447
+ - Use `genauth.users.iterate` for paginated user processing (limit 1-50); pass cancellation in the optional second argument.
448
+ - Roles and resources belong to an existing permission namespace. Resource action names use `resourceCode:action`; newer permission deployments require `apiIdentifier` for API resources.
449
+ - `customFields.set` requires `dataType` for both create and update. `customFields.list` defaults the subject to `USER`.
450
+ - HTTP-200 business errors throw typed Qoni errors, including numeric Authing API codes as strings. Do not replay writes after network or business failures.
451
+ - Respect employment rules: resign a user only when departure is intended. The SDK never automatically resigns users to make deletion succeed.
452
+ - Filtered user queries depend on the GenAuth search index. A successful `get` or batch lookup does not prove that a newly created user is searchable. Keep search acceptance failures visible and diagnose the server index separately.
453
+ - Run `pnpm test:e2e:genauth` using an isolated test userpool; cleanup failures fail the suite. Generated contract coverage does not imply live acceptance of every operation.
454
+
455
+ ### Agent Mail and Agent Phone (unreleased extension)
456
+
457
+ Use `qoni.genauth.agentMail` and `qoni.genauth.agentPhone`. Read `docs/genauth-communication.md` and `docs/genauth-communication-api.md` before using these APIs. Runtime requires native GenAuth credentials through `genauthCommunicationCredentials`; administrator calls reuse Qoni management exchange. Preserve stable idempotency keys, pass current revisions, and inspect original operations after ambiguous results. Never treat an unknown purchase/send outcome as safe to repeat. Phone numbers, mailboxes, and communication personas are distinct from login email/phone and delegation agentId.