@raidiant/notifai 10.1.0 → 10.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (204) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +3 -0
  3. package/dist/cli-bin.d.ts +21 -1
  4. package/dist/cli-bin.js +141 -10
  5. package/dist/cli-bin.js.map +1 -1
  6. package/dist/cli-update-contract.d.ts +7 -0
  7. package/dist/cli-update-contract.js +10 -0
  8. package/dist/cli-update-contract.js.map +1 -0
  9. package/dist/client.d.ts +6 -1
  10. package/dist/client.js +1 -0
  11. package/dist/client.js.map +1 -1
  12. package/dist/codex-wake.d.ts +2 -0
  13. package/dist/codex-wake.js +2 -1
  14. package/dist/codex-wake.js.map +1 -1
  15. package/dist/commands-auth.d.ts +15 -1
  16. package/dist/commands-auth.js +29 -11
  17. package/dist/commands-auth.js.map +1 -1
  18. package/dist/commands-core.d.ts +9 -2
  19. package/dist/commands-core.js +11 -4
  20. package/dist/commands-core.js.map +1 -1
  21. package/dist/commands-devices.d.ts +10 -1
  22. package/dist/commands-devices.js +17 -13
  23. package/dist/commands-devices.js.map +1 -1
  24. package/dist/commands-doctor.js +93 -49
  25. package/dist/commands-doctor.js.map +1 -1
  26. package/dist/commands-hooks.js +47 -10
  27. package/dist/commands-hooks.js.map +1 -1
  28. package/dist/commands-init.d.ts +16 -0
  29. package/dist/commands-init.js +254 -93
  30. package/dist/commands-init.js.map +1 -1
  31. package/dist/commands-send-support.js +3 -0
  32. package/dist/commands-send-support.js.map +1 -1
  33. package/dist/commands-send.d.ts +1 -1
  34. package/dist/commands-send.js +121 -35
  35. package/dist/commands-send.js.map +1 -1
  36. package/dist/commands-skill.js +72 -54
  37. package/dist/commands-skill.js.map +1 -1
  38. package/dist/commands-update.d.ts +10 -0
  39. package/dist/commands-update.js +141 -0
  40. package/dist/commands-update.js.map +1 -0
  41. package/dist/commands.d.ts +2 -1
  42. package/dist/commands.js +2 -1
  43. package/dist/commands.js.map +1 -1
  44. package/dist/hook-adapter.d.ts +2 -0
  45. package/dist/hook-adapter.js +12 -1
  46. package/dist/hook-adapter.js.map +1 -1
  47. package/dist/hooks.d.ts +43 -2
  48. package/dist/hooks.js +163 -21
  49. package/dist/hooks.js.map +1 -1
  50. package/dist/invocation-context.d.ts +1 -0
  51. package/dist/invocation-context.js +9 -1
  52. package/dist/invocation-context.js.map +1 -1
  53. package/dist/main-run.d.ts +1 -0
  54. package/dist/main-run.js +42 -0
  55. package/dist/main-run.js.map +1 -0
  56. package/dist/main.js +10 -31
  57. package/dist/main.js.map +1 -1
  58. package/dist/native-skills.d.ts +13 -3
  59. package/dist/native-skills.js +51 -12
  60. package/dist/native-skills.js.map +1 -1
  61. package/dist/node-floor.d.ts +19 -0
  62. package/dist/node-floor.js +39 -0
  63. package/dist/node-floor.js.map +1 -0
  64. package/dist/orca-session-title.d.ts +5 -4
  65. package/dist/orca-session-title.js +32 -19
  66. package/dist/orca-session-title.js.map +1 -1
  67. package/dist/platform.d.ts +11 -17
  68. package/dist/platform.js +20 -23
  69. package/dist/platform.js.map +1 -1
  70. package/dist/program.d.ts +2 -1
  71. package/dist/program.js +22 -5
  72. package/dist/program.js.map +1 -1
  73. package/dist/question-settlement-process.d.ts +15 -0
  74. package/dist/question-settlement-process.js +35 -0
  75. package/dist/question-settlement-process.js.map +1 -0
  76. package/dist/readiness.d.ts +25 -1
  77. package/dist/readiness.js +32 -2
  78. package/dist/readiness.js.map +1 -1
  79. package/dist/release.d.ts +5 -5
  80. package/dist/release.js +5 -5
  81. package/dist/setup-destinations.d.ts +23 -0
  82. package/dist/setup-destinations.js +44 -0
  83. package/dist/setup-destinations.js.map +1 -0
  84. package/dist/skill-integrity.d.ts +49 -0
  85. package/dist/skill-integrity.js +154 -0
  86. package/dist/skill-integrity.js.map +1 -0
  87. package/dist/skill-source/manifest.json +16 -0
  88. package/dist/skill-source/notifai/SKILL.md +390 -0
  89. package/dist/skill-source/notifai/references/harness-setup.md +220 -0
  90. package/package.json +11 -14
  91. package/src/atomic-file.ts +0 -198
  92. package/src/claude-wake.test.ts +0 -321
  93. package/src/claude-wake.ts +0 -499
  94. package/src/cli-bin.test.ts +0 -38
  95. package/src/cli-bin.ts +0 -67
  96. package/src/cli-release.test.ts +0 -46
  97. package/src/cli-release.ts +0 -78
  98. package/src/client.test.ts +0 -390
  99. package/src/client.ts +0 -345
  100. package/src/codex-wake.test.ts +0 -254
  101. package/src/codex-wake.ts +0 -297
  102. package/src/command-session.ts +0 -38
  103. package/src/commands-agent-sessions.ts +0 -72
  104. package/src/commands-auth.ts +0 -205
  105. package/src/commands-config.ts +0 -336
  106. package/src/commands-core.ts +0 -298
  107. package/src/commands-devices.test.ts +0 -15
  108. package/src/commands-devices.ts +0 -173
  109. package/src/commands-doctor.ts +0 -1440
  110. package/src/commands-guidance.ts +0 -181
  111. package/src/commands-harness-context.ts +0 -207
  112. package/src/commands-hook-shape.ts +0 -69
  113. package/src/commands-hooks.ts +0 -2268
  114. package/src/commands-init.ts +0 -878
  115. package/src/commands-io.ts +0 -108
  116. package/src/commands-logs.ts +0 -243
  117. package/src/commands-project.ts +0 -47
  118. package/src/commands-send-support.test.ts +0 -156
  119. package/src/commands-send-support.ts +0 -311
  120. package/src/commands-send.ts +0 -769
  121. package/src/commands-setup-proof.test.ts +0 -72
  122. package/src/commands-setup-proof.ts +0 -124
  123. package/src/commands-skill.test.ts +0 -44
  124. package/src/commands-skill.ts +0 -204
  125. package/src/commands-sounds.test.ts +0 -106
  126. package/src/commands-sounds.ts +0 -49
  127. package/src/commands.test.ts +0 -7828
  128. package/src/commands.ts +0 -75
  129. package/src/config-schema.test.ts +0 -153
  130. package/src/config-schema.ts +0 -349
  131. package/src/config.test.ts +0 -581
  132. package/src/config.ts +0 -475
  133. package/src/credentials.test.ts +0 -403
  134. package/src/credentials.ts +0 -403
  135. package/src/file-lock-process-worker.test.ts +0 -64
  136. package/src/file-lock.test.ts +0 -320
  137. package/src/file-lock.ts +0 -393
  138. package/src/fixtures/hermes-0.20.6-classic-cli-local.json +0 -50
  139. package/src/fixtures/openclaw-2026.7.1-2-agent-local.json +0 -57
  140. package/src/generated-session-label.ts +0 -155
  141. package/src/guidance-content.ts +0 -278
  142. package/src/guidance-render.ts +0 -56
  143. package/src/guidance.test.ts +0 -327
  144. package/src/guidance.ts +0 -233
  145. package/src/harness-integration.test.ts +0 -294
  146. package/src/harnesses.test.ts +0 -149
  147. package/src/harnesses.ts +0 -164
  148. package/src/hook-adapter.test.ts +0 -496
  149. package/src/hook-adapter.ts +0 -410
  150. package/src/hook-input.test.ts +0 -39
  151. package/src/hook-input.ts +0 -72
  152. package/src/hooks.test.ts +0 -5549
  153. package/src/hooks.ts +0 -3759
  154. package/src/install-hooks.test.ts +0 -1614
  155. package/src/install-hooks.ts +0 -1501
  156. package/src/interactive.test.ts +0 -387
  157. package/src/interactive.ts +0 -858
  158. package/src/invocation-context.test.ts +0 -352
  159. package/src/invocation-context.ts +0 -197
  160. package/src/logging-process-worker.test.ts +0 -45
  161. package/src/logging.test.ts +0 -544
  162. package/src/logging.ts +0 -698
  163. package/src/main.ts +0 -36
  164. package/src/native-skills.test.ts +0 -115
  165. package/src/native-skills.ts +0 -162
  166. package/src/onboarding-docs.test.ts +0 -25
  167. package/src/openclaw-plugin.ts +0 -409
  168. package/src/opencode-plugin.ts +0 -298
  169. package/src/orca-session-title.test.ts +0 -54
  170. package/src/orca-session-title.ts +0 -103
  171. package/src/pack-feedback-slice.test.ts +0 -212
  172. package/src/pack-feedback-slice.ts +0 -187
  173. package/src/platform.test.ts +0 -152
  174. package/src/platform.ts +0 -173
  175. package/src/program.test.ts +0 -282
  176. package/src/program.ts +0 -818
  177. package/src/project-enablement.test.ts +0 -55
  178. package/src/project-enablement.ts +0 -99
  179. package/src/question-caps.test.ts +0 -49
  180. package/src/question-timing.ts +0 -56
  181. package/src/readiness.test.ts +0 -25
  182. package/src/readiness.ts +0 -179
  183. package/src/release.ts +0 -51
  184. package/src/send-attempts.test.ts +0 -143
  185. package/src/send-attempts.ts +0 -188
  186. package/src/send.test.ts +0 -28
  187. package/src/send.ts +0 -436
  188. package/src/session-activation.ts +0 -61
  189. package/src/session-end-log-lock-worker.test.ts +0 -35
  190. package/src/session-labels.test.ts +0 -490
  191. package/src/session-labels.ts +0 -474
  192. package/src/skill.test.ts +0 -415
  193. package/src/sound-ref.test.ts +0 -31
  194. package/src/sound-ref.ts +0 -34
  195. package/src/trust-policy.test.ts +0 -701
  196. package/src/ui/banner.ts +0 -95
  197. package/src/ui/config-view.test.ts +0 -90
  198. package/src/ui/config-view.ts +0 -138
  199. package/src/ui/help.test.ts +0 -16
  200. package/src/ui/help.ts +0 -128
  201. package/src/ui/theme.test.ts +0 -92
  202. package/src/ui/theme.ts +0 -265
  203. package/src/url-policy.ts +0 -399
  204. 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
- }