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
10 changes: 9 additions & 1 deletion .mex/patterns/secure-local-project-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ edges:
condition: "when persisting a Hub job or migrating team.db"
- target: "context/architecture.md"
condition: "when wiring a real Graph or Wiki adapter"
last_updated: 2026-09-08
last_updated: 2026-09-10
mex:
id: mx_01M1M0CJQ2BSV71G1C7TXZD9RH
type: pattern
Expand Down Expand Up @@ -95,6 +95,14 @@ preview/apply services.
trace, or origin fields. Hub read models must omit those fields and bound
subject/message previews before response validation. Schema-v2 Activity
workflow/custom origin and optional labels use their closed projections.
- The Overview team-access card POSTs an allowlisted name/email payload (and an
optional follow-up) from the browser to `https://api.web3forms.com`. CSP
`connect-src` names that host only; Hub must not proxy repo, path, graph, or
machine data, and page load must not initiate that request.
- Keep optional Home dialogs and their styles behind an explicit open-on-demand
import; even shared dialog controls can exceed Home's frozen asset budget.
Bound external submissions across both the request and response-body read,
abort on expiry, and release the form for retry or dismissal after failure.

- New read surfaces need successful-job cache invalidation as well as their
initial query. Context's graph, selected record, and compact code queries all
Expand Down
11 changes: 0 additions & 11 deletions package-lock.json

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

13 changes: 13 additions & 0 deletions packages/hub-web/scripts/assert-production-build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,19 @@ if (!homeEntry || workbenchEntries.some((entry) => entry !== homeEntry && entry.
throw new Error("The production Hub Home workbench is not isolated in its own lazy chunk.");
}
const homeChunks = staticImportClosure(homeEntry.key);
const teamAccessDialogKey = Object.keys(manifest).find((candidate) => (
candidate === "src/pages/TeamAccessDialog.tsx"
|| manifest[candidate].src === "src/pages/TeamAccessDialog.tsx"
));
if (
!teamAccessDialogKey
|| !manifest[teamAccessDialogKey].isDynamicEntry
|| !(manifest[homeEntry.key].dynamicImports ?? []).includes(teamAccessDialogKey)
|| homeChunks.has(teamAccessDialogKey)
|| initialChunks.has(teamAccessDialogKey)
) {
throw new Error("The team-access dialog is not isolated behind its explicit open-on-demand boundary.");
}
for (const entry of workbenchEntries) {
if (entry !== homeEntry && homeChunks.has(entry.key)) {
throw new Error(`The production Hub Home workbench eagerly imports ${entry.source}.`);
Expand Down
1 change: 1 addition & 0 deletions packages/hub-web/src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
interface ImportMetaEnv {
readonly DEV: boolean;
readonly PROD: boolean;
readonly VITE_WEB3FORMS_ACCESS_KEY?: string;
}

interface ImportMeta {
Expand Down
259 changes: 259 additions & 0 deletions packages/hub-web/src/lib/team-access-lead.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import {
TEAM_ACCESS_FOLLOW_UP_SUBJECT,
TEAM_ACCESS_SOURCE,
TEAM_ACCESS_STORAGE_KEY,
TEAM_ACCESS_SUBJECT,
TEAM_ACCESS_SUBMIT_ERROR,
WEB3FORMS_SUBMIT_URL,
__setWeb3FormsAccessKeyForTests,
buildTeamAccessContactPayload,
buildTeamAccessFollowUpPayload,
readTeamAccessState,
submitTeamAccessPayload,
validateTeamAccessContact,
writeTeamAccessState,
} from "./team-access-lead";

afterEach(() => {
vi.useRealTimers();
__setWeb3FormsAccessKeyForTests(null);
window.localStorage.removeItem(TEAM_ACCESS_STORAGE_KEY);
});

describe("team-access lead payloads", () => {
it("builds a closed contact payload without repo or machine fields", () => {
__setWeb3FormsAccessKeyForTests("public-test-key");
const payload = buildTeamAccessContactPayload({
name: " Ada Lovelace ",
email: " ada@example.com ",
});

expect(payload).toEqual({
access_key: "public-test-key",
name: "Ada Lovelace",
email: "ada@example.com",
subject: TEAM_ACCESS_SUBJECT,
from_name: "mex Hub",
source: TEAM_ACCESS_SOURCE,
});
expect(Object.keys(payload).sort()).toEqual([
"access_key",
"email",
"from_name",
"name",
"source",
"subject",
]);
});

it("omits empty optional follow-up fields and keeps the same contact identity", () => {
__setWeb3FormsAccessKeyForTests("public-test-key");
const payload = buildTeamAccessFollowUpPayload({
name: "Ada Lovelace",
email: "ada@example.com",
company: " ",
teamSize: "",
foundMex: "",
installReason: "",
repoKind: "",
othersUseAgents: "",
need: "",
missing: "",
});

expect(payload).toEqual({
access_key: "public-test-key",
name: "Ada Lovelace",
email: "ada@example.com",
subject: TEAM_ACCESS_FOLLOW_UP_SUBJECT,
from_name: "mex Hub",
source: TEAM_ACCESS_SOURCE,
});
expect(payload).not.toHaveProperty("company");
expect(payload).not.toHaveProperty("i_want");
expect(payload).not.toHaveProperty("repo");
expect(payload).not.toHaveProperty("path");
});

it("includes only allowlisted optional answers", () => {
__setWeb3FormsAccessKeyForTests("public-test-key");
const payload = buildTeamAccessFollowUpPayload({
name: "Ada Lovelace",
email: "ada@example.com",
company: "Analytical Engines",
teamSize: "2–10",
foundMex: "GitHub",
installReason: "Agent memory",
repoKind: "Work",
othersUseAgents: "Yes",
need: "Shared team memory",
missing: "Shared follow-up",
});

expect(payload).toMatchObject({
company: "Analytical Engines",
team_size: "2–10",
found_mex: "GitHub",
install_reason: "Agent memory",
repo_kind: "Work",
others_use_agents: "Yes",
i_need: "Shared team memory",
whats_missing: "Shared follow-up",
});
expect(Object.keys(payload).sort()).toEqual([
"access_key",
"company",
"email",
"found_mex",
"from_name",
"i_need",
"install_reason",
"name",
"others_use_agents",
"repo_kind",
"source",
"subject",
"team_size",
"whats_missing",
]);
expect(payload).not.toHaveProperty("repo");
expect(payload).not.toHaveProperty("path");
});

it("rejects blank or malformed contact details before submit", () => {
expect(validateTeamAccessContact("", "ada@example.com")).toEqual({ name: "Enter your name." });
expect(validateTeamAccessContact("Ada", "")).toEqual({ email: "Enter your email." });
expect(validateTeamAccessContact("Ada", "not-an-email")).toEqual({ email: "Enter a valid email." });
expect(validateTeamAccessContact("Ada", "ada@example.com")).toEqual({});
});
});

describe("team-access Web3Forms submit", () => {
it("posts JSON to Web3Forms and requires a success response", async () => {
__setWeb3FormsAccessKeyForTests("public-test-key");
const fetchImpl = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true }),
});
const payload = buildTeamAccessContactPayload({ name: "Ada", email: "ada@example.com" });
const result = await submitTeamAccessPayload(payload, fetchImpl as unknown as typeof fetch);

expect(result).toEqual({ ok: true });
expect(fetchImpl).toHaveBeenCalledOnce();
expect(fetchImpl.mock.calls[0]?.[0]).toBe(WEB3FORMS_SUBMIT_URL);
expect(fetchImpl.mock.calls[0]?.[1]).toMatchObject({
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
});
expect(JSON.parse(String(fetchImpl.mock.calls[0]?.[1]?.body))).toEqual(payload);
});

it("stays failed when Web3Forms does not accept the payload", async () => {
const fetchImpl = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: false, message: "invalid" }),
});
const result = await submitTeamAccessPayload(
{ access_key: "public-test-key", name: "Ada", email: "ada@example.com" },
fetchImpl as unknown as typeof fetch,
);
expect(result).toEqual({ ok: false, message: TEAM_ACCESS_SUBMIT_ERROR });
});

it("aborts and settles after fifteen seconds when the request stalls", async () => {
vi.useFakeTimers();
const fetchImpl = vi.fn((_url: RequestInfo | URL, _init?: RequestInit) => new Promise<Response>(() => {}));
const settled = vi.fn();
const result = submitTeamAccessPayload(
{ access_key: "public-test-key", name: "Ada", email: "ada@example.com" },
fetchImpl,
).then(settled);
const signal = fetchImpl.mock.calls[0]?.[1]?.signal;

await vi.advanceTimersByTimeAsync(14_999);
expect(settled).not.toHaveBeenCalled();
expect(signal?.aborted).toBe(false);
await vi.advanceTimersByTimeAsync(1);
await result;

expect(settled).toHaveBeenCalledExactlyOnceWith({ ok: false, message: TEAM_ACCESS_SUBMIT_ERROR });
expect(signal?.aborted).toBe(true);
expect(vi.getTimerCount()).toBe(0);
});

it.each(["rejects", "ignores"] as const)(
"bounds a stalled response body when its reader %s the abort",
async (abortBehavior) => {
vi.useFakeTimers();
const bodyAborted = vi.fn();
const readBody = vi.fn();
const fetchImpl = vi.fn((_url: RequestInfo | URL, init?: RequestInit) => {
readBody.mockImplementation(() => new Promise((_resolve, reject) => {
init?.signal?.addEventListener("abort", () => {
bodyAborted();
if (abortBehavior === "rejects") reject(new DOMException("Aborted", "AbortError"));
}, { once: true });
}));
return Promise.resolve({ ok: true, json: readBody } as unknown as Response);
});
const result = submitTeamAccessPayload(
{ access_key: "public-test-key", name: "Ada", email: "ada@example.com" },
fetchImpl,
);

await vi.advanceTimersByTimeAsync(0);
expect(readBody).toHaveBeenCalledOnce();
await vi.advanceTimersByTimeAsync(15_000);

await expect(result).resolves.toEqual({ ok: false, message: TEAM_ACCESS_SUBMIT_ERROR });
expect(bodyAborted).toHaveBeenCalledOnce();
expect(vi.getTimerCount()).toBe(0);
},
);

it.each(["success", "rejection"] as const)(
"clears the deadline after early %s without later aborting the request",
async (outcome) => {
vi.useFakeTimers();
const fetchImpl = vi.fn((_url: RequestInfo | URL, _init?: RequestInit) => (
outcome === "success"
? Promise.resolve({ ok: true, json: async () => ({ success: true }) } as Response)
: Promise.reject(new Error("Network unavailable"))
));
const result = await submitTeamAccessPayload(
{ access_key: "public-test-key", name: "Ada", email: "ada@example.com" },
fetchImpl,
);
const signal = fetchImpl.mock.calls[0]?.[1]?.signal;

expect(result).toEqual(outcome === "success"
? { ok: true }
: { ok: false, message: TEAM_ACCESS_SUBMIT_ERROR });
expect(vi.getTimerCount()).toBe(0);
await vi.advanceTimersByTimeAsync(15_000);
expect(signal?.aborted).toBe(false);
},
);

it("does not call the network when the public access key is missing", async () => {
const fetchImpl = vi.fn();
const result = await submitTeamAccessPayload(
{ access_key: "", name: "Ada", email: "ada@example.com" },
fetchImpl as unknown as typeof fetch,
);
expect(result).toEqual({ ok: false, message: TEAM_ACCESS_SUBMIT_ERROR });
expect(fetchImpl).not.toHaveBeenCalled();
});
});

describe("team-access local state", () => {
it("remembers that this Hub checkout already sent contact details", () => {
expect(readTeamAccessState()).toBeNull();
writeTeamAccessState({ contactSent: true });
expect(readTeamAccessState()).toEqual({ contactSent: true });
});
});
Loading
Loading