@hienlh/ppm 0.8.49 → 0.8.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/web/assets/api-settings-DfTIjsPW.js +1 -0
  3. package/dist/web/assets/{chat-tab-CkVy9ut7.js → chat-tab-BZSpI1_2.js} +2 -2
  4. package/dist/web/assets/{code-editor-CdiHsvVd.js → code-editor-DB-y8tPy.js} +1 -1
  5. package/dist/web/assets/{database-viewer-DCB3fyHi.js → database-viewer-D4JQEtMD.js} +1 -1
  6. package/dist/web/assets/{diff-viewer-DZEXDwGs.js → diff-viewer-ToD0FLsL.js} +1 -1
  7. package/dist/web/assets/{git-graph-BfgY255b.js → git-graph-Dg7SVF-R.js} +1 -1
  8. package/dist/web/assets/index-DdLIa98_.js +28 -0
  9. package/dist/web/assets/index-XRJa3Ncz.css +2 -0
  10. package/dist/web/assets/keybindings-store-BofWHLIC.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-F1beFJEy.js → markdown-renderer-DMHeWMgi.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-CxO5FaWj.js → postgres-viewer-bUYwSwrp.js} +1 -1
  13. package/dist/web/assets/{settings-store-xG6mKqkD.js → settings-store-ChwdK0tt.js} +2 -2
  14. package/dist/web/assets/settings-tab-D2bgiL7t.js +1 -0
  15. package/dist/web/assets/{sqlite-viewer-CQNRFUxH.js → sqlite-viewer-BLUoWIZ5.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-CEvaCyVU.js → terminal-tab-B5sI9TDZ.js} +1 -1
  17. package/dist/web/assets/{use-monaco-theme-DlFSiqvG.js → use-monaco-theme-0hXmt0_2.js} +1 -1
  18. package/dist/web/index.html +4 -4
  19. package/dist/web/sw.js +1 -1
  20. package/package.json +1 -1
  21. package/src/server/routes/accounts.ts +137 -2
  22. package/src/web/components/settings/accounts-settings-section.tsx +255 -6
  23. package/src/web/lib/api-settings.ts +33 -0
  24. package/test-tokens.mjs +212 -0
  25. package/dist/web/assets/api-settings-CaKDC7_s.js +0 -1
  26. package/dist/web/assets/index-DubLYgN1.css +0 -2
  27. package/dist/web/assets/index-odr3ymlS.js +0 -28
  28. package/dist/web/assets/keybindings-store-tyvdfWMV.js +0 -1
  29. package/dist/web/assets/settings-tab-DLtrBBV2.js +0 -1
package/dist/web/sw.js CHANGED
@@ -1 +1 @@
1
- try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"b7fed017a8dd7ca50bdce37817482823","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DC-bdPS3.js"},{"revision":null,"url":"assets/use-monaco-theme-DlFSiqvG.js"},{"revision":null,"url":"assets/terminal-tab-CEvaCyVU.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/tag-DJUYe5BQ.js"},{"revision":null,"url":"assets/table-B6neW6Hr.js"},{"revision":null,"url":"assets/tab-store-NOBndc0_.js"},{"revision":null,"url":"assets/sqlite-viewer-CQNRFUxH.js"},{"revision":null,"url":"assets/settings-tab-DLtrBBV2.js"},{"revision":null,"url":"assets/settings-store-xG6mKqkD.js"},{"revision":null,"url":"assets/react-rgzL83kk.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-CxO5FaWj.js"},{"revision":null,"url":"assets/markdown-renderer-F1beFJEy.js"},{"revision":null,"url":"assets/keybindings-store-tyvdfWMV.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-CE3bFwLk.js"},{"revision":null,"url":"assets/index-odr3ymlS.js"},{"revision":null,"url":"assets/index-DubLYgN1.css"},{"revision":null,"url":"assets/git-graph-BfgY255b.js"},{"revision":null,"url":"assets/dist-QgqOdSYG.js"},{"revision":null,"url":"assets/diff-viewer-DZEXDwGs.js"},{"revision":null,"url":"assets/database-viewer-DCB3fyHi.js"},{"revision":null,"url":"assets/columns-2-BZ5wv2wA.js"},{"revision":null,"url":"assets/code-editor-CdiHsvVd.js"},{"revision":null,"url":"assets/chat-tab-CkVy9ut7.js"},{"revision":null,"url":"assets/api-settings-CaKDC7_s.js"},{"revision":null,"url":"assets/api-client-TUmacMRS.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
1
+ try{self[`workbox:core:7.3.0`]&&_()}catch{}var e=(e,...t)=>{let n=e;return t.length>0&&(n+=` :: ${JSON.stringify(t)}`),n},t=class extends Error{constructor(t,n){let r=e(t,n);super(r),this.name=t,this.details=n}},n={googleAnalytics:`googleAnalytics`,precache:`precache-v2`,prefix:`workbox`,runtime:`runtime`,suffix:typeof registration<`u`?registration.scope:``},r=e=>[n.prefix,e,n.suffix].filter(e=>e&&e.length>0).join(`-`),i=e=>{for(let t of Object.keys(n))e(t)},a={updateDetails:e=>{i(t=>{typeof e[t]==`string`&&(n[t]=e[t])})},getGoogleAnalyticsName:e=>e||r(n.googleAnalytics),getPrecacheName:e=>e||r(n.precache),getPrefix:()=>n.prefix,getRuntimeName:e=>e||r(n.runtime),getSuffix:()=>n.suffix};function o(e,t){let n=t();return e.waitUntil(n),n}try{self[`workbox:precaching:7.3.0`]&&_()}catch{}var s=`__WB_REVISION__`;function c(e){if(!e)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(typeof e==`string`){let t=new URL(e,location.href);return{cacheKey:t.href,url:t.href}}let{revision:n,url:r}=e;if(!r)throw new t(`add-to-cache-list-unexpected-type`,{entry:e});if(!n){let e=new URL(r,location.href);return{cacheKey:e.href,url:e.href}}let i=new URL(r,location.href),a=new URL(r,location.href);return i.searchParams.set(s,n),{cacheKey:i.href,url:a.href}}var l=class{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:e,state:t})=>{t&&(t.originalRequest=e)},this.cachedResponseWillBeUsed=async({event:e,state:t,cachedResponse:n})=>{if(e.type===`install`&&t&&t.originalRequest&&t.originalRequest instanceof Request){let e=t.originalRequest.url;n?this.notUpdatedURLs.push(e):this.updatedURLs.push(e)}return n}}},u=class{constructor({precacheController:e}){this.cacheKeyWillBeUsed=async({request:e,params:t})=>{let n=t?.cacheKey||this._precacheController.getCacheKeyForURL(e.url);return n?new Request(n,{headers:e.headers}):e},this._precacheController=e}},d;function f(){if(d===void 0){let e=new Response(``);if(`body`in e)try{new Response(e.body),d=!0}catch{d=!1}d=!1}return d}async function p(e,n){let r=null;if(e.url&&(r=new URL(e.url).origin),r!==self.location.origin)throw new t(`cross-origin-copy-response`,{origin:r});let i=e.clone(),a={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=n?n(a):a,s=f()?i.body:await i.blob();return new Response(s,o)}var m=e=>new URL(String(e),location.href).href.replace(RegExp(`^${location.origin}`),``);function h(e,t){let n=new URL(e);for(let e of t)n.searchParams.delete(e);return n.href}async function g(e,t,n,r){let i=h(t.url,n);if(t.url===i)return e.match(t,r);let a=Object.assign(Object.assign({},r),{ignoreSearch:!0}),o=await e.keys(t,a);for(let t of o)if(i===h(t.url,n))return e.match(t,r)}var v=class{constructor(){this.promise=new Promise((e,t)=>{this.resolve=e,this.reject=t})}},y=new Set;async function b(){for(let e of y)await e()}function x(e){return new Promise(t=>setTimeout(t,e))}try{self[`workbox:strategies:7.3.0`]&&_()}catch{}function S(e){return typeof e==`string`?new Request(e):e}var C=class{constructor(e,t){this._cacheKeys={},Object.assign(this,t),this.event=t.event,this._strategy=e,this._handlerDeferred=new v,this._extendLifetimePromises=[],this._plugins=[...e.plugins],this._pluginStateMap=new Map;for(let e of this._plugins)this._pluginStateMap.set(e,{});this.event.waitUntil(this._handlerDeferred.promise)}async fetch(e){let{event:n}=this,r=S(e);if(r.mode===`navigate`&&n instanceof FetchEvent&&n.preloadResponse){let e=await n.preloadResponse;if(e)return e}let i=this.hasCallback(`fetchDidFail`)?r.clone():null;try{for(let e of this.iterateCallbacks(`requestWillFetch`))r=await e({request:r.clone(),event:n})}catch(e){if(e instanceof Error)throw new t(`plugin-error-request-will-fetch`,{thrownErrorMessage:e.message})}let a=r.clone();try{let e;e=await fetch(r,r.mode===`navigate`?void 0:this._strategy.fetchOptions);for(let t of this.iterateCallbacks(`fetchDidSucceed`))e=await t({event:n,request:a,response:e});return e}catch(e){throw i&&await this.runCallbacks(`fetchDidFail`,{error:e,event:n,originalRequest:i.clone(),request:a.clone()}),e}}async fetchAndCachePut(e){let t=await this.fetch(e),n=t.clone();return this.waitUntil(this.cachePut(e,n)),t}async cacheMatch(e){let t=S(e),n,{cacheName:r,matchOptions:i}=this._strategy,a=await this.getCacheKey(t,`read`),o=Object.assign(Object.assign({},i),{cacheName:r});n=await caches.match(a,o);for(let e of this.iterateCallbacks(`cachedResponseWillBeUsed`))n=await e({cacheName:r,matchOptions:i,cachedResponse:n,request:a,event:this.event})||void 0;return n}async cachePut(e,n){let r=S(e);await x(0);let i=await this.getCacheKey(r,`write`);if(!n)throw new t(`cache-put-with-no-response`,{url:m(i.url)});let a=await this._ensureResponseSafeToCache(n);if(!a)return!1;let{cacheName:o,matchOptions:s}=this._strategy,c=await self.caches.open(o),l=this.hasCallback(`cacheDidUpdate`),u=l?await g(c,i.clone(),[`__WB_REVISION__`],s):null;try{await c.put(i,l?a.clone():a)}catch(e){if(e instanceof Error)throw e.name===`QuotaExceededError`&&await b(),e}for(let e of this.iterateCallbacks(`cacheDidUpdate`))await e({cacheName:o,oldResponse:u,newResponse:a.clone(),request:i,event:this.event});return!0}async getCacheKey(e,t){let n=`${e.url} | ${t}`;if(!this._cacheKeys[n]){let r=e;for(let e of this.iterateCallbacks(`cacheKeyWillBeUsed`))r=S(await e({mode:t,request:r,event:this.event,params:this.params}));this._cacheKeys[n]=r}return this._cacheKeys[n]}hasCallback(e){for(let t of this._strategy.plugins)if(e in t)return!0;return!1}async runCallbacks(e,t){for(let n of this.iterateCallbacks(e))await n(t)}*iterateCallbacks(e){for(let t of this._strategy.plugins)if(typeof t[e]==`function`){let n=this._pluginStateMap.get(t);yield r=>{let i=Object.assign(Object.assign({},r),{state:n});return t[e](i)}}}waitUntil(e){return this._extendLifetimePromises.push(e),e}async doneWaiting(){for(;this._extendLifetimePromises.length;){let e=this._extendLifetimePromises.splice(0),t=(await Promise.allSettled(e)).find(e=>e.status===`rejected`);if(t)throw t.reason}}destroy(){this._handlerDeferred.resolve(null)}async _ensureResponseSafeToCache(e){let t=e,n=!1;for(let e of this.iterateCallbacks(`cacheWillUpdate`))if(t=await e({request:this.request,response:t,event:this.event})||void 0,n=!0,!t)break;return n||t&&t.status!==200&&(t=void 0),t}},w=class{constructor(e={}){this.cacheName=a.getRuntimeName(e.cacheName),this.plugins=e.plugins||[],this.fetchOptions=e.fetchOptions,this.matchOptions=e.matchOptions}handle(e){let[t]=this.handleAll(e);return t}handleAll(e){e instanceof FetchEvent&&(e={event:e,request:e.request});let t=e.event,n=typeof e.request==`string`?new Request(e.request):e.request,r=`params`in e?e.params:void 0,i=new C(this,{event:t,request:n,params:r}),a=this._getResponse(i,n,t);return[a,this._awaitComplete(a,i,n,t)]}async _getResponse(e,n,r){await e.runCallbacks(`handlerWillStart`,{event:r,request:n});let i;try{if(i=await this._handle(n,e),!i||i.type===`error`)throw new t(`no-response`,{url:n.url})}catch(t){if(t instanceof Error){for(let a of e.iterateCallbacks(`handlerDidError`))if(i=await a({error:t,event:r,request:n}),i)break}if(!i)throw t}for(let t of e.iterateCallbacks(`handlerWillRespond`))i=await t({event:r,request:n,response:i});return i}async _awaitComplete(e,t,n,r){let i,a;try{i=await e}catch{}try{await t.runCallbacks(`handlerDidRespond`,{event:r,request:n,response:i}),await t.doneWaiting()}catch(e){e instanceof Error&&(a=e)}if(await t.runCallbacks(`handlerDidComplete`,{event:r,request:n,response:i,error:a}),t.destroy(),a)throw a}},T=class e extends w{constructor(t={}){t.cacheName=a.getPrecacheName(t.cacheName),super(t),this._fallbackToNetwork=t.fallbackToNetwork!==!1,this.plugins.push(e.copyRedirectedCacheableResponsesPlugin)}async _handle(e,t){return await t.cacheMatch(e)||(t.event&&t.event.type===`install`?await this._handleInstall(e,t):await this._handleFetch(e,t))}async _handleFetch(e,n){let r,i=n.params||{};if(this._fallbackToNetwork){let t=i.integrity,a=e.integrity,o=!a||a===t;r=await n.fetch(new Request(e,{integrity:e.mode===`no-cors`?void 0:a||t})),t&&o&&e.mode!==`no-cors`&&(this._useDefaultCacheabilityPluginIfNeeded(),await n.cachePut(e,r.clone()))}else throw new t(`missing-precache-entry`,{cacheName:this.cacheName,url:e.url});return r}async _handleInstall(e,n){this._useDefaultCacheabilityPluginIfNeeded();let r=await n.fetch(e);if(!await n.cachePut(e,r.clone()))throw new t(`bad-precaching-response`,{url:e.url,status:r.status});return r}_useDefaultCacheabilityPluginIfNeeded(){let t=null,n=0;for(let[r,i]of this.plugins.entries())i!==e.copyRedirectedCacheableResponsesPlugin&&(i===e.defaultPrecacheCacheabilityPlugin&&(t=r),i.cacheWillUpdate&&n++);n===0?this.plugins.push(e.defaultPrecacheCacheabilityPlugin):n>1&&t!==null&&this.plugins.splice(t,1)}};T.defaultPrecacheCacheabilityPlugin={async cacheWillUpdate({response:e}){return!e||e.status>=400?null:e}},T.copyRedirectedCacheableResponsesPlugin={async cacheWillUpdate({response:e}){return e.redirected?await p(e):e}};var E=class{constructor({cacheName:e,plugins:t=[],fallbackToNetwork:n=!0}={}){this._urlsToCacheKeys=new Map,this._urlsToCacheModes=new Map,this._cacheKeysToIntegrities=new Map,this._strategy=new T({cacheName:a.getPrecacheName(e),plugins:[...t,new u({precacheController:this})],fallbackToNetwork:n}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this._strategy}precache(e){this.addToCacheList(e),this._installAndActiveListenersAdded||=(self.addEventListener(`install`,this.install),self.addEventListener(`activate`,this.activate),!0)}addToCacheList(e){let n=[];for(let r of e){typeof r==`string`?n.push(r):r&&r.revision===void 0&&n.push(r.url);let{cacheKey:e,url:i}=c(r),a=typeof r!=`string`&&r.revision?`reload`:`default`;if(this._urlsToCacheKeys.has(i)&&this._urlsToCacheKeys.get(i)!==e)throw new t(`add-to-cache-list-conflicting-entries`,{firstEntry:this._urlsToCacheKeys.get(i),secondEntry:e});if(typeof r!=`string`&&r.integrity){if(this._cacheKeysToIntegrities.has(e)&&this._cacheKeysToIntegrities.get(e)!==r.integrity)throw new t(`add-to-cache-list-conflicting-integrities`,{url:i});this._cacheKeysToIntegrities.set(e,r.integrity)}if(this._urlsToCacheKeys.set(i,e),this._urlsToCacheModes.set(i,a),n.length>0){let e=`Workbox is precaching URLs without revision info: ${n.join(`, `)}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(e)}}}install(e){return o(e,async()=>{let t=new l;this.strategy.plugins.push(t);for(let[t,n]of this._urlsToCacheKeys){let r=this._cacheKeysToIntegrities.get(n),i=this._urlsToCacheModes.get(t),a=new Request(t,{integrity:r,cache:i,credentials:`same-origin`});await Promise.all(this.strategy.handleAll({params:{cacheKey:n},request:a,event:e}))}let{updatedURLs:n,notUpdatedURLs:r}=t;return{updatedURLs:n,notUpdatedURLs:r}})}activate(e){return o(e,async()=>{let e=await self.caches.open(this.strategy.cacheName),t=await e.keys(),n=new Set(this._urlsToCacheKeys.values()),r=[];for(let i of t)n.has(i.url)||(await e.delete(i),r.push(i.url));return{deletedURLs:r}})}getURLsToCacheKeys(){return this._urlsToCacheKeys}getCachedURLs(){return[...this._urlsToCacheKeys.keys()]}getCacheKeyForURL(e){let t=new URL(e,location.href);return this._urlsToCacheKeys.get(t.href)}getIntegrityForCacheKey(e){return this._cacheKeysToIntegrities.get(e)}async matchPrecache(e){let t=e instanceof Request?e.url:e,n=this.getCacheKeyForURL(t);if(n)return(await self.caches.open(this.strategy.cacheName)).match(n)}createHandlerBoundToURL(e){let n=this.getCacheKeyForURL(e);if(!n)throw new t(`non-precached-url`,{url:e});return t=>(t.request=new Request(e),t.params=Object.assign({cacheKey:n},t.params),this.strategy.handle(t))}},D,O=()=>(D||=new E,D);try{self[`workbox:routing:7.3.0`]&&_()}catch{}var k=e=>e&&typeof e==`object`?e:{handle:e},A=class{constructor(e,t,n=`GET`){this.handler=k(t),this.match=e,this.method=n}setCatchHandler(e){this.catchHandler=k(e)}},j=class extends A{constructor(e,t,n){super(({url:t})=>{let n=e.exec(t.href);if(n&&!(t.origin!==location.origin&&n.index!==0))return n.slice(1)},t,n)}},M=class{constructor(){this._routes=new Map,this._defaultHandlerMap=new Map}get routes(){return this._routes}addFetchListener(){self.addEventListener(`fetch`,(e=>{let{request:t}=e,n=this.handleRequest({request:t,event:e});n&&e.respondWith(n)}))}addCacheListener(){self.addEventListener(`message`,(e=>{if(e.data&&e.data.type===`CACHE_URLS`){let{payload:t}=e.data,n=Promise.all(t.urlsToCache.map(t=>{typeof t==`string`&&(t=[t]);let n=new Request(...t);return this.handleRequest({request:n,event:e})}));e.waitUntil(n),e.ports&&e.ports[0]&&n.then(()=>e.ports[0].postMessage(!0))}}))}handleRequest({request:e,event:t}){let n=new URL(e.url,location.href);if(!n.protocol.startsWith(`http`))return;let r=n.origin===location.origin,{params:i,route:a}=this.findMatchingRoute({event:t,request:e,sameOrigin:r,url:n}),o=a&&a.handler,s=e.method;if(!o&&this._defaultHandlerMap.has(s)&&(o=this._defaultHandlerMap.get(s)),!o)return;let c;try{c=o.handle({url:n,request:e,event:t,params:i})}catch(e){c=Promise.reject(e)}let l=a&&a.catchHandler;return c instanceof Promise&&(this._catchHandler||l)&&(c=c.catch(async r=>{if(l)try{return await l.handle({url:n,request:e,event:t,params:i})}catch(e){e instanceof Error&&(r=e)}if(this._catchHandler)return this._catchHandler.handle({url:n,request:e,event:t});throw r})),c}findMatchingRoute({url:e,sameOrigin:t,request:n,event:r}){let i=this._routes.get(n.method)||[];for(let a of i){let i,o=a.match({url:e,sameOrigin:t,request:n,event:r});if(o)return i=o,(Array.isArray(i)&&i.length===0||o.constructor===Object&&Object.keys(o).length===0||typeof o==`boolean`)&&(i=void 0),{route:a,params:i}}return{}}setDefaultHandler(e,t=`GET`){this._defaultHandlerMap.set(t,k(e))}setCatchHandler(e){this._catchHandler=k(e)}registerRoute(e){this._routes.has(e.method)||this._routes.set(e.method,[]),this._routes.get(e.method).push(e)}unregisterRoute(e){if(!this._routes.has(e.method))throw new t(`unregister-route-but-not-found-with-method`,{method:e.method});let n=this._routes.get(e.method).indexOf(e);if(n>-1)this._routes.get(e.method).splice(n,1);else throw new t(`unregister-route-route-not-registered`)}},N,P=()=>(N||(N=new M,N.addFetchListener(),N.addCacheListener()),N);function F(e,n,r){let i;if(typeof e==`string`){let t=new URL(e,location.href);i=new A(({url:e})=>e.href===t.href,n,r)}else if(e instanceof RegExp)i=new j(e,n,r);else if(typeof e==`function`)i=new A(e,n,r);else if(e instanceof A)i=e;else throw new t(`unsupported-route-type`,{moduleName:`workbox-routing`,funcName:`registerRoute`,paramName:`capture`});return P().registerRoute(i),i}function I(e,t=[]){for(let n of[...e.searchParams.keys()])t.some(e=>e.test(n))&&e.searchParams.delete(n);return e}function*L(e,{ignoreURLParametersMatching:t=[/^utm_/,/^fbclid$/],directoryIndex:n=`index.html`,cleanURLs:r=!0,urlManipulation:i}={}){let a=new URL(e,location.href);a.hash=``,yield a.href;let o=I(a,t);if(yield o.href,n&&o.pathname.endsWith(`/`)){let e=new URL(o.href);e.pathname+=n,yield e.href}if(r){let e=new URL(o.href);e.pathname+=`.html`,yield e.href}if(i){let e=i({url:a});for(let t of e)yield t.href}}var R=class extends A{constructor(e,t){super(({request:n})=>{let r=e.getURLsToCacheKeys();for(let i of L(n.url,t)){let t=r.get(i);if(t)return{cacheKey:t,integrity:e.getIntegrityForCacheKey(t)}}},e.strategy)}};function z(e){F(new R(O(),e))}function B(e){O().precache(e)}function V(e,t){B(e),z(t)}V([{"revision":"1872c500de691dce40960bb85481de07","url":"registerSW.js"},{"revision":"49865f1dc6c4195305930e2e5f60893d","url":"index.html"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-512.svg"},{"revision":"a0fb34fc84eb148d51812cd62669f20d","url":"icon-192.svg"},{"revision":"eb9818b9094675c0c5d303168f273345","url":"monacoeditorwork/ts.worker.bundle.js"},{"revision":"9af0be92dcefdc1f1290441cb5ff5d9b","url":"monacoeditorwork/json.worker.bundle.js"},{"revision":"a261b429c39dbb75ae97972d7d005e6d","url":"monacoeditorwork/html.worker.bundle.js"},{"revision":"79953d804e1bbacecfd79b85fd679016","url":"monacoeditorwork/editor.worker.bundle.js"},{"revision":"fdcba0d09aac31df7a0bc652f6e739bd","url":"monacoeditorwork/css.worker.bundle.js"},{"revision":null,"url":"assets/utils-DC-bdPS3.js"},{"revision":null,"url":"assets/use-monaco-theme-0hXmt0_2.js"},{"revision":null,"url":"assets/terminal-tab-BrP-ENHg.css"},{"revision":null,"url":"assets/terminal-tab-B5sI9TDZ.js"},{"revision":null,"url":"assets/tag-DJUYe5BQ.js"},{"revision":null,"url":"assets/table-B6neW6Hr.js"},{"revision":null,"url":"assets/tab-store-NOBndc0_.js"},{"revision":null,"url":"assets/sqlite-viewer-BLUoWIZ5.js"},{"revision":null,"url":"assets/settings-tab-D2bgiL7t.js"},{"revision":null,"url":"assets/settings-store-ChwdK0tt.js"},{"revision":null,"url":"assets/react-rgzL83kk.js"},{"revision":null,"url":"assets/react-CYzKIDNi.js"},{"revision":null,"url":"assets/postgres-viewer-bUYwSwrp.js"},{"revision":null,"url":"assets/markdown-renderer-DMHeWMgi.js"},{"revision":null,"url":"assets/keybindings-store-BofWHLIC.js"},{"revision":null,"url":"assets/jsx-runtime-wQxeESYQ.js"},{"revision":null,"url":"assets/input-CE3bFwLk.js"},{"revision":null,"url":"assets/index-XRJa3Ncz.css"},{"revision":null,"url":"assets/index-DdLIa98_.js"},{"revision":null,"url":"assets/git-graph-Dg7SVF-R.js"},{"revision":null,"url":"assets/dist-QgqOdSYG.js"},{"revision":null,"url":"assets/diff-viewer-ToD0FLsL.js"},{"revision":null,"url":"assets/database-viewer-D4JQEtMD.js"},{"revision":null,"url":"assets/columns-2-BZ5wv2wA.js"},{"revision":null,"url":"assets/code-editor-DB-y8tPy.js"},{"revision":null,"url":"assets/chat-tab-BZSpI1_2.js"},{"revision":null,"url":"assets/api-settings-DfTIjsPW.js"},{"revision":null,"url":"assets/api-client-TUmacMRS.js"},{"revision":"79c8870653c8f419f2e3323085e1f4be","url":"manifest.webmanifest"}]),self.addEventListener(`push`,e=>{e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{if(t.some(e=>e.visibilityState===`visible`))return;let n=e.data?.json()??{title:`PPM`,body:`Chat completed`};return self.registration.showNotification(n.title,{body:n.body,icon:`/icon-192.png`,badge:`/icon-192.png`,tag:`ppm-chat-done`,silent:!1,data:{url:self.location.origin}})}))}),self.addEventListener(`notificationclick`,e=>{e.notification.close(),e.waitUntil(self.clients.matchAll({type:`window`,includeUncontrolled:!0}).then(t=>{for(let e of t)if(e.url.includes(self.location.origin)&&`focus`in e)return e.focus();return self.clients.openWindow(e.notification.data?.url||`/`)}))});
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hienlh/ppm",
3
- "version": "0.8.49",
3
+ "version": "0.8.50",
4
4
  "description": "Personal Project Manager — mobile-first web IDE with AI assistance",
5
5
  "author": "hienlh",
6
6
  "license": "MIT",
@@ -160,9 +160,11 @@ accountsRoutes.post("/oauth/refresh/:id", async (c) => {
160
160
  /** POST /api/accounts/export — download password-encrypted accounts backup */
161
161
  accountsRoutes.post("/export", async (c) => {
162
162
  try {
163
- const { password, accountIds, includeRefreshToken } = await c.req.json() as { password: string; accountIds?: string[]; includeRefreshToken?: boolean };
163
+ const { password, accountIds, includeRefreshToken, refreshBeforeExport } = await c.req.json() as {
164
+ password: string; accountIds?: string[]; includeRefreshToken?: boolean; refreshBeforeExport?: boolean;
165
+ };
164
166
  if (!password) return c.json(err("Password required"), 400);
165
- await accountService.refreshBeforeExport(accountIds);
167
+ if (refreshBeforeExport) await accountService.refreshBeforeExport(accountIds);
166
168
  const blob = accountService.exportEncrypted(password, accountIds, includeRefreshToken ?? false);
167
169
  c.header("Content-Disposition", "attachment; filename=ppm-accounts-backup.json");
168
170
  c.header("Content-Type", "application/json");
@@ -213,6 +215,139 @@ accountsRoutes.post("/:id/verify", async (c) => {
213
215
  }
214
216
  });
215
217
 
218
+ /** POST /api/accounts/test-export — simulate export: returns decrypted tokens + current DB tokens for comparison */
219
+ accountsRoutes.post("/test-export", async (c) => {
220
+ try {
221
+ const { accountIds, includeRefreshToken } = await c.req.json() as { accountIds: string[]; includeRefreshToken?: boolean };
222
+ if (!accountIds?.length) return c.json(err("accountIds required"), 400);
223
+
224
+ // Snapshot current tokens BEFORE export (export calls refreshBeforeExport which may change tokens)
225
+ const preExportTokens = accountIds.map((id) => {
226
+ const acc = accountService.getWithTokens(id);
227
+ if (!acc) return null;
228
+ return { id, label: acc.label, email: acc.email, accessToken: acc.accessToken, expiresAt: acc.expiresAt };
229
+ }).filter(Boolean) as { id: string; label: string | null; email: string | null; accessToken: string; expiresAt: number | null }[];
230
+
231
+ // Do export with a temp password, then decrypt to get exported tokens
232
+ const tmpPwd = "test-export-" + Date.now();
233
+ await accountService.refreshBeforeExport(accountIds);
234
+ const blob = accountService.exportEncrypted(tmpPwd, accountIds, includeRefreshToken ?? false);
235
+
236
+ // Decrypt to get raw exported tokens
237
+ const { decryptWithPassword } = await import("../../lib/account-crypto.ts");
238
+ const rows = JSON.parse(decryptWithPassword(blob, tmpPwd)) as { id: string; label: string; email: string; access_token: string; expires_at: number | null }[];
239
+
240
+ // Get post-export current tokens (may differ if refreshBeforeExport changed them)
241
+ const postExportTokens = accountIds.map((id) => {
242
+ const acc = accountService.getWithTokens(id);
243
+ if (!acc) return null;
244
+ return { id, label: acc.label, email: acc.email, accessToken: acc.accessToken, expiresAt: acc.expiresAt };
245
+ }).filter(Boolean) as { id: string; label: string | null; email: string | null; accessToken: string; expiresAt: number | null }[];
246
+
247
+ const result = rows.map((row) => {
248
+ const pre = preExportTokens.find((t) => t.id === row.id);
249
+ const post = postExportTokens.find((t) => t.id === row.id);
250
+ return {
251
+ id: row.id,
252
+ label: row.label,
253
+ email: row.email,
254
+ preExportToken: pre?.accessToken ? pre.accessToken.slice(0, 20) + "..." : null,
255
+ preExportTokenFull: pre?.accessToken ?? null,
256
+ exportedToken: row.access_token.slice(0, 20) + "...",
257
+ exportedTokenFull: row.access_token,
258
+ postExportToken: post?.accessToken ? post.accessToken.slice(0, 20) + "..." : null,
259
+ postExportTokenFull: post?.accessToken ?? null,
260
+ preExportExpires: pre?.expiresAt ?? null,
261
+ exportedExpires: row.expires_at,
262
+ postExportExpires: post?.expiresAt ?? null,
263
+ tokenChanged: pre?.accessToken !== post?.accessToken,
264
+ };
265
+ });
266
+ return c.json(ok(result));
267
+ } catch (e) {
268
+ return c.json(err((e as Error).message), 400);
269
+ }
270
+ });
271
+
272
+ /** POST /api/accounts/test-raw-token — test an arbitrary access token against profile API */
273
+ accountsRoutes.post("/test-raw-token", async (c) => {
274
+ const { token } = await c.req.json<{ token: string }>();
275
+ if (!token) return c.json(err("token required"), 400);
276
+ try {
277
+ const res = await fetch("https://api.anthropic.com/api/oauth/profile", {
278
+ headers: {
279
+ Accept: "application/json",
280
+ Authorization: `Bearer ${token}`,
281
+ "anthropic-beta": "oauth-2025-04-20",
282
+ "User-Agent": "ppm/1.0",
283
+ },
284
+ signal: AbortSignal.timeout(10_000),
285
+ });
286
+ if (res.status === 200) return c.json(ok({ status: "valid", code: 200 }));
287
+ if (res.status === 429) return c.json(ok({ status: "valid_rate_limited", code: 429 }));
288
+ const body = await res.text().catch(() => "");
289
+ return c.json(ok({ status: "invalid", code: res.status, error: body.slice(0, 300) }));
290
+ } catch (e) {
291
+ return c.json(ok({ status: "error", error: (e as Error).message }));
292
+ }
293
+ });
294
+
295
+ /** POST /api/accounts/:id/test-token — test access token validity + optionally refresh token */
296
+ accountsRoutes.post("/:id/test-token", async (c) => {
297
+ const { id } = c.req.param();
298
+ const { testRefresh } = await c.req.json<{ testRefresh?: boolean }>().catch(() => ({ testRefresh: false }));
299
+ const account = accountService.getWithTokens(id);
300
+ if (!account) return c.json(err("Account not found"), 404);
301
+
302
+ const result: {
303
+ accessToken: { status: string; code?: number; error?: string };
304
+ refreshToken?: { status: string; code?: number; expiresIn?: number; newRefreshToken?: boolean; error?: string };
305
+ } = { accessToken: { status: "unknown" } };
306
+
307
+ // Test access token via profile API
308
+ try {
309
+ const res = await fetch("https://api.anthropic.com/api/oauth/profile", {
310
+ headers: {
311
+ Accept: "application/json",
312
+ Authorization: `Bearer ${account.accessToken}`,
313
+ "anthropic-beta": "oauth-2025-04-20",
314
+ "User-Agent": "ppm/1.0",
315
+ },
316
+ signal: AbortSignal.timeout(10_000),
317
+ });
318
+ if (res.status === 200) {
319
+ result.accessToken = { status: "valid", code: 200 };
320
+ } else if (res.status === 429) {
321
+ result.accessToken = { status: "valid_rate_limited", code: 429 };
322
+ } else {
323
+ const body = await res.text().catch(() => "");
324
+ result.accessToken = { status: "invalid", code: res.status, error: body.slice(0, 300) };
325
+ }
326
+ } catch (e) {
327
+ result.accessToken = { status: "error", error: (e as Error).message };
328
+ }
329
+
330
+ // Test refresh token
331
+ if (testRefresh && account.refreshToken) {
332
+ try {
333
+ await accountService.refreshAccessToken(id, false);
334
+ const refreshed = accountService.getWithTokens(id);
335
+ result.refreshToken = {
336
+ status: "valid",
337
+ code: 200,
338
+ expiresIn: refreshed?.expiresAt ? refreshed.expiresAt - Math.floor(Date.now() / 1000) : undefined,
339
+ newRefreshToken: true,
340
+ };
341
+ } catch (e) {
342
+ result.refreshToken = { status: "invalid", error: (e as Error).message };
343
+ }
344
+ } else if (testRefresh && !account.refreshToken) {
345
+ result.refreshToken = { status: "no_token", error: "No refresh token (temporary account)" };
346
+ }
347
+
348
+ return c.json(ok(result));
349
+ });
350
+
216
351
  /** DELETE /api/accounts/:id */
217
352
  accountsRoutes.delete("/:id", (c) => {
218
353
  const { id } = c.req.param();
@@ -6,7 +6,7 @@ import { Input } from "@/components/ui/input";
6
6
  import { Label } from "@/components/ui/label";
7
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
8
8
  import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog";
9
- import { Eye, Loader2, Copy, X, Download, Upload, Lock } from "lucide-react";
9
+ import { Eye, Loader2, Copy, X, Download, Upload, Lock, FlaskConical } from "lucide-react";
10
10
  import { getAuthToken } from "../../lib/api-client";
11
11
  import {
12
12
  getAccounts,
@@ -20,10 +20,15 @@ import {
20
20
  updateAccountSettings,
21
21
  getAllAccountUsages,
22
22
  importAccounts,
23
+ testAccountToken,
24
+ testExport,
25
+ testRawToken,
23
26
  type AccountInfo,
24
27
  type AccountSettings,
25
28
  type AccountUsageEntry,
26
29
  type OAuthProfileData,
30
+ type TokenTestResult,
31
+ type ExportedTokenInfo,
27
32
  } from "../../lib/api-settings";
28
33
 
29
34
  function detectTokenType(token: string): string {
@@ -152,6 +157,7 @@ export function AccountsSettingsSection() {
152
157
  const [exportSelected, setExportSelected] = useState<Set<string>>(new Set());
153
158
  const [exporting, setExporting] = useState(false);
154
159
  const [exportFullTransfer, setExportFullTransfer] = useState(false);
160
+ const [exportRefreshBefore, setExportRefreshBefore] = useState(false);
155
161
 
156
162
  // Import dialog
157
163
  const [showImportDialog, setShowImportDialog] = useState(false);
@@ -160,6 +166,16 @@ export function AccountsSettingsSection() {
160
166
  const [importing, setImporting] = useState(false);
161
167
  const [importError, setImportError] = useState<string | null>(null);
162
168
 
169
+ // Token test dialog
170
+ const [showTokenTest, setShowTokenTest] = useState(false);
171
+ const [tokenTestResults, setTokenTestResults] = useState<Map<string, { loading: boolean; result?: TokenTestResult; error?: string }>>(new Map());
172
+ const [tokenTestRefresh, setTokenTestRefresh] = useState(false);
173
+ // Export simulation — accumulate rounds
174
+ const [exportRounds, setExportRounds] = useState<{ round: number; time: string; includeRefresh: boolean; items: ExportedTokenInfo[] }[]>([]);
175
+ const [exportSimLoading, setExportSimLoading] = useState(false);
176
+ const [exportSimIncludeRefresh, setExportSimIncludeRefresh] = useState(false);
177
+ const [rawTokenTests, setRawTokenTests] = useState<Map<string, { loading: boolean; status?: string; code?: number; error?: string }>>(new Map());
178
+
163
179
  useEffect(() => {
164
180
  refresh();
165
181
  }, []);
@@ -304,6 +320,7 @@ export function AccountsSettingsSection() {
304
320
  setExportConfirm("");
305
321
  setExportSelected(new Set(exportableAccounts.map((a) => a.id)));
306
322
  setExportFullTransfer(false);
323
+ setExportRefreshBefore(false);
307
324
  setShowExportDialog(true);
308
325
  }
309
326
 
@@ -325,7 +342,7 @@ export function AccountsSettingsSection() {
325
342
  const res = await fetch("/api/accounts/export", {
326
343
  method: "POST",
327
344
  headers,
328
- body: JSON.stringify({ password: exportPassword, accountIds: [...exportSelected], includeRefreshToken: exportFullTransfer }),
345
+ body: JSON.stringify({ password: exportPassword, accountIds: [...exportSelected], includeRefreshToken: exportFullTransfer, refreshBeforeExport: exportRefreshBefore }),
329
346
  });
330
347
  if (!res.ok) { const j = await res.json() as any; throw new Error(j.error ?? `Export failed: ${res.status}`); }
331
348
  const text = await res.text();
@@ -375,6 +392,46 @@ export function AccountsSettingsSection() {
375
392
  }
376
393
 
377
394
 
395
+ async function simulateExport() {
396
+ const oauthIds = accounts.filter((a) => a.hasRefreshToken).map((a) => a.id);
397
+ if (!oauthIds.length) return;
398
+ setExportSimLoading(true);
399
+ try {
400
+ const data = await testExport(oauthIds, exportSimIncludeRefresh);
401
+ const roundNum = exportRounds.length + 1;
402
+ const time = new Date().toLocaleTimeString();
403
+ setExportRounds((prev) => [...prev, { round: roundNum, time, includeRefresh: exportSimIncludeRefresh, items: data }]);
404
+ } catch { /* ignore */ }
405
+ setExportSimLoading(false);
406
+ }
407
+
408
+ async function testRawTokenClick(key: string, token: string) {
409
+ setRawTokenTests((prev) => new Map(prev).set(key, { loading: true }));
410
+ try {
411
+ const result = await testRawToken(token);
412
+ setRawTokenTests((prev) => new Map(prev).set(key, { loading: false, ...result }));
413
+ } catch (e) {
414
+ setRawTokenTests((prev) => new Map(prev).set(key, { loading: false, status: "error", error: (e as Error).message }));
415
+ }
416
+ }
417
+
418
+ async function runTokenTest(id: string) {
419
+ setTokenTestResults((prev) => new Map(prev).set(id, { loading: true }));
420
+ try {
421
+ const result = await testAccountToken(id, tokenTestRefresh);
422
+ setTokenTestResults((prev) => new Map(prev).set(id, { loading: false, result }));
423
+ } catch (e) {
424
+ setTokenTestResults((prev) => new Map(prev).set(id, { loading: false, error: (e as Error).message }));
425
+ }
426
+ }
427
+
428
+ async function runAllTokenTests() {
429
+ const oauthAccounts = accounts.filter((a) => a.expiresAt !== null);
430
+ for (const acc of oauthAccounts) {
431
+ runTokenTest(acc.id);
432
+ }
433
+ }
434
+
378
435
  const tokenHint = newToken.trim() ? detectTokenType(newToken.trim()) : "";
379
436
 
380
437
  return (
@@ -442,7 +499,7 @@ export function AccountsSettingsSection() {
442
499
  })}
443
500
  </div>
444
501
 
445
- <div className="grid grid-cols-3 gap-1.5">
502
+ <div className={`grid gap-1.5 ${import.meta.env.DEV ? "grid-cols-4" : "grid-cols-3"}`}>
446
503
  <Button size="sm" className="h-8 text-xs cursor-pointer" onClick={() => setShowAddDialog(true)}>
447
504
  + Add
448
505
  </Button>
@@ -452,6 +509,11 @@ export function AccountsSettingsSection() {
452
509
  <Button size="sm" variant="outline" className="h-8 text-xs cursor-pointer" onClick={() => openImportDialog()}>
453
510
  <Upload className="size-3.5 mr-1" /> Import
454
511
  </Button>
512
+ {import.meta.env.DEV && (
513
+ <Button size="sm" variant="outline" className="h-8 text-xs cursor-pointer" onClick={() => { setTokenTestResults(new Map()); setTokenTestRefresh(false); setExportRounds([]); setRawTokenTests(new Map()); setExportSimIncludeRefresh(false); setShowTokenTest(true); }}>
514
+ <FlaskConical className="size-3.5 mr-1" /> Test
515
+ </Button>
516
+ )}
455
517
  </div>
456
518
  </div>
457
519
 
@@ -715,6 +777,19 @@ export function AccountsSettingsSection() {
715
777
  Include refresh tokens (full transfer)
716
778
  </label>
717
779
  </div>
780
+ {/* Refresh before export toggle */}
781
+ <div className="flex items-center gap-2">
782
+ <input
783
+ type="checkbox"
784
+ id="export-refresh-before"
785
+ checked={exportRefreshBefore}
786
+ onChange={(e) => setExportRefreshBefore(e.target.checked)}
787
+ className="size-3.5 accent-primary cursor-pointer"
788
+ />
789
+ <label htmlFor="export-refresh-before" className="text-[11px] cursor-pointer">
790
+ Refresh tokens before export
791
+ </label>
792
+ </div>
718
793
  {exportFullTransfer ? (
719
794
  <div className="rounded-md border border-red-500/30 bg-red-500/5 p-2.5 space-y-1">
720
795
  <p className="text-[10px] font-medium text-red-600">Full transfer — source accounts will expire</p>
@@ -722,11 +797,18 @@ export function AccountsSettingsSection() {
722
797
  Refresh tokens are included. Once the target machine refreshes, <strong>accounts on this machine will only work for ~1h</strong> then become temporary. Use this only to move accounts to another machine.
723
798
  </p>
724
799
  </div>
725
- ) : (
800
+ ) : exportRefreshBefore ? (
726
801
  <div className="rounded-md border border-amber-500/30 bg-amber-500/5 p-2.5 space-y-1">
727
- <p className="text-[10px] font-medium text-amber-600">Temporary access only (default)</p>
802
+ <p className="text-[10px] font-medium text-amber-600">Refresh before export — invalidates previous shares</p>
803
+ <p className="text-[10px] text-muted-foreground leading-relaxed">
804
+ Tokens will be refreshed to maximize validity (~1h). But <strong>any previously shared tokens will be invalidated</strong> because Anthropic only allows 1 active access token per account.
805
+ </p>
806
+ </div>
807
+ ) : (
808
+ <div className="rounded-md border border-green-500/30 bg-green-500/5 p-2.5 space-y-1">
809
+ <p className="text-[10px] font-medium text-green-600">Share current token (default, safe)</p>
728
810
  <p className="text-[10px] text-muted-foreground leading-relaxed">
729
- Only access tokens exported (~1h validity). Source machine is not affected. Target must login directly for permanent access.
811
+ Exports the current access token as-is. Host and target share the same token. No invalidation. Token validity = remaining time until next auto-refresh.
730
812
  </p>
731
813
  </div>
732
814
  )}
@@ -776,6 +858,173 @@ export function AccountsSettingsSection() {
776
858
  </DialogContent>
777
859
  </Dialog>
778
860
 
861
+ {/* Token test dialog */}
862
+ <Dialog open={showTokenTest} onOpenChange={(v) => { if (!v) setShowTokenTest(false); }}>
863
+ <DialogContent className="sm:max-w-xl max-h-[85vh] flex flex-col">
864
+ <DialogHeader>
865
+ <DialogTitle className="text-sm flex items-center gap-1.5"><FlaskConical className="size-3.5" /> Token Test</DialogTitle>
866
+ <DialogDescription className="text-xs">Test tokens & simulate export to compare token validity.</DialogDescription>
867
+ </DialogHeader>
868
+ <div className="space-y-3 overflow-y-auto flex-1 pr-1">
869
+ {/* Section 1: Quick test current DB tokens */}
870
+ <div className="space-y-2">
871
+ <div className="flex items-center justify-between">
872
+ <p className="text-[11px] font-medium">Current Tokens (DB)</p>
873
+ <Button size="sm" variant="outline" className="h-6 text-[10px] cursor-pointer" onClick={runAllTokenTests}>
874
+ Test All
875
+ </Button>
876
+ </div>
877
+ <div className="space-y-1.5">
878
+ {accounts.map((acc) => {
879
+ const entry = tokenTestResults.get(acc.id);
880
+ return (
881
+ <div key={acc.id} className="p-2 rounded border bg-card space-y-1">
882
+ <div className="flex items-center justify-between gap-2">
883
+ <div className="min-w-0 flex-1">
884
+ <span className="text-[11px] font-medium truncate block">{acc.label ?? acc.email ?? acc.id.slice(0, 8)}</span>
885
+ {acc.expiresAt && (
886
+ <span className="text-[10px] text-muted-foreground">
887
+ {acc.expiresAt > Math.floor(Date.now() / 1000)
888
+ ? `${Math.floor((acc.expiresAt - Math.floor(Date.now() / 1000)) / 60)}m left`
889
+ : `expired ${Math.floor((Math.floor(Date.now() / 1000) - acc.expiresAt) / 60)}m ago`}
890
+ </span>
891
+ )}
892
+ </div>
893
+ <Button size="sm" variant="outline" className="h-6 text-[10px] cursor-pointer shrink-0" disabled={entry?.loading} onClick={() => runTokenTest(acc.id)}>
894
+ {entry?.loading ? <Loader2 className="size-3 animate-spin" /> : "Test"}
895
+ </Button>
896
+ </div>
897
+ {entry && !entry.loading && (
898
+ <div className="text-[10px] pl-1 border-l-2 border-muted ml-1">
899
+ {entry.error && <p className="text-red-500">{entry.error}</p>}
900
+ {entry.result && (
901
+ <span className={entry.result.accessToken.status.startsWith("valid") ? "text-green-600" : "text-red-500"}>
902
+ {entry.result.accessToken.status} {entry.result.accessToken.code && `(${entry.result.accessToken.code})`}
903
+ </span>
904
+ )}
905
+ </div>
906
+ )}
907
+ </div>
908
+ );
909
+ })}
910
+ </div>
911
+ </div>
912
+
913
+ {/* Section 2: Export simulation */}
914
+ <div className="border-t pt-3 space-y-2">
915
+ <div className="flex items-center justify-between">
916
+ <p className="text-[11px] font-medium">Simulate Export</p>
917
+ {exportRounds.length > 0 && (
918
+ <span className="text-[10px] text-muted-foreground">{exportRounds.length} round(s)</span>
919
+ )}
920
+ </div>
921
+ <p className="text-[10px] text-muted-foreground">
922
+ Each "Run Export" appends a new round. All tokens from every round are kept for comparison.
923
+ </p>
924
+ <div className="flex items-center gap-3">
925
+ <div className="flex items-center gap-1.5">
926
+ <input
927
+ type="checkbox"
928
+ id="sim-include-refresh"
929
+ checked={exportSimIncludeRefresh}
930
+ onChange={(e) => setExportSimIncludeRefresh(e.target.checked)}
931
+ className="size-3 accent-primary cursor-pointer"
932
+ />
933
+ <label htmlFor="sim-include-refresh" className="text-[10px] cursor-pointer">Include refresh tokens</label>
934
+ </div>
935
+ <Button size="sm" className="h-7 text-[11px] cursor-pointer" disabled={exportSimLoading} onClick={simulateExport}>
936
+ {exportSimLoading ? <><Loader2 className="size-3 animate-spin mr-1" /> Exporting...</> : `Run Export #${exportRounds.length + 1}`}
937
+ </Button>
938
+ </div>
939
+
940
+ {exportRounds.length > 0 && (
941
+ <div className="space-y-3">
942
+ {/* Test All button across all rounds */}
943
+ <Button
944
+ size="sm"
945
+ variant="outline"
946
+ className="w-full h-7 text-[10px] cursor-pointer"
947
+ onClick={() => {
948
+ for (const round of exportRounds) {
949
+ for (const item of round.items) {
950
+ if (item.preExportTokenFull) testRawTokenClick(`r${round.round}-pre-${item.id}`, item.preExportTokenFull);
951
+ if (item.exportedTokenFull) testRawTokenClick(`r${round.round}-exp-${item.id}`, item.exportedTokenFull);
952
+ if (item.postExportTokenFull) testRawTokenClick(`r${round.round}-post-${item.id}`, item.postExportTokenFull);
953
+ }
954
+ }
955
+ }}
956
+ >
957
+ Test All Tokens ({exportRounds.reduce((n, r) => n + r.items.length * 3, 0)} tokens)
958
+ </Button>
959
+
960
+ {/* Render each round */}
961
+ {exportRounds.map((round) => (
962
+ <div key={round.round} className="space-y-1.5">
963
+ <div className="flex items-center gap-2">
964
+ <p className="text-[11px] font-medium text-primary">Round #{round.round}</p>
965
+ <span className="text-[9px] text-muted-foreground">{round.time}</span>
966
+ <Badge variant={round.includeRefresh ? "destructive" : "secondary"} className="text-[8px] px-1 py-0">
967
+ {round.includeRefresh ? "with refresh" : "access only"}
968
+ </Badge>
969
+ </div>
970
+ {round.items.map((item) => (
971
+ <div key={`r${round.round}-${item.id}`} className="p-2 rounded-lg border bg-card space-y-1.5">
972
+ <div className="flex items-center gap-2">
973
+ <span className="text-[10px] font-medium">{item.label ?? item.email ?? item.id.slice(0, 8)}</span>
974
+ {item.tokenChanged && <Badge variant="secondary" className="text-[8px] px-1 py-0">DB token changed</Badge>}
975
+ </div>
976
+ {[
977
+ { key: `r${round.round}-pre-${item.id}`, label: "Pre-export", token: item.preExportTokenFull, preview: item.preExportToken, expires: item.preExportExpires },
978
+ { key: `r${round.round}-exp-${item.id}`, label: "Exported", token: item.exportedTokenFull, preview: item.exportedToken, expires: item.exportedExpires },
979
+ { key: `r${round.round}-post-${item.id}`, label: "Post-export", token: item.postExportTokenFull, preview: item.postExportToken, expires: item.postExportExpires },
980
+ ].map((row) => {
981
+ const test = rawTokenTests.get(row.key);
982
+ return (
983
+ <div key={row.key} className="flex items-center gap-1.5 text-[10px]">
984
+ <span className="w-20 shrink-0 text-muted-foreground text-[9px]">{row.label}</span>
985
+ <code className="flex-1 truncate font-mono text-[9px] bg-muted px-1 rounded">{row.preview ?? "N/A"}</code>
986
+ {row.expires && (
987
+ <span className="text-[9px] text-muted-foreground shrink-0">
988
+ {row.expires > Math.floor(Date.now() / 1000)
989
+ ? `${Math.floor((row.expires - Math.floor(Date.now() / 1000)) / 60)}m`
990
+ : `exp`}
991
+ </span>
992
+ )}
993
+ {row.token ? (
994
+ <Button
995
+ size="sm"
996
+ variant="outline"
997
+ className="h-5 text-[9px] px-1.5 cursor-pointer shrink-0"
998
+ disabled={test?.loading}
999
+ onClick={() => testRawTokenClick(row.key, row.token!)}
1000
+ >
1001
+ {test?.loading ? <Loader2 className="size-2.5 animate-spin" /> : "Test"}
1002
+ </Button>
1003
+ ) : <span className="text-[9px] text-muted-foreground">-</span>}
1004
+ {test && !test.loading && (
1005
+ <span className={`text-[9px] font-medium shrink-0 ${test.status?.startsWith("valid") ? "text-green-600" : "text-red-500"}`}>
1006
+ {test.status}
1007
+ </span>
1008
+ )}
1009
+ </div>
1010
+ );
1011
+ })}
1012
+ </div>
1013
+ ))}
1014
+ </div>
1015
+ ))}
1016
+ </div>
1017
+ )}
1018
+ </div>
1019
+ </div>
1020
+ <DialogFooter>
1021
+ <Button size="sm" variant="outline" className="text-xs h-7 cursor-pointer" onClick={() => setShowTokenTest(false)}>
1022
+ Close
1023
+ </Button>
1024
+ </DialogFooter>
1025
+ </DialogContent>
1026
+ </Dialog>
1027
+
779
1028
  {/* Import dialog — paste/file data + password */}
780
1029
  <Dialog open={showImportDialog} onOpenChange={(v) => { if (!v) setShowImportDialog(false); }}>
781
1030
  <DialogContent className="sm:max-w-md">
@@ -119,6 +119,39 @@ export function importAccounts(params: { data: string; password: string }): Prom
119
119
  return api.post<{ imported: number; refreshed: number }>("/api/accounts/import", params);
120
120
  }
121
121
 
122
+ export interface TokenTestResult {
123
+ accessToken: { status: string; code?: number; error?: string };
124
+ refreshToken?: { status: string; code?: number; expiresIn?: number; newRefreshToken?: boolean; error?: string };
125
+ }
126
+
127
+ export function testAccountToken(id: string, testRefresh = false): Promise<TokenTestResult> {
128
+ return api.post<TokenTestResult>(`/api/accounts/${id}/test-token`, { testRefresh });
129
+ }
130
+
131
+ export interface ExportedTokenInfo {
132
+ id: string;
133
+ label: string;
134
+ email: string;
135
+ preExportToken: string | null;
136
+ preExportTokenFull: string | null;
137
+ exportedToken: string | null;
138
+ exportedTokenFull: string | null;
139
+ postExportToken: string | null;
140
+ postExportTokenFull: string | null;
141
+ preExportExpires: number | null;
142
+ exportedExpires: number | null;
143
+ postExportExpires: number | null;
144
+ tokenChanged: boolean;
145
+ }
146
+
147
+ export function testExport(accountIds: string[], includeRefreshToken = false): Promise<ExportedTokenInfo[]> {
148
+ return api.post<ExportedTokenInfo[]>("/api/accounts/test-export", { accountIds, includeRefreshToken });
149
+ }
150
+
151
+ export function testRawToken(token: string): Promise<{ status: string; code?: number; error?: string }> {
152
+ return api.post<{ status: string; code?: number; error?: string }>("/api/accounts/test-raw-token", { token });
153
+ }
154
+
122
155
  export interface AIProviderSettings {
123
156
  type?: string;
124
157
  api_key_env?: string;