From 401ab74ce1e69c86090bbe3660beac19da5ba264 Mon Sep 17 00:00:00 2001 From: reoring Date: Mon, 9 Nov 2026 18:23:51 +0900 Subject: [PATCH] web: template path never pairs with a marketplace definition resolveCreateCatalog picked the first ApplicationStackDefinition with the template's component count. Marketplace apps are ApplicationStackDefinitions too (catalogSource set, AppCatalogSource-owned); on Craftsman the first two-component definition was builder-console, so creating a Next.js app would deploy the console image under the user's app name. Only platform-shipped definitions (no catalogSource) are candidates. Mirrors platform #6481. --- web/src/model/create.test.ts | 6 ++++++ web/src/model/create.ts | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/web/src/model/create.test.ts b/web/src/model/create.test.ts index 6e7cabe..4ead346 100644 --- a/web/src/model/create.test.ts +++ b/web/src/model/create.test.ts @@ -168,6 +168,12 @@ describe('catalog', () => { expect(resolveCreateCatalog([imageOnly], [definition], 'sample-image')?.template).toBe(imageOnly) }) + test('marketplace definitions are never paired with a template, even when listed first with the same shape', () => { + const marketplace: ApplicationStackDefinition = { ...definition, id: 'builder-console', catalogSource: 'appthrust' } + expect(resolveCreateCatalog([template], [marketplace, definition], 'github-template')?.definitionRef?.name).toBe(definition.id) + expect(resolveCreateCatalog([template], [marketplace], 'github-template')).toBeUndefined() + }) + test('template allowlists trim and ignore case like the Platform validator; empty allowlists admit all templates', () => { const blocked = { ...sourceConnection, allowedRepositories: ['appthrust/template-other'] } expect(templateSourceProblem(template, [blocked])).toBe('not-admitted') diff --git a/web/src/model/create.ts b/web/src/model/create.ts index 22f6a2c..8241726 100644 --- a/web/src/model/create.ts +++ b/web/src/model/create.ts @@ -156,8 +156,11 @@ export function templatesFor( /** * Picks the requested (or default, or first) template for the source kind, and - * the stack definition whose component count matches it. Templates do not - * reference a definition, so the pairing is structural. + * the platform-shipped stack definition whose component count matches it. + * Templates do not reference a definition, so the pairing is structural; + * marketplace apps (`catalogSource` set, owned by an AppCatalogSource) are + * ApplicationStackDefinitions too and must never be candidates, or the template + * deploys that app's image under the user's name. */ export function resolveCreateCatalog( templates: readonly ApplicationTemplateSummary[], @@ -168,7 +171,7 @@ export function resolveCreateCatalog( const candidates = templatesFor(templates, sourceKind) const template = templateId ? candidates.find((item) => item.id === templateId) : candidates[0] if (!template) return undefined - const definition = definitions.find((item) => item.components.length === template.components.length) + const definition = definitions.find((item) => !item.catalogSource && item.components.length === template.components.length) if (!definition) return undefined const version = definition.versions?.find((item) => item.default && item.served !== false) ??