diff --git a/packages/plugins/@nocobase/plugin-js-template/README.md b/packages/plugins/@nocobase/plugin-js-template/README.md
index f55529c76e7..8677970c701 100644
--- a/packages/plugins/@nocobase/plugin-js-template/README.md
+++ b/packages/plugins/@nocobase/plugin-js-template/README.md
@@ -117,7 +117,7 @@ Push and Pull require the exact local Head, remote revision, target version, and
Git credentials are optional. Private HTTPS requires a complete `{{ $env.NAME }}` reference to a Secret JSON value containing `kind: "https"`, `username`, and `password`; literal credentials are rejected and only the Secret reference is stored in the remote record. HTTP is restricted to public repositories and rejects every authentication reference or credential input.
-New requests do not send a `transport` field; the server derives `http` or `https` from the repository URL. For compatibility, a legacy matching HTTP/HTTPS `transport` value is accepted and ignored, while a mismatching or unknown value is rejected. SSH URLs and SSH execution are not supported. Existing legacy SSH records remain readable so administrators can inspect, disconnect, or delete them, but they cannot be tested, planned, pulled, pushed, or executed. Removal of that persisted compatibility shape requires verified inventory across every supported environment.
+New requests do not send a `transport` field; the server derives `http` or `https` from the repository URL. For compatibility, a legacy matching HTTP/HTTPS `transport` value is accepted and ignored, while a mismatching or unknown value is rejected. SSH URLs, SSH execution, and persisted SSH remote compatibility are not supported.
Branch is optional while configuring or creating from a non-empty Git repository. When omitted, the remote symbolic `HEAD` is resolved and the resulting branch is persisted. An empty repository has no default branch, so its branch must be supplied explicitly; NocoBase does not guess `main` or another branch name.
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/client-v2/components/JsTemplateSyncDrawer.tsx b/packages/plugins/@nocobase/plugin-js-template/src/client-v2/components/JsTemplateSyncDrawer.tsx
index 723c20731c9..90846e58439 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/client-v2/components/JsTemplateSyncDrawer.tsx
+++ b/packages/plugins/@nocobase/plugin-js-template/src/client-v2/components/JsTemplateSyncDrawer.tsx
@@ -355,21 +355,10 @@ export function JsTemplateSyncDrawer(props: JsTemplateSyncDrawerProps) {
{loadState === 'ready' && source && plan ? (
<>
- {source.status === 'unsupported' ? (
-
- ) : null}
Git
{source.config.url}
-
- {source.config.transport === 'unsupported' ? 'SSH' : source.config.transport.toUpperCase()}
-
+ {source.config.transport.toUpperCase()}
{source.config.branch || t('Default branch')}
@@ -413,21 +402,21 @@ export function JsTemplateSyncDrawer(props: JsTemplateSyncDrawerProps) {
{t('Test connection')}
runSyncOperation('pull')}
>
{t('Pull from Git')}
{
it.each([
['get', { projectId: repo.id }, 'pullFromSyncSource'],
@@ -196,47 +194,6 @@ describe('jsTemplateSync resource', () => {
expect(fixture.runtime.planUnconfigured).toHaveBeenCalledWith(repo.vscRepoId);
});
- it('exposes legacy SSH metadata as unsupported while allowing disconnect only', async () => {
- const unsupportedRemote: VscFileRemoteRecord = {
- ...remote,
- status: 'unsupported',
- config: {
- url: `${unsupportedGitScheme}://git@example.com/project.git`,
- branch: 'main',
- subdirectory: null,
- transport: 'unsupported',
- legacyTransport: 'ssh',
- },
- };
- const fixture = createFixture({ remote: unsupportedRemote });
-
- const get = await runAction(fixture, 'get', { projectId: repo.id }, ['manageSyncSource']);
- const planned = await runAction(fixture, 'plan', { projectId: repo.id }, ['manageSyncSource']);
- const tested = await runAction(fixture, 'testConnection', { projectId: repo.id }, ['manageSyncSource']);
- const disconnected = await runAction(fixture, 'disconnect', { projectId: repo.id }, ['manageSyncSource']);
-
- expect(get.body).toMatchObject({
- source: {
- status: 'unsupported',
- config: { transport: 'unsupported', legacyTransport: 'ssh' },
- credentialConfigured: false,
- authRefDisplay: null,
- },
- });
- expect(JSON.stringify(get.body)).not.toContain('LEGACY_SSH');
- expect(planned.body).toMatchObject({
- source: { status: 'unsupported' },
- plan: { state: 'error', reasonCode: 'legacy-ssh-unsupported', canPull: false, canPush: false },
- });
- expect(tested).toMatchObject({
- status: 422,
- body: { errors: [{ code: 'JS_TEMPLATE_SYNC_CONFIG_INVALID' }] },
- });
- expect(fixture.runtime.testTarget).not.toHaveBeenCalled();
- expect(disconnected.status).toBeUndefined();
- expect(fixture.runtime.disconnectRemote).toHaveBeenCalledWith(unsupportedRemote.id);
- });
-
it('enforces strict input allowlists before calling the runtime', async () => {
const fixture = createFixture();
const ctx = await runAction(
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/server/__tests__/swagger.test.ts b/packages/plugins/@nocobase/plugin-js-template/src/server/__tests__/swagger.test.ts
index ec50d148d40..a176f520e31 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/server/__tests__/swagger.test.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/server/__tests__/swagger.test.ts
@@ -56,12 +56,10 @@ describe('js-template swagger', () => {
const schemas = swaggerDocument.components.schemas;
const config = schemas.JsTemplateGitRemoteConfigDraft;
const outputConfig = schemas.JsTemplateGitRemoteConfigOutput;
- const unsupportedOutputConfig = schemas.JsTemplateUnsupportedGitRemoteConfigOutput;
const createRequest = schemas.JsTemplateSyncCreateFromGitRequest;
expect(config.properties).not.toHaveProperty('transport');
expect(outputConfig.properties.transport).toMatchObject({ enum: ['http', 'https'], readOnly: true });
- expect(unsupportedOutputConfig.properties.transport).toMatchObject({ enum: ['unsupported'], readOnly: true });
expect(config.properties.url.pattern).toBe('^https?://');
expect(createRequest).toMatchObject({
type: 'object',
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/server/resources/jsTemplateSync.ts b/packages/plugins/@nocobase/plugin-js-template/src/server/resources/jsTemplateSync.ts
index a3e4336fe1d..fda42ff074c 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/server/resources/jsTemplateSync.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/server/resources/jsTemplateSync.ts
@@ -17,7 +17,7 @@ import type {
VscRemoteProvider,
VscRemoteSyncPlan,
} from '../../shared/vsc-file/remote-sync-types';
-import { isActiveVscRemote, isUnsupportedVscRemote } from '../../shared/vsc-file/remote-sync-types';
+import { isActiveVscRemote } from '../../shared/vsc-file/remote-sync-types';
import { uid } from '@nocobase/utils';
import { JS_TEMPLATE_COLLECTIONS, type JsTemplateAclAction } from '../../constants';
@@ -355,9 +355,6 @@ async function getSyncSource(
): Promise {
const project = await services.projectService.getInternalProject(requireProjectId(input), ctx);
const remote = await services.getRemoteSyncRuntime().getRemote(project.vscRepoId, remoteName);
- if (remote && isUnsupportedVscRemote(remote)) {
- return { projectId: project.id, source: toSourceSummary(remote, null) };
- }
const activeRemote = remote?.status === 'active' ? remote : null;
const revision = activeRemote ? await services.getRemoteSyncRuntime().getLatestMappedRevision(activeRemote.id) : null;
return {
@@ -374,9 +371,6 @@ async function configureSyncSource(
const project = await services.projectService.getInternalProject(requireProjectId(input), ctx);
const provider = requireProvider(input.provider);
const saved = await services.getRemoteSyncRuntime().getRemote(project.vscRepoId, remoteName);
- if (saved && isUnsupportedVscRemote(saved) && typeof input.authRef === 'undefined') {
- throw unsupportedLegacyRemote();
- }
const authRef = typeof input.authRef === 'undefined' ? saved?.authRef ?? null : requireNullableAuthRef(input.authRef);
return runSyncAudit(services, ctx, project.id, 'syncConfigure', async () => {
const runtime = services.getRemoteSyncRuntime();
@@ -424,9 +418,6 @@ async function testConnection(
): Promise {
const project = await services.projectService.getInternalProject(requireProjectId(input), ctx);
const saved = await services.getRemoteSyncRuntime().getRemote(project.vscRepoId, remoteName);
- if (saved && isUnsupportedVscRemote(saved)) {
- throw unsupportedLegacyRemote();
- }
const provider = typeof input.provider === 'undefined' ? saved?.provider : requireProvider(input.provider);
const config = typeof input.config === 'undefined' ? saved?.config : requireRecord(input.config, 'config');
const authRef = typeof input.authRef === 'undefined' ? saved?.authRef ?? null : requireNullableAuthRef(input.authRef);
@@ -460,13 +451,6 @@ async function planSync(
): Promise {
const project = await services.projectService.getInternalProject(requireProjectId(input), ctx);
const remote = await services.getRemoteSyncRuntime().getRemote(project.vscRepoId, remoteName);
- if (remote && isUnsupportedVscRemote(remote)) {
- return {
- projectId: project.id,
- source: toSourceSummary(remote, null),
- plan: unsupportedLegacyPlan(remote),
- };
- }
const activeRemote = remote?.status === 'active' ? remote : null;
return runSyncAudit(services, ctx, project.id, 'syncPlan', async () => {
const plan = activeRemote
@@ -483,21 +467,6 @@ async function planSync(
});
}
-function unsupportedLegacyPlan(remote: VscFileRemoteRecord): VscRemoteSyncPlan {
- return {
- state: 'error',
- action: 'conflict',
- reasonCode: 'legacy-ssh-unsupported',
- canPull: false,
- canPush: false,
- fingerprint: `unsupported:${remote.id}:${remote.version}`,
- remoteTargetVersion: remote.version,
- local: { headCommitId: null, contentHash: null },
- remote: { revision: null, contentHash: null, contentHashKnown: false },
- baseline: null,
- };
-}
-
async function pullSync(
services: SyncActionServices,
input: ResourceActionInput,
@@ -592,9 +561,6 @@ async function pushSync(
async function requireSavedRemote(services: SyncActionServices, vscRepoId: string): Promise {
const remote = await services.getRemoteSyncRuntime().getRemote(vscRepoId, remoteName);
- if (remote && isUnsupportedVscRemote(remote)) {
- throw unsupportedLegacyRemote();
- }
if (!remote || !isActiveVscRemote(remote)) {
throw new JsTemplateError('JS_TEMPLATE_SYNC_CONFIG_INVALID', 'An active sync source is required', {
details: { reasonCode: 'sync-source-not-configured' },
@@ -603,12 +569,6 @@ async function requireSavedRemote(services: SyncActionServices, vscRepoId: strin
return remote;
}
-function unsupportedLegacyRemote(): JsTemplateError {
- return new JsTemplateError('JS_TEMPLATE_SYNC_CONFIG_INVALID', 'Legacy SSH sync sources are unsupported', {
- details: { reasonCode: 'legacy-ssh-unsupported' },
- });
-}
-
async function assertScopedPermission(
db: Database,
ctx: JsTemplateServiceContext,
@@ -739,15 +699,14 @@ function normalizeSyncError(error: unknown): unknown {
}
function toSourceSummary(remote: VscFileRemoteRecord, revision: string | null = null): JsTemplateSyncSourceSummary {
- const unsupported = isUnsupportedVscRemote(remote);
return {
provider: remote.provider,
config: { ...remote.config },
- status: unsupported ? 'unsupported' : remote.status,
+ status: remote.status,
remoteTargetVersion: remote.version,
revision,
- credentialConfigured: unsupported ? false : remote.authRef !== null,
- authRefDisplay: unsupported ? null : toAuthRefDisplay(remote.authRef),
+ credentialConfigured: remote.authRef !== null,
+ authRefDisplay: toAuthRefDisplay(remote.authRef),
lastSyncedAt: remote.lastSyncedAt,
};
}
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/__tests__/remote-stores.test.ts b/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/__tests__/remote-stores.test.ts
index c58d82fb1d7..8b98d7340df 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/__tests__/remote-stores.test.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/__tests__/remote-stores.test.ts
@@ -24,7 +24,6 @@ const normalizedConfig: VscRemoteNormalizedConfig = {
transport: 'https',
};
-const unsupportedGitScheme = ['s', 's', 'h'].join('');
const forbiddenCredentialKeyPattern = new RegExp(
['private', 'Key|credential|authorization|password|secret|token'].join(''),
'i',
@@ -90,39 +89,6 @@ describe('vsc-file remote stores', () => {
await expect(db.getRepository('vscFileRemotes').count()).resolves.toBe(0);
});
- it('keeps legacy SSH remotes readable, disconnectable, and deletable without executing them', async () => {
- const repoId = await createRepository('legacy-ssh');
- const record = await db.getRepository('vscFileRemotes').create({
- values: {
- repoId,
- name: 'origin',
- provider: 'git',
- config: {
- url: `${unsupportedGitScheme}://git@example.com/project.git`,
- branch: 'main',
- subdirectory: null,
- transport: 'ssh',
- },
- authRef: '{{ $env.LEGACY_SSH_KEY }}',
- status: 'active',
- version: 1,
- },
- });
- const store = new RemoteStore(db);
-
- const remote = await store.get(record.get('id') as string);
- expect(remote).toMatchObject({
- status: 'unsupported',
- config: { transport: 'unsupported', legacyTransport: 'ssh', branch: 'main' },
- authRef: '{{ $env.LEGACY_SSH_KEY }}',
- });
-
- const disconnected = await store.disconnect(remote.id);
- expect(disconnected).toMatchObject({ status: 'unsupported', config: { transport: 'unsupported' }, authRef: null });
- await store.deleteRemote(remote.id);
- await expect(db.getRepository('vscFileRemotes').count()).resolves.toBe(0);
- });
-
it('preserves mappings across auth rotation and isolates old target versions', async () => {
const repoId = await createRepository('target-version');
const remoteStore = new RemoteStore(db);
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteReconcileService.ts b/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteReconcileService.ts
index e2a992829d6..bdbabfb3a4b 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteReconcileService.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteReconcileService.ts
@@ -127,7 +127,7 @@ export class RemoteReconcileService {
const remote = await this.remoteStore.get(claimed.remoteId);
if (remote.status !== 'active') {
const disabledError = new RemoteSyncError('CONFIG_INVALID', 'Remote is disabled', {
- details: { reasonCode: remote.status === 'unsupported' ? 'legacy-ssh-unsupported' : 'remote-disabled' },
+ details: { reasonCode: 'remote-disabled' },
});
const failed = await this.jobStore.fail(claimed.id, claimToken, disabledError.code);
await ctx.onRecoveryResult?.({ job: failed, errorCode: disabledError.code });
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteStore.ts b/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteStore.ts
index b8ee6853628..a64c9dba6c3 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteStore.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/server/vsc-file/remotes/RemoteStore.ts
@@ -16,7 +16,6 @@ import type {
VscFileRemoteRecord,
VscRemoteNormalizedConfig,
VscRemoteProvider,
- VscUnsupportedGitRemoteConfig,
} from '../../../shared/vsc-file/remote-sync-types';
import { normalizeGitRemoteConfig } from './providers/git/gitConfig';
import { RemoteSyncError } from './RemoteSyncAdapter';
@@ -313,7 +312,6 @@ export class RemoteStore {
export function remoteFromRecord(record: Model): VscFileRemoteRecord {
const provider = record.get('provider') as VscRemoteProvider;
const persistedConfig = record.get('config') as unknown;
- const unsupportedConfig = provider === 'git' ? normalizeUnsupportedPersistedGitConfig(persistedConfig) : null;
const common = {
id: record.get('id') as string,
repoId: record.get('repoId') as string,
@@ -327,9 +325,6 @@ export function remoteFromRecord(record: Model): VscFileRemoteRecord {
createdAt: nullableDateString(record.get('createdAt')) || undefined,
updatedAt: nullableDateString(record.get('updatedAt')) || undefined,
};
- if (unsupportedConfig) {
- return { ...common, config: unsupportedConfig, status: 'unsupported' };
- }
return {
...common,
config: validateNormalizedConfig(provider, persistedConfig as VscRemoteNormalizedConfig),
@@ -350,27 +345,6 @@ function validateNormalizedConfig(
throw new RemoteSyncError('UNSUPPORTED_PROVIDER', `Unsupported remote provider "${provider}"`);
}
-function normalizeUnsupportedPersistedGitConfig(value: unknown): VscUnsupportedGitRemoteConfig | null {
- if (!value || typeof value !== 'object' || Array.isArray(value)) {
- return null;
- }
- const config = value as Record;
- if (config.transport !== 'ssh') {
- return null;
- }
- return {
- url: safeLegacyString(config.url),
- branch: safeLegacyString(config.branch),
- subdirectory: typeof config.subdirectory === 'string' ? config.subdirectory : null,
- transport: 'unsupported',
- legacyTransport: 'ssh',
- };
-}
-
-function safeLegacyString(value: unknown): string {
- return typeof value === 'string' ? value : '';
-}
-
function assertNoSensitiveConfigKeys(value: unknown): void {
if (!value || typeof value !== 'object') {
return;
@@ -396,9 +370,6 @@ function serializeNullableAuthRef(authRef: VscRemoteCredentialRef | null): strin
}
function sameConfig(left: VscFileRemoteConfig, right: VscRemoteNormalizedConfig): boolean {
- if (left.transport === 'unsupported') {
- return false;
- }
return (
left.url === right.url &&
left.branch === right.branch &&
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/shared/types.ts b/packages/plugins/@nocobase/plugin-js-template/src/shared/types.ts
index aaee469617a..5b22951599c 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/shared/types.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/shared/types.ts
@@ -21,7 +21,6 @@ import type { RunJSSourceLocator, VscCommitRecord } from '@nocobase/runjs/worksp
import type {
VscGitRemoteConfig,
VscGitRemoteConfigDraft,
- VscUnsupportedGitRemoteConfig,
VscRemotePlannerAction,
VscRemotePlannerLocalSummary,
VscRemotePlannerRemoteSummary,
@@ -712,11 +711,11 @@ export type JsTemplateSyncState = VscRemotePlannerState;
export type JsTemplateSyncAction = VscRemotePlannerAction;
-export type JsTemplateSyncSourceStatus = 'active' | 'disabled' | 'unsupported';
+export type JsTemplateSyncSourceStatus = 'active' | 'disabled';
export interface JsTemplateSyncRemoteTarget {
provider: JsTemplateSyncProvider;
- config: VscGitRemoteConfig | VscUnsupportedGitRemoteConfig;
+ config: VscGitRemoteConfig;
}
export interface JsTemplateSyncRemoteTargetDraft {
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/shared/vsc-file/remote-sync-types.ts b/packages/plugins/@nocobase/plugin-js-template/src/shared/vsc-file/remote-sync-types.ts
index 91de3cc8713..3b9cf7af18d 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/shared/vsc-file/remote-sync-types.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/shared/vsc-file/remote-sync-types.ts
@@ -26,17 +26,9 @@ export interface VscGitRemoteConfig {
transport: VscGitRemoteTransport;
}
-export interface VscUnsupportedGitRemoteConfig {
- url: string;
- branch: string;
- subdirectory: string | null;
- transport: 'unsupported';
- legacyTransport: 'ssh';
-}
-
export type VscRemoteNormalizedConfig = VscGitRemoteConfig;
-export type VscFileRemoteConfig = VscRemoteNormalizedConfig | VscUnsupportedGitRemoteConfig;
+export type VscFileRemoteConfig = VscRemoteNormalizedConfig;
export type GitRemoteCredential = {
kind: 'https';
@@ -152,7 +144,7 @@ export const remoteSyncErrorCodes = [
export type RemoteSyncErrorCode = (typeof remoteSyncErrorCodes)[number];
-export type VscFileRemoteStatus = 'active' | 'disabled' | 'unsupported';
+export type VscFileRemoteStatus = 'active' | 'disabled';
interface VscFileRemoteRecordBase {
id: string;
@@ -169,11 +161,7 @@ interface VscFileRemoteRecordBase {
}
export type VscFileRemoteRecord = VscFileRemoteRecordBase &
- (
- | { config: VscRemoteNormalizedConfig; status: 'active' }
- | { config: VscRemoteNormalizedConfig; status: 'disabled' }
- | { config: VscUnsupportedGitRemoteConfig; status: 'unsupported' }
- );
+ ({ config: VscRemoteNormalizedConfig; status: 'active' } | { config: VscRemoteNormalizedConfig; status: 'disabled' });
export type VscFileActiveRemoteRecord = VscFileRemoteRecordBase & {
config: VscRemoteNormalizedConfig;
@@ -184,12 +172,6 @@ export function isActiveVscRemote(remote: VscFileRemoteRecord): remote is VscFil
return remote.status === 'active';
}
-export function isUnsupportedVscRemote(
- remote: Pick,
-): remote is Pick & { config: VscUnsupportedGitRemoteConfig } {
- return remote.config.transport === 'unsupported';
-}
-
export type VscFileSyncOperation = 'probe' | 'push' | 'pull';
export type VscFileSyncJobStatus = 'pending' | 'running' | 'succeeded' | 'failed' | 'finalize-pending';
diff --git a/packages/plugins/@nocobase/plugin-js-template/src/swagger/schemas.ts b/packages/plugins/@nocobase/plugin-js-template/src/swagger/schemas.ts
index 0bd34cc4168..9ad3f35e59d 100644
--- a/packages/plugins/@nocobase/plugin-js-template/src/swagger/schemas.ts
+++ b/packages/plugins/@nocobase/plugin-js-template/src/swagger/schemas.ts
@@ -59,18 +59,6 @@ export const jsTemplateSchemas = {
},
additionalProperties: false,
},
- JsTemplateUnsupportedGitRemoteConfigOutput: {
- type: 'object',
- required: ['url', 'branch', 'subdirectory', 'transport', 'legacyTransport'],
- properties: {
- url: { type: 'string' },
- branch: { type: 'string', minLength: 1 },
- subdirectory: nullableString,
- transport: { type: 'string', enum: ['unsupported'], readOnly: true },
- legacyTransport: { type: 'string', enum: ['ssh'], readOnly: true },
- },
- additionalProperties: false,
- },
JsTemplateSyncProjectRequest: {
type: 'object',
required: ['projectId'],
@@ -182,10 +170,7 @@ export const jsTemplateSchemas = {
nullable: true,
properties: {
config: {
- oneOf: [
- { $ref: '#/components/schemas/JsTemplateGitRemoteConfigOutput' },
- { $ref: '#/components/schemas/JsTemplateUnsupportedGitRemoteConfigOutput' },
- ],
+ $ref: '#/components/schemas/JsTemplateGitRemoteConfigOutput',
},
},
additionalProperties: true,