feat(auth-adapter,bff-kit,client-shell): 마지막 추출 배치
@yakcloud/auth-adapter: core 인터페이스(프레임워크 무관, ds-sdk type-only) + dev-bypass + next-auth 격리 계층(peer). exports 서브패스(./core·./dev-bypass). @yakcloud/bff-kit: respond(web 표준)·serialize(Prisma-free)·ratelimit·session(AuthAdapter DI). auth-adapter=devDependency(type-only), 죽은 next peer 제거. @yakcloud/client-shell: ServiceClientShell 셸 크롬(헤더/탭/pane/가드)+패널 슬롯+BackupPanel 스텁. (FileBrowser 는 Phase 1) FIX-FIRST 반영: client-shell tsconfig jsx:react-jsx + @types/react(-dom), bff-kit/auth-adapter += @types/node. 7개 패키지 전부 tsc --noEmit 통과. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
61
packages/bff-kit/src/handlers/binding.ts
Normal file
61
packages/bff-kit/src/handlers/binding.ts
Normal file
@ -0,0 +1,61 @@
|
||||
// @yakcloud/bff-kit — handlers/binding
|
||||
// 앱 ↔ 서비스 바인딩 생성. 같은 클러스터 강제(유일 격리점) + 별칭 중복 방지 → createBinding + 재배포.
|
||||
// Repo 는 DI. Response(accepted) 반환. yakconsole services/[id]/bindings/route.ts 하강.
|
||||
|
||||
import { ApiError, accepted } from "../respond";
|
||||
import { requireOwnership, type SessionUser } from "../session";
|
||||
import type { BindingRepo } from "./types";
|
||||
|
||||
export interface CreateBindingArgs {
|
||||
serviceId: string;
|
||||
deploymentId: string;
|
||||
alias?: string | null;
|
||||
}
|
||||
|
||||
// 소유·같은클러스터·READY·중복 검증 후 바인딩 생성 → 앱 재배포 큐잉 + 감사.
|
||||
export async function createServiceBinding(
|
||||
repo: BindingRepo,
|
||||
user: SessionUser,
|
||||
args: CreateBindingArgs,
|
||||
): Promise<Response> {
|
||||
const svc = await repo.getService(args.serviceId);
|
||||
if (!svc || svc.deletedAt) throw new ApiError("NOT_FOUND");
|
||||
requireOwnership(await repo.getCluster(svc.clusterId), user);
|
||||
|
||||
const dep = await repo.getDeployment(args.deploymentId);
|
||||
if (!dep) throw new ApiError("NOT_FOUND", "앱을 찾을 수 없습니다.");
|
||||
// LOCKED#1: 같은 클러스터의 앱에만 연결(크로스클러스터 차단) — 유일한 격리 강제점.
|
||||
if (dep.clusterId !== svc.clusterId) {
|
||||
throw new ApiError("VALIDATION_FAILED", "같은 클러스터의 앱에만 연결할 수 있습니다.");
|
||||
}
|
||||
if (svc.status !== "READY") {
|
||||
throw new ApiError("VALIDATION_FAILED", "서비스가 준비(READY)된 후에 연결할 수 있습니다.");
|
||||
}
|
||||
|
||||
const alias = args.alias ?? svc.name;
|
||||
const existing = await repo.listBindingsByDeployment(dep.id);
|
||||
if (existing.some((b) => b.serviceId === svc.id)) {
|
||||
throw new ApiError("NAME_TAKEN", "이미 이 앱에 연결된 서비스입니다.");
|
||||
}
|
||||
if (existing.some((b) => b.alias === alias)) {
|
||||
throw new ApiError("NAME_TAKEN", "이미 사용 중인 별칭입니다.");
|
||||
}
|
||||
|
||||
const binding = await repo.createBinding({
|
||||
serviceId: svc.id,
|
||||
deploymentId: dep.id,
|
||||
alias,
|
||||
injectedKeys: [], // 실제 주입 키(BIND_ENV_VARS 파생)는 배포 시 백엔드가 도출.
|
||||
});
|
||||
// 바인딩 반영 = 재배포(deployment_manifest 가 binds 를 다시 엮음).
|
||||
await repo.updateDeployment(dep.id, { status: "QUEUED", statusMessage: null });
|
||||
await repo.enqueueJob({ kind: "app.deploy", payload: { deploymentId: dep.id } });
|
||||
await repo.addAudit({
|
||||
userId: user.id,
|
||||
action: "service.bind",
|
||||
targetType: "service",
|
||||
targetId: svc.id,
|
||||
meta: { deploymentId: dep.id, app: dep.name, alias },
|
||||
});
|
||||
return accepted({ id: binding.id, alias, serviceId: svc.id, deploymentId: dep.id });
|
||||
}
|
||||
Reference in New Issue
Block a user