@goliapkg/sentori-cli 1.6.0 → 1.8.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.
package/src/workflow.ts CHANGED
@@ -3,6 +3,7 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'n
3
3
  import { basename, dirname, resolve } from 'node:path'
4
4
  import { gunzipSync } from 'node:zlib'
5
5
  import { parseArgs } from 'node:util'
6
+ import { showProject } from './apps.js'
6
7
  import { fetchArtifacts } from './artifacts.js'
7
8
  import { checkCompatibility } from './compatibility.js'
8
9
  import { dwarfBinariesIn } from './native-artifacts.js'
@@ -14,6 +15,7 @@ export type UploadConfig = {
14
15
  version: 1
15
16
  apiUrl: string
16
17
  tokenEnv: string
18
+ projectId?: string
17
19
  release?: string
18
20
  artifacts: { sourcemap?: string[]; dsym?: string[]; mapping?: string[] }
19
21
  sources?: { root?: string; include: string[]; exclude?: string[]; gitTag?: string }
@@ -23,6 +25,7 @@ export type UploadPlan = {
23
25
  release: string
24
26
  apiUrl: string
25
27
  tokenEnv: string
28
+ projectId?: string
26
29
  files: { kind: UploadOpts['kind']; name: string; path: string; hash: string; bytes: number }[]
27
30
  source?: { files: string[]; tag?: string; commit?: string }
28
31
  }
@@ -58,12 +61,13 @@ export function loadUploadConfig(path: string): UploadConfig {
58
61
  const value: unknown = JSON.parse(readFileSync(path, 'utf8'))
59
62
  if (!object(value) || value.version !== 1 || typeof value.apiUrl !== 'string' || !object(value.artifacts)) throw new Error('config requires version: 1, apiUrl, and artifacts; run sentori init')
60
63
  if (Object.hasOwn(value, 'token')) throw new Error('store the API token in the environment and set tokenEnv, not token in the config')
61
- const allowed = new Set(['version', 'apiUrl', 'tokenEnv', 'release', 'artifacts', 'sources', 'sourceArchive'])
64
+ const allowed = new Set(['version', 'apiUrl', 'tokenEnv', 'projectId', 'release', 'artifacts', 'sources', 'sourceArchive'])
62
65
  for (const key of Object.keys(value)) if (!allowed.has(key)) throw new Error(`unknown config field: ${key}`)
63
66
  for (const [kind, paths] of Object.entries(value.artifacts)) {
64
67
  if (!['sourcemap', 'dsym', 'mapping'].includes(kind)) throw new Error(`unknown artifact kind ${kind}; use sourcemap, dsym, or mapping`)
65
68
  strings(paths, `artifacts.${kind}`)
66
69
  }
70
+ if (value.projectId !== undefined && (typeof value.projectId !== 'string' || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value.projectId))) throw new Error('projectId must be the expected Project UUID')
67
71
  if (value.release !== undefined && (typeof value.release !== 'string' || !value.release.trim())) throw new Error('release must be a non-empty string')
68
72
  if (value.sourceArchive !== undefined && typeof value.sourceArchive !== 'string') throw new Error('sourceArchive must be a ZIP path')
69
73
  if (value.sources !== undefined) {
@@ -97,7 +101,7 @@ export function prepareUploadPlan(configPath: string, overrides: { release?: str
97
101
  const base = dirname(resolve(configPath))
98
102
  const release = overrides.release ?? process.env.SENTORI_RELEASE ?? config.release
99
103
  if (!release?.trim()) throw new Error('set --release, SENTORI_RELEASE, or config.release to the exact SDK release')
100
- const plan: UploadPlan = { release, apiUrl: apiUrl(overrides.apiUrl ?? process.env.SENTORI_API_URL ?? config.apiUrl), tokenEnv: config.tokenEnv, files: [] }
104
+ const plan: UploadPlan = { release, apiUrl: apiUrl(overrides.apiUrl ?? process.env.SENTORI_API_URL ?? config.apiUrl), tokenEnv: config.tokenEnv, ...(config.projectId ? { projectId: config.projectId } : {}), files: [] }
101
105
  const add = (kind: UploadOpts['kind'], path: string, name = basename(path)): void => {
102
106
  let bytes = readFileSync(path)
103
107
  if (bytes[0] === 0x1f && bytes[1] === 0x8b) { bytes = gunzipSync(bytes, { maxOutputLength: 512 * 1024 * 1024 }); name = name.replace(/\.gz$/, '') }
@@ -183,6 +187,7 @@ export async function runWorkflow(command: 'init' | 'validate' | 'upload' | 'ver
183
187
  const config = loadUploadConfig(configPath)
184
188
  const token = process.env[config.tokenEnv]
185
189
  if (!token) throw new Error(`set ${config.tokenEnv} to an API-scope token`)
190
+ await verifyProject({ apiUrl: apiUrl(parsed.values['api-url'] ?? process.env.SENTORI_API_URL ?? config.apiUrl), projectId: config.projectId }, token)
186
191
  const result = await fetchArtifacts({
187
192
  apiUrl: apiUrl(parsed.values['api-url'] ?? process.env.SENTORI_API_URL ?? config.apiUrl), token,
188
193
  release: parsed.values.release ?? process.env.SENTORI_RELEASE ?? config.release ?? '__sentori_compatibility_check__',
@@ -196,6 +201,7 @@ export async function runWorkflow(command: 'init' | 'validate' | 'upload' | 'ver
196
201
  if (command === 'validate' || parsed.values['dry-run']) { console.log('Local files checked; no upload or server verification performed.'); return 0 }
197
202
  const token = process.env[plan.tokenEnv]
198
203
  if (!token) throw new Error(`set ${plan.tokenEnv} to an API-scope token`)
204
+ await verifyProject(plan, token)
199
205
  // Check credentials before the first write. An unknown release is allowed.
200
206
  console.log(checkCompatibility(await fetchArtifacts({ ...plan, token }), plan.files.some(file => file.kind === 'srcbundle')))
201
207
  const failures: string[] = []
@@ -238,3 +244,12 @@ export async function runSourcesPack(argv: string[]): Promise<number> {
238
244
  return 0
239
245
  } catch (error) { console.error(`[sentori] ${error instanceof Error ? error.message : String(error)}`); return 1 }
240
246
  }
247
+
248
+ async function verifyProject(config: { apiUrl: string; projectId?: string }, token: string): Promise<void> {
249
+ if (!config.projectId) return
250
+ const { project } = await showProject({ apiUrl: config.apiUrl, token })
251
+ if (project.id.toLowerCase() !== config.projectId.toLowerCase()) {
252
+ throw new Error(`Project mismatch: expected ${config.projectId}, token belongs to ${project.id}. No files uploaded.`)
253
+ }
254
+ console.log(`Project verified: ${project.name} (${project.id})`)
255
+ }