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:
@@ -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('');
|
||||
}
|
||||
Reference in New Issue
Block a user