@atproto/oauth-client-browser
Version: 
ATPROTO OAuth client for the browser (relies on WebCrypto & Indexed DB)
176 lines • 7.89 kB
JavaScript
;
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
    if (kind === "m") throw new TypeError("Private method is not writable");
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
    return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
    if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
    if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
    return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _BrowserOAuthDatabase_dbPromise, _BrowserOAuthDatabase_cleanupInterval;
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserOAuthDatabase = void 0;
const jwk_webcrypto_1 = require("@atproto/jwk-webcrypto");
const index_js_1 = require("./indexed-db/index.js");
function encodeKey(key) {
    if (!(key instanceof jwk_webcrypto_1.WebcryptoKey) || !key.kid) {
        throw new Error('Invalid key object');
    }
    return {
        keyId: key.kid,
        keyPair: key.cryptoKeyPair,
    };
}
async function decodeKey(encoded) {
    return jwk_webcrypto_1.WebcryptoKey.fromKeypair(encoded.keyPair, encoded.keyId);
}
const STORES = [
    'state',
    'session',
    'didCache',
    'dpopNonceCache',
    'handleCache',
    'authorizationServerMetadataCache',
    'protectedResourceMetadataCache',
];
class BrowserOAuthDatabase {
    constructor(options) {
        _BrowserOAuthDatabase_dbPromise.set(this, void 0);
        _BrowserOAuthDatabase_cleanupInterval.set(this, void 0);
        __classPrivateFieldSet(this, _BrowserOAuthDatabase_dbPromise, index_js_1.DB.open(options?.name ?? '@atproto-oauth-client', [
            (db) => {
                for (const name of STORES) {
                    const store = db.createObjectStore(name, { autoIncrement: true });
                    store.createIndex('expiresAt', 'expiresAt', { unique: false });
                }
            },
        ], { durability: options?.durability ?? 'strict' }), "f");
        __classPrivateFieldSet(this, _BrowserOAuthDatabase_cleanupInterval, setInterval(() => {
            void this.cleanup();
        }, options?.cleanupInterval ?? 30e3), "f");
    }
    async run(storeName, mode, fn) {
        const db = await __classPrivateFieldGet(this, _BrowserOAuthDatabase_dbPromise, "f");
        return await db.transaction([storeName], mode, (tx) => fn(tx.objectStore(storeName)));
    }
    createStore(name, { encode, decode, expiresAt, }) {
        return {
            get: async (key) => {
                // Find item in store
                const item = await this.run(name, 'readonly', (store) => store.get(key));
                // Not found
                if (item === undefined)
                    return undefined;
                // Too old (delete)
                if (item.expiresAt != null && new Date(item.expiresAt) < new Date()) {
                    await this.run(name, 'readwrite', (store) => store.delete(key));
                    return undefined;
                }
                // Item found and valid. Decode
                return decode(item.value);
            },
            set: async (key, value) => {
                // Create encoded item record
                const item = {
                    value: await encode(value),
                    expiresAt: expiresAt(value)?.toISOString(),
                };
                // Store item record
                await this.run(name, 'readwrite', (store) => store.put(item, key));
            },
            del: async (key) => {
                // Delete
                await this.run(name, 'readwrite', (store) => store.delete(key));
            },
        };
    }
    getSessionStore() {
        return this.createStore('session', {
            expiresAt: ({ tokenSet }) => tokenSet.refresh_token || tokenSet.expires_at == null
                ? null
                : new Date(tokenSet.expires_at),
            encode: ({ dpopKey, ...session }) => ({
                ...session,
                dpopKey: encodeKey(dpopKey),
            }),
            decode: async ({ dpopKey, ...encoded }) => ({
                ...encoded,
                dpopKey: await decodeKey(dpopKey),
            }),
        });
    }
    getStateStore() {
        return this.createStore('state', {
            expiresAt: (_value) => new Date(Date.now() + 10 * 60e3),
            encode: ({ dpopKey, ...session }) => ({
                ...session,
                dpopKey: encodeKey(dpopKey),
            }),
            decode: async ({ dpopKey, ...encoded }) => ({
                ...encoded,
                dpopKey: await decodeKey(dpopKey),
            }),
        });
    }
    getDpopNonceCache() {
        return this.createStore('dpopNonceCache', {
            expiresAt: (_value) => new Date(Date.now() + 600e3),
            encode: (value) => value,
            decode: (encoded) => encoded,
        });
    }
    getDidCache() {
        return this.createStore('didCache', {
            expiresAt: (_value) => new Date(Date.now() + 60e3),
            encode: (value) => value,
            decode: (encoded) => encoded,
        });
    }
    getHandleCache() {
        return this.createStore('handleCache', {
            expiresAt: (_value) => new Date(Date.now() + 60e3),
            encode: (value) => value,
            decode: (encoded) => encoded,
        });
    }
    getAuthorizationServerMetadataCache() {
        return this.createStore('authorizationServerMetadataCache', {
            expiresAt: (_value) => new Date(Date.now() + 60e3),
            encode: (value) => value,
            decode: (encoded) => encoded,
        });
    }
    getProtectedResourceMetadataCache() {
        return this.createStore('protectedResourceMetadataCache', {
            expiresAt: (_value) => new Date(Date.now() + 60e3),
            encode: (value) => value,
            decode: (encoded) => encoded,
        });
    }
    async cleanup() {
        const db = await __classPrivateFieldGet(this, _BrowserOAuthDatabase_dbPromise, "f");
        for (const name of STORES) {
            await db.transaction([name], 'readwrite', (tx) => tx
                .objectStore(name)
                .index('expiresAt')
                .deleteAll(IDBKeyRange.upperBound(Date.now())));
        }
    }
    async [(_BrowserOAuthDatabase_dbPromise = new WeakMap(), _BrowserOAuthDatabase_cleanupInterval = new WeakMap(), Symbol.asyncDispose)]() {
        clearInterval(__classPrivateFieldGet(this, _BrowserOAuthDatabase_cleanupInterval, "f"));
        __classPrivateFieldSet(this, _BrowserOAuthDatabase_cleanupInterval, undefined, "f");
        const dbPromise = __classPrivateFieldGet(this, _BrowserOAuthDatabase_dbPromise, "f");
        __classPrivateFieldSet(this, _BrowserOAuthDatabase_dbPromise, Promise.reject(new Error('Database has been disposed')), "f");
        // Avoid "unhandled promise rejection"
        __classPrivateFieldGet(this, _BrowserOAuthDatabase_dbPromise, "f").catch(() => null);
        // Spec recommends not to throw errors in dispose
        const db = await dbPromise.catch(() => null);
        if (db)
            await (db[Symbol.asyncDispose] || db[Symbol.dispose]).call(db);
    }
}
exports.BrowserOAuthDatabase = BrowserOAuthDatabase;
//# sourceMappingURL=browser-oauth-database.js.map