@dsh-enhanced/lark-channel 0.1.3

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +342 -0
  3. package/bin/dsh-lark-setup.js +8 -0
  4. package/bin/dsh-supervised-growth-setup.js +8 -0
  5. package/cordis.patch.yml +21 -0
  6. package/lib/adapter.d.ts +135 -0
  7. package/lib/adapter.d.ts.map +1 -0
  8. package/lib/adapter.js +965 -0
  9. package/lib/adapter.js.map +1 -0
  10. package/lib/approval.d.ts +26 -0
  11. package/lib/approval.d.ts.map +1 -0
  12. package/lib/approval.js +104 -0
  13. package/lib/approval.js.map +1 -0
  14. package/lib/config.d.ts +21 -0
  15. package/lib/config.d.ts.map +1 -0
  16. package/lib/config.js +56 -0
  17. package/lib/config.js.map +1 -0
  18. package/lib/group-thread.d.ts +11 -0
  19. package/lib/group-thread.d.ts.map +1 -0
  20. package/lib/group-thread.js +22 -0
  21. package/lib/group-thread.js.map +1 -0
  22. package/lib/index.d.ts +27 -0
  23. package/lib/index.d.ts.map +1 -0
  24. package/lib/index.js +30 -0
  25. package/lib/index.js.map +1 -0
  26. package/lib/launchd.d.ts +44 -0
  27. package/lib/launchd.d.ts.map +1 -0
  28. package/lib/launchd.js +205 -0
  29. package/lib/launchd.js.map +1 -0
  30. package/lib/model-picker.d.ts +24 -0
  31. package/lib/model-picker.d.ts.map +1 -0
  32. package/lib/model-picker.js +95 -0
  33. package/lib/model-picker.js.map +1 -0
  34. package/lib/normalize.d.ts +12 -0
  35. package/lib/normalize.d.ts.map +1 -0
  36. package/lib/normalize.js +77 -0
  37. package/lib/normalize.js.map +1 -0
  38. package/lib/permission-picker.d.ts +29 -0
  39. package/lib/permission-picker.d.ts.map +1 -0
  40. package/lib/permission-picker.js +145 -0
  41. package/lib/permission-picker.js.map +1 -0
  42. package/lib/progress.d.ts +15 -0
  43. package/lib/progress.d.ts.map +1 -0
  44. package/lib/progress.js +137 -0
  45. package/lib/progress.js.map +1 -0
  46. package/lib/resident.d.ts +13 -0
  47. package/lib/resident.d.ts.map +1 -0
  48. package/lib/resident.js +41 -0
  49. package/lib/resident.js.map +1 -0
  50. package/lib/sdk.d.ts +131 -0
  51. package/lib/sdk.d.ts.map +1 -0
  52. package/lib/sdk.js +945 -0
  53. package/lib/sdk.js.map +1 -0
  54. package/lib/service.d.ts +30 -0
  55. package/lib/service.d.ts.map +1 -0
  56. package/lib/service.js +157 -0
  57. package/lib/service.js.map +1 -0
  58. package/lib/setup-materialization.d.ts +15 -0
  59. package/lib/setup-materialization.d.ts.map +1 -0
  60. package/lib/setup-materialization.js +221 -0
  61. package/lib/setup-materialization.js.map +1 -0
  62. package/lib/setup-profile.d.ts +23 -0
  63. package/lib/setup-profile.d.ts.map +1 -0
  64. package/lib/setup-profile.js +1126 -0
  65. package/lib/setup-profile.js.map +1 -0
  66. package/lib/setup.d.ts +206 -0
  67. package/lib/setup.d.ts.map +1 -0
  68. package/lib/setup.js +2143 -0
  69. package/lib/setup.js.map +1 -0
  70. package/lib/supervised-growth-profile.d.ts +74 -0
  71. package/lib/supervised-growth-profile.d.ts.map +1 -0
  72. package/lib/supervised-growth-profile.js +485 -0
  73. package/lib/supervised-growth-profile.js.map +1 -0
  74. package/lib/supervised-growth-setup.d.ts +85 -0
  75. package/lib/supervised-growth-setup.d.ts.map +1 -0
  76. package/lib/supervised-growth-setup.js +371 -0
  77. package/lib/supervised-growth-setup.js.map +1 -0
  78. package/lib/systemd.d.ts +39 -0
  79. package/lib/systemd.d.ts.map +1 -0
  80. package/lib/systemd.js +142 -0
  81. package/lib/systemd.js.map +1 -0
  82. package/lib/tool-approval.d.ts +22 -0
  83. package/lib/tool-approval.d.ts.map +1 -0
  84. package/lib/tool-approval.js +107 -0
  85. package/lib/tool-approval.js.map +1 -0
  86. package/lib/types.d.ts +158 -0
  87. package/lib/types.d.ts.map +1 -0
  88. package/lib/types.js +11 -0
  89. package/lib/types.js.map +1 -0
  90. package/lib/version.d.ts +2 -0
  91. package/lib/version.d.ts.map +1 -0
  92. package/lib/version.js +2 -0
  93. package/lib/version.js.map +1 -0
  94. package/lib/windows-task.d.ts +39 -0
  95. package/lib/windows-task.d.ts.map +1 -0
  96. package/lib/windows-task.js +117 -0
  97. package/lib/windows-task.js.map +1 -0
  98. package/lib/ws-card-callback.d.ts +7 -0
  99. package/lib/ws-card-callback.d.ts.map +1 -0
  100. package/lib/ws-card-callback.js +51 -0
  101. package/lib/ws-card-callback.js.map +1 -0
  102. package/package.json +96 -0
package/lib/setup.js ADDED
@@ -0,0 +1,2143 @@
1
+ #!/usr/bin/env node
2
+ import { createHash, randomBytes } from 'node:crypto';
3
+ import { realpathSync } from 'node:fs';
4
+ import { chmod, mkdir, open, readFile, readdir, rename, rm, unlink } from 'node:fs/promises';
5
+ import { homedir } from 'node:os';
6
+ import { basename, dirname, isAbsolute, join, win32 } from 'node:path';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { createInterface } from 'node:readline/promises';
10
+ import { spawnSync } from 'node:child_process';
11
+ import { registerApp } from '@larksuiteoapi/node-sdk';
12
+ import * as QRCode from 'qrcode';
13
+ import { isMap, isSeq, parseDocument } from 'yaml';
14
+ import { createOfficialLarkTransport } from './sdk.js';
15
+ import { installDshResidentService } from './resident.js';
16
+ import { configureLarkProfilePatch, refreshLarkAgentPolicyPatch } from './setup-profile.js';
17
+ import { assertRawManagedProfileIntegrity, materializeManagedProfileOverride, } from './setup-materialization.js';
18
+ const setupKeyPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u;
19
+ const keychainLocatorPattern = /^[A-Za-z0-9][A-Za-z0-9/._-]{0,255}$/u;
20
+ function argumentValue(argv, index, option) {
21
+ const value = argv[index + 1];
22
+ if (value === undefined || value.startsWith('--'))
23
+ throw new Error(`lark-channel setup: ${option} requires a value`);
24
+ return value;
25
+ }
26
+ export function parseLarkSetupArgs(argv) {
27
+ const result = {
28
+ profile: 'web',
29
+ domain: 'feishu',
30
+ account: 'primary',
31
+ accountProvided: false,
32
+ tenant: 'personal',
33
+ createApp: false,
34
+ appName: 'DSH Personal Assistant',
35
+ timeoutMs: 300_000,
36
+ installServiceOnly: false,
37
+ refreshAgentPolicy: false,
38
+ manageService: true,
39
+ agentTools: 'preserve',
40
+ help: false,
41
+ };
42
+ for (let index = 0; index < argv.length; index += 1) {
43
+ const option = argv[index];
44
+ if (/secret/iu.test(option))
45
+ throw new Error('lark-channel setup: secret arguments are not accepted; use the Keychain prompt');
46
+ if (option === '--help' || option === '-h') {
47
+ result.help = true;
48
+ continue;
49
+ }
50
+ if (option === '--profile')
51
+ result.profile = argumentValue(argv, index++, option);
52
+ else if (option === '--domain')
53
+ result.domain = argumentValue(argv, index++, option);
54
+ else if (option === '--account') {
55
+ result.account = argumentValue(argv, index++, option);
56
+ result.accountProvided = true;
57
+ }
58
+ else if (option === '--tenant')
59
+ result.tenant = argumentValue(argv, index++, option);
60
+ else if (option === '--app-id')
61
+ result.appId = argumentValue(argv, index++, option);
62
+ else if (option === '--create-app')
63
+ result.createApp = true;
64
+ else if (option === '--install-service')
65
+ result.installServiceOnly = true;
66
+ else if (option === '--refresh-agent-policy')
67
+ result.refreshAgentPolicy = true;
68
+ else if (option === '--no-service')
69
+ result.manageService = false;
70
+ else if (option === '--allow-agent-tools' || option === '--disable-agent-tools') {
71
+ if (result.agentTools !== 'preserve') {
72
+ throw new Error('lark-channel setup: --allow-agent-tools and --disable-agent-tools are mutually exclusive');
73
+ }
74
+ result.agentTools = option === '--allow-agent-tools' ? 'enable' : 'disable';
75
+ }
76
+ else if (option === '--app-name')
77
+ result.appName = argumentValue(argv, index++, option);
78
+ else if (option === '--timeout-ms')
79
+ result.timeoutMs = Number(argumentValue(argv, index++, option));
80
+ else
81
+ throw new Error(`lark-channel setup: unknown option: ${option}`);
82
+ }
83
+ if (!setupKeyPattern.test(result.profile))
84
+ throw new Error('lark-channel setup: invalid profile');
85
+ if (!setupKeyPattern.test(result.account))
86
+ throw new Error('lark-channel setup: invalid account');
87
+ if (!setupKeyPattern.test(result.tenant))
88
+ throw new Error('lark-channel setup: invalid tenant');
89
+ if (result.appId !== undefined && !/^cli_[0-9a-fA-F]{16}$/u.test(result.appId)) {
90
+ throw new Error('lark-channel setup: invalid App ID');
91
+ }
92
+ if (!result.help && result.refreshAgentPolicy) {
93
+ const incompatible = [
94
+ '--domain', '--tenant', '--app-id', '--create-app', '--install-service',
95
+ '--no-service', '--app-name', '--timeout-ms',
96
+ ].find(option => argv.includes(option));
97
+ if (incompatible !== undefined) {
98
+ throw new Error(`lark-channel setup: --refresh-agent-policy cannot be combined with ${incompatible}`);
99
+ }
100
+ if (result.agentTools === 'preserve') {
101
+ throw new Error('lark-channel setup: --refresh-agent-policy requires --allow-agent-tools or --disable-agent-tools');
102
+ }
103
+ }
104
+ if (result.installServiceOnly && !result.manageService) {
105
+ throw new Error('lark-channel setup: --install-service and --no-service cannot be used together');
106
+ }
107
+ if (result.installServiceOnly && (result.createApp || result.appId !== undefined)) {
108
+ throw new Error('lark-channel setup: --install-service cannot be combined with application setup options');
109
+ }
110
+ if (result.installServiceOnly && result.agentTools !== 'preserve') {
111
+ throw new Error('lark-channel setup: --install-service cannot be combined with agent-tools options');
112
+ }
113
+ const appNameHasControlCharacter = [...result.appName]
114
+ .some(character => character.codePointAt(0) <= 31 || character.codePointAt(0) === 127);
115
+ if (result.appName.trim().length === 0 || result.appName.length > 64 || appNameHasControlCharacter) {
116
+ throw new Error('lark-channel setup: app-name must be 1..64 characters without control characters');
117
+ }
118
+ if (result.domain !== 'feishu' && result.domain !== 'lark')
119
+ throw new Error('lark-channel setup: domain must be feishu or lark');
120
+ if (!Number.isSafeInteger(result.timeoutMs) || result.timeoutMs < 30_000 || result.timeoutMs > 900_000) {
121
+ throw new Error('lark-channel setup: timeout-ms must be an integer from 30000 to 900000');
122
+ }
123
+ return result;
124
+ }
125
+ export function createLarkRegistrationOptions(input) {
126
+ return {
127
+ domain: input.domain === 'lark' ? 'accounts.larksuite.com' : 'accounts.feishu.cn',
128
+ larkDomain: 'accounts.larksuite.com',
129
+ source: 'dsh-enhanced/lark-channel',
130
+ ...(input.appId === undefined ? {} : { appId: input.appId }),
131
+ signal: input.signal,
132
+ appPreset: {
133
+ name: input.appName,
134
+ desc: '由 DeepSeek Harness 驱动的个人助理',
135
+ },
136
+ addons: {
137
+ preset: false,
138
+ scopes: { tenant: [
139
+ 'application:bot.basic_info:read',
140
+ 'im:message.group_at_msg:readonly',
141
+ 'im:message.p2p_msg:readonly',
142
+ 'im:message.reactions:write_only',
143
+ 'im:message:send_as_bot',
144
+ 'im:resource',
145
+ ] },
146
+ events: { items: { tenant: ['im.message.receive_v1'] } },
147
+ callbacks: { items: ['card.action.trigger'] },
148
+ },
149
+ onQRCodeReady: input.onQRCodeReady,
150
+ onStatusChange: input.onStatusChange,
151
+ };
152
+ }
153
+ const credentialVersionPattern = /^[0-9a-f]{32}$/u;
154
+ /**
155
+ * Creates a locator which has never been active before. Profile validation can
156
+ * therefore read a candidate secret without replacing the currently active
157
+ * Keychain, Secret Service, or DPAPI value.
158
+ */
159
+ export function createVersionedCredentialLocator(input) {
160
+ if (!isAbsolute(input.dshHome) && !win32.isAbsolute(input.dshHome)) {
161
+ throw new Error('lark-channel setup: dshHome must be absolute');
162
+ }
163
+ if (!setupKeyPattern.test(input.profile))
164
+ throw new Error('lark-channel setup: invalid profile');
165
+ if (!setupKeyPattern.test(input.account))
166
+ throw new Error('lark-channel setup: invalid account');
167
+ const version = input.version ?? randomBytes(16).toString('hex');
168
+ if (!credentialVersionPattern.test(version)) {
169
+ throw new Error('lark-channel setup: invalid credential version');
170
+ }
171
+ if (input.provider === 'windows-dpapi') {
172
+ return {
173
+ provider: input.provider,
174
+ path: join(input.dshHome, 'credentials-keychain', `lark-${input.profile}-${input.account}-${version}.clixml`),
175
+ };
176
+ }
177
+ return {
178
+ provider: input.provider,
179
+ service: `dsh/lark/${input.profile}/${input.account}/versions/${version}`,
180
+ account: input.account,
181
+ };
182
+ }
183
+ const windowsPowerShell = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
184
+ const windowsDpapiWriteCommand = "$secret = [Console]::In.ReadToEnd(); $secure = ConvertTo-SecureString $secret -AsPlainText -Force; $credential = [pscredential]::new('dsh-enhanced', $secure); $credential | Export-Clixml -LiteralPath $args[0]";
185
+ const windowsDpapiReadCommand = '$credential = Import-Clixml -LiteralPath $args[0]; [Console]::Out.Write($credential.GetNetworkCredential().Password)';
186
+ export function credentialProviderForPlatform(platform) {
187
+ if (platform === 'darwin')
188
+ return 'macos-keychain';
189
+ if (platform === 'linux')
190
+ return 'linux-secret-service';
191
+ if (platform === 'win32')
192
+ return 'windows-dpapi';
193
+ throw new Error(`lark-channel setup: unsupported platform: ${platform}`);
194
+ }
195
+ export function createKeychainWriteRequest(service, account, secret) {
196
+ if (!keychainLocatorPattern.test(service) || !keychainLocatorPattern.test(account)) {
197
+ throw new Error('lark-channel setup: invalid Keychain locator');
198
+ }
199
+ if (secret.length === 0)
200
+ throw new Error('lark-channel setup: cannot store an empty secret');
201
+ const secretHex = Buffer.from(secret, 'utf8').toString('hex');
202
+ return {
203
+ args: ['-i'],
204
+ input: Buffer.from(`add-generic-password -U -a ${account} -s ${service} -X ${secretHex}\n`, 'utf8'),
205
+ };
206
+ }
207
+ export function createSecretServiceWriteRequest(service, account, secret) {
208
+ if (!keychainLocatorPattern.test(service) || !keychainLocatorPattern.test(account)) {
209
+ throw new Error('lark-channel setup: invalid Secret Service locator');
210
+ }
211
+ if (secret.length === 0)
212
+ throw new Error('lark-channel setup: cannot store an empty secret');
213
+ const suffix = service.startsWith('dsh/lark/') ? service.slice('dsh/lark/'.length) : service;
214
+ return {
215
+ executable: '/usr/bin/secret-tool',
216
+ args: ['store', `--label=DSH Lark ${suffix}`, 'service', service, 'account', account],
217
+ input: Buffer.from(secret, 'utf8'),
218
+ };
219
+ }
220
+ export function createWindowsDpapiWriteRequest(path, secret) {
221
+ if (path.length < 1 || path.length > 1_024 || path.includes('\0')
222
+ || (!isAbsolute(path) && !win32.isAbsolute(path))) {
223
+ throw new Error('lark-channel setup: invalid DPAPI credential path');
224
+ }
225
+ if (secret.length === 0)
226
+ throw new Error('lark-channel setup: cannot store an empty secret');
227
+ return {
228
+ executable: windowsPowerShell,
229
+ args: ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsDpapiWriteCommand, path],
230
+ input: Buffer.from(secret, 'utf8'),
231
+ };
232
+ }
233
+ export function matchOwnerHandshake(input) {
234
+ if (input.message.chatType !== 'p2p' || input.message.content.trim() !== input.phrase)
235
+ return undefined;
236
+ return {
237
+ channel: 'lark',
238
+ account: input.account,
239
+ tenant: input.tenant,
240
+ user: input.message.senderId,
241
+ };
242
+ }
243
+ export function isMainEntry(moduleUrl, argvPath) {
244
+ try {
245
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(argvPath);
246
+ }
247
+ catch {
248
+ return false;
249
+ }
250
+ }
251
+ function help() {
252
+ return `dsh-lark-setup [options]
253
+
254
+ Safely connect one Feishu/Lark self-built bot to a DSH profile.
255
+
256
+ Options:
257
+ --profile <name> DSH profile (default: web)
258
+ --domain <name> feishu or lark (default: feishu)
259
+ --account <id> Local channel account id (default: primary)
260
+ --tenant <id> Local tenant namespace (default: personal)
261
+ --create-app Select, create, or officially update an app
262
+ --app-name <name> New app name (default: DSH Personal Assistant)
263
+ --app-id <cli_...> Existing app id; combine with --create-app to update it
264
+ --install-service Only install/restart the profile's resident service
265
+ --refresh-agent-policy Only refresh policy from the existing channel binding
266
+ --no-service Configure Lark without installing a resident service
267
+ --allow-agent-tools Allow mounted foreground/external Agent capabilities
268
+ --disable-agent-tools Remove setup-managed Agent capability rules
269
+ --timeout-ms <ms> Owner DM wait, 30000..900000 (default: 300000)
270
+ -h, --help Show this help
271
+
272
+ Without --create-app or --app-id, press Enter at the App ID prompt to select or create an app.
273
+ App Secret is intentionally not accepted as an argument. It is stored in macOS Keychain,
274
+ Linux Secret Service, or a per-user Windows DPAPI file.`;
275
+ }
276
+ function runSecurity(args, stdio) {
277
+ const result = spawnSync('/usr/bin/security', [...args], {
278
+ encoding: 'utf8',
279
+ env: { PATH: '/usr/bin:/bin' },
280
+ maxBuffer: 128 * 1024,
281
+ stdio,
282
+ });
283
+ if (result.status !== 0)
284
+ throw new Error('lark-channel setup: macOS Keychain operation failed');
285
+ return typeof result.stdout === 'string' ? result.stdout.trimEnd() : '';
286
+ }
287
+ function linuxCredentialEnv() {
288
+ return {
289
+ PATH: '/usr/bin:/bin',
290
+ ...(process.env.DBUS_SESSION_BUS_ADDRESS === undefined
291
+ ? {} : { DBUS_SESSION_BUS_ADDRESS: process.env.DBUS_SESSION_BUS_ADDRESS }),
292
+ ...(process.env.XDG_RUNTIME_DIR === undefined ? {} : { XDG_RUNTIME_DIR: process.env.XDG_RUNTIME_DIR }),
293
+ };
294
+ }
295
+ function executeSecretWrite(request, env, failure) {
296
+ try {
297
+ const result = spawnSync(request.executable, request.args, {
298
+ encoding: 'utf8',
299
+ env,
300
+ input: request.input,
301
+ maxBuffer: 128 * 1024,
302
+ stdio: ['pipe', 'ignore', 'pipe'],
303
+ });
304
+ if (result.status !== 0)
305
+ throw new Error(failure);
306
+ }
307
+ finally {
308
+ request.input.fill(0);
309
+ }
310
+ }
311
+ function storeSecret(provider, service, account, credentialPath) {
312
+ if (provider === 'macos-keychain') {
313
+ process.stdout.write('\n请在 macOS Keychain 的安全提示中输入 App Secret(输入不会回显):\n');
314
+ runSecurity(['add-generic-password', '-U', '-a', account, '-s', service, '-w'], 'inherit');
315
+ return;
316
+ }
317
+ if (provider === 'linux-secret-service') {
318
+ const prompt = spawnSync('/usr/bin/systemd-ask-password', ['飞书 App Secret:'], {
319
+ encoding: 'utf8', env: linuxCredentialEnv(), maxBuffer: 128 * 1024, stdio: ['inherit', 'pipe', 'inherit'],
320
+ });
321
+ const secret = typeof prompt.stdout === 'string' ? prompt.stdout.replace(/[\r\n]+$/u, '') : '';
322
+ if (prompt.status !== 0 || secret.length === 0) {
323
+ throw new Error('lark-channel setup: Linux could not read the App Secret securely');
324
+ }
325
+ executeSecretWrite(createSecretServiceWriteRequest(service, account, secret), linuxCredentialEnv(), 'lark-channel setup: Linux Secret Service operation failed');
326
+ return;
327
+ }
328
+ if (credentialPath === undefined || (!isAbsolute(credentialPath) && !win32.isAbsolute(credentialPath))) {
329
+ throw new Error('lark-channel setup: invalid DPAPI credential path');
330
+ }
331
+ const manualCommand = "$secure = Read-Host 'Feishu App Secret' -AsSecureString; $credential = [pscredential]::new('dsh-enhanced', $secure); $credential | Export-Clixml -LiteralPath $args[0]";
332
+ const result = spawnSync(windowsPowerShell, [
333
+ '-NoLogo', '-NoProfile', '-Command', manualCommand, credentialPath,
334
+ ], { encoding: 'utf8', env: { PATH: 'C:\\Windows\\System32;C:\\Windows', SystemRoot: 'C:\\Windows' }, stdio: 'inherit' });
335
+ if (result.status !== 0)
336
+ throw new Error('lark-channel setup: Windows DPAPI operation failed');
337
+ }
338
+ function storeGeneratedSecret(provider, service, account, secret, credentialPath) {
339
+ if (provider === 'macos-keychain') {
340
+ const request = createKeychainWriteRequest(service, account, secret);
341
+ try {
342
+ const result = spawnSync('/usr/bin/security', request.args, {
343
+ encoding: 'utf8', env: { PATH: '/usr/bin:/bin' }, input: request.input,
344
+ maxBuffer: 128 * 1024, stdio: ['pipe', 'ignore', 'pipe'],
345
+ });
346
+ if (result.status !== 0)
347
+ throw new Error('lark-channel setup: macOS Keychain operation failed');
348
+ }
349
+ finally {
350
+ request.input.fill(0);
351
+ }
352
+ return;
353
+ }
354
+ if (provider === 'linux-secret-service') {
355
+ executeSecretWrite(createSecretServiceWriteRequest(service, account, secret), linuxCredentialEnv(), 'lark-channel setup: Linux Secret Service operation failed');
356
+ return;
357
+ }
358
+ if (credentialPath === undefined)
359
+ throw new Error('lark-channel setup: missing DPAPI credential path');
360
+ executeSecretWrite(createWindowsDpapiWriteRequest(credentialPath, secret), {
361
+ PATH: 'C:\\Windows\\System32;C:\\Windows', SystemRoot: 'C:\\Windows',
362
+ }, 'lark-channel setup: Windows DPAPI operation failed');
363
+ }
364
+ function readSecret(provider, service, account, credentialPath) {
365
+ let value = '';
366
+ if (provider === 'macos-keychain') {
367
+ value = runSecurity(['find-generic-password', '-w', '-a', account, '-s', service], 'pipe');
368
+ }
369
+ else if (provider === 'linux-secret-service') {
370
+ const result = spawnSync('/usr/bin/secret-tool', ['lookup', 'service', service, 'account', account], {
371
+ encoding: 'utf8', env: linuxCredentialEnv(), maxBuffer: 128 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
372
+ });
373
+ if (result.status !== 0)
374
+ throw new Error('lark-channel setup: Linux Secret Service lookup failed');
375
+ value = typeof result.stdout === 'string' ? result.stdout.trimEnd() : '';
376
+ }
377
+ else {
378
+ if (credentialPath === undefined)
379
+ throw new Error('lark-channel setup: missing DPAPI credential path');
380
+ const result = spawnSync(windowsPowerShell, [
381
+ '-NoLogo', '-NoProfile', '-NonInteractive', '-Command', windowsDpapiReadCommand, credentialPath,
382
+ ], {
383
+ encoding: 'utf8', env: { PATH: 'C:\\Windows\\System32;C:\\Windows', SystemRoot: 'C:\\Windows' },
384
+ maxBuffer: 128 * 1024, stdio: ['ignore', 'pipe', 'pipe'],
385
+ });
386
+ if (result.status !== 0)
387
+ throw new Error('lark-channel setup: Windows DPAPI lookup failed');
388
+ value = typeof result.stdout === 'string' ? result.stdout : '';
389
+ }
390
+ if (value.length === 0)
391
+ throw new Error('lark-channel setup: credential store returned an empty secret');
392
+ return value;
393
+ }
394
+ function findYamlRow(rows, id) {
395
+ if (!isSeq(rows))
396
+ return undefined;
397
+ return rows.items.find(item => isMap(item) && item.get('id') === id);
398
+ }
399
+ function platformJoin(root, ...segments) {
400
+ return !isAbsolute(root) && win32.isAbsolute(root) ? win32.join(root, ...segments) : join(root, ...segments);
401
+ }
402
+ function setupRowIsEnabled(row) {
403
+ const disabled = row.get('disabled');
404
+ if (disabled !== undefined && typeof disabled !== 'boolean') {
405
+ throw new Error('lark-channel setup: effective profile row has a non-boolean disabled value');
406
+ }
407
+ return disabled !== true;
408
+ }
409
+ function exactYamlStringSequence(node, expected) {
410
+ if (!isSeq(node) || node.items.length !== expected.length)
411
+ return false;
412
+ return node.items.every((item, index) => item?.toJSON() === expected[index]);
413
+ }
414
+ function exactManagedCredentialHandleShape(handle, provider) {
415
+ const expectedKeys = provider === 'windows-dpapi'
416
+ ? ['consumers', 'id', 'maxLeaseMs', 'path', 'provider', 'purposes']
417
+ : ['account', 'consumers', 'id', 'maxLeaseMs', 'provider', 'purposes', 'service'];
418
+ const value = handle.toJSON();
419
+ return Object.keys(value).sort().join('\0') === expectedKeys.join('\0')
420
+ && exactYamlStringSequence(handle.get('consumers', true), ['dsh-enhanced-lark-channel'])
421
+ && exactYamlStringSequence(handle.get('purposes', true), ['connect'])
422
+ && handle.get('maxLeaseMs') === 86_400_000;
423
+ }
424
+ function credentialHandleAliasesLocator(handle, locator) {
425
+ if (!isMap(handle) || handle.get('provider') !== locator.provider)
426
+ return false;
427
+ if (locator.provider === 'windows-dpapi')
428
+ return handle.get('path') === locator.path;
429
+ return handle.get('service') === locator.service && handle.get('account') === locator.account;
430
+ }
431
+ /**
432
+ * Returns only locators whose complete shape matches a setup-owned Lark
433
+ * credential. Ambiguous or user-authored handles are intentionally retained.
434
+ */
435
+ export function findManagedLarkCredentialLocator(input) {
436
+ const document = parseDocument(input.profilePatch);
437
+ if (document.errors.length > 0 || !isSeq(document.contents))
438
+ return undefined;
439
+ const larkRow = findYamlRow(document.contents, 'dsh-enhanced-lark-channel');
440
+ const credentialRow = findYamlRow(document.contents, 'dsh-enhanced-credentials-keychain');
441
+ if (larkRow === undefined || credentialRow === undefined)
442
+ return undefined;
443
+ const channel = larkRow.get('config', true);
444
+ const credentialConfig = credentialRow.get('config', true);
445
+ if (!isMap(channel) || !isMap(credentialConfig))
446
+ return undefined;
447
+ const account = channel.get('account');
448
+ const handleId = channel.get('credentialHandle');
449
+ if (typeof account !== 'string'
450
+ || !setupKeyPattern.test(account)
451
+ || handleId !== `lark-app-secret-${account}`)
452
+ return undefined;
453
+ const handles = credentialConfig.get('handles', true);
454
+ if (!isSeq(handles))
455
+ return undefined;
456
+ const matchingHandles = handles.items.filter(item => isMap(item) && item.get('id') === handleId);
457
+ if (matchingHandles.length !== 1)
458
+ return undefined;
459
+ const handle = matchingHandles[0];
460
+ if (!isMap(handle))
461
+ return undefined;
462
+ const provider = handle.get('provider');
463
+ if ((provider !== 'macos-keychain' && provider !== 'linux-secret-service' && provider !== 'windows-dpapi')
464
+ || !exactManagedCredentialHandleShape(handle, provider))
465
+ return undefined;
466
+ if (provider === 'macos-keychain' || provider === 'linux-secret-service') {
467
+ const service = handle.get('service');
468
+ const credentialAccount = handle.get('account');
469
+ const baseService = `dsh/lark/${input.profile}/${account}`;
470
+ const version = typeof service === 'string' && service.startsWith(`${baseService}/versions/`)
471
+ ? service.slice(`${baseService}/versions/`.length)
472
+ : undefined;
473
+ if (typeof service !== 'string'
474
+ || credentialAccount !== account
475
+ || (service !== baseService && (version === undefined || !credentialVersionPattern.test(version)))) {
476
+ return undefined;
477
+ }
478
+ const locator = { provider, service, account };
479
+ if (handles.items.some(item => item !== handle && credentialHandleAliasesLocator(item, locator)))
480
+ return undefined;
481
+ return locator;
482
+ }
483
+ if (provider === 'windows-dpapi') {
484
+ const path = handle.get('path');
485
+ if (typeof path !== 'string')
486
+ return undefined;
487
+ const directory = platformJoin(input.dshHome, 'credentials-keychain');
488
+ const legacyPath = platformJoin(directory, `lark-${input.profile}-${account}.clixml`);
489
+ const versionPrefix = platformJoin(directory, `lark-${input.profile}-${account}-`);
490
+ const version = path.startsWith(versionPrefix) && path.endsWith('.clixml')
491
+ ? path.slice(versionPrefix.length, -'.clixml'.length)
492
+ : undefined;
493
+ if (path !== legacyPath && (version === undefined || !credentialVersionPattern.test(version)))
494
+ return undefined;
495
+ const locator = { provider, path };
496
+ if (handles.items.some(item => item !== handle && credentialHandleAliasesLocator(item, locator)))
497
+ return undefined;
498
+ return locator;
499
+ }
500
+ return undefined;
501
+ }
502
+ async function removeCredential(locator) {
503
+ if (locator.provider === 'windows-dpapi') {
504
+ try {
505
+ await unlink(locator.path);
506
+ }
507
+ catch (error) {
508
+ if (fileSystemErrorCode(error) !== 'ENOENT') {
509
+ throw new Error('lark-channel setup: Windows DPAPI credential cleanup failed', { cause: error });
510
+ }
511
+ }
512
+ return;
513
+ }
514
+ const result = locator.provider === 'macos-keychain'
515
+ ? spawnSync('/usr/bin/security', [
516
+ 'delete-generic-password', '-a', locator.account, '-s', locator.service,
517
+ ], {
518
+ encoding: 'utf8', env: { PATH: '/usr/bin:/bin' }, maxBuffer: 128 * 1024, stdio: ['ignore', 'ignore', 'pipe'],
519
+ })
520
+ : spawnSync('/usr/bin/secret-tool', [
521
+ 'clear', 'service', locator.service, 'account', locator.account,
522
+ ], {
523
+ encoding: 'utf8', env: linuxCredentialEnv(), maxBuffer: 128 * 1024, stdio: ['ignore', 'ignore', 'pipe'],
524
+ });
525
+ const missingMacosItem = locator.provider === 'macos-keychain' && result.status === 44;
526
+ const missingItemMessage = typeof result.stderr === 'string'
527
+ && /(?:could not be found|not found|no matching|no such)/iu.test(result.stderr);
528
+ if (result.status !== 0 && !missingMacosItem && !missingItemMessage) {
529
+ throw new Error(`lark-channel setup: ${locator.provider} credential cleanup failed`);
530
+ }
531
+ }
532
+ function credentialIsAlreadyAbsent(error) {
533
+ const code = fileSystemErrorCode(error);
534
+ return code === 'ENOENT' || code === 'not-found';
535
+ }
536
+ async function removeCredentialIdempotently(locator, operation) {
537
+ try {
538
+ await operation(locator);
539
+ }
540
+ catch (error) {
541
+ if (!credentialIsAlreadyAbsent(error))
542
+ throw error;
543
+ }
544
+ }
545
+ async function retirePreviousCredentialBestEffort(locator, operation) {
546
+ try {
547
+ await removeCredentialIdempotently(locator, operation);
548
+ }
549
+ catch {
550
+ // The profile and Delivery owner already reference the staged credential.
551
+ // Retaining an inactive old secret is safer than blocking all future setup
552
+ // recovery or falsely reporting that the committed rotation failed.
553
+ process.stderr.write('lark-channel setup: previous credential cleanup failed after commit\n');
554
+ }
555
+ }
556
+ async function registerLarkApplication(args) {
557
+ const abortController = new AbortController();
558
+ const abort = () => abortController.abort();
559
+ process.once('SIGINT', abort);
560
+ let qrRender = Promise.resolve();
561
+ try {
562
+ return await registerApp(createLarkRegistrationOptions({
563
+ domain: args.domain,
564
+ appName: args.appName,
565
+ ...(args.appId === undefined ? {} : { appId: args.appId }),
566
+ signal: abortController.signal,
567
+ onQRCodeReady(info) {
568
+ process.stdout.write(`\n请在飞书中打开以下链接,选择已有应用或创建新应用(${info.expireIn} 秒内有效):\n${info.url}\n\n`);
569
+ qrRender = QRCode.toString(info.url, { type: 'terminal', small: true })
570
+ .then(code => { process.stdout.write(`${code}\n`); })
571
+ .catch(() => { process.stdout.write('终端二维码生成失败,请直接打开上面的链接。\n'); });
572
+ },
573
+ onStatusChange(info) {
574
+ if (info.status === 'domain_switched')
575
+ process.stdout.write('已切换到 Lark 国际版授权域。\n');
576
+ if (info.status === 'slow_down')
577
+ process.stdout.write('飞书要求降低授权轮询频率,正在继续等待确认…\n');
578
+ },
579
+ }));
580
+ }
581
+ catch (error) {
582
+ const code = typeof error === 'object' && error !== null && 'code' in error ? String(error.code) : undefined;
583
+ if (code === 'access_denied')
584
+ throw new Error('lark-channel setup: app creation was denied in Feishu');
585
+ if (code === 'expired_token')
586
+ throw new Error('lark-channel setup: the app creation link expired; run the wizard again');
587
+ if (code === 'abort')
588
+ throw new Error('lark-channel setup: app creation was cancelled');
589
+ throw new Error('lark-channel setup: Feishu could not create the application');
590
+ }
591
+ finally {
592
+ await qrRender;
593
+ process.removeListener('SIGINT', abort);
594
+ }
595
+ }
596
+ async function discoverOwner(transport, phrase, account, tenant, timeoutMs) {
597
+ let finish;
598
+ let fail;
599
+ const owner = new Promise((resolve, reject) => {
600
+ finish = resolve;
601
+ fail = reject;
602
+ });
603
+ const timer = setTimeout(() => fail(new Error('lark-channel setup: timed out waiting for the owner DM')), timeoutMs);
604
+ const unsubscribe = transport.subscribe({
605
+ async message(message) {
606
+ const principal = matchOwnerHandshake({ message, phrase, account, tenant });
607
+ if (principal !== undefined)
608
+ finish(principal);
609
+ },
610
+ async cardAction() { },
611
+ reconnecting() { },
612
+ reconnected() { },
613
+ error() { fail(new Error('lark-channel setup: Lark WebSocket disconnected during owner discovery')); },
614
+ });
615
+ try {
616
+ await transport.connect();
617
+ process.stdout.write(`\n连接成功。现在请在飞书中私聊该机器人,原样发送:\n\n ${phrase}\n\n`);
618
+ return await owner;
619
+ }
620
+ finally {
621
+ clearTimeout(timer);
622
+ unsubscribe();
623
+ await transport.disconnect().catch(() => { });
624
+ }
625
+ }
626
+ const defaultProfileSetupLockOptions = {
627
+ timeoutMs: 30_000,
628
+ pollMs: 100,
629
+ staleMs: 120_000,
630
+ heartbeatMs: 10_000,
631
+ };
632
+ function fileSystemErrorCode(error) {
633
+ return typeof error === 'object' && error !== null && 'code' in error
634
+ ? String(error.code)
635
+ : undefined;
636
+ }
637
+ function normalizedProfileSetupLockOptions(options) {
638
+ const normalized = { ...defaultProfileSetupLockOptions, ...options };
639
+ if (!Number.isSafeInteger(normalized.timeoutMs) || normalized.timeoutMs < 1 || normalized.timeoutMs > 300_000) {
640
+ throw new Error('lark-channel setup: invalid profile lock timeout');
641
+ }
642
+ if (!Number.isSafeInteger(normalized.pollMs) || normalized.pollMs < 1 || normalized.pollMs > 5_000) {
643
+ throw new Error('lark-channel setup: invalid profile lock poll interval');
644
+ }
645
+ if (!Number.isSafeInteger(normalized.staleMs) || normalized.staleMs < 100 || normalized.staleMs > 3_600_000) {
646
+ throw new Error('lark-channel setup: invalid profile lock stale interval');
647
+ }
648
+ if (!Number.isSafeInteger(normalized.heartbeatMs) || normalized.heartbeatMs < 10
649
+ || normalized.heartbeatMs >= normalized.staleMs) {
650
+ throw new Error('lark-channel setup: invalid profile lock heartbeat interval');
651
+ }
652
+ return normalized;
653
+ }
654
+ async function delay(ms) {
655
+ await new Promise(resolve => setTimeout(resolve, ms));
656
+ }
657
+ function sqliteDatabaseIsBusy(error) {
658
+ return typeof error === 'object' && error !== null
659
+ && (('errcode' in error && error.errcode === 5)
660
+ || ('errstr' in error && error.errstr === 'database is locked'));
661
+ }
662
+ async function acquireProfileSetupLock(patchPath, options) {
663
+ const lockPath = `${normalizeSetupResourcePath(patchPath)}.lark-setup-lock.sqlite`;
664
+ await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
665
+ const seed = await open(lockPath, 'a', 0o600);
666
+ await seed.close();
667
+ await chmod(lockPath, 0o600);
668
+ const deadline = Date.now() + options.timeoutMs;
669
+ for (;;) {
670
+ const database = new DatabaseSync(lockPath);
671
+ try {
672
+ database.exec('PRAGMA busy_timeout = 0');
673
+ database.exec('BEGIN IMMEDIATE');
674
+ }
675
+ catch (error) {
676
+ database.close();
677
+ if (!sqliteDatabaseIsBusy(error))
678
+ throw error;
679
+ if (Date.now() >= deadline) {
680
+ throw new Error('lark-channel setup: another setup is already running for this profile');
681
+ }
682
+ await delay(Math.min(options.pollMs, Math.max(1, deadline - Date.now())));
683
+ continue;
684
+ }
685
+ let released = false;
686
+ return async () => {
687
+ if (released)
688
+ return;
689
+ released = true;
690
+ try {
691
+ database.exec('COMMIT');
692
+ }
693
+ finally {
694
+ database.close();
695
+ }
696
+ };
697
+ }
698
+ }
699
+ /**
700
+ * Serializes every profile mutation through a SQLite write transaction. The
701
+ * OS releases its file lock if a process is killed, avoiding stale-lock and
702
+ * rename ABA races while preserving bounded non-blocking acquisition.
703
+ */
704
+ export async function withProfileSetupLock(patchPath, operation, options = {}) {
705
+ if (!isAbsolute(patchPath) && !win32.isAbsolute(patchPath)) {
706
+ throw new Error('lark-channel setup: profile patch path must be absolute');
707
+ }
708
+ const release = await acquireProfileSetupLock(patchPath, normalizedProfileSetupLockOptions(options));
709
+ let outcome;
710
+ try {
711
+ outcome = { ok: true, value: await operation() };
712
+ }
713
+ catch (error) {
714
+ outcome = { ok: false, error };
715
+ }
716
+ let releaseError;
717
+ try {
718
+ await release();
719
+ }
720
+ catch (error) {
721
+ releaseError = error;
722
+ }
723
+ if (!outcome.ok)
724
+ throw outcome.error;
725
+ if (releaseError !== undefined)
726
+ throw releaseError;
727
+ return outcome.value;
728
+ }
729
+ async function atomicWrite(path, value) {
730
+ const temporary = join(dirname(path), `.cordis.patch.yml.lark-setup-${process.pid}-${randomBytes(8).toString('hex')}`);
731
+ try {
732
+ const file = await open(temporary, 'w', 0o600);
733
+ try {
734
+ await file.writeFile(value, 'utf8');
735
+ await file.sync();
736
+ }
737
+ finally {
738
+ await file.close();
739
+ }
740
+ await rename(temporary, path);
741
+ await syncDirectory(dirname(path));
742
+ }
743
+ catch (error) {
744
+ await rm(temporary, { force: true });
745
+ throw error;
746
+ }
747
+ }
748
+ async function syncDirectory(path) {
749
+ let directory;
750
+ try {
751
+ directory = await open(path, 'r');
752
+ await directory.sync();
753
+ }
754
+ catch (error) {
755
+ if (process.platform !== 'win32')
756
+ throw error;
757
+ }
758
+ finally {
759
+ await directory?.close();
760
+ }
761
+ }
762
+ function setupJournalPath(patchPath) {
763
+ return `${patchPath}.lark-setup.journal.json`;
764
+ }
765
+ function textSha256(value) {
766
+ return createHash('sha256').update(value).digest('hex');
767
+ }
768
+ function escapeRegularExpression(value) {
769
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&');
770
+ }
771
+ function credentialLocatorEquals(left, right) {
772
+ if (left.provider !== right.provider)
773
+ return false;
774
+ if (left.provider === 'windows-dpapi' && right.provider === 'windows-dpapi')
775
+ return left.path === right.path;
776
+ if (left.provider === 'windows-dpapi' || right.provider === 'windows-dpapi')
777
+ return false;
778
+ return left.service === right.service && left.account === right.account;
779
+ }
780
+ function validateJournalCredentialLocator(input) {
781
+ if (typeof input.locator !== 'object' || input.locator === null || !('provider' in input.locator)) {
782
+ throw new Error('lark-channel setup: setup journal credential locator is invalid');
783
+ }
784
+ const locator = input.locator;
785
+ if (locator.provider === 'macos-keychain' || locator.provider === 'linux-secret-service') {
786
+ if (typeof locator.service !== 'string' || typeof locator.account !== 'string'
787
+ || !setupKeyPattern.test(locator.account)
788
+ || (input.stagedAccount !== undefined && locator.account !== input.stagedAccount)) {
789
+ throw new Error('lark-channel setup: setup journal credential locator is invalid');
790
+ }
791
+ const base = `dsh/lark/${input.profile}/${locator.account}`;
792
+ const versionPrefix = `${base}/versions/`;
793
+ const version = locator.service.startsWith(versionPrefix) ? locator.service.slice(versionPrefix.length) : undefined;
794
+ if (input.stagedAccount === undefined) {
795
+ if (locator.service !== base && (version === undefined || !credentialVersionPattern.test(version))) {
796
+ throw new Error('lark-channel setup: setup journal credential locator is not setup-managed');
797
+ }
798
+ }
799
+ else if (version === undefined || !credentialVersionPattern.test(version)) {
800
+ throw new Error('lark-channel setup: staged journal credential must use a versioned locator');
801
+ }
802
+ return { provider: locator.provider, service: locator.service, account: locator.account };
803
+ }
804
+ if (locator.provider === 'windows-dpapi') {
805
+ if (typeof locator.path !== 'string') {
806
+ throw new Error('lark-channel setup: setup journal DPAPI locator is invalid');
807
+ }
808
+ const directory = platformJoin(input.dshHome, 'credentials-keychain');
809
+ if (dirname(locator.path) !== directory) {
810
+ throw new Error('lark-channel setup: setup journal DPAPI locator is outside the credential directory');
811
+ }
812
+ const name = locator.path.slice(directory.length + 1);
813
+ const profile = escapeRegularExpression(input.profile);
814
+ const account = input.stagedAccount === undefined
815
+ ? '[A-Za-z0-9][A-Za-z0-9._-]{0,63}'
816
+ : escapeRegularExpression(input.stagedAccount);
817
+ const versioned = new RegExp(`^lark-${profile}-${account}-[0-9a-f]{32}\\.clixml$`, 'u');
818
+ const legacy = new RegExp(`^lark-${profile}-${account}\\.clixml$`, 'u');
819
+ if (!versioned.test(name) && (input.stagedAccount !== undefined || !legacy.test(name))) {
820
+ throw new Error('lark-channel setup: setup journal DPAPI locator is not setup-managed');
821
+ }
822
+ return { provider: locator.provider, path: locator.path };
823
+ }
824
+ throw new Error('lark-channel setup: setup journal credential provider is invalid');
825
+ }
826
+ function normalizeSetupJournalInput(input) {
827
+ if ((!isAbsolute(input.patchPath) && !win32.isAbsolute(input.patchPath))
828
+ || (!isAbsolute(input.dshHome) && !win32.isAbsolute(input.dshHome))
829
+ || !setupKeyPattern.test(input.profile)
830
+ || input.patchPath !== platformJoin(input.dshHome, 'profiles', input.profile, 'cordis.patch.yml')) {
831
+ throw new Error('lark-channel setup: setup journal profile identity is invalid');
832
+ }
833
+ if (!['full', 'refresh'].includes(input.operation)
834
+ || !['staging', 'candidate', 'validated', 'paired', 'aborting'].includes(input.phase)
835
+ || typeof input.originalPatch !== 'string'
836
+ || input.originalPatch.length > 16 * 1024 * 1024
837
+ || (input.updatedPatch !== undefined
838
+ && (typeof input.updatedPatch !== 'string' || input.updatedPatch.length > 16 * 1024 * 1024))) {
839
+ throw new Error('lark-channel setup: setup journal payload is invalid');
840
+ }
841
+ const transactionId = input.transactionId ?? randomBytes(16).toString('hex');
842
+ if (!credentialVersionPattern.test(transactionId)) {
843
+ throw new Error('lark-channel setup: setup journal transaction id is invalid');
844
+ }
845
+ if (input.operation === 'refresh') {
846
+ if (input.phase === 'staging' || input.phase === 'paired'
847
+ || input.updatedPatch === undefined
848
+ || input.databasePath !== undefined
849
+ || input.account !== undefined
850
+ || input.principal !== undefined
851
+ || input.stagedCredential !== undefined
852
+ || input.previousCredential !== undefined
853
+ || input.installService) {
854
+ throw new Error('lark-channel setup: refresh journal payload is invalid');
855
+ }
856
+ return { ...input, transactionId };
857
+ }
858
+ if (typeof input.databasePath !== 'string'
859
+ || (!isAbsolute(input.databasePath) && !win32.isAbsolute(input.databasePath))
860
+ || input.stagedCredential === undefined) {
861
+ throw new Error('lark-channel setup: full setup journal payload is incomplete');
862
+ }
863
+ const stagedAccount = input.account ?? input.principal?.account;
864
+ if (stagedAccount === undefined || !setupKeyPattern.test(stagedAccount)
865
+ || (input.principal !== undefined && input.principal.account !== stagedAccount)) {
866
+ throw new Error('lark-channel setup: full setup journal account is invalid');
867
+ }
868
+ const stagedCredential = validateJournalCredentialLocator({
869
+ locator: input.stagedCredential,
870
+ dshHome: input.dshHome,
871
+ profile: input.profile,
872
+ ...(stagedAccount === undefined ? {} : { stagedAccount }),
873
+ });
874
+ const previousCredential = input.previousCredential === undefined
875
+ ? undefined
876
+ : validateJournalCredentialLocator({
877
+ locator: input.previousCredential,
878
+ dshHome: input.dshHome,
879
+ profile: input.profile,
880
+ });
881
+ if (input.phase === 'staging') {
882
+ if (input.updatedPatch !== undefined || input.principal !== undefined) {
883
+ throw new Error('lark-channel setup: staging journal cannot contain a candidate owner');
884
+ }
885
+ }
886
+ else {
887
+ const principal = input.principal;
888
+ if (input.updatedPatch === undefined
889
+ || principal?.channel !== 'lark'
890
+ || !setupKeyPattern.test(principal.account)
891
+ || !setupKeyPattern.test(principal.tenant)
892
+ || typeof principal.user !== 'string'
893
+ || principal.user.length < 1
894
+ || principal.user.length > 256) {
895
+ throw new Error('lark-channel setup: setup journal owner is invalid');
896
+ }
897
+ }
898
+ return {
899
+ ...input,
900
+ account: stagedAccount,
901
+ transactionId,
902
+ stagedCredential,
903
+ ...(previousCredential === undefined ? {} : { previousCredential }),
904
+ };
905
+ }
906
+ /** Persists one crash-recovery phase. Callers must already hold the profile lock. */
907
+ export async function persistLarkSetupJournal(input) {
908
+ const normalized = normalizeSetupJournalInput(input);
909
+ const stored = {
910
+ ...normalized,
911
+ version: 1,
912
+ transactionId: normalized.transactionId,
913
+ originalSha256: textSha256(normalized.originalPatch),
914
+ ...(normalized.updatedPatch === undefined ? {} : { updatedSha256: textSha256(normalized.updatedPatch) }),
915
+ };
916
+ await atomicWrite(setupJournalPath(normalized.patchPath), `${JSON.stringify(stored)}\n`);
917
+ }
918
+ async function readLarkSetupJournal(input) {
919
+ let text;
920
+ try {
921
+ text = await readFile(setupJournalPath(input.patchPath), 'utf8');
922
+ }
923
+ catch (error) {
924
+ if (fileSystemErrorCode(error) === 'ENOENT')
925
+ return undefined;
926
+ throw error;
927
+ }
928
+ let value;
929
+ try {
930
+ value = JSON.parse(text);
931
+ }
932
+ catch (error) {
933
+ throw new Error('lark-channel setup: setup journal is not valid JSON', { cause: error });
934
+ }
935
+ if (typeof value !== 'object' || value === null) {
936
+ throw new Error('lark-channel setup: setup journal is invalid');
937
+ }
938
+ const stored = value;
939
+ if (stored.version !== 1
940
+ || stored.patchPath !== input.patchPath
941
+ || stored.dshHome !== input.dshHome
942
+ || stored.profile !== input.profile
943
+ || typeof stored.originalSha256 !== 'string'
944
+ || stored.originalSha256 !== textSha256(String(stored.originalPatch ?? ''))
945
+ || (stored.updatedPatch !== undefined
946
+ && stored.updatedSha256 !== textSha256(String(stored.updatedPatch)))) {
947
+ throw new Error('lark-channel setup: setup journal identity or checksum is invalid');
948
+ }
949
+ return {
950
+ ...normalizeSetupJournalInput(stored),
951
+ version: 1,
952
+ transactionId: stored.transactionId,
953
+ originalSha256: stored.originalSha256,
954
+ ...(stored.updatedSha256 === undefined ? {} : { updatedSha256: stored.updatedSha256 }),
955
+ };
956
+ }
957
+ async function clearLarkSetupJournal(patchPath) {
958
+ try {
959
+ await unlink(setupJournalPath(patchPath));
960
+ }
961
+ catch (error) {
962
+ if (fileSystemErrorCode(error) !== 'ENOENT')
963
+ throw error;
964
+ }
965
+ await syncDirectory(dirname(patchPath));
966
+ }
967
+ function journalPersistInput(journal, phase) {
968
+ return {
969
+ patchPath: journal.patchPath,
970
+ dshHome: journal.dshHome,
971
+ profile: journal.profile,
972
+ operation: journal.operation,
973
+ phase,
974
+ originalPatch: journal.originalPatch,
975
+ ...(journal.updatedPatch === undefined ? {} : { updatedPatch: journal.updatedPatch }),
976
+ ...(journal.databasePath === undefined ? {} : { databasePath: journal.databasePath }),
977
+ ...(journal.account === undefined ? {} : { account: journal.account }),
978
+ ...(journal.principal === undefined ? {} : { principal: journal.principal }),
979
+ ...(journal.stagedCredential === undefined ? {} : { stagedCredential: journal.stagedCredential }),
980
+ ...(journal.previousCredential === undefined ? {} : { previousCredential: journal.previousCredential }),
981
+ installService: journal.installService,
982
+ transactionId: journal.transactionId,
983
+ };
984
+ }
985
+ function normalizeSetupResourcePath(path) {
986
+ try {
987
+ return realpathSync(path);
988
+ }
989
+ catch {
990
+ const windowsPath = !isAbsolute(path) && win32.isAbsolute(path);
991
+ const parent = windowsPath ? win32.dirname(path) : dirname(path);
992
+ if (parent === path)
993
+ return windowsPath ? win32.normalize(path) : join(path);
994
+ const name = windowsPath ? win32.basename(path) : basename(path);
995
+ return platformJoin(normalizeSetupResourcePath(parent), name);
996
+ }
997
+ }
998
+ function dshEffectiveDataPath(value, dshHome, field) {
999
+ const expression = /^dshHomePath\((['"])([^'"\\]+)\1\)$/u;
1000
+ if (typeof value === 'string') {
1001
+ const matched = expression.exec(value);
1002
+ if (matched !== null) {
1003
+ const relative = matched[2];
1004
+ if (relative.split(/[\\/]/u).some(segment => segment === '' || segment === '.' || segment === '..')) {
1005
+ throw new Error(`lark-channel setup: ${field} has an unsafe dshHomePath expression`);
1006
+ }
1007
+ return platformJoin(dshHome, ...relative.split(/[\\/]/u));
1008
+ }
1009
+ }
1010
+ if (typeof value !== 'string' || (!isAbsolute(value) && !win32.isAbsolute(value))) {
1011
+ throw new Error(`lark-channel setup: ${field} must be an absolute path or dshHomePath expression`);
1012
+ }
1013
+ return value;
1014
+ }
1015
+ /** Resolves the exact Delivery database used by the composed profile. */
1016
+ export function deliveryDatabasePathFromEffectiveProfile(effectiveProfile, dshHome) {
1017
+ const document = parseDocument(effectiveProfile, { uniqueKeys: true });
1018
+ if (document.errors.length > 0 || !isSeq(document.contents)) {
1019
+ throw new Error('lark-channel setup: effective profile must be a valid YAML sequence');
1020
+ }
1021
+ const deliveries = document.contents.items.filter(item => isMap(item)
1022
+ && item.get('id') === 'dsh-enhanced-assistant-delivery');
1023
+ const delivery = deliveries[0];
1024
+ if (deliveries.length !== 1
1025
+ || delivery?.get('name') !== '@dsh-enhanced/assistant-delivery'
1026
+ || !setupRowIsEnabled(delivery)) {
1027
+ throw new Error('lark-channel setup: effective profile has no enabled assistant-delivery row');
1028
+ }
1029
+ const config = delivery.get('config', true);
1030
+ if (!isMap(config))
1031
+ throw new Error('lark-channel setup: effective assistant-delivery config must be a mapping');
1032
+ return dshEffectiveDataPath(config.get('databasePath'), dshHome, 'assistant-delivery databasePath');
1033
+ }
1034
+ function effectiveProfileHasEnabledLarkChannel(effectiveProfile, profile) {
1035
+ const document = parseDocument(effectiveProfile, { uniqueKeys: true });
1036
+ if (document.errors.length > 0 || !isSeq(document.contents)) {
1037
+ throw new Error(`lark-channel setup: cannot verify effective Lark ownership in profile ${profile}`);
1038
+ }
1039
+ const rows = document.contents.items.filter(item => isMap(item)
1040
+ && item.get('id') === 'dsh-enhanced-lark-channel');
1041
+ if (rows.length === 0)
1042
+ return false;
1043
+ const lark = rows[0];
1044
+ if (rows.length !== 1 || lark.get('name') !== '@dsh-enhanced/lark-channel') {
1045
+ throw new Error(`lark-channel setup: profile ${profile} has an invalid effective Lark package row`);
1046
+ }
1047
+ if (!setupRowIsEnabled(lark))
1048
+ return false;
1049
+ const config = lark.get('config', true);
1050
+ if (config === undefined)
1051
+ return true;
1052
+ if (!isMap(config))
1053
+ throw new Error(`lark-channel setup: profile ${profile} has an invalid Lark config`);
1054
+ return config.get('enabled') !== false;
1055
+ }
1056
+ function externalPrincipalKeyId(principal) {
1057
+ return [principal.channel, principal.account, principal.tenant, principal.user]
1058
+ .map(component => encodeURIComponent(component))
1059
+ .join('/');
1060
+ }
1061
+ function exactYamlMapKeys(node, expected) {
1062
+ const value = node.toJSON();
1063
+ return Object.keys(value).sort().join('\0') === [...expected].sort().join('\0');
1064
+ }
1065
+ const managedPolicyRulePrefixes = [
1066
+ 'lark-channel-credential-',
1067
+ 'lark-owner-approval-',
1068
+ 'lark-owner-ingress-',
1069
+ 'lark-owner-reply-',
1070
+ 'lark-owner-capability-',
1071
+ 'lark-owner-tool-',
1072
+ 'dsh-enhanced-foreground-capability-',
1073
+ 'dsh-enhanced-foreground-tool-',
1074
+ 'lark-foreground-capability-',
1075
+ 'lark-foreground-tool-',
1076
+ ];
1077
+ function stableJsonValue(value) {
1078
+ if (Array.isArray(value))
1079
+ return value.map(stableJsonValue);
1080
+ if (typeof value !== 'object' || value === null)
1081
+ return value;
1082
+ return Object.fromEntries(Object.entries(value)
1083
+ .sort(([left], [right]) => left.localeCompare(right))
1084
+ .map(([key, entry]) => [key, stableJsonValue(entry)]));
1085
+ }
1086
+ function managedPolicyFingerprint(rules) {
1087
+ if (!isSeq(rules))
1088
+ throw new Error('profile does not have assistant policy rules');
1089
+ return JSON.stringify(rules.items
1090
+ .filter(item => {
1091
+ if (!isMap(item))
1092
+ return false;
1093
+ const id = item.get('id');
1094
+ return typeof id === 'string' && managedPolicyRulePrefixes.some(prefix => id.startsWith(prefix));
1095
+ })
1096
+ .map(item => stableJsonValue(item.toJSON()))
1097
+ .sort((left, right) => JSON.stringify(left).localeCompare(JSON.stringify(right))));
1098
+ }
1099
+ function assertEffectiveManagedPolicyMatchesCandidate(input) {
1100
+ const fingerprint = (profilePatch, effective) => {
1101
+ const document = parseDocument(profilePatch, { uniqueKeys: true });
1102
+ if (document.errors.length > 0 || !isSeq(document.contents)) {
1103
+ throw new Error('profile is not a valid YAML sequence');
1104
+ }
1105
+ const rows = document.contents.items.filter(item => isMap(item)
1106
+ && item.get('id') === 'dsh-enhanced-personal-assistant');
1107
+ const personal = rows[0];
1108
+ if (rows.length !== 1 || personal === undefined) {
1109
+ throw new Error('profile does not have the expected enabled personal-assistant package');
1110
+ }
1111
+ if (!setupRowIsEnabled(personal)
1112
+ || (effective && personal.get('name') !== '@dsh-enhanced/personal-assistant')) {
1113
+ throw new Error('profile does not have the expected enabled personal-assistant package');
1114
+ }
1115
+ const config = personal.get('config', true);
1116
+ const policy = isMap(config) ? config.get('assistantPolicy', true) : undefined;
1117
+ const rules = isMap(policy) ? policy.get('rules', true) : undefined;
1118
+ if (rules === undefined)
1119
+ throw new Error('profile does not have assistant policy rules');
1120
+ return managedPolicyFingerprint(rules);
1121
+ };
1122
+ if (fingerprint(input.candidatePatch, false) !== fingerprint(input.effectiveProfile, true)) {
1123
+ throw new Error('lark-channel setup: effective Agent policy does not match the validated candidate');
1124
+ }
1125
+ }
1126
+ function uniqueProfileRow(rows, id, label) {
1127
+ const matches = rows.items.filter(item => isMap(item) && item.get('id') === id);
1128
+ if (matches.length > 1)
1129
+ throw new Error(`lark-channel setup: ${label} has duplicate ${id} rows`);
1130
+ return matches[0];
1131
+ }
1132
+ function requireEffectivePackageRow(rows, id, packageName, optional = false) {
1133
+ const row = uniqueProfileRow(rows, id, 'effective profile');
1134
+ if (row === undefined) {
1135
+ if (optional)
1136
+ return undefined;
1137
+ throw new Error(`lark-channel setup: effective profile has no ${id} row`);
1138
+ }
1139
+ if (row.get('name') !== packageName) {
1140
+ throw new Error(`lark-channel setup: effective ${id} row does not mount ${packageName}`);
1141
+ }
1142
+ if (!setupRowIsEnabled(row)) {
1143
+ if (optional)
1144
+ return undefined;
1145
+ throw new Error(`lark-channel setup: effective ${id} row is disabled`);
1146
+ }
1147
+ return row;
1148
+ }
1149
+ function policyRulesOf(row, label) {
1150
+ const config = row.get('config', true);
1151
+ const policy = isMap(config) ? config.get('assistantPolicy', true) : undefined;
1152
+ const rules = isMap(policy) ? policy.get('rules', true) : undefined;
1153
+ if (!isSeq(rules))
1154
+ throw new Error(`lark-channel setup: ${label} has no assistant policy rules`);
1155
+ return rules;
1156
+ }
1157
+ function replaceProfileRow(rows, id, replacement) {
1158
+ for (let index = rows.items.length - 1; index >= 0; index -= 1) {
1159
+ const item = rows.items[index];
1160
+ if (isMap(item) && item.get('id') === id)
1161
+ rows.items.splice(index, 1);
1162
+ }
1163
+ if (replacement !== undefined)
1164
+ rows.add(replacement.clone());
1165
+ }
1166
+ /**
1167
+ * Computes only the policy delta from composed Lark semantics. The temporary
1168
+ * working profile receives effective channel/account/owner and Delivery agent
1169
+ * identity, while the returned patch retains raw channel and credential rows.
1170
+ * A policy refresh therefore cannot pin an inherited channel or delete a
1171
+ * credential override which it does not own.
1172
+ */
1173
+ function refreshLarkAgentPolicyFromEffective(input) {
1174
+ // Effective rows determine active channel semantics, but may never conceal
1175
+ // malformed raw overrides which remain on disk after this policy-only edit.
1176
+ assertRawManagedProfileIntegrity(input.profilePatch);
1177
+ const working = parseDocument(input.profilePatch, { uniqueKeys: true });
1178
+ const effective = parseDocument(input.effectiveProfile, { uniqueKeys: true });
1179
+ if (working.errors.length > 0 || !isSeq(working.contents)) {
1180
+ throw new Error('lark-channel setup: policy refresh candidate must be a valid YAML sequence');
1181
+ }
1182
+ if (effective.errors.length > 0 || !isSeq(effective.contents)) {
1183
+ throw new Error('lark-channel setup: effective policy refresh profile must be a valid YAML sequence');
1184
+ }
1185
+ const workingPersonal = uniqueProfileRow(working.contents, 'dsh-enhanced-personal-assistant', 'policy refresh candidate');
1186
+ if (workingPersonal === undefined) {
1187
+ throw new Error('lark-channel setup: policy refresh candidate has no personal-assistant row');
1188
+ }
1189
+ const effectivePersonal = requireEffectivePackageRow(effective.contents, 'dsh-enhanced-personal-assistant', '@dsh-enhanced/personal-assistant');
1190
+ const workingPolicy = workingPersonal.get('config', true);
1191
+ const assistantPolicy = isMap(workingPolicy)
1192
+ ? workingPolicy.get('assistantPolicy', true)
1193
+ : undefined;
1194
+ if (!isMap(assistantPolicy)) {
1195
+ throw new Error('lark-channel setup: policy refresh candidate has no assistantPolicy config');
1196
+ }
1197
+ assistantPolicy.set('rules', policyRulesOf(effectivePersonal, 'effective personal-assistant').clone());
1198
+ const effectiveDelivery = requireEffectivePackageRow(effective.contents, 'dsh-enhanced-assistant-delivery', '@dsh-enhanced/assistant-delivery');
1199
+ replaceProfileRow(working.contents, 'dsh-enhanced-assistant-delivery', effectiveDelivery);
1200
+ const effectiveLark = requireEffectivePackageRow(effective.contents, 'dsh-enhanced-lark-channel', '@dsh-enhanced/lark-channel', true);
1201
+ const effectiveLarkConfig = effectiveLark?.get('config', true);
1202
+ const channelEnabled = isMap(effectiveLarkConfig) && effectiveLarkConfig.get('enabled') === true;
1203
+ replaceProfileRow(working.contents, 'dsh-enhanced-lark-channel', channelEnabled ? effectiveLark : undefined);
1204
+ const refreshed = refreshLarkAgentPolicyPatch({
1205
+ profilePatch: working.toString({ lineWidth: 0 }),
1206
+ dshHome: input.dshHome,
1207
+ agentTools: input.agentTools,
1208
+ ...(input.account === undefined ? {} : { account: input.account }),
1209
+ });
1210
+ const refreshedDocument = parseDocument(refreshed, { uniqueKeys: true });
1211
+ const result = parseDocument(input.profilePatch, { uniqueKeys: true });
1212
+ if (!isSeq(refreshedDocument.contents) || !isSeq(result.contents)) {
1213
+ throw new Error('lark-channel setup: policy refresh produced an invalid profile sequence');
1214
+ }
1215
+ const refreshedPersonal = uniqueProfileRow(refreshedDocument.contents, 'dsh-enhanced-personal-assistant', 'refreshed profile');
1216
+ const resultPersonal = uniqueProfileRow(result.contents, 'dsh-enhanced-personal-assistant', 'policy refresh result');
1217
+ const resultConfig = resultPersonal.get('config', true);
1218
+ const resultPolicy = isMap(resultConfig) ? resultConfig.get('assistantPolicy', true) : undefined;
1219
+ if (!isMap(resultPolicy))
1220
+ throw new Error('lark-channel setup: policy refresh result has no assistantPolicy config');
1221
+ resultPolicy.set('rules', policyRulesOf(refreshedPersonal, 'refreshed personal-assistant').clone());
1222
+ return result.toString({ lineWidth: 0 });
1223
+ }
1224
+ function inspectLarkOwnerBinding(input) {
1225
+ const document = parseDocument(input.profilePatch, { uniqueKeys: true });
1226
+ if (document.errors.length > 0 || !isSeq(document.contents)) {
1227
+ throw new Error('profile is not a valid YAML sequence');
1228
+ }
1229
+ const larkRows = document.contents.items
1230
+ .filter(item => isMap(item) && item.get('id') === 'dsh-enhanced-lark-channel');
1231
+ if (larkRows.length !== 1 || !setupRowIsEnabled(larkRows[0])) {
1232
+ throw new Error('profile does not have exactly one enabled Lark channel row');
1233
+ }
1234
+ if (input.effective && larkRows[0].get('name') !== '@dsh-enhanced/lark-channel') {
1235
+ throw new Error('effective Lark row does not mount the expected package');
1236
+ }
1237
+ const channel = larkRows[0].get('config', true);
1238
+ if (!isMap(channel) || channel.get('enabled') !== true) {
1239
+ throw new Error('profile does not have an enabled Lark channel config');
1240
+ }
1241
+ const account = channel.get('account');
1242
+ const tenant = channel.get('tenant');
1243
+ const appId = channel.get('appId');
1244
+ const domain = channel.get('domain');
1245
+ const credentialHandle = channel.get('credentialHandle');
1246
+ const credentialPurpose = channel.get('credentialPurpose');
1247
+ const credentialLeaseMs = channel.get('credentialLeaseMs');
1248
+ if (account !== input.principal.account
1249
+ || tenant !== input.principal.tenant
1250
+ || typeof appId !== 'string'
1251
+ || (domain !== 'feishu' && domain !== 'lark')
1252
+ || credentialHandle !== `lark-app-secret-${input.principal.account}`
1253
+ || credentialPurpose !== 'connect'
1254
+ || credentialLeaseMs !== 86_400_000) {
1255
+ throw new Error('Lark channel config does not match the setup owner');
1256
+ }
1257
+ const locator = findManagedLarkCredentialLocator({
1258
+ profilePatch: input.profilePatch,
1259
+ dshHome: input.dshHome,
1260
+ profile: input.profile,
1261
+ });
1262
+ if (locator === undefined || !credentialLocatorEquals(locator, input.stagedCredential)) {
1263
+ throw new Error('Lark channel credential does not match the staged setup credential');
1264
+ }
1265
+ const credentialRows = document.contents.items
1266
+ .filter(item => isMap(item) && item.get('id') === 'dsh-enhanced-credentials-keychain');
1267
+ if (credentialRows.length !== 1
1268
+ || !setupRowIsEnabled(credentialRows[0])
1269
+ || (input.effective && credentialRows[0].get('name') !== '@dsh-enhanced/credentials-keychain')) {
1270
+ throw new Error('profile does not have the expected enabled credentials-keychain package');
1271
+ }
1272
+ const personalRows = document.contents.items
1273
+ .filter(item => isMap(item) && item.get('id') === 'dsh-enhanced-personal-assistant');
1274
+ if (personalRows.length !== 1 || !setupRowIsEnabled(personalRows[0])) {
1275
+ throw new Error('profile does not have exactly one enabled personal-assistant row');
1276
+ }
1277
+ if (input.effective && personalRows[0].get('name') !== '@dsh-enhanced/personal-assistant') {
1278
+ throw new Error('effective personal-assistant row does not mount the expected package');
1279
+ }
1280
+ const personal = personalRows[0].get('config', true);
1281
+ const policy = isMap(personal) ? personal.get('assistantPolicy', true) : undefined;
1282
+ const rules = isMap(policy) ? policy.get('rules', true) : undefined;
1283
+ if (!isSeq(rules))
1284
+ throw new Error('profile does not have assistant policy rules');
1285
+ const ownerRuleId = `lark-owner-ingress-${input.principal.account}`;
1286
+ const ownerRules = rules.items.filter(item => isMap(item) && item.get('id') === ownerRuleId);
1287
+ const ownerRule = ownerRules[0];
1288
+ const subject = ownerRule?.get('subject', true);
1289
+ const resource = ownerRule?.get('resource', true);
1290
+ const context = ownerRule?.get('context', true);
1291
+ if (ownerRules.length !== 1
1292
+ || ownerRule?.get('effect') !== 'allow'
1293
+ || !isMap(subject)
1294
+ || !exactYamlMapKeys(subject, ['kind', 'id'])
1295
+ || subject.get('kind') !== 'external'
1296
+ || subject.get('id') !== externalPrincipalKeyId(input.principal)
1297
+ || !exactYamlStringSequence(ownerRule.get('actions', true), ['approval.decide', 'ingest'])
1298
+ || !isMap(resource)
1299
+ || !exactYamlMapKeys(resource, ['kind', 'id'])
1300
+ || resource.get('kind') !== 'message'
1301
+ || resource.get('id') !== '*'
1302
+ || !isMap(context)
1303
+ || !exactYamlMapKeys(context, ['initiators'])
1304
+ || !exactYamlStringSequence(context.get('initiators', true), ['external'])) {
1305
+ throw new Error('assistant policy does not contain the exact setup owner ingress');
1306
+ }
1307
+ return {
1308
+ account,
1309
+ tenant,
1310
+ appId,
1311
+ domain,
1312
+ credentialHandle,
1313
+ credentialPurpose,
1314
+ credentialLeaseMs,
1315
+ managedPolicyRules: managedPolicyFingerprint(rules),
1316
+ };
1317
+ }
1318
+ function assertEffectiveLarkOwnerBinding(input) {
1319
+ let candidate;
1320
+ try {
1321
+ candidate = inspectLarkOwnerBinding({
1322
+ profilePatch: input.candidatePatch,
1323
+ dshHome: input.dshHome,
1324
+ profile: input.profile,
1325
+ principal: input.principal,
1326
+ stagedCredential: input.stagedCredential,
1327
+ effective: false,
1328
+ });
1329
+ }
1330
+ catch (error) {
1331
+ const detail = error instanceof Error ? `: ${error.message}` : '';
1332
+ throw new Error(`lark-channel setup: candidate profile does not represent the setup owner${detail}`, { cause: error });
1333
+ }
1334
+ let effective;
1335
+ try {
1336
+ effective = inspectLarkOwnerBinding({
1337
+ profilePatch: input.effectiveProfile,
1338
+ dshHome: input.dshHome,
1339
+ profile: input.profile,
1340
+ principal: input.principal,
1341
+ stagedCredential: input.stagedCredential,
1342
+ effective: true,
1343
+ });
1344
+ }
1345
+ catch (error) {
1346
+ const suffix = input.context === 'journal' ? 'journal owner' : 'validated setup owner';
1347
+ throw new Error(`lark-channel setup: effective Lark binding no longer represents the ${suffix}`, { cause: error });
1348
+ }
1349
+ if (JSON.stringify(effective) !== JSON.stringify(candidate)) {
1350
+ const suffix = input.context === 'journal' ? 'journal owner' : 'validated setup owner';
1351
+ throw new Error(`lark-channel setup: effective Lark binding no longer represents the ${suffix}`);
1352
+ }
1353
+ }
1354
+ async function assertNoOtherLarkProfileSharesDelivery(input) {
1355
+ let entries;
1356
+ try {
1357
+ entries = await readdir(platformJoin(input.dshHome, 'profiles'), { withFileTypes: true });
1358
+ }
1359
+ catch (error) {
1360
+ if (fileSystemErrorCode(error) === 'ENOENT')
1361
+ return;
1362
+ throw error;
1363
+ }
1364
+ const databasePath = normalizeSetupResourcePath(input.databasePath);
1365
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
1366
+ if (entry.name === input.profile
1367
+ || (!entry.isDirectory() && !entry.isSymbolicLink())
1368
+ || !setupKeyPattern.test(entry.name))
1369
+ continue;
1370
+ const patchPath = platformJoin(input.dshHome, 'profiles', entry.name, 'cordis.patch.yml');
1371
+ // A profile can inherit Lark entirely from a lower bundle while its raw
1372
+ // override contains no channel row. Activation and database identity must
1373
+ // therefore come from effective config whenever that profile composes.
1374
+ try {
1375
+ await readFile(patchPath, 'utf8');
1376
+ }
1377
+ catch (error) {
1378
+ if (fileSystemErrorCode(error) === 'ENOENT')
1379
+ continue;
1380
+ throw error;
1381
+ }
1382
+ let effective;
1383
+ try {
1384
+ effective = await input.readEffectiveProfile(entry.name);
1385
+ }
1386
+ catch (error) {
1387
+ // Raw absence cannot prove absence: Lark may be inherited from a lower
1388
+ // bundle and a resident using the last valid composition may still run.
1389
+ // Without a durable ownership registry, an unverifiable real profile is
1390
+ // therefore a fail-closed handoff boundary.
1391
+ throw new Error(`lark-channel setup: cannot verify effective Lark ownership in profile ${entry.name}`, {
1392
+ cause: error,
1393
+ });
1394
+ }
1395
+ if (!effectiveProfileHasEnabledLarkChannel(effective, entry.name))
1396
+ continue;
1397
+ const otherDatabasePath = deliveryDatabasePathFromEffectiveProfile(effective, input.dshHome);
1398
+ if (normalizeSetupResourcePath(otherDatabasePath) === databasePath) {
1399
+ throw new Error(`lark-channel setup: profile ${entry.name} already owns Lark for the same assistant-delivery database; `
1400
+ + 'refusing to revoke its owner from another profile');
1401
+ }
1402
+ }
1403
+ }
1404
+ async function withDeliveryOwnerSetupLock(databasePath, operation, options = {}) {
1405
+ const resourcePath = normalizeSetupResourcePath(databasePath);
1406
+ await mkdir(dirname(resourcePath), { recursive: true, mode: 0o700 });
1407
+ return withProfileSetupLock(`${resourcePath}.lark-owner`, operation, options);
1408
+ }
1409
+ async function recoverLarkSetupJournalRecordUnlocked(input, journal) {
1410
+ const remove = input.operations?.removeCredential ?? removeCredential;
1411
+ const pair = input.operations?.pairPrincipal ?? defaultPairPrincipal;
1412
+ const install = input.operations?.installService
1413
+ ?? (async () => { await installDshResidentService({ dshHome: input.dshHome, profile: input.profile }); });
1414
+ const current = await readFile(input.patchPath, 'utf8');
1415
+ if (journal.phase === 'staging') {
1416
+ if (current !== journal.originalPatch) {
1417
+ throw new Error('lark-channel setup: staging journal conflicts with the live profile');
1418
+ }
1419
+ await removeCredentialIdempotently(journal.stagedCredential, remove);
1420
+ await clearLarkSetupJournal(input.patchPath);
1421
+ return true;
1422
+ }
1423
+ if (journal.phase === 'candidate' || journal.phase === 'aborting') {
1424
+ if (current === journal.updatedPatch)
1425
+ await atomicWrite(input.patchPath, journal.originalPatch);
1426
+ else if (current !== journal.originalPatch) {
1427
+ throw new Error('lark-channel setup: candidate recovery refused because the live profile diverged');
1428
+ }
1429
+ if (journal.stagedCredential !== undefined) {
1430
+ await removeCredentialIdempotently(journal.stagedCredential, remove);
1431
+ }
1432
+ await clearLarkSetupJournal(input.patchPath);
1433
+ return true;
1434
+ }
1435
+ if (journal.updatedPatch === undefined) {
1436
+ throw new Error('lark-channel setup: validated journal has no candidate profile');
1437
+ }
1438
+ if (current === journal.originalPatch)
1439
+ await atomicWrite(input.patchPath, journal.updatedPatch);
1440
+ else if (current !== journal.updatedPatch) {
1441
+ throw new Error('lark-channel setup: validated recovery refused because the live profile diverged');
1442
+ }
1443
+ if (journal.operation === 'full') {
1444
+ await pair({ databasePath: journal.databasePath, principal: journal.principal });
1445
+ if (journal.phase !== 'paired')
1446
+ await persistLarkSetupJournal(journalPersistInput(journal, 'paired'));
1447
+ if (journal.previousCredential !== undefined
1448
+ && !credentialLocatorEquals(journal.previousCredential, journal.stagedCredential)) {
1449
+ await retirePreviousCredentialBestEffort(journal.previousCredential, remove);
1450
+ }
1451
+ if (journal.installService)
1452
+ await install();
1453
+ }
1454
+ await clearLarkSetupJournal(input.patchPath);
1455
+ return true;
1456
+ }
1457
+ async function recoverValidatedRefreshJournalUnlocked(input, journal) {
1458
+ const updatedPatch = journal.updatedPatch;
1459
+ const current = await readFile(input.patchPath, 'utf8');
1460
+ if (current === journal.originalPatch)
1461
+ await atomicWrite(input.patchPath, updatedPatch);
1462
+ else if (current !== updatedPatch) {
1463
+ throw new Error('lark-channel setup: validated refresh recovery refused because the live profile diverged');
1464
+ }
1465
+ try {
1466
+ assertRawManagedProfileIntegrity(updatedPatch);
1467
+ const readEffectiveProfile = input.operations?.readEffectiveProfile ?? dumpProfile;
1468
+ assertEffectiveManagedPolicyMatchesCandidate({
1469
+ candidatePatch: updatedPatch,
1470
+ effectiveProfile: await readEffectiveProfile(input.profile),
1471
+ });
1472
+ }
1473
+ catch (error) {
1474
+ try {
1475
+ await persistLarkSetupJournal(journalPersistInput(journal, 'aborting'));
1476
+ }
1477
+ catch (journalError) {
1478
+ throw new AggregateError([error, journalError], 'lark-channel setup: refresh recovery failed before rollback intent could be persisted');
1479
+ }
1480
+ if (await rollbackCandidatePatch({
1481
+ patchPath: input.patchPath,
1482
+ originalPatch: journal.originalPatch,
1483
+ updatedPatch,
1484
+ }) === 'conflict') {
1485
+ throw new Error('lark-channel setup: refresh recovery rollback refused because the profile changed concurrently', {
1486
+ cause: error,
1487
+ });
1488
+ }
1489
+ await clearLarkSetupJournal(input.patchPath);
1490
+ throw error;
1491
+ }
1492
+ await clearLarkSetupJournal(input.patchPath);
1493
+ return true;
1494
+ }
1495
+ async function recoverLarkSetupJournalUnlocked(input) {
1496
+ const journal = await readLarkSetupJournal(input);
1497
+ if (journal === undefined)
1498
+ return false;
1499
+ if (journal.operation === 'refresh' && journal.phase === 'validated') {
1500
+ return recoverValidatedRefreshJournalUnlocked(input, journal);
1501
+ }
1502
+ if (journal.operation !== 'full'
1503
+ || journal.phase === 'staging'
1504
+ || journal.phase === 'candidate'
1505
+ || journal.phase === 'aborting') {
1506
+ return recoverLarkSetupJournalRecordUnlocked(input, journal);
1507
+ }
1508
+ const databasePath = journal.databasePath;
1509
+ const readEffectiveProfile = input.operations?.readEffectiveProfile ?? dumpProfile;
1510
+ return withDeliveryOwnerSetupLock(databasePath, async () => {
1511
+ const effectiveProfile = await readEffectiveProfile(input.profile);
1512
+ const effectiveDatabasePath = deliveryDatabasePathFromEffectiveProfile(effectiveProfile, input.dshHome);
1513
+ if (normalizeSetupResourcePath(effectiveDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
1514
+ throw new Error('lark-channel setup: effective assistant-delivery database changed since the setup journal');
1515
+ }
1516
+ assertEffectiveManagedPolicyMatchesCandidate({
1517
+ candidatePatch: journal.updatedPatch,
1518
+ effectiveProfile,
1519
+ });
1520
+ assertEffectiveLarkOwnerBinding({
1521
+ effectiveProfile,
1522
+ candidatePatch: journal.updatedPatch,
1523
+ dshHome: input.dshHome,
1524
+ profile: input.profile,
1525
+ principal: journal.principal,
1526
+ stagedCredential: journal.stagedCredential,
1527
+ context: 'journal',
1528
+ });
1529
+ await assertNoOtherLarkProfileSharesDelivery({
1530
+ dshHome: input.dshHome,
1531
+ profile: input.profile,
1532
+ databasePath,
1533
+ readEffectiveProfile,
1534
+ });
1535
+ return recoverLarkSetupJournalRecordUnlocked(input, journal);
1536
+ }, input.profileLockOptions);
1537
+ }
1538
+ /** Recovers one abandoned setup transaction under the same profile lock. */
1539
+ export async function recoverLarkSetupJournal(input) {
1540
+ await withProfileSetupLock(input.patchPath, async () => {
1541
+ await recoverLarkSetupJournalUnlocked(input);
1542
+ }, input.profileLockOptions);
1543
+ }
1544
+ function dumpProfile(profile) {
1545
+ const result = spawnSync('dsh', ['--profile', profile, '--dump-config'], {
1546
+ encoding: 'utf8',
1547
+ maxBuffer: 4 * 1024 * 1024,
1548
+ stdio: ['ignore', 'pipe', 'pipe'],
1549
+ });
1550
+ if (result.status !== 0) {
1551
+ const detail = typeof result.stderr === 'string' ? result.stderr.trim().slice(0, 2_000) : '';
1552
+ throw new Error(`lark-channel setup: DSH rejected the updated profile${detail === '' ? '' : `: ${detail}`}`);
1553
+ }
1554
+ return typeof result.stdout === 'string' ? result.stdout : String(result.stdout ?? '');
1555
+ }
1556
+ function validateProfile(profile) {
1557
+ void dumpProfile(profile);
1558
+ }
1559
+ async function defaultPairPrincipal(input) {
1560
+ // Load the local-only control plane only after the candidate profile has
1561
+ // passed composition/schema validation. A rejected profile therefore has
1562
+ // no opportunity to mutate pairing state.
1563
+ const { pairPrincipalLocally } = await import('@dsh-enhanced/assistant-delivery');
1564
+ pairPrincipalLocally(input);
1565
+ }
1566
+ async function cleanupFailedCandidateCredential(transition, operation, setupError) {
1567
+ if (transition === undefined)
1568
+ throw setupError;
1569
+ try {
1570
+ await operation(transition.staged);
1571
+ }
1572
+ catch (cleanupError) {
1573
+ throw new AggregateError([setupError, cleanupError], 'lark-channel setup: setup failed and staged credential cleanup also failed');
1574
+ }
1575
+ throw setupError;
1576
+ }
1577
+ async function rollbackCandidatePatch(input) {
1578
+ const current = await readFile(input.patchPath, 'utf8');
1579
+ if (current === input.originalPatch)
1580
+ return 'restored';
1581
+ if (current !== input.updatedPatch)
1582
+ return 'conflict';
1583
+ await atomicWrite(input.patchPath, input.originalPatch);
1584
+ return 'restored';
1585
+ }
1586
+ /**
1587
+ * Makes profile validation a precondition of Delivery owner rotation. The
1588
+ * candidate patch is restored on either validation or handoff failure, while
1589
+ * the store performs the old-owner retirement and replacement promotion in one
1590
+ * database transaction.
1591
+ */
1592
+ export async function commitValidatedLarkOwnerSetup(input) {
1593
+ const validate = input.operations?.validateProfile ?? validateProfile;
1594
+ const pair = input.operations?.pairPrincipal ?? defaultPairPrincipal;
1595
+ const remove = input.operations?.removeCredential ?? removeCredential;
1596
+ const before = await readFile(input.patchPath, 'utf8');
1597
+ if (before !== input.originalPatch) {
1598
+ await cleanupFailedCandidateCredential(input.credentialTransition, remove, new Error('lark-channel setup: profile changed concurrently before candidate commit'));
1599
+ }
1600
+ try {
1601
+ await atomicWrite(input.patchPath, input.updatedPatch);
1602
+ }
1603
+ catch (error) {
1604
+ await cleanupFailedCandidateCredential(input.credentialTransition, remove, error);
1605
+ }
1606
+ try {
1607
+ await validate(input.profile);
1608
+ }
1609
+ catch (error) {
1610
+ if (await rollbackCandidatePatch(input) === 'conflict') {
1611
+ throw new Error('lark-channel setup: rollback refused because the profile changed concurrently', { cause: error });
1612
+ }
1613
+ await cleanupFailedCandidateCredential(input.credentialTransition, remove, error);
1614
+ }
1615
+ if (await readFile(input.patchPath, 'utf8') !== input.updatedPatch) {
1616
+ throw new Error('lark-channel setup: candidate profile changed concurrently before owner handoff');
1617
+ }
1618
+ try {
1619
+ await pair({ databasePath: input.databasePath, principal: input.principal });
1620
+ }
1621
+ catch (error) {
1622
+ if (await rollbackCandidatePatch(input) === 'conflict') {
1623
+ throw new Error('lark-channel setup: rollback refused because the profile changed concurrently', { cause: error });
1624
+ }
1625
+ await cleanupFailedCandidateCredential(input.credentialTransition, remove, error);
1626
+ }
1627
+ const previous = input.credentialTransition?.previous;
1628
+ if (previous !== undefined && JSON.stringify(previous) !== JSON.stringify(input.credentialTransition?.staged)) {
1629
+ try {
1630
+ await remove(previous);
1631
+ }
1632
+ catch {
1633
+ // The profile and owner are already committed. Keep the now-inactive
1634
+ // credential rather than reporting a false setup failure or rolling back
1635
+ // a successful handoff; a later setup can retire the orphan safely.
1636
+ process.stderr.write('lark-channel setup: previous credential cleanup failed after commit\n');
1637
+ }
1638
+ }
1639
+ }
1640
+ async function abortStagingJournal(input) {
1641
+ const current = await readFile(input.patchPath, 'utf8');
1642
+ if (current !== input.originalPatch) {
1643
+ throw new Error('lark-channel setup: staging abort refused because the live profile diverged', {
1644
+ cause: input.setupError,
1645
+ });
1646
+ }
1647
+ try {
1648
+ await removeCredentialIdempotently(input.stagedCredential, input.remove);
1649
+ await clearLarkSetupJournal(input.patchPath);
1650
+ }
1651
+ catch (cleanupError) {
1652
+ throw new AggregateError([input.setupError, cleanupError], 'lark-channel setup: setup failed and staged credential cleanup also failed');
1653
+ }
1654
+ throw input.setupError;
1655
+ }
1656
+ async function abortCandidateJournal(input) {
1657
+ if (await rollbackCandidatePatch(input) === 'conflict') {
1658
+ throw new Error('lark-channel setup: rollback refused because the profile changed concurrently', {
1659
+ cause: input.setupError,
1660
+ });
1661
+ }
1662
+ try {
1663
+ await removeCredentialIdempotently(input.stagedCredential, input.remove);
1664
+ await clearLarkSetupJournal(input.patchPath);
1665
+ }
1666
+ catch (cleanupError) {
1667
+ throw new AggregateError([input.setupError, cleanupError], 'lark-channel setup: setup rollback completed but staged credential cleanup failed');
1668
+ }
1669
+ throw input.setupError;
1670
+ }
1671
+ /**
1672
+ * Executes the mutation-bearing portion of onboarding while holding the
1673
+ * profile lock. Credential staging, owner discovery, candidate composition,
1674
+ * validation, and the Delivery owner handoff therefore observe one serialized
1675
+ * profile generation.
1676
+ */
1677
+ export async function executeLarkSetupProfileTransaction(input) {
1678
+ const remove = input.operations.removeCredential ?? removeCredential;
1679
+ const pair = input.operations.pairPrincipal ?? defaultPairPrincipal;
1680
+ const validate = input.operations.validateProfile ?? validateProfile;
1681
+ const readEffectiveProfile = input.operations.readEffectiveProfile ?? dumpProfile;
1682
+ await withProfileSetupLock(input.patchPath, async () => {
1683
+ await recoverLarkSetupJournalUnlocked({
1684
+ patchPath: input.patchPath,
1685
+ dshHome: input.dshHome,
1686
+ profile: input.args.profile,
1687
+ operations: {
1688
+ pairPrincipal: pair,
1689
+ removeCredential: remove,
1690
+ readEffectiveProfile,
1691
+ ...(input.operations.afterCommit === undefined ? {} : { installService: input.operations.afterCommit }),
1692
+ },
1693
+ });
1694
+ const originalPatch = await readFile(input.patchPath, 'utf8');
1695
+ const effectiveProfile = await readEffectiveProfile(input.args.profile);
1696
+ const databasePath = deliveryDatabasePathFromEffectiveProfile(effectiveProfile, input.dshHome);
1697
+ const materializedPatch = materializeManagedProfileOverride({
1698
+ profilePatch: originalPatch,
1699
+ effectiveProfile,
1700
+ });
1701
+ await withDeliveryOwnerSetupLock(databasePath, async () => {
1702
+ await assertNoOtherLarkProfileSharesDelivery({
1703
+ dshHome: input.dshHome,
1704
+ profile: input.args.profile,
1705
+ databasePath,
1706
+ readEffectiveProfile,
1707
+ });
1708
+ const previousCredential = findManagedLarkCredentialLocator({
1709
+ profilePatch: originalPatch,
1710
+ dshHome: input.dshHome,
1711
+ profile: input.args.profile,
1712
+ });
1713
+ const stagedCredential = createVersionedCredentialLocator({
1714
+ provider: input.credentialProvider,
1715
+ dshHome: input.dshHome,
1716
+ profile: input.args.profile,
1717
+ account: input.args.account,
1718
+ });
1719
+ if (stagedCredential.provider === 'windows-dpapi') {
1720
+ await mkdir(dirname(stagedCredential.path), { recursive: true, mode: 0o700 });
1721
+ }
1722
+ const transactionId = randomBytes(16).toString('hex');
1723
+ const stagingJournal = {
1724
+ patchPath: input.patchPath,
1725
+ dshHome: input.dshHome,
1726
+ profile: input.args.profile,
1727
+ operation: 'full',
1728
+ phase: 'staging',
1729
+ originalPatch,
1730
+ databasePath,
1731
+ account: input.args.account,
1732
+ stagedCredential,
1733
+ ...(previousCredential === undefined ? {} : { previousCredential }),
1734
+ installService: input.operations.afterCommit !== undefined,
1735
+ transactionId,
1736
+ };
1737
+ await persistLarkSetupJournal(stagingJournal);
1738
+ const { updatedPatch, owner } = await (async () => {
1739
+ try {
1740
+ await input.operations.storeCredential(stagedCredential);
1741
+ const appSecret = await input.operations.readCredential(stagedCredential);
1742
+ if (appSecret.length === 0)
1743
+ throw new Error('lark-channel setup: credential store returned an empty secret');
1744
+ const owner = await input.operations.discoverOwner({
1745
+ appId: input.application.appId,
1746
+ appSecret,
1747
+ domain: input.application.domain,
1748
+ account: input.args.account,
1749
+ tenant: input.args.tenant,
1750
+ timeoutMs: input.args.timeoutMs,
1751
+ });
1752
+ if (owner.channel !== 'lark'
1753
+ || owner.account !== input.args.account
1754
+ || owner.tenant !== input.args.tenant) {
1755
+ throw new Error('lark-channel setup: discovered owner does not match the setup account and tenant');
1756
+ }
1757
+ const keychainService = stagedCredential.provider === 'windows-dpapi'
1758
+ ? `dsh/lark/${input.args.profile}/${input.args.account}/versions/windows-dpapi`
1759
+ : stagedCredential.service;
1760
+ return {
1761
+ owner,
1762
+ updatedPatch: configureLarkProfilePatch({
1763
+ profilePatch: materializedPatch,
1764
+ dshHome: input.dshHome,
1765
+ appId: input.application.appId,
1766
+ account: input.args.account,
1767
+ tenant: input.args.tenant,
1768
+ domain: input.application.domain,
1769
+ ownerUserId: owner.user,
1770
+ keychainService,
1771
+ keychainAccount: input.args.account,
1772
+ credentialProvider: input.credentialProvider,
1773
+ agentTools: input.args.agentTools,
1774
+ ...(stagedCredential.provider === 'windows-dpapi' ? { credentialPath: stagedCredential.path } : {}),
1775
+ }),
1776
+ };
1777
+ }
1778
+ catch (error) {
1779
+ return abortStagingJournal({
1780
+ patchPath: input.patchPath,
1781
+ originalPatch,
1782
+ stagedCredential,
1783
+ remove,
1784
+ setupError: error,
1785
+ });
1786
+ }
1787
+ })();
1788
+ const candidateJournal = {
1789
+ ...stagingJournal,
1790
+ phase: 'candidate',
1791
+ updatedPatch,
1792
+ principal: owner,
1793
+ };
1794
+ try {
1795
+ await persistLarkSetupJournal(candidateJournal);
1796
+ }
1797
+ catch (error) {
1798
+ await abortStagingJournal({
1799
+ patchPath: input.patchPath,
1800
+ originalPatch,
1801
+ stagedCredential,
1802
+ remove,
1803
+ setupError: error,
1804
+ });
1805
+ }
1806
+ try {
1807
+ await atomicWrite(input.patchPath, updatedPatch);
1808
+ await validate(input.args.profile);
1809
+ const validatedEffectiveProfile = await readEffectiveProfile(input.args.profile);
1810
+ const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(validatedEffectiveProfile, input.dshHome);
1811
+ if (normalizeSetupResourcePath(validatedDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
1812
+ throw new Error('lark-channel setup: effective assistant-delivery database changed during validation');
1813
+ }
1814
+ assertEffectiveManagedPolicyMatchesCandidate({
1815
+ candidatePatch: updatedPatch,
1816
+ effectiveProfile: validatedEffectiveProfile,
1817
+ });
1818
+ assertEffectiveLarkOwnerBinding({
1819
+ effectiveProfile: validatedEffectiveProfile,
1820
+ candidatePatch: updatedPatch,
1821
+ dshHome: input.dshHome,
1822
+ profile: input.args.profile,
1823
+ principal: owner,
1824
+ stagedCredential,
1825
+ context: 'validation',
1826
+ });
1827
+ }
1828
+ catch (error) {
1829
+ await abortCandidateJournal({
1830
+ patchPath: input.patchPath,
1831
+ originalPatch,
1832
+ updatedPatch,
1833
+ stagedCredential,
1834
+ remove,
1835
+ setupError: error,
1836
+ });
1837
+ }
1838
+ if (await readFile(input.patchPath, 'utf8') !== updatedPatch) {
1839
+ await abortCandidateJournal({
1840
+ patchPath: input.patchPath,
1841
+ originalPatch,
1842
+ updatedPatch,
1843
+ stagedCredential,
1844
+ remove,
1845
+ setupError: new Error('lark-channel setup: candidate profile changed after validation'),
1846
+ });
1847
+ }
1848
+ // Do not roll back if this phase write reports an error: rename may already
1849
+ // have made the validated record durable. Recovery reads the actual record
1850
+ // and deterministically chooses forward commit or rollback.
1851
+ await persistLarkSetupJournal({ ...candidateJournal, phase: 'validated' });
1852
+ try {
1853
+ await pair({ databasePath, principal: owner });
1854
+ }
1855
+ catch (error) {
1856
+ // The rollback direction must be durable before profile or credential
1857
+ // rollback starts. A crash after deleting the staged secret can therefore
1858
+ // never replay the old validated record and activate a missing secret.
1859
+ try {
1860
+ await persistLarkSetupJournal({ ...candidateJournal, phase: 'aborting' });
1861
+ }
1862
+ catch (journalError) {
1863
+ throw new AggregateError([error, journalError], 'lark-channel setup: owner handoff failed before rollback intent could be persisted');
1864
+ }
1865
+ await abortCandidateJournal({
1866
+ patchPath: input.patchPath,
1867
+ originalPatch,
1868
+ updatedPatch,
1869
+ stagedCredential,
1870
+ remove,
1871
+ setupError: error,
1872
+ });
1873
+ }
1874
+ // Once owner handoff returns, rollback is no longer safe. If this durable
1875
+ // phase write fails, leave the validated journal so the next entry repeats
1876
+ // the idempotent exact-owner handoff and converges forward.
1877
+ await persistLarkSetupJournal({ ...candidateJournal, phase: 'paired' });
1878
+ if (previousCredential !== undefined && !credentialLocatorEquals(previousCredential, stagedCredential)) {
1879
+ await retirePreviousCredentialBestEffort(previousCredential, remove);
1880
+ }
1881
+ await input.operations.afterCommit?.();
1882
+ await clearLarkSetupJournal(input.patchPath);
1883
+ }, input.profileLockOptions);
1884
+ }, input.profileLockOptions);
1885
+ }
1886
+ export async function runLarkSetup(argv = process.argv.slice(2), runtime = {}) {
1887
+ const args = parseLarkSetupArgs(argv);
1888
+ if (args.help) {
1889
+ process.stdout.write(`${help()}\n`);
1890
+ return;
1891
+ }
1892
+ const dshHome = process.env.DSH_HOME?.trim() || join(homedir(), '.dsh');
1893
+ if (!isAbsolute(dshHome))
1894
+ throw new Error('lark-channel setup: DSH_HOME must be absolute');
1895
+ const patchPath = join(dshHome, 'profiles', args.profile, 'cordis.patch.yml');
1896
+ const validate = runtime.validateProfile ?? validateProfile;
1897
+ const readEffectiveProfile = runtime.readEffectiveProfile ?? dumpProfile;
1898
+ const installResidentService = runtime.installResidentService ?? installDshResidentService;
1899
+ const recoveryOperations = {
1900
+ ...runtime.journalOperations,
1901
+ readEffectiveProfile: runtime.journalOperations?.readEffectiveProfile ?? readEffectiveProfile,
1902
+ installService: runtime.journalOperations?.installService
1903
+ ?? (async () => { await installResidentService({ dshHome, profile: args.profile }); }),
1904
+ };
1905
+ if (args.refreshAgentPolicy) {
1906
+ await withProfileSetupLock(patchPath, async () => {
1907
+ await recoverLarkSetupJournalUnlocked({
1908
+ patchPath,
1909
+ dshHome,
1910
+ profile: args.profile,
1911
+ operations: recoveryOperations,
1912
+ });
1913
+ const effectiveProfile = await readEffectiveProfile(args.profile);
1914
+ const databasePath = deliveryDatabasePathFromEffectiveProfile(effectiveProfile, dshHome);
1915
+ await withDeliveryOwnerSetupLock(databasePath, async () => {
1916
+ await assertNoOtherLarkProfileSharesDelivery({
1917
+ dshHome,
1918
+ profile: args.profile,
1919
+ databasePath,
1920
+ readEffectiveProfile,
1921
+ });
1922
+ const originalPatch = await readFile(patchPath, 'utf8');
1923
+ const materializedPatch = materializeManagedProfileOverride({
1924
+ profilePatch: originalPatch,
1925
+ effectiveProfile,
1926
+ });
1927
+ const refreshInput = {
1928
+ profilePatch: materializedPatch,
1929
+ effectiveProfile,
1930
+ dshHome,
1931
+ agentTools: args.agentTools === 'enable' ? 'enable' : 'disable',
1932
+ ...(args.accountProvided ? { account: args.account } : {}),
1933
+ };
1934
+ const updatedPatch = refreshLarkAgentPolicyFromEffective(refreshInput);
1935
+ const journal = {
1936
+ patchPath,
1937
+ dshHome,
1938
+ profile: args.profile,
1939
+ operation: 'refresh',
1940
+ phase: 'candidate',
1941
+ originalPatch,
1942
+ updatedPatch,
1943
+ installService: false,
1944
+ transactionId: randomBytes(16).toString('hex'),
1945
+ };
1946
+ await persistLarkSetupJournal(journal);
1947
+ try {
1948
+ await atomicWrite(patchPath, updatedPatch);
1949
+ await validate(args.profile);
1950
+ const validatedEffectiveProfile = await readEffectiveProfile(args.profile);
1951
+ const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(validatedEffectiveProfile, dshHome);
1952
+ if (normalizeSetupResourcePath(validatedDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
1953
+ throw new Error('lark-channel setup: effective assistant-delivery database changed during refresh validation');
1954
+ }
1955
+ const validatedSemanticPatch = refreshLarkAgentPolicyFromEffective({
1956
+ ...refreshInput,
1957
+ effectiveProfile: validatedEffectiveProfile,
1958
+ });
1959
+ assertEffectiveManagedPolicyMatchesCandidate({
1960
+ candidatePatch: validatedSemanticPatch,
1961
+ effectiveProfile: validatedEffectiveProfile,
1962
+ });
1963
+ assertEffectiveManagedPolicyMatchesCandidate({
1964
+ candidatePatch: updatedPatch,
1965
+ effectiveProfile: validatedEffectiveProfile,
1966
+ });
1967
+ if (await readFile(patchPath, 'utf8') !== updatedPatch) {
1968
+ throw new Error('lark-channel setup: refreshed profile changed during validation');
1969
+ }
1970
+ }
1971
+ catch (error) {
1972
+ if (await rollbackCandidatePatch({ patchPath, originalPatch, updatedPatch }) === 'conflict') {
1973
+ throw new Error('lark-channel setup: rollback refused because the profile changed concurrently', { cause: error });
1974
+ }
1975
+ await clearLarkSetupJournal(patchPath);
1976
+ throw error;
1977
+ }
1978
+ // If this durable phase transition or journal removal reports an error,
1979
+ // leave recovery to inspect the on-disk phase. Rolling back here could
1980
+ // contradict a validated record already renamed into place.
1981
+ await persistLarkSetupJournal({ ...journal, phase: 'validated' });
1982
+ await clearLarkSetupJournal(patchPath);
1983
+ }, runtime.profileLockOptions);
1984
+ }, runtime.profileLockOptions);
1985
+ process.stdout.write(`DSH profile ${args.profile} 的 Agent capability policy 已刷新;飞书应用、凭据和 owner 绑定未改动。\n`);
1986
+ return;
1987
+ }
1988
+ if (args.installServiceOnly) {
1989
+ const service = await withProfileSetupLock(patchPath, async () => {
1990
+ await recoverLarkSetupJournalUnlocked({
1991
+ patchPath,
1992
+ dshHome,
1993
+ profile: args.profile,
1994
+ operations: recoveryOperations,
1995
+ });
1996
+ const databasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
1997
+ return withDeliveryOwnerSetupLock(databasePath, async () => {
1998
+ await assertNoOtherLarkProfileSharesDelivery({
1999
+ dshHome,
2000
+ profile: args.profile,
2001
+ databasePath,
2002
+ readEffectiveProfile,
2003
+ });
2004
+ const profileSnapshot = await readFile(patchPath, 'utf8');
2005
+ await validate(args.profile);
2006
+ const validatedDatabasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
2007
+ if (normalizeSetupResourcePath(validatedDatabasePath) !== normalizeSetupResourcePath(databasePath)) {
2008
+ throw new Error('lark-channel setup: effective assistant-delivery database changed during service validation');
2009
+ }
2010
+ if (await readFile(patchPath, 'utf8') !== profileSnapshot) {
2011
+ throw new Error('lark-channel setup: profile changed during resident service validation');
2012
+ }
2013
+ return installResidentService({ dshHome, profile: args.profile });
2014
+ }, runtime.profileLockOptions);
2015
+ }, runtime.profileLockOptions);
2016
+ process.stdout.write(`DSH profile ${args.profile} 已由 ${service.kind} 启动并保持常驻。\n`
2017
+ + `状态:${service.statusCommand}\n日志:${service.logCommand}\n`);
2018
+ return;
2019
+ }
2020
+ // Resolve a prior crash before prompting the user or mutating a cloud app.
2021
+ // The mutation transaction repeats this check after registration to close
2022
+ // the gap against a different setup process that completed in the meantime.
2023
+ await withProfileSetupLock(patchPath, async () => {
2024
+ await recoverLarkSetupJournalUnlocked({
2025
+ patchPath,
2026
+ dshHome,
2027
+ profile: args.profile,
2028
+ operations: recoveryOperations,
2029
+ });
2030
+ const databasePath = deliveryDatabasePathFromEffectiveProfile(await readEffectiveProfile(args.profile), dshHome);
2031
+ await withDeliveryOwnerSetupLock(databasePath, async () => {
2032
+ await assertNoOtherLarkProfileSharesDelivery({
2033
+ dshHome,
2034
+ profile: args.profile,
2035
+ databasePath,
2036
+ readEffectiveProfile,
2037
+ });
2038
+ }, runtime.profileLockOptions);
2039
+ }, runtime.profileLockOptions);
2040
+ const credentialProvider = credentialProviderForPlatform(process.platform);
2041
+ let appId = args.appId;
2042
+ let createApp = args.createApp;
2043
+ if (appId === undefined && !createApp) {
2044
+ const prompts = createInterface({ input: process.stdin, output: process.stdout });
2045
+ try {
2046
+ appId = (await prompts.question('已有飞书 App ID(cli_...;直接回车则打开一键选择/创建):')).trim();
2047
+ createApp = appId.length === 0;
2048
+ }
2049
+ finally {
2050
+ prompts.close();
2051
+ }
2052
+ }
2053
+ let resolvedDomain = args.domain;
2054
+ let generatedSecret;
2055
+ if (createApp) {
2056
+ process.stdout.write(args.appId === undefined
2057
+ ? '\n将通过飞书官方设备授权选择已有应用或创建新应用,并增量申请最小权限。App Secret 不会显示或写入 profile。\n'
2058
+ : `\n将通过飞书官方设备授权更新已有应用 ${args.appId},并增量申请最小权限。App Secret 不会显示或写入 profile。\n`);
2059
+ const registration = await registerLarkApplication(args);
2060
+ appId = registration.client_id;
2061
+ resolvedDomain = registration.user_info?.tenant_brand ?? resolvedDomain;
2062
+ generatedSecret = registration.client_secret;
2063
+ process.stdout.write(`应用 ${appId} 已完成授权;凭据将在 profile 事务中暂存并激活。\n`);
2064
+ }
2065
+ else {
2066
+ if (appId === undefined || !/^cli_[0-9a-fA-F]{16}$/u.test(appId)) {
2067
+ throw new Error('lark-channel setup: invalid App ID');
2068
+ }
2069
+ process.stdout.write(`请确认已有应用已开启机器人、im:message 与 im:resource 权限、长连接事件 im.message.receive_v1,且版本已发布。\n`
2070
+ + `控制台:https://open.feishu.cn/app\n`);
2071
+ }
2072
+ if (appId === undefined)
2073
+ throw new Error('lark-channel setup: Feishu did not return an App ID');
2074
+ const configuredAppId = appId;
2075
+ let installedService;
2076
+ await executeLarkSetupProfileTransaction({
2077
+ args,
2078
+ dshHome,
2079
+ patchPath,
2080
+ application: { appId: configuredAppId, domain: resolvedDomain },
2081
+ credentialProvider,
2082
+ ...(runtime.profileLockOptions === undefined ? {} : { profileLockOptions: runtime.profileLockOptions }),
2083
+ operations: {
2084
+ storeCredential(locator) {
2085
+ const service = locator.provider === 'windows-dpapi'
2086
+ ? `dsh/lark/${args.profile}/${args.account}/versions/windows-dpapi`
2087
+ : locator.service;
2088
+ const credentialPath = locator.provider === 'windows-dpapi' ? locator.path : undefined;
2089
+ if (generatedSecret === undefined) {
2090
+ storeSecret(credentialProvider, service, args.account, credentialPath);
2091
+ }
2092
+ else {
2093
+ storeGeneratedSecret(credentialProvider, service, args.account, generatedSecret, credentialPath);
2094
+ }
2095
+ process.stdout.write(`候选凭据已安全暂存到 ${credentialProvider};旧凭据仍保持有效。\n`);
2096
+ },
2097
+ readCredential(locator) {
2098
+ const service = locator.provider === 'windows-dpapi'
2099
+ ? `dsh/lark/${args.profile}/${args.account}/versions/windows-dpapi`
2100
+ : locator.service;
2101
+ return readSecret(credentialProvider, service, args.account, locator.provider === 'windows-dpapi' ? locator.path : undefined);
2102
+ },
2103
+ readEffectiveProfile,
2104
+ async discoverOwner(ownerInput) {
2105
+ const phrase = `DSH-CONNECT-${randomBytes(4).toString('hex').toUpperCase()}`;
2106
+ const transport = createOfficialLarkTransport({
2107
+ appId: ownerInput.appId,
2108
+ appSecret: ownerInput.appSecret,
2109
+ domain: ownerInput.domain,
2110
+ handshakeTimeoutMs: 15_000,
2111
+ imageDownloadTimeoutMs: 30_000,
2112
+ });
2113
+ return discoverOwner(transport, phrase, ownerInput.account, ownerInput.tenant, ownerInput.timeoutMs);
2114
+ },
2115
+ ...(args.manageService ? {
2116
+ async afterCommit() {
2117
+ installedService = await installResidentService({ dshHome, profile: args.profile });
2118
+ },
2119
+ } : {}),
2120
+ },
2121
+ });
2122
+ if (args.manageService) {
2123
+ const service = installedService;
2124
+ if (service === undefined)
2125
+ throw new Error('lark-channel setup: resident service installation did not complete');
2126
+ process.stdout.write(`\n飞书 channel 已写入 profile ${args.profile},owner 已精确绑定。\n`
2127
+ + `DSH Host 已由 ${service.kind} 启动并保持常驻,无需再手动运行 dsh web。\n`
2128
+ + `状态:${service.statusCommand}\n日志:${service.logCommand}\n`
2129
+ + `健康检查:在 Web 会话中调用 assistant_health。\n`);
2130
+ }
2131
+ else {
2132
+ process.stdout.write(`\n飞书 channel 已写入 profile ${args.profile},owner 已精确绑定。\n`
2133
+ + `本次跳过常驻服务;手动启动:dsh --profile ${args.profile} --no-open\n`
2134
+ + `健康检查:在 Web 会话中调用 assistant_health。\n`);
2135
+ }
2136
+ }
2137
+ if (process.argv[1] !== undefined && isMainEntry(import.meta.url, process.argv[1])) {
2138
+ void runLarkSetup().catch(error => {
2139
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
2140
+ process.exitCode = 1;
2141
+ });
2142
+ }
2143
+ //# sourceMappingURL=setup.js.map