Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e2df3f5
feat(daemon): JS-exact JSON for the content protocol
tlgimenes Oct 8, 2026
2482783
feat(daemon): block keys/file names and protocol errors
tlgimenes Oct 8, 2026
4fd78e4
feat(daemon): filesystem content store over the working tree
tlgimenes Oct 8, 2026
f0ba46d
refactor(api): extract gen-artifact + commit message from commit-coal…
tlgimenes Oct 8, 2026
77d7a4e
chore(api): depend on @decocms/blocks; run Studio CLI by path
tlgimenes Oct 8, 2026
98ee981
feat(daemon): secret guard and ciphertext checks
tlgimenes Oct 8, 2026
3911ab5
feat(daemon): content-protocol RPC (describe, schema.get, blocks.list…
tlgimenes Oct 8, 2026
6f8c7ec
feat(daemon): asset uploads + handler tests
tlgimenes Oct 8, 2026
b188110
feat(daemon): mount /_sandbox/rpc and /assets; bounded tree-lock wait
tlgimenes Oct 8, 2026
f9c02ed
feat(hosted): delivery bucket settings, store and CDN purge
tlgimenes Oct 8, 2026
0010dd7
feat(hosted): CDN draft store and draft-over-main content storage
tlgimenes Oct 8, 2026
558a2a8
test(daemon-e2e): content protocol conformance against the daemon
tlgimenes Oct 8, 2026
b8ab85b
feat(hosted): release objects and publishDraft
tlgimenes Oct 8, 2026
0d56c53
feat(hosted): site_editor_content_protocol flag; releases timeline, s…
tlgimenes Oct 8, 2026
23cba81
feat(hosted): site tokens
tlgimenes Oct 8, 2026
7778db9
feat(hosted): claim a project's site slug for its org on create
tlgimenes Oct 8, 2026
e4ce82f
feat(hosted): admin backfill of site claims
tlgimenes Oct 8, 2026
84d9332
feat(api): /rpc, v8 draft pointer and publish; hosted routes
tlgimenes Oct 8, 2026
2a13ed6
feat(api): proxy /_sandbox/rpc and report draft publish status
tlgimenes Oct 8, 2026
57c1373
Merge the API track (split/11-sandbox-rpc-proxy) into the e2e stack
tlgimenes Oct 8, 2026
4bd82b4
Merge the Go daemon track (split/19-daemon-e2e-conformance) into the …
tlgimenes Oct 8, 2026
62c6449
test(e2e): delivery CDN stub server
tlgimenes Oct 8, 2026
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
6 changes: 5 additions & 1 deletion apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ ENV DATABASE_URL=file:/app/data/mesh.db

# The CLI handles migrations automatically on startup
# Use --skip-migrations if you want to manage migrations separately
CMD ["bun", "run", "deco", "--no-tui", "--no-local-mode"]
# Runs Studio's CLI by path, not through the `deco` bin name: another installed
# package may declare a `deco` bin too (@decocms/blocks does), and whichever
# wins `node_modules/.bin/deco` would start instead of Studio
# (src/cli/deco-bin.test.ts).
CMD ["bun", "run", "node_modules/decocms/dist/server/cli.js", "--no-tui", "--no-local-mode"]
1 change: 1 addition & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@better-auth/sso": "1.4.1",
"@decocms/better-auth": "1.5.17",
"@decocms/bindings": "workspace:*",
"@decocms/blocks": "8.1.0-next.7",
"@decocms/mcp-utils": "workspace:*",
"@decocms/runtime": "workspace:*",
"@decocms/sandbox": "workspace:*",
Expand Down
20 changes: 19 additions & 1 deletion apps/api/scripts/smoke-tarball.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
*/

import { $ } from "bun";
import { mkdtemp, writeFile } from "fs/promises";
import { mkdtemp, realpath, writeFile } from "fs/promises";
import { join } from "path";
import { tmpdir } from "os";

Expand Down Expand Up @@ -71,7 +71,25 @@ if (!(await Bun.file(clientIndex).exists())) {
// eagerly during load, so a missing external crashes here before
// --version prints — same symptom a real consumer would hit.
const cliBin = join(scratch, "node_modules", ".bin", "deco");
const studioCli = join(
scratch,
"node_modules",
"decocms",
"dist",
"server",
"cli.js",
);
// Another installed package with a `deco` bin (@decocms/blocks has one) can
// win the link and start instead of Studio: the bin must be Studio's CLI.
if ((await realpath(cliBin)) !== (await realpath(studioCli))) {
console.error(
`node_modules/.bin/deco resolves to ${await realpath(cliBin)}, not Studio's ${studioCli}`,
);
process.exit(1);
}
await $`${cliBin} --version`.cwd(scratch);
// What the Docker image runs (apps/api/Dockerfile CMD): the CLI by path.
await $`bun run node_modules/decocms/dist/server/cli.js --version`.cwd(scratch);

console.log(
"✅ Smoke test passed — browser assets are present and every external resolves at startup.",
Expand Down
119 changes: 119 additions & 0 deletions apps/api/src/api/routes/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ import {
OrgSiteStorage,
} from "@/storage/org-sites";
import { VirtualMCPStorage } from "@/storage/virtual";
import {
backfillSiteClaims,
decoSiteExists,
type SiteClaimCandidate,
} from "@/hosted/claim-site";
import { mainIsV8, projectSite } from "@/hosted/scope";
import { contentClientForProjectRepo } from "@/git-providers";
import { parseRepositoryBinding } from "@/tools/sandbox/sync-git-credentials";
import { OrgNoticeInputSchema } from "@decocms/shared/organization/notice";
import { isOrgArchived } from "@decocms/shared/organization/org-archived";
import { invalidateOrgNoticeCache } from "@/core/org-notice-gate";
Expand Down Expand Up @@ -947,6 +955,117 @@ export function createAdminRoutes(): Hono<Env> {
return c.json({ ok: true });
});

/**
* Links every existing Blocks v8 project (main's `.deco/schema.gen.json`
* says `blocksMajor: 8`) to the site it names (`metadata.siteSlug`) in
* `org_sites`, as project creation now does, claiming a free slug for its
* org first. Safe to re-run; never takes a slug another org, project or
* deco.cx has, nor a deleted org's (reported as `refused`), and links
* nobody to a slug several orgs' or projects name (`ambiguous`). A dry run
* unless the body says `{"dryRun": false}`. A route rather than a
* migration: telling a v8 project needs a GitHub read.
*/
app.post("/hosted/site-claims/backfill", async (c) => {
const raw = (await c.req.json().catch(() => ({}))) as {
dryRun?: unknown;
} | null;
const dryRun = raw?.dryRun !== false;
const { actorId: effectiveActorId, impersonatedBy } =
await getAuditActor(c);
const actorId = impersonatedBy ?? effectiveActorId;
if (!actorId) {
return c.json({ error: "Unauthorized" }, 401);
}
const db = getDb().db;
const ctx = c.var.studioContext;
const rows = await db
.selectFrom("connections")
.innerJoin(
"organization",
"organization.id",
"connections.organization_id",
)
.select([
"connections.id as id",
"connections.organization_id as organizationId",
"connections.metadata as metadata",
"organization.slug as orgSlug",
"organization.name as orgName",
])
.where("connections.connection_type", "=", "VIRTUAL")
.orderBy("connections.created_at", "asc")
.execute();
const candidates: SiteClaimCandidate[] = [];
const orgs = new Map<string, { id: string; slug: string; name: string }>();
for (const row of rows) {
let metadata: Record<string, unknown> | null = null;
try {
metadata =
typeof row.metadata === "string"
? (JSON.parse(row.metadata) as Record<string, unknown>)
: (row.metadata as Record<string, unknown> | null);
} catch {
continue;
}
const slug = projectSite(metadata);
if (!slug) continue;
candidates.push({
organizationId: row.organizationId,
projectId: row.id,
slug,
});
orgs.set(row.organizationId, {
id: row.organizationId,
slug: row.orgSlug,
name: row.orgName,
});
}
const projects = new VirtualMCPStorage(db);
const report = await backfillSiteClaims({
orgSites: new OrgSiteStorage(db),
isDecoSite: decoSiteExists,
candidates,
by: actorId,
dryRun,
isV8: async ({ organizationId, projectId }) => {
const [project] = await projects.listByIds(organizationId, [projectId]);
const metadata = (project?.metadata ?? null) as Record<
string,
unknown
> | null;
const repository = parseRepositoryBinding(
metadata,
project?.connections?.map((conn) => conn.connection_id) ?? [],
);
if (!repository) return false;
// Credentials resolve against the project's org, as in admin-prompts.
const client = await contentClientForProjectRepo(
{ ...ctx, organization: orgs.get(organizationId)! },
organizationId,
repository,
);
const runtime = metadata?.runtime as
| { path?: string | null }
| undefined;
return mainIsV8(
client,
runtime?.path?.replace(/^\/+|\/+$/g, "") || null,
await client.getDefaultBranch(),
);
},
});
auditAdminAction("hosted_site_claims_backfill", {
actor_user_id: actorId,
...(impersonatedBy ? { impersonated_user_id: effectiveActorId } : {}),
dry_run: dryRun,
linked: report.linked.length,
refused: report.refused.length,
ambiguous: report.ambiguous.length,
errors: report.errors.length,
});
return c.json(report);
});

// The agent-prompt editor (reads/writes decocms/studio over GitHub) — its own
// module, mounted here so it inherits this router's admin fence.
app.route("/", createAdminPromptRoutes());
Expand Down
Loading
Loading