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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@treeseed/api",
"version": "0.8.0-rc.61",
"version": "0.8.0-rc.62",
"description": "TreeSeed control-plane API and operations runner.",
"license": "AGPL-3.0-only",
"repository": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createHash } from 'node:crypto';
import { ControlPlaneStore,isoNow,parseJson } from '../../../../persistence/store.ts';

export const MANAGED_TEAM_PROJECT_SLUG='team';
export const MANAGED_TEAM_PROJECT_KIND='system-team-library';
export const managedTeamLibraryRepositoryName=(teamId:string)=>`team-library-${createHash('sha256').update(teamId).digest('hex').slice(0,12)}`;
export const isManagedTeamLibraryRepositoryName=(teamId:string,name:string)=>name==='team-library'||name===managedTeamLibraryRepositoryName(teamId);

export async function ensureManagedTeamLibraryProjectMethod(this:ControlPlaneStore,teamId:string){
await this.ensureInitialized();
Expand All @@ -10,8 +13,9 @@ export async function ensureManagedTeamLibraryProjectMethod(this:ControlPlaneSto
if(existing.metadata?.kind!==MANAGED_TEAM_PROJECT_KIND||existing.metadata?.systemManaged!==true)throw new Error('The reserved team project slug is occupied by a user-managed project.');
return existing;
}
const repositoryName=managedTeamLibraryRepositoryName(teamId);
const details=await this.createProject(teamId,{slug:MANAGED_TEAM_PROJECT_SLUG,name:'Team Library',description:'System-managed shared knowledge for this team.',metadata:{kind:MANAGED_TEAM_PROJECT_KIND,systemManaged:true,
libraryOnly:true,library:{repositoryName:'team-library',defaultBranch:'main',integrationBranch:'staging',status:'provisioning'},
libraryOnly:true,library:{repositoryName,defaultBranch:'main',integrationBranch:'staging',status:'provisioning'},
inventory:{status:'active'},provisioning:{state:'pending',requiredFiles:['README.md','objectives/core']}}});
const project=details?.project??details,teamRow=await this.first('SELECT metadata_json FROM teams WHERE id = ? LIMIT 1',[teamId]);
const metadata=parseJson(teamRow?.metadata_json,{});metadata.teamLibrary={projectId:project.id,projectSlug:MANAGED_TEAM_PROJECT_SLUG,state:'provisioning'};
Expand Down
17 changes: 10 additions & 7 deletions src/api/teams/managed-team-library-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ensureProjectKnowledgeBinding } from '../../control-plane/seeds/apply-s
import { reconcileLibraryProvider } from '../../control-plane/seeds/apply-support/projects/projects-core/library-provider-reconciliation.ts';
import { createR2PublicationClient } from '../providers/cloudflare/r2-publication-client.ts';
import { enqueueTreeDxCommitReplication } from '../capacity/services/treedx/repositories/treedx-commit-replication.ts';
import { isManagedTeamLibraryRepositoryName,managedTeamLibraryRepositoryName } from '../store/teams/contracts/managed-library/ensure-managed-team-library-project.ts';

const text=(...values:unknown[])=>values.find((value)=>typeof value==='string'&&value.trim())?.toString().trim()??'';
const record=(value:unknown):Record<string,unknown>=>value&&typeof value==='object'&&!Array.isArray(value)?value as Record<string,unknown>:{};
Expand Down Expand Up @@ -35,8 +36,10 @@ export async function reconcileManagedTeamLibrary(store:any,teamId:string,env:No
for(const candidate of projects)for(const repository of await store.listHubRepositories(String(candidate.id)))if(repository.owner){owner=String(repository.owner);break;}
}
if(!owner)throw new Error('A GitHub library owner must be configured before the managed Team Library can be provisioned.');
const provider=await reconcileLibraryProvider({store,teamId,projectId:String(project.id),projectSlug:'team',owner,name:'team-library',visibility:'private',lifecycle:'create-or-adopt',env,fetchImpl:store.config?.fetchImpl,seedFiles});
const binding=await ensureProjectKnowledgeBinding({store,projectId:String(project.id),teamId,projectSlug:'team',libraryRoot:'.',libraryRef:'refs/remotes/origin/staging',libraryRepositoryUrl:`https://github.com/${owner}/team-library.git`,libraryDefaultBranch:'main',libraryCredentialId:provider.credentialId,expectedUpstreamHeads:provider.heads,env});
const projectLibrary=record(record(project.metadata).library),repositoryName=text(projectLibrary.repositoryName,managedTeamLibraryRepositoryName(teamId));
if(!isManagedTeamLibraryRepositoryName(teamId,repositoryName))throw new Error('Managed Team Library repository identity does not match its owning team.');
const provider=await reconcileLibraryProvider({store,teamId,projectId:String(project.id),projectSlug:'team',owner,name:repositoryName,visibility:'private',lifecycle:'create-or-adopt',env,fetchImpl:store.config?.fetchImpl,seedFiles});
const binding=await ensureProjectKnowledgeBinding({store,projectId:String(project.id),teamId,projectSlug:'team',libraryRoot:'.',libraryRef:'refs/remotes/origin/staging',libraryRepositoryUrl:`https://github.com/${owner}/${repositoryName}.git`,libraryDefaultBranch:'main',libraryCredentialId:provider.credentialId,expectedUpstreamHeads:provider.heads,env});
const now=new Date().toISOString();
await enqueueTreeDxCommitReplication(store,{teamId,projectId:String(project.id),commitSha:binding.resolvedRef,sourceRef:binding.sourceRef,createdAt:now});
const replication=await store.first(`SELECT status,r2_status,r2_receipt_json FROM treedx_commit_replications
Expand All @@ -46,11 +49,11 @@ export async function reconcileManagedTeamLibrary(store:any,teamId:string,env:No
&&r2Receipt.schemaVersion==='treeseed.treedx-r2-file-mirror/v2'&&r2Receipt.commitSha===binding.resolvedRef;
const provisioning=mirrorReady?{state:'known-good',completedAt:now,requiredFiles:['README.md','objectives/core']}
:{state:'replicating',updatedAt:now,requiredFiles:['README.md','objectives/core']};
const projectMetadata={...record(project.metadata),library:{...record(record(project.metadata).library),status:mirrorReady?'known-good':'replicating',owner,repositoryName:'team-library',heads:provider.heads},provisioning};
const projectMetadata={...record(project.metadata),library:{...projectLibrary,status:mirrorReady?'known-good':'replicating',owner,repositoryName,heads:provider.heads},provisioning};
await store.run('UPDATE projects SET metadata_json = ?, updated_at = ? WHERE id = ?',[JSON.stringify(projectMetadata),now,project.id]);
const teamMetadata={...metadata,teamLibrary:{projectId:project.id,projectSlug:'team',state:mirrorReady?'known-good':'replicating',repository:`${owner}/team-library`,repositoryId:binding.repositoryId}};
const teamMetadata={...metadata,teamLibrary:{projectId:project.id,projectSlug:'team',state:mirrorReady?'known-good':'replicating',repository:`${owner}/${repositoryName}`,repositoryName,repositoryId:binding.repositoryId}};
await store.run('UPDATE teams SET metadata_json = ?, updated_at = ? WHERE id = ?',[JSON.stringify(teamMetadata),now,teamId]);
return {teamId,projectId:project.id,repository:`${owner}/team-library`,state:mirrorReady?'known-good':'replicating',...binding};
return {teamId,projectId:project.id,repository:`${owner}/${repositoryName}`,state:mirrorReady?'known-good':'replicating',...binding};
}

export async function markManagedTeamLibraryMirrorKnownGood(store:any,input:{teamId:string;projectId:string;commitSha:string;r2Receipt:unknown}) {
Expand Down Expand Up @@ -91,8 +94,8 @@ export async function deleteManagedTeamLibraryResources(input:{teamId:string;pro
if(input.project.metadata?.kind!=='system-team-library'||input.project.metadata?.systemManaged!==true)
throw new Error('Managed Team Library cleanup requires the protected system project.');
const env=input.env??process.env,fetchImpl=input.fetchImpl??fetch,library=record(input.project.metadata?.library);
const owner=text(library.owner),name=text(library.repositoryName,'team-library'),token=text(env.TREESEED_GITHUB_TOKEN);
if(!owner||name!=='team-library'||!token)throw new Error('Managed Team Library GitHub deletion authority is unavailable.');
const owner=text(library.owner),name=text(library.repositoryName),token=text(env.TREESEED_GITHUB_TOKEN);
if(!owner||!isManagedTeamLibraryRepositoryName(input.teamId,name)||!token)throw new Error('Managed Team Library GitHub deletion authority is unavailable.');
const response=await fetchImpl(`https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`,{
method:'DELETE',headers:{accept:'application/vnd.github+json',authorization:`Bearer ${token}`,'user-agent':'treeseed-team-library-deleter','x-github-api-version':'2022-11-28'},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,35 @@
import { describe, expect, it, vi } from 'vitest';
import { deleteManagedTeamLibraryResources } from '../../../../src/api/teams/managed-team-library-service.ts';
import { isManagedTeamLibraryRepositoryName,managedTeamLibraryRepositoryName } from '../../../../src/api/store/teams/contracts/managed-library/ensure-managed-team-library-project.ts';

describe('managed Team Library deletion', () => {
it('deletes the exact GitHub repository and both team-scoped R2 prefixes', async () => {
const repositoryName=managedTeamLibraryRepositoryName('team-1');
const requests:string[]=[];
const fetchImpl=vi.fn(async (input:string|URL|Request,init?:RequestInit) => {
const url=String(input);requests.push(`${init?.method??'GET'} ${url}`);
if(url.startsWith('https://api.github.com/'))return new Response(null,{status:204});
if((init?.method??'GET')==='GET')return new Response('<ListBucketResult><IsTruncated>false</IsTruncated></ListBucketResult>',{status:200});
return new Response(null,{status:204});
});
const result=await deleteManagedTeamLibraryResources({teamId:'team-1',project:{id:'project-team',metadata:{kind:'system-team-library',systemManaged:true,library:{owner:'treeseed-ai',repositoryName:'team-library'}}},
const result=await deleteManagedTeamLibraryResources({teamId:'team-1',project:{id:'project-team',metadata:{kind:'system-team-library',systemManaged:true,library:{owner:'treeseed-ai',repositoryName}}},
env:{TREESEED_GITHUB_TOKEN:'github-token',TREESEED_CLOUDFLARE_ACCOUNT_ID:'account',TREESEED_CONTENT_BUCKET_NAME:'treeseed-dev-library',TREESEED_R2_ACCESS_KEY_ID:'access',TREESEED_R2_SECRET_ACCESS_KEY:'secret'},fetchImpl:fetchImpl as typeof fetch});
expect(result).toEqual(expect.objectContaining({teamId:'team-1',github:{repository:'treeseed-ai/team-library',deleted:true,alreadyAbsent:false},r2:expect.objectContaining({deletedObjects:0})}));
expect(requests.some((request)=>request.startsWith('DELETE https://api.github.com/repos/treeseed-ai/team-library'))).toBe(true);
expect(result).toEqual(expect.objectContaining({teamId:'team-1',github:{repository:`treeseed-ai/${repositoryName}`,deleted:true,alreadyAbsent:false},r2:expect.objectContaining({deletedObjects:0})}));
expect(requests.some((request)=>request.startsWith(`DELETE https://api.github.com/repos/treeseed-ai/${repositoryName}`))).toBe(true);
expect(requests.filter((request)=>request.startsWith('GET https://account.r2.cloudflarestorage.com/treeseed-dev-library/')).length).toBe(2);
});

it('derives stable collision-free identities and retains only the recorded legacy exception', async () => {
const first=managedTeamLibraryRepositoryName('team-1'),second=managedTeamLibraryRepositoryName('team-2');
expect(first).toMatch(/^team-library-[a-f0-9]{12}$/u);
expect(first).not.toBe(second);
expect(isManagedTeamLibraryRepositoryName('team-1',first)).toBe(true);
expect(isManagedTeamLibraryRepositoryName('team-1','team-library')).toBe(true);
expect(isManagedTeamLibraryRepositoryName('team-1',second)).toBe(false);
await expect(deleteManagedTeamLibraryResources({teamId:'team-1',project:{id:'project-team',metadata:{kind:'system-team-library',systemManaged:true,library:{owner:'treeseed-ai',repositoryName:second}}},
env:{TREESEED_GITHUB_TOKEN:'github-token'}})).rejects.toThrow('GitHub deletion authority is unavailable');
});

it('refuses to delete a normal project', async () => {
await expect(deleteManagedTeamLibraryResources({teamId:'team-1',project:{id:'project-1',metadata:{}},env:{}}))
.rejects.toThrow('protected system project');
Expand Down
Loading