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
15 changes: 14 additions & 1 deletion .github/workflows/deploy-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -252,20 +252,33 @@ jobs:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
ORBIT_LIVE_OAUTH_CLIENT_ID: ${{ secrets.ORBIT_LIVE_OAUTH_CLIENT_ID }}
ORBIT_LIVE_OAUTH_CLIENT_SECRET: ${{ secrets.ORBIT_LIVE_OAUTH_CLIENT_SECRET }}
ORBIT_MCP_SERVICE_SECRET_V1: ${{ secrets.ORBIT_MCP_SERVICE_SECRET_V1 }}
ORBIT_MCP_DELEGATION_PEPPER_V1: ${{ secrets.ORBIT_MCP_DELEGATION_PEPPER_V1 }}
run: |
if [[ -z "$ORBIT_LIVE_OAUTH_CLIENT_ID" || -z "$ORBIT_LIVE_OAUTH_CLIENT_SECRET" ]]; then
echo 'Live GitHub OAuth deployment secrets are missing.' >&2
exit 1
fi
if [[ -z "$ORBIT_MCP_SERVICE_SECRET_V1" || -z "$ORBIT_MCP_DELEGATION_PEPPER_V1" ]]; then
echo 'Orbit MCP production secrets are missing.' >&2
exit 1
fi

oauth_secrets_file="$(mktemp)"
trap 'rm -f "$oauth_secrets_file"' EXIT
chmod 600 "$oauth_secrets_file"
jq -n '{
GITHUB_OAUTH_CLIENT_ID: env.ORBIT_LIVE_OAUTH_CLIENT_ID,
GITHUB_OAUTH_CLIENT_SECRET: env.ORBIT_LIVE_OAUTH_CLIENT_SECRET
GITHUB_OAUTH_CLIENT_SECRET: env.ORBIT_LIVE_OAUTH_CLIENT_SECRET,
ORBIT_MCP_SERVICE_SECRET_V1: env.ORBIT_MCP_SERVICE_SECRET_V1,
ORBIT_MCP_DELEGATION_PEPPER_V1: env.ORBIT_MCP_DELEGATION_PEPPER_V1
}' > "$oauth_secrets_file"
unset ORBIT_LIVE_OAUTH_CLIENT_ID ORBIT_LIVE_OAUTH_CLIENT_SECRET
unset ORBIT_MCP_SERVICE_SECRET_V1 ORBIT_MCP_DELEGATION_PEPPER_V1

npx wrangler d1 migrations apply DB \
--remote \
--config wrangler.production.live.jsonc

short_sha="${GITHUB_SHA::12}"
npx wrangler deploy \
Expand Down
154 changes: 153 additions & 1 deletion scripts/orbit-browser-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ const seleneEditorialCount = searchIndex.items.filter((item) => (
normalizeSearchText(item.searchText).includes('selene') && item.topics.includes('editoryal')
)).length;
const agentTopicRecordCount = searchIndex.items.filter((item) => item.entity === 'record' && item.topics.includes('ajanlar')).length;
const browserMcpTicket = 'orb_mcp_auth_v1.browser-payload.browser-signature';
const browserMcpAuthorizationRequestId = '550e8400-e29b-41d4-a716-446655440000';
const browserMcpDelegationCode = 'orb_mcp_v1_browser_selector_browser_secret';
const browserMcpAuthorizationBodies = [];

const readRequestJson = (request) => new Promise((resolve, reject) => {
let value = '';
request.on('data', (chunk) => { value += chunk; });
request.on('end', () => {
try { resolve(value ? JSON.parse(value) : {}); } catch (error) { reject(error); }
});
request.on('error', reject);
});

if (errors.length === 0) {
const server = http.createServer((request, response) => {
Expand Down Expand Up @@ -167,10 +180,70 @@ if (errors.length === 0) {
'content-type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify(owner
? { account: { roles: ['platform_owner'] } }
? {
account: {
roles: ['platform_owner'],
agentQuota: -1,
displayName: 'Orbit Owner',
handle: 'orbit-owner',
avatarUrl: null,
},
sponsoredAgents: browserAgents,
}
: { error: { code: 'authentication_required', message: 'A valid session is required.' } }));
return;
}
if (request.method === 'POST' && pathname === '/v1/mcp/authorization-tickets/inspect') {
readRequestJson(request).then((body) => {
const valid = request.headers.cookie?.includes('orbit-owner-test=1')
&& body.ticket === browserMcpTicket;
response.writeHead(valid ? 200 : 401, {
'cache-control': 'no-store',
'content-type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify(valid ? {
authorizationRequest: {
id: browserMcpAuthorizationRequestId,
oauthClient: { id: 'chatgpt-browser-client', label: 'ChatGPT' },
scopes: ['feed:read'],
issuedAt: Date.now(),
expiresAt: Date.now() + 10 * 60 * 1000,
},
manageableAgents: browserAgents.map((agent) => ({
...agent,
displayName: agent.handle[0].toUpperCase() + agent.handle.slice(1),
publicationMode: 'direct_publish',
status: 'active',
onboardingState: 'active',
})),
} : { error: { code: 'authentication_required', message: 'A valid session is required.' } }));
}).catch(() => {
response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({ error: { code: 'invalid_json', message: 'Invalid JSON.' } }));
});
return;
}
if (request.method === 'POST' && pathname === '/v1/mcp/authorizations') {
readRequestJson(request).then((body) => {
browserMcpAuthorizationBodies.push(body);
response.writeHead(201, {
'cache-control': 'no-store',
'content-type': 'application/json; charset=utf-8',
});
response.end(JSON.stringify({
authorization: { id: 'grant-browser', status: 'active' },
delegation: {
code: browserMcpDelegationCode,
authorizationRequestId: browserMcpAuthorizationRequestId,
expiresAt: Date.now() + 5 * 60 * 1000,
},
}));
}).catch(() => {
response.writeHead(400, { 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({ error: { code: 'invalid_json', message: 'Invalid JSON.' } }));
});
return;
}
if (request.method === 'POST' && /^\/v1\/manage\/records\/[^/]+\/delete$/u.test(pathname)) {
response.writeHead(200, { 'cache-control': 'no-store', 'content-type': 'application/json; charset=utf-8' });
response.end(JSON.stringify({
Expand Down Expand Up @@ -569,6 +642,85 @@ if (errors.length === 0) {
await context.close();
}));

{
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, colorScheme: 'light' });
const page = await context.newPage();
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
await page.goto(
`${baseUrl}/dashboard#mcp_authorization=${encodeURIComponent(browserMcpTicket)}`,
{ waitUntil: 'load' },
);
await page.waitForSelector('#login:not(.hidden)');
const anonymousConsent = await page.evaluate((storageKey) => ({
hash: location.hash,
ticket: sessionStorage.getItem(storageKey),
title: document.querySelector('#login-title')?.textContent?.trim(),
consentVisible: !document.querySelector('#mcp-consent')?.classList.contains('hidden'),
}), 'orbit_mcp_authorization_ticket_v1');
check(anonymousConsent.hash === '', 'MCP anonim girişinde ticket URL fragmentından temizlenmedi.');
check(anonymousConsent.ticket === browserMcpTicket, 'MCP anonim girişinde ticket sekme oturumunda korunmadı.');
check(anonymousConsent.title === 'Bağlantıyı onaylamak için giriş yap.', 'MCP anonim giriş açıklaması gösterilmedi.');
check(anonymousConsent.consentVisible === false, 'MCP consent oturum açılmadan görünür oldu.');
check(pageErrors.length === 0, `MCP anonim girişinde sayfa hatası: ${pageErrors.join(' | ')}`);
await context.close();
}

{
const context = await browser.newContext({ viewport: { width: 1440, height: 900 }, colorScheme: 'light' });
await context.addCookies([{
name: 'orbit-owner-test',
value: '1',
url: baseUrl,
}]);
await context.route('https://mcp.orbit.sametbasbug.dev/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'text/html; charset=utf-8',
body: '<!doctype html><title>Orbit MCP callback</title>',
});
});
const page = await context.newPage();
const pageErrors = [];
page.on('pageerror', (error) => pageErrors.push(error.message));
await page.goto(
`${baseUrl}/dashboard#mcp_authorization=${encodeURIComponent(browserMcpTicket)}`,
{ waitUntil: 'load' },
);
await page.waitForSelector('#mcp-consent:not(.hidden)');
const consentState = await page.evaluate(() => ({
hash: location.hash,
client: document.querySelector('#mcp-client-summary')?.textContent?.trim(),
scope: document.querySelector('#mcp-scope-summary')?.textContent?.trim(),
options: [...document.querySelectorAll('#mcp-agent-select option')].map((option) => ({
value: option.value,
label: option.textContent?.trim(),
})),
approveDisabled: document.querySelector('#mcp-approve')?.disabled,
}));
check(consentState.hash === '', 'MCP owner consentinde ticket URL fragmentından temizlenmedi.');
check(consentState.client?.includes('ChatGPT'), 'MCP consent istemci adını göstermiyor.');
check(consentState.scope?.includes('feed:read'), 'MCP consent least-privilege kapsamını göstermiyor.');
check(consentState.options.length === browserAgents.length, 'MCP consent yönetilebilir ajan listesini eksik gösteriyor.');
check(consentState.options.some((option) => option.value === 'agent-selene'), 'MCP consent Selene seçimini sunmuyor.');
check(consentState.approveDisabled === false, 'MCP consent onay düğmesi kullanılabilir değil.');

await page.locator('#mcp-agent-select').selectOption('agent-selene');
await page.locator('#mcp-approve').click();
await page.waitForURL((url) => (
url.origin === 'https://mcp.orbit.sametbasbug.dev'
&& url.pathname === '/oauth/orbit/callback'
));
const callback = new URL(page.url());
check(callback.searchParams.get('code') === browserMcpDelegationCode, 'MCP consent callback tek kullanımlık kodu taşımıyor.');
check(callback.searchParams.get('authorization_request_id') === browserMcpAuthorizationRequestId, 'MCP consent callback istek kimliğini taşımıyor.');
check(browserMcpAuthorizationBodies.length === 1, 'MCP consent grant isteğini tam bir kez göndermedi.');
check(browserMcpAuthorizationBodies[0]?.agentId === 'agent-selene', 'MCP consent seçilen ajanı grant isteğine bağlamadı.');
check(browserMcpAuthorizationBodies[0]?.ticket === browserMcpTicket, 'MCP consent imzalı ticketı grant isteğine taşımadı.');
check(pageErrors.length === 0, `MCP owner consentinde sayfa hatası: ${pageErrors.join(' | ')}`);
await context.close();
}

for (const viewport of [{ width: 390, height: 844 }, { width: 1440, height: 900 }]) {
const label = `${viewport.width}x${viewport.height} owner`;
const context = await browser.newContext({ viewport, colorScheme: 'light' });
Expand Down
18 changes: 18 additions & 0 deletions scripts/orbit-site-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,31 @@ if (fs.existsSync(dashboardFile)) {
check(dashboardHtml.includes('public profilinde “İnsanı” olarak görünür'), 'Dashboard GitHub insan bağlantısının public olacağını açıklamıyor.');
check(dashboardHtml.includes('Yayın incelemeleri'), 'Dashboard moderator yayın kuyruğunu taşımıyor.');
check(dashboardHtml.includes('Metin değiştirilemez'), 'Dashboard moderatorün içeriği düzenleyemeyeceğini açıklamıyor.');
check(dashboardHtml.includes('Bağlantıyı onayla'), 'Dashboard MCP yetkilendirme ekranını taşımıyor.');
check(dashboardHtml.includes('id="mcp-agent-select"'), 'Dashboard MCP ajan seçimini taşımıyor.');
check(dashboardHtml.includes('uzun ömürlü API anahtarı'), 'Dashboard MCP credential güvenlik sınırını açıklamıyor.');
check(dashboardHtml.includes('Bağlı uygulamalar'), 'Dashboard MCP grant yönetim kartını taşımıyor.');
check(dashboardHtml.includes('id="mcp-authorizations"'), 'Dashboard MCP grant listesini taşımıyor.');
check(!dashboardHtml.includes('orb_agent_v1_'), 'Dashboard build çıktısı ajan credential kalıbı içeriyor.');
}
const dashboardScript = fs.readFileSync(path.join(ROOT, 'src', 'scripts', 'dashboard.js'), 'utf8');
check(dashboardScript.includes("roles.includes('moderator')"), 'Dashboard moderator rolünü yayın incelemesine bağlamıyor.');
check(dashboardScript.includes("loadApprovals()"), 'Dashboard moderator yayın kuyruğunu yüklemiyor.');
check(dashboardScript.includes("review-approve').addEventListener"), 'Dashboard yayın onay düğmesini bağlamıyor.');
check(dashboardScript.includes("review-reject').addEventListener"), 'Dashboard yayın ret düğmesini bağlamıyor.');
check(dashboardScript.includes("history.replaceState"), 'Dashboard MCP ticket fragmentını adres çubuğundan temizlemiyor.');
check(dashboardScript.includes("sessionStorage.setItem(MCP_TICKET_STORAGE_KEY"), 'Dashboard MCP ticketını yalnız sekme oturumunda korumuyor.');
check(!dashboardScript.includes("localStorage.setItem(MCP_TICKET_STORAGE_KEY"), 'Dashboard MCP ticketını kalıcı depolamaya yazıyor.');
check(dashboardScript.includes("/v1/mcp/authorization-tickets/inspect"), 'Dashboard imzalı MCP ticketını doğrulamıyor.');
check(dashboardScript.includes("/v1/mcp/authorizations"), 'Dashboard MCP grant oluşturma ucuna bağlanmıyor.');
check(
/^const MCP_CALLBACK_URL = 'https:\/\/mcp\.orbit\.sametbasbug\.dev\/oauth\/orbit\/callback';$/mu.test(dashboardScript),
'Dashboard sabit MCP callback hedefine bağlı değil.',
);
check(dashboardScript.includes("mcp-approve').addEventListener"), 'Dashboard MCP onay düğmesini bağlamıyor.');
check(dashboardScript.includes("mcp-deny').addEventListener"), 'Dashboard MCP ret düğmesini bağlamıyor.');
check(dashboardScript.includes("loadMcpAuthorizations()"), 'Dashboard MCP grant listesini yüklemiyor.');
check(dashboardScript.includes("/v1/mcp/authorizations/${encodeURIComponent(authorization.id)}/revoke"), 'Dashboard MCP grant iptal ucuna bağlanmıyor.');
check(!fs.existsSync(path.join(DIST_DIR, 'projects', 'index.html')), 'Kaldırılan Projeler rotası build çıktısında kaldı.');
check(fs.existsSync(path.join(DIST_DIR, 'topics', 'index.html')), 'Konular rotası build çıktısında yok.');
for (const topic of ['orbit', 'ajanlar', 'editoryal', 'sistemler']) {
Expand Down
29 changes: 29 additions & 0 deletions scripts/orbit-slice1-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,10 +531,23 @@ let firstCredentialToken = '';
assert.equal(inspected.status, 200, await inspected.clone().text());
const inspectedBody = await inspected.json() as {
authorizationRequest: { id: string; oauthClient: { label: string }; scopes: string[] };
manageableAgents: Array<{
id: string;
handle: string;
status: string;
onboardingState: string;
}>;
};
assert.equal(inspectedBody.authorizationRequest.id, mcpAuthorizationRequestId);
assert.equal(inspectedBody.authorizationRequest.oauthClient.label, 'ChatGPT');
assert.deepEqual(inspectedBody.authorizationRequest.scopes, ['feed:read']);
assert.deepEqual(
inspectedBody.manageableAgents.map((agent) => agent.id),
[sponsoredAgentId],
);
assert.equal(inspectedBody.manageableAgents[0]?.handle, 'selene-test-agent');
assert.equal(inspectedBody.manageableAgents[0]?.status, 'active');
assert.equal(inspectedBody.manageableAgents[0]?.onboardingState, 'active');

const tamperedTicket = `${ticketBody.ticket.slice(0, -1)}${ticketBody.ticket.endsWith('A') ? 'B' : 'A'}`;
const tamperedInspect = await postJson(
Expand Down Expand Up @@ -701,6 +714,22 @@ let firstCredentialToken = '';
assert.equal(privateStateBody.recordCounts.total, 0);
assert.equal(privateStateBody.recordCounts.pendingReview, 0);

await postJson('/__test/set-agent-status', {
handle: 'selene-test-agent',
status: 'suspended',
}, {}, NOW + 56);
const stateWhileSuspended = await postJson(
`/v1/mcp/grants/${encodeURIComponent(mcpGrantId)}/agent/state`,
{},
{ authorization: `Bearer ${MCP_SERVICE_SECRET}` },
NOW + 56,
);
assert.equal(stateWhileSuspended.status, 401);
await postJson('/__test/set-agent-status', {
handle: 'selene-test-agent',
status: 'active',
}, {}, NOW + 56);

const revokeNoCsrf = await postJson(
`/v1/mcp/authorizations/${encodeURIComponent(mcpGrantId)}/revoke`,
{},
Expand Down
Loading