Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.10.0] - 2026-07-16

### Added

- App publisher identity in `bagdock.json` (BDOK-678): an optional `publisher` block (`company`, `website`, `supportEmail`, `docsUrl`, `privacyPolicy`), an `icon` path, and a `description`. Publisher fields inherit from your org profile by default and override per field, so most apps need no `publisher` block at all.
- `bagdock validate` now checks the app icon (square PNG or SVG, at least 128px, up to 256KB) and any publisher fields. A public app with no publisher block gets a warning, not a failure, since its identity inherits the org profile.
- `bagdock init` offers interactive publisher overrides and inherits from the org profile by default. `--yes` (or a non-interactive shell) skips the prompts.
- `bagdock deploy` ships the publisher block, description, and icon bytes. The platform hosts the icon and mirrors publisher identity into the dashboard.
- `bagdock submit` runs an advisory publisher-completeness check for public apps before submitting for review.

## [0.5.0] - 2026-04-05

### Added
Expand Down
1 change: 1 addition & 0 deletions bin/bagdock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ program
.option('-c, --category <category>', 'Category')
.option('-s, --slug <slug>', 'Unique project slug')
.option('-n, --name <name>', 'Display name')
.option('-y, --yes', 'Skip interactive prompts (inherit publisher identity from the org profile)')
.action((dir: string | undefined, opts: Record<string, string>) => init(dir ?? '.', opts))

// ---------- Dev ----------
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bagdock/cli",
"version": "0.9.1",
"version": "0.10.0",
"description": "Bagdock developer CLI — build, test, and deploy apps and edges on the Bagdock platform",
"keywords": [
"bagdock",
Expand Down
31 changes: 31 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,27 @@ export interface DisplayDeclaration {
copyable?: boolean
}

/**
* Optional publisher-identity override (BDOK-678). Publisher identity normally
* derives from the owning org's profile at render time; this block lets an app
* override individual fields from the code that ships it (HubSpot-projects
* style). It is a PER-FIELD raw-config override — the platform mirrors it into
* the owning operator's regional `publisher_configs` on deploy, where the
* dashboard resolves each field as `manifest override → org profile default`.
*
* Every field is optional: an omitted field falls through to the org profile,
* so a public app with no `publisher` block still renders its org's identity
* (the derivation law — no N stale copies of one org fact). Field names follow
* the BDOK-560 camelCase-manifest → mirrored-config convention.
*/
export interface PublisherDeclaration {
company?: string
website?: string
supportEmail?: string
docsUrl?: string
privacyPolicy?: string
}

export interface BagdockJson {
name: string
slug: string
Expand All @@ -319,6 +340,16 @@ export interface BagdockJson {
visibility: 'public' | 'private'
main: string
compatibilityDate?: string
/** One-line marketplace description. Mirrored into control-plane config. */
description?: string
/**
* Repo-relative path to the app icon (square PNG or SVG). Per-app by nature,
* so it is always authored in the manifest rather than derived from the org
* profile. Validated by `bagdock validate` (square, PNG/SVG, ≥128px, ≤256KB).
*/
icon?: string
/** Per-field publisher-identity override. See {@link PublisherDeclaration}. */
publisher?: PublisherDeclaration
env?: Record<string, { description?: string; required?: boolean }>
kv?: Record<string, KvDeclaration>
webhooks?: WebhookDeclaration[]
Expand Down
29 changes: 29 additions & 0 deletions src/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,28 @@ export async function deploy(opts: DeployOptions) {
...(config.webhooks ? { webhooks: config.webhooks } : {}),
...(config.inputs ? { inputs: config.inputs } : {}),
...(config.displays ? { displays: config.displays } : {}),
// BDOK-678: publisher identity override + marketplace description. The icon
// ships as a separate `icon` file part below. Only non-preview deploys
// persist these server-side.
...(config.description ? { description: config.description } : {}),
...(config.publisher ? { publisher: config.publisher } : {}),
}))

// App icon (BDOK-678): ship the bytes so the platform can host it and mirror a
// stable icon_url. `bagdock validate` has already checked it is a square
// PNG/SVG within the size cap; a missing file is skipped (validate warns).
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.`))
}
Comment on lines +141 to +150

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.

}

try {
const res = await fetch(`${getApiBase()}/api/v1/developer/apps/${config.slug}/deploy`, {
method: 'POST',
Expand Down Expand Up @@ -168,6 +188,8 @@ export async function deploy(opts: DeployOptions) {
workerUrl: string
namespace: string
previewHash?: string
publisher_warning?: string
secrets_warning?: string
}
}

Expand All @@ -180,6 +202,13 @@ export async function deploy(opts: DeployOptions) {
}
console.log(` Namespace: ${chalk.dim(result.data.namespace)}`)

if (result.data.secrets_warning) {
console.log(chalk.yellow(`\n ⚠ ${result.data.secrets_warning}`))
}
if (result.data.publisher_warning) {
console.log(chalk.yellow(`\n ⚠ ${result.data.publisher_warning}`))
}

if (environment === 'preview') {
console.log(chalk.dim('\n This is an ephemeral preview deploy. It will not replace the stable staging URL.'))
}
Expand Down
45 changes: 44 additions & 1 deletion src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
import { existsSync, mkdirSync, writeFileSync } from 'fs'
import { join, basename } from 'path'
import chalk from 'chalk'
import type { BagdockJson, ProjectType, ProjectKind } from './config'
import type { BagdockJson, ProjectType, ProjectKind, PublisherDeclaration } from './config'

interface InitOptions {
type?: string
kind?: string
category?: string
slug?: string
name?: string
yes?: boolean
}

const EDGE_KINDS = ['adapter', 'comms', 'webhook'] as const
Expand Down Expand Up @@ -57,6 +58,8 @@ export async function init(dir: string, opts: InitOptions) {
},
}

await promptPublisherOverrides(config, opts)

writeFileSync(join(projectDir, 'bagdock.json'), JSON.stringify(config, null, 2))

const srcDir = join(projectDir, 'src')
Expand Down Expand Up @@ -121,6 +124,46 @@ export async function init(dir: string, opts: InitOptions) {
console.log(` 3. ${chalk.cyan('bagdock deploy')} — deploy to Bagdock platform`)
}

/**
* Interactive, opt-in publisher-identity overrides (BDOK-678 Phase 2).
* Apps inherit publisher identity from the owning operator by default;
* this only runs in a TTY and lets the author override individual fields.
*/
async function promptPublisherOverrides(config: BagdockJson, opts: InitOptions): Promise<void> {
if (opts.yes === true || !process.stdin.isTTY) return

const readline = await import('readline')
const rl = readline.createInterface({ input: process.stdin, output: process.stdout })
const ask = (q: string): Promise<string> => new Promise((res) => rl.question(q, (a) => res(a.trim())))

try {
const wantsOverrides = await ask('Add publisher identity overrides now? Most apps inherit from your org profile. [y/N] ')
if (!/^y(es)?$/i.test(wantsOverrides)) return

const company = await ask('Company name (optional): ')
const website = await ask('Website URL (optional): ')
const supportEmail = await ask('Support email (optional): ')
const docsUrl = await ask('Docs URL (optional): ')
const privacyPolicy = await ask('Privacy policy URL (optional): ')

const publisher: PublisherDeclaration = {}
if (company) publisher.company = company
if (website) publisher.website = website
if (supportEmail) publisher.supportEmail = supportEmail
if (docsUrl) publisher.docsUrl = docsUrl
if (privacyPolicy) publisher.privacyPolicy = privacyPolicy
if (Object.keys(publisher).length > 0) config.publisher = publisher

const description = await ask('One-line description (optional): ')
if (description) config.description = description

const icon = await ask('Icon path, square PNG/SVG >=128px (optional): ')
if (icon) config.icon = icon
} finally {
rl.close()
}
}

function resolveKind(type: ProjectType, kindOpt?: string): ProjectKind {
if (kindOpt) return kindOpt as ProjectKind
return type === 'app' ? 'ui-extension' : 'adapter'
Expand Down
20 changes: 20 additions & 0 deletions src/submit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,26 @@ export async function submit() {
process.exit(1)
}

// Advisory publisher-identity pre-check (BDOK-678). This is a heads-up, not a
// gate: the authoritative completeness check runs server-side at review, where
// your org profile can complete fields the manifest omits (which this offline
// CLI cannot see). So we only nudge on likely gaps for public apps.
if (config.visibility === 'public') {
const pub = config.publisher ?? {}
const recommended: Array<[keyof typeof pub, string]> = [
['company', 'company'],
['website', 'website'],
['supportEmail', 'supportEmail'],
['privacyPolicy', 'privacyPolicy'],
]
const missing = recommended.filter(([k]) => !pub[k]).map(([, label]) => label)
if (missing.length) {
console.log(chalk.yellow(` Heads-up: publisher ${missing.join(', ')} not set in bagdock.json.`))
console.log(chalk.dim(` Reviewers need a complete publisher identity. These fields fall back to your org`))
console.log(chalk.dim(` profile — add a "publisher" block to override them per app. Submitting anyway.\n`))
}
}

console.log(chalk.cyan(`\nSubmitting ${chalk.bold(config.slug)} for marketplace review...\n`))

try {
Expand Down
Loading