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
22 changes: 15 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
release-shards:
name: Release / ${{ matrix.shard }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: read
timeout-minutes: 30
strategy:
fail-fast: false
Expand Down Expand Up @@ -108,13 +111,6 @@ jobs:
.artifacts/release-cache/${{ matrix.shard }}/playwright-browsers
LOCAL_DB_HOST: 127.0.0.1
LOCAL_DB_PASSWORD: postgres
# `supabase start` pulls nine service images, and the CLI's default
# registry is public.ecr.aws, whose anonymous pull rate is shared by
# every runner behind the same egress address — it answers
# "toomanyrequests: Rate exceeded" and the database shards never start.
# The CLI mirrors the identical tags on ghcr.io, which needs no registry
# credential and so no repository secret.
SUPABASE_INTERNAL_IMAGE_REGISTRY: ghcr.io
steps:
- name: Start release budget clock
run: echo "CLOCKWORK_RELEASE_STARTED_AT=$(date +%s000)" >> "$GITHUB_ENV"
Expand Down Expand Up @@ -191,6 +187,18 @@ jobs:
fi
echo "CLOCKWORK_UPGRADE_REPLAY=$replay" >> "$GITHUB_ENV"
echo "Populated upgrade replay: $replay"
# The pinned CLI falls back from ECR to GHCR to Docker Hub when an image
# pull fails. Do not force SUPABASE_INTERNAL_IMAGE_REGISTRY: doing so
# disables that fallback, and even authenticated GHCR can be throttled.
# Give its GHCR fallback the job's read-only token, without a long-lived
# repository credential.
- name: Authenticate database image pulls
if: matrix.shard == 'integration' || matrix.shard == 'proof'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Start freshly migrated and seeded database
if: matrix.shard == 'integration' || matrix.shard == 'proof'
run: |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AccountDocumentsSection } from "@/src/features/internal-ops/account-documents/section";
import type { Metadata } from "next";
import Link from "next/link";

Expand Down Expand Up @@ -62,7 +63,16 @@ async function Page({ params }: { params: Promise<{ id: string }> }) {
)}
</SurfaceActionGate>
}
supporting={<AccountDerivationSection recordKey={id} />}
supporting={
<>
{record?.aggregateId ? (
<AccountDocumentsSection
target={{ type: "account", id: record.aggregateId }}
/>
) : null}
<AccountDerivationSection recordKey={id} />
</>
}
/>
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AccountDocumentsSection } from "@/src/features/internal-ops/account-documents/section";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { cache } from "react";
Expand Down Expand Up @@ -60,6 +61,9 @@ async function Page({
organization={organization}
created={query.created === "1"}
>
<AccountDocumentsSection
target={{ type: "account", id: organization.accountId }}
/>
<OrganizationInvites
organizationId={organization.organizationId}
side={organization.side}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { AccountDocumentsSection } from "@/src/features/internal-ops/account-documents/section";
import type { Metadata } from "next";
import { cache } from "react";

Expand Down Expand Up @@ -45,6 +46,11 @@ async function Page({ params }: { params: Promise<{ id: string }> }) {
<PartnerDetail
t={t}
locale={locale}
documents={
<AccountDocumentsSection
target={{ type: "partner", id: loaded.partner.id }}
/>
}
partner={loaded.partner}
deals={loaded.deals}
activity={loaded.activity}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import "server-only";
import { z } from "zod";
import { AccountDocumentTargetSchema } from "@clockwork/contracts";
import { contractActor } from "@/src/features/internal-ops/contracts/server";
import {
jsonFailure,
pdfResponse,
withDocumentSlot,
} from "@/src/features/internal-ops/contracts/http";
import {
accountDocumentRepository,
accountDocumentStaff,
} from "@/src/features/internal-ops/account-documents/server";

type Params = { type: string; id: string; fileId: string };
export async function GET(
request: Request,
context: { params: Promise<Params> },
) {
try {
const { type, id, fileId } = await context.params;
const target = AccountDocumentTargetSchema.parse({ type, id });
const session = await accountDocumentStaff(target);
return await withDocumentSlot(async () => {
const { file, bytes } = await accountDocumentRepository().read(
target,
z.uuid().parse(fileId),
contractActor(session),
);
return pdfResponse(
bytes,
file.fileName,
new URL(request.url).searchParams.get("view") === "1"
? "inline"
: "attachment",
);
});
} catch (error) {
return jsonFailure(error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import "server-only";
import {
AccountDocumentInputSchema,
AccountDocumentTargetSchema,
} from "@clockwork/contracts";
import { contractActor } from "@/src/features/internal-ops/contracts/server";
import {
fromApplicationOrigin,
itemField,
jsonFailure,
readUploadForm,
uploadedPdf,
withDocumentSlot,
} from "@/src/features/internal-ops/contracts/http";
import {
accountDocumentRepository,
accountDocumentStaff,
} from "@/src/features/internal-ops/account-documents/server";

type Params = { type: string; id: string };
export async function POST(
request: Request,
context: { params: Promise<Params> },
) {
if (!fromApplicationOrigin(request))
return new Response(null, { status: 403 });
try {
const { type, id } = await context.params;
const target = AccountDocumentTargetSchema.parse({ type, id });
const session = await accountDocumentStaff(target, true);
const file = await withDocumentSlot(async () => {
const form = await readUploadForm(request);
const input = AccountDocumentInputSchema.parse(itemField(form));
return accountDocumentRepository().add(
target,
input,
await uploadedPdf(form),
contractActor(session),
);
});
return Response.json(
{ ok: true, value: file },
{ headers: { "cache-control": "private, no-store" } },
);
} catch (error) {
return jsonFailure(error);
}
}
80 changes: 80 additions & 0 deletions apps/web/e2e/production-proof.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1330,3 +1330,83 @@ test("@internal creates an access token, calls MCP with it and revokes it", asyn
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
});

test("@internal attaches external documents to partners and accounts", async ({
page,
}) => {
const name = `Attachment proof ${Date.now()}`;
const pdf = Buffer.from(`%PDF-1.7\nExternal MNDA ${name}\n%%EOF`);
await page.goto("/internal/partners/new");
await page.getByLabel(/^Partner name/u).fill(name);
await page.getByRole("button", { name: "Save partner" }).click();
await expect(page).toHaveURL(/\/internal\/partners\/[0-9a-f-]{36}$/u, {
timeout: 15_000,
});
const partnerUrl = page.url();
async function attach(title: string) {
await expect(page.getByLabel("Choose PDF")).toBeEnabled();
await page.getByLabel("Choose PDF").setInputFiles({
name: "External MNDA.pdf",
mimeType: "application/pdf",
buffer: pdf,
});
await page.getByLabel("Document title", { exact: true }).fill(title);
await page.getByLabel("Signed on (optional)").fill("2026-10-10");
await page
.getByRole("button", { name: "Attach document", exact: true })
.click();
await expect(page.getByText(`“${title}” is attached.`)).toBeVisible();
await page.reload();
const link = page.getByRole("link", {
name: `Download ${title}`,
exact: true,
});
await expect(link).toBeVisible();
const href = await link.getAttribute("href");
if (!href) throw new Error("The saved attachment has no download link");
const response = await page.request.get(href);
expect(response.status()).toBe(200);
expect(await response.body()).toEqual(pdf);
return href;
}
const partnerFile = await attach("Signed external partner MNDA");
const documents = page.getByRole("region", {
name: "Documents",
exact: true,
});
await expect(documents.getByText("Uploaded externally")).toBeVisible();
const accessibility = await new AxeBuilder({ page }).analyze();
expect(accessibility.violations).toEqual([]);
await documents.screenshot({
path: test.info().outputPath("partner-documents-desktop.png"),
});
await page.setViewportSize({ width: 390, height: 844 });
await documents.screenshot({
path: test.info().outputPath("partner-documents-mobile.png"),
});
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= window.innerWidth,
),
).toBe(true);
await page.setViewportSize({ width: 1280, height: 720 });

// The organization and account views share the account, not the partner ID.
await page.goto(
"/internal/organizations/30000000-0000-4000-8000-000000000001",
);
const accountFile = await attach("Signed external customer MNDA");
expect(accountFile).toContain(`/account/${customerAccount}/`);
const mismatched = partnerFile.replace(
/\/partner\/[^/]+\//u,
`/account/${customerAccount}/`,
);
expect((await page.request.get(mismatched)).status()).toBe(404);
await page.goto(partnerUrl);
await expect(
page.getByRole("link", { name: "Download Signed external partner MNDA" }),
).toBeVisible();
await expect(
page.getByRole("link", { name: "Download Signed external customer MNDA" }),
).toHaveCount(0);
});
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@clockwork/testing": "workspace:*",
"@clockwork/ui": "workspace:*",
"@clockwork/workflows": "workspace:*",
"@modelcontextprotocol/sdk": "1.30.1",
"@modelcontextprotocol/sdk": "1.32.0",
"@netlify/blobs": "10.7.11",
"@swc/helpers": "0.5.15",
"@workos-inc/authkit-nextjs": "catalog:",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
.form {
display: grid;
gap: var(--cw-space-3);
margin-top: var(--cw-space-4);
}
.fields {
display: grid;
grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
gap: var(--cw-space-3);
}
.fileRow {
display: flex;
flex-wrap: wrap;
align-items: end;
gap: var(--cw-space-3);
}
.fileRow > label {
flex: 1;
min-width: 0;
}
.filename {
overflow-wrap: anywhere;
}
.metadata {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--cw-space-2);
}
@media (max-width: 48rem) {
.fileRow {
display: grid;
grid-template-columns: minmax(0, 1fr);
}
.fileRow > button {
justify-self: start;
}
.fields {
grid-template-columns: minmax(0, 1fr);
}
}
Loading
Loading