@ducking-mind/proxy-app-cli 0.1.2 → 0.2.0

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.
@@ -0,0 +1,83 @@
1
+ import { lstat, readdir, readFile } from 'node:fs/promises'
2
+ import { extname, join, resolve } from 'node:path'
3
+ import { assertSafeArtifactFile } from './artifact-security.mjs'
4
+
5
+ const MAX_FUNCTION_SOURCE_BYTES = 4 * 1024 * 1024
6
+ const MAX_FUNCTION_SOURCE_FILES = 2000
7
+
8
+ function functionEntries(manifest) {
9
+ const functions = manifest?.functions
10
+ if (!functions || typeof functions !== 'object' || Array.isArray(functions)) return []
11
+ return ['request', 'task']
12
+ .map(kind => functions[kind]?.entry)
13
+ .filter(entry => typeof entry === 'string' && entry.length > 0)
14
+ }
15
+
16
+ // Read the same runtime module files that proxy-app pack uploads. Development
17
+ // sends these sources to Admin so Gateway can execute the versioned draft; the
18
+ // browser continues to call Gateway and never receives function source.
19
+ export async function readLocalFunctionSources(appDir, manifest) {
20
+ const requiredEntries = functionEntries(manifest)
21
+ if (requiredEntries.length === 0) return []
22
+
23
+ const root = resolve(appDir)
24
+ const runtimeRoot = join(root, 'runtime')
25
+ const files = []
26
+ let totalBytes = 0
27
+
28
+ try {
29
+ const runtimeMetadata = await lstat(runtimeRoot)
30
+ if (runtimeMetadata.isSymbolicLink() || !runtimeMetadata.isDirectory()) {
31
+ throw new Error('runtime function sources must be inside a real runtime/ directory')
32
+ }
33
+ } catch (error) {
34
+ if (error?.code !== 'ENOENT') throw error
35
+ }
36
+
37
+ const visit = async (directory, prefix) => {
38
+ let entries
39
+ try {
40
+ entries = await readdir(directory, { withFileTypes: true })
41
+ } catch (error) {
42
+ if (error?.code === 'ENOENT' && directory === runtimeRoot) return
43
+ throw error
44
+ }
45
+ entries.sort((left, right) => left.name.localeCompare(right.name))
46
+ for (const entry of entries) {
47
+ if (entry.name.includes('\\') || entry.name === '.' || entry.name === '..') {
48
+ throw new Error(`invalid runtime source path: ${entry.name}`)
49
+ }
50
+ const relativePath = `${prefix}/${entry.name}`
51
+ const absolutePath = join(directory, entry.name)
52
+ if (entry.isSymbolicLink()) throw new Error(`runtime function sources cannot be symbolic links: ${relativePath}`)
53
+ if (entry.isDirectory()) {
54
+ await visit(absolutePath, relativePath)
55
+ continue
56
+ }
57
+ if (!entry.isFile() || !['.js', '.mjs'].includes(extname(entry.name).toLowerCase())) continue
58
+ if (Buffer.byteLength(relativePath, 'utf8') > 255) throw new Error(`runtime function path is too long: ${relativePath}`)
59
+ if (files.length >= MAX_FUNCTION_SOURCE_FILES) throw new Error('runtime function source contains too many files')
60
+ const metadata = await lstat(absolutePath)
61
+ if (!metadata.isFile() || metadata.isSymbolicLink()) throw new Error(`runtime function sources cannot be symbolic links: ${relativePath}`)
62
+ if (metadata.size === 0) throw new Error(`runtime function source is empty: ${relativePath}`)
63
+ totalBytes += metadata.size
64
+ if (totalBytes > MAX_FUNCTION_SOURCE_BYTES) throw new Error('runtime function source exceeds 4 MiB')
65
+ const sourceBytes = await readFile(absolutePath)
66
+ let source
67
+ try {
68
+ source = new TextDecoder('utf-8', { fatal: true }).decode(sourceBytes)
69
+ } catch {
70
+ throw new Error(`runtime function source is not valid UTF-8: ${relativePath}`)
71
+ }
72
+ assertSafeArtifactFile(relativePath, source)
73
+ files.push({ path: relativePath, source })
74
+ }
75
+ }
76
+
77
+ await visit(runtimeRoot, 'runtime')
78
+ const available = new Set(files.map(file => file.path))
79
+ for (const entry of requiredEntries) {
80
+ if (!available.has(entry)) throw new Error(`manifest function entry is missing from runtime/: ${entry}`)
81
+ }
82
+ return files
83
+ }