publishport-opencli 1.0.14 → 1.0.15

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.
@@ -1,6 +1,6 @@
1
- import{CommandExecutionError as Y}from"@jackwener/opencli/errors";import{PAGE_RUNTIME as Z}from"./page-runtime.js";import{gotoWritePage as $}from"./publish.js";export function buildCheckAuthJs(z){return`(async () => {
1
+ import{CommandExecutionError as X}from"@jackwener/opencli/errors";import{PAGE_RUNTIME as Z}from"./page-runtime.js";import{gotoWritePage as $}from"./publish.js";export function buildCheckAuthJs(B){return`(async () => {
2
2
  `+Z+`
3
- const __checkAuth = (`+z+`);
3
+ const __checkAuth = (`+B+`);
4
4
  try {
5
5
  const r = await __checkAuth(PP);
6
6
  return {
@@ -11,6 +11,6 @@ try {
11
11
  error: (r && r.error) || "",
12
12
  };
13
13
  } catch (e) {
14
- return { isAuthenticated: false, userId: "", username: "", avatar: "", error: String((e && e.message) || e) };
14
+ return { isAuthenticated: false, probeFailed: true, userId: "", username: "", avatar: "", error: String((e && e.message) || e) };
15
15
  }
16
- })()`}export async function checkLogin(z,v){if(!v||!v.home)throw Error("checkLogin: profile.home is required");if(typeof v.checkAuth!=="function")throw Error("checkLogin: profile.checkAuth must be a function");try{await $(z,v.home,v.originRe)}catch(V){return{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"未登录(无法进入需登录的页面,疑似被重定向到登录页)"}}const D=buildCheckAuthJs(v.checkAuth.toString());return await z.evaluate(D)||{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"no result"}}export async function requireLogin(z,v,D){const B=await checkLogin(z,v);if(!B.isAuthenticated)throw new Y(`未登录${D||""}。请先在已连接 ppcli 浏览器桥的 Chrome 里登录该平台。`+(B.error?`(${B.error})`:""));return B}export function cookieQuickCheck(z,v,D=[]){const B=new Set(v);return async(V)=>{return{logged_in:(await V.getCookies({url:z})).some((K)=>K.value&&(B.has(K.name)||D.some((X)=>K.name.startsWith(X))))}}}export const __test__={buildCheckAuthJs,checkLogin,requireLogin,cookieQuickCheck};
16
+ })()`}export async function checkLogin(B,z){if(!z||!z.home)throw Error("checkLogin: profile.home is required");if(typeof z.checkAuth!=="function")throw Error("checkLogin: profile.checkAuth must be a function");try{await $(B,z.home,z.originRe)}catch(D){if(!D||!D.landedOffSite)throw D;return{isAuthenticated:!1,userId:"",username:"",avatar:"",error:"未登录(访问需登录的页面时被重定向到 "+(D.lastHref||"其它页面")+")"}}const K=buildCheckAuthJs(z.checkAuth.toString()),v=await B.evaluate(K);if(!v)throw new X("登录检测未返回结果(页面内脚本没有执行成功),无法判定登录状态。");if(v.probeFailed)throw new X("登录检测失败(页面内请求异常:"+(v.error||"未知错误")+")。这不代表未登录,请稍后重试。");return v}export async function requireLogin(B,z,K){const v=await checkLogin(B,z);if(!v.isAuthenticated)throw new X(`未登录${K||""}。请先在已连接 ppcli 浏览器桥的 Chrome 里登录该平台。`+(v.error?`(${v.error})`:""));return v}export function cookieQuickCheck(B,z,K=[]){const v=new Set(z);return async(D)=>{return{logged_in:(await D.getCookies({url:B})).some((V)=>V.value&&(v.has(V.name)||K.some((Y)=>V.name.startsWith(Y))))}}}export const __test__={buildCheckAuthJs,checkLogin,requireLogin,cookieQuickCheck};
@@ -1,9 +1,9 @@
1
- import{CommandExecutionError as O}from"@jackwener/opencli/errors";import{normalizeContent as H}from"./format.js";import{assertLiteralContent as C}from"../content-guard.js";import{inlineLocalImages as U,isLocalImagePath as I,localImageToDataUri as E}from"./images.js";import{PAGE_RUNTIME as S}from"./page-runtime.js";export function selectContent(K,Q,X="auto"){const Z=H(K,{format:X});return Q.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(K){let Q="";try{Q=new URL(K).host}catch(Z){Q=""}return"^https?://([^/]*\\.)?"+Q.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(K,Q,X){const Z=X||originReFromHome(Q);for(let G=0;G<8;G++){await K.goto(Q);await K.wait({time:1});const D=await K.evaluate(`(() => {
1
+ import{CommandExecutionError as _}from"@jackwener/opencli/errors";import{normalizeContent as k}from"./format.js";import{assertLiteralContent as C}from"../content-guard.js";import{inlineLocalImages as w,isLocalImagePath as I,localImageToDataUri as E}from"./images.js";import{PAGE_RUNTIME as S}from"./page-runtime.js";export function selectContent(K,Q,X="auto"){const Z=k(K,{format:X});return Q.outputFormat==="markdown"?Z.markdown:Z.html}export function originReFromHome(K){let Q="";try{Q=new URL(K).host}catch(Z){Q=""}return"^https?://([^/]*\\.)?"+Q.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(/|$)"}export async function gotoWritePage(K,Q,X){const Z=X||originReFromHome(Q);let O=null;for(let W=0;W<8;W++){await K.goto(Q);await K.wait({time:1});const $=await K.evaluate(`(() => {
2
2
  var href = ''; try { href = location.href; } catch (e) {}
3
3
  var onSite = new RegExp(${JSON.stringify(Z)}).test(href);
4
4
  var cookieOk = true; try { void document.cookie; } catch (e) { cookieOk = false; }
5
5
  return { href: href, onSite: onSite, cookieOk: cookieOk };
6
- })()`);if(D&&D.onSite&&D.cookieOk)return;await K.wait({time:1})}throw new O(`无法在浏览器中打开已登录的写作页(页面停留在空白页,未落到 ${Q})。`+"请确认登录该平台的 Chrome 已连接 ppcli 浏览器桥。")}export function buildPublishJs(K,Q,X){return`(async () => {
6
+ })()`);if($&&$.onSite&&$.cookieOk)return;if($)O=$;await K.wait({time:1})}const G=O&&O.href||"",q=new _(`无法在浏览器中打开已登录的写作页(未落到 ${Q}${G?`,当前停留在 ${G}`:",页面停留在空白页"})。`+"请确认登录该平台的 Chrome 已连接 ppcli 浏览器桥。");q.lastHref=G;q.landedOffSite=/^https?:/i.test(G);throw q}export function buildPublishJs(K,Q,X){return`(async () => {
7
7
  `+S+`
8
8
  const I = `+JSON.stringify(K)+`;
9
9
  const __publish = (`+Q+`);
@@ -31,4 +31,4 @@ if (!__pub || __pub.ok === false) {
31
31
  return { ok: false, stage: (__pub && __pub.stage) || "publish", status: __pub && __pub.status, message: (__pub && __pub.message) || "publish failed", detail: __pub && __pub.detail, uploaded: __upN, failed: __failN };
32
32
  }
33
33
  return { ok: true, id: String(__pub.id == null ? "" : __pub.id), url: __pub.url || "", draft: !!__pub.draft, uploaded: __upN, failed: __failN };
34
- })()`}export async function publishArticle(K,Q){const{title:X,body:Z,format:G="auto",draftOnly:D=!1,profile:q,publishParams:k=null}=Q;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");C(Z,{fileFlag:"--file"});const W=H(Z,{format:G}),w=q.outputFormat==="markdown"||q.needsMarkdown===!0,L=q.outputFormat==="html"||q.needsHtml===!0,_=w?await U(W.markdown):{content:null,missing:[]},z=L?await U(W.html):{content:null,missing:[]},A=_.content,J=z.content,y=q.outputFormat==="markdown"?A:J,N=[],T=new Set;for(const V of[..._.missing,...z.missing]){if(T.has(V.src))continue;T.add(V.src);N.push(V)}let Y=k;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new O("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(I(V))try{const{dataUri:$}=await E(V);Y={...Y,cover:$}}catch($){throw new O("封面图读取失败:"+V+"("+String($&&$.message||$)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const R={title:X,content:y,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?A:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?J:null,draftOnly:!!D,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],imageAllowRelative:q.image?.allowRelative===!0,publishParams:Y},j=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,x=buildPublishJs(R,q.publish.toString(),j),B=await K.evaluate(x);if(!B||B.ok===!1){const V=B?.stage||"publish",$=B?.status!=null?` (HTTP ${B.status})`:"",M=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";throw new O(`[${V}] ${B?.message||"发布失败"}${$}${M}`)}return{id:B.id,url:B.url,draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(N)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
34
+ })()`}export async function publishArticle(K,Q){const{title:X,body:Z,format:O="auto",draftOnly:G=!1,profile:q,publishParams:W=null}=Q;if(!q)throw Error("publishArticle: profile is required");if(!q.home)throw Error("publishArticle: profile.home is required");if(typeof q.publish!=="function")throw Error("publishArticle: profile.publish must be a function");C(Z,{fileFlag:"--file"});const $=k(Z,{format:O}),L=q.outputFormat==="markdown"||q.needsMarkdown===!0,y=q.outputFormat==="html"||q.needsHtml===!0,z=L?await w($.markdown):{content:null,missing:[]},A=y?await w($.html):{content:null,missing:[]},J=z.content,N=A.content,R=q.outputFormat==="markdown"?J:N,T=[],U=new Set;for(const V of[...z.missing,...A.missing]){if(U.has(V.src))continue;U.add(V.src);T.push(V)}let Y=W;if(Y&&typeof Y.cover==="string"&&Y.cover){const V=Y.cover.trim();if(/["<>\s]/.test(V))throw new _("封面图路径/URL 含非法字符(引号/尖括号/空白):"+V.slice(0,120));if(I(V))try{const{dataUri:D}=await E(V);Y={...Y,cover:D}}catch(D){throw new _("封面图读取失败:"+V+"("+String(D&&D.message||D)+")")}else Y={...Y,cover:V}}await gotoWritePage(K,q.home,q.originRe);const j={title:X,content:R,markdown:q.needsMarkdown===!0&&q.outputFormat!=="markdown"?J:null,html:q.needsHtml===!0&&q.outputFormat!=="html"?N:null,draftOnly:!!G,outputFormat:q.outputFormat,preprocessConfig:q.preprocessConfig||null,imageSpec:q.image?.spec||null,imageSkip:q.image?.skip||[],imageAllowRelative:q.image?.allowRelative===!0,publishParams:Y},x=typeof q.image?.uploadFn==="function"?q.image.uploadFn.toString():null,H=buildPublishJs(j,q.publish.toString(),x),B=await K.evaluate(H);if(!B||B.ok===!1){const V=B?.stage||"publish",D=B?.status!=null?` (HTTP ${B.status})`:"",M=B?.detail!=null?`;接口原始响应:${typeof B.detail==="string"?B.detail:JSON.stringify(B.detail)}`:"";throw new _(`[${V}] ${B?.message||"发布失败"}${D}${M}`)}return{id:B.id,url:B.url,draft:B.draft,images:{uploaded:B.uploaded||[],failed:(B.failed||[]).concat(T)}}}export const __test__={selectContent,originReFromHome,buildPublishJs,publishArticle};
@@ -7,8 +7,12 @@ export interface AuthStatusRow {
7
7
  status: AuthStatus;
8
8
  logged_in: boolean | '';
9
9
  identity: string;
10
- checked: AuthStatusMode | 'skipped' | 'navigation_required';
10
+ checked: AuthStatusMode | 'skipped' | 'navigation_required' | 'cached';
11
11
  error: string;
12
+ /** [pp-only] 该结论最后一次**真实探测**的时间(ISO)。缓存命中时指向原探测时刻。 */
13
+ checked_at?: string;
14
+ /** [pp-only] 该「已登录」是否经 whoami 实证(false = 仅按 cookie 存在推断)。 */
15
+ verified?: boolean;
12
16
  }
13
17
  interface AuthStatusOptions {
14
18
  sites?: string;
@@ -17,6 +21,12 @@ interface AuthStatusOptions {
17
21
  concurrency?: string | number;
18
22
  timeout?: string | number;
19
23
  profile?: string;
24
+ /** [pp-only] 复用不超过该秒数的本机缓存结论,命中就不再驱动浏览器。0 = 每次真探。 */
25
+ maxAge?: string | number;
26
+ /** [pp-only] 本轮最多用 whoami 实证几个「cookie 说已登录但尚未实证」的站点。 */
27
+ verify?: string | number;
28
+ statePath?: string;
29
+ now?: Date;
20
30
  }
21
31
  export interface AuthRefreshRow {
22
32
  site: string;
@@ -1,2 +1,3 @@
1
- import{mkdir as y,readFile as w,writeFile as g}from"node:fs/promises";import{homedir as h}from"node:os";import{dirname as l,join as u}from"node:path";import{pathToFileURL as n}from"node:url";import{InvalidArgumentError as I,Option as p}from"commander";import{AuthRequiredError as P,CliError as F,getErrorMessage as M}from"../errors.js";import{executeCommand as N}from"../execution.js";import{ensureBrowserBridgeReady as i}from"../browser/daemon-lifecycle.js";import{resolveProfile as a}from"../browser/profile.js";import{isElectronApp as U}from"../electron-apps.js";import{fullName as c,getRegistry as _}from"../registry.js";import{render as x}from"../output.js";const V=1,q=86400000;function j(G,J,X){if(G===void 0||G===null||G==="")return X;const Z=Number(G);if(!Number.isInteger(Z)||Z<=0)throw new I(`${J} must be a positive integer. Received: "${String(G)}"`);return Z}function C(G){if(!G||!G.trim())return null;const J=G.split(",").map((X)=>X.trim()).filter(Boolean);return J.length>0?new Set(J):null}function m(){return u(h(),".ppcli","auth-refresh.json")}function d(){return{version:V,sites:{}}}async function o(G){try{const J=JSON.parse(await w(G,"utf8"));if(J&&J.version===V&&J.sites&&typeof J.sites==="object")return{version:V,sites:J.sites}}catch(J){if(J.code!=="ENOENT")throw J}return d()}async function s(G,J){await y(l(G),{recursive:!0});await g(G,`${JSON.stringify(J,null,2)}
2
- `,"utf8")}function T(G){if(!G?.last_touched_at)return null;const J=Date.parse(G.last_touched_at);return Number.isFinite(J)?J:null}function r(G,J){const X=T(G);return X!==null&&J.getTime()-X<q}function B(G){const J=T(G);return J===null?"":new Date(J+q).toISOString()}function b(){const G=new Set;return[..._().values()].filter((J)=>{if(G.has(J))return!1;G.add(J);return J.name==="whoami"&&J.access==="read"}).sort((J,X)=>J.site.localeCompare(X.site))}async function O(G){const J=G;if(!J._lazy||!J._modulePath)return G;await import(n(J._modulePath).href);return _().get(c(G))??G}function z(G,J){const X=G.args.some((Z)=>Z.name==="timeout");return{...G,args:X?G.args:[...G.args,{name:"timeout",type:"int",default:J,help:"Per-site auth command timeout in seconds"}]}}function t(G,J){if(G.browser!==!0||typeof G.authStatus?.quickCheck!=="function")return null;return z({...G,func:G.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},J)}function v(G){if(typeof G==="boolean")return G;if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G.logged_in;if(typeof J==="boolean")return J}return null}function k(G){if(G===void 0||G===null)return"";if(typeof G==="string"||typeof G==="number"||typeof G==="boolean")return String(G);return""}function S(G){if(!G||typeof G!=="object"||Array.isArray(G))return"";const J=G,X=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const Z of["username","handle","user_id","id","name","nickname","user_type","url"]){if(X.test(Z))continue;const Y=k(J[Z]);if(Y)return Y}for(const[Z,Y]of Object.entries(J)){if(Z==="site"||Z==="logged_in"||X.test(Z))continue;const $=k(Y);if($)return $}return""}function H(G){return G instanceof P||typeof G==="object"&&G!==null&&G.code==="AUTH_REQUIRED"}function A(G,J,X){if(H(X))return{site:G,status:"not_logged_in",logged_in:!1,identity:"",checked:J,error:""};const Z=X instanceof F?X.code:"",Y=M(X);return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:Z?`${Z}: ${Y}`:Y}}function e(G,J){if(G.browser!==!0)return null;let X=G.authStatus?.refresh;if(typeof X!=="function"){const Z=G.authStatus?.quickCheck;if(typeof Z!=="function"||!G.domain)return null;const Y=G.domain.startsWith("http://")||G.domain.startsWith("https://")?G.domain:`https://${G.domain}`;X=async($,D,Q)=>{await $.goto(Y);await $.wait(1);if(v(await Z($,D,Q))!==!0)throw new P(G.domain??G.site,`Auth refresh quickCheck failed for ${G.site}`);return{status:"touched"}}}return z({...G,func:X,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},J)}function GG(G){if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G;if(J.status==="refreshed"||J.refreshed===!0)return"refreshed"}return"touched"}function JG(G,J,X){if(H(X))return{site:G,status:"not_logged_in",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:""};const Z=X instanceof F?X.code:"",Y=M(X);return{site:G,status:"error",last_touched_at:J?.last_touched_at??"",next_refresh_at:B(J),error:Z?`${Z}: ${Y}`:Y}}function E(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}async function XG(G,J){try{const X=await O(G);if(X.browser!==!0){const D=z(X,J.timeoutSeconds),Q=await N(D,{timeout:J.timeoutSeconds},!1,{...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:S(Q),checked:"quick",error:""}}if(X.authStatus?.quickCheckRequiresNavigation===!0)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const Z=t(X,J.timeoutSeconds);if(!Z)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const Y=await N(Z,{timeout:J.timeoutSeconds},!1,{...E(),windowMode:"background",...J.profile?{profile:J.profile}:{}}),$=v(Y);if($===!0)return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if($===!1)return{site:G.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(X){return A(G.site,"quick",X)}}async function YG(G,J){try{const X=await O(G),Z=z(X,J.timeoutSeconds),Y=await N(Z,{timeout:J.timeoutSeconds},!1,{...E(),windowMode:"background",...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:S(Y),checked:"full",error:""}}catch(X){return A(G.site,"full",X)}}function ZG(G,J){return J?`${G}@${J}`:G}async function $G(G,J){const X=ZG(G.site,J.profile),Z=J.state.sites[X];if(!J.force&&r(Z,J.now))return{site:G.site,status:"skipped",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:""};const Y=J.now.toISOString();try{const $=await O(G),D=e($,J.timeoutSeconds);if(!D){J.state.sites[X]={...Z,last_attempt_at:Y,last_status:"unsupported"};return{site:G.site,status:"unsupported",last_touched_at:Z?.last_touched_at??"",next_refresh_at:B(Z),error:"refresh probe is not available for this site"}}const Q=await N(D,{timeout:J.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...J.profile?{profile:J.profile}:{}}),K=GG(Q);J.state.sites[X]={...Z,last_attempt_at:Y,last_touched_at:Y,last_status:K};return{site:G.site,status:K,last_touched_at:Y,next_refresh_at:new Date(J.now.getTime()+q).toISOString(),error:""}}catch($){const D=H($)?"not_logged_in":"error";J.state.sites[X]={...Z,last_attempt_at:Y,last_status:D};return JG(G.site,Z,$)}}async function R(G,J){const X=a(J);if(X.kind!=="extension")return null;if(!G.some((Y)=>Y.browser===!0&&!U(Y.site)))return null;try{await i({timeoutSeconds:15,contextId:X.contextId,verbose:!1});return null}catch(Y){return M(Y)}}function DG(G,J,X){return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:`BROWSER_CONNECT: ${X}`}}async function f(G,J,X){const Z=Array(G.length);let Y=0;const $=Array.from({length:Math.min(J,G.length)},async()=>{while(Y<G.length){const D=Y++;Z[D]=await X(G[D])}});await Promise.all($);return Z}export async function collectAuthStatus(G){const J=C(G.sites),X=G.full?"full":"quick",Z=j(G.concurrency,"--concurrency",X==="full"?3:8),Y=j(G.timeout,"--timeout",X==="full"?20:8),$=String(G.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes($))throw new I("--only must be one of: all, logged-in, not-logged-in, unknown, error");const D=b().filter((W)=>!J||J.has(W.site)),Q=await R(D,G.profile),K=await f(D,Z,(W)=>Q&&W.browser===!0&&!U(W.site)?Promise.resolve(DG(W.site,X,Q)):X==="full"?YG(W,{timeoutSeconds:Y,profile:G.profile}):XG(W,{timeoutSeconds:Y,profile:G.profile})),L=$.replace(/-/g,"_");return L==="all"?K:K.filter((W)=>W.status===L)}export async function collectAuthRefresh(G){const J=C(G.sites),X=j(G.concurrency,"--concurrency",3),Z=j(G.timeout,"--timeout",20),Y=G.statePath??m(),$=G.now??new Date,D=await o(Y),Q=b().filter((W)=>!J||J.has(W.site)),K=await R(Q,G.profile),L=await f(Q,X,(W)=>K&&W.browser===!0&&!U(W.site)?Promise.resolve({site:W.site,status:"error",last_touched_at:D.sites[W.site]?.last_touched_at??"",next_refresh_at:B(D.sites[W.site]),error:`BROWSER_CONNECT: ${K}`}):$G(W,{timeoutSeconds:Z,profile:G.profile,now:$,state:D,force:G.all===!0}));await s(Y,D);return L}export function registerAuthCommands(G){const J=G.command("auth").description("Inspect website login status"),X=J.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").addOption(new p("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(Y)=>{const $=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},D=await collectAuthStatus({sites:Y.site,full:Y.full===!0,concurrency:Y.concurrency,timeout:Y.timeout,only:Y.only,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),Q=typeof Y.format==="string"?Y.format:"table";x(D,{fmt:Q,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"ppcli/auth status",source:Y.full?"full whoami probe":"quick auth check"})}),Z=J.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(Y)=>{const $=typeof Z.optsWithGlobals==="function"?Z.optsWithGlobals():{},D=await collectAuthRefresh({sites:Y.site,all:Y.all===!0,concurrency:Y.concurrency,timeout:Y.timeout,profile:typeof $.profile==="string"&&$.profile.trim()?$.profile.trim():void 0}),Q=typeof Y.format==="string"?Y.format:"table";x(D,{fmt:Q,fmtExplicit:Z.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"ppcli/auth refresh",source:Y.all?"forced persistent touch":"persistent touch with 24h throttle"})});return J}
1
+ import{mkdir as l,readFile as u,rename as KG,writeFile as p}from"node:fs/promises";import{homedir as n}from"node:os";import{dirname as c,join as i}from"node:path";import{pathToFileURL as LG}from"node:url";import{InvalidArgumentError as k,Option as jG}from"commander";import{AuthRequiredError as m,CliError as a,getErrorMessage as b}from"../errors.js";import{executeCommand as H}from"../execution.js";import{ensureBrowserBridgeReady as zG}from"../browser/daemon-lifecycle.js";import{resolveProfile as qG}from"../browser/profile.js";import{isElectronApp as R}from"../electron-apps.js";import{fullName as UG,getRegistry as d}from"../registry.js";import{render as s}from"../output.js";const E=1,v=86400000;function M(G,J,X){if(G===void 0||G===null||G==="")return X;const $=Number(G);if(!Number.isInteger($)||$<=0)throw new k(`${J} must be a positive integer. Received: "${String(G)}"`);return $}function o(G,J,X){if(G===void 0||G===null||G==="")return X;const $=Number(G);if(!Number.isInteger($)||$<0)throw new k(`${J} must be a non-negative integer. Received: "${String(G)}"`);return $}function r(G){if(!G||!G.trim())return null;const J=G.split(",").map((X)=>X.trim()).filter(Boolean);return J.length>0?new Set(J):null}function VG(){return i(n(),".ppcli","auth-refresh.json")}const S=1,OG=600000;function NG(){return i(n(),".ppcli","auth-state.json")}async function BG(G){try{const J=JSON.parse(await u(G,"utf8"));if(J&&J.version===S&&J.profiles&&typeof J.profiles==="object")return{version:S,profiles:J.profiles}}catch{}return{version:S,profiles:{}}}async function HG(G,J){await l(c(G),{recursive:!0});const X=`${G}.${process.pid}.tmp`;await p(X,`${JSON.stringify(J,null,2)}
2
+ `,"utf8");await KG(X,G)}function MG(G,J){return{site:G,status:J.status,logged_in:J.logged_in,identity:J.identity,checked:"cached",error:J.error,checked_at:new Date(J.at).toISOString(),verified:J.verifiedAt!==void 0}}function IG(G,J,X){return{status:G.status,logged_in:G.logged_in,identity:G.identity,checked:G.checked,error:G.error,at:J,...X!==void 0?{verifiedAt:X}:{}}}function CG(){return{version:E,sites:{}}}async function xG(G){try{const J=JSON.parse(await u(G,"utf8"));if(J&&J.version===E&&J.sites&&typeof J.sites==="object")return{version:E,sites:J.sites}}catch(J){if(J.code!=="ENOENT")throw J}return CG()}async function _G(G,J){await l(c(G),{recursive:!0});await p(G,`${JSON.stringify(J,null,2)}
3
+ `,"utf8")}function t(G){if(!G?.last_touched_at)return null;const J=Date.parse(G.last_touched_at);return Number.isFinite(J)?J:null}function FG(G,J){const X=t(G);return X!==null&&J.getTime()-X<v}function O(G){const J=t(G);return J===null?"":new Date(J+v).toISOString()}function e(){const G=new Set;return[...d().values()].filter((J)=>{if(G.has(J))return!1;G.add(J);return J.name==="whoami"&&J.access==="read"}).sort((J,X)=>J.site.localeCompare(X.site))}async function A(G){const J=G;if(!J._lazy||!J._modulePath)return G;await import(LG(J._modulePath).href);return d().get(UG(G))??G}function I(G,J){const X=G.args.some(($)=>$.name==="timeout");return{...G,args:X?G.args:[...G.args,{name:"timeout",type:"int",default:J,help:"Per-site auth command timeout in seconds"}]}}function PG(G,J){if(G.browser!==!0||typeof G.authStatus?.quickCheck!=="function")return null;return I({...G,func:G.authStatus.quickCheck,navigateBefore:!1,siteSession:"ephemeral",defaultWindowMode:"background"},J)}function GG(G){if(typeof G==="boolean")return G;if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G.logged_in;if(typeof J==="boolean")return J}return null}function TG(G){const J=Array.isArray(G)?G[0]:G;if(J&&typeof J==="object"&&!Array.isArray(J)){const X=J.logged_in;if(typeof X==="boolean")return X}return null}function kG(G){const J=Array.isArray(G)?G[0]:G;if(J&&typeof J==="object"&&!Array.isArray(J)){const X=J.error;if(typeof X==="string"&&X.trim())return X.trim()}return""}function JG(G){if(G===void 0||G===null)return"";if(typeof G==="string"||typeof G==="number"||typeof G==="boolean")return String(G);return""}function XG(G){const J=Array.isArray(G)?G[0]:G;if(!J||typeof J!=="object"||Array.isArray(J))return"";const X=J,$=/(?:email|phone|real.?name|first.?name|last.?name|cookie|token|session|secret|password|csrf|jwt|bearer|wt2)/i;for(const Z of["username","handle","user_id","id","name","nickname","user_type","url"]){if($.test(Z))continue;const Y=JG(X[Z]);if(Y)return Y}for(const[Z,Y]of Object.entries(X)){if(Z==="site"||Z==="logged_in"||$.test(Z))continue;const Q=JG(Y);if(Q)return Q}return""}function f(G){return G instanceof m||typeof G==="object"&&G!==null&&G.code==="AUTH_REQUIRED"}function ZG(G,J,X){if(f(X))return{site:G,status:"not_logged_in",logged_in:!1,identity:"",checked:J,error:""};const $=X instanceof a?X.code:"",Z=b(X);return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:$?`${$}: ${Z}`:Z}}function bG(G,J){if(G.browser!==!0)return null;let X=G.authStatus?.refresh;if(typeof X!=="function"){const $=G.authStatus?.quickCheck;if(typeof $!=="function"||!G.domain)return null;const Z=G.domain.startsWith("http://")||G.domain.startsWith("https://")?G.domain:`https://${G.domain}`;X=async(Y,Q,K)=>{await Y.goto(Z);await Y.wait(1);if(GG(await $(Y,Q,K))!==!0)throw new m(G.domain??G.site,`Auth refresh quickCheck failed for ${G.site}`);return{status:"touched"}}}return I({...G,func:X,navigateBefore:!1,siteSession:"persistent",defaultWindowMode:"background"},J)}function RG(G){if(G&&typeof G==="object"&&!Array.isArray(G)){const J=G;if(J.status==="refreshed"||J.refreshed===!0)return"refreshed"}return"touched"}function EG(G,J,X){if(f(X))return{site:G,status:"not_logged_in",last_touched_at:J?.last_touched_at??"",next_refresh_at:O(J),error:""};const $=X instanceof a?X.code:"",Z=b(X);return{site:G,status:"error",last_touched_at:J?.last_touched_at??"",next_refresh_at:O(J),error:$?`${$}: ${Z}`:Z}}function $G(){return process.env.PUBLISHPORT_SITE_SESSION==="ephemeral"?{siteSession:"ephemeral",keepTab:"false"}:{siteSession:"persistent",keepTab:"true"}}async function vG(G,J){try{const X=await A(G);if(X.browser!==!0){const Q=I(X,J.timeoutSeconds),K=await H(Q,{timeout:J.timeoutSeconds},!1,{...J.profile?{profile:J.profile}:{}});return{site:G.site,status:"logged_in",logged_in:!0,identity:XG(K),checked:"quick",error:""}}if(X.authStatus?.quickCheckRequiresNavigation===!0)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"navigation_required",error:""};const $=PG(X,J.timeoutSeconds);if(!$)return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"skipped",error:"quickCheck not implemented; use --full to run whoami"};const Z=await H($,{timeout:J.timeoutSeconds},!1,{...$G(),windowMode:"background",...J.profile?{profile:J.profile}:{}}),Y=GG(Z);if(Y===!0)return{site:G.site,status:"logged_in",logged_in:!0,identity:"",checked:"quick",error:""};if(Y===!1)return{site:G.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"quick",error:""};return{site:G.site,status:"unknown",logged_in:"",identity:"",checked:"quick",error:"quickCheck returned no boolean logged_in signal"}}catch(X){return ZG(G.site,"quick",X)}}async function DG(G,J){try{const X=await A(G),$=I(X,J.timeoutSeconds),Z=await H($,{timeout:J.timeoutSeconds},!1,{...$G(),windowMode:"background",...J.profile?{profile:J.profile}:{}});if(TG(Z)===!1)return{site:G.site,status:"not_logged_in",logged_in:!1,identity:"",checked:"full",error:kG(Z)};return{site:G.site,status:"logged_in",logged_in:!0,identity:XG(Z),checked:"full",error:""}}catch(X){return ZG(G.site,"full",X)}}function SG(G,J){return J?`${G}@${J}`:G}async function AG(G,J){const X=SG(G.site,J.profile),$=J.state.sites[X];if(!J.force&&FG($,J.now))return{site:G.site,status:"skipped",last_touched_at:$?.last_touched_at??"",next_refresh_at:O($),error:""};const Z=J.now.toISOString();try{const Y=await A(G),Q=bG(Y,J.timeoutSeconds);if(!Q){J.state.sites[X]={...$,last_attempt_at:Z,last_status:"unsupported"};return{site:G.site,status:"unsupported",last_touched_at:$?.last_touched_at??"",next_refresh_at:O($),error:"refresh probe is not available for this site"}}const K=await H(Q,{timeout:J.timeoutSeconds},!1,{siteSession:"persistent",keepTab:"true",windowMode:"background",...J.profile?{profile:J.profile}:{}}),z=RG(K);J.state.sites[X]={...$,last_attempt_at:Z,last_touched_at:Z,last_status:z};return{site:G.site,status:z,last_touched_at:Z,next_refresh_at:new Date(J.now.getTime()+v).toISOString(),error:""}}catch(Y){const Q=f(Y)?"not_logged_in":"error";J.state.sites[X]={...$,last_attempt_at:Z,last_status:Q};return EG(G.site,$,Y)}}async function YG(G,J){const X=qG(J);if(X.kind!=="extension")return null;if(!G.some((Z)=>Z.browser===!0&&!R(Z.site)))return null;try{await zG({timeoutSeconds:15,contextId:X.contextId,verbose:!1});return null}catch(Z){return b(Z)}}function fG(G,J,X){return{site:G,status:"error",logged_in:"",identity:"",checked:J,error:`BROWSER_CONNECT: ${X}`}}async function g(G,J,X){const $=Array(G.length);let Z=0;const Y=Array.from({length:Math.min(J,G.length)},async()=>{while(Z<G.length){const Q=Z++;$[Q]=await X(G[Q])}});await Promise.all(Y);return $}export async function collectAuthStatus(G){const J=r(G.sites),X=G.full?"full":"quick",$=M(G.concurrency,"--concurrency",X==="full"?3:8),Z=M(G.timeout,"--timeout",X==="full"?20:8),Y=String(G.only??"all");if(!["all","logged-in","not-logged-in","unknown","error"].includes(Y))throw new k("--only must be one of: all, logged-in, not-logged-in, unknown, error");const Q=o(G.maxAge,"--max-age",0)*1000,K=o(G.verify,"--verify",0),z=G.statePath??NG(),q=(G.now??new Date).getTime(),j=G.profile??"",C=e().filter((D)=>!J||J.has(D.site)),x=await BG(z),_=x.profiles[j]??{},F=new Map,N=[];for(const D of C){const L=_[D.site];if(Q>0&&L&&q-L.at<Q)F.set(D.site,MG(D.site,L));else N.push(D)}const P=N.length>0?await YG(N,G.profile):null,QG=await g(N,$,(D)=>P&&D.browser===!0&&!R(D.site)?Promise.resolve(fG(D.site,X,P)):X==="full"?DG(D,{timeoutSeconds:Z,profile:G.profile}):vG(D,{timeoutSeconds:Z,profile:G.profile})),WG=new Map(C.map((D)=>[D.site,D])),U=new Map(F),T=new Map;for(const D of QG){U.set(D.site,D);if(X==="full"&&D.status!=="error")T.set(D.site,q)}const V=(D)=>{const L=T.get(D.site);if(L!==void 0)return L;const W=_[D.site];return W&&W.status===D.status?W.verifiedAt:void 0};if(K>0&&!P){const D=[...U.values()].filter((W)=>W.status==="logged_in"&&W.checked!=="full").filter((W)=>{const B=V(W);return B===void 0||q-B>=OG}).sort((W,B)=>(V(W)??0)-(V(B)??0)).slice(0,K).map((W)=>WG.get(W.site)).filter((W)=>W!==void 0),L=await g(D,Math.min($,3),(W)=>DG(W,{timeoutSeconds:Math.max(Z,20),profile:G.profile}));for(const W of L){if(W.status==="error")continue;U.set(W.site,W);T.set(W.site,q)}}const y={..._};for(const D of U.keys()){if(F.has(D))continue;const L=U.get(D);if(!L)continue;y[D]=IG(L,q,V(L))}x.profiles[j]=y;await HG(z,x).catch(()=>{});const h=C.map((D)=>U.get(D.site)).filter((D)=>D!==void 0).map((D)=>({...D,checked_at:D.checked_at??new Date(q).toISOString(),verified:D.verified??V(D)!==void 0})),w=Y.replace(/-/g,"_");return w==="all"?h:h.filter((D)=>D.status===w)}export async function collectAuthRefresh(G){const J=r(G.sites),X=M(G.concurrency,"--concurrency",3),$=M(G.timeout,"--timeout",20),Z=G.statePath??VG(),Y=G.now??new Date,Q=await xG(Z),K=e().filter((j)=>!J||J.has(j.site)),z=await YG(K,G.profile),q=await g(K,X,(j)=>z&&j.browser===!0&&!R(j.site)?Promise.resolve({site:j.site,status:"error",last_touched_at:Q.sites[j.site]?.last_touched_at??"",next_refresh_at:O(Q.sites[j.site]),error:`BROWSER_CONNECT: ${z}`}):AG(j,{timeoutSeconds:$,profile:G.profile,now:Y,state:Q,force:G.all===!0}));await _G(Z,Q);return q}export function registerAuthCommands(G){const J=G.command("auth").description("Inspect website login status"),X=J.command("status").description("Show login status for sites with auth adapters").option("--site <sites>","Comma-separated site names to check, e.g. github,chatgpt").option("--full","Run full per-site whoami probes instead of quick no-navigation checks",!1).option("--concurrency <n>","Maximum sites to check at once").option("--timeout <seconds>","Per-site timeout in seconds").option("--max-age <seconds>","Reuse a local cached verdict younger than this instead of probing").option("--verify <n>",'Re-confirm at most N cookie-only "logged in" verdicts with a real whoami').addOption(new jG("--only <status>","Filter rows by status").choices(["all","logged-in","not-logged-in","unknown","error"]).default("all")).option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(Z)=>{const Y=typeof X.optsWithGlobals==="function"?X.optsWithGlobals():{},Q=await collectAuthStatus({sites:Z.site,full:Z.full===!0,concurrency:Z.concurrency,timeout:Z.timeout,only:Z.only,maxAge:Z.maxAge,verify:Z.verify,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),K=typeof Z.format==="string"?Z.format:"table";s(Q,{fmt:K,fmtExplicit:X.getOptionValueSource("format")==="cli",columns:["site","status","identity","checked","error"],title:"ppcli/auth status",source:Z.full?"full whoami probe":"quick auth check"})}),$=J.command("refresh").description("Touch logged-in site sessions to keep browser auth fresh").option("--site <sites>","Comma-separated site names to refresh, e.g. github,claude").option("--all","Ignore the 24h refresh throttle and force every selected site",!1).option("--concurrency <n>","Maximum sites to refresh at once").option("--timeout <seconds>","Per-site timeout in seconds").option("-f, --format <fmt>","Output format: table, plain, json, yaml, md, csv","table").action(async(Z)=>{const Y=typeof $.optsWithGlobals==="function"?$.optsWithGlobals():{},Q=await collectAuthRefresh({sites:Z.site,all:Z.all===!0,concurrency:Z.concurrency,timeout:Z.timeout,profile:typeof Y.profile==="string"&&Y.profile.trim()?Y.profile.trim():void 0}),K=typeof Z.format==="string"?Z.format:"table";s(Q,{fmt:K,fmtExplicit:$.getOptionValueSource("format")==="cli",columns:["site","status","last_touched_at","next_refresh_at","error"],title:"ppcli/auth refresh",source:Z.all?"forced persistent touch":"persistent touch with 24h throttle"})});return J}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "publishport-opencli",
3
- "version": "1.0.14",
3
+ "version": "1.0.15",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "provenance": false