@raidiant/notifai 10.1.1 → 10.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 (168) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/commands-core.d.ts +4 -1
  3. package/dist/commands-core.js.map +1 -1
  4. package/dist/commands-doctor.js +2 -7
  5. package/dist/commands-doctor.js.map +1 -1
  6. package/dist/commands-hooks.d.ts +12 -0
  7. package/dist/commands-hooks.js +15 -1
  8. package/dist/commands-hooks.js.map +1 -1
  9. package/dist/commands-init.js +9 -6
  10. package/dist/commands-init.js.map +1 -1
  11. package/dist/commands-send-support.js +10 -11
  12. package/dist/commands-send-support.js.map +1 -1
  13. package/dist/commands-skill.js +72 -54
  14. package/dist/commands-skill.js.map +1 -1
  15. package/dist/harness-session-title.d.ts +22 -0
  16. package/dist/harness-session-title.js +74 -0
  17. package/dist/harness-session-title.js.map +1 -0
  18. package/dist/invocation-context.d.ts +1 -0
  19. package/dist/invocation-context.js +15 -1
  20. package/dist/invocation-context.js.map +1 -1
  21. package/dist/native-skills.d.ts +13 -3
  22. package/dist/native-skills.js +51 -12
  23. package/dist/native-skills.js.map +1 -1
  24. package/dist/node-floor.d.ts +2 -1
  25. package/dist/node-floor.js +9 -5
  26. package/dist/node-floor.js.map +1 -1
  27. package/dist/orca-session-title.d.ts +5 -4
  28. package/dist/orca-session-title.js +32 -19
  29. package/dist/orca-session-title.js.map +1 -1
  30. package/dist/platform.d.ts +11 -17
  31. package/dist/platform.js +20 -23
  32. package/dist/platform.js.map +1 -1
  33. package/dist/release.d.ts +5 -5
  34. package/dist/release.js +5 -5
  35. package/dist/session-labels.d.ts +2 -0
  36. package/dist/session-labels.js +20 -9
  37. package/dist/session-labels.js.map +1 -1
  38. package/dist/setup-destinations.js +1 -1
  39. package/dist/setup-destinations.js.map +1 -1
  40. package/dist/skill-integrity.d.ts +49 -0
  41. package/dist/skill-integrity.js +154 -0
  42. package/dist/skill-integrity.js.map +1 -0
  43. package/dist/skill-source/manifest.json +16 -0
  44. package/dist/skill-source/notifai/SKILL.md +390 -0
  45. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  46. package/package.json +11 -14
  47. package/src/atomic-file.ts +0 -198
  48. package/src/claude-wake.test.ts +0 -321
  49. package/src/claude-wake.ts +0 -499
  50. package/src/cli-bin.test.ts +0 -176
  51. package/src/cli-bin.ts +0 -233
  52. package/src/cli-release.test.ts +0 -46
  53. package/src/cli-release.ts +0 -78
  54. package/src/cli-update-contract.ts +0 -9
  55. package/src/cli-update.test.ts +0 -125
  56. package/src/client.test.ts +0 -390
  57. package/src/client.ts +0 -352
  58. package/src/codex-wake.test.ts +0 -254
  59. package/src/codex-wake.ts +0 -301
  60. package/src/command-session.ts +0 -38
  61. package/src/commands-agent-sessions.ts +0 -72
  62. package/src/commands-auth.ts +0 -238
  63. package/src/commands-config.ts +0 -336
  64. package/src/commands-core.ts +0 -312
  65. package/src/commands-devices.ts +0 -180
  66. package/src/commands-doctor.ts +0 -1501
  67. package/src/commands-guidance.ts +0 -181
  68. package/src/commands-harness-context.ts +0 -207
  69. package/src/commands-hook-shape.ts +0 -69
  70. package/src/commands-hooks.ts +0 -2318
  71. package/src/commands-init.ts +0 -1055
  72. package/src/commands-io.ts +0 -108
  73. package/src/commands-logs.ts +0 -243
  74. package/src/commands-project.ts +0 -47
  75. package/src/commands-send-support.test.ts +0 -156
  76. package/src/commands-send-support.ts +0 -311
  77. package/src/commands-send.ts +0 -875
  78. package/src/commands-setup-proof.test.ts +0 -72
  79. package/src/commands-setup-proof.ts +0 -124
  80. package/src/commands-skill.test.ts +0 -44
  81. package/src/commands-skill.ts +0 -204
  82. package/src/commands-sounds.test.ts +0 -106
  83. package/src/commands-sounds.ts +0 -49
  84. package/src/commands-update.ts +0 -180
  85. package/src/commands.test.ts +0 -8297
  86. package/src/commands.ts +0 -77
  87. package/src/config-schema.test.ts +0 -153
  88. package/src/config-schema.ts +0 -349
  89. package/src/config.test.ts +0 -581
  90. package/src/config.ts +0 -475
  91. package/src/credentials.test.ts +0 -403
  92. package/src/credentials.ts +0 -403
  93. package/src/file-lock-process-worker.test.ts +0 -64
  94. package/src/file-lock.test.ts +0 -320
  95. package/src/file-lock.ts +0 -393
  96. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  97. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  98. package/src/generated-session-label.ts +0 -155
  99. package/src/guidance-content.ts +0 -278
  100. package/src/guidance-render.ts +0 -56
  101. package/src/guidance.test.ts +0 -327
  102. package/src/guidance.ts +0 -233
  103. package/src/harness-integration.test.ts +0 -294
  104. package/src/harnesses.test.ts +0 -149
  105. package/src/harnesses.ts +0 -164
  106. package/src/hook-adapter.test.ts +0 -496
  107. package/src/hook-adapter.ts +0 -424
  108. package/src/hook-input.test.ts +0 -39
  109. package/src/hook-input.ts +0 -72
  110. package/src/hooks.test.ts +0 -5824
  111. package/src/hooks.ts +0 -4011
  112. package/src/install-hooks.test.ts +0 -1614
  113. package/src/install-hooks.ts +0 -1501
  114. package/src/interactive.test.ts +0 -387
  115. package/src/interactive.ts +0 -858
  116. package/src/invocation-context.test.ts +0 -352
  117. package/src/invocation-context.ts +0 -197
  118. package/src/logging-process-worker.test.ts +0 -45
  119. package/src/logging.test.ts +0 -544
  120. package/src/logging.ts +0 -698
  121. package/src/main-run.ts +0 -45
  122. package/src/main.ts +0 -12
  123. package/src/native-skills.test.ts +0 -115
  124. package/src/native-skills.ts +0 -162
  125. package/src/node-floor.test.ts +0 -27
  126. package/src/node-floor.ts +0 -37
  127. package/src/onboarding-docs.test.ts +0 -25
  128. package/src/openclaw-plugin.ts +0 -409
  129. package/src/opencode-plugin.ts +0 -298
  130. package/src/orca-session-title.test.ts +0 -54
  131. package/src/orca-session-title.ts +0 -103
  132. package/src/pack-feedback-slice.test.ts +0 -212
  133. package/src/pack-feedback-slice.ts +0 -187
  134. package/src/platform.test.ts +0 -152
  135. package/src/platform.ts +0 -173
  136. package/src/program.test.ts +0 -317
  137. package/src/program.ts +0 -842
  138. package/src/project-enablement.test.ts +0 -55
  139. package/src/project-enablement.ts +0 -99
  140. package/src/question-caps.test.ts +0 -49
  141. package/src/question-settlement-process.ts +0 -47
  142. package/src/question-timing.ts +0 -56
  143. package/src/readiness.test.ts +0 -25
  144. package/src/readiness.ts +0 -212
  145. package/src/release.ts +0 -51
  146. package/src/send-attempts.test.ts +0 -143
  147. package/src/send-attempts.ts +0 -188
  148. package/src/send.test.ts +0 -28
  149. package/src/send.ts +0 -436
  150. package/src/session-activation.ts +0 -61
  151. package/src/session-end-log-lock-worker.test.ts +0 -35
  152. package/src/session-labels.test.ts +0 -490
  153. package/src/session-labels.ts +0 -474
  154. package/src/setup-destinations.test.ts +0 -41
  155. package/src/setup-destinations.ts +0 -50
  156. package/src/skill.test.ts +0 -426
  157. package/src/sound-ref.test.ts +0 -31
  158. package/src/sound-ref.ts +0 -34
  159. package/src/trust-policy.test.ts +0 -701
  160. package/src/ui/banner.ts +0 -95
  161. package/src/ui/config-view.test.ts +0 -90
  162. package/src/ui/config-view.ts +0 -138
  163. package/src/ui/help.test.ts +0 -16
  164. package/src/ui/help.ts +0 -128
  165. package/src/ui/theme.test.ts +0 -92
  166. package/src/ui/theme.ts +0 -265
  167. package/src/url-policy.ts +0 -399
  168. package/tsconfig.json +0 -10
@@ -1,403 +0,0 @@
1
- import { execFileSync, spawnSync } from 'node:child_process'
2
- import { existsSync, readFileSync, rmSync } from 'node:fs'
3
- import path from 'node:path'
4
- import { atomicWriteFileSync } from './atomic-file.js'
5
- import { globalConfigDir } from './config.js'
6
-
7
- export interface MachineCredential {
8
- machineId: string
9
- secret: string
10
- baseUrl: string
11
- machineName: string
12
- }
13
-
14
- export interface CredentialStore {
15
- load(): MachineCredential | null
16
- save(credential: MachineCredential): void
17
- clear(): void
18
- /** Where the secret lives, for `doctor` and docs. */
19
- describe(): string
20
- }
21
-
22
- export interface RunCommandSpec {
23
- command: string
24
- args: readonly string[]
25
- input: Buffer
26
- timeoutMs?: number
27
- }
28
-
29
- export interface RunCommandResult {
30
- status: number
31
- stdout: Buffer
32
- }
33
-
34
- /** Injected process seam. Production uses `defaultProcessRunner`. */
35
- export interface ProcessRunner {
36
- run(spec: RunCommandSpec): RunCommandResult
37
- }
38
-
39
- export interface CredentialStoreOptions {
40
- env?: NodeJS.ProcessEnv
41
- platform?: NodeJS.Platform
42
- run?: ProcessRunner
43
- keychainAvailable?: boolean
44
- }
45
-
46
- const SERVICE = 'io.notifai.cli'
47
- const CREDENTIAL_FORMAT = 'notifai.machine-credential.v1'
48
- const DPAPI_FORMAT = 'notifai.dpapi.current-user.v1'
49
- const DPAPI_ENTROPY = 'io.notifai.cli'
50
- // Inbox Windows PowerShell can have a slow first start on a newly provisioned
51
- // or heavily loaded machine. Keep the helper bounded without rejecting a
52
- // valid DPAPI operation during that cold-start window.
53
- const DPAPI_TIMEOUT_MS = 30_000
54
- const DPAPI_FILE = 'credentials.dpapi'
55
- const KEYCHAIN_TIMEOUT_MS = 15_000
56
-
57
- const PROTECT_SCRIPT = [
58
- "$ErrorActionPreference = 'Stop'",
59
- 'Add-Type -AssemblyName System.Security',
60
- '$in = New-Object System.IO.MemoryStream',
61
- '[Console]::OpenStandardInput().CopyTo($in)',
62
- `$entropy = [System.Text.Encoding]::UTF8.GetBytes('${DPAPI_ENTROPY}')`,
63
- '$protected = [System.Security.Cryptography.ProtectedData]::Protect($in.ToArray(), $entropy, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
64
- '$bytes = [System.Text.Encoding]::ASCII.GetBytes([Convert]::ToBase64String($protected))',
65
- '[Console]::OpenStandardOutput().Write($bytes, 0, $bytes.Length)',
66
- ].join('; ')
67
-
68
- const UNPROTECT_SCRIPT = [
69
- "$ErrorActionPreference = 'Stop'",
70
- 'Add-Type -AssemblyName System.Security',
71
- '$b64 = [System.Text.Encoding]::ASCII.GetString((& { $in = New-Object System.IO.MemoryStream; [Console]::OpenStandardInput().CopyTo($in); $in.ToArray() })).Trim()',
72
- '$protected = [Convert]::FromBase64String($b64)',
73
- `$entropy = [System.Text.Encoding]::UTF8.GetBytes('${DPAPI_ENTROPY}')`,
74
- '$plain = [System.Security.Cryptography.ProtectedData]::Unprotect($protected, $entropy, [System.Security.Cryptography.DataProtectionScope]::CurrentUser)',
75
- '[Console]::OpenStandardOutput().Write($plain, 0, $plain.Length)',
76
- ].join('; ')
77
-
78
- const PROTECT_ENCODED = encodePowerShell(PROTECT_SCRIPT)
79
- const UNPROTECT_ENCODED = encodePowerShell(UNPROTECT_SCRIPT)
80
-
81
- function encodePowerShell(script: string): string {
82
- return Buffer.from(script, 'utf16le').toString('base64')
83
- }
84
-
85
- function powerShellInvocation(encoded: string, env: NodeJS.ProcessEnv): { command: string; args: string[] } {
86
- const root = env['SystemRoot'] || env['SYSTEMROOT'] || 'C:\\Windows'
87
- return {
88
- command: path.win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
89
- args: ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
90
- }
91
- }
92
-
93
- /** Which DPAPI operation a process invocation asked for. Args never carry secrets. */
94
- export function windowsDpapiOperation(args: readonly string[]): 'protect' | 'unprotect' | null {
95
- const index = args.indexOf('-EncodedCommand')
96
- const encoded = index >= 0 ? args[index + 1] : undefined
97
- if (encoded === PROTECT_ENCODED) return 'protect'
98
- if (encoded === UNPROTECT_ENCODED) return 'unprotect'
99
- return null
100
- }
101
-
102
- export const defaultProcessRunner: ProcessRunner = {
103
- run(spec) {
104
- const result = spawnSync(spec.command, [...spec.args], {
105
- input: spec.input,
106
- timeout: spec.timeoutMs ?? DPAPI_TIMEOUT_MS,
107
- maxBuffer: 1024 * 1024,
108
- windowsHide: true,
109
- })
110
- const stdout = Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.alloc(0)
111
- if (result.error) return { status: 1, stdout: Buffer.alloc(0) }
112
- return { status: result.status ?? 1, stdout }
113
- },
114
- }
115
-
116
- /**
117
- * macOS Keychain via the first-party `security` Security.framework client.
118
- *
119
- * `security add-generic-password -w <value>` puts the value in process argv.
120
- * Interactive mode instead parses a command from stdin, so the credential
121
- * crosses the helper boundary only through the pipe. `-X` keeps arbitrary
122
- * serialized bytes out of the command parser's quoting rules.
123
- */
124
- export class KeychainStore implements CredentialStore {
125
- private readonly run: ProcessRunner
126
-
127
- constructor(options: CredentialStoreOptions = {}) {
128
- this.run = options.run ?? defaultProcessRunner
129
- }
130
-
131
- load(): MachineCredential | null {
132
- try {
133
- const raw = execFileSync(
134
- 'security',
135
- ['find-generic-password', '-s', SERVICE, '-a', 'machine', '-w'],
136
- { stdio: ['ignore', 'pipe', 'ignore'] },
137
- )
138
- .toString()
139
- .trim()
140
- return parseCredentialJson(raw)
141
- } catch {
142
- return null
143
- }
144
- }
145
-
146
- save(credential: MachineCredential): void {
147
- const serialized = Buffer.from(JSON.stringify(serializeCredential(credential)), 'utf8')
148
- const command = Buffer.from(
149
- `add-generic-password -U -s ${SERVICE} -a machine -X ${serialized.toString('hex')}\n`,
150
- 'ascii',
151
- )
152
- const result = this.run.run({
153
- command: 'security',
154
- args: ['-q', '-i'],
155
- input: command,
156
- timeoutMs: KEYCHAIN_TIMEOUT_MS,
157
- })
158
- if (result.status !== 0) {
159
- throw new Error('macOS Keychain credential save failed')
160
- }
161
- }
162
-
163
- clear(): void {
164
- try {
165
- execFileSync('security', ['delete-generic-password', '-s', SERVICE, '-a', 'machine'], {
166
- stdio: 'ignore',
167
- })
168
- } catch {
169
- // nothing stored
170
- }
171
- }
172
-
173
- describe(): string {
174
- return `macOS Keychain (${SERVICE})`
175
- }
176
- }
177
-
178
- /**
179
- * Explicit plaintext file store.
180
- *
181
- * Default on Linux, where mode 0600 is a real ACL. Also the
182
- * `NOTIFAI_CREDENTIALS=file` development and test escape hatch on every
183
- * platform. POSIX mode bits are not an NTFS ACL, so this is not a protected
184
- * Windows store.
185
- */
186
- export class FileStore implements CredentialStore {
187
- private readonly env: NodeJS.ProcessEnv
188
- private readonly platform: NodeJS.Platform
189
-
190
- constructor(env: NodeJS.ProcessEnv = process.env, options: CredentialStoreOptions = {}) {
191
- this.env = env
192
- this.platform = options.platform ?? process.platform
193
- }
194
-
195
- private filePath(): string {
196
- return path.join(globalConfigDir(this.env, this.platform), 'credentials.json')
197
- }
198
-
199
- load(): MachineCredential | null {
200
- const file = this.filePath()
201
- if (!existsSync(file)) return null
202
- try {
203
- return parseCredentialJson(readFileSync(file, 'utf8'))
204
- } catch {
205
- return null
206
- }
207
- }
208
-
209
- save(credential: MachineCredential): void {
210
- atomicWriteFileSync(this.filePath(), `${JSON.stringify(serializeCredential(credential), null, 2)}\n`, {
211
- mode: 0o600,
212
- preserveMode: false,
213
- requireCurrentUserOwner: true,
214
- })
215
- }
216
-
217
- clear(): void {
218
- rmSync(this.filePath(), { force: true })
219
- }
220
-
221
- describe(): string {
222
- const file = this.filePath()
223
- if (this.platform === 'win32') {
224
- return `plaintext file ${file} (NOTIFAI_CREDENTIALS=file; POSIX mode bits are not protection on NTFS)`
225
- }
226
- return `plaintext file ${file} (mode 0600)`
227
- }
228
- }
229
-
230
- /**
231
- * Windows default: DPAPI CurrentUser via first-party PowerShell.
232
- *
233
- * The protected blob lives under LOCALAPPDATA, never roaming APPDATA.
234
- * Plaintext crosses the process boundary only on stdin/stdout — never as a
235
- * command-line argument.
236
- */
237
- export class WindowsDpapiStore implements CredentialStore {
238
- private readonly env: NodeJS.ProcessEnv
239
- private readonly run: ProcessRunner
240
-
241
- constructor(env: NodeJS.ProcessEnv = process.env, options: CredentialStoreOptions = {}) {
242
- this.env = env
243
- this.run = options.run ?? defaultProcessRunner
244
- }
245
-
246
- private filePath(): string | null {
247
- const local = this.env['LOCALAPPDATA']
248
- if (typeof local !== 'string' || local.trim() === '') return null
249
- return path.join(local, 'notifai', DPAPI_FILE)
250
- }
251
-
252
- load(): MachineCredential | null {
253
- const file = this.filePath()
254
- if (file === null || !existsSync(file)) return null
255
- let text: string
256
- try {
257
- text = readFileSync(file, 'utf8')
258
- } catch {
259
- return null
260
- }
261
- const blob = parseDpapiEnvelope(text)
262
- if (blob === null) return null
263
- const plain = this.unprotect(blob)
264
- return plain === null ? null : parseCredentialJson(plain)
265
- }
266
-
267
- save(credential: MachineCredential): void {
268
- const file = this.filePath()
269
- if (file === null) {
270
- throw new Error('Windows credential store path is not configured (LOCALAPPDATA is missing)')
271
- }
272
- const blob = this.protect(Buffer.from(JSON.stringify(serializeCredential(credential)), 'utf8'))
273
- atomicWriteFileSync(file, `${JSON.stringify({ format: DPAPI_FORMAT, data: blob })}\n`, {
274
- mode: 0o600,
275
- preserveMode: false,
276
- requireCurrentUserOwner: true,
277
- })
278
- }
279
-
280
- clear(): void {
281
- const file = this.filePath()
282
- if (file === null) return
283
- rmSync(file, { force: true })
284
- }
285
-
286
- describe(): string {
287
- const file = this.filePath()
288
- if (file === null) return 'Windows DPAPI current-user (LOCALAPPDATA unset)'
289
- return `Windows DPAPI current-user (${file})`
290
- }
291
-
292
- private protect(plaintext: Buffer): string {
293
- const result = this.invoke('protect', plaintext)
294
- if (result.status !== 0) {
295
- throw new Error('Windows credential protection failed')
296
- }
297
- const encoded = result.stdout.toString('utf8').trim()
298
- if (!isBase64(encoded)) {
299
- throw new Error('Windows credential protection failed')
300
- }
301
- return encoded
302
- }
303
-
304
- private unprotect(blob: string): string | null {
305
- const result = this.invoke('unprotect', Buffer.from(blob, 'utf8'))
306
- if (result.status !== 0) return null
307
- const plain = result.stdout.toString('utf8')
308
- return plain === '' ? null : plain
309
- }
310
-
311
- private invoke(operation: 'protect' | 'unprotect', input: Buffer): RunCommandResult {
312
- const encoded = operation === 'protect' ? PROTECT_ENCODED : UNPROTECT_ENCODED
313
- const invocation = powerShellInvocation(encoded, this.env)
314
- return this.run.run({
315
- command: invocation.command,
316
- args: invocation.args,
317
- input,
318
- timeoutMs: DPAPI_TIMEOUT_MS,
319
- })
320
- }
321
- }
322
-
323
- function serializeCredential(credential: MachineCredential): Record<string, string> {
324
- return { format: CREDENTIAL_FORMAT, ...credential }
325
- }
326
-
327
- function parseCredentialJson(raw: string): MachineCredential | null {
328
- try {
329
- return parseCredential(JSON.parse(raw) as unknown)
330
- } catch {
331
- return null
332
- }
333
- }
334
-
335
- function parseCredential(raw: unknown): MachineCredential | null {
336
- if (raw === null || typeof raw !== 'object') return null
337
- const value = raw as Record<string, unknown>
338
- const format = value['format']
339
- // Unversioned credentials are the pre-epoch v1 representation. A future
340
- // format is left untouched on disk/keychain and treated as unavailable;
341
- // interpreting it as v1 could silently change authentication meaning.
342
- if (format !== undefined && format !== CREDENTIAL_FORMAT) return null
343
- const machineId = value['machineId']
344
- const secret = value['secret']
345
- const baseUrl = value['baseUrl']
346
- const machineName = value['machineName']
347
- if (
348
- typeof machineId !== 'string' ||
349
- machineId === '' ||
350
- typeof secret !== 'string' ||
351
- secret === '' ||
352
- typeof baseUrl !== 'string' ||
353
- baseUrl === '' ||
354
- typeof machineName !== 'string' ||
355
- machineName === ''
356
- ) {
357
- return null
358
- }
359
- return { machineId, secret, baseUrl, machineName }
360
- }
361
-
362
- function parseDpapiEnvelope(text: string): string | null {
363
- let parsed: unknown
364
- try {
365
- parsed = JSON.parse(text)
366
- } catch {
367
- return null
368
- }
369
- if (parsed === null || typeof parsed !== 'object') return null
370
- const value = parsed as Record<string, unknown>
371
- if (value['format'] !== DPAPI_FORMAT) return null
372
- const data = value['data']
373
- if (typeof data !== 'string' || !isBase64(data)) return null
374
- return data
375
- }
376
-
377
- function isBase64(value: string): boolean {
378
- return value.length > 0 && value.length % 4 === 0 && /^[A-Za-z0-9+/]+={0,2}$/.test(value)
379
- }
380
-
381
- function keychainProbeSucceeds(): boolean {
382
- try {
383
- execFileSync('security', ['help'], { stdio: 'ignore' })
384
- return true
385
- } catch {
386
- return false
387
- }
388
- }
389
-
390
- export function defaultCredentialStore(
391
- env: NodeJS.ProcessEnv = process.env,
392
- options: CredentialStoreOptions = {},
393
- ): CredentialStore {
394
- const platform = options.platform ?? process.platform
395
- if (env['NOTIFAI_CREDENTIALS'] === 'file') return new FileStore(env, { platform })
396
- if (platform === 'win32') return new WindowsDpapiStore(env, options)
397
- const keychain =
398
- options.keychainAvailable !== undefined
399
- ? options.keychainAvailable
400
- : platform === 'darwin' && keychainProbeSucceeds()
401
- if (keychain) return new KeychainStore(options)
402
- return new FileStore(env, { platform })
403
- }
@@ -1,64 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { describe, expect, it } from 'vitest'
3
- import { withFileLock, type FileLockObservation } from './file-lock.js'
4
-
5
- const worker = process.env['NOTIFAI_FILE_LOCK_WORKER']
6
- const sleep = new Int32Array(new SharedArrayBuffer(4))
7
-
8
- function waitFor(file: string): void {
9
- const deadline = Date.now() + 15_000
10
- while (!existsSync(file)) {
11
- if (Date.now() >= deadline) throw new Error(`timed out waiting for ${file}`)
12
- Atomics.wait(sleep, 0, 0, 2)
13
- }
14
- }
15
-
16
- if (worker === undefined) {
17
- describe('file lock process worker', () => {
18
- it('stays dormant outside the cross-process regressions', () => {
19
- expect(worker).toBeUndefined()
20
- })
21
- })
22
- } else {
23
- describe('file lock process worker', () => {
24
- it('runs one instrumented lock contender', () => {
25
- const pauseAt = process.env['NOTIFAI_FILE_LOCK_PAUSE_AT']
26
- const pausePath = process.env['NOTIFAI_FILE_LOCK_PAUSED']!
27
- const continuePath = process.env['NOTIFAI_FILE_LOCK_CONTINUE']!
28
- const enteredPath = process.env['NOTIFAI_FILE_LOCK_ENTERED']!
29
- const releasePath = process.env['NOTIFAI_FILE_LOCK_RELEASE']!
30
- const waitPath = process.env['NOTIFAI_FILE_LOCK_WAITS']!
31
- let waits = 0
32
-
33
- withFileLock(
34
- process.env['NOTIFAI_FILE_LOCK_PATH']!,
35
- () => {
36
- writeFileSync(enteredPath, worker)
37
- if (process.env['NOTIFAI_FILE_LOCK_ACTION'] === 'crash') {
38
- process.kill(process.pid, 'SIGKILL')
39
- throw new Error('SIGKILL did not terminate the worker')
40
- }
41
- waitFor(releasePath)
42
- },
43
- {
44
- waitMs: 15_000,
45
- observe(observation: FileLockObservation) {
46
- if (
47
- observation.phase === 'choosing-published' ||
48
- observation.phase === 'stale-entry'
49
- ) {
50
- writeFileSync(pausePath, observation.entry)
51
- if (pauseAt === observation.phase) waitFor(continuePath)
52
- }
53
- if (observation.phase === 'waiting') {
54
- waits += 1
55
- writeFileSync(waitPath, String(waits))
56
- }
57
- },
58
- },
59
- )
60
-
61
- expect(readFileSync(enteredPath, 'utf8')).toBe(worker)
62
- })
63
- })
64
- }