feat(arcrun): mira wiki page with tag filter + accumulated WIP

- landing/app/mira/wiki: tag=mira-wiki list now shows all wiki paragraphs
  (depends on KBDB tag filter exposed in matrix/kbdb commit, separate repo)
- landing: app/mira hub + feed split + various WIP from prior sessions
- registry/components: claude_api / kbdb_create_block / kbdb_get / km_writer /
  platform_crypto / auth_oauth2 contracts + main.go (accumulated)
- .component-builds: pkg-lock updates + index.ts adjustments (WIP)
- .agents/specs/arcrun/frontend-redesign: design notes
- docs/test_credentials, docs/user_requirements/arcrun-landing-page: WIP docs
- cypher-executor: auth-dispatcher / wasi-shim adjustments (WIP)

Includes accumulated work from prior sessions plus the wiki UI tag-filter
update that surfaces the AI-generated wiki paragraphs at /mira/wiki.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-07 16:52:01 +08:00
parent e8fca33f80
commit 519423cb0d
127 changed files with 23909 additions and 264 deletions
@@ -0,0 +1,108 @@
/**
* arcrun platform_crypto Worker
*
* POST / → JSON input {action, ...} → JSON output
*
* Actions:
* generate_api_key — HMAC-SHA256(email, ENCRYPTION_KEY) → ak_{hex[:32]}
* encrypt — AES-GCM(plaintext, ENCRYPTION_KEY) → {encrypted, iv}base64
* random_token — crypto random bytes → hex string
*
* 安全邊界:ENCRYPTION_KEY 只存在於 closure,永不進入外部(rule 02 §2.2)。
* 此 Worker 直接用 crypto.subtle 實作,不走 WASM runner。
* TinyGo WASM async host function 在 Cloudflare Workers 的 u6u namespace 不支援 Promise.
* WASM 零件 (registry/components/platform_crypto/) 保留作為 edge-Go 移植時的參考。
*/
import { Hono } from 'hono';
import { cors } from 'hono/cors';
type Env = {
ENCRYPTION_KEY: string;
};
type Input = {
action: string;
email?: string;
plaintext?: string;
bytes?: number;
};
const app = new Hono<{ Bindings: Env }>();
app.use('*', cors());
app.get('/', (c) => c.json({ ok: true, component: 'platform_crypto' }));
app.post('/', async (c) => {
let input: Input;
try {
input = await c.req.json() as Input;
} catch {
return c.json({ success: false, error: 'request body must be JSON' }, 400);
}
const encryptionKey = c.env.ENCRYPTION_KEY;
if (!encryptionKey) {
return c.json({ success: false, error: 'ENCRYPTION_KEY not configured' }, 503);
}
try {
switch (input.action) {
case 'generate_api_key': {
if (!input.email) return c.json({ success: false, error: 'email 必填' }, 400);
const apiKey = await generateApiKey(input.email, encryptionKey);
return c.json({ success: true, api_key: apiKey });
}
case 'encrypt': {
if (!input.plaintext) return c.json({ success: false, error: 'plaintext 必填' }, 400);
const { encrypted, iv } = await aesEncrypt(input.plaintext, encryptionKey);
return c.json({ success: true, encrypted, iv });
}
case 'random_token': {
const numBytes = (input.bytes ?? 32) > 0 ? (input.bytes ?? 32) : 32;
const token = randomHex(numBytes);
return c.json({ success: true, token });
}
default:
return c.json({ success: false, error: `不支援的 action: ${input.action}` }, 400);
}
} catch (e) {
return c.json(
{ success: false, error: e instanceof Error ? e.message : String(e) },
500,
);
}
});
export default app;
// ── Crypto implementations (rule 02 §2.2: crypto.subtle 只准在 wasi-shim.ts 或 platform_crypto) ──
async function generateApiKey(email: string, encryptionKey: string): Promise<string> {
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
const cryptoKey = await crypto.subtle.importKey(
'raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign'],
);
const sig = await crypto.subtle.sign('HMAC', cryptoKey, new TextEncoder().encode(email));
const hex = Array.from(new Uint8Array(sig)).map(b => b.toString(16).padStart(2, '0')).join('');
return 'ak_' + hex.slice(0, 32);
}
async function aesEncrypt(plaintext: string, encryptionKey: string): Promise<{ encrypted: string; iv: string }> {
const keyBytes = new TextEncoder().encode(encryptionKey.slice(0, 32));
const cryptoKey = await crypto.subtle.importKey('raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']);
const iv = crypto.getRandomValues(new Uint8Array(12));
const enc = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
new TextEncoder().encode(plaintext),
);
const toB64 = (buf: ArrayBuffer | Uint8Array) =>
btoa(String.fromCharCode(...new Uint8Array(buf instanceof ArrayBuffer ? buf : buf)));
return { encrypted: toB64(enc), iv: toB64(iv) };
}
function randomHex(numBytes: number): string {
const arr = crypto.getRandomValues(new Uint8Array(numBytes));
return Array.from(arr).map(b => b.toString(16).padStart(2, '0')).join('');
}