fix(mindspace): lower page access password minimum to 6 characters
Align publication validation, portal gate HTML, H5 publish UI, MCP docs, and page-data skill guidance so passwords like 888888 are accepted while 3-character values such as 888 remain rejected. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -78,11 +78,20 @@ function normalizeAccessMode(value) {
|
|||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PAGE_ACCESS_PASSWORD_MIN_LENGTH = 6;
|
||||||
|
export const PAGE_ACCESS_PASSWORD_MAX_LENGTH = 128;
|
||||||
|
|
||||||
function normalizePassword(value, required) {
|
function normalizePassword(value, required) {
|
||||||
const password = String(value ?? '');
|
const password = String(value ?? '');
|
||||||
if (!required && !password) return null;
|
if (!required && !password) return null;
|
||||||
if (password.length < 8 || password.length > 128) {
|
if (
|
||||||
throw publicationError('访问密码长度必须为 8 到 128 个字符', 'invalid_publish_input');
|
password.length < PAGE_ACCESS_PASSWORD_MIN_LENGTH
|
||||||
|
|| password.length > PAGE_ACCESS_PASSWORD_MAX_LENGTH
|
||||||
|
) {
|
||||||
|
throw publicationError(
|
||||||
|
`访问密码长度必须为 ${PAGE_ACCESS_PASSWORD_MIN_LENGTH} 到 ${PAGE_ACCESS_PASSWORD_MAX_LENGTH} 个字符`,
|
||||||
|
'invalid_publish_input',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return password;
|
return password;
|
||||||
}
|
}
|
||||||
@@ -1083,7 +1092,7 @@ export function createPublicationService(pool, options = {}) {
|
|||||||
} else if (publication.password_hash) {
|
} else if (publication.password_hash) {
|
||||||
passwordHash = publication.password_hash;
|
passwordHash = publication.password_hash;
|
||||||
} else {
|
} else {
|
||||||
throw publicationError('访问密码长度必须为 8 到 128 个字符', 'invalid_publish_input');
|
throw publicationError('口令访问模式必须提供访问密码', 'invalid_publish_input');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -1498,6 +1507,8 @@ async function registerPublicationArtifactForConversation({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const publicationInternals = {
|
export const publicationInternals = {
|
||||||
|
PAGE_ACCESS_PASSWORD_MIN_LENGTH,
|
||||||
|
PAGE_ACCESS_PASSWORD_MAX_LENGTH,
|
||||||
normalizeSlug,
|
normalizeSlug,
|
||||||
normalizeAccessMode,
|
normalizeAccessMode,
|
||||||
normalizePassword,
|
normalizePassword,
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ test('hashes access passwords with a random salt', () => {
|
|||||||
() => publicationInternals.normalizePassword('short', true),
|
() => publicationInternals.normalizePassword('short', true),
|
||||||
(error) => error.code === 'invalid_publish_input',
|
(error) => error.code === 'invalid_publish_input',
|
||||||
);
|
);
|
||||||
|
assert.equal(publicationInternals.normalizePassword('888888', true), '888888');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('requires future expiry timestamps for time-limited pages', () => {
|
test('requires future expiry timestamps for time-limited pages', () => {
|
||||||
|
|||||||
@@ -473,7 +473,7 @@ const ALL_TOOLS = [
|
|||||||
password: {
|
password: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
description:
|
description:
|
||||||
'口令访问密码,accessMode=password 时可选;未提供则默认 88888888(至少 8 位)',
|
'口令访问密码,accessMode=password 时可选;未提供则默认 88888888(至少 6 位)',
|
||||||
},
|
},
|
||||||
urlSlug: { type: 'string', description: '发布 URL slug,可选' },
|
urlSlug: { type: 'string', description: '发布 URL slug,可选' },
|
||||||
datasets: {
|
datasets: {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const SUGGESTION_BY_CODE = Object.freeze({
|
|||||||
title: '页面数据绑定或发布失败',
|
title: '页面数据绑定或发布失败',
|
||||||
actions: Object.freeze([
|
actions: Object.freeze([
|
||||||
'确认 dataset 已 private_data_register_dataset 注册',
|
'确认 dataset 已 private_data_register_dataset 注册',
|
||||||
'对每个公开页调用 private_data_bind_workspace_page(含 password 策略时口令 ≥8 位)',
|
'对每个公开页调用 private_data_bind_workspace_page(含 password 策略时口令 ≥6 位)',
|
||||||
'检查 .mindspace/page-data-policies 是否生成且列白名单与表单字段一致',
|
'检查 .mindspace/page-data-policies 是否生成且列白名单与表单字段一致',
|
||||||
]),
|
]),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function resolvePageDataBindAccess(accessMode = 'public') {
|
|||||||
return publicationInternals.normalizeAccessMode(accessMode);
|
return publicationInternals.normalizeAccessMode(accessMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD;<8 位抛错 */
|
/** password 模式:用户未传口令时使用 DEFAULT_PAGE_DATA_ADMIN_PASSWORD;<6 位抛错 */
|
||||||
export function resolvePageDataBindPassword(accessMode, password = null) {
|
export function resolvePageDataBindPassword(accessMode, password = null) {
|
||||||
const mode = resolvePageDataBindAccess(accessMode);
|
const mode = resolvePageDataBindAccess(accessMode);
|
||||||
if (mode !== 'password') return null;
|
if (mode !== 'password') return null;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function renderPublicationPasswordGate(
|
|||||||
<form method="post" action="${action}">
|
<form method="post" action="${action}">
|
||||||
<h1>此页面受密码保护</h1>
|
<h1>此页面受密码保护</h1>
|
||||||
<p>请输入发布者提供的访问密码。密码不会写入链接或浏览器日志。</p>
|
<p>请输入发布者提供的访问密码。密码不会写入链接或浏览器日志。</p>
|
||||||
<input type="password" name="password" minlength="8" maxlength="128" required autocomplete="current-password">
|
<input type="password" name="password" minlength="6" maxlength="128" required autocomplete="current-password">
|
||||||
<button type="submit">访问页面</button>
|
<button type="submit">访问页面</button>
|
||||||
</form>
|
</form>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ HTML 页面只能调用 Page Data API(page-data-client.js);
|
|||||||
|----|--------|
|
|----|--------|
|
||||||
| 访客 | 匿名提交(`public`,仅 `insert`) |
|
| 访客 | 匿名提交(`public`,仅 `insert`) |
|
||||||
| 后台 | **独立** HTML 页(`password`,仅 `read`) |
|
| 后台 | **独立** HTML 页(`password`,仅 `read`) |
|
||||||
| 后台口令 | **`88888888`**(平台要求 8~128 位;用户可指定其它合法口令覆盖) |
|
| 后台口令 | **`88888888`**(平台要求 6~128 位;用户可指定其它合法口令覆盖,如 `888888`) |
|
||||||
| 提交后修改 | 不支持(一次性) |
|
| 提交后修改 | 不支持(一次性) |
|
||||||
| 页面 | `public/*-survey.html` + `public/*-admin.html`(两文件各 bind 一次) |
|
| 页面 | `public/*-survey.html` + `public/*-admin.html`(两文件各 bind 一次) |
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ HTML 页面只能调用 Page Data API(page-data-client.js);
|
|||||||
|
|
||||||
口令规则:
|
口令规则:
|
||||||
|
|
||||||
- **禁止**接受或使用 <8 位口令(如 `888`);若用户坚持短口令,说明平台限制并代用 `88888888` 或请其给出 ≥8 位。
|
- **禁止**接受或使用 <6 位口令(如 `888`);若用户坚持更短口令,说明平台限制并代用 `888888` / `88888888` 或请其给出 ≥6 位。
|
||||||
- bind 时 **`password` 必须传入**且会写入发布记录;禁止只改 `access_mode` 不写 `password_hash`。
|
- bind 时 **`password` 必须传入**且会写入发布记录;禁止只改 `access_mode` 不写 `password_hash`。
|
||||||
- 交付说明写明后台口令;用户可在 MindSpace「页面数据」面板重置。
|
- 交付说明写明后台口令;用户可在 MindSpace「页面数据」面板重置。
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ HTML 页面只能调用 Page Data API(page-data-client.js);
|
|||||||
**仅以下情况才问用户(每次最多 1~2 点)**:
|
**仅以下情况才问用户(每次最多 1~2 点)**:
|
||||||
|
|
||||||
1. 表述同时命中两个互斥分支(如「匿名提交」+「同页内嵌口令看全量」)→ 给 A/E 选项说明须拆页
|
1. 表述同时命中两个互斥分支(如「匿名提交」+「同页内嵌口令看全量」)→ 给 A/E 选项说明须拆页
|
||||||
2. 用户明确要的口令 <8 位 → 请改口令或确认用默认 `88888888`
|
2. 用户明确要的口令 <6 位 → 请改口令或确认用默认 `88888888`
|
||||||
3. 需要 B/C 但表结构是否要 `created_by_user_id` 等列尚不清楚 → 确认登录隔离
|
3. 需要 B/C 但表结构是否要 `created_by_user_id` 等列尚不清楚 → 确认登录隔离
|
||||||
|
|
||||||
**禁止**连续多轮只输出「我先检查工作区/加载技能」而不调用工具;**第一轮工具**应是 `load_skill` 或 `list_dir` / `private_data_execute`,不是空计划。
|
**禁止**连续多轮只输出「我先检查工作区/加载技能」而不调用工具;**第一轮工具**应是 `load_skill` 或 `list_dir` / `private_data_execute`,不是空计划。
|
||||||
@@ -82,7 +82,7 @@ HTML 页面只能调用 Page Data API(page-data-client.js);
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
- public:可匿名 insert;服务端禁止 update / softDelete
|
- public:可匿名 insert;服务端禁止 update / softDelete
|
||||||
- password:所有 API 须先 authenticate;口令 ≥8 位且须写入发布记录
|
- password:所有 API 须先 authenticate;口令 ≥6 位且须写入发布记录
|
||||||
- `public` 策略中的 `insert: true` 等价于任何访客都能直接调用 API 写入;前端口令、隐藏按钮和 JavaScript 判断都不是权限控制
|
- `public` 策略中的 `insert: true` 等价于任何访客都能直接调用 API 写入;前端口令、隐藏按钮和 JavaScript 判断都不是权限控制
|
||||||
- password 页面必须调用 `client.authenticate(password)` 获取服务端 token;禁止在 HTML 中硬编码口令或只做 `password === "..."` 比较
|
- password 页面必须调用 `client.authenticate(password)` 获取服务端 token;禁止在 HTML 中硬编码口令或只做 `password === "..."` 比较
|
||||||
- 同一公开页若要求「所有人可评价、只有所有者可写正文」:正文 dataset 在公开页必须 `insert: false`,评价 dataset 才能 `insert: true`;所有者写正文必须使用独立 `password` 作者页或 `login_required` 页面
|
- 同一公开页若要求「所有人可评价、只有所有者可写正文」:正文 dataset 在公开页必须 `insert: false`,评价 dataset 才能 `insert: true`;所有者写正文必须使用独立 `password` 作者页或 `login_required` 页面
|
||||||
@@ -99,7 +99,7 @@ HTML 页面只能调用 Page Data API(page-data-client.js);
|
|||||||
- 分支:A 匿名提交 + 独立口令后台
|
- 分支:A 匿名提交 + 独立口令后台
|
||||||
- 访客:匿名提交(public,仅 insert)
|
- 访客:匿名提交(public,仅 insert)
|
||||||
- 管理员:独立后台页(password,仅 read)
|
- 管理员:独立后台页(password,仅 read)
|
||||||
- 后台口令:88888888(可改为你的 ≥8 位口令)
|
- 后台口令:88888888(可改为你的 ≥6 位口令,如 888888)
|
||||||
- 提交后修改:不支持
|
- 提交后修改:不支持
|
||||||
- 页面:public/xxx.html + public/xxx-admin.html
|
- 页面:public/xxx.html + public/xxx-admin.html
|
||||||
- 内容:(由你的描述生成题目/字段/报表)
|
- 内容:(由你的描述生成题目/字段/报表)
|
||||||
@@ -283,7 +283,7 @@ await client.deleteRow('dataset_name', rowId);
|
|||||||
|
|
||||||
若问卷与后台是多个 HTML 文件,对每个 `public/*.html` 各调用一次 bind:
|
若问卷与后台是多个 HTML 文件,对每个 `public/*.html` 各调用一次 bind:
|
||||||
- **问卷页**:`accessMode: "public"` + dataset 仅 `insert`
|
- **问卷页**:`accessMode: "public"` + dataset 仅 `insert`
|
||||||
- **后台页**:`accessMode: "password"` + dataset 仅 `read`(发布口令至少 8 位)
|
- **后台页**:`accessMode: "password"` + dataset 仅 `read`(发布口令至少 6 位)
|
||||||
|
|
||||||
**顺序**:先 `write_file` 完整 HTML → 再 `bind`;禁止只写「问卷页面」占位文字就发布。
|
**顺序**:先 `write_file` 完整 HTML → 再 `bind`;禁止只写「问卷页面」占位文字就发布。
|
||||||
|
|
||||||
@@ -301,7 +301,7 @@ await client.deleteRow('dataset_name', rowId);
|
|||||||
// pageId 可省略:平台访问时注入 __MINDSPACE_PAGE_DATA__
|
// pageId 可省略:平台访问时注入 __MINDSPACE_PAGE_DATA__
|
||||||
const client = MindSpacePageData.createClient({ apiBase: '/api' });
|
const client = MindSpacePageData.createClient({ apiBase: '/api' });
|
||||||
|
|
||||||
// 口令页后台:await client.authenticate('88888888'); // 口令 ≥8 位,与发布时一致
|
// 口令页后台:await client.authenticate('88888888'); // 口令 ≥6 位,与发布时一致
|
||||||
// 提交:await client.insertRow('survey_responses', { q1_feature: '...', ... });
|
// 提交:await client.insertRow('survey_responses', { q1_feature: '...', ... });
|
||||||
// 列表:const { rows } = await client.listRows('survey_responses', { limit: 50 });
|
// 列表:const { rows } = await client.listRows('survey_responses', { limit: 50 });
|
||||||
</script>
|
</script>
|
||||||
@@ -324,7 +324,7 @@ await client.deleteRow('dataset_name', rowId);
|
|||||||
1. **禁止**在尚未获得创建授权时建表 / bind / 发布;用户明确要求「创建、完成、发布、直接做、不用询问、持续推进」均视为确认,禁止再次停下来询问
|
1. **禁止**在尚未获得创建授权时建表 / bind / 发布;用户明确要求「创建、完成、发布、直接做、不用询问、持续推进」均视为确认,禁止再次停下来询问
|
||||||
2. **禁止**让 LLM 自行发明 accessMode/拆页/口令;必须落在分支 A~E 与默认口令规则内
|
2. **禁止**让 LLM 自行发明 accessMode/拆页/口令;必须落在分支 A~E 与默认口令规则内
|
||||||
3. **禁止**连续两轮仅输出计划、不调用 `load_skill` / `list_dir` / `write_file` / `private_data_execute` / `bind`
|
3. **禁止**连续两轮仅输出计划、不调用 `load_skill` / `list_dir` / `write_file` / `private_data_execute` / `bind`
|
||||||
4. **禁止**接受 <8 位发布口令;用户未指定口令时后台默认 **`88888888`**
|
4. **禁止**接受 <6 位发布口令;用户未指定口令时后台默认 **`88888888`**
|
||||||
5. **禁止**创建 `scripts/*-api.mjs`、Express 服务、或监听独立端口(如 `8899`)
|
5. **禁止**创建 `scripts/*-api.mjs`、Express 服务、或监听独立端口(如 `8899`)
|
||||||
6. **禁止** HTML 中硬编码 `http://127.0.0.1:端口` 或自定义 `/api/survey/*`
|
6. **禁止** HTML 中硬编码 `http://127.0.0.1:端口` 或自定义 `/api/survey/*`
|
||||||
7. **禁止**在 HTML 中使用 `onclick` / `oninput` 等内联事件属性(MindSpace 发布页 CSP 不允许);改用 `addEventListener`
|
7. **禁止**在 HTML 中使用 `onclick` / `oninput` 等内联事件属性(MindSpace 发布页 CSP 不允许);改用 `addEventListener`
|
||||||
@@ -340,7 +340,7 @@ await client.deleteRow('dataset_name', rowId);
|
|||||||
|
|
||||||
## 交付前自检
|
## 交付前自检
|
||||||
|
|
||||||
0. 已选定分支 A~E,方案摘要已确认;`password` 页口令 ≥8 位且 bind 已传入
|
0. 已选定分支 A~E,方案摘要已确认;`password` 页口令 ≥6 位且 bind 已传入
|
||||||
1. `private_data_register_dataset` 已注册;PostgreSQL 表字段与 policy 一致(含 `deleted_at` 等)
|
1. `private_data_register_dataset` 已注册;PostgreSQL 表字段与 policy 一致(含 `deleted_at` 等)
|
||||||
2. `private_data_bind_workspace_page` **已成功**,返回 UUID 形式 `pageId` 与 `deliveryUrl`
|
2. `private_data_bind_workspace_page` **已成功**,返回 UUID 形式 `pageId` 与 `deliveryUrl`
|
||||||
3. `.mindspace/page-data-policies/<pageId>.json` 已写入(文件名必须是 UUID,不是 slug)
|
3. `.mindspace/page-data-policies/<pageId>.json` 已写入(文件名必须是 UUID,不是 slug)
|
||||||
|
|||||||
@@ -401,12 +401,12 @@ export function MindSpacePageDataOpsPanel({ pageId, pageTitle, accessMode, onClo
|
|||||||
type="password"
|
type="password"
|
||||||
value={newPassword}
|
value={newPassword}
|
||||||
onChange={(event) => setNewPassword(event.target.value)}
|
onChange={(event) => setNewPassword(event.target.value)}
|
||||||
placeholder="新页面口令(至少 8 位)"
|
placeholder="新页面口令(至少 6 位)"
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={busyAction != null || newPassword.trim().length < 8}
|
disabled={busyAction != null || newPassword.trim().length < 6}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
void runAction('reset-password', async () => {
|
void runAction('reset-password', async () => {
|
||||||
const result = await resetPageDataPassword(pageId, newPassword.trim());
|
const result = await resetPageDataPassword(pageId, newPassword.trim());
|
||||||
|
|||||||
@@ -1362,7 +1362,7 @@ export function MindSpacePageDetail({
|
|||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={accessPassword}
|
value={accessPassword}
|
||||||
minLength={8}
|
minLength={6}
|
||||||
maxLength={128}
|
maxLength={128}
|
||||||
autoComplete="new-password"
|
autoComplete="new-password"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user