pinokiod 8.2.23 → 8.2.24

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.
@@ -1,6 +1,6 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
- const { randomUUID } = require('crypto')
3
+ const { randomUUID, createHash } = require('crypto')
4
4
 
5
5
  // The managed CA bundle: the conda-forge ca-certificates package inside Miniforge.
6
6
  // Every app's Python HTTPS traffic is pointed at this one file so a revoked root is
@@ -83,25 +83,39 @@ async function readManagedBundle(file) {
83
83
  async function repairPipVendoredBundle(prefix, bundleFile, platform = process.platform) {
84
84
  const managed = await readManagedBundle(bundleFile).catch(() => null)
85
85
  if (!managed) return []
86
+ const sha256 = data => createHash('sha256').update(data).digest('hex')
86
87
  const repaired = []
87
88
  for (const target of await pipVendoredBundles(prefix, platform)) {
88
89
  const stat = await fs.promises.stat(target).catch(() => null)
89
90
  if (stat && !stat.isFile()) continue
91
+ const current = stat ? await fs.promises.readFile(target).catch(() => null) : null
90
92
  if (!stat) {
91
93
  // No pip here: nothing to do. pip present but its bundle gone (the user deleted a file
92
94
  // Pinokio could not replace): write a fresh one, or pip fails on every network call.
93
95
  const certifiDir = await fs.promises.stat(path.dirname(target)).catch(() => null)
94
96
  if (!certifiDir || !certifiDir.isDirectory()) continue
95
- } else if (stat.size === managed.length) {
96
- const current = await fs.promises.readFile(target).catch(() => null)
97
- if (current && current.equals(managed)) continue
97
+ } else if (current && current.equals(managed)) {
98
+ console.log(`[cert] pip's vendored CA bundle already current: ${target}`, JSON.stringify({
99
+ timestamp: new Date().toISOString(), size_bytes: current.length, sha256: sha256(current),
100
+ }))
101
+ continue
98
102
  }
99
103
  const temporary = path.join(path.dirname(target), `.cacert-${randomUUID()}.tmp`)
100
104
  try {
101
105
  await fs.promises.writeFile(temporary, managed, { mode: stat ? stat.mode & 0o777 : 0o644 })
102
106
  await fs.promises.rename(temporary, target)
107
+ console.log(`[cert] replaced pip's vendored CA bundle: ${target}`, JSON.stringify({
108
+ timestamp: new Date().toISOString(),
109
+ before_size_bytes: current ? current.length : stat ? stat.size : null,
110
+ after_size_bytes: managed.length,
111
+ before_sha256: current ? sha256(current) : null,
112
+ after_sha256: sha256(managed),
113
+ }))
103
114
  repaired.push(target)
104
115
  } catch (error) {
116
+ console.log(`[cert] could not replace pip's vendored CA bundle: ${target}`, JSON.stringify({
117
+ timestamp: new Date().toISOString(), error: error.message,
118
+ }))
105
119
  // The caller decides what a failed replacement means; it needs to know which file.
106
120
  error.target = target
107
121
  error.prefix = prefix
@@ -623,8 +623,8 @@ const init_folders = async (homedir, kernel) => {
623
623
  let is_relative = val.startsWith("./")
624
624
  if (is_absolute || is_relative) {
625
625
 
626
- // skip condarc and pipconfig => special case
627
- if (["PIP_CONFIG_FILE", "CONDARC", "HF_TOKEN_PATH"].includes(key)) {
626
+ // skip values that name a file, not a folder: config files and CA bundles
627
+ if (["PIP_CONFIG_FILE", "CONDARC", "HF_TOKEN_PATH", "SSL_CERT_FILE", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE", "PIP_CERT"].includes(kernel.platform === "win32" ? key.toUpperCase() : key)) {
628
628
  continue
629
629
  }
630
630
  // it's a path
package/kernel/shell.js CHANGED
@@ -979,9 +979,6 @@ class Shell {
979
979
  blocked.cause = error
980
980
  throw blocked
981
981
  }
982
- for (const target of repaired) {
983
- console.log(`[cert] replaced pip's vendored CA bundle: ${target}`)
984
- }
985
982
  return repaired
986
983
  }
987
984
  build (params) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.23",
3
+ "version": "8.2.24",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -310,6 +310,62 @@ test('Shell.activate stops the step when the pip bundle cannot be replaced', { s
310
310
  await fs.chmod(certifiDir, 0o700)
311
311
  })
312
312
 
313
+ test('init_folders never turns a certificate file path into a directory', async t => {
314
+ const Environment = require('../kernel/environment')
315
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'cert-env-folders-'))
316
+ t.after(() => fs.rm(root, { recursive: true, force: true }).catch(() => {}))
317
+ const app = path.join(root, 'api', 'demo.git')
318
+ await fs.mkdir(app, { recursive: true })
319
+ const lines = [
320
+ `SSL_CERT_FILE=${path.join(root, 'custom', 'ssl.pem')}`,
321
+ `REQUESTS_CA_BUNDLE=${path.join(root, 'custom', 'requests.pem')}`,
322
+ `CURL_CA_BUNDLE=${path.join(root, 'custom', 'curl.pem')}`,
323
+ `PIP_CERT=${path.join(root, 'custom', 'pip.pem')}`,
324
+ `HF_HOME=${path.join(root, 'custom', 'hf')}`,
325
+ ]
326
+ await fs.writeFile(path.join(app, 'ENVIRONMENT'), lines.join('\n') + '\n')
327
+
328
+ await Environment.init_folders(app, createKernel(root))
329
+
330
+ for (const name of ['ssl.pem', 'requests.pem', 'curl.pem', 'pip.pem']) {
331
+ assert.equal(await fs.stat(path.join(root, 'custom', name)).catch(() => null), null, `${name} must not be created as a folder`)
332
+ }
333
+ assert.ok((await fs.stat(path.join(root, 'custom', 'hf'))).isDirectory(), 'ordinary path values are still created as folders')
334
+ })
335
+
336
+ test('init_folders handles Windows certificate key casing without changing POSIX key semantics', async t => {
337
+ const Environment = require('../kernel/environment')
338
+ for (const platform of ['win32', 'linux']) {
339
+ for (const casing of ['upper', 'lower', 'mixed']) {
340
+ await t.test(`${platform} ${casing}`, async t => {
341
+ const root = await tempRoot(t, 'cert-env-case-')
342
+ const app = path.join(root, 'api', 'demo.git')
343
+ await fs.mkdir(app, { recursive: true })
344
+ const keys = ['SSL_CERT_FILE', 'REQUESTS_CA_BUNDLE', 'CURL_CA_BUNDLE', 'PIP_CERT']
345
+ const lines = keys.map((key, index) => {
346
+ if (casing === 'lower') key = key.toLowerCase()
347
+ if (casing === 'mixed') key = key[0] + key.slice(1).toLowerCase()
348
+ return `${key}=${path.join(root, 'custom', `${index}.pem`)}`
349
+ })
350
+ lines.push(`HF_HOME=${path.join(root, 'custom', 'hf')}`)
351
+ await fs.writeFile(path.join(app, 'ENVIRONMENT'), lines.join('\n') + '\n')
352
+ const kernel = createKernel(root)
353
+ kernel.platform = platform
354
+ await Environment.init_folders(app, kernel)
355
+ for (let index = 0; index < keys.length; index++) {
356
+ const stat = await fs.stat(path.join(root, 'custom', `${index}.pem`)).catch(() => null)
357
+ if (platform === 'win32' || casing === 'upper') {
358
+ assert.equal(stat, null, 'recognized certificate files must not become folders')
359
+ } else {
360
+ assert.ok(stat.isDirectory(), 'nonstandard POSIX keys retain ordinary folder handling')
361
+ }
362
+ }
363
+ assert.ok((await fs.stat(path.join(root, 'custom', 'hf'))).isDirectory())
364
+ })
365
+ }
366
+ }
367
+ })
368
+
313
369
  function escapeForHtml(value) {
314
370
  return require('escape-html')(value)
315
371
  }
@@ -0,0 +1,97 @@
1
+ const assert = require('node:assert/strict')
2
+ const fs = require('node:fs/promises')
3
+ const os = require('node:os')
4
+ const path = require('node:path')
5
+ const { createHash } = require('node:crypto')
6
+ const test = require('node:test')
7
+ const { repairPipVendoredBundle } = require('../kernel/cert_env')
8
+
9
+ const sha256 = data => createHash('sha256').update(data).digest('hex')
10
+ const MANAGED = 'new certificate bundle\n'
11
+ const STALE = 'old certificate bundle\n' // Equal sizes must still produce different hashes.
12
+
13
+ async function fixture(t, platform = 'linux') {
14
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-cert-log-'))
15
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
16
+ const prefix = path.join(root, 'env')
17
+ const site = platform === 'win32' ? 'Lib/site-packages' : 'lib/python3.10/site-packages'
18
+ const target = path.join(prefix, site, 'pip/_vendor/certifi/cacert.pem')
19
+ const managed = path.join(root, 'managed.pem')
20
+ await fs.mkdir(path.dirname(target), { recursive: true })
21
+ await fs.writeFile(target, STALE)
22
+ await fs.writeFile(managed, MANAGED)
23
+ const messages = []
24
+ t.mock.method(console, 'log', (...args) => messages.push(args))
25
+ return { root, prefix, target, managed, messages }
26
+ }
27
+
28
+ test('console logs contain independently verifiable evidence even for equal-size replacements', async t => {
29
+ for (const platform of ['linux', 'win32']) {
30
+ await t.test(platform, async t => {
31
+ const f = await fixture(t, platform)
32
+ assert.deepEqual(await repairPipVendoredBundle(f.prefix, f.managed, platform), [f.target])
33
+ assert.equal(f.messages.length, 1)
34
+ assert.ok(f.messages[0][0].includes(f.target))
35
+ const record = JSON.parse(f.messages[0][1])
36
+ assert.equal(record.before_size_bytes, Buffer.byteLength(STALE))
37
+ assert.equal(record.after_size_bytes, record.before_size_bytes)
38
+ assert.equal(record.before_sha256, sha256(STALE))
39
+ assert.equal(record.after_sha256, sha256(await fs.readFile(f.target)))
40
+ assert.notEqual(record.before_sha256, record.after_sha256)
41
+ assert.equal(new Date(record.timestamp).toISOString(), record.timestamp)
42
+ const mtime = (await fs.stat(f.target)).mtimeMs
43
+ assert.deepEqual(await repairPipVendoredBundle(f.prefix, f.managed, platform), [])
44
+ assert.equal((await fs.stat(f.target)).mtimeMs, mtime)
45
+ assert.ok(f.messages[1][0].includes('already current'))
46
+ assert.equal(JSON.parse(f.messages[1][1]).sha256, record.after_sha256)
47
+ })
48
+ }
49
+ })
50
+
51
+ test('a created PEM logs a missing baseline and the written size/hash', async t => {
52
+ const f = await fixture(t)
53
+ await fs.rm(f.target)
54
+ await repairPipVendoredBundle(f.prefix, f.managed, 'linux')
55
+ const record = JSON.parse(f.messages[0][1])
56
+ assert.equal(record.before_size_bytes, null)
57
+ assert.equal(record.before_sha256, null)
58
+ assert.equal(record.after_size_bytes, Buffer.byteLength(MANAGED))
59
+ assert.equal(record.after_sha256, sha256(MANAGED))
60
+ })
61
+
62
+ test('a failed rename logs failure and leaves the stale file and no temp file', async t => {
63
+ const f = await fixture(t)
64
+ t.mock.method(fs, 'rename', async () => { throw Object.assign(new Error('file locked'), { code: 'EPERM' }) })
65
+ await assert.rejects(repairPipVendoredBundle(f.prefix, f.managed, 'linux'), /file locked/)
66
+ assert.equal(f.messages.length, 1)
67
+ assert.equal(JSON.parse(f.messages[0][1]).error, 'file locked')
68
+ assert.equal(await fs.readFile(f.target, 'utf8'), STALE)
69
+ assert.deepEqual(await fs.readdir(path.dirname(f.target)), ['cacert.pem'])
70
+ })
71
+
72
+ test('logging a successful replacement adds no verification read or launch failure', async t => {
73
+ const f = await fixture(t)
74
+ const read = fs.readFile.bind(fs)
75
+ const rename = fs.rename.bind(fs)
76
+ let renamed = false
77
+ let reread = false
78
+ t.mock.method(fs, 'rename', async (...args) => {
79
+ await rename(...args)
80
+ renamed = true
81
+ })
82
+ t.mock.method(fs, 'readFile', async (file, ...args) => {
83
+ if (file === f.target && renamed) {
84
+ reread = true
85
+ throw Object.assign(new Error('reread denied'), { code: 'EACCES' })
86
+ }
87
+ return read(file, ...args)
88
+ })
89
+ assert.deepEqual(await repairPipVendoredBundle(f.prefix, f.managed, 'linux'), [f.target])
90
+ const record = JSON.parse(f.messages[0][1])
91
+ assert.equal(reread, false)
92
+ assert.equal(record.after_size_bytes, Buffer.byteLength(MANAGED))
93
+ assert.equal(record.after_sha256, sha256(MANAGED))
94
+ assert.equal(Object.hasOwn(record, 'verified'), false)
95
+ assert.equal(await read(f.target, 'utf8'), MANAGED)
96
+ assert.deepEqual(await fs.readdir(path.dirname(f.target)), ['cacert.pem'])
97
+ })