@innertia-solutions/innertia-nuxt 0.1.65 → 0.1.66
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.
- package/build/__tests__/version.test.ts +45 -0
- package/build/version.ts +75 -0
- package/components/App/Versions.vue +115 -0
- package/nuxt.config.ts +26 -0
- package/package.json +1 -1
- package/server/routes/_version/details.get.ts +39 -0
- package/server/routes/_version/index.get.ts +8 -0
- package/server/utils/__tests__/version.test.ts +31 -0
- package/server/utils/version.ts +41 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { collectBuildVersion, readPackageVersions } from '../version'
|
|
6
|
+
|
|
7
|
+
let root: string
|
|
8
|
+
|
|
9
|
+
function pkg(dir: string, json: Record<string, unknown>) {
|
|
10
|
+
mkdirSync(dir, { recursive: true })
|
|
11
|
+
writeFileSync(join(dir, 'package.json'), JSON.stringify(json))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
root = mkdtempSync(join(tmpdir(), 'innertia-version-'))
|
|
16
|
+
pkg(root, { name: 'acme-front', version: '1.2.3' })
|
|
17
|
+
pkg(join(root, 'node_modules', 'nuxt'), { name: 'nuxt', version: '4.4.6' })
|
|
18
|
+
pkg(join(root, 'node_modules', 'vue'), { name: 'vue', version: '3.5.34' })
|
|
19
|
+
pkg(join(root, 'node_modules', '@innertia-solutions', 'innertia-nuxt'), { name: '@innertia-solutions/innertia-nuxt', version: '0.1.66' })
|
|
20
|
+
})
|
|
21
|
+
afterEach(() => rmSync(root, { recursive: true, force: true }))
|
|
22
|
+
|
|
23
|
+
describe('readPackageVersions', () => {
|
|
24
|
+
it('lee versiones instaladas y omite las ausentes', () => {
|
|
25
|
+
expect(readPackageVersions(root, ['nuxt', 'vue', '@innertia-solutions/innertia-nuxt', 'no-existe']))
|
|
26
|
+
.toEqual({ nuxt: '4.4.6', vue: '3.5.34', '@innertia-solutions/innertia-nuxt': '0.1.66' })
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('collectBuildVersion', () => {
|
|
31
|
+
it('usa APP_COMMIT/APP_BUILD_DATE/APP_VERSION del entorno de build', () => {
|
|
32
|
+
const v = collectBuildVersion(root, {
|
|
33
|
+
APP_COMMIT: 'abcdef1234567890', APP_BUILD_DATE: '2026-11-04T10:00:00Z', APP_VERSION: '9.9.9',
|
|
34
|
+
}, { git: false })
|
|
35
|
+
expect(v).toMatchObject({ app: 'acme-front', version: '9.9.9', commit: 'abcdef1234567890', buildDate: '2026-11-04T10:00:00Z' })
|
|
36
|
+
expect(v.packages.nuxt).toBe('4.4.6')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('cae al package.json del producto y a la fecha actual', () => {
|
|
40
|
+
const v = collectBuildVersion(root, {}, { git: false })
|
|
41
|
+
expect(v.version).toBe('1.2.3')
|
|
42
|
+
expect(v.commit).toBe('')
|
|
43
|
+
expect(Number.isNaN(Date.parse(v.buildDate))).toBe(false)
|
|
44
|
+
})
|
|
45
|
+
})
|
package/build/version.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Build-time (Node): recolecta la info de versión del PRODUCTO al correr `nuxi build`.
|
|
2
|
+
// La usa el módulo inline del nuxt.config de la capa para llenar
|
|
3
|
+
// runtimeConfig.innertiaVersion (PRIVADO: nunca llega al payload del browser).
|
|
4
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_FRONT_PACKAGES = [
|
|
9
|
+
'nuxt',
|
|
10
|
+
'vue',
|
|
11
|
+
'@innertia-solutions/innertia-nuxt',
|
|
12
|
+
'pinia',
|
|
13
|
+
'@pinia/nuxt',
|
|
14
|
+
'@tanstack/vue-query',
|
|
15
|
+
'tailwindcss',
|
|
16
|
+
'pusher-js',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export interface BuildVersion {
|
|
20
|
+
app: string
|
|
21
|
+
version: string
|
|
22
|
+
commit: string
|
|
23
|
+
buildDate: string
|
|
24
|
+
packages: Record<string, string>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJson(path: string): Record<string, any> | null {
|
|
28
|
+
try {
|
|
29
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null
|
|
30
|
+
} catch {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Versiones instaladas (node_modules del producto). Las ausentes se omiten. */
|
|
36
|
+
export function readPackageVersions(rootDir: string, names: string[]): Record<string, string> {
|
|
37
|
+
const out: Record<string, string> = {}
|
|
38
|
+
for (const name of names) {
|
|
39
|
+
const v = readJson(join(rootDir, 'node_modules', name, 'package.json'))?.version
|
|
40
|
+
if (typeof v === 'string' && v) out[name] = v
|
|
41
|
+
}
|
|
42
|
+
return out
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function gitCommit(rootDir: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: rootDir, stdio: ['ignore', 'pipe', 'ignore'], timeout: 2000 })
|
|
48
|
+
.toString().trim()
|
|
49
|
+
} catch {
|
|
50
|
+
return ''
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Prioridad por campo: env de build (APP_COMMIT / APP_BUILD_DATE / APP_VERSION)
|
|
56
|
+
* → build-info.json en rootDir (lo escribe el Dockerfile) → fallback
|
|
57
|
+
* (git rev-parse en dev, package.json del producto, fecha actual).
|
|
58
|
+
*/
|
|
59
|
+
export function collectBuildVersion(
|
|
60
|
+
rootDir: string,
|
|
61
|
+
env: Record<string, string | undefined> = process.env,
|
|
62
|
+
opts: { git?: boolean, packages?: string[] } = {},
|
|
63
|
+
): BuildVersion {
|
|
64
|
+
const product = readJson(join(rootDir, 'package.json')) ?? {}
|
|
65
|
+
const info = readJson(join(rootDir, 'build-info.json')) ?? {}
|
|
66
|
+
const str = (v: unknown) => (typeof v === 'string' ? v.trim() : '')
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
app: str(product.name),
|
|
70
|
+
version: str(env.APP_VERSION) || str(info.version) || str(product.version),
|
|
71
|
+
commit: str(env.APP_COMMIT) || str(info.commit) || (opts.git === false ? '' : gitCommit(rootDir)),
|
|
72
|
+
buildDate: str(env.APP_BUILD_DATE) || str(info.build_date) || new Date().toISOString(),
|
|
73
|
+
packages: readPackageVersions(rootDir, opts.packages ?? DEFAULT_FRONT_PACKAGES),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
<script setup>
|
|
2
|
+
/**
|
|
3
|
+
* <AppVersions /> — tarjeta "Versiones" para la consola /platform.
|
|
4
|
+
* Consume /_version/details (ruta Nitro de la capa), que autoriza contra el backend
|
|
5
|
+
* (GET platform/version → solo platform admin) y adjunta el detalle del front.
|
|
6
|
+
*
|
|
7
|
+
* ```vue
|
|
8
|
+
* <AppVersions />
|
|
9
|
+
* <AppVersions endpoint="/_version/details" title="Versiones" />
|
|
10
|
+
* ```
|
|
11
|
+
*/
|
|
12
|
+
import { IconVersions, IconRefresh } from '@tabler/icons-vue'
|
|
13
|
+
|
|
14
|
+
const props = defineProps({
|
|
15
|
+
endpoint: { type: String, default: '/_version/details' },
|
|
16
|
+
title: { type: String, default: 'Versiones' },
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const { data, error, pending, refresh } = useFetch(props.endpoint, { server: false, key: `innertia-versions:${props.endpoint}` })
|
|
20
|
+
|
|
21
|
+
function fmtDate(iso) {
|
|
22
|
+
if (!iso) return '—'
|
|
23
|
+
const d = new Date(iso)
|
|
24
|
+
return Number.isNaN(d.getTime()) ? iso : d.toLocaleString()
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const sides = computed(() => {
|
|
28
|
+
if (!data.value) return []
|
|
29
|
+
const { frontend: f, backend: b } = data.value
|
|
30
|
+
return [
|
|
31
|
+
{
|
|
32
|
+
key: 'frontend',
|
|
33
|
+
label: 'Frontend',
|
|
34
|
+
info: f,
|
|
35
|
+
runtime: [['Node', f?.runtime?.node]],
|
|
36
|
+
packages: Object.entries(f?.packages ?? {}).map(([name, version]) => ({ name, version })),
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
key: 'backend',
|
|
40
|
+
label: 'Backend',
|
|
41
|
+
info: b,
|
|
42
|
+
runtime: [
|
|
43
|
+
['PHP', b?.runtime?.php],
|
|
44
|
+
['Laravel', b?.runtime?.laravel],
|
|
45
|
+
[b?.runtime?.database?.driver ? `BD (${b.runtime.database.driver})` : 'BD', b?.runtime?.database?.version],
|
|
46
|
+
],
|
|
47
|
+
packages: Object.entries(b?.packages ?? {}).map(([name, p]) => ({ name, version: p?.version, reference: p?.reference })),
|
|
48
|
+
},
|
|
49
|
+
]
|
|
50
|
+
})
|
|
51
|
+
</script>
|
|
52
|
+
|
|
53
|
+
<template>
|
|
54
|
+
<section class="rounded-card border border-card-line bg-card">
|
|
55
|
+
<header class="flex items-center justify-between gap-3 border-b border-card-line px-4 py-3">
|
|
56
|
+
<div class="flex items-center gap-2">
|
|
57
|
+
<IconVersions class="size-5 text-muted-foreground" />
|
|
58
|
+
<h2 class="text-sm font-semibold text-foreground">{{ title }}</h2>
|
|
59
|
+
</div>
|
|
60
|
+
<button
|
|
61
|
+
type="button"
|
|
62
|
+
class="inline-flex items-center gap-1 rounded-control px-2 py-1 text-xs text-muted-foreground hover:bg-muted hover:text-foreground disabled:opacity-50"
|
|
63
|
+
:disabled="pending"
|
|
64
|
+
@click="refresh()"
|
|
65
|
+
>
|
|
66
|
+
<IconRefresh class="size-3.5" :class="{ 'animate-spin': pending }" /> Actualizar
|
|
67
|
+
</button>
|
|
68
|
+
</header>
|
|
69
|
+
|
|
70
|
+
<div v-if="pending && !data" class="p-4 text-sm text-muted-foreground">Cargando…</div>
|
|
71
|
+
<div v-else-if="error" class="p-4 text-sm text-red-600 dark:text-red-400">
|
|
72
|
+
No se pudieron cargar las versiones ({{ error.statusCode ?? 'error' }}).
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div v-else class="grid divide-y divide-card-line md:grid-cols-2 md:divide-x md:divide-y-0">
|
|
76
|
+
<div v-for="side in sides" :key="side.key" class="space-y-4 p-4">
|
|
77
|
+
<div>
|
|
78
|
+
<p class="text-xs font-medium uppercase tracking-wide text-muted-foreground">{{ side.label }}</p>
|
|
79
|
+
<p class="mt-1 text-lg font-semibold text-foreground">
|
|
80
|
+
{{ side.info?.app || '—' }}
|
|
81
|
+
<span class="text-sm font-normal text-muted-foreground">{{ side.info?.version || '' }}</span>
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<dl class="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1.5 text-sm">
|
|
86
|
+
<dt class="text-muted-foreground">Commit</dt>
|
|
87
|
+
<dd class="flex items-center gap-1 font-mono text-foreground">
|
|
88
|
+
{{ side.info?.commit || '—' }}
|
|
89
|
+
<CopyButton v-if="side.info?.commit" :value="side.info.commit" size="xs" />
|
|
90
|
+
</dd>
|
|
91
|
+
<dt class="text-muted-foreground">Build</dt>
|
|
92
|
+
<dd class="text-foreground">{{ fmtDate(side.info?.build_date) }}</dd>
|
|
93
|
+
<dt class="text-muted-foreground">Entorno</dt>
|
|
94
|
+
<dd class="text-foreground">{{ side.info?.environment || '—' }}</dd>
|
|
95
|
+
<template v-for="[label, value] in side.runtime" :key="label">
|
|
96
|
+
<dt class="text-muted-foreground">{{ label }}</dt>
|
|
97
|
+
<dd class="font-mono text-foreground">{{ value || '—' }}</dd>
|
|
98
|
+
</template>
|
|
99
|
+
</dl>
|
|
100
|
+
|
|
101
|
+
<div v-if="side.packages.length">
|
|
102
|
+
<p class="mb-1.5 text-xs font-medium text-muted-foreground">Paquetes</p>
|
|
103
|
+
<ul class="divide-y divide-card-line rounded-control border border-card-line text-sm">
|
|
104
|
+
<li v-for="p in side.packages" :key="p.name" class="flex items-center justify-between gap-3 px-3 py-1.5">
|
|
105
|
+
<span class="truncate text-foreground">{{ p.name }}</span>
|
|
106
|
+
<span class="shrink-0 font-mono text-xs text-muted-foreground">
|
|
107
|
+
{{ p.version || '—' }}<template v-if="p.reference && String(p.version).startsWith('dev-')"> · {{ p.reference }}</template>
|
|
108
|
+
</span>
|
|
109
|
+
</li>
|
|
110
|
+
</ul>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
</template>
|
package/nuxt.config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import tailwindcss from '@tailwindcss/vite'
|
|
2
|
+
import { collectBuildVersion } from './build/version'
|
|
2
3
|
|
|
3
4
|
// @innertia-solutions/innertia-nuxt — capa Nuxt unificada de Innertia Solutions.
|
|
4
5
|
// Provee: utilidades base + auth + multitenancy + design system + app contexts.
|
|
@@ -7,6 +8,21 @@ export default defineNuxtConfig({
|
|
|
7
8
|
'@nuxtjs/seo',
|
|
8
9
|
'@pinia/nuxt',
|
|
9
10
|
'pinia-plugin-persistedstate/nuxt', // requerido para persistir auth, dockedPreviews, etc.
|
|
11
|
+
// Versiones: en build llena runtimeConfig.innertiaVersion (PRIVADO) con app/version/
|
|
12
|
+
// commit/fecha del producto + versiones de nuxt/vue/innertia-nuxt de su node_modules.
|
|
13
|
+
// Lo sirven /_version (público, sin deps) y /_version/details (vía backend platform).
|
|
14
|
+
(_opts: unknown, nuxt: any) => {
|
|
15
|
+
const current = nuxt.options.runtimeConfig.innertiaVersion ?? {}
|
|
16
|
+
const built = collectBuildVersion(nuxt.options.rootDir)
|
|
17
|
+
nuxt.options.runtimeConfig.innertiaVersion = {
|
|
18
|
+
...current,
|
|
19
|
+
app: current.app || nuxt.options.runtimeConfig.public?.appName || built.app,
|
|
20
|
+
version: current.version || built.version,
|
|
21
|
+
commit: current.commit || built.commit,
|
|
22
|
+
buildDate: current.buildDate || built.buildDate,
|
|
23
|
+
packages: { ...built.packages, ...(current.packages ?? {}) },
|
|
24
|
+
}
|
|
25
|
+
},
|
|
10
26
|
],
|
|
11
27
|
css: ['@innertia-solutions/innertia-nuxt/theme.css'],
|
|
12
28
|
components: [
|
|
@@ -26,6 +42,16 @@ export default defineNuxtConfig({
|
|
|
26
42
|
session: {
|
|
27
43
|
password: '',
|
|
28
44
|
},
|
|
45
|
+
// Privado. Lo llena el módulo inline de arriba en build; override runtime con
|
|
46
|
+
// NUXT_INNERTIA_VERSION_COMMIT / _BUILD_DATE / _VERSION / _DETAILS_PATH.
|
|
47
|
+
innertiaVersion: {
|
|
48
|
+
app: '',
|
|
49
|
+
version: '',
|
|
50
|
+
commit: '',
|
|
51
|
+
buildDate: '',
|
|
52
|
+
packages: {},
|
|
53
|
+
detailsPath: 'platform/version',
|
|
54
|
+
},
|
|
29
55
|
public: {
|
|
30
56
|
loginPath: '/backoffice/login',
|
|
31
57
|
homePath: '/backoffice',
|
package/package.json
CHANGED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseHeader, setResponseStatus } from 'h3'
|
|
2
|
+
import { readSession } from '../../utils/session'
|
|
3
|
+
import { resolveApiTarget, buildAuthHeaders } from '../../utils/bff'
|
|
4
|
+
import { frontendDetails } from '../../utils/version'
|
|
5
|
+
|
|
6
|
+
// Detalle (front + back). Nitro no sabe quién es platform admin: delega la
|
|
7
|
+
// autorización al backend llamando su endpoint protegido con el Bearer de la sesión.
|
|
8
|
+
// ≠2xx → mismo status, sin datos del front. 2xx → { frontend, backend }.
|
|
9
|
+
export default defineEventHandler(async (event) => {
|
|
10
|
+
setResponseHeader(event, 'cache-control', 'no-store')
|
|
11
|
+
const config = useRuntimeConfig(event)
|
|
12
|
+
const cfg = config.innertiaVersion ?? {}
|
|
13
|
+
const { token } = await readSession(event)
|
|
14
|
+
|
|
15
|
+
if (!token) {
|
|
16
|
+
setResponseStatus(event, 401)
|
|
17
|
+
return { message: 'No autenticado.' }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const target = resolveApiTarget(config.apiInternalUrl, cfg.detailsPath || 'platform/version', '')
|
|
21
|
+
const res = await $fetch.raw(target, {
|
|
22
|
+
headers: { ...buildAuthHeaders(token), accept: 'application/json' },
|
|
23
|
+
ignoreResponseError: true,
|
|
24
|
+
}).catch(() => null)
|
|
25
|
+
|
|
26
|
+
if (!res) {
|
|
27
|
+
setResponseStatus(event, 502)
|
|
28
|
+
return { message: 'Backend no disponible.' }
|
|
29
|
+
}
|
|
30
|
+
if (res.status < 200 || res.status >= 300) {
|
|
31
|
+
setResponseStatus(event, res.status)
|
|
32
|
+
return { message: (res._data as any)?.message ?? 'No autorizado.' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
frontend: frontendDetails(cfg, process.env, process.version),
|
|
37
|
+
backend: res._data,
|
|
38
|
+
}
|
|
39
|
+
})
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { defineEventHandler, setResponseHeader } from 'h3'
|
|
2
|
+
import { publicVersion } from '../../utils/version'
|
|
3
|
+
|
|
4
|
+
// Público: qué build del front está desplegado. Sin dependencias.
|
|
5
|
+
export default defineEventHandler((event) => {
|
|
6
|
+
setResponseHeader(event, 'cache-control', 'no-store')
|
|
7
|
+
return publicVersion(useRuntimeConfig(event).innertiaVersion ?? {}, process.env)
|
|
8
|
+
})
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { publicVersion, frontendDetails } from '../version'
|
|
3
|
+
|
|
4
|
+
const cfg = {
|
|
5
|
+
app: 'acme-front', version: '1.2.3', commit: '0123456789abcdef', buildDate: '2026-11-04T10:00:00Z',
|
|
6
|
+
packages: { nuxt: '4.4.6', vue: '3.5.34' }, detailsPath: 'platform/version',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('publicVersion', () => {
|
|
10
|
+
it('expone solo los 5 campos, commit corto, sin dependencias', () => {
|
|
11
|
+
const v = publicVersion(cfg, { APP_ENV: 'staging' })
|
|
12
|
+
expect(Object.keys(v)).toEqual(['app', 'version', 'commit', 'build_date', 'environment'])
|
|
13
|
+
expect(v).toEqual({ app: 'acme-front', version: '1.2.3', commit: '0123456', build_date: '2026-11-04T10:00:00Z', environment: 'staging' })
|
|
14
|
+
expect(JSON.stringify(v)).not.toContain('4.4.6')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('commit vacío → null; environment cae a NODE_ENV', () => {
|
|
18
|
+
const v = publicVersion({ ...cfg, commit: '' }, { NODE_ENV: 'production' })
|
|
19
|
+
expect(v.commit).toBeNull()
|
|
20
|
+
expect(v.environment).toBe('production')
|
|
21
|
+
})
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
describe('frontendDetails', () => {
|
|
25
|
+
it('agrega runtime node y paquetes', () => {
|
|
26
|
+
const d = frontendDetails(cfg, { APP_ENV: 'local' }, 'v22.12.0')
|
|
27
|
+
expect(d.runtime).toEqual({ node: 'v22.12.0' })
|
|
28
|
+
expect(d.packages).toEqual({ nuxt: '4.4.6', vue: '3.5.34' })
|
|
29
|
+
expect(d.commit).toBe('0123456')
|
|
30
|
+
})
|
|
31
|
+
})
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// Lógica pura del endpoint de versiones (sin h3, testeable aislada).
|
|
2
|
+
|
|
3
|
+
export interface VersionConfig {
|
|
4
|
+
app?: string
|
|
5
|
+
version?: string
|
|
6
|
+
commit?: string
|
|
7
|
+
buildDate?: string
|
|
8
|
+
packages?: Record<string, string>
|
|
9
|
+
detailsPath?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PublicVersion {
|
|
13
|
+
app: string | null
|
|
14
|
+
version: string | null
|
|
15
|
+
commit: string | null
|
|
16
|
+
build_date: string | null
|
|
17
|
+
environment: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const orNull = (v?: string) => (v && v.trim() ? v.trim() : null)
|
|
21
|
+
|
|
22
|
+
/** Público: SOLO estos 5 campos. Nunca dependencias ni runtime (fingerprinting/CVE). */
|
|
23
|
+
export function publicVersion(cfg: VersionConfig, env: Record<string, string | undefined>): PublicVersion {
|
|
24
|
+
const commit = orNull(cfg.commit)
|
|
25
|
+
return {
|
|
26
|
+
app: orNull(cfg.app),
|
|
27
|
+
version: orNull(cfg.version),
|
|
28
|
+
commit: commit ? commit.slice(0, 7) : null,
|
|
29
|
+
build_date: orNull(cfg.buildDate),
|
|
30
|
+
environment: env.APP_ENV || env.NODE_ENV || 'production',
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Detalle del front: público + runtime + paquetes. Solo tras autorizar contra el backend. */
|
|
35
|
+
export function frontendDetails(cfg: VersionConfig, env: Record<string, string | undefined>, node: string) {
|
|
36
|
+
return {
|
|
37
|
+
...publicVersion(cfg, env),
|
|
38
|
+
runtime: { node },
|
|
39
|
+
packages: { ...(cfg.packages ?? {}) },
|
|
40
|
+
}
|
|
41
|
+
}
|