···99// CONSTANTS
1010////////////////////////////////////////////
11111212-const IDB_KEY = "diffuse/output/raw/atproto/passkey";
1313-const IDB_KEY_CIPHER = "diffuse/output/raw/atproto/passkey/cipher-key";
1212+const IDB_PREFIX = "diffuse/transformer/output/refiner/track-uri-passkey";
1313+1414+/**
1515+ * @param {string} namespace
1616+ * @returns {{ credential: string, cipher: string }}
1717+ */
1818+function idbKeys(namespace) {
1919+ const prefix = namespace && namespace.length
2020+ ? `${IDB_PREFIX}/${namespace}`
2121+ : IDB_PREFIX;
2222+ return {
2323+ credential: `${prefix}/passkey`,
2424+ cipher: `${prefix}/passkey/cipher-key`,
2525+ };
2626+}
14271528////////////////////////////////////////////
1629// RELYING PARTY
···3144/**
3245 * Register a new passkey with the PRF extension.
3346 *
3434- * @returns {Promise<{ supported: true, credentialId: Uint8Array } | { supported: false, reason: string }>}
4747+ * @param {string} namespace
4848+ * @returns {Promise<{ supported: true, credentialId: Uint8Array, prfSecond: ArrayBuffer } | { supported: false, reason: string }>}
3549 */
3636-export async function createPasskey() {
5050+export async function createPasskey(namespace) {
3751 const rp = relyingParty();
3852 const challenge = crypto.getRandomValues(new Uint8Array(32));
3953 const userId = crypto.getRandomValues(new Uint8Array(16));
···97111 };
98112 }
99113114114+ // @ts-ignore — PRF is not yet in the TS DOM types
115115+ const prfSecond = extensions.prf?.results?.second;
116116+117117+ if (!prfSecond) {
118118+ return {
119119+ supported: false,
120120+ reason: "Authenticator did not return PRF results at registration time",
121121+ };
122122+ }
123123+100124 const credentialId = new Uint8Array(credential.rawId);
101101- await IDB.set(IDB_KEY, { credentialId: Array.from(credentialId) });
125125+ await IDB.set(idbKeys(namespace).credential, {
126126+ credentialId: Array.from(credentialId),
127127+ });
102128103103- return { supported: true, credentialId };
129129+ return { supported: true, credentialId, prfSecond: /** @type {ArrayBuffer} */ (prfSecond) };
104130}
105131106132/**
···108134 * (no `allowCredentials`), so it works on a new device that has no stored
109135 * credential ID yet. Saves the credential ID to IDB and returns PRF material.
110136 *
137137+ * @param {string} namespace
111138 * @returns {Promise<{ supported: true, credentialId: Uint8Array, prfSecond: ArrayBuffer } | { supported: false, reason: string }>}
112139 */
113113-export async function adoptPasskeyPrfResult() {
140140+export async function adoptPasskeyPrfResult(namespace) {
114141 const rp = relyingParty();
115142 const challenge = crypto.getRandomValues(new Uint8Array(32));
116143···163190 }
164191165192 const credentialId = new Uint8Array(assertion.rawId);
166166- await IDB.set(IDB_KEY, { credentialId: Array.from(credentialId) });
193193+ await IDB.set(idbKeys(namespace).credential, {
194194+ credentialId: Array.from(credentialId),
195195+ });
167196168197 return {
169198 supported: true,
···175204/**
176205 * Remove the stored passkey credential ID and cached cipher key from IDB.
177206 *
207207+ * @param {string} namespace
178208 * @returns {Promise<void>}
179209 */
180180-export async function removeStoredPasskey() {
181181- await Promise.all([IDB.del(IDB_KEY), IDB.del(IDB_KEY_CIPHER)]);
210210+export async function removeStoredPasskey(namespace) {
211211+ const keys = idbKeys(namespace);
212212+ await Promise.all([IDB.del(keys.credential), IDB.del(keys.cipher)]);
182213}
183214184215/**
185216 * Persist the derived cipher key to IDB so it survives page reloads.
186217 *
218218+ * @param {string} namespace
187219 * @param {Uint8Array} key
188220 * @returns {Promise<void>}
189221 */
190190-export async function storeCipherKey(key) {
191191- await IDB.set(IDB_KEY_CIPHER, key);
222222+export async function storeCipherKey(namespace, key) {
223223+ await IDB.set(idbKeys(namespace).cipher, key);
192224}
193225194226/**
195227 * Retrieve the previously persisted cipher key from IDB.
196228 *
229229+ * @param {string} namespace
197230 * @returns {Promise<Uint8Array | undefined>}
198231 */
199199-export async function loadStoredCipherKey() {
200200- return IDB.get(IDB_KEY_CIPHER);
232232+export async function loadStoredCipherKey(namespace) {
233233+ return IDB.get(idbKeys(namespace).cipher);
201234}
202235203236////////////////////////////////////////////
-16
src/components/output/raw/atproto/types.d.ts
···11import type { SignalReader } from "@common/signal.d.ts";
22-import type { Track } from "@definitions/types.d.ts";
32import type { OutputElement } from "../../types.d.ts";
4354export type ATProtoOutputElement =
···87 did: SignalReader<string | null>;
98 rev: SignalReader<string | null>;
1091111- /** Track records with encrypted URIs that cannot be decrypted without the passkey. */
1212- lockedTracks: SignalReader<Track[]>;
1313-1414- /** True if passkey encryption is active for this session. */
1515- passkeyActive: SignalReader<boolean>;
1616-1710 getLatestCommit(): Promise<string | null>;
1811 login(handle: string): Promise<void>;
1912 logout(): Promise<void>;
2020-2121- /** Adopt an existing passkey from another device via discoverable-credential lookup. */
2222- adoptPasskey(): Promise<void>;
2323-2424- /** Remove the stored passkey credential and clear the in-memory key. */
2525- removePasskey(): Promise<void>;
2626-2727- /** Register a new passkey for track URI encryption. Throws if PRF is not supported. */
2828- setupPasskey(): Promise<void>;
2913 };