Skip to content

feat: app publisher identity in the manifest + CLI pipeline (v0.10.0) [BDOK-678] - #24

Merged
DuncanAForbes merged 1 commit into
mainfrom
feat/BDOK-678-publisher-manifest
Jul 16, 2026
Merged

feat: app publisher identity in the manifest + CLI pipeline (v0.10.0) [BDOK-678]#24
DuncanAForbes merged 1 commit into
mainfrom
feat/BDOK-678-publisher-manifest

Conversation

@DuncanAForbes

@DuncanAForbes DuncanAForbes commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

CLI half of BDOK-678 Phase 2 (app identity). Adds optional publisher identity to bagdock.json and wires it through validate / init / deploy / submit. Publisher fields inherit from the owning org's profile by default and override per field, so existing first-party apps need zero manifest edits. Bumps @bagdock/cli 0.9.1 -> 0.10.0.

Changes:

  • config.ts: BagdockJson gains publisher (company/website/supportEmail/docsUrl/privacyPolicy), icon (repo-relative path to a square PNG/SVG), and description. camelCase-manifest convention (BDOK-560).
  • validate: dependency-free icon checks (square, PNG/SVG, >=128px, <=256KB via PNG IHDR / SVG width|height|viewBox) + publisher field validation. Public app with no publisher block WARNs (offline CLI cannot see the org profile), never fails.
  • init: inherit-by-default; interactive TTY-gated overrides; new --yes skips prompts.
  • deploy: ships publisher + description in metadata and the icon bytes as an icon file part; surfaces the server publisher_warning.
  • submit: advisory publisher-completeness pre-check for public apps.

Testing: new tests/validate.test.ts (15 tests); tsc clean; 28/28 pass; validate + init --yes driven end-to-end against fixtures.

Sequencing: requires the server-side deploy handler that consumes metadata.publisher + the icon part (monorepo PR, ships first). Non-breaking either way. Release this after the server reaches prod.

Linear: Refs BDOK-678 (Phase 2; issue stays open for Phases 3-4).

Summary by CodeRabbit

  • New Features
    • Added publisher identity details to app configuration, including company, website, support email, documentation and privacy policy.
    • Added optional app icon and description support.
    • Added interactive publisher setup during bagdock init, with a non-interactive option.
    • Deployments now include publisher details and app icons.
  • Bug Fixes
    • Added validation for icon format, size and dimensions, plus publisher URLs and email addresses.
    • Added advisory warnings for incomplete publisher information before validation, deployment or submission.
  • Release
    • Updated version to 0.10.0.

Phase 2 of app identity (BDOK-678). Adds optional publisher identity to
bagdock.json and wires it through the CLI. Publisher fields inherit from the
owning org's profile by default and override per field, so the existing
first-party apps need no manifest edits.

- config.ts: `BagdockJson` gains `publisher` (company/website/supportEmail/
  docsUrl/privacyPolicy), `icon` (repo-relative path to a square PNG/SVG), and
  `description`. Field names follow the BDOK-560 camelCase-manifest convention.
- validate: dependency-free icon checks (square, PNG/SVG, >=128px, <=256KB via
  PNG IHDR / SVG width|height|viewBox parsing) + publisher field validation.
  A public app with no publisher block WARNs (the offline CLI cannot see the org
  profile that may complete it), never fails.
- init: inherit-by-default (no publisher block scaffolded); interactive,
  TTY-gated overrides. New `--yes` skips the prompts for scripted use.
- deploy: ships `publisher` + `description` in metadata and the icon bytes as an
  `icon` file part; surfaces the server's new `publisher_warning`.
- submit: advisory publisher-completeness pre-check for public apps.

Requires the server-side deploy handler that consumes `metadata.publisher` and
the `icon` part (ships first). Bumps to 0.10.0.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Bagdock 0.10.0 adds optional publisher identity, description, and icon manifest fields. Initialisation can collect publisher overrides, validation checks publisher data and icons, deployment uploads the metadata and icon, and submission reports incomplete publisher identity for public apps.

Changes

Publisher identity and icon workflow

Layer / File(s) Summary
Manifest schema and interactive initialisation
src/config.ts, src/init.ts, bin/bagdock.ts
Adds typed publisher, description, and icon fields and supports interactive publisher overrides, with --yes and non-TTY execution skipping prompts.
Icon and publisher validation
src/validate.ts, tests/validate.test.ts
Validates PNG/SVG icon dimensions and size, checks publisher field names and formats, warns when public apps inherit publisher identity, and adds corresponding tests.
Deployment and submission metadata flow
src/deploy.ts, src/submit.ts, package.json, CHANGELOG.md
Uploads publisher metadata and icon bytes during deployment, displays server warnings, adds an advisory public-app submission check, and records the 0.10.0 release.
Estimated code review effort: 3 (Moderate) ~25 minutes

Possibly related PRs

  • bagdock/bagdock-cli#5: Introduces the local validation implementation extended here with icon and publisher checks.

Poem

A bunny packs an icon bright,
With publisher fields tucked in tight.
The dashboard gets each byte,
Warnings glow when fields aren’t right,
And public apps hop toward review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.41% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarises the main change: publisher identity support across the manifest and CLI pipeline for v0.10.0.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/BDOK-678-publisher-manifest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@DuncanAForbes
DuncanAForbes merged commit d8b1e83 into main Jul 16, 2026
4 of 5 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/deploy.ts`:
- Around line 141-150: The icon upload logic in deploy must prevent arbitrary
file disclosure. Before reading bytes in the config.icon path, resolve the path
and ensure it remains within the repository, then reuse checkIcon’s format and
size validation so only supported icons are uploaded; skip or reject invalid
paths and files without reading them.

In `@src/validate.ts`:
- Around line 191-202: Update the Icon validation flow in the shown path after
resolving abs: catch statSync and readFileSync filesystem errors, return an Icon
failure instead of allowing validation to terminate, and reject non-file paths
by checking stat.isFile(). Preserve the existing missing-file and size-limit
failures for valid regular files.
- Around line 201-218: Update the PNG branch around pngDimensions in
src/validate.ts:201-218 to fully decode or structurally validate PNG chunks
before returning a passing result, rejecting truncated header-only data;
preserve the existing dimension and minimum-size checks. In
tests/validate.test.ts:10-16, replace makePng() with a complete valid PNG
fixture and add coverage asserting truncated PNG files are rejected.
- Around line 253-264: Update the publisher validation guard in the surrounding
validation function to reject null explicitly alongside non-object and array
values, before calling Object.keys or Object.entries. In the publisher field
loop, stop skipping null values; treat null like undefined or any non-string
value and add the existing non-empty-string validation error, while preserving
valid string handling.
- Around line 218-226: Update the SVG validation branch using svgDimensions and
ICON_MIN_PX so parsing must yield positive dimensions, both dimensions meet the
128px minimum, and the aspect ratio remains square. Return an Icon failure for
missing, malformed, undersized, or non-square dimensions; preserve the existing
pass result for valid SVGs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73dab82c-6820-4e2c-a99a-232fcffa2e9f

📥 Commits

Reviewing files that changed from the base of the PR and between bec7e1f and 7831b97.

📒 Files selected for processing (9)
  • CHANGELOG.md
  • bin/bagdock.ts
  • package.json
  • src/config.ts
  • src/deploy.ts
  • src/init.ts
  • src/submit.ts
  • src/validate.ts
  • tests/validate.test.ts

Comment thread src/deploy.ts
Comment on lines +141 to +150
if (config.icon) {
const iconPath = join(cwd, config.icon)
if (existsSync(iconPath)) {
const iconBytes = readFileSync(iconPath)
const ext = config.icon.toLowerCase().slice(config.icon.lastIndexOf('.'))
const iconType = ext === '.svg' ? 'image/svg+xml' : ext === '.png' ? 'image/png' : 'application/octet-stream'
formData.append('icon', new Blob([iconBytes], { type: iconType }), `icon${ext}`)
} else {
console.log(chalk.yellow(` Icon not found at ${config.icon}, skipping upload.`))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Prevent arbitrary local-file disclosure through icon.

deploy does not run checkIcon, confine the resolved path to the repository, or restrict extensions. A malicious bagdock.json can therefore set icon to ../… and upload any readable local file to the deploy endpoint. Resolve and enforce repository containment, then apply the same format and size validation before reading bytes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/deploy.ts` around lines 141 - 150, The icon upload logic in deploy must
prevent arbitrary file disclosure. Before reading bytes in the config.icon path,
resolve the path and ensure it remains within the repository, then reuse
checkIcon’s format and size validation so only supported icons are uploaded;
skip or reject invalid paths and files without reading them.

Comment thread src/validate.ts
Comment on lines +191 to +202
const abs = join(dir, iconPath)
if (!existsSync(abs)) {
return { name: 'Icon', status: 'fail', message: `File not found: ${iconPath}` }
}

const size = statSync(abs).size
if (size > ICON_MAX_BYTES) {
return { name: 'Icon', status: 'fail', message: `${(size / 1024).toFixed(0)} KB exceeds the ${ICON_MAX_BYTES / 1024} KB limit` }
}

const ext = iconPath.toLowerCase().slice(iconPath.lastIndexOf('.'))
const buf = readFileSync(abs)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reject directories and handle filesystem errors as validation failures.

A directory or unreadable path can make readFileSync() throw, terminating bagdock validate instead of returning an Icon failure. Check stat.isFile() and catch statSync/readFileSync errors.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validate.ts` around lines 191 - 202, Update the Icon validation flow in
the shown path after resolving abs: catch statSync and readFileSync filesystem
errors, return an Icon failure instead of allowing validation to terminate, and
reject non-file paths by checking stat.isFile(). Preserve the existing
missing-file and size-limit failures for valid regular files.

Comment thread src/validate.ts
Comment on lines +201 to +218
const ext = iconPath.toLowerCase().slice(iconPath.lastIndexOf('.'))
const buf = readFileSync(abs)

if (ext === '.png' || isPng(buf)) {
const dims = pngDimensions(buf)
if (!dims) {
return { name: 'Icon', status: 'fail', message: `${iconPath} is not a valid PNG` }
}
if (dims.width !== dims.height) {
return { name: 'Icon', status: 'fail', message: `${iconPath} must be square (got ${dims.width}×${dims.height})` }
}
if (dims.width < ICON_MIN_PX) {
return { name: 'Icon', status: 'fail', message: `${iconPath} is ${dims.width}px — must be at least ${ICON_MIN_PX}px` }
}
return { name: 'Icon', status: 'pass', message: `${iconPath} (${dims.width}×${dims.height} PNG, ${(size / 1024).toFixed(0)} KB)` }
}

if (ext === '.svg' || isSvg(buf)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate complete PNG files rather than header-only fixtures.

The implementation and test fixture jointly establish a truncated 24-byte header as a valid PNG.

  • src/validate.ts#L201-L218: fully decode or structurally validate PNG chunks before accepting the icon.
  • tests/validate.test.ts#L10-L16: replace makePng() with a complete PNG and add a truncated-file rejection test.
📍 Affects 2 files
  • src/validate.ts#L201-L218 (this comment)
  • tests/validate.test.ts#L10-L16
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validate.ts` around lines 201 - 218, Update the PNG branch around
pngDimensions in src/validate.ts:201-218 to fully decode or structurally
validate PNG chunks before returning a passing result, rejecting truncated
header-only data; preserve the existing dimension and minimum-size checks. In
tests/validate.test.ts:10-16, replace makePng() with a complete valid PNG
fixture and add coverage asserting truncated PNG files are rejected.

Comment thread src/validate.ts
Comment on lines +218 to +226
if (ext === '.svg' || isSvg(buf)) {
const dims = svgDimensions(buf.toString('utf-8'))
if (dims && dims.width > 0 && dims.height > 0) {
const ratio = dims.width / dims.height
if (ratio < 0.98 || ratio > 1.02) {
return { name: 'Icon', status: 'fail', message: `${iconPath} must be square (viewBox/size is ${dims.width}×${dims.height})` }
}
}
return { name: 'Icon', status: 'pass', message: `${iconPath} (SVG, ${(size / 1024).toFixed(0)} KB)` }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fail SVGs without parseable square dimensions and enforce 128px.

An empty or malformed .svg passes when svgDimensions() returns null, and a 64×64 SVG also passes despite the documented minimum. Require dimensions and apply ICON_MIN_PX consistently.

Proposed validation adjustment
   if (ext === '.svg' || isSvg(buf)) {
     const dims = svgDimensions(buf.toString('utf-8'))
-    if (dims && dims.width > 0 && dims.height > 0) {
-      const ratio = dims.width / dims.height
-      if (ratio < 0.98 || ratio > 1.02) {
-        return { name: 'Icon', status: 'fail', message: `${iconPath} must be square (viewBox/size is ${dims.width}×${dims.height})` }
-      }
+    if (!dims || dims.width <= 0 || dims.height <= 0) {
+      return { name: 'Icon', status: 'fail', message: `${iconPath} must declare valid width/height or viewBox dimensions` }
+    }
+    const ratio = dims.width / dims.height
+    if (ratio < 0.98 || ratio > 1.02) {
+      return { name: 'Icon', status: 'fail', message: `${iconPath} must be square (viewBox/size is ${dims.width}×${dims.height})` }
+    }
+    if (dims.width < ICON_MIN_PX || dims.height < ICON_MIN_PX) {
+      return { name: 'Icon', status: 'fail', message: `${iconPath} must be at least ${ICON_MIN_PX}px` }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (ext === '.svg' || isSvg(buf)) {
const dims = svgDimensions(buf.toString('utf-8'))
if (dims && dims.width > 0 && dims.height > 0) {
const ratio = dims.width / dims.height
if (ratio < 0.98 || ratio > 1.02) {
return { name: 'Icon', status: 'fail', message: `${iconPath} must be square (viewBox/size is ${dims.width}×${dims.height})` }
}
}
return { name: 'Icon', status: 'pass', message: `${iconPath} (SVG, ${(size / 1024).toFixed(0)} KB)` }
if (ext === '.svg' || isSvg(buf)) {
const dims = svgDimensions(buf.toString('utf-8'))
if (!dims || dims.width <= 0 || dims.height <= 0) {
return { name: 'Icon', status: 'fail', message: `${iconPath} must declare valid width/height or viewBox dimensions` }
}
const ratio = dims.width / dims.height
if (ratio < 0.98 || ratio > 1.02) {
return { name: 'Icon', status: 'fail', message: `${iconPath} must be square (viewBox/size is ${dims.width}×${dims.height})` }
}
if (dims.width < ICON_MIN_PX || dims.height < ICON_MIN_PX) {
return { name: 'Icon', status: 'fail', message: `${iconPath} must be at least ${ICON_MIN_PX}px` }
}
return { name: 'Icon', status: 'pass', message: `${iconPath} (SVG, ${(size / 1024).toFixed(0)} KB)` }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validate.ts` around lines 218 - 226, Update the SVG validation branch
using svgDimensions and ICON_MIN_PX so parsing must yield positive dimensions,
both dimensions meet the 128px minimum, and the aspect ratio remains square.
Return an Icon failure for missing, malformed, undersized, or non-square
dimensions; preserve the existing pass result for valid SVGs.

Comment thread src/validate.ts
Comment on lines +253 to +264
if (typeof pub !== 'object' || Array.isArray(pub)) {
checks.push({ name: 'Publisher', status: 'fail', message: '"publisher" must be an object of { company?, website?, supportEmail?, docsUrl?, privacyPolicy? }' })
return checks
}

const problems: string[] = []
const unknown = Object.keys(pub).filter((k) => !PUBLISHER_FIELDS.includes(k as any))
if (unknown.length) problems.push(`unknown field(s): ${unknown.join(', ')}`)

for (const [field, val] of Object.entries(pub)) {
if (val === undefined || val === null) continue
if (typeof val !== 'string' || !val.trim()) { problems.push(`"${field}" must be a non-empty string`); continue }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Handle null publisher values without crashing or silently accepting them.

publisher: null reaches Object.keys(pub) and crashes validation. Field-level null values are also skipped and reported as valid despite the string-only schema. Reject both forms explicitly.

Proposed fix
-  if (typeof pub !== 'object' || Array.isArray(pub)) {
+  if (pub === null || typeof pub !== 'object' || Array.isArray(pub)) {
     checks.push({ name: 'Publisher', status: 'fail', message: '"publisher" must be an object of { company?, website?, supportEmail?, docsUrl?, privacyPolicy? }' })
     return checks
   }
@@
-    if (val === undefined || val === null) continue
+    if (val === undefined) continue
     if (typeof val !== 'string' || !val.trim()) { problems.push(`"${field}" must be a non-empty string`); continue }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof pub !== 'object' || Array.isArray(pub)) {
checks.push({ name: 'Publisher', status: 'fail', message: '"publisher" must be an object of { company?, website?, supportEmail?, docsUrl?, privacyPolicy? }' })
return checks
}
const problems: string[] = []
const unknown = Object.keys(pub).filter((k) => !PUBLISHER_FIELDS.includes(k as any))
if (unknown.length) problems.push(`unknown field(s): ${unknown.join(', ')}`)
for (const [field, val] of Object.entries(pub)) {
if (val === undefined || val === null) continue
if (typeof val !== 'string' || !val.trim()) { problems.push(`"${field}" must be a non-empty string`); continue }
if (pub === null || typeof pub !== 'object' || Array.isArray(pub)) {
checks.push({ name: 'Publisher', status: 'fail', message: '"publisher" must be an object of { company?, website?, supportEmail?, docsUrl?, privacyPolicy? }' })
return checks
}
const problems: string[] = []
const unknown = Object.keys(pub).filter((k) => !PUBLISHER_FIELDS.includes(k as any))
if (unknown.length) problems.push(`unknown field(s): ${unknown.join(', ')}`)
for (const [field, val] of Object.entries(pub)) {
if (val === undefined) continue
if (typeof val !== 'string' || !val.trim()) { problems.push(`"${field}" must be a non-empty string`); continue }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/validate.ts` around lines 253 - 264, Update the publisher validation
guard in the surrounding validation function to reject null explicitly alongside
non-object and array values, before calling Object.keys or Object.entries. In
the publisher field loop, stop skipping null values; treat null like undefined
or any non-string value and add the existing non-empty-string validation error,
while preserving valid string handling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant