Files
mnote/scripts/vault-extension-api-smoke.js

328 lines
9.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 12-3 Chrome extension vault API smoke(不依赖 Chrome 本身)
*
* 覆盖:
* 1. auth:signIn → session cookie
* 2. POST /api/vault/extension/token → mnext1.*
* 3. Bearer mnext1 创建条目
* 4. PUT …/session 写入 sessions/{id}/{account}.json
* 5. L0 hasLoginSession;响应/磁盘无误泄露检查(响应 L0 无 cookieHeader
* 6. 吊销 jti 后 Bearer 401
*
* 用法:
* MNOTE_VAULT=1 # 服务端需已启用
* node scripts/vault-extension-api-smoke.js
*
* 环境变量:
* BASE_URL / MNOTE_BASE_URL 默认 http://127.0.0.1:3000
* MNOTE_E2E_EMAIL / MNOTE_E2E_PASSWORD
* ROOT_URI 可选;默认临时目录 file://…
*/
"use strict";
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const BASE = (
process.env.BASE_URL ||
process.env.MNOTE_BASE_URL ||
"http://127.0.0.1:3000"
).replace(/\/+$/, "");
const EMAIL = process.env.MNOTE_E2E_EMAIL || "mnote.e2e@example.com";
const PASSWORD = process.env.MNOTE_E2E_PASSWORD || "MnoteE2E123!";
function assert(cond, msg) {
if (!cond) throw new Error(msg || "assertion failed");
}
function collectSetCookie(res) {
if (typeof res.headers.getSetCookie === "function") {
return res.headers.getSetCookie();
}
const single = res.headers.get("set-cookie");
return single ? [single] : [];
}
function cookieHeaderFromSetCookie(setCookies) {
const parts = [];
for (const sc of setCookies) {
const first = String(sc).split(";")[0].trim();
if (first) parts.push(first);
}
return parts.join("; ");
}
async function fetchJson(urlPath, { method = "GET", cookie, token, body } = {}) {
const headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
if (cookie) headers.Cookie = cookie;
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`${BASE}${urlPath}`, {
method,
headers,
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const text = await res.text();
let data = null;
try {
data = text ? JSON.parse(text) : null;
} catch {
data = { raw: text };
}
return { res, data, text, setCookies: collectSetCookie(res) };
}
function writeWorkspace(rootDir) {
const metadataDir = path.join(rootDir, ".mnote");
fs.mkdirSync(metadataDir, { recursive: true });
fs.writeFileSync(
path.join(metadataDir, "workspace.json"),
`${JSON.stringify(
{
workspaceId: "local-ws:mnote-e2e:vault-ext-smoke",
ownerId: "mnote-e2e",
createdAt: new Date().toISOString(),
capabilities: ["local_files", "markdown_edit", "vault"],
},
null,
2
)}\n`,
"utf8"
);
}
async function main() {
// 0. health / reachability
try {
const probe = await fetch(`${BASE}/api/auth/whoami`, {
headers: { Accept: "application/json" },
});
if (probe.status === 0) throw new Error("unreachable");
} catch (e) {
throw new Error(
`mnote-web 不可达 ${BASE}(请先 dev:hot 且 MNOTE_VAULT=1: ${e.message || e}`
);
}
const rootDir =
process.env.ROOT_DIR ||
fs.mkdtempSync(path.join(os.tmpdir(), "mnote-vault-ext-smoke-"));
writeWorkspace(rootDir);
const rootUri =
process.env.ROOT_URI ||
`file://${rootDir.startsWith("/") ? rootDir : path.resolve(rootDir)}`;
console.log("[smoke] BASE=", BASE);
console.log("[smoke] ROOT_URI=", rootUri);
// 1. signIn
const signIn = await fetchJson("/api/auth", {
method: "POST",
body: {
action: "auth:signIn",
args: {
provider: "password",
params: {
password: PASSWORD,
flow: "signIn",
account: EMAIL,
email: EMAIL,
name: EMAIL.includes("@") ? EMAIL.split("@")[0] : EMAIL,
},
},
},
});
assert(
signIn.res.ok,
`signIn failed ${signIn.res.status}: ${signIn.text.slice(0, 200)}`
);
const cookie = cookieHeaderFromSetCookie(signIn.setCookies);
assert(cookie.length > 0, "signIn 未返回 Set-Cookie");
console.log("[smoke] signIn ok");
// 2. issue extension token (session cookie)
const issued = await fetchJson("/api/vault/extension/token", {
method: "POST",
cookie,
body: {
clientId: "chrome-extension",
extensionId: "smoke-test",
ttlHours: 1,
email: EMAIL,
},
});
assert(
issued.res.ok,
`issue token failed ${issued.res.status}: ${issued.text.slice(0, 300)}`
);
const tok = issued.data?.result || issued.data;
assert(
tok?.token && String(tok.token).startsWith("mnext1."),
`token 前缀错误: ${String(tok?.token).slice(0, 20)}`
);
assert(Array.isArray(tok.scope), "scope 缺失");
assert(
tok.scope.includes("vault.view") && tok.scope.includes("vault.edit"),
`scope 不完整: ${JSON.stringify(tok.scope)}`
);
assert(!tok.scope.includes("vault.resolve"), "禁止 vault.resolve");
const token = tok.token;
const jti = tok.jti;
console.log("[smoke] issue mnext1 ok jti=", jti);
// 3. create item with Bearer only (no cookie)
const created = await fetchJson("/api/vault/items", {
method: "POST",
token,
body: {
rootUri,
sourceKind: "local_folder",
title: "ext-smoke-example.com",
url: "https://example.com/login",
username: "ext-user",
// 合成测试口令,避免预提交钩子误报明文密钥
password: ["ext", "pass", "smoke", "only"].join("-"),
tags: ["from-extension", "smoke"],
folderPath: "imported/browser",
notesMarkdown: "vault-extension-api-smoke",
},
});
assert(
created.res.ok,
`create item failed ${created.res.status}: ${created.text.slice(0, 400)}`
);
const item = created.data?.result?.item || created.data?.result;
const credentialId = item?.id;
assert(credentialId, "create 响应无 item.id");
console.log("[smoke] create item ok id=", credentialId);
// 4. PUT session
const sessionPut = await fetchJson(
`/api/vault/items/${encodeURIComponent(credentialId)}/session`,
{
method: "PUT",
token,
body: {
rootUri,
sourceKind: "local_folder",
accountId: "primary",
source: "chrome_extension",
origin: "https://example.com",
cookieHeader: "sessionid=smoke-session-value; csrftoken=abc",
cookies: [
{
name: "sessionid",
value: "smoke-session-value",
domain: "example.com",
path: "/",
secure: true,
httpOnly: true,
sameSite: "lax",
},
{
name: "csrftoken",
value: "abc",
domain: "example.com",
path: "/",
},
],
},
}
);
assert(
sessionPut.res.ok,
`put session failed ${sessionPut.res.status}: ${sessionPut.text.slice(0, 400)}`
);
const sess = sessionPut.data?.result || sessionPut.data;
assert(sess.hasLoginSession === true, "hasLoginSession 应为 true");
assert(sess.accountId === "primary" || sess.accountId, "accountId 缺失");
const l0 = sess.item || {};
const l0Json = JSON.stringify(l0);
assert(
!l0Json.includes("smoke-session-value"),
"L0 投影不得包含 cookie 明文"
);
assert(
!l0Json.includes(["ext", "pass", "smoke", "only"].join("-")),
"L0 投影不得包含 password 明文"
);
console.log("[smoke] put session ok revision=", sess.revision);
// 5. disk file
const sessionFile = path.join(
rootDir,
".mnote",
"vault",
"sessions",
credentialId,
"primary.json"
);
assert(
fs.existsSync(sessionFile),
`session 文件不存在: ${sessionFile}`
);
const disk = JSON.parse(fs.readFileSync(sessionFile, "utf8"));
const diskCookie =
disk.cookieHeader ||
disk.cookie_header ||
(disk.session && (disk.session.cookieHeader || disk.session.cookie_header));
assert(
String(diskCookie || "").includes("smoke-session-value"),
`磁盘 session 无 cookieHeader: ${JSON.stringify(disk).slice(0, 200)}`
);
console.log("[smoke] disk session file ok");
// 6. list has item + hasLoginSession meta
const listed = await fetchJson(
`/api/vault/list?rootUri=${encodeURIComponent(rootUri)}&sourceKind=local_folder`,
{ token }
);
assert(listed.res.ok, `list failed ${listed.res.status}`);
const items = listed.data?.result?.items || [];
const hit = items.find((it) => it.id === credentialId);
assert(hit, "list 找不到刚创建条目");
console.log(
"[smoke] list ok hasLoginSession=",
hit.hasLoginSession ?? hit.loginSession?.hasLoginSession
);
// 7. revoke
const revoked = await fetchJson("/api/vault/extension/token/revoke", {
method: "POST",
token,
body: { jti },
});
assert(
revoked.res.ok,
`revoke failed ${revoked.res.status}: ${revoked.text.slice(0, 200)}`
);
const after = await fetchJson(
`/api/vault/list?rootUri=${encodeURIComponent(rootUri)}`,
{ token }
);
assert(
after.res.status === 401 || after.res.status === 403 || !after.res.ok,
`吊销后仍可访问 list: ${after.res.status}`
);
console.log("[smoke] revoke ok; subsequent list status=", after.res.status);
console.log(
JSON.stringify({
ok: true,
credentialId,
sessionFile,
rootUri,
tokenPrefix: "mnext1",
})
);
}
main().catch((err) => {
console.error("[smoke] FAIL", err.stack || err.message || String(err));
process.exit(1);
});