@modesto_ma/dsh-channels 0.4.0 → 0.5.0-beta.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.
Files changed (58) hide show
  1. package/README.md +73 -55
  2. package/docs/access-control-design.md +7 -5
  3. package/docs/attachment-pipeline-v2.md +33 -41
  4. package/docs/feishu-onboarding.md +33 -33
  5. package/docs/persistence.md +6 -4
  6. package/docs/refactor-plan.md +1 -1
  7. package/docs/workspace-isolation-design.md +7 -8
  8. package/lib/client.js +173 -287
  9. package/lib/client.js.map +1 -1
  10. package/lib/index.js +3001 -2627
  11. package/lib/index.js.map +1 -1
  12. package/package.json +1 -1
  13. package/src/access/cards.ts +9 -9
  14. package/src/access/controller.ts +10 -10
  15. package/src/access/index.ts +1 -0
  16. package/src/access/policy.ts +10 -10
  17. package/src/access/requests.ts +32 -31
  18. package/src/access/store.ts +12 -12
  19. package/src/access/targets.ts +56 -0
  20. package/src/cards/approval.ts +96 -26
  21. package/src/cards/index.ts +1 -0
  22. package/src/cards/onboarding.ts +1 -0
  23. package/src/cards/picker.ts +115 -0
  24. package/src/cards/streaming.ts +56 -7
  25. package/src/client/api.ts +5 -25
  26. package/src/client/panel/access-card.tsx +24 -15
  27. package/src/client/panel/access-tab.tsx +3 -3
  28. package/src/client/panel/connection-tab.tsx +70 -1
  29. package/src/client/panel/index.tsx +11 -62
  30. package/src/client/panel/shared.tsx +0 -23
  31. package/src/client/panel/workspace-tab.tsx +2 -18
  32. package/src/client/styles.ts +2 -0
  33. package/src/commands/registry.ts +2 -2
  34. package/src/config.ts +25 -56
  35. package/src/domain.ts +28 -125
  36. package/src/feishu/inbound-attachments.ts +1 -1
  37. package/src/feishu/manifest.ts +8 -31
  38. package/src/feishu/message-read.ts +19 -22
  39. package/src/feishu/scopes.ts +171 -0
  40. package/src/feishu/send-file.ts +543 -0
  41. package/src/feishu/slash-commands.ts +134 -0
  42. package/src/feishu/tools.ts +72 -112
  43. package/src/feishu-channel.ts +270 -9
  44. package/src/index.ts +5 -4
  45. package/src/interactions/approvals.ts +187 -7
  46. package/src/interactions/pending.ts +3 -3
  47. package/src/routing/inbound.ts +73 -106
  48. package/src/service.ts +463 -438
  49. package/src/session/manager.ts +1 -1
  50. package/src/session/routes.ts +2 -3
  51. package/src/settings/contract.ts +68 -85
  52. package/src/settings/routes.ts +3 -17
  53. package/src/settings/surface.ts +37 -88
  54. package/src/streamer.ts +94 -5
  55. package/src/workspaces.ts +21 -29
  56. package/src/cleanup.ts +0 -546
  57. package/src/client/panel/cleanup-card.tsx +0 -106
  58. package/src/feishu/pending-attachments.ts +0 -562
@@ -293,7 +293,7 @@ export class SessionManager {
293
293
  const workspacePath = record.workspacePath
294
294
  const workspace = await this.deps.ctx.workspaceRegistry.create(
295
295
  workspacePath,
296
- workspaceTitleFor(record.chatType, record.chatId, record.senderOpenId),
296
+ workspaceTitleFor(record.chatType, record.chatId, record.senderUserId),
297
297
  )
298
298
 
299
299
  let newHandle: AgentHandle | undefined
@@ -135,8 +135,7 @@ export class RouteStore {
135
135
 
136
136
  /**
137
137
  * Retire one active thread route into a persistent tombstone regardless of
138
- * the configured TTL. Used by the workspace-retention cleanup, which may
139
- * close topics earlier than the route TTL.
138
+ * the configured TTL.
140
139
  */
141
140
  async retire(key: string, now = Date.now()): Promise<ExpiredRouteRecord | undefined> {
142
141
  const record = this.get(key)
@@ -145,7 +144,7 @@ export class RouteStore {
145
144
  key,
146
145
  chatId: record.chatId,
147
146
  chatType: record.chatType,
148
- senderOpenId: record.senderOpenId,
147
+ senderUserId: record.senderUserId,
149
148
  expiredAt: now,
150
149
  workspacePath: record.workspacePath,
151
150
  ...(record.sessionDir === undefined ? {} : { sessionDir: record.sessionDir }),
@@ -20,13 +20,11 @@ export const CREDENTIALS_ROUTE = '/dsh-channels/credentials'
20
20
  export const RECONNECT_ROUTE = '/dsh-channels/reconnect'
21
21
  /** 模型/推理档位/预设选择器的宿主目录(provider → model → effort 级联刷新)。 */
22
22
  export const CATALOG_ROUTE = '/dsh-channels/catalog'
23
- /** 手动执行一次过期目录清理(POST,无请求体)。 */
24
- export const CLEANUP_ROUTE = '/dsh-channels/cleanup'
25
23
  /** 访问与授权只读快照(GET/HEAD):准入模式、管理员、群/用户白名单、待处理申请。 */
26
24
  export const ACCESS_ROUTE = '/dsh-channels/access'
27
25
  /** 群白名单增删(POST/DELETE,body `{ chatId, name? }` / `{ chatId }`)。 */
28
26
  export const ACCESS_GROUPS_ROUTE = '/dsh-channels/access/groups'
29
- /** 用户白名单增删(POST/DELETE,body `{ openId, name? }` / `{ openId }`)。 */
27
+ /** 用户白名单增删(POST/DELETE,body `{ userId, name? }` / `{ userId }`)。 */
30
28
  export const ACCESS_USERS_ROUTE = '/dsh-channels/access/users'
31
29
  /** 结算一条待处理授权申请(POST,body `{ token, decision }`)。 */
32
30
  export const ACCESS_REQUESTS_ROUTE = '/dsh-channels/access/requests'
@@ -56,16 +54,15 @@ export const PICK_DIRECTORY_ROUTE = '/dsh-channels/pick-directory'
56
54
  export const EDITABLE_KEYS = [
57
55
  // 工作区
58
56
  'baseDir',
59
- 'workspaceRetention',
60
- 'workspaceRetentionAuto',
61
57
  // 行为
62
58
  'dmMode',
63
59
  'requireMention',
64
60
  'threadFallback',
65
61
  'welcomeOnboarding',
62
+ 'slashCommandPanel',
66
63
  'allowPermissionSwitch',
67
64
  // 准入与授权(管理员/准入模式/申请有效期;白名单由「访问与授权」卡片管理)
68
- 'adminOpenId',
65
+ 'adminUserId',
69
66
  'accessMode',
70
67
  'authRequestTtlDays',
71
68
  // 内容与附件(v2 管线;`feishuTools.*` 是嵌套配置,覆盖层仍写成嵌套对象)
@@ -76,8 +73,6 @@ export const EDITABLE_KEYS = [
76
73
  'prefetchImageMaxBytes',
77
74
  'prefetchTotalMaxBytes',
78
75
  'historyBootstrapLimit',
79
- 'pendingAttachmentTtlMs',
80
- 'pendingAttachmentLimit',
81
76
  // 模型工具(feishuTools 子项)
82
77
  'feishuTools.attachmentFetch',
83
78
  'feishuTools.attachmentFetchMaxResources',
@@ -85,6 +80,7 @@ export const EDITABLE_KEYS = [
85
80
  'feishuTools.attachmentFetchTimeoutMs',
86
81
  'feishuTools.topicMessages',
87
82
  'feishuTools.topicMessagesTimeoutMs',
83
+ 'feishuTools.sendFile',
88
84
  'promptContextTimeoutMs',
89
85
  // 模型
90
86
  'provider',
@@ -106,6 +102,7 @@ export type EditableKey = (typeof EDITABLE_KEYS)[number]
106
102
  export const RESTART_KEYS = [
107
103
  'dmMode',
108
104
  'requireMention',
105
+ 'slashCommandPanel',
109
106
  'refreshMs',
110
107
  'statusRefreshMs',
111
108
  'loggerLevel',
@@ -266,8 +263,40 @@ export interface ChannelStatusWire {
266
263
  overlayPath: string
267
264
  /** 正在连接或重连时为 true,此时面板的写操作按钮禁用。 */
268
265
  busy: boolean
266
+ /** 连接后的权限自检结果;未连接或自检尚未完成时缺省。 */
267
+ scopeCheck?: ScopeCheckWire
268
+ /** 连接后飞书原生斜杠指令面板的同步结果;未连接或同步尚未完成时缺省。 */
269
+ slashCommandSync?: SlashCommandSyncWire
269
270
  }
270
271
 
272
+ /** One plugin need whose scopes are all missing. */
273
+ export interface MissingScopeWire {
274
+ /** Scope names, preferred first; granting any one satisfies the need. */
275
+ scopes: string[]
276
+ /** `required` breaks a core path; `optional` only degrades a feature. */
277
+ level: 'required' | 'optional'
278
+ /** What the plugin loses without it. */
279
+ purpose: string
280
+ /** Developer-console permission page filtered to the preferred scope. */
281
+ consoleUrl: string
282
+ }
283
+
284
+ /** Post-connect scope self-check outcome. */
285
+ export type ScopeCheckWire =
286
+ /** Every applicable need is granted. */
287
+ | { state: 'ok'; checkedAt: number }
288
+ /** Some needs are missing; `consoleUrl` opens the console filtered to all of them. */
289
+ | { state: 'missing'; checkedAt: number; missing: MissingScopeWire[]; consoleUrl: string }
290
+ /** The grant list could not be read; `error` carries the Feishu reason. */
291
+ | { state: 'error'; checkedAt: number; error: string }
292
+
293
+ /** Post-connect native slash-command panel sync outcome. */
294
+ export type SlashCommandSyncWire =
295
+ /** The panel lists exactly the registry commands; counts are this sync's writes. */
296
+ | { state: 'ok'; checkedAt: number; created: number; updated: number; removed: number; total: number }
297
+ /** A Feishu call failed; `error` carries the Feishu reason. */
298
+ | { state: 'error'; checkedAt: number; error: string }
299
+
271
300
  /** The whole editable configuration surface, with provenance. */
272
301
  export interface PanelConfigWire {
273
302
  /** profile `cordis.patch.yml` 提供的值(覆盖层之下)。 */
@@ -298,9 +327,9 @@ export interface AccessGroupWire {
298
327
  addedAt: number
299
328
  }
300
329
 
301
- /** One authorized user `open_id` (global across every authorized group). */
330
+ /** One authorized user `user_id` (global across every authorized group). */
302
331
  export interface AccessUserWire {
303
- openId: string
332
+ userId: string
304
333
  /** Display name captured at authorization time. */
305
334
  name?: string
306
335
  /** Which authorization entry produced the grant. */
@@ -314,7 +343,7 @@ export interface AccessRequestWire {
314
343
  /** Opaque token; never derived from the target id. */
315
344
  token: string
316
345
  kind: 'group' | 'user'
317
- /** `chat_id` for a group request, `open_id` for a user request. */
346
+ /** `chat_id` for a group request, `user_id` for a user request. */
318
347
  targetId: string
319
348
  /** Display name of the requested target (group name / user name), when known. */
320
349
  name?: string
@@ -337,9 +366,14 @@ export interface AccessRequestWire {
337
366
  export interface AccessWire {
338
367
  mode: 'open' | 'guarded'
339
368
  admin: {
340
- openId?: string
369
+ /** The effective administrator: configured `adminUserId`, else the app owner. */
370
+ userId?: string
371
+ /** Where `userId` came from: explicit config or the app-owner fallback. */
372
+ source?: 'config' | 'owner'
341
373
  /** false while guarded mode would refuse every conversation (fail closed). */
342
374
  configured: boolean
375
+ /** Why the app-owner fallback is unavailable, when its lookup failed. */
376
+ ownerLookupError?: string
343
377
  }
344
378
  /**
345
379
  * Whether the persisted whitelists were loaded. While the channel holds no
@@ -361,51 +395,10 @@ export type AccessMutationResponse =
361
395
  export interface PanelWire {
362
396
  status: ChannelStatusWire
363
397
  config: PanelConfigWire
364
- cleanup: CleanupPreviewWire
365
398
  /** 访问与授权卡片的数据(准入模式、管理员、白名单、待处理申请)。 */
366
399
  access: AccessWire
367
400
  }
368
401
 
369
- /**
370
- * What the workspace-retention cleanup would delete right now. Counts are
371
- * zero and `error` is set when the Host cannot compute the preview (for
372
- * example the cleanup seam is unavailable); the panel stays usable.
373
- */
374
- export interface CleanupPreviewWire {
375
- /** Effective `workspaceRetention` value (`off` / `7d` / `30d` / `90d` / `180d`). */
376
- retention: string
377
- /** Retention threshold in milliseconds; absent when `retention` is `off`. */
378
- thresholdMs?: number
379
- /** Periodic cleanup runs at startup and once per day. */
380
- auto: boolean
381
- /** Plugin-created workspaces under the current baseDir (user projects). */
382
- workspaces: number
383
- /** Directories older than the threshold (legacy topic workspaces + per-topic dirs). */
384
- candidates: number
385
- /** Total bytes currently occupied by the candidates. */
386
- candidateBytes: number
387
- /** Why the preview could not be computed (counts are zero then). */
388
- error?: string
389
- }
390
-
391
- /** Outcome of one manual cleanup run (partial failures do not abort the run). */
392
- export interface CleanupRunWire {
393
- ok: boolean
394
- deletedWorkspaces: number
395
- /** Per-topic working directories removed (current layout). */
396
- deletedSessionDirs: number
397
- retiredRoutes: number
398
- freedBytes: number
399
- /** One `path: message` line per directory that could not be deleted. */
400
- failures: string[]
401
- error?: string
402
- }
403
-
404
- /** `POST /dsh-channels/cleanup` response. */
405
- export type CleanupResponse =
406
- | { ok: true; panel: PanelWire; result: CleanupRunWire }
407
- | { ok: false; error: string }
408
-
409
402
  /**
410
403
  * `POST /dsh-channels/pick-directory` response. `canceled` is a normal
411
404
  * outcome (the operator dismissed the OS dialog), not an error; `path` is an
@@ -540,6 +533,21 @@ export const PANEL_COPY = {
540
533
  statusSummaryConnecting: '正在连接飞书长连接…',
541
534
  statusSummaryError: '最近一次连接飞书失败,请检查凭据与网络后重试。',
542
535
  lastError: '最近一次连接失败',
536
+ scopeCheckTitle: '权限自检',
537
+ scopeCheckOk: '已开通插件需要的全部飞书权限。',
538
+ scopeCheckMissing: '以下飞书权限未开通,开通后需在开发者后台发布新版本:',
539
+ scopeCheckOpenAll: '在开发者后台开通全部缺失权限',
540
+ scopeCheckOpenOne: '去开通',
541
+ scopeCheckOptional: '可选',
542
+ scopeCheckRequired: '必需',
543
+ scopeCheckError: '无法读取应用的权限授予状态:',
544
+ slashCommandSyncTitle: '斜杠指令面板',
545
+ slashCommandSyncOk: '插件命令已同步到飞书原生斜杠指令面板,在聊天框输入 / 即可选择(飞书客户端约 5 分钟后刷新)。',
546
+ slashCommandSyncTotal: '命令数',
547
+ slashCommandSyncCreated: '本次新增',
548
+ slashCommandSyncUpdated: '本次更新',
549
+ slashCommandSyncRemoved: '本次删除',
550
+ slashCommandSyncError: '同步飞书原生斜杠指令面板失败(不影响直接输入命令):',
543
551
  missingConfig: '缺少配置',
544
552
  statusHintUnconfigured: '配置完成后插件会自动连接;也可以稍后在本页手动连接。',
545
553
  connectionInfo: '连接信息',
@@ -588,9 +596,9 @@ export const PANEL_COPY = {
588
596
  credentialsPendingHint: '填写后点「保存凭据」;两项齐全时插件会自动连接飞书。',
589
597
  // ---- tabs / forms -------------------------------------------------------
590
598
  connectionTabHelp: '连接状态与飞书应用凭据。凭据不完整时不会连接飞书。',
591
- sessionsTabHelp: '私聊、群聊触发方式与入站内容:附件存储/预取、静默登记保留、模型工具开关与读取预算;「需重连」项在下次连接时读取。',
599
+ sessionsTabHelp: '私聊、群聊触发方式与入站内容:附件存储/预取、模型工具开关与读取预算;「需重连」项在下次连接时读取。',
592
600
  modelTabHelp: '飞书会话使用的模型服务商、模型、推理档位与 Agent 预设;下拉选项来自宿主机实时目录,保存后对新会话生效。',
593
- workspaceTabHelp: '每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录,也不会被这里的目录清理删除。',
601
+ workspaceTabHelp: '每个用户一个工作区(模型/工具产生文件的落点),每个话题在其中拥有独立工作目录;只影响之后新建的话题。入站附件默认存于 DSH 原生附件库,不占用话题目录。插件不会自动删除工作区或话题目录;过期目录可手动删除,话题下次对话时会重建空目录。',
594
602
  accessTabHelp: '准入采用 fail-closed:管理员不受白名单限制,非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。',
595
603
  advancedTabHelp: '流式刷新等低层参数、Web 覆盖文件与只读信息。',
596
604
  save: '保存更改',
@@ -637,37 +645,12 @@ export const PANEL_COPY = {
637
645
  advancedHelp: '这些值来自 profile 配置,不能在面板里编辑。',
638
646
  readOnlyCard: '部署级参数',
639
647
  // ---- model --------------------------------------------------------------
640
- // ---- cleanup ------------------------------------------------------------
641
- cleanupCard: '目录清理',
642
- cleanupCardHelp: '只清理工作区与 Agent 产物:删除超过保留期且不再活跃的飞书话题目录、旧版本按话题建立的工作区目录,以及这些目录内 Agent 产生的文件;不影响 Session 附件——入站图片/文件默认存于 DSH 原生附件库,不随目录清理删除。用户工作区本身与 DSH 会话日志也不会删除。删除不可恢复:相关旧话题会失效并提示已过期。',
643
- cleanupRetention: '保留期',
644
- cleanupAutoLabel: '自动清理',
645
- cleanupCandidates: '可清理目录',
646
- cleanupWorkspaces: '插件工作区',
647
- cleanupCandidateBytes: '预计释放空间',
648
- cleanupRetentionOff: '关闭(不删除)',
649
- cleanupAutoOn: '已开启(启动时 + 每天一次)',
650
- cleanupAutoOff: '已关闭(仅手动执行)',
651
- cleanupAutoInactive: '未执行(保留期为关闭)',
652
- cleanupNow: '立即清理',
653
- cleanupRunning: '清理中…',
654
- cleanupConfirm: '确定清理过期目录吗?只清理工作区与 Agent 产物,不影响 Session 附件;但这些目录内未提交的文件会被删除,且操作不可恢复。',
655
- cleanupConfirmHint: '删除前会再次确认;保留期为「关闭」或没有过期目录时按钮不可用。',
656
- cleanupDisabledOff: '保留策略为「关闭」:请先在上方「过期目录保留期」中选择保留天数。',
657
- cleanupDisabledEmpty: '没有超过保留期的过期目录。',
658
- cleanupPreviewFailed: '无法读取清理预览',
659
- cleanupDone: '清理完成',
660
- cleanupFailed: '清理失败',
661
- cleanupDeleted: '已删除工作区',
662
- cleanupDeletedDirs: '话题目录',
663
- cleanupRetired: '已清理话题路由',
664
- cleanupFreed: '释放空间',
665
- cleanupFailures: '以下目录未能删除:',
666
648
  // ---- access -------------------------------------------------------------
667
649
  accessCard: '访问与授权',
668
- accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 open_id。',
650
+ accessCardHelp: '准入采用 fail-closed:管理员不受白名单限制(私聊、任意群聊都可直接对话);非管理员私聊会被拒绝,群聊需要「群已授权 + 用户在白名单」两层同时命中。管理员与准入模式在上方「准入与授权」中编辑;私聊机器人发送 /userid 获取自己的 user_id。',
669
651
  accessModeOpenWarning: '当前准入模式为开放(open):应用可用范围内的任何人都能开启对话。该模式不安全,仅建议调试时使用。',
670
- accessAdminMissingWarning: '当前准入=guarded 但未配置管理员,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 open_id。',
652
+ accessAdminMissingWarning: '当前准入=guarded 但未配置管理员,也无法自动使用应用所有者,所有对话都会被拒绝。请在上方「准入与授权」填写管理员 user_id(私聊机器人发送 /userid 获取;私聊模式关闭时 /userid 不可用,可在飞书管理后台的成员详情里查看用户 ID)。',
653
+ accessAdminOwnerNotice: '未配置管理员,已自动使用应用所有者作为管理员:',
671
654
  accessGroupsTitle: '已授权群',
672
655
  accessGroupsEmpty: '尚未授权任何群。机器人被拉进群后,管理员会收到带 chat_id 的群信息卡;也可在群内发送 /groupid 获取。',
673
656
  accessUsersTitle: '已授权用户',
@@ -676,7 +659,7 @@ export const PANEL_COPY = {
676
659
  accessRequestsEmpty: '没有待处理的授权申请。',
677
660
  accessGroupChatIdPlaceholder: '群 chat_id(oc_…)',
678
661
  accessGroupNamePlaceholder: '群名(可选)',
679
- accessUserOpenIdPlaceholder: '用户 open_id(ou_…)',
662
+ accessUserIdPlaceholder: '用户 user_id',
680
663
  accessUserNamePlaceholder: '昵称(可选)',
681
664
  accessAddGroup: '添加群',
682
665
  accessAddUser: '添加用户',
@@ -698,5 +681,5 @@ export const PANEL_COPY = {
698
681
  accessRequestAllowed: '已授权',
699
682
  accessRequestDenied: '已忽略',
700
683
  accessHints: '三种授权入口:群内 /auth 命令、入群与用户授权卡、本面板。撤销立即生效;白名单只约束非管理员(管理员在任何私聊/群聊都直接放行),非管理员需要群和用户两层同时命中。',
701
- accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;open_id 可私聊机器人发送 /userid 获取。',
684
+ accessChatIdHint: 'chat_id 可从入群群信息卡或群内 /groupid 获取;user_id 可私聊机器人发送 /userid 获取。',
702
685
  } as const
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Web 设置面板的 HTTP 路由:status / panel / catalog / config(PUT,DELETE) /
3
- * credentials(PUT,DELETE) / reconnect / cleanup / access。
3
+ * credentials(PUT,DELETE) / reconnect / access。
4
4
  *
5
5
  * 路由注册进宿主可选的 `webServer` 服务(headless profile 没有它时面板自然
6
6
  * 不存在,飞书通道不受影响)。写操作额外做同源校验 —— 本机非回环绑定下
@@ -20,7 +20,6 @@ import {
20
20
  ACCESS_ROUTE,
21
21
  ACCESS_USERS_ROUTE,
22
22
  CATALOG_ROUTE,
23
- CLEANUP_ROUTE,
24
23
  CONFIG_ROUTE,
25
24
  CREDENTIALS_ROUTE,
26
25
  PANEL_ROUTE,
@@ -310,19 +309,6 @@ export function registerSettingsRoutes(server: HostWebServer, surface: SettingsS
310
309
  sendJson(res, result.ok ? 200 : (result.unconfigured === true ? 400 : 500), result)
311
310
  }),
312
311
  }),
313
- server.register({
314
- kind: 'exact',
315
- path: CLEANUP_ROUTE,
316
- handler: guarded(async (req, res) => {
317
- if (!allowOnly(req, res, ['POST'])) return
318
- if (!isSameOriginRequest(req)) {
319
- forbiddenWrite(res)
320
- return
321
- }
322
- const result = await surface.runCleanup()
323
- sendJson(res, result.ok ? 200 : 500, result)
324
- }),
325
- }),
326
312
  server.register({
327
313
  kind: 'exact',
328
314
  path: PICK_DIRECTORY_ROUTE,
@@ -404,8 +390,8 @@ export function registerSettingsRoutes(server: HostWebServer, surface: SettingsS
404
390
  const payload = await readAccessBody(req, res)
405
391
  if (payload === undefined) return
406
392
  const result = methodOf(req) === 'DELETE'
407
- ? await surface.removeAccessUser(payload.openId)
408
- : await surface.saveAccessUser(payload.openId, payload.name)
393
+ ? await surface.removeAccessUser(payload.userId)
394
+ : await surface.saveAccessUser(payload.userId, payload.name)
409
395
  sendJson(res, result.ok ? 200 : 400, result)
410
396
  }),
411
397
  }),