@bolloon/bolloon-agent 0.4.29 → 0.4.30
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/dist/agents/contacts/chain.js +579 -0
- package/dist/agents/contacts/cli.js +158 -0
- package/dist/agents/contacts/consent.js +124 -0
- package/dist/agents/contacts/grant-payload.js +45 -0
- package/dist/agents/contacts/grants.js +451 -0
- package/dist/agents/contacts/policy.js +264 -0
- package/dist/agents/contacts/preview-types.js +6 -0
- package/dist/agents/contacts/providers.js +341 -0
- package/dist/agents/contacts/store.js +326 -0
- package/dist/agents/contacts/tools.js +154 -0
- package/dist/agents/contacts/types.js +148 -0
- package/dist/agents/pi-sdk.js +10 -0
- package/dist/electron-build/agents/contacts/chain.js +619 -0
- package/dist/electron-build/agents/contacts/chain.js.map +1 -0
- package/dist/electron-build/agents/contacts/consent.js +163 -0
- package/dist/electron-build/agents/contacts/consent.js.map +1 -0
- package/dist/electron-build/agents/contacts/grant-payload.js +52 -0
- package/dist/electron-build/agents/contacts/grant-payload.js.map +1 -0
- package/dist/electron-build/agents/contacts/grants.js +500 -0
- package/dist/electron-build/agents/contacts/grants.js.map +1 -0
- package/dist/electron-build/agents/contacts/policy.js +271 -0
- package/dist/electron-build/agents/contacts/policy.js.map +1 -0
- package/dist/electron-build/agents/contacts/preview-types.js +8 -0
- package/dist/electron-build/agents/contacts/preview-types.js.map +1 -0
- package/dist/electron-build/agents/contacts/providers.js +384 -0
- package/dist/electron-build/agents/contacts/providers.js.map +1 -0
- package/dist/electron-build/agents/contacts/store.js +370 -0
- package/dist/electron-build/agents/contacts/store.js.map +1 -0
- package/dist/electron-build/agents/contacts/tools.js +159 -0
- package/dist/electron-build/agents/contacts/tools.js.map +1 -0
- package/dist/electron-build/agents/contacts/types.js +166 -0
- package/dist/electron-build/agents/contacts/types.js.map +1 -0
- package/dist/electron-build/agents/external-events.js.map +1 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +10 -0
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/security/tool-gate.js +4 -0
- package/dist/electron-build/security/tool-gate.js.map +1 -1
- package/dist/index.js +30 -0
- package/dist/security/tool-gate.js +4 -0
- package/dist/web/mobile-contacts.js +329 -0
- package/dist/web/mobile-core.js +481 -15
- package/dist/web/mobile.html +32 -0
- package/dist/web/mobile.js +139 -0
- package/dist/web/routes-contacts.js +420 -0
- package/dist/web/server.js +5 -0
- package/package.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contacts/cli.ts — `/contacts` 统一入口 (2026-09-19, Phase 10)
|
|
3
|
+
*
|
|
4
|
+
* 刻意只有三个用户入口 (leo 要求不要再堆页面):
|
|
5
|
+
* /contacts 看联系方式 + 授权状态 + 等待中的回复
|
|
6
|
+
* /contacts authorize [选项] 一次性授予 长期/完全 授权 (默认"长期使用")
|
|
7
|
+
* /contacts revoke [目标] 暂停/撤销授权
|
|
8
|
+
*
|
|
9
|
+
* 输出是纯行数组, 由调用方 (CLI/Web/手机) 决定怎么上色 —— 三端读同一份 Grant 事实。
|
|
10
|
+
*/
|
|
11
|
+
import { FORBIDDEN_CATEGORIES, presetForChoice } from './grants.js';
|
|
12
|
+
const CHOICE_ALIAS = {
|
|
13
|
+
once: 'task_once', task: 'task_once', 'task_once': 'task_once', '本次': 'task_once', '仅本次任务': 'task_once',
|
|
14
|
+
long: 'persistent', persistent: 'persistent', '长期': 'persistent', '长期使用': 'persistent',
|
|
15
|
+
full: 'full_contact_access', all: 'full_contact_access', 'full_contact_access': 'full_contact_access', '完全': 'full_contact_access', '完全授权': 'full_contact_access',
|
|
16
|
+
};
|
|
17
|
+
/** 统一授权卡 (Phase 3): 用户不需要理解 phone.contact / email.contact 是两个 Skill */
|
|
18
|
+
export function authorizationCard() {
|
|
19
|
+
const p = presetForChoice('persistent');
|
|
20
|
+
const f = presetForChoice('full_contact_access');
|
|
21
|
+
return [
|
|
22
|
+
'让 Bolloon 代表你联系外部的人',
|
|
23
|
+
'',
|
|
24
|
+
'将获得权限:',
|
|
25
|
+
' ✓ 使用已验证的手机号发送消息',
|
|
26
|
+
' ✓ 使用已验证的邮箱发送邮件',
|
|
27
|
+
' ✓ 自动等待回复, 回复到达后继续执行任务',
|
|
28
|
+
' ✓ 在未来任务中自动使用 (不再逐次打断你)',
|
|
29
|
+
'',
|
|
30
|
+
'不会获得权限:',
|
|
31
|
+
' × 读取全部邮箱 / 读取通讯录 / 短信历史',
|
|
32
|
+
' × 群发消息 (批量永远禁止)',
|
|
33
|
+
' × 自动支付或转账',
|
|
34
|
+
' × 代表你签署合同或做承诺',
|
|
35
|
+
' × 拿到手机号/邮箱明文 (只看得到脱敏值)',
|
|
36
|
+
'',
|
|
37
|
+
`选项: 仅本次任务 (task_once) · 长期使用 (persistent, 推荐) · 完全授权联系方式能力 (full_contact_access)`,
|
|
38
|
+
`默认长期使用: channels=${p.channels} · contactScope=${p.contactScope} · taskScope=${p.taskScope} · contentScope=${p.contentScope}`,
|
|
39
|
+
`完全授权额外带来: contactScope=${f.contactScope} (仍只覆盖**已验证**联系人) · contentScope=${f.contentScope} (身份信息/项目数据可直接发)`,
|
|
40
|
+
`无论哪一档都不放行: ${FORBIDDEN_CATEGORIES.join(', ')} (凭证/资金指令/合同承诺属另一类高风险能力)`,
|
|
41
|
+
`无论哪一档都保留: 单收件人 · 频率限制 · 任务关联 · requestId 幂等 · provider 检查 · 发送证据 · 随时撤销`,
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
function label(g) {
|
|
45
|
+
return g.level === 'full_contact_access' ? '完全授权' : g.level === 'persistent' ? '长期自动使用' : g.level === 'task_once' ? '仅本次任务' : '不允许';
|
|
46
|
+
}
|
|
47
|
+
export async function contactsCli(chain, rest) {
|
|
48
|
+
const argv = rest.split(/\s+/).filter(Boolean);
|
|
49
|
+
const sub = (argv[0] || '').toLowerCase();
|
|
50
|
+
// ── /contacts authorize [...] ─────────────────────────────────────────────
|
|
51
|
+
if (sub === 'authorize' || sub === 'grant') {
|
|
52
|
+
const choice = CHOICE_ALIAS[(argv[1] || '').toLowerCase()] || 'persistent';
|
|
53
|
+
const out = authorizationCard();
|
|
54
|
+
const r = await chain.authorize({ choice, grantedBy: 'leo', grantedVia: 'cli' });
|
|
55
|
+
out.push('');
|
|
56
|
+
out.push(`已授权: ${r.userLabel} — grantId=${r.grant.grantId} v${r.grant.grantVersion}`);
|
|
57
|
+
out.push(`范围: channels=${r.grant.channels} · contactScope=${r.grant.contactScope} · taskScope=${r.grant.taskScope} · contentScope=${r.grant.contentScope}`);
|
|
58
|
+
out.push('之后 Agent 联系已验证的人不再逐次打断你; 仍然受频率/任务/幂等/证据/撤销约束');
|
|
59
|
+
out.push('随时可用: /contacts revoke all (撤销) · /contacts pause (暂停, 保留配置)');
|
|
60
|
+
return { ok: true, lines: out };
|
|
61
|
+
}
|
|
62
|
+
// ── /contacts revoke [...] ────────────────────────────────────────────────
|
|
63
|
+
if (sub === 'revoke') {
|
|
64
|
+
const target = (argv[1] || 'all').toLowerCase();
|
|
65
|
+
if (target === 'all' || target === 'grant') {
|
|
66
|
+
const r = await chain.revokeAllGrants({ by: 'leo', reason: 'CLI /contacts revoke all' });
|
|
67
|
+
return {
|
|
68
|
+
ok: true,
|
|
69
|
+
lines: [
|
|
70
|
+
`已撤销授权: ${r.revoked.length ? r.revoked.join(', ') : '(没有生效中的授权)'}`,
|
|
71
|
+
r.affectedGoals.length ? `受影响任务 (转人工, 不再自动唤醒): ${r.affectedGoals.join(', ')}` : '没有正在等待回复的任务受影响',
|
|
72
|
+
'历史 Run/Goal 证据保留; 旧授权不能重新启用 —— 需要就要重新 /contacts authorize',
|
|
73
|
+
],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const g = await chain.grants.get(target);
|
|
77
|
+
if (g) {
|
|
78
|
+
const r = await chain.revokeGrant(target, { by: 'leo', reason: 'CLI' });
|
|
79
|
+
return { ok: r.ok, lines: [`已撤销授权 ${target}`, r.affectedGoals.length ? `受影响任务: ${r.affectedGoals.join(', ')}` : '(无等待中的任务)'] };
|
|
80
|
+
}
|
|
81
|
+
const rc = await chain.revoke({ contactId: target, by: 'leo', reason: 'CLI /contacts revoke' });
|
|
82
|
+
if (!rc.ok)
|
|
83
|
+
return { ok: false, lines: [`没找到授权或联系方式: ${target}`, '用法: /contacts revoke all | <grantId> | <contactId>'] };
|
|
84
|
+
return { ok: true, lines: [`已撤销联系方式 ${target} 的授权 (历史证据保留, 后续调用一律拒绝)`, rc.affectedGoals?.length ? `受影响任务: ${rc.affectedGoals.join(', ')}` : '(无等待中的任务)'] };
|
|
85
|
+
}
|
|
86
|
+
// ── /contacts pause | resume ──────────────────────────────────────────────
|
|
87
|
+
if (sub === 'pause' || sub === 'resume') {
|
|
88
|
+
const g = argv[1] ? await chain.grants.get(argv[1]) : await chain.grants.activeFor(chain.ownerDid);
|
|
89
|
+
if (!g)
|
|
90
|
+
return { ok: false, lines: ['没有找到可操作的授权 (用 /contacts 看当前状态)'] };
|
|
91
|
+
const r = sub === 'pause' ? await chain.pauseGrant(g.grantId, { by: 'leo' }) : await chain.resumeGrant(g.grantId, { by: 'leo' });
|
|
92
|
+
return { ok: r.ok, lines: [r.ok ? `${sub === 'pause' ? '已暂停' : '已恢复'}授权 ${g.grantId} (v${r.grant?.grantVersion})` : `失败: ${r.error}`] };
|
|
93
|
+
}
|
|
94
|
+
// ── /contacts bind phone|email <值> [别名] ────────────────────────────────
|
|
95
|
+
if (sub === 'bind') {
|
|
96
|
+
const kind = (argv[1] || '').toLowerCase();
|
|
97
|
+
if (kind !== 'phone' && kind !== 'email')
|
|
98
|
+
return { ok: false, lines: ['用法: /contacts bind phone|email <号码/邮箱> [别名]'] };
|
|
99
|
+
const b = await chain.bind({ kind, value: argv[2] || '', aliases: argv[3] ? [argv[3]] : undefined });
|
|
100
|
+
if (!b.ok)
|
|
101
|
+
return { ok: false, lines: [`绑定失败: ${b.error}`] };
|
|
102
|
+
const lines = [
|
|
103
|
+
`已登记 ${kind} ${b.contact.displayValue} (待验证, 通道 ${b.channelLabel || b.contact.provider})`,
|
|
104
|
+
'完整' + (kind === 'phone' ? '号码' : '邮箱') + '不会进入 prompt / Run / Git; 别人只看得到脱敏值',
|
|
105
|
+
];
|
|
106
|
+
if (b.otpForLocalSink)
|
|
107
|
+
lines.push(`本地落盘通道验证码 (未真实外发): ${b.otpForLocalSink} — 用 /contacts verify ${b.contact.contactId} <码>`);
|
|
108
|
+
else
|
|
109
|
+
lines.push(`验证码已通过通道外发, 收到后用 /contacts verify ${b.contact.contactId} <码> 完成验证`);
|
|
110
|
+
return { ok: true, lines };
|
|
111
|
+
}
|
|
112
|
+
if (sub === 'verify') {
|
|
113
|
+
const r = await chain.verify({ contactId: argv[1] || '', challengeId: argv[3] || '', code: argv[2] || '' });
|
|
114
|
+
if (!r.ok)
|
|
115
|
+
return { ok: false, lines: [`验证失败: ${r.error}`] };
|
|
116
|
+
return { ok: true, lines: [`验证通过: ${r.contact.displayValue} (能力 ${r.contact.capabilities.join(', ')})`, '下一步: /contacts authorize (给长期授权) —— 验证 ≠ 自动获得长期发送权限'] };
|
|
117
|
+
}
|
|
118
|
+
// ── /contacts (状态) ──────────────────────────────────────────────────────
|
|
119
|
+
const summary = await chain.grantsSummary();
|
|
120
|
+
const contacts = await chain.listAuthorized();
|
|
121
|
+
const pending = await chain.consents.list('pending');
|
|
122
|
+
const sends = await chain.store.listSends();
|
|
123
|
+
const lines = [];
|
|
124
|
+
lines.push('联系方式与授权');
|
|
125
|
+
lines.push(` 授权状态: ${summary.effective}`);
|
|
126
|
+
if (summary.grants.length) {
|
|
127
|
+
for (const g of summary.grants) {
|
|
128
|
+
lines.push(` · ${g.grantId} ${label({ level: g.level })} [${g.status}] channels=${g.channels} contactScope=${g.contactScope} taskScope=${g.taskScope} contentScope=${g.contentScope} v${g.grantVersion}${g.signed ? ` · 手机签名(${g.deviceId})` : ''}${g.lastUsedAt ? ` · 最近使用 ${g.lastUsedAt.slice(0, 16)}` : ''}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
lines.push(' (没有长期授权 —— 每次联系都会打断你一次; 用 /contacts authorize 授权一次即可长期使用)');
|
|
133
|
+
}
|
|
134
|
+
lines.push(' 联系方式:');
|
|
135
|
+
if (!contacts.length)
|
|
136
|
+
lines.push(' (还没有绑定 —— /contacts bind email 你的邮箱 / /contacts bind phone +8613… )');
|
|
137
|
+
for (const c of contacts) {
|
|
138
|
+
lines.push(` · ${c.displayValue} [${c.kind}] ${c.status} · 能力 ${c.capabilities.length ? c.capabilities.join(', ') : '无'} · 策略 ${c.policy} · ${c.capabilities.includes('send') ? '可联系' : '不可联系'}`);
|
|
139
|
+
}
|
|
140
|
+
if (pending.length) {
|
|
141
|
+
lines.push(` 待你批准 (${pending.length}):`);
|
|
142
|
+
for (const p of pending)
|
|
143
|
+
lines.push(` · ${p.consentId} ${p.contactName} [${p.channel}] ${p.reason} · requestId=${p.requestId}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
lines.push(' 待你批准: 无');
|
|
147
|
+
}
|
|
148
|
+
const waiting = sends.filter((s) => s.status === 'sent' && s.goalId);
|
|
149
|
+
lines.push(` 正在等回复: ${waiting.length ? waiting.map((s) => `${s.goalId}(${s.contactId})`).join(', ') : '无'}`);
|
|
150
|
+
const last = (await chain.store.readLedger({ limit: 5 })).slice(-5);
|
|
151
|
+
if (last.length) {
|
|
152
|
+
lines.push(' 最近通信:');
|
|
153
|
+
for (const l of last)
|
|
154
|
+
lines.push(` · ${l.ts.slice(0, 16)} ${l.activity} ${l.detail.slice(0, 80)}`);
|
|
155
|
+
}
|
|
156
|
+
lines.push(' 用法: /contacts authorize [once|long|full] · /contacts revoke all|<grantId> · /contacts pause|resume · /contacts bind|verify');
|
|
157
|
+
return { ok: true, lines };
|
|
158
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contacts/consent.ts — 人工批准 (授权) 流程 (2026-09-19, Phase 6/7)
|
|
3
|
+
*
|
|
4
|
+
* 与 payment-approval.ts 同构: 首次联系/敏感内容/每次确认/策略要求时,
|
|
5
|
+
* 先落一条 pending 授权请求 → 人批准后才真正发送 (executor 由 chain 注入)。
|
|
6
|
+
*
|
|
7
|
+
* 落盘: ~/.bolloon/contacts/consents.json
|
|
8
|
+
* 关键纪律: 批准后**不绕过** policy —— executor 里会再判一次 (批准 ≠ 免检)。
|
|
9
|
+
*/
|
|
10
|
+
import * as fsp from 'fs/promises';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
let _executor = null;
|
|
13
|
+
export function setConsentExecutor(fn) { _executor = fn; }
|
|
14
|
+
const DEFAULT_TTL_MS = 24 * 60 * 60_000;
|
|
15
|
+
export class ContactConsentStore {
|
|
16
|
+
ttlMs;
|
|
17
|
+
file;
|
|
18
|
+
items = [];
|
|
19
|
+
loaded = false;
|
|
20
|
+
constructor(dir, ttlMs = DEFAULT_TTL_MS) {
|
|
21
|
+
this.ttlMs = ttlMs;
|
|
22
|
+
this.file = path.join(dir, 'consents.json');
|
|
23
|
+
}
|
|
24
|
+
async ensure() {
|
|
25
|
+
if (this.loaded)
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
const raw = JSON.parse(await fsp.readFile(this.file, 'utf8'));
|
|
29
|
+
this.items = Array.isArray(raw) ? raw : [];
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
if (err?.code !== 'ENOENT')
|
|
33
|
+
throw new Error(`授权存储损坏 (${this.file}): ${err?.message || err}`);
|
|
34
|
+
this.items = [];
|
|
35
|
+
}
|
|
36
|
+
this.loaded = true;
|
|
37
|
+
}
|
|
38
|
+
async persist() {
|
|
39
|
+
const tmp = `${this.file}.tmp-${process.pid}`;
|
|
40
|
+
await fsp.mkdir(path.dirname(this.file), { recursive: true });
|
|
41
|
+
await fsp.writeFile(tmp, JSON.stringify(this.items.slice(-300), null, 2), 'utf8');
|
|
42
|
+
await fsp.rename(tmp, this.file);
|
|
43
|
+
}
|
|
44
|
+
async request(input) {
|
|
45
|
+
await this.ensure();
|
|
46
|
+
const now = input.now ?? Date.now();
|
|
47
|
+
const c = {
|
|
48
|
+
...input,
|
|
49
|
+
consentId: `cc-${now.toString(36)}-${Math.random().toString(36).slice(2, 6)}`,
|
|
50
|
+
status: 'pending',
|
|
51
|
+
createdAt: now,
|
|
52
|
+
expiresAt: now + this.ttlMs,
|
|
53
|
+
};
|
|
54
|
+
this.items.push(c);
|
|
55
|
+
await this.persist();
|
|
56
|
+
return c;
|
|
57
|
+
}
|
|
58
|
+
async get(consentId) {
|
|
59
|
+
await this.ensure();
|
|
60
|
+
return this.items.find((x) => x.consentId === consentId) || null;
|
|
61
|
+
}
|
|
62
|
+
async list(status) {
|
|
63
|
+
await this.ensure();
|
|
64
|
+
return status ? this.items.filter((x) => x.status === status) : [...this.items];
|
|
65
|
+
}
|
|
66
|
+
/** 批准 → 立刻执行 (executor 里再判 policy) */
|
|
67
|
+
async approve(consentId, opts = {}) {
|
|
68
|
+
await this.ensure();
|
|
69
|
+
const c = await this.get(consentId);
|
|
70
|
+
if (!c)
|
|
71
|
+
return { ok: false, error: 'consent_not_found' };
|
|
72
|
+
if (c.status !== 'pending')
|
|
73
|
+
return { ok: false, error: `授权 ${consentId} 状态 ${c.status}, 不可批准` };
|
|
74
|
+
const now = opts.now ?? Date.now();
|
|
75
|
+
if (c.expiresAt < now) {
|
|
76
|
+
c.status = 'expired';
|
|
77
|
+
await this.persist();
|
|
78
|
+
return { ok: false, error: 'consent_expired' };
|
|
79
|
+
}
|
|
80
|
+
c.status = 'approved';
|
|
81
|
+
c.decidedAt = now;
|
|
82
|
+
c.decidedBy = opts.by || 'user';
|
|
83
|
+
c.decidedVia = opts.via || 'cli';
|
|
84
|
+
await this.persist();
|
|
85
|
+
if (!_executor)
|
|
86
|
+
return { ok: true, consent: c, error: 'no_executor: 已批准但没有执行器 (调用方未注入)' };
|
|
87
|
+
const r = await _executor(c);
|
|
88
|
+
const fresh = await this.get(consentId);
|
|
89
|
+
if (fresh) {
|
|
90
|
+
fresh.status = r.ok ? 'executed' : 'failed';
|
|
91
|
+
fresh.result = r.ok ? r.result : r.error;
|
|
92
|
+
await this.persist();
|
|
93
|
+
}
|
|
94
|
+
return { ok: r.ok, error: r.ok ? undefined : r.error, consent: fresh || c };
|
|
95
|
+
}
|
|
96
|
+
async reject(consentId, opts = {}) {
|
|
97
|
+
await this.ensure();
|
|
98
|
+
const c = await this.get(consentId);
|
|
99
|
+
if (!c)
|
|
100
|
+
return { ok: false, error: 'consent_not_found' };
|
|
101
|
+
if (c.status !== 'pending')
|
|
102
|
+
return { ok: false, error: `授权 ${consentId} 状态 ${c.status}, 不可拒绝` };
|
|
103
|
+
c.status = 'rejected';
|
|
104
|
+
c.decidedAt = opts.now ?? Date.now();
|
|
105
|
+
c.decidedBy = opts.by || 'user';
|
|
106
|
+
c.result = opts.reason;
|
|
107
|
+
await this.persist();
|
|
108
|
+
return { ok: true, consent: c };
|
|
109
|
+
}
|
|
110
|
+
/** 过期的 pending 授权 → expired (不静默留在 pending) */
|
|
111
|
+
async expireStale(now = Date.now()) {
|
|
112
|
+
await this.ensure();
|
|
113
|
+
const out = [];
|
|
114
|
+
for (const c of this.items) {
|
|
115
|
+
if (c.status === 'pending' && c.expiresAt < now) {
|
|
116
|
+
c.status = 'expired';
|
|
117
|
+
out.push(c);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (out.length)
|
|
121
|
+
await this.persist();
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* contacts/grant-payload.ts — Grant 载荷的**单一规范** (2026-09-19)
|
|
3
|
+
*
|
|
4
|
+
* 为什么单独一个文件: 手机端 (WebCrypto) 与桌面端 (Node crypto) 必须对"同一条授权"算出**同一个字节串**,
|
|
5
|
+
* 否则签名永远验不过。这里只放纯函数 (无 node: 导入), 所以 browser bundle 与 Node 都能用。
|
|
6
|
+
*
|
|
7
|
+
* 铁律: 字段顺序固定 · 不含 signature 自身 · 不含 lastUsedAt (每次用都会变, 不该让签名失效)。
|
|
8
|
+
*/
|
|
9
|
+
/** 参与签名的字段 (顺序即规范顺序, 改动等于换协议版本) */
|
|
10
|
+
export const GRANT_SIGNED_FIELDS = [
|
|
11
|
+
'grantId', 'identityId', 'ownerDid', 'level', 'channels', 'contactScope', 'taskScope', 'contentScope',
|
|
12
|
+
'replyWakeAllowed', 'autoSend', 'sensitiveContentAllowed', 'newRecipientAllowed',
|
|
13
|
+
'grantedAt', 'grantedBy', 'grantedVia', 'deviceIds', 'status', 'revokedAt', 'grantVersion',
|
|
14
|
+
];
|
|
15
|
+
export function canonicalGrantPayload(g) {
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
grantId: g.grantId, identityId: g.identityId, ownerDid: g.ownerDid, level: g.level,
|
|
18
|
+
channels: g.channels, contactScope: g.contactScope, taskScope: g.taskScope, contentScope: g.contentScope,
|
|
19
|
+
replyWakeAllowed: g.replyWakeAllowed, autoSend: g.autoSend, sensitiveContentAllowed: g.sensitiveContentAllowed,
|
|
20
|
+
newRecipientAllowed: g.newRecipientAllowed, grantedAt: g.grantedAt, grantedBy: g.grantedBy,
|
|
21
|
+
grantedVia: g.grantedVia, deviceIds: [...g.deviceIds].sort(), status: g.status,
|
|
22
|
+
revokedAt: g.revokedAt || '', grantVersion: g.grantVersion,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function canonicalRevocationPayload(r) {
|
|
26
|
+
return JSON.stringify({ grantId: r.grantId, grantVersion: r.grantVersion, revokedAt: r.revokedAt, by: r.by });
|
|
27
|
+
}
|
|
28
|
+
/** 用户可见的三个选项 → 内部等级 + 默认范围 (手机端与 CLI 共用同一份, 避免两端范围不一致) */
|
|
29
|
+
export function presetForChoice(choice) {
|
|
30
|
+
const base = { channels: 'both', replyWakeAllowed: true, autoSend: true, newRecipientAllowed: false };
|
|
31
|
+
switch (choice) {
|
|
32
|
+
case 'task_once':
|
|
33
|
+
return { ...base, level: 'task_once', contactScope: 'verified_contacts', taskScope: 'current_goal', contentScope: 'normal', autoSend: false, sensitiveContentAllowed: false };
|
|
34
|
+
case 'persistent':
|
|
35
|
+
return { ...base, level: 'persistent', contactScope: 'verified_contacts', taskScope: 'all_future_goals', contentScope: 'normal', sensitiveContentAllowed: false };
|
|
36
|
+
case 'full_contact_access':
|
|
37
|
+
return { ...base, level: 'full_contact_access', contactScope: 'all_contacts', taskScope: 'all_future_goals', contentScope: 'sensitive', sensitiveContentAllowed: true };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export const GRANT_LEVEL_LABEL = {
|
|
41
|
+
none: '不允许使用',
|
|
42
|
+
task_once: '本次任务',
|
|
43
|
+
persistent: '长期自动使用',
|
|
44
|
+
full_contact_access: '完全授权联系方式能力',
|
|
45
|
+
};
|