@adea-ai/plugins 4.0.0 → 4.0.6

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 (147) hide show
  1. package/dist/agent-plugins-catalog.d.ts +2 -4
  2. package/dist/agent-plugins-catalog.js +42 -50
  3. package/dist/agent-plugins-catalog.js.map +1 -1
  4. package/dist/agent-plugins-schema.d.ts +8 -0
  5. package/dist/agent-plugins-schema.js +10 -1
  6. package/dist/agent-plugins-schema.js.map +1 -1
  7. package/dist/artifacts.d.ts +57 -0
  8. package/dist/artifacts.js +159 -0
  9. package/dist/artifacts.js.map +1 -0
  10. package/dist/build.d.ts +20 -0
  11. package/dist/build.js +103 -0
  12. package/dist/build.js.map +1 -0
  13. package/dist/capabilities.d.ts +7 -0
  14. package/dist/capabilities.js +116 -0
  15. package/dist/capabilities.js.map +1 -0
  16. package/dist/catalog-store.d.ts +76 -0
  17. package/dist/catalog-store.js +463 -0
  18. package/dist/catalog-store.js.map +1 -0
  19. package/dist/concurrency.d.ts +1 -0
  20. package/dist/concurrency.js +15 -0
  21. package/dist/concurrency.js.map +1 -0
  22. package/dist/consumer-index.d.ts +7 -29
  23. package/dist/consumer-index.js +24 -15
  24. package/dist/consumer-index.js.map +1 -1
  25. package/dist/digest.d.ts +13 -0
  26. package/dist/digest.js +47 -0
  27. package/dist/digest.js.map +1 -0
  28. package/dist/fs-atomic.d.ts +7 -0
  29. package/dist/fs-atomic.js +30 -0
  30. package/dist/fs-atomic.js.map +1 -0
  31. package/dist/harness.js +2 -1
  32. package/dist/harness.js.map +1 -1
  33. package/dist/icon-mirror.d.ts +5 -4
  34. package/dist/icon-mirror.js +73 -72
  35. package/dist/icon-mirror.js.map +1 -1
  36. package/dist/icons.d.ts +2 -2
  37. package/dist/icons.js +4 -12
  38. package/dist/icons.js.map +1 -1
  39. package/dist/index.d.ts +16 -309
  40. package/dist/index.js +12 -1900
  41. package/dist/index.js.map +1 -1
  42. package/dist/integrity-manifest.d.ts +43 -13
  43. package/dist/integrity-manifest.js +28 -17
  44. package/dist/integrity-manifest.js.map +1 -1
  45. package/dist/json-values.d.ts +3 -0
  46. package/dist/json-values.js +10 -0
  47. package/dist/json-values.js.map +1 -0
  48. package/dist/plugin-release.d.ts +38 -0
  49. package/dist/plugin-release.js +321 -0
  50. package/dist/plugin-release.js.map +1 -0
  51. package/dist/plugin-skips.d.ts +19 -0
  52. package/dist/plugin-skips.js +65 -0
  53. package/dist/plugin-skips.js.map +1 -0
  54. package/dist/policy.d.ts +13 -0
  55. package/dist/policy.js +26 -0
  56. package/dist/policy.js.map +1 -0
  57. package/dist/portable-catalog.d.ts +4 -4
  58. package/dist/portable-catalog.js +14 -126
  59. package/dist/portable-catalog.js.map +1 -1
  60. package/dist/portable-verify.d.ts +2 -0
  61. package/dist/portable-verify.js +68 -0
  62. package/dist/portable-verify.js.map +1 -0
  63. package/dist/publication.d.ts +12 -12
  64. package/dist/publication.js +31 -21
  65. package/dist/publication.js.map +1 -1
  66. package/dist/published-output.d.ts +17 -0
  67. package/dist/published-output.js +49 -0
  68. package/dist/published-output.js.map +1 -0
  69. package/dist/site-icons.d.ts +4 -3
  70. package/dist/site-icons.js +38 -11
  71. package/dist/site-icons.js.map +1 -1
  72. package/dist/snapshots.d.ts +39 -0
  73. package/dist/snapshots.js +238 -0
  74. package/dist/snapshots.js.map +1 -0
  75. package/dist/source-resolution.d.ts +28 -0
  76. package/dist/source-resolution.js +158 -0
  77. package/dist/source-resolution.js.map +1 -0
  78. package/dist/sources.d.ts +5 -1
  79. package/dist/sources.js +21 -27
  80. package/dist/sources.js.map +1 -1
  81. package/dist/synchronize.d.ts +44 -0
  82. package/dist/synchronize.js +118 -0
  83. package/dist/synchronize.js.map +1 -0
  84. package/dist/taxonomy.d.ts +23 -0
  85. package/dist/taxonomy.js +62 -0
  86. package/dist/taxonomy.js.map +1 -0
  87. package/dist/upstream.d.ts +42 -0
  88. package/dist/upstream.js +179 -0
  89. package/dist/upstream.js.map +1 -0
  90. package/dist/verify-artifacts.d.ts +36 -0
  91. package/dist/verify-artifacts.js +286 -0
  92. package/dist/verify-artifacts.js.map +1 -0
  93. package/native/build-provenance.txt +5 -5
  94. package/native/staged-reader-win32-arm64.node +0 -0
  95. package/native/staged-reader-win32-x64.node +0 -0
  96. package/package.json +3 -3
  97. package/src/agent-plugins-catalog.test.ts +2 -2
  98. package/src/agent-plugins-catalog.ts +46 -49
  99. package/src/agent-plugins-schema.ts +11 -1
  100. package/src/artifacts.test.ts +80 -0
  101. package/src/artifacts.ts +234 -0
  102. package/src/build.ts +131 -0
  103. package/src/capabilities.ts +153 -0
  104. package/src/catalog-store.ts +519 -0
  105. package/src/concurrency.ts +19 -0
  106. package/src/consumer-index.test.ts +147 -16
  107. package/src/consumer-index.ts +35 -43
  108. package/src/default-head.test.ts +143 -0
  109. package/src/digest.ts +55 -0
  110. package/src/fs-atomic.ts +32 -0
  111. package/src/harness.test.ts +4 -32
  112. package/src/harness.ts +2 -1
  113. package/src/icon-mirror.ts +78 -72
  114. package/src/icons.test.ts +0 -4
  115. package/src/icons.ts +4 -4
  116. package/src/index.test.ts +172 -46
  117. package/src/index.ts +58 -2562
  118. package/src/integrity-manifest.ts +41 -21
  119. package/src/json-values.ts +11 -0
  120. package/src/manifest-retry.test.ts +49 -0
  121. package/src/plugin-release.ts +457 -0
  122. package/src/plugin-skips.ts +110 -0
  123. package/src/policy.ts +39 -0
  124. package/src/portable-catalog.test.ts +0 -5
  125. package/src/portable-catalog.ts +16 -143
  126. package/src/portable-verify.ts +71 -0
  127. package/src/publication.test.ts +29 -1
  128. package/src/publication.ts +38 -22
  129. package/src/published-output.ts +60 -0
  130. package/src/site-icons.test.ts +73 -1
  131. package/src/site-icons.ts +31 -14
  132. package/src/snapshots.ts +287 -0
  133. package/src/source-config-sync.test.ts +84 -0
  134. package/src/source-digest.test.ts +61 -8
  135. package/src/source-resolution.ts +233 -0
  136. package/src/sources-json.test.ts +18 -2
  137. package/src/sources.ts +27 -33
  138. package/src/synchronize.ts +213 -0
  139. package/src/taxonomy.ts +87 -0
  140. package/src/upstream.ts +194 -0
  141. package/src/verify-artifacts.ts +365 -0
  142. package/src/write-artifacts.test.ts +374 -0
  143. package/dist/source-digest.d.ts +0 -2
  144. package/dist/source-digest.js +0 -12
  145. package/dist/source-digest.js.map +0 -1
  146. package/native/README.md +0 -24
  147. package/src/source-digest.ts +0 -14
@@ -0,0 +1,374 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import {
3
+ mkdtemp,
4
+ mkdir,
5
+ readFile,
6
+ readdir,
7
+ rename,
8
+ rm,
9
+ symlink,
10
+ utimes,
11
+ writeFile,
12
+ } from 'node:fs/promises'
13
+ import { tmpdir } from 'node:os'
14
+ import { join } from 'node:path'
15
+ import { fileURLToPath } from 'node:url'
16
+ import { describe, expect, test } from 'bun:test'
17
+ import {
18
+ catalogFingerprint,
19
+ withCatalogRead,
20
+ writeArtifacts,
21
+ type GeneratedArtifacts,
22
+ type PublicationStep,
23
+ } from './index.js'
24
+ import { catalogPathsIn } from './catalog-store.js'
25
+
26
+ const artifacts = (name: string) =>
27
+ ({ 'catalog.v1.json': name, 'integrity.json': name }) as GeneratedArtifacts
28
+
29
+ const child = fileURLToPath(new URL('../test-support/catalog-writer-child.ts', import.meta.url))
30
+
31
+ /** Runs a writer in its own process, optionally with its own TMPDIR. */
32
+ function runWriter(
33
+ output: string,
34
+ content: string,
35
+ crashAt: PublicationStep | 'none',
36
+ options: { waitMs?: number; tmp?: string } = {}
37
+ ) {
38
+ const result = spawnSync(
39
+ process.execPath,
40
+ [
41
+ child,
42
+ output,
43
+ content,
44
+ crashAt,
45
+ ...(options.waitMs === undefined ? [] : [String(options.waitMs)]),
46
+ ],
47
+ {
48
+ encoding: 'utf8',
49
+ env: {
50
+ ...process.env,
51
+ ...(options.tmp ? { TMPDIR: options.tmp, TMP: options.tmp, TEMP: options.tmp } : {}),
52
+ },
53
+ }
54
+ )
55
+ // Windows reports a forced termination as an exit status, not a signal.
56
+ const killed =
57
+ result.signal === 'SIGKILL' || (process.platform === 'win32' && result.status !== 0)
58
+ return { stdout: result.stdout.trim(), stderr: result.stderr, killed }
59
+ }
60
+
61
+ /** A promise the test resolves to let a paused writer continue. */
62
+ function gate() {
63
+ let open!: () => void
64
+ const opened = new Promise<void>((resolve) => {
65
+ open = resolve
66
+ })
67
+ return { open, opened }
68
+ }
69
+
70
+ async function withParent(run: (parent: string) => Promise<void>) {
71
+ const parent = await mkdtemp(join(tmpdir(), 'catalog-writers-'))
72
+ try {
73
+ await run(parent)
74
+ } finally {
75
+ await rm(parent, { recursive: true, force: true })
76
+ }
77
+ }
78
+
79
+ const read = (output: string) => readFile(join(output, 'catalog.v1.json'), 'utf8')
80
+ /** Coordination leftovers beside an output; a settled output has none. */
81
+ const leftovers = async (parent: string) =>
82
+ (await readdir(parent)).filter(
83
+ (name) => name.startsWith('.catalog-') || name.endsWith('.previous')
84
+ )
85
+
86
+ describe('writer coordination', () => {
87
+ test('writers with different temporary directories exclude each other', () =>
88
+ withParent(async (parent) => {
89
+ const output = join(parent, 'generated')
90
+ await writeArtifacts(output, artifacts('first'))
91
+ const otherTmp = join(parent, 'other-tmp')
92
+ await mkdir(otherTmp)
93
+ const paused = gate()
94
+ const reached = gate()
95
+ const holder = writeArtifacts(output, artifacts('holder'), {
96
+ onStep: async (step) => {
97
+ if (step !== 'staged') return
98
+ reached.open()
99
+ await paused.opened
100
+ },
101
+ })
102
+ await reached.opened
103
+ // The diagnosed race: the lock lived in TMPDIR, so a writer with another
104
+ // TMPDIR took its own lock and swapped concurrently. Both in-process and
105
+ // as a separate process with its own TMPDIR, the second writer now meets
106
+ // the first writer's lock.
107
+ const previousTmp = process.env.TMPDIR
108
+ process.env.TMPDIR = otherTmp
109
+ try {
110
+ await expect(writeArtifacts(output, artifacts('intruder'), { waitMs: 0 })).rejects.toThrow(
111
+ 'CATALOG_WRITE_IN_PROGRESS'
112
+ )
113
+ } finally {
114
+ if (previousTmp === undefined) delete process.env.TMPDIR
115
+ else process.env.TMPDIR = previousTmp
116
+ }
117
+ const separate = runWriter(output, 'separate', 'none', { waitMs: 0, tmp: otherTmp })
118
+ expect(separate.stdout).toContain('CATALOG_WRITE_IN_PROGRESS')
119
+ expect(await read(output)).toBe('first')
120
+ paused.open()
121
+ await holder
122
+ expect(await read(output)).toBe('holder')
123
+ expect(await readdir(otherTmp)).toEqual([])
124
+ expect(await leftovers(parent)).toEqual([])
125
+ }))
126
+
127
+ test('a waiting writer publishes after the holder, never in between', () =>
128
+ withParent(async (parent) => {
129
+ const output = join(parent, 'generated')
130
+ const steps: string[] = []
131
+ const paused = gate()
132
+ const reached = gate()
133
+ const first = writeArtifacts(output, artifacts('first'), {
134
+ onStep: async (step) => {
135
+ steps.push(`first:${step}`)
136
+ if (step !== 'backed-up') return
137
+ reached.open()
138
+ await paused.opened
139
+ },
140
+ })
141
+ await reached.opened
142
+ const contending = gate()
143
+ const second = writeArtifacts(output, artifacts('second'), {
144
+ pollMs: 1,
145
+ onStep: (step) => {
146
+ steps.push(`second:${step}`)
147
+ if (step === 'acquiring') contending.open()
148
+ },
149
+ })
150
+ await contending.opened
151
+ // Let the second writer poll the held lock a few times.
152
+ await new Promise((resolve) => setTimeout(resolve, 20))
153
+ paused.open()
154
+ await Promise.all([first, second])
155
+ expect(steps).toEqual([
156
+ 'first:acquiring',
157
+ 'first:staged',
158
+ 'first:backed-up',
159
+ 'second:acquiring',
160
+ 'first:swapped',
161
+ 'first:cleaned',
162
+ 'second:staged',
163
+ 'second:backed-up',
164
+ 'second:swapped',
165
+ 'second:cleaned',
166
+ ])
167
+ expect(await read(output)).toBe('second')
168
+ expect(await leftovers(parent)).toEqual([])
169
+ }))
170
+
171
+ test('outputs that differ only in case share one lock', () => {
172
+ expect(catalogPathsIn('/x', 'Generated').lock).toBe(catalogPathsIn('/x', 'generated').lock)
173
+ expect(catalogPathsIn('/x', 'generated').lock).not.toBe(catalogPathsIn('/x', 'other').lock)
174
+ expect(
175
+ catalogPathsIn('/x', 'generated').lock.startsWith(join('/x', '.catalog-generated-'))
176
+ ).toBe(true)
177
+ })
178
+ })
179
+
180
+ describe('publication against a read snapshot', () => {
181
+ test('refuses to overwrite a catalog replaced after the base was read', () =>
182
+ withParent(async (parent) => {
183
+ const output = join(parent, 'generated')
184
+ await writeArtifacts(output, artifacts('base'))
185
+ // The diagnosed race: a scoped sync validated the output and read its
186
+ // lock, another writer replaced the catalog, and the sync then published
187
+ // over it. The base read and the publication now compare fingerprints.
188
+ const base = await withCatalogRead(output, () => catalogFingerprint(output))
189
+ await writeArtifacts(output, artifacts('concurrent'))
190
+ await expect(
191
+ writeArtifacts(output, artifacts('stale'), { expectedBase: base })
192
+ ).rejects.toThrow('CATALOG_BASE_CHANGED')
193
+ expect(await read(output)).toBe('concurrent')
194
+ const fresh = await withCatalogRead(output, () => catalogFingerprint(output))
195
+ await writeArtifacts(output, artifacts('rebased'), { expectedBase: fresh })
196
+ expect(await read(output)).toBe('rebased')
197
+ expect(await leftovers(parent)).toEqual([])
198
+ }))
199
+
200
+ test('an absent output is a base, and a first publication must still find it absent', () =>
201
+ withParent(async (parent) => {
202
+ const output = join(parent, 'generated')
203
+ const base = await withCatalogRead(output, () => catalogFingerprint(output))
204
+ expect(base).toBe('absent')
205
+ await writeArtifacts(output, artifacts('other'))
206
+ await expect(
207
+ writeArtifacts(output, artifacts('mine'), { expectedBase: base })
208
+ ).rejects.toThrow('CATALOG_BASE_CHANGED')
209
+ expect(await read(output)).toBe('other')
210
+ }))
211
+
212
+ test('a coordinated reader never observes the swap window', () =>
213
+ withParent(async (parent) => {
214
+ const output = join(parent, 'generated')
215
+ await writeArtifacts(output, artifacts('old'))
216
+ const paused = gate()
217
+ const reached = gate()
218
+ const writer = writeArtifacts(output, artifacts('new'), {
219
+ onStep: async (step) => {
220
+ if (step !== 'backed-up') return
221
+ reached.open()
222
+ await paused.opened
223
+ },
224
+ })
225
+ await reached.opened
226
+ // Between the renames the output does not exist on disk.
227
+ await expect(read(output)).rejects.toThrow()
228
+ const reader = withCatalogRead(output, () => read(output), { pollMs: 1 })
229
+ await new Promise((resolve) => setTimeout(resolve, 10))
230
+ paused.open()
231
+ expect(await reader).toBe('new')
232
+ await writer
233
+ }))
234
+ })
235
+
236
+ describe('crash recovery', () => {
237
+ const cases: readonly { step: PublicationStep; expected: string }[] = [
238
+ { step: 'staged', expected: 'old' },
239
+ { step: 'backed-up', expected: 'old' },
240
+ { step: 'swapped', expected: 'new' },
241
+ { step: 'cleaned', expected: 'new' },
242
+ ]
243
+ for (const { step, expected } of cases) {
244
+ test(`a writer killed after "${step}" is ${expected === 'old' ? 'rolled back' : 'completed'} by the next reader`, () =>
245
+ withParent(async (parent) => {
246
+ const output = join(parent, 'generated')
247
+ await writeArtifacts(output, artifacts('old'))
248
+ const crashed = runWriter(output, 'new', step)
249
+ expect(crashed.killed).toBe(true)
250
+ expect(crashed.stdout).toBe('')
251
+ // The killed process left its lock behind; its holder is gone.
252
+ expect((await readdir(parent)).some((name) => name.endsWith('.lock'))).toBe(true)
253
+ expect(await withCatalogRead(output, () => read(output), { waitMs: 0 })).toBe(expected)
254
+ expect(await leftovers(parent)).toEqual([])
255
+ await writeArtifacts(output, artifacts('after'), { waitMs: 0 })
256
+ expect(await read(output)).toBe('after')
257
+ }))
258
+ }
259
+
260
+ test('a writer killed during a first publication leaves no output and no residue', () =>
261
+ withParent(async (parent) => {
262
+ const output = join(parent, 'generated')
263
+ expect(runWriter(output, 'new', 'staged').killed).toBe(true)
264
+ await writeArtifacts(output, artifacts('next'), { waitMs: 0 })
265
+ expect(await read(output)).toBe('next')
266
+ expect(await leftovers(parent)).toEqual([])
267
+ }))
268
+
269
+ test('a holder on another host is broken only after its heartbeat lease expires', () =>
270
+ withParent(async (parent) => {
271
+ const output = join(parent, 'generated')
272
+ await writeArtifacts(output, artifacts('old'))
273
+ const { lock, owner } = catalogPathsIn(parent, 'generated')
274
+ await mkdir(lock)
275
+ await writeFile(owner, JSON.stringify({ token: 'remote', pid: 1, host: 'elsewhere.invalid' }))
276
+ await expect(
277
+ writeArtifacts(output, artifacts('blocked'), { waitMs: 0, staleAfterMs: 60_000 })
278
+ ).rejects.toThrow(/CATALOG_WRITE_IN_PROGRESS: .*pid 1 on elsewhere\.invalid/)
279
+ const old = new Date(Date.now() - 120_000)
280
+ await utimes(owner, old, old)
281
+ await writeArtifacts(output, artifacts('new'), { waitMs: 0, staleAfterMs: 60_000 })
282
+ expect(await read(output)).toBe('new')
283
+ expect(await leftovers(parent)).toEqual([])
284
+ }))
285
+
286
+ test('a holder that lost its lock refuses to swap', () =>
287
+ withParent(async (parent) => {
288
+ const output = join(parent, 'generated')
289
+ await writeArtifacts(output, artifacts('old'))
290
+ const { lock, owner } = catalogPathsIn(parent, 'generated')
291
+ const writer = writeArtifacts(output, artifacts('late'), {
292
+ onStep: async (step) => {
293
+ if (step !== 'staged') return
294
+ // Another contender judged this holder stale and took the lock.
295
+ await rename(lock, `${lock}.taken`)
296
+ await mkdir(lock)
297
+ await writeFile(owner, JSON.stringify({ token: 'other', pid: process.pid, host: 'x' }))
298
+ },
299
+ })
300
+ await expect(writer).rejects.toThrow('CATALOG_LOCK_LOST')
301
+ expect(await read(output)).toBe('old')
302
+ expect(JSON.parse(await readFile(owner, 'utf8')).token).toBe('other')
303
+ }))
304
+
305
+ test('preserves a backup whose provenance is unknown', () =>
306
+ withParent(async (parent) => {
307
+ const output = join(parent, 'generated')
308
+ await mkdir(output)
309
+ await writeFile(join(output, 'catalog.v1.json'), 'current')
310
+ await mkdir(`${output}.previous`)
311
+ await writeFile(join(`${output}.previous`, 'recovery'), 'saved')
312
+ await expect(writeArtifacts(output, artifacts('blocked'))).rejects.toThrow(
313
+ 'CATALOG_RECOVERY_REQUIRED'
314
+ )
315
+ expect(await readFile(join(`${output}.previous`, 'recovery'), 'utf8')).toBe('saved')
316
+ expect(await read(output)).toBe('current')
317
+ expect(await withCatalogRead(output, () => read(output))).toBe('current')
318
+ }))
319
+
320
+ test('a failed write keeps the previous catalog and releases the lock', () =>
321
+ withParent(async (parent) => {
322
+ const output = join(parent, 'generated')
323
+ await writeArtifacts(output, artifacts('previous'))
324
+ await expect(
325
+ writeArtifacts(output, { ...artifacts('broken'), 'missing/file': 'bad' })
326
+ ).rejects.toThrow('CATALOG_ARTIFACT_NAME_INVALID')
327
+ await expect(
328
+ writeArtifacts(output, artifacts('broken'), {
329
+ onStep: (step) => {
330
+ if (step === 'backed-up') throw new Error('injected failure')
331
+ },
332
+ })
333
+ ).rejects.toThrow('injected failure')
334
+ expect(await read(output)).toBe('previous')
335
+ expect(await leftovers(parent)).toEqual([])
336
+ await writeArtifacts(output, artifacts('later'), { waitMs: 0 })
337
+ expect(await read(output)).toBe('later')
338
+ }))
339
+ })
340
+
341
+ describe('reserved paths', () => {
342
+ test('refuses outputs inside coordination files or backups', () =>
343
+ withParent(async (parent) => {
344
+ const output = join(parent, 'generated')
345
+ const { lock } = catalogPathsIn(parent, 'generated')
346
+ for (const destination of [lock, `${output}.previous`, join(`${output}.previous`, 'x')])
347
+ await expect(writeArtifacts(destination, artifacts('blocked'))).rejects.toThrow(
348
+ 'CATALOG_WRITE_LOCK_RESERVED'
349
+ )
350
+ await mkdir(lock)
351
+ const alias = join(parent, 'lock-alias')
352
+ await symlink(lock, alias, 'dir')
353
+ await expect(writeArtifacts(join(alias, 'nested'), artifacts('blocked'))).rejects.toThrow(
354
+ 'CATALOG_WRITE_LOCK_RESERVED'
355
+ )
356
+ await rm(lock, { recursive: true })
357
+ // A backup can still be read by hand, without coordination.
358
+ await mkdir(`${output}.previous`)
359
+ await writeFile(join(`${output}.previous`, 'catalog.v1.json'), 'saved')
360
+ expect(await withCatalogRead(`${output}.previous`, () => read(`${output}.previous`))).toBe(
361
+ 'saved'
362
+ )
363
+ }))
364
+
365
+ test('preserves filesystem semantics for a symlink followed by dot dot', () =>
366
+ withParent(async (parent) => {
367
+ const real = join(parent, 'real')
368
+ await mkdir(join(real, 'child'), { recursive: true })
369
+ await symlink(join(real, 'child'), join(parent, 'link'), 'dir')
370
+ await writeArtifacts(`${parent}/link/../generated`, artifacts('real target'))
371
+ expect(await read(join(real, 'generated'))).toBe('real target')
372
+ await expect(read(join(parent, 'generated'))).rejects.toThrow()
373
+ }))
374
+ })
@@ -1,2 +0,0 @@
1
- /** Shared source-byte identity for catalog provenance and canonical package recipes. */
2
- export declare function sourceDigest(files: ReadonlyMap<string, Uint8Array>): string;
@@ -1,12 +0,0 @@
1
- import { createHash } from 'node:crypto';
2
- /** Shared source-byte identity for catalog provenance and canonical package recipes. */
3
- export function sourceDigest(files) {
4
- const hash = createHash('sha256');
5
- // English matches the compiler runtime; ordinal ties distinguish equivalent Unicode names.
6
- for (const [path, bytes] of [...files].toSorted(([a], [b]) => a.localeCompare(b, 'en') || (a < b ? -1 : a > b ? 1 : 0))) {
7
- hash.update(`${path.length}:${path}:${bytes.byteLength}:`);
8
- hash.update(bytes);
9
- }
10
- return `sha256:${hash.digest('hex')}`;
11
- }
12
- //# sourceMappingURL=source-digest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"source-digest.js","sourceRoot":"","sources":["../src/source-digest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAExC,wFAAwF;AACxF,MAAM,UAAU,YAAY,CAAC,KAAsC;IACjE,MAAM,IAAI,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAA;IACjC,2FAA2F;IAC3F,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC,QAAQ,CAC7C,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CACvE,EAAE,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,IAAI,IAAI,IAAI,KAAK,CAAC,UAAU,GAAG,CAAC,CAAA;QAC1D,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IACpB,CAAC;IACD,OAAO,UAAU,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAA;AACvC,CAAC"}
package/native/README.md DELETED
@@ -1,24 +0,0 @@
1
- # Windows staged reader
2
-
3
- The Windows reader opens the final path component with `CreateFileW` and
4
- `FILE_FLAG_OPEN_REPARSE_POINT`, refuses reparse points, directories and non-disk
5
- handles, then reads from that same handle on a Node-API async worker. The parent
6
- directory remains trusted, and regular-file contents may change through another
7
- handle. This is not filesystem confinement or a snapshot. The Windows reader
8
- caps input at 512 MiB and returns no bytes on failure; the POSIX reader retains
9
- its existing behavior. Missing paths retain `ENOENT`.
10
-
11
- `scripts/build-staged-reader.sh` builds x64 and arm64 modules on Linux from
12
- Zig 0.14.1 and Node 24.22.1 headers and import libraries, checking each pinned
13
- SHA-256 before use. It records the source commit and output hashes in
14
- `build-provenance.txt`. There is no runtime download or installation hook.
15
- The release workflow builds the modules before packing them; the package
16
- includes only the production modules and provenance, while the repository root
17
- build copies available production modules beside the compiled reader. Missing,
18
- invalid, or unsupported binaries fail closed.
19
-
20
- The Windows qualification workflow builds the same sources and exercises both
21
- architectures with Node and Bun, including size limits and concurrent pathname
22
- replacement. It also checks the source and compiled reader layouts. A successful
23
- cross-compile alone does not establish Windows compatibility; in particular,
24
- Bun's Windows loader must be tested with the Node import library.
@@ -1,14 +0,0 @@
1
- import { createHash } from 'node:crypto'
2
-
3
- /** Shared source-byte identity for catalog provenance and canonical package recipes. */
4
- export function sourceDigest(files: ReadonlyMap<string, Uint8Array>): string {
5
- const hash = createHash('sha256')
6
- // English matches the compiler runtime; ordinal ties distinguish equivalent Unicode names.
7
- for (const [path, bytes] of [...files].toSorted(
8
- ([a], [b]) => a.localeCompare(b, 'en') || (a < b ? -1 : a > b ? 1 : 0)
9
- )) {
10
- hash.update(`${path.length}:${path}:${bytes.byteLength}:`)
11
- hash.update(bytes)
12
- }
13
- return `sha256:${hash.digest('hex')}`
14
- }