diff --git a/packages/targets/browser-edge/src/index.test.ts b/packages/targets/browser-edge/src/index.test.ts index 53ff9180..3e95b6db 100644 --- a/packages/targets/browser-edge/src/index.test.ts +++ b/packages/targets/browser-edge/src/index.test.ts @@ -86,12 +86,12 @@ describe('browser-edge target adapter', () => { expect(result).toEqual({ artifact }); }); - it('keeps product IDs with path separators inside the output directory', async () => { + it('rejects Edge product IDs that are blank or not URL path segments', async () => { const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-out-')); const projectDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-project-')); tempDirs.push(outDir, projectDir); - const result = await adapter.build(fakeBuildContext({ + await expect(adapter.build(fakeBuildContext({ outDir, projectDir, version: '1.2.3', @@ -99,11 +99,44 @@ describe('browser-edge target adapter', () => { }) as any, { productId: '../edge-product', sourceDir: 'extension-dist', - }); + })).rejects.toThrow('productId must be a single URL path segment'); - const plan = JSON.parse(await readFile(result.artifact, 'utf-8')); - expect(plan.productId).toBe('../edge-product'); - expect(plan.artifact).toBe(join(outDir, 'edge-product-1.2.3.zip')); - expect(plan.command).toEqual(['zip', '-r', join(outDir, 'edge-product-1.2.3.zip'), '.']); + await expect(adapter.build(fakeBuildContext({ + outDir, + projectDir, + version: '1.2.3', + dryRun: true, + }) as any, { + productId: ' ', + sourceDir: 'extension-dist', + })).rejects.toThrow('browser-edge requires productId'); + + expect(execMock).not.toHaveBeenCalled(); + }); + + it('rejects blank optional text before packaging or publishing', async () => { + const outDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-out-')); + const projectDir = await mkdtemp(join(tmpdir(), 'sh1pt-edge-project-')); + tempDirs.push(outDir, projectDir); + + await expect(adapter.build(fakeBuildContext({ + outDir, + projectDir, + version: '1.2.3', + dryRun: true, + }) as any, { + productId: 'edge-product', + sourceDir: ' ', + })).rejects.toThrow('browser-edge requires sourceDir'); + + await expect(adapter.ship(fakeBuildContext({ + outDir, + projectDir, + version: '1.2.3', + dryRun: true, + }) as any, { + productId: 'edge-product', + notes: ' ', + })).rejects.toThrow('browser-edge requires notes'); }); }); diff --git a/packages/targets/browser-edge/src/index.ts b/packages/targets/browser-edge/src/index.ts index 33cda9a6..cdbffa23 100644 --- a/packages/targets/browser-edge/src/index.ts +++ b/packages/targets/browser-edge/src/index.ts @@ -8,8 +8,29 @@ interface Config { notes?: string; // release notes for reviewer } +function requireText(value: string | undefined, name: string): string { + if (typeof value !== 'string' || value.trim() === '') { + throw new Error(`browser-edge requires ${name}`); + } + return value.trim(); +} + +function optionalText(value: string | undefined, name: string): string | undefined { + if (value === undefined) return undefined; + const trimmed = requireText(value, name); + return trimmed; +} + +function requireProductId(value: string | undefined): string { + const productId = requireText(value, 'productId'); + if (/[\\/?#\x00-\x1F\x7F]/.test(productId)) { + throw new Error('browser-edge productId must be a single URL path segment'); + } + return productId; +} + function sourceDir(ctx: { projectDir: string }, config: Config): string { - const dir = config.sourceDir ?? 'dist'; + const dir = optionalText(config.sourceDir, 'sourceDir') ?? 'dist'; return isAbsolute(dir) ? dir : join(ctx.projectDir, dir); } @@ -21,15 +42,16 @@ function safeFileStem(value: string): string { } function packageArtifact(ctx: { outDir: string; version: string }, config: Config): string { - return join(ctx.outDir, `${safeFileStem(config.productId)}-${safeFileStem(ctx.version)}.zip`); + return join(ctx.outDir, `${safeFileStem(requireProductId(config.productId))}-${safeFileStem(ctx.version)}.zip`); } function packagePlan(ctx: { projectDir: string; outDir: string; version: string }, config: Config) { + const productId = requireProductId(config.productId); const src = sourceDir(ctx, config); const artifact = packageArtifact(ctx, config); return { provider: 'microsoft-edge-addons', - productId: config.productId, + productId, version: ctx.version, sourceDir: src, artifact, @@ -43,10 +65,11 @@ export default defineTarget({ kind: 'browser-ext', label: 'Microsoft Edge Add-ons', async build(ctx, config) { + const productId = requireProductId(config.productId); const src = sourceDir(ctx, config); const zipPath = packageArtifact(ctx, config); - ctx.log(`pack Edge extension from ${src} for v${ctx.version}`); + ctx.log(`pack Edge extension ${productId} from ${src} for v${ctx.version}`); if (ctx.dryRun) { const planPath = join(ctx.outDir, 'edge-package.json'); @@ -79,9 +102,12 @@ export default defineTarget({ return { artifact: zipPath }; }, async ship(ctx, config) { - ctx.log(`upload ${config.productId} to Edge Partner Center (v${ctx.version})`); + const productId = requireProductId(config.productId); + const notes = optionalText(config.notes, 'notes'); + + ctx.log(`upload ${productId} to Edge Partner Center (v${ctx.version})`); if (ctx.dryRun) { - return { id: `${config.productId}@${ctx.version}`, url: `https://microsoftedge.microsoft.com/addons/detail/${config.productId}` }; + return { id: `${productId}@${ctx.version}`, url: `https://microsoftedge.microsoft.com/addons/detail/${productId}` }; } // Fetch secrets for Edge Publish API OAuth @@ -119,7 +145,7 @@ export default defineTarget({ // Step 2: Upload the package (zip) as a draft submission ctx.log('uploading package...'); - const uploadUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${config.productId}/submissions/draft/package`; + const uploadUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${productId}/submissions/draft/package`; const zipBuf = await readFile(ctx.artifact); const uploadRes = await fetch(uploadUrl, { @@ -141,8 +167,7 @@ export default defineTarget({ // Step 3: Submit the draft for review ctx.log('submitting for review...'); - const submitUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${config.productId}/submissions`; - const notes = config.notes; + const submitUrl = `https://api.addons.microsoftedge.microsoft.com/v1/products/${productId}/submissions`; const submitBody: Record = {}; if (notes) { submitBody.notes = notes; } @@ -164,8 +189,8 @@ export default defineTarget({ ctx.log('✓ submitted to Edge Partner Center'); return { - id: `${config.productId}@${ctx.version}`, - url: `https://microsoftedge.microsoft.com/addons/detail/${config.productId}`, + id: `${productId}@${ctx.version}`, + url: `https://microsoftedge.microsoft.com/addons/detail/${productId}`, meta: { submissionId: submitData.id }, }; },