├── .eslintrc.js ├── .gitignore ├── .ncurc.js ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── TODO.md ├── jest.config.js ├── nodemon.json ├── package.json ├── src ├── classes │ ├── session-manager.ts │ └── storage-provider.ts ├── constants │ ├── constants.ts │ └── dexie.ts ├── exceptions.ts ├── index.ts ├── plugin │ ├── injector.spec.ts │ ├── injector.ts │ ├── plugin.spec.ts │ └── plugin.ts ├── providers │ ├── cookies.spec.ts │ ├── cookies.ts │ ├── indexedDb.spec.ts │ ├── indexedDb.ts │ ├── localStorage.ts │ └── sessionStorage.ts ├── schemas.ts ├── session.ts ├── types.ts └── types │ └── session-data.ts ├── tsconfig.json └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // This is a workaround for https://github.com/eslint/eslint/issues/3458 2 | require('@rushstack/eslint-config/patch/modern-module-resolution'); 3 | 4 | var isDev = process.env.NODE_ENV === 'development'; 5 | 6 | /** 7 | * @type {import("eslint").Linter.Config} 8 | */ 9 | const config = { 10 | root: true, 11 | extends: ['@rushstack/eslint-config/profile/node'], // <---- put your profile string here 12 | env: { 13 | es6: true, 14 | }, 15 | ignorePatterns: [], 16 | rules: { 17 | // This rule reduces performance by 84%, so only enabled during CI. 18 | '@typescript-eslint/no-floating-promises': isDev ? 'off' : 'error', 19 | '@typescript-eslint/no-invalid-this': 'error', 20 | 'no-console': 'warn', 21 | }, 22 | overrides: [ 23 | { 24 | files: ['**/*.{ts,js}'], 25 | /** 26 | * TypeScript configuration 27 | */ 28 | parser: '@typescript-eslint/parser', 29 | parserOptions: { 30 | tsconfigRootDir: __dirname, 31 | project: './tsconfig.json', 32 | }, 33 | }, 34 | { 35 | files: ['src/packlets/scripts/*', '*.spec.ts'], 36 | rules: { 37 | '@rushstack/packlets/mechanics': 0, 38 | }, 39 | }, 40 | { 41 | files: ['**/schemas/*.ts', '**/env.ts'], 42 | rules: { 43 | '@typescript-eslint/typedef': 0, 44 | }, 45 | }, 46 | { 47 | files: ['src/commands/**'], 48 | rules: { 49 | '@typescript-eslint/typedef': 0, 50 | '@typescript-eslint/explicit-member-accessibility': 0, 51 | }, 52 | }, 53 | ], 54 | }; 55 | 56 | module.exports = config; 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | lib 4 | package-lock.json -------------------------------------------------------------------------------- /.ncurc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | upgrade: true, 3 | reject: ['@types/node', 'got', 'execa'], 4 | loglevel: 'error', 5 | errorLevel: 1, 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true, 5 | "printWidth": 80, 6 | "useTabs": false, 7 | "tabWidth": 2 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["dexie", "Dexie"] 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # puppeteer-extra-plugin-session 2 | 3 | > A puppeteer-extra plugin to export and import session data (cookies, localStorage, sessionStorage, indexedDb) 4 | 5 | ## Introduction 6 | 7 | Dump and restore session data from your puppeteer pages. 8 | 9 | This plugin supports: 10 | 11 | - cookies 12 | - localStorage 13 | - sessionStorage 14 | - IndexedDB _(currently, only the securityOrigin of the current page will get dumped)_ 15 | 16 | ## Installation 17 | 18 | ```bash 19 | yarn add puppeteer-extra-plugin-session 20 | # or 21 | npm install puppeteer-extra-plugin-session 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### Basic usage 27 | 28 | First of all, you have to register the plugin with `puppeteer-extra`. 29 | 30 | JavaScript: 31 | 32 | ```js 33 | puppeteer.use(require('puppeteer-extra-plugin-session').default()); 34 | ``` 35 | 36 | TypeScript: 37 | 38 | ```ts 39 | import SessionPlugin from 'puppeteer-extra-plugin-session'; 40 | puppeteer.use(SessionPlugin()); 41 | ``` 42 | 43 | Then, you'll have access to session data helpers: 44 | 45 | ```ts 46 | const sessionData = await page.session.dump(); // or page.session.dumpString() 47 | 48 | // [...] 49 | 50 | await page.session.restore(sessionData); // or page.session.restoreString(sessionData) 51 | ``` 52 | 53 | ### Selecting storage backends 54 | 55 | You may wish to exclude certain storage backends from being dumped or restored. 56 | This can be done by passing an options object to the `dump` and `restore` methods: 57 | 58 | ```ts 59 | import { StorageProviderName } from 'puppeteer-extra-plugin-session'; 60 | 61 | const sessionData = await page.session.dump({ 62 | storageProviders: [ 63 | StorageProviderName.Cookie, 64 | StorageProviderName.LocalStorage, 65 | ], // only dump cookies and LocalStorage 66 | }); 67 | 68 | // Here is the list of StorageProviderName: 69 | // * StorageProviderName.Cookie 70 | // * StorageProviderName.LocalStorage 71 | // * StorageProviderName.SessionStorage 72 | // * StorageProviderName.IndexedDB 73 | 74 | // You can also filter what gets restored: 75 | await page.session.restore(sessionData, { 76 | storageProviders: [StorageProviderName.Cookie], // only restore cookies (the previous dump also contained LocalStorage) 77 | }); 78 | ``` 79 | 80 | ## Testing 81 | 82 | Tests are defined in `*.spec.ts` files. 83 | 84 | You can run the tests watcher using `yarn test` or `npm run test` 85 | 86 | ## Debugging 87 | 88 | You can see the package's logs by setting the `DEBUG=puppeteer-extra-plugin:session` env variable. 89 | 90 | Example: `DEBUG=puppeteer-extra-plugin:session npm test` 91 | 92 | ### Base Puppeteer-Extra Plugin System 93 | 94 | See the core Puppeteer-Extra Plugin docs for additional information: 95 | 96 | 97 | ## Contributing 98 | 99 | We appreciate all contributions. 100 | 101 | See [TODO.md](/TODO.md) 102 | 103 | ## License 104 | 105 | MIT 106 | 107 | ## Resources 108 | 109 | - [Puppeteer documentation](https://pptr.dev) 110 | - [Puppeteer-Extra plugin documentation](https://github.com/berstend/puppeteer-extra/tree/master/packages/puppeteer-extra-plugin) 111 | - [CDP documentation](https://chromedevtools.github.io/devtools-protocol/) 112 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### TODOs 2 | 3 | | Filename | line # | TODO | 4 | | :---------------------------------------------------------------------------------------- | :----: | :---------------------------------------------------------- | 5 | | [src/index.ts](src/index.ts#L3) | 3 | make sure that everything is exported | 6 | | [src/plugin.ts](src/plugin.ts#L7) | 7 | use documentation.js to generate documentation in README.md | 7 | | [src/providers/IndexedDB/database-names.ts](src/providers/IndexedDB/database-names.ts#L4) | 4 | change this to an appropriate name | 8 | | [src/providers/IndexedDB/set.ts](src/providers/IndexedDB/set.ts#L15) | 15 | investigate database versions | 9 | 10 | ### STEALTHs 11 | 12 | | Filename | line # | STEALTH | 13 | | :------------------------------------------------------------------- | :----: | :------------------ | 14 | | [src/providers/localStorage.ts](src/providers/localStorage.ts#L4) | 4 | use isolated worlds | 15 | | [src/providers/localStorage.ts](src/providers/localStorage.ts#L12) | 12 | use isolated worlds | 16 | | [src/providers/IndexedDB/get.ts](src/providers/IndexedDB/get.ts#L16) | 16 | isolated worlds | 17 | | [src/providers/IndexedDB/set.ts](src/providers/IndexedDB/set.ts#L14) | 14 | isolated worlds | 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | testPathIgnorePatterns: ['/lib'], 5 | }; 6 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/nodemon.json", 3 | "verbose": false, 4 | "ignore": ["*.md", ".git", "node_modules"], 5 | "watch": ["src", "package.json"], 6 | "ext": "js,ts,json", 7 | "execMap": { 8 | "ts": "tsc" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-extra-plugin-session", 3 | "version": "1.0.1", 4 | "description": "A puppeteer plugin to dump and inject session data.", 5 | "keywords": [ 6 | "puppeteer", 7 | "puppeteer-extra", 8 | "puppeteer-extra-plugin", 9 | "session", 10 | "puppeteer-extra-plugin-stealth", 11 | "cookies", 12 | "indexeddb", 13 | "profile", 14 | "sessionStorage", 15 | "localStorage" 16 | ], 17 | "repository": "https://github.com/clouedoc/puppeteer-extra-plugin-session", 18 | "license": "MIT", 19 | "author": "Camille Louédoc ", 20 | "main": "lib/index.js", 21 | "types": "lib/index.d.ts", 22 | "files": [ 23 | "lib" 24 | ], 25 | "scripts": { 26 | "build": "yarn tsc", 27 | "detect-circular-dependencies": "madge --circular --extensions ts ./src", 28 | "dev": "nodemon --exec 'yarn build && yarn lint && yarn detect-circular-dependencies && yarn sort'", 29 | "dev:publish": "rm -rf lib && yarn build && yarn publish", 30 | "lint": "eslint ./src --fix --max-warnings 0", 31 | "ncu": "npx npm-check-updates", 32 | "prettier": "prettier --config .prettierrc --write --loglevel error .", 33 | "sort": "npx sort-package-json", 34 | "test": "yarn jest --watchAll", 35 | "todo": "leasot -x --reporter markdown 'src/**/*.ts' --tags STEALTH > TODO.md" 36 | }, 37 | "dependencies": { 38 | "puppeteer-extra-plugin": "^3.2.2", 39 | "tslib": "^2.4.0", 40 | "zod": "^3.19.1" 41 | }, 42 | "devDependencies": { 43 | "@rushstack/eslint-config": "^3.0.1", 44 | "@types/jest": "^29", 45 | "@types/node": "^14", 46 | "eslint": "^8", 47 | "eslint-config-prettier": "^8", 48 | "eslint-config-prettier-standard": "^4", 49 | "eslint-config-standard": "^17", 50 | "eslint-plugin-import": "^2", 51 | "eslint-plugin-node": "^11", 52 | "eslint-plugin-prettier": "^4", 53 | "eslint-plugin-promise": "^6", 54 | "eslint-plugin-standard": "^5.0.0", 55 | "jest": "^29.0.3", 56 | "leasot": "^13.2.0", 57 | "madge": "^5.0.1", 58 | "nodemon": "^2.0.20", 59 | "prettier": "^2.7.1", 60 | "prettier-config-standard": "^5", 61 | "puppeteer": "^18.0.5", 62 | "puppeteer-core": "^18.0.5", 63 | "puppeteer-extra": "^3.3.4", 64 | "puppeteer-extra-plugin-stealth": "^2.11.1", 65 | "ts-jest": "^29.0.2", 66 | "typescript": "^4.8.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/classes/session-manager.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { ZodError } from 'zod'; 3 | import { CorruptedSessionDataError } from '../exceptions'; 4 | import { SessionDataSchema } from '../schemas'; 5 | import { getSessionData, setSessionData } from '../session'; 6 | import { SessionData } from '../types/session-data'; 7 | import { StorageProviderName } from './storage-provider'; 8 | 9 | export interface ISessionManagerOptions { 10 | /** 11 | * List of storage providers to enable. 12 | * If none is provided, all the storage providers will be enabled. 13 | */ 14 | storageProviders?: StorageProviderName[]; 15 | } 16 | 17 | export const defaultSessionManagerOptions: ISessionManagerOptions = { 18 | storageProviders: Object.values(StorageProviderName), 19 | }; 20 | 21 | export class SessionManager { 22 | protected readonly page: Page; 23 | 24 | public constructor(page: Page) { 25 | this.page = page; 26 | } 27 | 28 | public async dump( 29 | options: ISessionManagerOptions = defaultSessionManagerOptions, 30 | ): Promise { 31 | return getSessionData(this.page, options.storageProviders); 32 | } 33 | 34 | public async restore( 35 | sessionData: SessionData, 36 | options: ISessionManagerOptions = defaultSessionManagerOptions, 37 | ): Promise { 38 | let data; 39 | try { 40 | data = SessionDataSchema.parse(sessionData); 41 | } catch (err) { 42 | if (err instanceof ZodError) { 43 | throw new CorruptedSessionDataError(err); 44 | } 45 | 46 | throw err; 47 | } 48 | 49 | await setSessionData(this.page, data, options.storageProviders); 50 | } 51 | 52 | /** 53 | * Helper function to serialize the output of dump into JSON format. 54 | */ 55 | public async dumpString( 56 | options: ISessionManagerOptions = defaultSessionManagerOptions, 57 | ): Promise { 58 | return JSON.stringify(await this.dump(options)); 59 | } 60 | 61 | /** 62 | * Helper function to parse a JSON string into a SessionData object and feed it to `restore` 63 | */ 64 | public async restoreString( 65 | sessionData: string, 66 | options: ISessionManagerOptions = defaultSessionManagerOptions, 67 | ): Promise { 68 | await this.restore(JSON.parse(sessionData), options); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/classes/storage-provider.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | 3 | export enum StorageProviderName { 4 | Cookie = 'cookie', 5 | LocalStorage = 'localStorage', 6 | SessionStorage = 'sessionStorage', 7 | IndexedDB = 'indexedDB', 8 | } 9 | 10 | export abstract class StorageProvider { 11 | public abstract name: StorageProviderName; 12 | public abstract get(page: Page): Promise; 13 | public abstract set(page: Page, data: string): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/constants/constants.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | /** 4 | * Define the plugin name to report to extra. 5 | */ 6 | export const PLUGIN_NAME: string = 'session'; 7 | 8 | /** 9 | * Used for testing only. 10 | * 11 | * Get this by navigating to chrome://version 12 | */ 13 | export let TestBrowserExecutablePath: string | undefined = undefined; 14 | 15 | switch (os.platform()) { 16 | case 'darwin': 17 | TestBrowserExecutablePath = 18 | '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'; 19 | break; 20 | case 'linux': 21 | TestBrowserExecutablePath = '/usr/bin/google-chrome'; 22 | break; 23 | } 24 | -------------------------------------------------------------------------------- /src/constants/dexie.ts: -------------------------------------------------------------------------------- 1 | // https://cdnjs.cloudflare.com/ajax/libs/dexie/3.0.3/dexie.min.js 2 | // https://unpkg.com/dexie-export-import@1.0.0/dist/dexie-export-import.js 3 | 4 | export const dexieCore: string = String.raw` 5 | (function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Dexie=t()})(this,function(){"use strict";var m=function(){return(m=Object.assign||function(e){for(var t,n=1,r=arguments.length;n.",_t="String expected.",wt=[],xt="undefined"!=typeof navigator&&/(MSIE|Trident|Edge)/.test(navigator.userAgent),kt=xt,Pt=xt,Et="__dbnames",Ot="readonly",jt="readwrite";function St(e,t){return e?t?function(){return e.apply(this,arguments)&&t.apply(this,arguments)}:e:t}var At={type:3,lower:-1/0,lowerOpen:!1,upper:[[]],upperOpen:!1};function Kt(t){return function(e){return void 0===x(e,t)&&P(e=K(e),t),e}}var Ct=(It.prototype._trans=function(e,r,t){var n=this._tx||Te.trans,i=this.name;function o(e,t,n){if(!n.schema[i])throw new te.NotFound("Table "+i+" not part of transaction");return r(n.idbtrans,n)}var u=Ye();try{return n&&n.db===this.db?n===Te.trans?n._promise(e,o,t):rt(function(){return n._promise(e,o,t)},{trans:n,transless:Te.transless||Te}):function e(t,n,r,i){if(t._state.openComplete||Te.letThrough){var o=t._createTransaction(n,r,t._dbSchema);try{o.create()}catch(e){return vt(e)}return o._promise(n,function(e,t){return rt(function(){return Te.trans=o,i(e,t,o)})}).then(function(e){return o._completion.then(function(){return e})})}if(!t._state.isBeingOpened){if(!t._options.autoOpen)return vt(new te.DatabaseClosed);t.open().catch(ie)}return t._state.dbReadyPromise.then(function(){return e(t,n,r,i)})}(this.db,e,[this.name],o)}finally{u&&Ge()}},It.prototype.get=function(t,e){var n=this;return t&&t.constructor===Object?this.where(t).first(e):this._trans("readonly",function(e){return n.core.get({trans:e,key:t}).then(function(e){return n.hook.reading.fire(e)})}).then(e)},It.prototype.where=function(u){if("string"==typeof u)return new this.db.WhereClause(this,u);if(d(u))return new this.db.WhereClause(this,"["+u.join("+")+"]");var n=_(u);if(1===n.length)return this.where(n[0]).equals(u[n[0]]);var e=this.schema.indexes.concat(this.schema.primKey).filter(function(t){return t.compound&&n.every(function(e){return 0<=t.keyPath.indexOf(e)})&&t.keyPath.every(function(e){return 0<=n.indexOf(e)})})[0];if(e&&this.db._maxKey!==gt)return this.where(e.name).equals(e.keyPath.map(function(e){return u[e]}));!e&&N&&console.warn("The query "+JSON.stringify(u)+" on "+this.name+" would benefit of a compound index ["+n.join("+")+"]");var a=this.schema.idxByName,r=this.db._deps.indexedDB;function s(e,t){try{return 0===r.cmp(e,t)}catch(e){return!1}}var t=n.reduce(function(e,n){var t=e[0],r=e[1],i=a[n],o=u[n];return[t||i,t||!i?St(r,i&&i.multi?function(e){var t=x(e,n);return d(t)&&t.some(function(e){return s(o,e)})}:function(e){return s(o,x(e,n))}):r]},[null,null]),i=t[0],o=t[1];return i?this.where(i.name).equals(u[i.keyPath]).filter(o):e?this.filter(o):this.where(n).equals("")},It.prototype.filter=function(e){return this.toCollection().and(e)},It.prototype.count=function(e){return this.toCollection().count(e)},It.prototype.offset=function(e){return this.toCollection().offset(e)},It.prototype.limit=function(e){return this.toCollection().limit(e)},It.prototype.each=function(e){return this.toCollection().each(e)},It.prototype.toArray=function(e){return this.toCollection().toArray(e)},It.prototype.toCollection=function(){return new this.db.Collection(new this.db.WhereClause(this))},It.prototype.orderBy=function(e){return new this.db.Collection(new this.db.WhereClause(this,d(e)?"["+e.join("+")+"]":e))},It.prototype.reverse=function(){return this.toCollection().reverse()},It.prototype.mapToClass=function(r){function e(e){if(!e)return e;var t=Object.create(r.prototype);for(var n in e)if(g(e,n))try{t[n]=e[n]}catch(e){}return t}return this.schema.mappedClass=r,this.schema.readHook&&this.hook.reading.unsubscribe(this.schema.readHook),this.schema.readHook=e,this.hook("reading",e),r},It.prototype.defineClass=function(){return this.mapToClass(function(e){s(this,e)})},It.prototype.add=function(t,n){var r=this,e=this.schema.primKey,i=e.auto,o=e.keyPath,u=t;return o&&i&&(u=Kt(o)(t)),this._trans("readwrite",function(e){return r.core.mutate({trans:e,type:"add",keys:null!=n?[n]:null,values:[u]})}).then(function(e){return e.numFailures?Fe.reject(e.failures[0]):e.lastResult}).then(function(e){if(o)try{k(t,o,e)}catch(e){}return e})},It.prototype.update=function(t,n){if("object"!=typeof n||d(n))throw new te.InvalidArgument("Modifications must be an object.");if("object"!=typeof t||d(t))return this.where(":id").equals(t).modify(n);_(n).forEach(function(e){k(t,e,n[e])});var e=x(t,this.schema.primKey.keyPath);return void 0===e?vt(new te.InvalidArgument("Given object does not contain its primary key")):this.where(":id").equals(e).modify(n)},It.prototype.put=function(t,n){var r=this,e=this.schema.primKey,i=e.auto,o=e.keyPath,u=t;return o&&i&&(u=Kt(o)(t)),this._trans("readwrite",function(e){return r.core.mutate({trans:e,type:"put",values:[u],keys:null!=n?[n]:null})}).then(function(e){return e.numFailures?Fe.reject(e.failures[0]):e.lastResult}).then(function(e){if(o)try{k(t,o,e)}catch(e){}return e})},It.prototype.delete=function(t){var n=this;return this._trans("readwrite",function(e){return n.core.mutate({trans:e,type:"delete",keys:[t]})}).then(function(e){return e.numFailures?Fe.reject(e.failures[0]):void 0})},It.prototype.clear=function(){var t=this;return this._trans("readwrite",function(e){return t.core.mutate({trans:e,type:"deleteRange",range:At})}).then(function(e){return e.numFailures?Fe.reject(e.failures[0]):void 0})},It.prototype.bulkGet=function(t){var n=this;return this._trans("readonly",function(e){return n.core.getMany({keys:t,trans:e}).then(function(e){return e.map(function(e){return n.hook.reading.fire(e)})})})},It.prototype.bulkAdd=function(u,e,t){var a=this,s=Array.isArray(e)?e:void 0,c=(t=t||(s?void 0:e))?t.allKeys:void 0;return this._trans("readwrite",function(e){var t=a.schema.primKey,n=t.auto,r=t.keyPath;if(r&&s)throw new te.InvalidArgument("bulkAdd(): keys argument invalid on tables with inbound keys");if(s&&s.length!==u.length)throw new te.InvalidArgument("Arguments objects and keys must have the same length");var o=u.length,i=r&&n?u.map(Kt(r)):u;return a.core.mutate({trans:e,type:"add",keys:s,values:i,wantResults:c}).then(function(e){var t=e.numFailures,n=e.results,r=e.lastResult,i=e.failures;if(0===t)return c?n:r;throw new $(a.name+".bulkAdd(): "+t+" of "+o+" operations failed",Object.keys(i).map(function(e){return i[e]}))})})},It.prototype.bulkPut=function(u,e,t){var a=this,s=Array.isArray(e)?e:void 0,c=(t=t||(s?void 0:e))?t.allKeys:void 0;return this._trans("readwrite",function(e){var t=a.schema.primKey,n=t.auto,r=t.keyPath;if(r&&s)throw new te.InvalidArgument("bulkPut(): keys argument invalid on tables with inbound keys");if(s&&s.length!==u.length)throw new te.InvalidArgument("Arguments objects and keys must have the same length");var o=u.length,i=r&&n?u.map(Kt(r)):u;return a.core.mutate({trans:e,type:"put",keys:s,values:i,wantResults:c}).then(function(e){var t=e.numFailures,n=e.results,r=e.lastResult,i=e.failures;if(0===t)return c?n:r;throw new $(a.name+".bulkPut(): "+t+" of "+o+" operations failed",Object.keys(i).map(function(e){return i[e]}))})})},It.prototype.bulkDelete=function(t){var i=this,o=t.length;return this._trans("readwrite",function(e){return i.core.mutate({trans:e,type:"delete",keys:t})}).then(function(e){var t=e.numFailures,n=e.lastResult,r=e.failures;if(0===t)return n;throw new $(i.name+".bulkDelete(): "+t+" of "+o+" operations failed",r)})},It);function It(){}function Tt(i){var u={},t=function(e,t){if(t){for(var n=arguments.length,r=new Array(n-1);--n;)r[n-1]=arguments[n];return u[e].subscribe.apply(null,r),i}if("string"==typeof e)return u[e]};t.addEventType=a;for(var e=1,n=arguments.length;es+c&&f(s+g)})})};return f(0).then(function(){if(0=l}).forEach(function(s){t.push(function(){var t=p,e=s._cfg.dbschema;On(c,t,h),On(c,e,h),p=c._dbSchema=e;var n=xn(t,e);n.add.forEach(function(e){kn(h,e[0],e[1].primKey,e[1].indexes)}),n.change.forEach(function(e){if(e.recreate)throw new te.Upgrade("Not yet support for changing primary key");var t=h.objectStore(e.name);e.add.forEach(function(e){return Pn(t,e)}),e.change.forEach(function(e){t.deleteIndex(e.name),Pn(t,e)}),e.del.forEach(function(e){return t.deleteIndex(e)})});var r=s._cfg.contentUpgrade;if(r&&s._cfg.version>l){mn(c,h),f._memoizedTables={},d=!0;var i=E(e);n.del.forEach(function(e){i[e]=t[e]}),bn(c,[c.Transaction.prototype]),gn(c,[c.Transaction.prototype],_(i),i),f.schema=i;var o,u=M(r);u&&it();var a=Fe.follow(function(){var e;(o=r(f))&&u&&(e=ot.bind(null,null),o.then(e,e))});return o&&"function"==typeof o.then?Fe.resolve(o):a.then(function(){return o})}}),t.push(function(e){d&&kt||function(e,t){for(var n=0;nMath.pow(2,62)?0:e.oldVersion,c.idbdb=s.result,wn(c,n/10,f,r))},r),s.onsuccess=Xe(function(){f=null;var e,t,n,r=c.idbdb=s.result,i=p(r.objectStoreNames);if(00&&i[i.length-1])&&(6===o[0]||2===o[0])){s=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]2&&void 0!==arguments[2]?arguments[2]:{};return n.returnTypeNames=!0,this.encapsulate(e,t,n)},this.rootTypeName=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.iterateNone=!0,this.encapsulate(e,t,n)};var A=this.encapsulate=function(f,l,v){var g=(v=Object.assign({sync:!0},o,v)).sync,m={},w=[],E=[],A=[],S=!(v&&"cyclic"in v)||v.cyclic,T=v.encapsulateObserver,N=B("",f,S,l||{},A);function j(e){var t=Object.values(m);if(v.iterateNone)return t.length?t[0]:y.getJSONType(e);if(t.length){if(v.returnTypeNames)return[].concat(n(new Set(t)));e&&p(e)&&!e.hasOwnProperty("$types")?e.$types=m:e={$:e,$types:{$:m}}}else d(e)&&e.hasOwnProperty("$types")&&(e={$:e,$types:!0});return!v.returnTypeNames&&e}return A.length?g&&v.throwOnBadSyncType?function(){throw new TypeError("Sync method requested but async result obtained")}():Promise.resolve(function e(n,r){return Promise.all(r.map(function(e){return e[1].p})).then(function(i){return Promise.all(i.map(function(i){var o=[],s=r.splice(0,1)[0],a=t(s,7),u=a[0],c=a[2],f=a[3],l=a[4],p=a[5],d=a[6],y=B(u,i,c,f,o,!0,d),v=h(y,O);return u&&v?y.p.then(function(t){return l[p]=t,e(n,o)}):(u?l[p]=y:n=v?y.p:y,e(n,o))}))}).then(function(){return n})}(N,A)).then(j):!g&&v.throwOnBadSyncType?function(){throw new TypeError("Async method requested but sync result obtained")}():v.stringification&&g?[j(N)]:g?j(N):Promise.resolve(j(N));function _(e,t,n){Object.assign(e,t);var r=c.map(function(t){var n=e[t];return delete e[t],n});n(),c.forEach(function(t,n){e[t]=r[n]})}function B(t,n,o,a,u,c,f){var l=void 0,d={},g=void 0===n?"undefined":e(n),A=T?function(e){var r=f||a.type||y.getJSONType(n);T(Object.assign(e||d,{keypath:t,value:n,cyclic:o,stateObj:a,promisesData:u,resolvingTypesonPromise:c,awaitingTypesonPromise:h(n,O)},void 0!==r?{type:r}:{}))}:null;if(g in{string:1,boolean:1,number:1,undefined:1})return void 0===n||"number"===g&&(isNaN(n)||n===-1/0||n===1/0)?(l=x(t,n,a,u,!1,c,A))!==n&&(d={replaced:l}):l=n,A&&A(),l;if(null===n)return A&&A(),n;if(o&&!a.iterateIn&&!a.iterateUnsetNumeric){var S=w.indexOf(n);if(!(S<0))return m[t]="#",A&&A({cyclicKeypath:E[S]}),"#"+E[S];!0===o&&(w.push(n),E.push(t))}var N=p(n),j=i(n),C=(N||j)&&(!s.length||a.replaced)||a.iterateIn?n:x(t,n,a,u,N||j,null,A),U=void 0;if(C!==n?(l=C,d={replaced:C}):j||"array"===a.iterateIn?(U=new Array(n.length),d={clone:U}):N||"object"===a.iterateIn?d={clone:U={}}:""===t&&h(n,O)?(u.push([t,n,o,a,void 0,void 0,a.type]),l=n):l=n,A&&A(),v.iterateNone)return U||l;if(!U)return l;if(a.iterateIn){var I=function(e){var r={ownKeys:n.hasOwnProperty(e)};_(a,r,function(){var r=t+(t?".":"")+b(e),i=B(r,n[e],!!o,a,u,c);h(i,O)?u.push([r,i,!!o,a,U,e,a.type]):void 0!==i&&(U[e]=i)})};for(var L in n)I(L);A&&A({endIterateIn:!0,end:!0})}else r(n).forEach(function(e){var r=t+(t?".":"")+b(e);_(a,{ownKeys:!0},function(){var t=B(r,n[e],!!o,a,u,c);h(t,O)?u.push([r,t,!!o,a,U,e,a.type]):void 0!==t&&(U[e]=t)})}),A&&A({endIterateOwn:!0,end:!0});if(a.iterateUnsetNumeric){for(var P=n.length,k=function(e){if(!(e in n)){var r=t+(t?".":"")+e;_(a,{ownKeys:!1},function(){var t=B(r,void 0,!!o,a,u,c);h(t,O)?u.push([r,t,!!o,a,U,e,a.type]):void 0!==t&&(U[e]=t)})}},R=0;R-1){var r=e[g(t.substr(0,n))];return void 0===r?void 0:m(r,t.substr(n+1))}return e[g(t)]}function w(){}function O(e){this.p=new Promise(e)}return O.prototype.then=function(e,t){var n=this;return new O(function(r,i){n.p.then(function(t){r(e?e(t):t)},function(e){n.p.catch(function(e){return t?t(e):Promise.reject(e)}).then(r,i)})})},O.prototype.catch=function(e){return this.then(null,e)},O.resolve=function(e){return new O(function(t){t(e)})},O.reject=function(e){return new O(function(t,n){n(e)})},["all","race"].map(function(e){O[e]=function(t){return new O(function(n,r){Promise[e](t.map(function(e){return e.p})).then(n,r)})}}),y.Undefined=w,y.Promise=O,y.isThenable=f,y.toStringTag=l,y.hasConstructorOf=h,y.isObject=d,y.isPlainObject=p,y.isUserObject=function e(t){if(!t||"Object"!==l(t))return!1;var n=s(t);return!n||h(t,Object)||e(n)},y.escapeKeyPathComponent=b,y.unescapeKeyPathComponent=g,y.getByKeyPath=m,y.getJSONType=function(t){return null===t?"null":i(t)?"array":void 0===t?"undefined":e(t)},y.JSON_TYPES=["null","boolean","number","string","array","object"],y}()}),f=u(function(e,t){e.exports=function(){var e="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},t=function(e,t){if(Array.isArray(e))return e;if(Symbol.iterator in Object(e))return function(e,t){var n=[],r=!0,i=!1,o=void 0;try{for(var s,a=e[Symbol.iterator]();!(r=(s=a.next()).done)&&(n.push(s.value),!t||n.length!==t);r=!0);}catch(e){i=!0,o=e}finally{try{!r&&a.return&&a.return()}finally{if(i)throw o}}return n}(e,t);throw new TypeError("Invalid attempt to destructure non-iterable instance")},n=function(e){if(Array.isArray(e)){for(var t=0,n=Array(e.length);t2&&void 0!==arguments[2]?arguments[2]:{};return n.returnTypeNames=!0,this.encapsulate(e,t,n)},this.rootTypeName=function(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return n.iterateNone=!0,this.encapsulate(e,t,n)};var A=this.encapsulate=function(c,l,h){var b=(h=Object.assign({sync:!0},o,h)).sync,m={},w=[],O=[],A=[],S=!(h&&"cyclic"in h)||h.cyclic,T=h.encapsulateObserver,N=B("",c,S,l||{},A);function j(e){var t=Object.values(m);if(h.iterateNone)return t.length?t[0]:v.getJSONType(e);if(t.length){if(h.returnTypeNames)return[].concat(n(new Set(t)));e&&d(e)&&!e.hasOwnProperty("$types")?e.$types=m:e={$:e,$types:{$:m}}}else y(e)&&e.hasOwnProperty("$types")&&(e={$:e,$types:!0});return!h.returnTypeNames&&e}return A.length?b&&h.throwOnBadSyncType?function(){throw new TypeError("Sync method requested but async result obtained")}():Promise.resolve(function e(n,r){return Promise.all(r.map(function(e){return e[1].p})).then(function(i){return Promise.all(i.map(function(i){var o=[],s=r.splice(0,1)[0],a=t(s,7),u=a[0],c=a[2],f=a[3],l=a[4],h=a[5],d=a[6],y=B(u,i,c,f,o,!0,d),v=p(y,E);return u&&v?y.p.then(function(t){return l[h]=t,e(n,o)}):(u?l[h]=y:n=v?y.p:y,e(n,o))}))}).then(function(){return n})}(N,A)).then(j):!b&&h.throwOnBadSyncType?function(){throw new TypeError("Async method requested but sync result obtained")}():h.stringification&&b?[j(N)]:b?j(N):Promise.resolve(j(N));function _(e,t,n){Object.assign(e,t);var r=f.map(function(t){var n=e[t];return delete e[t],n});n(),f.forEach(function(t,n){e[t]=r[n]})}function B(t,n,o,a,u,c,f){var l=void 0,y={},b=void 0===n?"undefined":e(n),A=T?function(e){var r=f||a.type||v.getJSONType(n);T(Object.assign(e||y,{keypath:t,value:n,cyclic:o,stateObj:a,promisesData:u,resolvingTypesonPromise:c,awaitingTypesonPromise:p(n,E)},void 0!==r?{type:r}:{}))}:null;if(b in{string:1,boolean:1,number:1,undefined:1})return void 0===n||"number"===b&&(isNaN(n)||n===-1/0||n===1/0)?(l=x(t,n,a,u,!1,c,A))!==n&&(y={replaced:l}):l=n,A&&A(),l;if(null===n)return A&&A(),n;if(o&&!a.iterateIn&&!a.iterateUnsetNumeric){var S=w.indexOf(n);if(!(S<0))return m[t]="#",A&&A({cyclicKeypath:O[S]}),"#"+O[S];!0===o&&(w.push(n),O.push(t))}var N=d(n),j=i(n),C=(N||j)&&(!s.length||a.replaced)||a.iterateIn?n:x(t,n,a,u,N||j,null,A),U=void 0;if(C!==n?(l=C,y={replaced:C}):j||"array"===a.iterateIn?(U=new Array(n.length),y={clone:U}):N||"object"===a.iterateIn?y={clone:U={}}:""===t&&p(n,E)?(u.push([t,n,o,a,void 0,void 0,a.type]),l=n):l=n,A&&A(),h.iterateNone)return U||l;if(!U)return l;if(a.iterateIn){var I=function(e){var r={ownKeys:n.hasOwnProperty(e)};_(a,r,function(){var r=t+(t?".":"")+g(e),i=B(r,n[e],!!o,a,u,c);p(i,E)?u.push([r,i,!!o,a,U,e,a.type]):void 0!==i&&(U[e]=i)})};for(var L in n)I(L);A&&A({endIterateIn:!0,end:!0})}else r(n).forEach(function(e){var r=t+(t?".":"")+g(e);_(a,{ownKeys:!0},function(){var t=B(r,n[e],!!o,a,u,c);p(t,E)?u.push([r,t,!!o,a,U,e,a.type]):void 0!==t&&(U[e]=t)})}),A&&A({endIterateOwn:!0,end:!0});if(a.iterateUnsetNumeric){for(var P=n.length,k=function(e){if(!(e in n)){var r=t+(t?".":"")+e;_(a,{ownKeys:!1},function(){var t=B(r,void 0,!!o,a,u,c);p(t,E)?u.push([r,t,!!o,a,U,e,a.type]):void 0!==t&&(U[e]=t)})}},R=0;R-1){var r=e[m(t.substr(0,n))];return void 0===r?void 0:w(r,t.substr(n+1))}return e[m(t)]}function O(){}function E(e){this.p=new Promise(e)}E.prototype.then=function(e,t){var n=this;return new E(function(r,i){n.p.then(function(t){r(e?e(t):t)},function(e){n.p.catch(function(e){return t?t(e):Promise.reject(e)}).then(r,i)})})},E.prototype.catch=function(e){return this.then(null,e)},E.resolve=function(e){return new E(function(t){t(e)})},E.reject=function(e){return new E(function(t,n){n(e)})},["all","race"].map(function(e){E[e]=function(t){return new E(function(n,r){Promise[e](t.map(function(e){return e.p})).then(n,r)})}}),v.Undefined=O,v.Promise=E,v.isThenable=l,v.toStringTag=h,v.hasConstructorOf=p,v.isObject=y,v.isPlainObject=d,v.isUserObject=function e(t){if(!t||"Object"!==h(t))return!1;var n=s(t);return!n||p(t,Object)||e(n)},v.escapeKeyPathComponent=g,v.unescapeKeyPathComponent=m,v.getByKeyPath=w,v.getJSONType=function(t){return null===t?"null":i(t)?"array":void 0===t?"undefined":e(t)},v.JSON_TYPES=["null","boolean","number","string","array","object"];for(var A={userObject:{test:function(e,t){return v.isUserObject(e)},replace:function(e){return Object.assign({},e)},revive:function(e){return e}}},S=[[{sparseArrays:{testPlainObjects:!0,test:function(e){return Array.isArray(e)},replace:function(e,t){return t.iterateUnsetNumeric=!0,e}}},{sparseUndefined:{test:function(e,t){return void 0===e&&!1===t.ownKeys},replace:function(e){return null},revive:function(e){}}}],{undef:{test:function(e,t){return void 0===e&&(t.ownKeys||!("ownKeys"in t))},replace:function(e){return null},revive:function(e){return new v.Undefined}}}],T={StringObject:{test:function(t){return"String"===v.toStringTag(t)&&"object"===(void 0===t?"undefined":e(t))},replace:function(e){return String(e)},revive:function(e){return new String(e)}},BooleanObject:{test:function(t){return"Boolean"===v.toStringTag(t)&&"object"===(void 0===t?"undefined":e(t))},replace:function(e){return Boolean(e)},revive:function(e){return new Boolean(e)}},NumberObject:{test:function(t){return"Number"===v.toStringTag(t)&&"object"===(void 0===t?"undefined":e(t))},replace:function(e){return Number(e)},revive:function(e){return new Number(e)}}},N=[{nan:{test:function(e){return"number"==typeof e&&isNaN(e)},replace:function(e){return"NaN"},revive:function(e){return NaN}}},{infinity:{test:function(e){return e===1/0},replace:function(e){return"Infinity"},revive:function(e){return 1/0}}},{negativeInfinity:{test:function(e){return e===-1/0},replace:function(e){return"-Infinity"},revive:function(e){return-1/0}}}],j={date:{test:function(e){return"Date"===v.toStringTag(e)},replace:function(e){var t=e.getTime();return isNaN(t)?"NaN":t},revive:function(e){return"NaN"===e?new Date(NaN):new Date(e)}}},_={regexp:{test:function(e){return"RegExp"===v.toStringTag(e)},replace:function(e){return{source:e.source,flags:(e.global?"g":"")+(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.sticky?"y":"")+(e.unicode?"u":"")}},revive:function(e){var t=e.source,n=e.flags;return new RegExp(t,n)}}},B={map:{test:function(e){return"Map"===v.toStringTag(e)},replace:function(e){return Array.from(e.entries())},revive:function(e){return new Map(e)}}},x={set:{test:function(e){return"Set"===v.toStringTag(e)},replace:function(e){return Array.from(e.values())},revive:function(e){return new Set(e)}}},C="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",U=new Uint8Array(256),I=0;I>2],o+=C[(3&r[s])<<4|r[s+1]>>4],o+=C[(15&r[s+1])<<2|r[s+2]>>6],o+=C[63&r[s+2]];return i%3==2?o=o.substring(0,o.length-1)+"=":i%3==1&&(o=o.substring(0,o.length-2)+"=="),o},P=function(e){var t=e.length,n=.75*e.length,r=0,i=void 0,o=void 0,s=void 0,a=void 0;"="===e[e.length-1]&&(n--,"="===e[e.length-2]&&n--);for(var u=new ArrayBuffer(n),c=new Uint8Array(u),f=0;f>4,c[r++]=(15&o)<<4|s>>2,c[r++]=(3&s)<<6|63&a;return u},k={arraybuffer:{test:function(e){return"ArrayBuffer"===v.toStringTag(e)},replace:function(e,t){t.buffers||(t.buffers=[]);var n=t.buffers.indexOf(e);return n>-1?{index:n}:(t.buffers.push(e),L(e))},revive:function(t,n){if(n.buffers||(n.buffers=[]),"object"===(void 0===t?"undefined":e(t)))return n.buffers[t.index];var r=P(t);return n.buffers.push(r),r}}},R="undefined"==typeof self?a:self,F={};["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array"].forEach(function(e){var t=e,n=R[t];n&&(F[e.toLowerCase()]={test:function(e){return v.toStringTag(e)===t},replace:function(e,t){var n=e.buffer,r=e.byteOffset,i=e.length;t.buffers||(t.buffers=[]);var o=t.buffers.indexOf(n);return o>-1?{index:o,byteOffset:r,length:i}:(t.buffers.push(n),{encoded:L(n),byteOffset:r,length:i})},revive:function(e,t){t.buffers||(t.buffers=[]);var r=e.byteOffset,i=e.length,o=e.encoded,s=e.index,a=void 0;return"index"in e?a=t.buffers[s]:(a=P(o),t.buffers.push(a)),new n(a,r,i)}})});var D={dataview:{test:function(e){return"DataView"===v.toStringTag(e)},replace:function(e,t){var n=e.buffer,r=e.byteOffset,i=e.byteLength;t.buffers||(t.buffers=[]);var o=t.buffers.indexOf(n);return o>-1?{index:o,byteOffset:r,byteLength:i}:(t.buffers.push(n),{encoded:L(n),byteOffset:r,byteLength:i})},revive:function(e,t){t.buffers||(t.buffers=[]);var n=e.byteOffset,r=e.byteLength,i=e.encoded,o=e.index,s=void 0;return"index"in e?s=t.buffers[o]:(s=P(i),t.buffers.push(s)),new DataView(s,n,r)}}},J={IntlCollator:{test:function(e){return v.hasConstructorOf(e,Intl.Collator)},replace:function(e){return e.resolvedOptions()},revive:function(e){return new Intl.Collator(e.locale,e)}},IntlDateTimeFormat:{test:function(e){return v.hasConstructorOf(e,Intl.DateTimeFormat)},replace:function(e){return e.resolvedOptions()},revive:function(e){return new Intl.DateTimeFormat(e.locale,e)}},IntlNumberFormat:{test:function(e){return v.hasConstructorOf(e,Intl.NumberFormat)},replace:function(e){return e.resolvedOptions()},revive:function(e){return new Intl.NumberFormat(e.locale,e)}}};function M(e){for(var t=new Uint16Array(e.length),n=0;n>2],o+=l[(3&r[s])<<4|r[s+1]>>4],o+=l[(15&r[s+1])<<2|r[s+2]>>6],o+=l[63&r[s+2]];return i%3==2?o=o.substring(0,o.length-1)+"=":i%3==1&&(o=o.substring(0,o.length-2)+"=="),o},y=function(e){var t,n,r,i,o=e.length,s=.75*e.length,a=0;"="===e[e.length-1]&&(s--,"="===e[e.length-2]&&s--);for(var u=new ArrayBuffer(s),c=new Uint8Array(u),f=0;f>4,c[a++]=(15&n)<<4|r>>2,c[a++]=(3&r)<<6|63&i;return u},v="undefined"==typeof self?global:self,b={};["Int8Array","Uint8Array","Uint8ClampedArray","Int16Array","Uint16Array","Int32Array","Uint32Array","Float32Array","Float64Array"].forEach(function(e){var t=e,n=v[t];n&&(b[e.toLowerCase()+"2"]={test:function(e){return c.toStringTag(e)===t},replace:function(e){return{buffer:e.buffer,byteOffset:e.byteOffset,length:e.length}},revive:function(e){var t=e.buffer,r=e.byteOffset,i=e.length;return new n(t,r,i)}})});var g={arraybuffer:{test:function(e){return"ArrayBuffer"===c.toStringTag(e)},replace:function(e){return d(e,0,e.byteLength)},revive:function(e){return y(e)}}},m=(new c).register(f),w="FileReaderSync"in self,O=[],E=0;m.register([g,b,{blob2:{test:function(e){return"Blob"===c.toStringTag(e)},replace:function(e){if(e.isClosed)throw new Error("The Blob is closed");if(w){var t=s(e,"binary"),n=d(t,0,t.byteLength);return{type:e.type,data:n}}O.push(e);var r={type:e.type,data:{start:E,end:E+e.size}};return console.log("b.size: "+e.size),E+=e.size,r},finalize:function(e,t){e.data=d(t,0,t.byteLength)},revive:function(e){var t=e.type,n=e.data;return new Blob([y(n)],{type:t})}}}]),m.mustFinalize=function(){return O.length>0},m.finalize=function(e){return n(void 0,void 0,void 0,function(){var n,i,s,a,u,c,f,l,h,p;return r(this,function(r){switch(r.label){case 0:return[4,o(new Blob(O),"binary")];case 1:if(n=r.sent(),e)for(i=0,s=e;i0&&(a.push(","),c&&a.push("\n ")),w=o.length===f,u?(p=d?o.filter(function(e){return d(n,e)}):o,y=p.map(function(e){return m.encapsulate(e)}),m.mustFinalize()?[4,t.waitFor(m.finalize(y))]:[3,3]):[3,4]);case 2:r.sent(),r.label=3;case 3:return A=JSON.stringify(y,void 0,c?2:void 0),c&&(A=A.split("\n").join("\n ")),a.push(new Blob([A.substring(1,A.length-1)])),g=p.length,b=o.length>0?t.getByKeyPath(o[o.length-1],s.keyPath):null,[3,8];case 4:return[4,e.primaryKeys()];case 5:return v=r.sent(),O=v.map(function(e,t){return[e,o[t]]}),d&&(O=O.filter(function(e){var t=e[0],r=e[1];return d(n,r,t)})),E=O.map(function(e){return m.encapsulate(e)}),m.mustFinalize()?[4,t.waitFor(m.finalize(E))]:[3,7];case 6:r.sent(),r.label=7;case 7:A=JSON.stringify(E,void 0,c?2:void 0),c&&(A=A.split("\n").join("\n ")),a.push(new Blob([A.substring(1,A.length-1)])),g=O.length,b=v.length>0?v[v.length-1]:null,r.label=8;case 8:return h.completedRows+=o.length,[2]}})},E.label=1;case 1:return w?[5,O()]:[3,3];case 2:return"break"===E.sent()?[3,3]:[3,1];case 3:return a.push(y.substr(v)),h.completedTables+=1,h.completedTables ["+t+"]");for(;i&&(s=i,this.c=i=t.charCodeAt(n++),s!==i?this.p=s:s=this.p,i);)switch(e.DEBUG&&console.log(n,i,e.STATE[this.state]),this.position++,i===a.lineFeed?(this.line++,this.column=0):this.column++,this.state){case o.BEGIN:i===a.openBrace?this.state=o.OPEN_OBJECT:i===a.openBracket?this.state=o.OPEN_ARRAY:g(i)||v(this,"Non-whitespace before {[.");continue;case o.OPEN_KEY:case o.OPEN_OBJECT:if(g(i))continue;if(this.state===o.OPEN_KEY)this.stack.push(o.CLOSE_KEY);else{if(i===a.closeBrace){h(this,"onopenobject"),this.depth++,h(this,"oncloseobject"),this.depth--,this.state=this.stack.pop()||o.VALUE;continue}this.stack.push(o.CLOSE_OBJECT)}i===a.doubleQuote?this.state=o.STRING:v(this,'Malformed object key should start with "');continue;case o.CLOSE_KEY:case o.CLOSE_OBJECT:if(g(i))continue;this.state,o.CLOSE_KEY;i===a.colon?(this.state===o.CLOSE_OBJECT?(this.stack.push(o.CLOSE_OBJECT),d(this,"onopenobject"),this.depth++):d(this,"onkey"),this.state=o.VALUE):i===a.closeBrace?(p(this,"oncloseobject"),this.depth--,this.state=this.stack.pop()||o.VALUE):i===a.comma?(this.state===o.CLOSE_OBJECT&&this.stack.push(o.CLOSE_OBJECT),d(this),this.state=o.OPEN_KEY):v(this,"Bad object");continue;case o.OPEN_ARRAY:case o.VALUE:if(g(i))continue;if(this.state===o.OPEN_ARRAY){if(h(this,"onopenarray"),this.depth++,this.state=o.VALUE,i===a.closeBracket){h(this,"onclosearray"),this.depth--,this.state=this.stack.pop()||o.VALUE;continue}this.stack.push(o.CLOSE_ARRAY)}i===a.doubleQuote?this.state=o.STRING:i===a.openBrace?this.state=o.OPEN_OBJECT:i===a.openBracket?this.state=o.OPEN_ARRAY:i===a.t?this.state=o.TRUE:i===a.f?this.state=o.FALSE:i===a.n?this.state=o.NULL:i===a.minus?this.numberNode+="-":a._0<=i&&i<=a._9?(this.numberNode+=String.fromCharCode(i),this.state=o.NUMBER_DIGIT):v(this,"Bad value");continue;case o.CLOSE_ARRAY:if(i===a.comma)this.stack.push(o.CLOSE_ARRAY),d(this,"onvalue"),this.state=o.VALUE;else if(i===a.closeBracket)p(this,"onclosearray"),this.depth--,this.state=this.stack.pop()||o.VALUE;else{if(g(i))continue;v(this,"Bad array")}continue;case o.STRING:void 0===this.textNode&&(this.textNode="");var u=n-1,f=this.slashed,l=this.unicodeI;e:for(;;){for(e.DEBUG&&console.log(n,i,e.STATE[this.state],f);l>0;)if(this.unicodeS+=String.fromCharCode(i),i=t.charCodeAt(n++),this.position++,4===l?(this.textNode+=String.fromCharCode(parseInt(this.unicodeS,16)),l=0,u=n-1):l++,!i)break e;if(i===a.doubleQuote&&!f){this.state=this.stack.pop()||o.VALUE,this.textNode+=t.substring(u,n-1),this.position+=n-1-u;break}if(i===a.backslash&&!f&&(f=!0,this.textNode+=t.substring(u,n-1),this.position+=n-1-u,i=t.charCodeAt(n++),this.position++,!i))break;if(f){if(f=!1,i===a.n?this.textNode+="\n":i===a.r?this.textNode+="\r":i===a.t?this.textNode+="\t":i===a.f?this.textNode+="\f":i===a.b?this.textNode+="\b":i===a.u?(l=1,this.unicodeS=""):this.textNode+=String.fromCharCode(i),i=t.charCodeAt(n++),this.position++,u=n-1,i)continue;break}c.lastIndex=n;var m=c.exec(t);if(null===m){n=t.length+1,this.textNode+=t.substring(u,n-1),this.position+=n-1-u;break}if(n=m.index+1,!(i=t.charCodeAt(m.index))){this.textNode+=t.substring(u,n-1),this.position+=n-1-u;break}}this.slashed=f,this.unicodeI=l;continue;case o.TRUE:i===a.r?this.state=o.TRUE2:v(this,"Invalid true started with t"+i);continue;case o.TRUE2:i===a.u?this.state=o.TRUE3:v(this,"Invalid true started with tr"+i);continue;case o.TRUE3:i===a.e?(h(this,"onvalue",!0),this.state=this.stack.pop()||o.VALUE):v(this,"Invalid true started with tru"+i);continue;case o.FALSE:i===a.a?this.state=o.FALSE2:v(this,"Invalid false started with f"+i);continue;case o.FALSE2:i===a.l?this.state=o.FALSE3:v(this,"Invalid false started with fa"+i);continue;case o.FALSE3:i===a.s?this.state=o.FALSE4:v(this,"Invalid false started with fal"+i);continue;case o.FALSE4:i===a.e?(h(this,"onvalue",!1),this.state=this.stack.pop()||o.VALUE):v(this,"Invalid false started with fals"+i);continue;case o.NULL:i===a.u?this.state=o.NULL2:v(this,"Invalid null started with n"+i);continue;case o.NULL2:i===a.l?this.state=o.NULL3:v(this,"Invalid null started with nu"+i);continue;case o.NULL3:i===a.l?(h(this,"onvalue",null),this.state=this.stack.pop()||o.VALUE):v(this,"Invalid null started with nul"+i);continue;case o.NUMBER_DECIMAL_POINT:i===a.period?(this.numberNode+=".",this.state=o.NUMBER_DIGIT):v(this,"Leading zero not followed by .");continue;case o.NUMBER_DIGIT:a._0<=i&&i<=a._9?this.numberNode+=String.fromCharCode(i):i===a.period?(-1!==this.numberNode.indexOf(".")&&v(this,"Invalid number has two dots"),this.numberNode+="."):i===a.e||i===a.E?(-1===this.numberNode.indexOf("e")&&-1===this.numberNode.indexOf("E")||v(this,"Invalid number has two exponential"),this.numberNode+="e"):i===a.plus||i===a.minus?(s!==a.e&&s!==a.E&&v(this,"Invalid symbol in number"),this.numberNode+=String.fromCharCode(i)):(y(this),n--,this.state=this.stack.pop()||o.VALUE);continue;default:v(this,"Unknown state: "+this.state)}this.position>=this.bufferCheckPosition&&function(t){var n=Math.max(e.MAX_BUFFER_LENGTH,10),i=0;for(var o in r){var s=void 0===t[o]?0:t[o].length;if(s>n)switch(o){case"text":closeText(t);break;default:v(t,"Max buffer length exceeded: "+o)}i=Math.max(i,s)}t.bufferCheckPosition=e.MAX_BUFFER_LENGTH-i+t.position}(this);return this},resume:function(){return this.error=null,this},close:function(){return this.write(null)}};try{n=N}catch(e){n=function(){}}function l(e){if(!(this instanceof l))return new l(e);this._parser=new f(e),this.writable=!0,this.readable=!0,this.bytes_remaining=0,this.bytes_in_sequence=0,this.temp_buffs={2:new Buffer(2),3:new Buffer(3),4:new Buffer(4)},this.string="";var t=this;n.apply(t),this._parser.onend=function(){t.emit("end")},this._parser.onerror=function(e){t.emit("error",e),t._parser.error=null},i.forEach(function(e){Object.defineProperty(t,"on"+e,{get:function(){return t._parser["on"+e]},set:function(n){if(!n)return t.removeAllListeners(e),t._parser["on"+e]=n,n;t.on(e,n)},enumerable:!0,configurable:!1})})}function h(t,n,r){e.INFO&&console.log("-- emit",n,r),t[n]&&t[n](r)}function p(e,t,n){d(e),h(e,t,n)}function d(e,t){e.textNode=function(e,t){if(void 0===t)return t;e.trim&&(t=t.trim());e.normalize&&(t=t.replace(/\s+/g," "));return t}(e.opt,e.textNode),void 0!==e.textNode&&h(e,t||"onvalue",e.textNode),e.textNode=void 0}function y(e){e.numberNode&&h(e,"onvalue",parseFloat(e.numberNode)),e.numberNode=""}function v(e,t){return d(e),t+="\nLine: "+e.line+"\nColumn: "+e.column+"\nChar: "+e.c,t=new Error(t),e.error=t,h(e,"onerror",t),e}function b(e){return e.state===o.VALUE&&0===e.depth||v(e,"Unexpected end"),d(e),e.c="",e.closed=!0,h(e,"onend"),f.call(e,e.opt),e}function g(e){return e===a.carriageReturn||e===a.lineFeed||e===a.space||e===a.tab}l.prototype=Object.create(n.prototype,{constructor:{value:l}}),l.prototype.write=function(e){e=new Buffer(e);for(var t=0;t0){for(var r=0;r=128){if(n>=194&&n<=223&&(this.bytes_in_sequence=2),n>=224&&n<=239&&(this.bytes_in_sequence=3),n>=240&&n<=244&&(this.bytes_in_sequence=4),this.bytes_in_sequence+t>e.length){for(var i=0;i<=e.length-1-t;i++)this.temp_buffs[this.bytes_in_sequence][i]=e[t+i];return this.bytes_remaining=t+this.bytes_in_sequence-e.length,!0}this.string=e.slice(t,t+this.bytes_in_sequence).toString(),t=t+this.bytes_in_sequence-1,this._parser.write(this.string),this.emit("data",this.string)}else{for(var o=t;o=128);o++);this.string=e.slice(t,o).toString(),this._parser.write(this.string),this.emit("data",this.string),t=o-1}}},l.prototype.end=function(e){return e&&e.length&&this._parser.write(e.toString()),this._parser.end(),!0},l.prototype.on=function(e,t){var r=this;return r._parser["on"+e]||-1===i.indexOf(e)||(r._parser["on"+e]=function(){var t=1===arguments.length?[arguments[0]]:Array.apply(null,arguments);t.splice(0,0,e),r.emit.apply(r,t)}),n.prototype.on.call(r,e,t)},l.prototype.destroy=function(){u(this._parser),this.emit("close")}}(t)});function _(e){var t=0,i=function(e){var t,n,r,i=j.parser(),o=0,s=[],a=!1,u=!1;return i.onopenobject=function(i){var a={incomplete:!0};t||(t=a),n&&(s.push([r,n,u]),e&&(u?n.push(a):n[r]=a)),n=a,r=i,u=!1,++o},i.onkey=function(e){return r=e},i.onvalue=function(e){return u?n.push(e):n[r]=e},i.oncloseobject=function(){var t;if(delete n.incomplete,r=null,0==--o)a=!0;else{var i=n;t=s.pop(),r=t[0],n=t[1],u=t[2],e||(u?n.push(i):n[r]=i)}},i.onopenarray=function(){var i=[];i.incomplete=!0,t||(t=i),n&&(s.push([r,n,u]),e&&(u?n.push(i):n[r]=i)),n=i,u=!0,r=null,++o},i.onclosearray=function(){var t;if(delete n.incomplete,r=null,0==--o)a=!0;else{var i=n;t=s.pop(),r=t[0],n=t[1],u=t[2],e||(u?n.push(i):n[r]=i)}},{write:function(e){return i.write(e),t},done:function(){return a}}}(!0),a={pullAsync:function(s){return n(this,void 0,void 0,function(){var n,u,c;return r(this,function(r){switch(r.label){case 0:return n=e.slice(t,t+s),t+=s,[4,o(n,"text")];case 1:return u=r.sent(),c=i.write(u),a.result=c||{},[2,c]}})})},pullSync:function(n){var r=e.slice(t,t+n);t+=n;var o=s(r,"text"),u=i.write(o);return a.result=u||{},u},done:function(){return i.done()},eof:function(){return t>=e.size},result:{}};return a}var B=1024;function x(e,i){return n(this,void 0,void 0,function(){var n,o,s,a;return r(this,function(r){switch(r.label){case 0:return n=(i=i||{}).chunkSizeBytes||1024*B,[4,U(e,n)];case 1:return o=r.sent(),s=o.result.data,(a=new t(s.databaseName)).version(s.databaseVersion).stores(function(e){for(var t={},n=0,r=e.tables;n0&&l.data[0].rows&&!l.data[0].rows.incomplete;)l.data.splice(0,1);return u.done()||u.eof()?[3,8]:f?(u.pullSync(a),[3,7]):[3,5];case 5:return[4,t.waitFor(u.pullAsync(a))];case 6:d.sent(),d.label=7;case 7:return[3,9];case 8:return[3,10];case 9:return[3,0];case 10:return[2]}})})}var a,u,c,f,l,h,p;return r(this,function(n){switch(n.label){case 0:return a=(o=o||{}).chunkSizeBytes||1024*B,[4,U(i,a)];case 1:if(u=n.sent(),c=u.result,f="FileReaderSync"in self,l=c.data,!o.acceptNameDiff&&e.name!==l.databaseName)throw new Error("Name differs. Current database name is "+e.name+" but export is "+l.databaseName);if(!o.acceptVersionDiff&&e.verno!==l.databaseVersion)throw new Error("Database version differs. Current database is in version "+e.verno+" but export is "+l.databaseVersion);return h=o.progressCallback,p={done:!1,completedRows:0,completedTables:0,totalRows:l.tables.reduce(function(e,t){return e+t.rowCount},0),totalTables:l.tables.length},h&&t.ignoreTransaction(function(){return h(p)}),o.noTransaction?[4,s()]:[3,3];case 2:return n.sent(),[3,5];case 3:return[4,e.transaction("rw",e.tables,s)];case 4:n.sent(),n.label=5;case 5:return p.done=!0,h&&t.ignoreTransaction(function(){return h(p)}),[2]}})})}function U(e,t){return n(this,void 0,void 0,function(){var n,i;return r(this,function(r){switch(r.label){case 0:n="slice"in e?_(e):e,r.label=1;case 1:return n.eof()?[3,3]:[4,n.pullAsync(t)];case 2:return r.sent(),n.result.data&&n.result.data.data?[3,3]:[3,1];case 3:if(!(i=n.result)||"dexie"!=i.formatName)throw new Error("Given file is not a dexie export");if(i.formatVersion>T)throw new Error("Format version "+i.formatVersion+" not supported");if(!i.data)throw new Error("No data in export file");if(!i.data.databaseName)throw new Error("Missing databaseName in export file");if(!i.data.databaseVersion)throw new Error("Missing databaseVersion in export file");if(!i.data.tables)throw new Error("Missing tables in export file");return[2,n]}})})}t.prototype.export=function(e){return S(this,e)},t.prototype.import=function(e,t){return C(this,e,t)},t.import=function(e,t){return x(e,t)};e.exportDB=S,e.importDB=x,e.importInto=C,e.peakImportFile=function(e){return n(this,void 0,void 0,function(){var t;return r(this,function(n){switch(n.label){case 0:t=_(e),n.label=1;case 1:return t.eof()?[3,3]:[4,t.pullAsync(5120)];case 2:return n.sent(),t.result.data&&t.result.data.data?(delete t.result.data.data,[3,3]):[3,1];case 3:return[2,t.result]}})})},e.default=function(){throw new Error("This addon extends Dexie.prototype globally and does not have be included in Dexie constructor's addons options.")},Object.defineProperty(e,"__esModule",{value:!0})}); 10 | `; 11 | -------------------------------------------------------------------------------- /src/exceptions.ts: -------------------------------------------------------------------------------- 1 | import { ZodError } from 'zod'; 2 | 3 | export class CorruptedSessionDataError extends Error { 4 | public readonly zodError: ZodError; 5 | public constructor(zodError: ZodError) { 6 | super( 7 | `Session data couldn't be parsed. See the embedded ZodError for additional informations.`, 8 | ); 9 | this.zodError = zodError; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Export public modules. 3 | * TODO: make sure that everything is exported 4 | */ 5 | export * from './classes/session-manager'; 6 | export * from './classes/storage-provider'; 7 | export * from './constants/constants'; 8 | export * from './exceptions'; 9 | export * from './plugin/injector'; 10 | export * from './plugin/plugin'; 11 | export * from './schemas'; 12 | export * from './session'; 13 | 14 | /** 15 | * Export plugin factory as default export. 16 | * @return {SessionPlugin} 17 | */ 18 | import { SessionPlugin } from './plugin/plugin'; 19 | 20 | export default (): SessionPlugin => new SessionPlugin(); 21 | -------------------------------------------------------------------------------- /src/plugin/injector.spec.ts: -------------------------------------------------------------------------------- 1 | import puppeteer, { Browser, Page } from 'puppeteer'; 2 | import { SessionManager } from '../classes/session-manager'; 3 | import { TestBrowserExecutablePath } from '../constants/constants'; 4 | import { inject } from './injector'; 5 | 6 | jest.setTimeout(10000); 7 | 8 | let browser: Browser; 9 | let page: Page; 10 | beforeAll(async () => { 11 | browser = await puppeteer.launch({ 12 | headless: true, 13 | executablePath: TestBrowserExecutablePath, 14 | args: ['--no-sandbox'], 15 | }); 16 | page = await browser.newPage(); 17 | }); 18 | 19 | afterAll(async () => { 20 | await browser?.close(); 21 | }); 22 | 23 | it('can inject the SessionManager', async () => { 24 | const injected = inject(page); 25 | expect(injected.session).toBeInstanceOf(SessionManager); 26 | }); 27 | -------------------------------------------------------------------------------- /src/plugin/injector.ts: -------------------------------------------------------------------------------- 1 | import { Page as VanillaPage } from 'puppeteer'; 2 | import { SessionManager } from '../classes/session-manager'; 3 | 4 | export interface ISessionPage extends VanillaPage { 5 | readonly session: SessionManager; 6 | } 7 | 8 | /** 9 | * Inject a new SessionPlugin instance into a Puppeteer page. 10 | * Makes self available on the `session` property. 11 | */ 12 | export function inject(page: VanillaPage): ISessionPage { 13 | if (page.session) { 14 | return page as ISessionPage; 15 | } 16 | return Object.defineProperty(page, 'session', { 17 | value: new SessionManager(page), 18 | writable: false, 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/plugin/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer-extra'; 2 | import { SessionManager } from '../classes/session-manager'; 3 | import { TestBrowserExecutablePath } from '../constants/constants'; 4 | import SessionPlugin from './plugin'; 5 | 6 | puppeteer.use(SessionPlugin()); 7 | 8 | jest.setTimeout(15000); 9 | 10 | it('injects the session manager when a page is created', async () => { 11 | const browser = await puppeteer.launch({ 12 | executablePath: TestBrowserExecutablePath, 13 | args: ['--no-sandbox'], 14 | }); 15 | const page = await browser.newPage(); 16 | expect(page).toHaveProperty('session'); 17 | expect(page.session).toBeInstanceOf(SessionManager); 18 | await browser.close(); 19 | }); 20 | -------------------------------------------------------------------------------- /src/plugin/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { PuppeteerExtraPlugin } from 'puppeteer-extra-plugin'; 3 | import { PLUGIN_NAME } from '../constants/constants'; 4 | import { ISessionPluginOptions } from '../types'; 5 | import { inject } from './injector'; 6 | 7 | // TODO: use documentation.js to generate documentation in README.md 8 | 9 | /** 10 | * Puppeteer Extra Session Plugin 11 | */ 12 | export class SessionPlugin extends PuppeteerExtraPlugin { 13 | /** 14 | * Constructor 15 | * Receives standard puppeteer-extra plugin config options. 16 | */ 17 | public constructor(opts: ISessionPluginOptions = {}) { 18 | super(opts); 19 | } 20 | 21 | /** 22 | * Describe the identifier for plugin. 23 | */ 24 | public get name(): string { 25 | return PLUGIN_NAME; 26 | } 27 | 28 | public async onPageCreated(page: Page): Promise { 29 | inject(page); 30 | } 31 | } 32 | 33 | /** 34 | * Export plugin factory as default export. 35 | */ 36 | export default (): SessionPlugin => new SessionPlugin(); 37 | -------------------------------------------------------------------------------- /src/providers/cookies.spec.ts: -------------------------------------------------------------------------------- 1 | import { Browser, Page } from 'puppeteer'; 2 | import puppeteer from 'puppeteer-extra'; 3 | import { TestBrowserExecutablePath } from '../constants/constants'; 4 | import { SessionPlugin } from '../plugin/plugin'; 5 | import { CDPCookie } from '../schemas'; 6 | 7 | let browser: Browser; 8 | let page: Page; 9 | puppeteer.use(new SessionPlugin()); 10 | beforeAll(async () => { 11 | browser = await puppeteer.launch({ 12 | headless: true, 13 | executablePath: TestBrowserExecutablePath, 14 | args: ['--no-sandbox'], 15 | }); 16 | page = await browser.newPage(); 17 | await page.goto('https://httpbin.org/ip'); 18 | }); 19 | 20 | afterAll(async () => { 21 | await browser?.close(); 22 | }); 23 | 24 | afterEach(async () => { 25 | // delete the "foo" cookie after each tests 26 | const session = await page.target().createCDPSession(); 27 | await session.send('Network.deleteCookies', { 28 | name: 'foo', 29 | domain: 'httpbin.org', 30 | }); 31 | await session.detach(); 32 | }); 33 | 34 | it('can get cookies', async () => { 35 | await page.evaluate(() => { 36 | function setCookie(name: string, value: string, days?: number): void { 37 | let expires: string; 38 | if (days) { 39 | const date = new Date(); 40 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); 41 | expires = '; expires=' + date.toString(); 42 | } else expires = ''; 43 | document.cookie = name + '=' + value + expires + ';'; 44 | } 45 | 46 | setCookie('foo', 'bar'); 47 | }); 48 | 49 | const session = await page.session.dump(); 50 | 51 | // the cookie exists and was obtained 52 | expect( 53 | JSON.parse(session.cookie!).some( 54 | (cookie: CDPCookie) => cookie.name === 'foo', 55 | ), 56 | ).toBe(true); 57 | 58 | // the cookie contains the right value 59 | expect( 60 | JSON.parse(session.cookie!).find( 61 | (cookie: CDPCookie) => cookie.name === 'foo', 62 | )?.value, 63 | ).toBe('bar'); 64 | }); 65 | 66 | it('can edit and overwrite cookies', async () => { 67 | // set dummy cookie (foo:bar) 68 | await page.evaluate(() => { 69 | function setCookie(name: string, value: string, days?: number): void { 70 | let expires: string; 71 | if (days) { 72 | const date = new Date(); 73 | date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); 74 | expires = '; expires=' + date.toString(); 75 | } else expires = ''; 76 | document.cookie = name + '=' + value + expires + ';'; 77 | } 78 | 79 | setCookie('foo', 'bar'); 80 | }); 81 | 82 | const initialSession = await page.session.dump(); 83 | 84 | // edit cookies 85 | initialSession.cookie = JSON.stringify( 86 | JSON.parse(initialSession.cookie!).map((cookie: CDPCookie) => { 87 | // note: beware, here, it's baz, while before, it was ba*r*. 88 | cookie.value = 'baz'; 89 | return cookie; 90 | }), 91 | ); 92 | 93 | await page.session.restore(initialSession); 94 | 95 | const restoredSession = await page.session.dump(); 96 | 97 | // the cookie exists again 98 | expect( 99 | JSON.parse(restoredSession.cookie!).some( 100 | (cookie: CDPCookie) => cookie.name === 'foo', 101 | ), 102 | ).toBe(true); 103 | 104 | // the cookie contains the right value 105 | expect( 106 | JSON.parse(restoredSession.cookie!).find( 107 | (cookie: CDPCookie) => cookie.name === 'foo', 108 | )?.value, 109 | ).toBe('baz'); 110 | }); 111 | 112 | it.todo('can add a cookie'); 113 | it.todo('gets cookies from other domains'); 114 | it.todo('overwrites cookies from other domain'); 115 | -------------------------------------------------------------------------------- /src/providers/cookies.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { z } from 'zod'; 3 | import { 4 | StorageProvider, 5 | StorageProviderName, 6 | } from '../classes/storage-provider'; 7 | import { CDPCookieParam, CDPCookieSchema } from '../schemas'; 8 | 9 | export class CookieStorageProvider extends StorageProvider { 10 | public get name(): StorageProviderName { 11 | return StorageProviderName.Cookie; 12 | } 13 | 14 | public async get(page: Page): Promise { 15 | const session = await page.target().createCDPSession(); 16 | const resp = await session.send('Network.getAllCookies'); 17 | await session.detach(); 18 | 19 | /** 20 | * @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-getAllCookies 21 | */ 22 | const parsed = z.object({ cookies: z.array(CDPCookieSchema) }).parse(resp); 23 | 24 | return JSON.stringify(parsed.cookies); 25 | } 26 | 27 | public async set(page: Page, data: string): Promise { 28 | const session = await page.target().createCDPSession(); 29 | 30 | const parsed = z.array(CDPCookieParam).parse(JSON.parse(data)); 31 | 32 | /** 33 | * @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#method-setCookies 34 | */ 35 | await session.send('Network.setCookies', { 36 | cookies: parsed, 37 | }); 38 | 39 | await session.detach(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/providers/indexedDb.spec.ts: -------------------------------------------------------------------------------- 1 | import { Browser, Page } from 'puppeteer'; 2 | import puppeteer from 'puppeteer-extra'; 3 | import StealthPlugin from 'puppeteer-extra-plugin-stealth'; 4 | import { TestBrowserExecutablePath } from '../constants/constants'; 5 | import { SessionPlugin } from '../plugin/plugin'; 6 | 7 | const TESTING_URL: string = 'https://twitter.com'; 8 | 9 | jest.setTimeout(15000); 10 | 11 | let browser: Browser; 12 | let page: Page; 13 | puppeteer.use(new SessionPlugin()).use(StealthPlugin()); 14 | beforeAll(async () => { 15 | browser = await puppeteer.launch({ 16 | headless: true, 17 | executablePath: TestBrowserExecutablePath, 18 | args: ['--no-sandbox'], 19 | }); 20 | }); 21 | 22 | beforeEach(async () => { 23 | page = await browser.newPage(); 24 | await page.goto(TESTING_URL, { waitUntil: 'networkidle2' }); 25 | }); 26 | 27 | afterAll(async () => { 28 | await browser?.close(); 29 | }); 30 | 31 | it('can get localdb', async () => { 32 | const session = await page.session.dump(); 33 | 34 | // the db exist and was obtained 35 | // note(clouedoc): long string, just making sure it's the exact same length than the one I get under normal conditions 36 | expect(session.indexedDB?.length).toBe(335); 37 | expect( 38 | JSON.parse(session.indexedDB!).some( 39 | (db: { name: string }) => db.name === 'localforage', 40 | ), 41 | ).toBe(true); 42 | }); 43 | 44 | it('can set indexDB', async () => { 45 | const session = await page.session.dump(); 46 | expect( 47 | JSON.parse(session.indexedDB!).some( 48 | (db: { name: string }) => db.name === 'localforage', 49 | ), 50 | ).toBe(true); 51 | 52 | // Delete the database using CDP 53 | const client = await page.target().createCDPSession(); 54 | await client.send('IndexedDB.deleteDatabase', { 55 | databaseName: 'localforage', 56 | securityOrigin: TESTING_URL, 57 | }); 58 | await client.detach(); 59 | 60 | // Dump the session again to make sure there is no localforage 61 | const emptySession = await page.session.dump(); 62 | expect( 63 | JSON.parse(emptySession.indexedDB!).some( 64 | (db: { name: string }) => db.name === 'localforage', 65 | ), 66 | ).toBe(false); 67 | 68 | // Restore the indexedDB 69 | await page.session.restore(session); 70 | 71 | // Check to make sure the db was restored 72 | const finalSession = await page.session.dump(); 73 | expect( 74 | JSON.parse(finalSession.indexedDB!).some( 75 | (db: { name: string }) => db.name === 'localforage', 76 | ), 77 | ).toBe(true); 78 | }); 79 | -------------------------------------------------------------------------------- /src/providers/indexedDb.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { 3 | StorageProvider, 4 | StorageProviderName, 5 | } from '../classes/storage-provider'; 6 | import { dexieCore, dexieExportImport } from '../constants/dexie'; 7 | import { 8 | CDPIndexedDBDatabaseNames, 9 | IndexedDBDatabaseSchema, 10 | IndexedDBSchema, 11 | } from '../schemas'; 12 | 13 | export class IndexedDBStorageProvider extends StorageProvider { 14 | public get name(): StorageProviderName { 15 | return StorageProviderName.IndexedDB; 16 | } 17 | 18 | public async get(page: Page): Promise { 19 | const securityOrigin = await page.evaluate(() => location.origin); 20 | 21 | const dbNames = await getDatabaseNames(page, securityOrigin); 22 | 23 | const indexedDBs = await Promise.all( 24 | dbNames.map((db) => getIndexedDB(page, db)), 25 | ); 26 | 27 | return JSON.stringify( 28 | dbNames.map((db, index) => { 29 | return { 30 | name: db, 31 | data: indexedDBs[index], 32 | securityOrigin, 33 | }; 34 | }), 35 | ); 36 | } 37 | 38 | public async set(page: Page, data: string): Promise { 39 | const databases = IndexedDBDatabaseSchema.array().parse(JSON.parse(data)); 40 | 41 | for (const db of databases) { 42 | if (!page.url().includes(db.securityOrigin)) { 43 | await page.goto(db.securityOrigin); 44 | } 45 | 46 | await setIndexedDB(page, db.data); 47 | } 48 | } 49 | } 50 | 51 | function generateSetContentScript(data: string): string { 52 | return `(async() => { 53 | // note(clouedoc): required for some websites 54 | // see https://stackoverflow.com/a/48690342/4564097 55 | define = undefined; 56 | exports = undefined; 57 | if (window.module) module.exports = undefined; 58 | 59 | ${dexieCore} 60 | ${dexieExportImport} 61 | const importBlob = new Blob([\`${data}\`], { type: "text/json" }); 62 | const importDB = await Dexie.import(importBlob, { overwriteValues: true }); 63 | return importDB.backendDB(); 64 | })()`; 65 | } 66 | 67 | // STEALTH: isolated worlds 68 | // TODO: investigate database versions 69 | export async function setIndexedDB(page: Page, data: string): Promise { 70 | await page.evaluate(generateSetContentScript(data)); 71 | } 72 | 73 | // TODO: change this to an appropriate name 74 | export async function getDatabaseNames( 75 | page: Page, 76 | securityOrigin: string, 77 | ): Promise { 78 | const session = await page.target().createCDPSession(); 79 | 80 | let dbNames: string[]; 81 | try { 82 | const resp = CDPIndexedDBDatabaseNames.parse( 83 | await session.send('IndexedDB.requestDatabaseNames', { 84 | securityOrigin, 85 | }), 86 | ); 87 | dbNames = resp.databaseNames; 88 | } catch (err) { 89 | if ((err as Error).message.includes('No document for given frame found')) { 90 | dbNames = []; 91 | } else { 92 | throw err; 93 | } 94 | } 95 | 96 | await session.detach(); 97 | 98 | return dbNames; 99 | } 100 | 101 | function generateGetContentScript(dbName: string): string { 102 | return ` 103 | (async() => { 104 | // note(clouedoc): required for some websites 105 | // see https://stackoverflow.com/a/48690342/4564097 106 | define = undefined; 107 | exports = undefined; 108 | if (window.module) module.exports = undefined; 109 | 110 | ${dexieCore} 111 | ${dexieExportImport} 112 | const db = await new Dexie("${dbName}").open(); 113 | const blob = await db.export(); 114 | return blob.text(); 115 | })()`; 116 | } 117 | 118 | // STEALTH: isolated worlds 119 | async function getIndexedDB(page: Page, dbName: string): Promise { 120 | const result = await page.evaluate(generateGetContentScript(dbName)); 121 | return IndexedDBSchema.parse(result); 122 | } 123 | -------------------------------------------------------------------------------- /src/providers/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { 3 | StorageProvider, 4 | StorageProviderName, 5 | } from '../classes/storage-provider'; 6 | 7 | export class LocalStorageProvider extends StorageProvider { 8 | public get name(): StorageProviderName { 9 | return StorageProviderName.LocalStorage; 10 | } 11 | 12 | public async get(page: Page): Promise { 13 | // STEALTH: use isolated worlds 14 | const localStorage = await page.evaluate(() => 15 | Object.assign({}, window.localStorage), 16 | ); 17 | return JSON.stringify(localStorage); 18 | } 19 | 20 | public async set(page: Page, data: string): Promise { 21 | // STEALTH: use isolated worlds 22 | await page.evaluate((localStorage: string) => { 23 | for (const [key, val] of Object.entries(JSON.parse(localStorage))) { 24 | window.localStorage.setItem(key, val as string); 25 | } 26 | }, data); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/providers/sessionStorage.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { 3 | StorageProvider, 4 | StorageProviderName, 5 | } from '../classes/storage-provider'; 6 | 7 | export class SessionStorageProvider extends StorageProvider { 8 | public get name(): StorageProviderName { 9 | return StorageProviderName.SessionStorage; 10 | } 11 | 12 | public async get(page: Page): Promise { 13 | // STEALTH: use isolated worlds 14 | return page.evaluate(() => 15 | JSON.stringify(Object.assign({}, window.sessionStorage)), 16 | ); 17 | } 18 | 19 | public async set(page: Page, data: string): Promise { 20 | // STEALTH: use isolated worlds 21 | await page.evaluate((sessionStorage: string) => { 22 | for (const [key, val] of Object.entries(JSON.parse(sessionStorage))) { 23 | window.sessionStorage.setItem(key, val as string); 24 | } 25 | }, data); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/schemas.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @rushstack/typedef-var */ 2 | import { z } from 'zod'; 3 | 4 | /** 5 | * @see https://chromedevtools.github.io/devtools-protocol/tot/IndexedDB/#method-requestDatabaseNames 6 | */ 7 | export const CDPIndexedDBDatabaseNames = z.object({ 8 | databaseNames: z.array(z.string()), 9 | }); 10 | 11 | /** 12 | * IndexedDB schemas 13 | */ 14 | export const IndexedDBSchema = z.string(); 15 | export const IndexedDBDatabaseSchema = z.object({ 16 | name: z.string(), 17 | data: IndexedDBSchema, 18 | securityOrigin: z.string(), 19 | }); 20 | export type IndexedDBDatabase = z.infer; 21 | 22 | const CDPSameSite = z.enum(['Strict', 'Lax', 'None']); 23 | const CDPCookiePriority = z.enum(['Low', 'Medium', 'High']); 24 | const CDPSourceScheme = z.enum(['Unset', 'NonSecure', 'Secure']); 25 | 26 | /** 27 | * CDP Network.Cookie schema 28 | * @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie 29 | */ 30 | export const CDPCookieSchema = z.object({ 31 | name: z.string(), 32 | value: z.string(), 33 | domain: z.string(), 34 | path: z.string(), 35 | expires: z.number(), 36 | size: z.number(), 37 | httpOnly: z.boolean(), 38 | secure: z.boolean(), 39 | session: z.boolean(), 40 | sameSite: CDPSameSite.optional(), 41 | priority: CDPCookiePriority, 42 | sameParty: z.boolean(), 43 | sourceScheme: CDPSourceScheme, 44 | sourcePort: z.number(), 45 | }); 46 | export type CDPCookie = z.infer; 47 | 48 | /** 49 | * CDP Network.CookieParam schema 50 | * @see https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam 51 | */ 52 | export const CDPCookieParam = z.object({ 53 | name: z.string(), 54 | value: z.string(), 55 | url: z.string().optional(), 56 | domain: z.string().optional(), 57 | path: z.string().optional(), 58 | secure: z.boolean().optional(), 59 | httpOnly: z.boolean().optional(), 60 | sameSite: CDPSameSite.optional(), 61 | /** 62 | * Time since Epoch 63 | */ 64 | expires: z.number().optional(), 65 | priority: CDPCookiePriority.optional(), 66 | sameParty: z.boolean().optional(), 67 | sourceScheme: CDPSourceScheme.optional(), 68 | sourcePort: z.number().optional(), 69 | }); 70 | 71 | export const SessionDataSchema = z.object({ 72 | localStorage: z.string().optional(), 73 | sessionStorage: z.string().optional(), 74 | indexedDB: z.string().optional(), 75 | cookie: z.string().optional(), 76 | }); 77 | -------------------------------------------------------------------------------- /src/session.ts: -------------------------------------------------------------------------------- 1 | import { Page } from 'puppeteer'; 2 | import { 3 | StorageProvider, 4 | StorageProviderName, 5 | } from './classes/storage-provider'; 6 | import { CookieStorageProvider } from './providers/cookies'; 7 | import { IndexedDBStorageProvider } from './providers/indexedDb'; 8 | import { LocalStorageProvider } from './providers/localStorage'; 9 | import { SessionStorageProvider } from './providers/sessionStorage'; 10 | import { SessionData } from './types/session-data'; 11 | 12 | export const storageProviderMap: Record< 13 | StorageProviderName, 14 | StorageProvider 15 | > = { 16 | [StorageProviderName.Cookie]: new CookieStorageProvider(), 17 | [StorageProviderName.LocalStorage]: new LocalStorageProvider(), 18 | [StorageProviderName.SessionStorage]: new SessionStorageProvider(), 19 | [StorageProviderName.IndexedDB]: new IndexedDBStorageProvider(), 20 | }; 21 | 22 | /** 23 | * All the storage providers! 24 | */ 25 | const allStorageProviders: StorageProviderName[] = Object.values( 26 | StorageProviderName, 27 | ); 28 | 29 | export async function getSessionData( 30 | page: Page, 31 | providers: StorageProviderName[] = allStorageProviders, 32 | ): Promise { 33 | const data: Partial = {}; 34 | for (const provider of providers) { 35 | data[provider] = await storageProviderMap[provider].get(page); 36 | } 37 | return data; 38 | } 39 | 40 | export async function setSessionData( 41 | page: Page, 42 | sessionData: SessionData, 43 | providers: StorageProviderName[] = allStorageProviders, 44 | ): Promise { 45 | await Promise.all( 46 | providers.map(async (providerName) => { 47 | const data = sessionData[providerName]; 48 | if (data) { 49 | await storageProviderMap[providerName].set(page, data); 50 | } 51 | }), 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import {} from 'puppeteer'; 2 | import {} from 'puppeteer-core'; 3 | import { PluginOptions } from 'puppeteer-extra-plugin'; 4 | import { SessionManager } from './classes/session-manager'; 5 | 6 | export interface ISessionPluginOptions extends PluginOptions {} 7 | 8 | /** 9 | * This part down below works for some reason, and that's all I need to know. 10 | * May TypeScript bless you the same way I was. 11 | */ 12 | 13 | export interface ISessionPluginPageAdditions { 14 | session: SessionManager; 15 | } 16 | 17 | declare module 'puppeteer' { 18 | // eslint-disable-next-line @typescript-eslint/naming-convention 19 | interface Page extends ISessionPluginPageAdditions {} 20 | } 21 | 22 | declare module 'puppeteer-core' { 23 | // eslint-disable-next-line @typescript-eslint/naming-convention 24 | interface Page extends ISessionPluginPageAdditions {} 25 | } 26 | -------------------------------------------------------------------------------- /src/types/session-data.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | import { SessionDataSchema } from '../schemas'; 3 | 4 | export type SessionData = z.infer; 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "declaration": true, 5 | "emitDecoratorMetadata": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "lib": ["ES2017", "DOM"], 9 | "module": "commonjs", 10 | "moduleResolution": "node", 11 | "noFallthroughCasesInSwitch": true, 12 | "noImplicitReturns": false, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": false, 15 | "outDir": "lib", 16 | "pretty": true, 17 | "resolveJsonModule": true, 18 | "rootDir": "src", 19 | "sourceMap": true, 20 | "strict": true, 21 | "stripInternal": true, 22 | "target": "ES2017" 23 | }, 24 | "include": ["src/**/*"], 25 | "exclude": ["lib", "node_modules"] 26 | } 27 | --------------------------------------------------------------------------------