@postplus/cli 0.1.14 → 0.1.16

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.
package/README.md CHANGED
@@ -33,7 +33,7 @@ Requires Node.js and npm.
33
33
  ```bash
34
34
  npm install -g @postplus/cli
35
35
  postplus auth login
36
- npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent claude-code codex cursor --yes
36
+ npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent claude-code codex cursor github-copilot windsurf trae trae-cn --yes
37
37
  ```
38
38
 
39
39
  Useful checks:
@@ -1,53 +1,16 @@
1
+ import { refreshRemoteAuthSession } from './auth-session.js';
1
2
  import { clearAuthState, generateAuthStatusReport } from './auth.js';
2
3
  import { requireHostedBaseUrl } from './hosted-release.js';
3
- import { resolveAccessTokenState, resolveRefreshTokenState, setLocalSession, } from './local-state.js';
4
+ import { resolveAccessTokenState, resolveRefreshTokenState, } from './local-state.js';
4
5
  export async function refreshRemoteAuth() {
5
- const [apiBaseUrl, accessTokenState, refreshTokenState] = await Promise.all([
6
- requireHostedBaseUrl(),
7
- resolveAccessTokenState(),
8
- resolveRefreshTokenState(),
9
- ]);
10
- if (!refreshTokenState.present || !refreshTokenState.value) {
11
- throw new Error('Run `postplus auth login` before refreshing PostPlus auth.');
12
- }
13
- const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/refresh`, {
14
- method: 'POST',
15
- headers: {
16
- accept: 'application/json',
17
- ...(accessTokenState.value
18
- ? { authorization: `Bearer ${accessTokenState.value}` }
19
- : {}),
20
- 'content-type': 'application/json',
21
- },
22
- body: JSON.stringify({
23
- refreshToken: refreshTokenState.value,
24
- }),
25
- signal: AbortSignal.timeout(15000),
26
- });
27
- const payload = (await response.json());
28
- if (!response.ok) {
29
- throw new Error('error' in payload && typeof payload.error === 'string'
30
- ? payload.error
31
- : 'Failed to refresh remote PostPlus auth.');
32
- }
33
- const successPayload = payload;
34
- await setLocalSession({
35
- accessToken: successPayload.accessToken,
36
- accountId: successPayload.accountId,
37
- apiBaseUrl,
38
- refreshToken: successPayload.refreshToken,
39
- sessionExpiresAt: successPayload.sessionExpiresAt,
40
- userEmail: successPayload.userEmail,
41
- userId: successPayload.userId,
42
- });
6
+ const refreshed = await refreshRemoteAuthSession();
43
7
  return {
44
- accessTokenExpiresAt: successPayload.sessionExpiresAt,
45
- accountId: successPayload.accountId,
46
- apiBaseUrl,
8
+ accountId: refreshed.accountId,
9
+ apiBaseUrl: refreshed.apiBaseUrl,
47
10
  ok: true,
48
- subscriptionStatus: successPayload.subscriptionStatus,
49
- userEmail: successPayload.userEmail,
50
- userId: successPayload.userId,
11
+ subscriptionStatus: refreshed.subscriptionStatus,
12
+ userEmail: refreshed.userEmail,
13
+ userId: refreshed.userId,
51
14
  };
52
15
  }
53
16
  export function formatAuthRefreshReport(report) {
@@ -59,9 +22,6 @@ export function formatAuthRefreshReport(report) {
59
22
  `Account: ${report.accountId}`,
60
23
  `User: ${report.userEmail ?? report.userId}`,
61
24
  `Subscription: ${report.subscriptionStatus ?? 'unknown'}`,
62
- `Session expires at: ${typeof report.accessTokenExpiresAt === 'number'
63
- ? new Date(report.accessTokenExpiresAt * 1000).toISOString()
64
- : 'unknown'}`,
65
25
  ].join('\n');
66
26
  }
67
27
  export async function revokeRemoteAuth() {
@@ -1,61 +1,104 @@
1
- import { randomUUID } from 'node:crypto';
2
- import { createServer } from 'node:http';
3
1
  import { requireHostedBaseUrl } from './hosted-release.js';
4
2
  import { setLocalSession } from './local-state.js';
5
- export const CLI_AUTH_HANDOFF_TIMEOUT_MS = 30 * 60 * 1000;
6
- export async function loginWithBrowserHandoff() {
3
+ export const CLI_AUTH_LOGIN_TIMEOUT_MS = 30 * 60 * 1000;
4
+ export async function loginWithCloudHandoff() {
7
5
  const baseUrl = await requireHostedBaseUrl();
8
- const handoff = await createCliAuthHandoffServer({
9
- allowedOrigin: new URL(baseUrl).origin,
10
- });
11
- const loginUrl = buildCliLoginUrl({
12
- baseUrl,
13
- bridgeUrl: handoff.bridgeUrl,
14
- requestId: handoff.requestId,
15
- });
6
+ const started = await startCloudAuthLogin(baseUrl);
16
7
  process.stdout.write([
17
8
  'PostPlus CLI login',
18
9
  '',
19
10
  'Open this URL in your browser to continue:',
20
- loginUrl,
11
+ started.verificationUrl,
12
+ '',
13
+ `Code: ${started.userCode}`,
21
14
  '',
22
- 'Waiting for browser sign-in (up to 30 minutes)...',
15
+ 'Waiting for browser sign-in...',
23
16
  '',
24
17
  ].join('\n'));
25
- try {
26
- const handoffPayload = await handoff.waitForPayload();
27
- if ('error' in handoffPayload) {
28
- throw new Error(handoffPayload.error);
29
- }
30
- const validated = await validateCliSession({
31
- accessToken: handoffPayload.accessToken,
32
- apiBaseUrl: baseUrl,
33
- });
34
- await setLocalSession({
35
- accessToken: handoffPayload.accessToken,
36
- accountId: validated.accountId,
37
- apiBaseUrl: baseUrl,
38
- refreshToken: handoffPayload.refreshToken,
39
- sessionExpiresAt: validated.sessionExpiresAt ?? handoffPayload.expiresAt ?? null,
40
- userEmail: validated.userEmail,
41
- userId: validated.userId,
42
- });
43
- return {
44
- accountId: validated.accountId,
45
- apiBaseUrl: baseUrl,
46
- ok: true,
47
- sessionExpiresAt: validated.sessionExpiresAt ?? handoffPayload.expiresAt ?? null,
48
- userEmail: validated.userEmail,
49
- userId: validated.userId,
50
- };
18
+ const handoffPayload = await waitForCloudAuthLogin({
19
+ apiBaseUrl: baseUrl,
20
+ expiresAt: started.expiresAt,
21
+ pollIntervalSeconds: started.pollIntervalSeconds,
22
+ pollSecret: started.pollSecret,
23
+ requestId: started.requestId,
24
+ });
25
+ const validated = await validateCliSession({
26
+ accessToken: handoffPayload.accessToken,
27
+ apiBaseUrl: baseUrl,
28
+ });
29
+ await setLocalSession({
30
+ accessToken: handoffPayload.accessToken,
31
+ accountId: validated.accountId,
32
+ apiBaseUrl: baseUrl,
33
+ refreshToken: handoffPayload.refreshToken,
34
+ sessionExpiresAt: validated.sessionExpiresAt ?? handoffPayload.sessionExpiresAt ?? null,
35
+ userEmail: validated.userEmail,
36
+ userId: validated.userId,
37
+ });
38
+ return {
39
+ accountId: validated.accountId,
40
+ apiBaseUrl: baseUrl,
41
+ ok: true,
42
+ userEmail: validated.userEmail,
43
+ userId: validated.userId,
44
+ };
45
+ }
46
+ export async function startCloudAuthLogin(apiBaseUrl) {
47
+ const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/login/start`, {
48
+ method: 'POST',
49
+ headers: {
50
+ accept: 'application/json',
51
+ },
52
+ signal: AbortSignal.timeout(15000),
53
+ });
54
+ const payload = (await response.json());
55
+ if (!response.ok) {
56
+ throw new Error(formatRemoteAuthLoginError(payload));
51
57
  }
52
- finally {
53
- await handoff.close();
58
+ if (!isCliAuthLoginStartSuccessPayload(payload)) {
59
+ throw new Error('PostPlus CLI sign-in start returned incomplete data.');
60
+ }
61
+ return payload;
62
+ }
63
+ async function waitForCloudAuthLogin(input) {
64
+ const expiresAtMs = Date.parse(input.expiresAt);
65
+ const deadlineMs = Number.isFinite(expiresAtMs)
66
+ ? expiresAtMs
67
+ : Date.now() + CLI_AUTH_LOGIN_TIMEOUT_MS;
68
+ const pollIntervalMs = Math.max(1000, input.pollIntervalSeconds * 1000);
69
+ while (Date.now() < deadlineMs) {
70
+ const payload = await pollCloudAuthLogin(input);
71
+ if (payload.status === 'completed') {
72
+ return payload;
73
+ }
74
+ await delay(pollIntervalMs);
54
75
  }
76
+ throw new Error('Timed out waiting for the cloud sign-in handoff.');
55
77
  }
56
- function buildCliLoginUrl(input) {
57
- const nextPath = `/auth/cli-callback?bridgeUrl=${encodeURIComponent(input.bridgeUrl)}&requestId=${encodeURIComponent(input.requestId)}`;
58
- return `${input.baseUrl}/auth/sign-in?next=${encodeURIComponent(nextPath)}`;
78
+ export async function pollCloudAuthLogin(input) {
79
+ const response = await fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/login/poll`, {
80
+ method: 'POST',
81
+ headers: {
82
+ accept: 'application/json',
83
+ 'content-type': 'application/json',
84
+ },
85
+ body: JSON.stringify({
86
+ pollSecret: input.pollSecret,
87
+ requestId: input.requestId,
88
+ }),
89
+ signal: AbortSignal.timeout(15000),
90
+ });
91
+ const payload = (await response.json());
92
+ if (!response.ok) {
93
+ throw new Error(formatRemoteAuthLoginError(payload));
94
+ }
95
+ if (isCliAuthLoginCompletedPayload(payload)) {
96
+ return payload;
97
+ }
98
+ if (isCliAuthLoginPendingPayload(payload)) {
99
+ return payload;
100
+ }
101
+ throw new Error('PostPlus CLI sign-in poll returned incomplete data.');
59
102
  }
60
103
  export async function validateCliSession(input) {
61
104
  const response = await fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
@@ -85,125 +128,33 @@ export function formatCliSessionAuthError(payload) {
85
128
  }
86
129
  return 'Failed to validate the browser session for PostPlus CLI.';
87
130
  }
88
- export async function createCliAuthHandoffServer(input) {
89
- const requestId = randomUUID();
90
- return new Promise((resolve, reject) => {
91
- let settled = false;
92
- let cleanupTimer = null;
93
- let resolvePayload = null;
94
- let rejectPayload = null;
95
- const payloadPromise = new Promise((innerResolve, innerReject) => {
96
- resolvePayload = innerResolve;
97
- rejectPayload = innerReject;
98
- });
99
- const server = createServer((request, response) => {
100
- const origin = request.headers.origin ?? null;
101
- const allowOrigin = origin === input.allowedOrigin ? input.allowedOrigin : null;
102
- if (request.method === 'OPTIONS') {
103
- if (!allowOrigin) {
104
- response.writeHead(403);
105
- response.end();
106
- return;
107
- }
108
- response.writeHead(204, {
109
- 'Access-Control-Allow-Headers': 'Content-Type',
110
- 'Access-Control-Allow-Methods': 'POST, OPTIONS',
111
- 'Access-Control-Allow-Private-Network': 'true',
112
- 'Access-Control-Allow-Origin': allowOrigin,
113
- 'Access-Control-Max-Age': '600',
114
- Vary: 'Origin',
115
- });
116
- response.end();
117
- return;
118
- }
119
- if (request.method !== 'POST' || request.url !== '/handoff') {
120
- response.writeHead(404);
121
- response.end();
122
- return;
123
- }
124
- if (!allowOrigin) {
125
- response.writeHead(403);
126
- response.end();
127
- return;
128
- }
129
- const chunks = [];
130
- request.on('data', (chunk) => {
131
- chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
132
- });
133
- request.on('end', () => {
134
- try {
135
- const payload = JSON.parse(Buffer.concat(chunks).toString('utf8'));
136
- if (payload.requestId !== requestId) {
137
- throw new Error('Mismatched CLI auth handoff request id.');
138
- }
139
- response.writeHead(200, {
140
- 'Access-Control-Allow-Origin': allowOrigin,
141
- 'Content-Type': 'application/json',
142
- Vary: 'Origin',
143
- });
144
- response.end(JSON.stringify({ ok: true }));
145
- if (!settled) {
146
- settled = true;
147
- resolvePayload?.(payload);
148
- }
149
- }
150
- catch (error) {
151
- response.writeHead(400, {
152
- 'Access-Control-Allow-Origin': allowOrigin,
153
- 'Content-Type': 'application/json',
154
- Vary: 'Origin',
155
- });
156
- response.end(JSON.stringify({
157
- error: error instanceof Error
158
- ? error.message
159
- : 'Invalid CLI auth handoff payload.',
160
- }));
161
- }
162
- });
163
- });
164
- server.on('error', (error) => {
165
- if (!settled) {
166
- settled = true;
167
- reject(error);
168
- }
169
- else {
170
- rejectPayload?.(error);
171
- }
172
- });
173
- server.listen(0, '127.0.0.1', () => {
174
- const address = server.address();
175
- if (!address || typeof address === 'string') {
176
- const error = new Error('Failed to bind the local CLI auth bridge.');
177
- if (!settled) {
178
- settled = true;
179
- reject(error);
180
- }
181
- return;
182
- }
183
- cleanupTimer = setTimeout(() => {
184
- if (!settled) {
185
- settled = true;
186
- rejectPayload?.(new Error('Timed out waiting for the browser sign-in handoff.'));
187
- }
188
- server.close();
189
- }, CLI_AUTH_HANDOFF_TIMEOUT_MS);
190
- resolve({
191
- bridgeUrl: `http://127.0.0.1:${address.port}/handoff`,
192
- close: async () => new Promise((innerResolve, innerReject) => {
193
- if (cleanupTimer) {
194
- clearTimeout(cleanupTimer);
195
- }
196
- server.close((error) => {
197
- if (error) {
198
- innerReject(error);
199
- return;
200
- }
201
- innerResolve();
202
- });
203
- }),
204
- requestId,
205
- waitForPayload: () => payloadPromise,
206
- });
207
- });
208
- });
131
+ function isCliAuthLoginStartSuccessPayload(payload) {
132
+ return ('requestId' in payload &&
133
+ typeof payload.requestId === 'string' &&
134
+ typeof payload.pollSecret === 'string' &&
135
+ typeof payload.userCode === 'string' &&
136
+ typeof payload.verificationUrl === 'string' &&
137
+ typeof payload.expiresAt === 'string' &&
138
+ typeof payload.pollIntervalSeconds === 'number');
139
+ }
140
+ function isCliAuthLoginCompletedPayload(payload) {
141
+ return ('status' in payload &&
142
+ payload.status === 'completed' &&
143
+ typeof payload.accessToken === 'string' &&
144
+ typeof payload.refreshToken === 'string' &&
145
+ typeof payload.accountId === 'string' &&
146
+ typeof payload.userId === 'string');
147
+ }
148
+ function isCliAuthLoginPendingPayload(payload) {
149
+ return 'status' in payload && payload.status === 'pending';
150
+ }
151
+ function formatRemoteAuthLoginError(payload) {
152
+ return 'error' in payload &&
153
+ typeof payload.error === 'string' &&
154
+ payload.error.trim().length > 0
155
+ ? payload.error
156
+ : 'PostPlus CLI sign-in failed.';
157
+ }
158
+ function delay(ms) {
159
+ return new Promise((resolve) => setTimeout(resolve, ms));
209
160
  }
@@ -0,0 +1,137 @@
1
+ import { requireHostedBaseUrl } from './hosted-release.js';
2
+ import { readLocalConfig, resolveAccessTokenState, resolveRefreshTokenState, setLocalSession, } from './local-state.js';
3
+ export const AUTH_SESSION_REFRESH_LEEWAY_SECONDS = 60;
4
+ export async function resolveFreshRemoteAuth(options = {}) {
5
+ const [apiBaseUrl, accessTokenState, refreshTokenState, config] = await Promise.all([
6
+ requireHostedBaseUrl(),
7
+ resolveAccessTokenState(),
8
+ resolveRefreshTokenState(),
9
+ readLocalConfig(),
10
+ ]);
11
+ if (!refreshTokenState.present || !refreshTokenState.value) {
12
+ if (!accessTokenState.present || !accessTokenState.value) {
13
+ throw new Error('Run `postplus auth login` before validating PostPlus auth.');
14
+ }
15
+ return {
16
+ accessToken: accessTokenState.value,
17
+ apiBaseUrl,
18
+ refreshed: false,
19
+ source: 'config',
20
+ };
21
+ }
22
+ const existingAccessToken = accessTokenState.value;
23
+ const decodedTokenExpiresAt = existingAccessToken
24
+ ? decodeAccessTokenExpiration(existingAccessToken)
25
+ : null;
26
+ const tokenExpiresAt = typeof decodedTokenExpiresAt === 'number'
27
+ ? decodedTokenExpiresAt
28
+ : typeof config?.sessionExpiresAt === 'number'
29
+ ? config.sessionExpiresAt
30
+ : null;
31
+ const shouldRefresh = options.forceRefresh === true ||
32
+ !accessTokenState.present ||
33
+ !accessTokenState.value ||
34
+ isExpiringSoon(tokenExpiresAt);
35
+ if (!shouldRefresh) {
36
+ if (!existingAccessToken) {
37
+ throw new Error('Run `postplus auth login` before validating PostPlus auth.');
38
+ }
39
+ return {
40
+ accessToken: existingAccessToken,
41
+ apiBaseUrl,
42
+ refreshed: false,
43
+ source: 'config',
44
+ };
45
+ }
46
+ const refreshed = await refreshRemoteAuthSession({
47
+ accessToken: accessTokenState.value,
48
+ apiBaseUrl,
49
+ refreshToken: refreshTokenState.value,
50
+ });
51
+ return {
52
+ accessToken: refreshed.accessToken,
53
+ apiBaseUrl,
54
+ refreshed: true,
55
+ source: 'config',
56
+ };
57
+ }
58
+ export async function refreshRemoteAuthSession(input) {
59
+ const [apiBaseUrl, accessTokenState, refreshTokenState] = await Promise.all([
60
+ input?.apiBaseUrl ?? requireHostedBaseUrl(),
61
+ input?.accessToken === undefined ? resolveAccessTokenState() : null,
62
+ input?.refreshToken === undefined ? resolveRefreshTokenState() : null,
63
+ ]);
64
+ const accessToken = input?.accessToken === undefined
65
+ ? accessTokenState?.value
66
+ : input.accessToken;
67
+ const refreshToken = input?.refreshToken === undefined
68
+ ? refreshTokenState?.value
69
+ : input.refreshToken;
70
+ if (!refreshToken) {
71
+ throw new Error('Run `postplus auth login` before refreshing PostPlus auth.');
72
+ }
73
+ const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/refresh`, {
74
+ method: 'POST',
75
+ headers: {
76
+ accept: 'application/json',
77
+ ...(accessToken ? { authorization: `Bearer ${accessToken}` } : {}),
78
+ 'content-type': 'application/json',
79
+ },
80
+ body: JSON.stringify({
81
+ refreshToken,
82
+ }),
83
+ signal: AbortSignal.timeout(15000),
84
+ });
85
+ const payload = (await response.json());
86
+ if (!response.ok) {
87
+ throw new Error('error' in payload && typeof payload.error === 'string'
88
+ ? payload.error
89
+ : 'Failed to refresh remote PostPlus auth.');
90
+ }
91
+ if (!isRemoteAuthRefreshSuccessPayload(payload)) {
92
+ throw new Error('PostPlus auth refresh returned incomplete session tokens.');
93
+ }
94
+ await setLocalSession({
95
+ accessToken: payload.accessToken,
96
+ accountId: payload.accountId,
97
+ apiBaseUrl,
98
+ refreshToken: payload.refreshToken,
99
+ sessionExpiresAt: payload.sessionExpiresAt,
100
+ userEmail: payload.userEmail,
101
+ userId: payload.userId,
102
+ });
103
+ return {
104
+ ...payload,
105
+ apiBaseUrl,
106
+ };
107
+ }
108
+ export function decodeAccessTokenExpiration(accessToken) {
109
+ try {
110
+ const [, payload] = accessToken.split('.');
111
+ if (!payload) {
112
+ return null;
113
+ }
114
+ const decoded = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
115
+ return typeof decoded.exp === 'number' ? decoded.exp : null;
116
+ }
117
+ catch {
118
+ return null;
119
+ }
120
+ }
121
+ function isExpiringSoon(expiresAt) {
122
+ if (typeof expiresAt !== 'number' || !Number.isFinite(expiresAt)) {
123
+ return false;
124
+ }
125
+ const nowSeconds = Math.floor(Date.now() / 1_000);
126
+ return expiresAt - nowSeconds <= AUTH_SESSION_REFRESH_LEEWAY_SECONDS;
127
+ }
128
+ function isRemoteAuthRefreshSuccessPayload(payload) {
129
+ return (typeof payload === 'object' &&
130
+ payload !== null &&
131
+ typeof payload.accessToken === 'string' &&
132
+ payload.accessToken.trim().length > 0 &&
133
+ typeof payload.refreshToken === 'string' &&
134
+ payload.refreshToken.trim().length > 0 &&
135
+ typeof payload.accountId === 'string' &&
136
+ typeof payload.userId === 'string');
137
+ }
@@ -1,21 +1,13 @@
1
- import { requireHostedBaseUrl } from './hosted-release.js';
2
- import { resolveAccessTokenState } from './local-state.js';
1
+ import { resolveFreshRemoteAuth } from './auth-session.js';
3
2
  export async function validateRemoteAuth() {
4
- const [apiBaseUrl, accessTokenState] = await Promise.all([
5
- requireHostedBaseUrl(),
6
- resolveAccessTokenState(),
7
- ]);
8
- if (!accessTokenState.present || !accessTokenState.value) {
9
- throw new Error('Run `postplus auth login` before validating PostPlus auth.');
3
+ let auth = await resolveFreshRemoteAuth();
4
+ let response = await fetchWhoami(auth);
5
+ if (response.status === 401) {
6
+ auth = await resolveFreshRemoteAuth({
7
+ forceRefresh: true,
8
+ });
9
+ response = await fetchWhoami(auth);
10
10
  }
11
- const response = await fetch(`${apiBaseUrl}/api/postplus-cli/auth/whoami`, {
12
- method: 'GET',
13
- headers: {
14
- accept: 'application/json',
15
- authorization: `Bearer ${accessTokenState.value}`,
16
- },
17
- signal: AbortSignal.timeout(15000),
18
- });
19
11
  const payload = (await response.json());
20
12
  if (!response.ok) {
21
13
  throw new Error('error' in payload && typeof payload.error === 'string'
@@ -25,12 +17,9 @@ export async function validateRemoteAuth() {
25
17
  const successPayload = payload;
26
18
  return {
27
19
  accountId: successPayload.accountId,
28
- apiBaseUrl,
20
+ apiBaseUrl: auth.apiBaseUrl,
29
21
  ok: true,
30
- sessionExpiresAt: successPayload.sessionExpiresAt,
31
- source: accessTokenState.source === 'missing'
32
- ? 'config'
33
- : accessTokenState.source,
22
+ source: auth.source,
34
23
  subscriptionStatus: successPayload.subscriptionStatus,
35
24
  userEmail: successPayload.userEmail,
36
25
  userId: successPayload.userId,
@@ -45,8 +34,15 @@ export function formatAuthValidateReport(report) {
45
34
  `Account: ${report.accountId}`,
46
35
  `User: ${report.userEmail ?? report.userId}`,
47
36
  `Subscription: ${report.subscriptionStatus ?? 'unknown'}`,
48
- `Session expires at: ${typeof report.sessionExpiresAt === 'number'
49
- ? new Date(report.sessionExpiresAt * 1000).toISOString()
50
- : 'unknown'}`,
51
37
  ].join('\n');
52
38
  }
39
+ function fetchWhoami(input) {
40
+ return fetch(`${input.apiBaseUrl}/api/postplus-cli/auth/whoami`, {
41
+ method: 'GET',
42
+ headers: {
43
+ accept: 'application/json',
44
+ authorization: `Bearer ${input.accessToken}`,
45
+ },
46
+ signal: AbortSignal.timeout(15000),
47
+ });
48
+ }
package/build/auth.js CHANGED
@@ -1,4 +1,4 @@
1
- import { clearLocalAuthState, getPostPlusConfigPath, hasLocalConfigFile, maskSecret, readLocalConfig, resolveApiBaseUrlState, resolveLocalSessionState, setLocalAccessToken, setLocalApiBaseUrl, setLocalRefreshToken, } from './local-state.js';
1
+ import { clearLocalAuthState, getPostPlusConfigPath, hasLocalConfigFile, maskSecret, readLocalConfig, resolveApiBaseUrlState, resolveLocalSessionState, setLocalApiBaseUrl, } from './local-state.js';
2
2
  export async function generateAuthStatusReport() {
3
3
  const [sessionState, apiBaseUrlState, configExists, config] = await Promise.all([
4
4
  resolveLocalSessionState(),
@@ -32,7 +32,6 @@ export async function generateAuthStatusReport() {
32
32
  present: sessionState.refreshToken.present,
33
33
  maskedValue: maskSecret(sessionState.refreshToken.value),
34
34
  },
35
- sessionExpiresAt: sessionState.expiresAt,
36
35
  };
37
36
  }
38
37
  export function formatAuthStatusReport(report) {
@@ -60,20 +59,9 @@ export function formatAuthStatusReport(report) {
60
59
  : `[PASS] local config path: ${report.config.path}`);
61
60
  lines.push(` Account: ${report.config.accountId ?? 'not bound'}`);
62
61
  lines.push(` User: ${report.config.userEmail ?? report.config.userId ?? 'not bound'}`);
63
- lines.push(` Session expires at: ${typeof report.sessionExpiresAt === 'number'
64
- ? new Date(report.sessionExpiresAt * 1000).toISOString()
65
- : 'unknown'}`);
66
62
  lines.push('', report.ok ? 'Auth status OK.' : 'Auth status incomplete.');
67
63
  return lines.join('\n');
68
64
  }
69
- export async function configureAccessToken(accessToken) {
70
- await setLocalAccessToken(accessToken);
71
- return generateAuthStatusReport();
72
- }
73
- export async function configureRefreshToken(refreshToken) {
74
- await setLocalRefreshToken(refreshToken);
75
- return generateAuthStatusReport();
76
- }
77
65
  export async function configureApiBaseUrl(apiBaseUrl) {
78
66
  await setLocalApiBaseUrl(apiBaseUrl);
79
67
  return generateAuthStatusReport();
package/build/doctor.js CHANGED
@@ -1,5 +1,5 @@
1
+ import { resolveFreshRemoteAuth, } from './auth-session.js';
1
2
  import { resolveHostedBaseUrl } from './hosted-release.js';
2
- import { resolveAccessTokenState } from './local-state.js';
3
3
  function createPass(id, label, detail) {
4
4
  return {
5
5
  id,
@@ -22,25 +22,24 @@ export async function generateDoctorReport() {
22
22
  const checks = [
23
23
  createPass('hosted_base_url', 'PostPlus Cloud', `Using ${hostedBaseUrl ?? 'https://postplus.io'}`),
24
24
  ];
25
- const accessToken = await resolveAccessTokenState();
26
25
  if (!hostedBaseUrl) {
27
26
  checks.push(createFail('remote_auth', 'Remote auth', 'PostPlus Cloud base URL could not be resolved.', 'Configure POSTPLUS_API_BASE_URL or run `postplus auth login`.'));
28
27
  return buildDoctorReport(checks);
29
28
  }
30
- if (!accessToken.present || !accessToken.value) {
31
- checks.push(createFail('remote_auth', 'Remote auth', 'No PostPlus CLI session is configured.', 'Run `postplus auth login`.'));
29
+ const auth = await resolveFreshRemoteAuth().catch((error) => {
30
+ const message = error instanceof Error
31
+ ? error.message
32
+ : 'No PostPlus CLI session is configured.';
33
+ checks.push(createFail('remote_auth', 'Remote auth', message, 'Run `postplus auth login`.'));
34
+ return null;
35
+ });
36
+ if (!auth) {
32
37
  return buildDoctorReport(checks);
33
38
  }
34
- const authCheck = await checkRemoteAuth({
35
- accessToken: accessToken.value,
36
- hostedBaseUrl,
37
- });
39
+ const authCheck = await checkRemoteAuth(auth);
38
40
  checks.push(authCheck);
39
41
  if (authCheck.status === 'pass') {
40
- checks.push(await checkHostedCapabilities({
41
- accessToken: accessToken.value,
42
- hostedBaseUrl,
43
- }));
42
+ checks.push(await checkHostedCapabilities(auth));
44
43
  }
45
44
  return buildDoctorReport(checks);
46
45
  }
@@ -52,13 +51,13 @@ function buildDoctorReport(checks) {
52
51
  }
53
52
  async function checkRemoteAuth(input) {
54
53
  try {
55
- const response = await fetch(`${input.hostedBaseUrl}/api/postplus-cli/auth/whoami`, {
56
- headers: {
57
- accept: 'application/json',
58
- authorization: `Bearer ${input.accessToken}`,
59
- },
60
- signal: AbortSignal.timeout(15000),
61
- });
54
+ let response = await requestWithAuth(input, '/api/postplus-cli/auth/whoami');
55
+ if (response.status === 401) {
56
+ const refreshedAuth = await resolveFreshRemoteAuth({
57
+ forceRefresh: true,
58
+ });
59
+ response = await requestWithAuth(refreshedAuth, '/api/postplus-cli/auth/whoami');
60
+ }
62
61
  const payload = (await response.json());
63
62
  if (!response.ok) {
64
63
  return createFail('remote_auth', 'Remote auth', readErrorMessage(payload, 'PostPlus Cloud rejected the CLI session.'), 'Run `postplus auth login`.');
@@ -82,13 +81,13 @@ async function checkRemoteAuth(input) {
82
81
  }
83
82
  async function checkHostedCapabilities(input) {
84
83
  try {
85
- const response = await fetch(`${input.hostedBaseUrl}/api/postplus-cli/hosted/readiness`, {
86
- headers: {
87
- accept: 'application/json',
88
- authorization: `Bearer ${input.accessToken}`,
89
- },
90
- signal: AbortSignal.timeout(15000),
91
- });
84
+ let response = await requestWithAuth(input, '/api/postplus-cli/hosted/readiness');
85
+ if (response.status === 401) {
86
+ const refreshedAuth = await resolveFreshRemoteAuth({
87
+ forceRefresh: true,
88
+ });
89
+ response = await requestWithAuth(refreshedAuth, '/api/postplus-cli/hosted/readiness');
90
+ }
92
91
  const payload = (await response.json());
93
92
  if (!response.ok) {
94
93
  return createFail('hosted_capabilities', 'Hosted capabilities', readErrorMessage(payload, 'PostPlus Cloud hosted readiness check failed.'));
@@ -132,6 +131,15 @@ function readErrorMessage(payload, fallback) {
132
131
  ? payload.error
133
132
  : fallback;
134
133
  }
134
+ function requestWithAuth(input, path) {
135
+ return fetch(`${input.apiBaseUrl}${path}`, {
136
+ headers: {
137
+ accept: 'application/json',
138
+ authorization: `Bearer ${input.accessToken}`,
139
+ },
140
+ signal: AbortSignal.timeout(15000),
141
+ });
142
+ }
135
143
  export function formatDoctorReport(report) {
136
144
  const lines = ['PostPlus CLI doctor', ''];
137
145
  for (const check of report.checks) {
package/build/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import { formatAuthRefreshReport, refreshRemoteAuth, revokeRemoteAuthAndReport, } from './auth-lifecycle.js';
3
- import { loginWithBrowserHandoff } from './auth-login.js';
3
+ import { loginWithCloudHandoff } from './auth-login.js';
4
4
  import { formatAuthValidateReport, validateRemoteAuth, } from './auth-validate.js';
5
5
  import { clearAuthState, formatAuthStatusReport, generateAuthStatusReport, } from './auth.js';
6
- import { formatDoctorReport, generateDoctorReport, } from './doctor.js';
6
+ import { formatDoctorReport, generateDoctorReport } from './doctor.js';
7
7
  import { assertConfigFilePermissions } from './local-state.js';
8
8
  import { POSTPLUS_SKILLS_INSTALL_COMMAND, loadPublicSkillCatalog, } from './skill-catalog.js';
9
9
  import { runPostPlusSkillUninstall, runPostPlusSkillUpdate, } from './skill-management.js';
@@ -140,16 +140,13 @@ async function runAuthRevoke(json) {
140
140
  return 0;
141
141
  }
142
142
  async function runAuthLogin() {
143
- const report = await loginWithBrowserHandoff();
143
+ const report = await loginWithCloudHandoff();
144
144
  process.stdout.write([
145
145
  '',
146
146
  'PostPlus CLI login complete.',
147
147
  `Account: ${report.accountId}`,
148
148
  `PostPlus Cloud: ${report.apiBaseUrl}`,
149
149
  `User: ${report.userEmail ?? 'unknown'}`,
150
- `Session expires at: ${typeof report.sessionExpiresAt === 'number'
151
- ? new Date(report.sessionExpiresAt * 1000).toISOString()
152
- : 'unknown'}`,
153
150
  '',
154
151
  ].join('\n'));
155
152
  return report.ok ? 0 : 1;
@@ -1,5 +1,5 @@
1
1
  import { constants as fsConstants } from 'node:fs';
2
- import { access, chmod, mkdir, readFile, stat, writeFile } from 'node:fs/promises';
2
+ import { access, chmod, mkdir, readFile, stat, writeFile, } from 'node:fs/promises';
3
3
  import { homedir, platform } from 'node:os';
4
4
  import { dirname, join, resolve } from 'node:path';
5
5
  export const DEFAULT_POSTPLUS_API_BASE_URL = 'https://postplus.io';
@@ -103,26 +103,6 @@ export async function clearLocalAuthState() {
103
103
  return next;
104
104
  });
105
105
  }
106
- export async function setLocalAccessToken(accessToken) {
107
- const normalizedAccessToken = accessToken.trim();
108
- if (normalizedAccessToken.length === 0) {
109
- throw new Error('POSTPLUS_ACCESS_TOKEN cannot be empty.');
110
- }
111
- return updateLocalConfig((current) => ({
112
- ...(current ?? {}),
113
- accessToken: normalizedAccessToken,
114
- }));
115
- }
116
- export async function setLocalRefreshToken(refreshToken) {
117
- const normalizedRefreshToken = refreshToken.trim();
118
- if (normalizedRefreshToken.length === 0) {
119
- throw new Error('POSTPLUS_REFRESH_TOKEN cannot be empty.');
120
- }
121
- return updateLocalConfig((current) => ({
122
- ...(current ?? {}),
123
- refreshToken: normalizedRefreshToken,
124
- }));
125
- }
126
106
  export async function setLocalApiBaseUrl(apiBaseUrl) {
127
107
  const normalizedApiBaseUrl = apiBaseUrl.trim();
128
108
  if (normalizedApiBaseUrl.length === 0) {
@@ -139,10 +119,10 @@ export async function setLocalSession(input) {
139
119
  const refreshToken = input.refreshToken.trim();
140
120
  const apiBaseUrl = input.apiBaseUrl.trim().replace(/\/+$/, '');
141
121
  if (accessToken.length === 0) {
142
- throw new Error('POSTPLUS_ACCESS_TOKEN cannot be empty.');
122
+ throw new Error('PostPlus CLI access token cannot be empty.');
143
123
  }
144
124
  if (refreshToken.length === 0) {
145
- throw new Error('POSTPLUS_REFRESH_TOKEN cannot be empty.');
125
+ throw new Error('PostPlus CLI refresh token cannot be empty.');
146
126
  }
147
127
  if (apiBaseUrl.length === 0) {
148
128
  throw new Error('POSTPLUS_API_BASE_URL cannot be empty.');
@@ -168,14 +148,6 @@ export async function hasLocalConfigFile() {
168
148
  }
169
149
  }
170
150
  export async function resolveAccessTokenState() {
171
- const envValue = process.env.POSTPLUS_ACCESS_TOKEN?.trim();
172
- if (envValue && envValue.length > 0) {
173
- return {
174
- source: 'env',
175
- present: true,
176
- value: envValue,
177
- };
178
- }
179
151
  const config = await readLocalConfig();
180
152
  const configValue = config?.accessToken?.trim();
181
153
  if (configValue && configValue.length > 0) {
@@ -192,14 +164,6 @@ export async function resolveAccessTokenState() {
192
164
  };
193
165
  }
194
166
  export async function resolveRefreshTokenState() {
195
- const envValue = process.env.POSTPLUS_REFRESH_TOKEN?.trim();
196
- if (envValue && envValue.length > 0) {
197
- return {
198
- source: 'env',
199
- present: true,
200
- value: envValue,
201
- };
202
- }
203
167
  const config = await readLocalConfig();
204
168
  const configValue = config?.refreshToken?.trim();
205
169
  if (configValue && configValue.length > 0) {
@@ -216,16 +180,12 @@ export async function resolveRefreshTokenState() {
216
180
  };
217
181
  }
218
182
  export async function resolveLocalSessionState() {
219
- const [accessToken, refreshToken, config] = await Promise.all([
183
+ const [accessToken, refreshToken] = await Promise.all([
220
184
  resolveAccessTokenState(),
221
185
  resolveRefreshTokenState(),
222
- readLocalConfig(),
223
186
  ]);
224
187
  return {
225
188
  accessToken,
226
- expiresAt: typeof config?.sessionExpiresAt === 'number'
227
- ? config.sessionExpiresAt
228
- : null,
229
189
  refreshToken,
230
190
  };
231
191
  }
@@ -1,5 +1,15 @@
1
1
  export const POSTPLUS_SKILLS_REPO = 'PostPlusAI/postplus-skills';
2
- export const POSTPLUS_SKILLS_INSTALL_COMMAND = "npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent claude-code codex cursor --yes";
2
+ export const POSTPLUS_SKILLS_AGENT_TARGETS = [
3
+ 'claude-code',
4
+ 'codex',
5
+ 'cursor',
6
+ 'github-copilot',
7
+ 'windsurf',
8
+ 'trae',
9
+ 'trae-cn',
10
+ ];
11
+ const POSTPLUS_SKILLS_AGENT_ARGS = POSTPLUS_SKILLS_AGENT_TARGETS.join(' ');
12
+ export const POSTPLUS_SKILLS_INSTALL_COMMAND = `npx -y skills add PostPlusAI/postplus-skills --full-depth --skill '*' --agent ${POSTPLUS_SKILLS_AGENT_ARGS} --yes`;
3
13
  export const POSTPLUS_SKILLS_LIST_COMMAND = 'npx -y skills add PostPlusAI/postplus-skills --list --full-depth';
4
14
  const POSTPLUS_SKILLS_INDEX_URL = 'https://raw.githubusercontent.com/PostPlusAI/postplus-skills/main/skills/INDEX.md';
5
15
  export async function loadPublicSkillCatalog() {
@@ -1,6 +1,5 @@
1
- import { POSTPLUS_SKILLS_INSTALL_COMMAND, POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
1
+ import { POSTPLUS_SKILLS_AGENT_TARGETS, POSTPLUS_SKILLS_INSTALL_COMMAND, POSTPLUS_SKILLS_REPO, loadPublicSkillCatalog, } from './skill-catalog.js';
2
2
  import { runCommand, runInteractiveCommand } from './command-runner.js';
3
- const SKILLS_AGENTS = ['claude-code', 'codex', 'cursor'];
4
3
  const NPX_SKILLS = ['-y', 'skills'];
5
4
  export async function runPostPlusSkillUpdate() {
6
5
  const catalog = await loadPublicSkillCatalog();
@@ -93,7 +92,7 @@ export function buildPostPlusSkillUninstallArgs(skillNames) {
93
92
  'remove',
94
93
  ...skillNames,
95
94
  '--agent',
96
- ...SKILLS_AGENTS,
95
+ ...POSTPLUS_SKILLS_AGENT_TARGETS,
97
96
  '--yes',
98
97
  ];
99
98
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@postplus/cli",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "packageManager": "pnpm@10.30.3+sha512.c961d1e0a2d8e354ecaa5166b822516668b7f44cb5bd95122d590dd81922f606f5473b6d23ec4a5be05e7fcd18e8488d47d978bbe981872f1145d06e9a740017",
5
5
  "type": "module",
6
6
  "description": "PostPlus CLI for PostPlus Cloud auth, status, and diagnostics.",
@@ -8,6 +8,7 @@
8
8
  "files": [
9
9
  "build/auth-lifecycle.js",
10
10
  "build/auth-login.js",
11
+ "build/auth-session.js",
11
12
  "build/auth-validate.js",
12
13
  "build/auth.js",
13
14
  "build/command-runner.js",
@@ -31,7 +32,7 @@
31
32
  "node": ">=20.10.0"
32
33
  },
33
34
  "bin": {
34
- "postplus": "./build/index.js"
35
+ "postplus": "build/index.js"
35
36
  },
36
37
  "scripts": {
37
38
  "build": "node ./scripts/clean-build.mjs && tsc",