@hienlh/ppm 0.8.85 → 0.8.87
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/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +8 -4
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/test-tokens.mjs +0 -212
package/test-tokens.mjs
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
/**
|
|
3
|
-
* Test script to check access token & refresh token expiry behavior.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* bun test-tokens.mjs # test all accounts in dev DB
|
|
7
|
-
* bun test-tokens.mjs <account-id> # test specific account
|
|
8
|
-
* bun test-tokens.mjs --refresh <id> # force refresh and show new expiry
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import Database from "bun:sqlite";
|
|
12
|
-
import { join } from "node:path";
|
|
13
|
-
import { homedir } from "node:os";
|
|
14
|
-
import { createDecipheriv } from "node:crypto";
|
|
15
|
-
|
|
16
|
-
// --- Config ---
|
|
17
|
-
const DB_PATH = join(homedir(), ".ppm", "ppm.dev.db");
|
|
18
|
-
const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
19
|
-
const OAUTH_TOKEN_URL = "https://api.anthropic.com/v1/oauth/token";
|
|
20
|
-
const PROFILE_URL = "https://api.anthropic.com/api/oauth/profile";
|
|
21
|
-
|
|
22
|
-
// --- Crypto (matches src/lib/account-crypto.ts) ---
|
|
23
|
-
function getEncryptionKey() {
|
|
24
|
-
const keyPath = join(homedir(), ".ppm", "account.key");
|
|
25
|
-
try {
|
|
26
|
-
const hex = require("node:fs").readFileSync(keyPath, "utf-8").trim();
|
|
27
|
-
return Buffer.from(hex, "hex");
|
|
28
|
-
} catch {
|
|
29
|
-
console.error(`Cannot find encryption key at ${keyPath}`);
|
|
30
|
-
process.exit(1);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function decrypt(encryptedValue) {
|
|
35
|
-
if (!encryptedValue || encryptedValue === "") return "";
|
|
36
|
-
const parts = encryptedValue.split(":");
|
|
37
|
-
if (parts.length !== 3) return encryptedValue; // not encrypted
|
|
38
|
-
const [ivHex, authTagHex, cipherHex] = parts;
|
|
39
|
-
const key = getEncryptionKey();
|
|
40
|
-
const decipher = createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
41
|
-
decipher.setAuthTag(Buffer.from(authTagHex, "hex"));
|
|
42
|
-
let decrypted = decipher.update(Buffer.from(cipherHex, "hex"), undefined, "utf8");
|
|
43
|
-
decrypted += decipher.final("utf8");
|
|
44
|
-
return decrypted;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// --- Helpers ---
|
|
48
|
-
function formatExpiry(expiresAt) {
|
|
49
|
-
if (!expiresAt) return "N/A (no expiry)";
|
|
50
|
-
const now = Math.floor(Date.now() / 1000);
|
|
51
|
-
const diff = expiresAt - now;
|
|
52
|
-
const absMin = Math.abs(Math.floor(diff / 60));
|
|
53
|
-
const absHr = Math.floor(absMin / 60);
|
|
54
|
-
const remMin = absMin % 60;
|
|
55
|
-
const date = new Date(expiresAt * 1000).toLocaleString("vi-VN", { timeZone: "Asia/Saigon" });
|
|
56
|
-
if (diff > 0) {
|
|
57
|
-
return `${date} (còn ${absHr}h${remMin}m)`;
|
|
58
|
-
} else {
|
|
59
|
-
return `${date} (HẾT HẠN ${absHr}h${remMin}m trước)`;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async function testAccessToken(token) {
|
|
64
|
-
try {
|
|
65
|
-
const res = await fetch(PROFILE_URL, {
|
|
66
|
-
headers: {
|
|
67
|
-
Accept: "application/json",
|
|
68
|
-
Authorization: `Bearer ${token}`,
|
|
69
|
-
"anthropic-beta": "oauth-2025-04-20",
|
|
70
|
-
"User-Agent": "ppm-token-test/1.0",
|
|
71
|
-
},
|
|
72
|
-
signal: AbortSignal.timeout(10_000),
|
|
73
|
-
});
|
|
74
|
-
if (res.status === 200) {
|
|
75
|
-
const data = await res.json();
|
|
76
|
-
return { status: "VALID", code: 200, email: data.account?.email, name: data.account?.display_name };
|
|
77
|
-
}
|
|
78
|
-
if (res.status === 429) return { status: "VALID (rate-limited)", code: 429 };
|
|
79
|
-
const body = await res.text().catch(() => "");
|
|
80
|
-
return { status: "INVALID", code: res.status, error: body.slice(0, 200) };
|
|
81
|
-
} catch (e) {
|
|
82
|
-
return { status: "ERROR", error: e.message };
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function testRefreshToken(refreshToken) {
|
|
87
|
-
if (!refreshToken || refreshToken === "") {
|
|
88
|
-
return { status: "NO_TOKEN", error: "Empty refresh token (temporary account)" };
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const res = await fetch(OAUTH_TOKEN_URL, {
|
|
92
|
-
method: "POST",
|
|
93
|
-
headers: { "Content-Type": "application/json" },
|
|
94
|
-
body: JSON.stringify({
|
|
95
|
-
grant_type: "refresh_token",
|
|
96
|
-
client_id: OAUTH_CLIENT_ID,
|
|
97
|
-
refresh_token: refreshToken,
|
|
98
|
-
}),
|
|
99
|
-
});
|
|
100
|
-
if (res.ok) {
|
|
101
|
-
const data = await res.json();
|
|
102
|
-
return {
|
|
103
|
-
status: "VALID",
|
|
104
|
-
code: 200,
|
|
105
|
-
newAccessToken: data.access_token?.slice(0, 20) + "...",
|
|
106
|
-
newRefreshToken: data.refresh_token ? "YES (rotated)" : "NO (same)",
|
|
107
|
-
expiresIn: data.expires_in,
|
|
108
|
-
expiresInReadable: `${Math.floor(data.expires_in / 3600)}h${Math.floor((data.expires_in % 3600) / 60)}m`,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const body = await res.text().catch(() => "");
|
|
112
|
-
return { status: "INVALID", code: res.status, error: body.slice(0, 300) };
|
|
113
|
-
} catch (e) {
|
|
114
|
-
return { status: "ERROR", error: e.message };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// --- Main ---
|
|
119
|
-
const args = process.argv.slice(2);
|
|
120
|
-
const doRefresh = args.includes("--refresh");
|
|
121
|
-
const targetId = args.find((a) => a !== "--refresh");
|
|
122
|
-
|
|
123
|
-
console.log("=".repeat(70));
|
|
124
|
-
console.log("PPM Token Expiry Test");
|
|
125
|
-
console.log(`DB: ${DB_PATH}`);
|
|
126
|
-
console.log(`Time: ${new Date().toLocaleString("vi-VN", { timeZone: "Asia/Saigon" })}`);
|
|
127
|
-
console.log("=".repeat(70));
|
|
128
|
-
|
|
129
|
-
let db;
|
|
130
|
-
try {
|
|
131
|
-
db = new Database(DB_PATH, { readonly: true });
|
|
132
|
-
} catch (e) {
|
|
133
|
-
console.error(`Cannot open DB: ${e.message}`);
|
|
134
|
-
process.exit(1);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const query = targetId
|
|
138
|
-
? db.prepare("SELECT * FROM accounts WHERE id = ?").all(targetId)
|
|
139
|
-
: db.prepare("SELECT * FROM accounts ORDER BY priority ASC, created_at ASC").all();
|
|
140
|
-
|
|
141
|
-
if (query.length === 0) {
|
|
142
|
-
console.log("No accounts found.");
|
|
143
|
-
process.exit(0);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
for (const row of query) {
|
|
147
|
-
console.log("\n" + "-".repeat(70));
|
|
148
|
-
console.log(`Account: ${row.label ?? "N/A"}`);
|
|
149
|
-
console.log(` ID: ${row.id}`);
|
|
150
|
-
console.log(` Email: ${row.email ?? "N/A"}`);
|
|
151
|
-
console.log(` Status: ${row.status}`);
|
|
152
|
-
console.log(` Expires: ${formatExpiry(row.expires_at)}`);
|
|
153
|
-
|
|
154
|
-
let accessToken, refreshToken;
|
|
155
|
-
try {
|
|
156
|
-
accessToken = decrypt(row.access_token);
|
|
157
|
-
refreshToken = decrypt(row.refresh_token);
|
|
158
|
-
} catch (e) {
|
|
159
|
-
console.log(` ⚠ Decrypt failed: ${e.message}`);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const isOAuth = accessToken.startsWith("sk-ant-oat");
|
|
164
|
-
console.log(` Type: ${isOAuth ? "OAuth token" : "API key"}`);
|
|
165
|
-
console.log(` Has refresh token: ${refreshToken && refreshToken !== "" ? "YES" : "NO"}`);
|
|
166
|
-
|
|
167
|
-
if (!isOAuth) {
|
|
168
|
-
console.log(` → API keys don't expire. Skipping token tests.`);
|
|
169
|
-
continue;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Test 1: Is access token still valid?
|
|
173
|
-
console.log("\n [1] Testing access token against profile API...");
|
|
174
|
-
const accessResult = await testAccessToken(accessToken);
|
|
175
|
-
if (accessResult.status.startsWith("VALID")) {
|
|
176
|
-
console.log(` ✓ Access token: ${accessResult.status}`);
|
|
177
|
-
if (accessResult.email) console.log(` Email: ${accessResult.email}, Name: ${accessResult.name}`);
|
|
178
|
-
} else {
|
|
179
|
-
console.log(` ✗ Access token: ${accessResult.status} (HTTP ${accessResult.code})`);
|
|
180
|
-
if (accessResult.error) console.log(` Error: ${accessResult.error}`);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// Test 2: Is refresh token still valid?
|
|
184
|
-
if (doRefresh) {
|
|
185
|
-
console.log("\n [2] Testing refresh token (will actually refresh!)...");
|
|
186
|
-
const refreshResult = await testRefreshToken(refreshToken);
|
|
187
|
-
if (refreshResult.status === "VALID") {
|
|
188
|
-
console.log(` ✓ Refresh token: VALID`);
|
|
189
|
-
console.log(` New access token: ${refreshResult.newAccessToken}`);
|
|
190
|
-
console.log(` New refresh token: ${refreshResult.newRefreshToken}`);
|
|
191
|
-
console.log(` New expires_in: ${refreshResult.expiresIn}s (${refreshResult.expiresInReadable})`);
|
|
192
|
-
console.log(` ⚠ NOTE: Old access token may now be invalidated!`);
|
|
193
|
-
console.log(` ⚠ Run this script WITHOUT --refresh to avoid side effects.`);
|
|
194
|
-
} else {
|
|
195
|
-
console.log(` ✗ Refresh token: ${refreshResult.status}`);
|
|
196
|
-
if (refreshResult.error) console.log(` Error: ${refreshResult.error}`);
|
|
197
|
-
}
|
|
198
|
-
} else {
|
|
199
|
-
console.log("\n [2] Refresh token: SKIPPED (use --refresh to test)");
|
|
200
|
-
console.log(` ⚠ --refresh sẽ tạo access token MỚI, token cũ có thể bị invalidate!`);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
console.log("\n" + "=".repeat(70));
|
|
205
|
-
console.log("Summary:");
|
|
206
|
-
console.log(" - Access token (sk-ant-oat*): thường hết hạn sau ~1h (expires_in từ OAuth)");
|
|
207
|
-
console.log(" - Refresh token: không có expiry rõ ràng, chết khi Anthropic trả invalid_grant");
|
|
208
|
-
console.log(" - Khi refresh: Anthropic CÓ THỂ rotate refresh token (trả token mới)");
|
|
209
|
-
console.log(" - PPM auto-refresh mỗi 5 phút cho token sắp hết hạn (<5 phút)");
|
|
210
|
-
console.log("=".repeat(70));
|
|
211
|
-
|
|
212
|
-
db.close();
|