{"record":{"id":"0e53e65451a8b3f1","repo":"can1357/oh-my-pi","slug":"room-key-must-be-room-key-bytes-bytes-got-ra","errorCode":null,"errorMessage":"Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLength}","messagePattern":"Room key must be (.+?) bytes, got (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/collab/crypto.ts","lineNumber":29,"sourceCode":"const IV_LENGTH = 12;\nconst TEXT_ENCODER = new TextEncoder();\nconst TEXT_DECODER = new TextDecoder();\n\nexport function generateRoomKey(): Uint8Array {\n\tconst key = new Uint8Array(ROOM_KEY_BYTES);\n\tcrypto.getRandomValues(key);\n\treturn key;\n}\n\nexport function generateWriteToken(): Uint8Array {\n\tconst token = new Uint8Array(WRITE_TOKEN_BYTES);\n\tcrypto.getRandomValues(token);\n\treturn token;\n}\n\nexport function importRoomKey(raw: Uint8Array): Promise<CryptoKey> {\n\tif (raw.byteLength !== ROOM_KEY_BYTES) {\n\t\tthrow new Error(`Room key must be ${ROOM_KEY_BYTES} bytes, got ${raw.byteLength}`);\n\t}\n\treturn crypto.subtle.importKey(\"raw\", asStrict(raw), AES_ALGORITHM, false, [\"encrypt\", \"decrypt\"]);\n}\n\nexport async function seal(key: CryptoKey, frame: CollabFrame): Promise<Uint8Array> {\n\tconst iv = new Uint8Array(IV_LENGTH);\n\tcrypto.getRandomValues(iv);\n\tconst plaintext = TEXT_ENCODER.encode(JSON.stringify(frame));\n\tconst ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: AES_ALGORITHM, iv }, key, plaintext));\n\tconst out = new Uint8Array(IV_LENGTH + ciphertext.byteLength);\n\tout.set(iv, 0);\n\tout.set(ciphertext, IV_LENGTH);\n\treturn out;\n}\n\n/** Inverse of {@link seal}. Throws on auth failure or malformed input. */\nexport async function open(key: CryptoKey, data: Uint8Array): Promise<CollabFrame> {\n\tif (data.byteLength <= IV_LENGTH) {","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/collab/crypto.ts#L11-L47","documentation":"importRoomKey wraps a raw symmetric AES-GCM key for the collaboration room. The library enforces that the raw key bytes are exactly ROOM_KEY_BYTES (32 bytes); anything else is rejected before reaching WebCrypto so a mistyped or truncated secret fails loudly instead of producing a key that cannot decrypt host frames. This guards the room-key wire format produced by parseCollabLink and generateRoomKey.","triggerScenarios":"Calling importRoomKey(raw) with a Uint8Array whose byteLength !== ROOM_KEY_BYTES: a key decoded from a truncated/corrupted collab link, a hand-rolled hex string decoded to 16/64 bytes, a key padded or stripped by base64 vs base64url confusion, or a key sliced to the wrong subarray bounds.","commonSituations":"A guest pastes a collab link that was mangled by a terminal or chat client (truncated base64url); a developer constructs the key from a hex-encoded secret instead of the base64url fragment; an older client produced 16-byte keys before the format moved to 32; tests hardcode short dummy keys.","solutions":["Decode the key strictly from the link's base64url key fragment via parseCollabLink, which validates the 32-byte (or 48-byte with token) length before you call importRoomKey.","Check raw.byteLength === ROOM_KEY_BYTES before calling and regenerate/re-obtain the key if it differs.","If the key came from hex or a password, derive/re-encode it to exactly 32 bytes (e.g. Buffer.from(hex, 'hex') must yield 32 bytes) before importing.","For custom keys, use crypto.getRandomValues(new Uint8Array(ROOM_KEY_BYTES)) as generateRoomKey does."],"exampleFix":"// before\nconst key = await importRoomKey(Buffer.from(secretHex, \"hex\")); // 16 or 64 bytes for many hex inputs\n// after\nconst raw = Buffer.from(secretB64url, \"base64url\");\nif (raw.byteLength !== ROOM_KEY_BYTES) throw new Error(\"bad room key length\");\nconst key = await importRoomKey(new Uint8Array(raw));","handlingStrategy":"validation","validationCode":"if (raw.byteLength !== ROOM_KEY_BYTES) throw new Error(`expected ${ROOM_KEY_BYTES}-byte room key, got ${raw.byteLength}`);\nconst key = await importRoomKey(raw);","typeGuard":"function isValidRoomKey(raw: Uint8Array): boolean {\n  return raw instanceof Uint8Array && raw.byteLength === ROOM_KEY_BYTES;\n}","tryCatchPattern":null,"preventionTips":["Always obtain the key via parseCollabLink or generateRoomKey, never by hand-decoding strings.","Use base64url (not hex or base64) when round-tripping key bytes.","Assert key length in tests that construct keys manually."],"tags":["crypto","validation","collaboration"],"backgroundTag":"invalid-key-length","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}