oasis_test_v2 2.2.80 → 2.2.81
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/index.js +971 -74
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -218387,6 +218387,8 @@ var init_memory_control_plane_store = __esm({
|
|
|
218387
218387
|
// key = companyId\0type\0id
|
|
218388
218388
|
offboardings = /* @__PURE__ */ new Map();
|
|
218389
218389
|
// key = companyId\0operationId
|
|
218390
|
+
/** 存量身份映射审计(ADR D3.4):**只追加**的一条数组,插入序即写入序。 */
|
|
218391
|
+
roleMigrations = [];
|
|
218390
218392
|
// —— 公司 ——
|
|
218391
218393
|
async createCompany(c) {
|
|
218392
218394
|
if (this.companies.has(c.id)) throw new Error(`company already exists: ${c.id}`);
|
|
@@ -218418,8 +218420,25 @@ var init_memory_control_plane_store = __esm({
|
|
|
218418
218420
|
}
|
|
218419
218421
|
if (patch.status !== void 0) c.status = patch.status;
|
|
218420
218422
|
if (patch.onboardingVariant !== void 0) c.onboardingVariant = patch.onboardingVariant;
|
|
218423
|
+
if (patch.dissolvedAt !== void 0) {
|
|
218424
|
+
if (patch.dissolvedAt === null) delete c.dissolvedAt;
|
|
218425
|
+
else c.dissolvedAt = patch.dissolvedAt;
|
|
218426
|
+
}
|
|
218427
|
+
if (patch.dissolvedBy !== void 0) {
|
|
218428
|
+
if (patch.dissolvedBy === null) delete c.dissolvedBy;
|
|
218429
|
+
else c.dissolvedBy = patch.dissolvedBy;
|
|
218430
|
+
}
|
|
218421
218431
|
this.companies.set(id, c);
|
|
218422
218432
|
}
|
|
218433
|
+
async dissolveCompanyIfOwner(companyId, accountId, at2) {
|
|
218434
|
+
const c = this.companies.get(companyId);
|
|
218435
|
+
if (!c) return { ok: false, reason: "not_found" };
|
|
218436
|
+
const caller = this.members.get(memberKey(companyId, accountId));
|
|
218437
|
+
if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
|
|
218438
|
+
const next = { ...c, status: "suspended", dissolvedAt: at2, dissolvedBy: accountId };
|
|
218439
|
+
this.companies.set(companyId, next);
|
|
218440
|
+
return { ok: true, company: { ...next } };
|
|
218441
|
+
}
|
|
218423
218442
|
// —— 成员 ——
|
|
218424
218443
|
async upsertMember(m2) {
|
|
218425
218444
|
const key2 = memberKey(m2.companyId, m2.accountId);
|
|
@@ -218476,6 +218495,41 @@ var init_memory_control_plane_store = __esm({
|
|
|
218476
218495
|
this.members.set(key2, updated);
|
|
218477
218496
|
return { ok: true, member: { ...updated } };
|
|
218478
218497
|
}
|
|
218498
|
+
async setSoleCompanyOwner(companyId, toAccountId, opts) {
|
|
218499
|
+
const target = this.members.get(memberKey(companyId, toAccountId));
|
|
218500
|
+
if (!target) return { ok: false, reason: "not_found" };
|
|
218501
|
+
if (opts?.requireCurrentOwner !== void 0) {
|
|
218502
|
+
const caller = this.members.get(memberKey(companyId, opts.requireCurrentOwner));
|
|
218503
|
+
if (!caller || caller.role !== "owner") return { ok: false, reason: "not_owner" };
|
|
218504
|
+
}
|
|
218505
|
+
if (!target.onboardingCompletedAt && target.role !== "owner") {
|
|
218506
|
+
return { ok: false, reason: "target_not_listed" };
|
|
218507
|
+
}
|
|
218508
|
+
const acc = this.accounts.get(toAccountId);
|
|
218509
|
+
if (acc && acc.status !== "active") return { ok: false, reason: "target_inactive" };
|
|
218510
|
+
const others = [];
|
|
218511
|
+
for (const m2 of this.members.values()) {
|
|
218512
|
+
if (m2.companyId === companyId && m2.role === "owner" && m2.accountId !== toAccountId) others.push(m2);
|
|
218513
|
+
}
|
|
218514
|
+
if (others.length === 0 && target.role === "owner") {
|
|
218515
|
+
return { ok: true, owner: { ...target }, demoted: [] };
|
|
218516
|
+
}
|
|
218517
|
+
const demoted = others.map((m2) => {
|
|
218518
|
+
const next = { ...m2, role: "admin" };
|
|
218519
|
+
this.members.set(memberKey(companyId, m2.accountId), next);
|
|
218520
|
+
return { ...next };
|
|
218521
|
+
});
|
|
218522
|
+
const owner = { ...target, role: "owner" };
|
|
218523
|
+
this.members.set(memberKey(companyId, toAccountId), owner);
|
|
218524
|
+
return { ok: true, owner: { ...owner }, demoted };
|
|
218525
|
+
}
|
|
218526
|
+
// —— 存量身份映射审计(ADR D3.4):只追加 ——
|
|
218527
|
+
async appendMemberRoleMigrations(rows) {
|
|
218528
|
+
for (const r of rows) this.roleMigrations.push({ ...r });
|
|
218529
|
+
}
|
|
218530
|
+
async listMemberRoleMigrations(companyId) {
|
|
218531
|
+
return this.roleMigrations.filter((r) => companyId === void 0 || r.companyId === companyId).map((r) => ({ ...r }));
|
|
218532
|
+
}
|
|
218479
218533
|
// —— 邀请 ——
|
|
218480
218534
|
async createInvitation(inv) {
|
|
218481
218535
|
if (this.invitations.has(inv.id)) throw new Error(`invitation already exists: ${inv.id}`);
|
|
@@ -223753,9 +223807,13 @@ var init_control_plane_file_store = __esm({
|
|
|
223753
223807
|
MUTATORS2 = [
|
|
223754
223808
|
"createCompany",
|
|
223755
223809
|
"updateCompany",
|
|
223810
|
+
"dissolveCompanyIfOwner",
|
|
223756
223811
|
"upsertMember",
|
|
223757
223812
|
"removeMember",
|
|
223758
223813
|
"removeMemberIfNotLastOwner",
|
|
223814
|
+
"updateMemberRoleIfNotLastOwner",
|
|
223815
|
+
"setSoleCompanyOwner",
|
|
223816
|
+
"appendMemberRoleMigrations",
|
|
223759
223817
|
"createInvitation",
|
|
223760
223818
|
"updateInvitationStatus",
|
|
223761
223819
|
"updateInvitationStatusIf",
|
|
@@ -223807,6 +223865,7 @@ var init_control_plane_file_store = __esm({
|
|
|
223807
223865
|
for (const c of snap.agentCredentials ?? []) await MemoryControlPlaneStore.prototype.putAgentCredential.call(store, c);
|
|
223808
223866
|
for (const b2 of snap.principalBridges ?? []) await MemoryControlPlaneStore.prototype.putPrincipalBridge.call(store, b2);
|
|
223809
223867
|
for (const op of snap.offboardings ?? []) store.offboardings.set(`${op.companyId}\0${op.operationId}`, op);
|
|
223868
|
+
for (const r of snap.roleMigrations ?? []) store.roleMigrations.push({ ...r });
|
|
223810
223869
|
if (needsOnboardingBackfill) await store.save();
|
|
223811
223870
|
} else {
|
|
223812
223871
|
fs25.mkdirSync(path24.dirname(file), { recursive: true });
|
|
@@ -223844,7 +223903,8 @@ var init_control_plane_file_store = __esm({
|
|
|
223844
223903
|
agentCredentials,
|
|
223845
223904
|
principalBridges,
|
|
223846
223905
|
// 交接操作无按公司列举的读面(按 operationId / accountId 定位),直接读 protected map。
|
|
223847
|
-
offboardings: [...this.offboardings.values()]
|
|
223906
|
+
offboardings: [...this.offboardings.values()],
|
|
223907
|
+
roleMigrations: await this.listMemberRoleMigrations()
|
|
223848
223908
|
};
|
|
223849
223909
|
fs25.writeFileSync(this.file, JSON.stringify(snap, null, 2));
|
|
223850
223910
|
}
|
|
@@ -225071,6 +225131,197 @@ var init_org_model_backfill = __esm({
|
|
|
225071
225131
|
}
|
|
225072
225132
|
});
|
|
225073
225133
|
|
|
225134
|
+
// ../server/src/org-identity-migration.ts
|
|
225135
|
+
function e1Candidate(members) {
|
|
225136
|
+
if (members.length === 0) return void 0;
|
|
225137
|
+
if (members.some((m2) => !m2.createdAt)) return void 0;
|
|
225138
|
+
return [...members].sort((a, b2) => {
|
|
225139
|
+
const ta = a.createdAt;
|
|
225140
|
+
const tb = b2.createdAt;
|
|
225141
|
+
if (ta !== tb) return ta < tb ? -1 : 1;
|
|
225142
|
+
return a.accountId < b2.accountId ? -1 : a.accountId > b2.accountId ? 1 : 0;
|
|
225143
|
+
})[0];
|
|
225144
|
+
}
|
|
225145
|
+
async function inventoryOrgIdentity(deps, opts = {}) {
|
|
225146
|
+
const listMembers = deps.listMembersWithCreatedAt ?? ((cid) => deps.store.listMembers(cid));
|
|
225147
|
+
const companies = (await deps.store.listCompanies()).filter((c) => opts.companyId === void 0 || c.id === opts.companyId);
|
|
225148
|
+
const rows = [];
|
|
225149
|
+
for (const c of companies) {
|
|
225150
|
+
const members = await listMembers(c.id);
|
|
225151
|
+
rows.push(decide(c, members, opts.manifest));
|
|
225152
|
+
}
|
|
225153
|
+
return {
|
|
225154
|
+
rows,
|
|
225155
|
+
blocked: rows.filter((r) => r.decision.startsWith("blocked")).length,
|
|
225156
|
+
pending: rows.filter((r) => (r.decision === "remap" || r.decision === "manifest") && r.changes.length > 0).length
|
|
225157
|
+
};
|
|
225158
|
+
}
|
|
225159
|
+
function decide(company, members, manifest) {
|
|
225160
|
+
const owners = members.filter((m2) => m2.role === "owner");
|
|
225161
|
+
const base = {
|
|
225162
|
+
companyId: company.id,
|
|
225163
|
+
companyName: company.name,
|
|
225164
|
+
status: company.status,
|
|
225165
|
+
members: members.length,
|
|
225166
|
+
owners: owners.length,
|
|
225167
|
+
admins: members.filter((m2) => m2.role === "admin").length,
|
|
225168
|
+
plainMembers: members.filter((m2) => m2.role === "member").length,
|
|
225169
|
+
rowsWithoutCreatedAt: members.filter((m2) => !m2.createdAt).length,
|
|
225170
|
+
changes: []
|
|
225171
|
+
};
|
|
225172
|
+
if (members.length === 0) {
|
|
225173
|
+
return { ...base, decision: "blocked:empty", reason: "\u7EC4\u7EC7\u6CA1\u6709\u4EFB\u4F55\u6210\u5458\u884C\u2014\u2014\u5220\u8FD8\u662F\u7559\u7531\u4EBA\u51B3\u5B9A\uFF0C\u811A\u672C\u4E0D\u78B0" };
|
|
225174
|
+
}
|
|
225175
|
+
const pinned = manifest?.owners[company.id];
|
|
225176
|
+
if (pinned !== void 0) {
|
|
225177
|
+
if (!members.some((m2) => m2.accountId === pinned)) {
|
|
225178
|
+
return {
|
|
225179
|
+
...base,
|
|
225180
|
+
decision: "blocked:no-owner",
|
|
225181
|
+
reason: `manifest \u6307\u5B9A\u7684 ${pinned} \u4E0D\u662F\u8BE5\u7EC4\u7EC7\u73B0\u5B58\u6210\u5458\u884C\u2014\u2014\u6307\u9519\u4EBA\u6BD4\u4E0D\u6307\u66F4\u7CDF\uFF0C\u62D2\u7EDD\u6267\u884C`
|
|
225182
|
+
};
|
|
225183
|
+
}
|
|
225184
|
+
return {
|
|
225185
|
+
...base,
|
|
225186
|
+
decision: "manifest",
|
|
225187
|
+
targetOwnerAccountId: pinned,
|
|
225188
|
+
changes: plannedChanges(members, pinned),
|
|
225189
|
+
reason: `\u6309\u51BB\u7ED3 manifest\uFF08${manifest.manifestVersion}\uFF09\u6307\u5B9A ${pinned} \u4E3A\u7EC4\u7EC7\u7BA1\u7406\u5458`
|
|
225190
|
+
};
|
|
225191
|
+
}
|
|
225192
|
+
const e1 = e1Candidate(members);
|
|
225193
|
+
if (!e1) {
|
|
225194
|
+
return {
|
|
225195
|
+
...base,
|
|
225196
|
+
decision: "blocked:no-created-at",
|
|
225197
|
+
reason: `\u6709 ${base.rowsWithoutCreatedAt} \u884C\u6210\u5458\u6CA1\u6709 created_at\uFF08\u8BE5\u5217\u662F expand \u671F\u624D\u52A0\u7684\uFF09\uFF0CE1 \u4E0D\u6210\u7ACB \u2192 \u5199\u8FDB manifest \u7531\u4EBA\u6307\u5B9A\u521B\u5EFA\u8005`
|
|
225198
|
+
};
|
|
225199
|
+
}
|
|
225200
|
+
if (e1.role === "member") {
|
|
225201
|
+
return {
|
|
225202
|
+
...base,
|
|
225203
|
+
decision: "blocked:e1-is-plain-member",
|
|
225204
|
+
e1AccountId: e1.accountId,
|
|
225205
|
+
reason: `\u6700\u65E9\u52A0\u5165\u7684 ${e1.accountId} \u5F53\u524D\u662F\u666E\u901A\u6210\u5458\uFF08\u53EF\u80FD\u88AB\u964D\u7EA7\u8FC7\uFF0C\u6216\u6570\u636E\u6765\u81EA\u8FC1\u79FB\u6307\u6D3E\uFF09\u2192 \u7531\u4EBA\u6307\u5B9A`
|
|
225206
|
+
};
|
|
225207
|
+
}
|
|
225208
|
+
if (owners.length === 0) {
|
|
225209
|
+
return {
|
|
225210
|
+
...base,
|
|
225211
|
+
decision: "blocked:no-owner",
|
|
225212
|
+
e1AccountId: e1.accountId,
|
|
225213
|
+
reason: "\u4E00\u4E2A owner \u90FD\u6CA1\u6709 \u2192 \u7531\u4EBA\u6307\u5B9A\uFF08\u8C01\u5F53\u552F\u4E00\u6301\u6709\u4EBA\u662F\u4E0D\u53EF\u9006\u7684\u4EBA\u4E8B\u51B3\u5B9A\uFF09"
|
|
225214
|
+
};
|
|
225215
|
+
}
|
|
225216
|
+
const changes = plannedChanges(members, e1.accountId);
|
|
225217
|
+
if (changes.length === 0) {
|
|
225218
|
+
return {
|
|
225219
|
+
...base,
|
|
225220
|
+
decision: "ok",
|
|
225221
|
+
e1AccountId: e1.accountId,
|
|
225222
|
+
targetOwnerAccountId: e1.accountId,
|
|
225223
|
+
reason: "\u5DF2\u7ECF\u6070\u6709\u4E00\u4F4D\u7EC4\u7EC7\u7BA1\u7406\u5458\u3001\u4E14\u5C31\u662F\u6700\u65E9\u52A0\u5165\u7684\u90A3\u4F4D\u2014\u2014\u65E0\u9700\u6539\u52A8"
|
|
225224
|
+
};
|
|
225225
|
+
}
|
|
225226
|
+
return {
|
|
225227
|
+
...base,
|
|
225228
|
+
decision: "remap",
|
|
225229
|
+
e1AccountId: e1.accountId,
|
|
225230
|
+
targetOwnerAccountId: e1.accountId,
|
|
225231
|
+
changes,
|
|
225232
|
+
reason: `E1\uFF1A\u6700\u65E9\u52A0\u5165\u7684 ${e1.accountId}\uFF08\u5F53\u524D ${e1.role}\uFF09\u2192 \u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u5176\u4F59\u539F owner \u964D\u4E3A\u7BA1\u7406\u5458`
|
|
225233
|
+
};
|
|
225234
|
+
}
|
|
225235
|
+
function plannedChanges(members, targetOwner) {
|
|
225236
|
+
const out = [];
|
|
225237
|
+
for (const m2 of members) {
|
|
225238
|
+
if (m2.accountId === targetOwner) {
|
|
225239
|
+
if (m2.role !== "owner") out.push({ accountId: m2.accountId, roleBefore: m2.role, roleAfter: "owner" });
|
|
225240
|
+
continue;
|
|
225241
|
+
}
|
|
225242
|
+
if (m2.role === "owner") out.push({ accountId: m2.accountId, roleBefore: "owner", roleAfter: "admin" });
|
|
225243
|
+
}
|
|
225244
|
+
return out;
|
|
225245
|
+
}
|
|
225246
|
+
async function backfillOrgIdentity(deps, opts = {}) {
|
|
225247
|
+
const log3 = opts.log ?? (() => {
|
|
225248
|
+
});
|
|
225249
|
+
const now = opts.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
|
|
225250
|
+
const inv = await inventoryOrgIdentity(deps, opts);
|
|
225251
|
+
let applied = 0;
|
|
225252
|
+
let auditRows = 0;
|
|
225253
|
+
for (const row of inv.rows) {
|
|
225254
|
+
if (row.decision !== "remap" && row.decision !== "manifest") {
|
|
225255
|
+
if (row.decision.startsWith("blocked")) log3(` \u26D4 ${row.companyId} ${row.decision}\uFF1A${row.reason}`);
|
|
225256
|
+
continue;
|
|
225257
|
+
}
|
|
225258
|
+
if (row.changes.length === 0) continue;
|
|
225259
|
+
const target = row.targetOwnerAccountId;
|
|
225260
|
+
const desc = row.changes.map((c) => `${c.accountId} ${c.roleBefore}\u2192${c.roleAfter}`).join("\uFF0C");
|
|
225261
|
+
log3(` ${row.companyId}\uFF1A${desc}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
|
|
225262
|
+
if (!opts.apply) continue;
|
|
225263
|
+
const res = await deps.store.setSoleCompanyOwner(row.companyId, target);
|
|
225264
|
+
if (!res.ok) {
|
|
225265
|
+
throw new Error(
|
|
225266
|
+
`${row.companyId} \u6536\u655B\u5230 ${target} \u5931\u8D25\uFF1A${res.reason}\uFF08target_not_listed = \u90A3\u4E2A\u4EBA\u8FD8\u6CA1\u8D70\u5B8C\u65B0\u624B\u6307\u5F15\uFF1Btarget_inactive = \u8D26\u53F7\u5DF2\u505C\u7528\uFF09`
|
|
225267
|
+
);
|
|
225268
|
+
}
|
|
225269
|
+
applied += 1;
|
|
225270
|
+
const at2 = now();
|
|
225271
|
+
const evidence = row.decision === "manifest" ? "manifest" : "E1";
|
|
225272
|
+
const rows = res.demoted.map((m2) => ({
|
|
225273
|
+
companyId: row.companyId,
|
|
225274
|
+
accountId: m2.accountId,
|
|
225275
|
+
roleBefore: "owner",
|
|
225276
|
+
roleAfter: "admin",
|
|
225277
|
+
evidence,
|
|
225278
|
+
...opts.manifest?.manifestVersion !== void 0 && evidence === "manifest" ? { manifestVersion: opts.manifest.manifestVersion } : {},
|
|
225279
|
+
appliedAt: at2
|
|
225280
|
+
}));
|
|
225281
|
+
const promoted = row.changes.find((c) => c.accountId === target);
|
|
225282
|
+
if (promoted) {
|
|
225283
|
+
rows.push({
|
|
225284
|
+
companyId: row.companyId,
|
|
225285
|
+
accountId: target,
|
|
225286
|
+
roleBefore: promoted.roleBefore,
|
|
225287
|
+
roleAfter: "owner",
|
|
225288
|
+
evidence,
|
|
225289
|
+
...opts.manifest?.manifestVersion !== void 0 && evidence === "manifest" ? { manifestVersion: opts.manifest.manifestVersion } : {},
|
|
225290
|
+
appliedAt: at2
|
|
225291
|
+
});
|
|
225292
|
+
}
|
|
225293
|
+
await deps.store.appendMemberRoleMigrations(rows);
|
|
225294
|
+
auditRows += rows.length;
|
|
225295
|
+
}
|
|
225296
|
+
return { ...inv, applied, auditRows };
|
|
225297
|
+
}
|
|
225298
|
+
async function rollbackOrgIdentity(deps, opts) {
|
|
225299
|
+
const log3 = opts.log ?? (() => {
|
|
225300
|
+
});
|
|
225301
|
+
const audit2 = await deps.store.listMemberRoleMigrations(opts.companyId);
|
|
225302
|
+
const plan = [];
|
|
225303
|
+
let reverted = 0;
|
|
225304
|
+
for (const r of [...audit2].reverse()) {
|
|
225305
|
+
const member = await deps.store.getMember(r.companyId, r.accountId);
|
|
225306
|
+
if (!member) {
|
|
225307
|
+
log3(` \u8DF3\u8FC7 ${r.accountId}\uFF1A\u6210\u5458\u884C\u5DF2\u4E0D\u5B58\u5728\uFF08\u88AB\u79FB\u9664\u8FC7\uFF09`);
|
|
225308
|
+
continue;
|
|
225309
|
+
}
|
|
225310
|
+
if (member.role === r.roleBefore) continue;
|
|
225311
|
+
plan.push({ companyId: r.companyId, accountId: r.accountId, from: member.role, to: r.roleBefore });
|
|
225312
|
+
log3(` ${r.companyId} ${r.accountId} ${member.role}\u2192${r.roleBefore}${opts.apply ? "" : "\uFF08dry-run\uFF09"}`);
|
|
225313
|
+
if (!opts.apply) continue;
|
|
225314
|
+
await deps.store.upsertMember({ ...member, role: r.roleBefore });
|
|
225315
|
+
reverted += 1;
|
|
225316
|
+
}
|
|
225317
|
+
return { plan, reverted };
|
|
225318
|
+
}
|
|
225319
|
+
var init_org_identity_migration = __esm({
|
|
225320
|
+
"../server/src/org-identity-migration.ts"() {
|
|
225321
|
+
"use strict";
|
|
225322
|
+
}
|
|
225323
|
+
});
|
|
225324
|
+
|
|
225074
225325
|
// ../server/src/config-audit.ts
|
|
225075
225326
|
function shallowDiff(before, after) {
|
|
225076
225327
|
const out = {};
|
|
@@ -232382,6 +232633,68 @@ var init_runtime_tenancy = __esm({
|
|
|
232382
232633
|
}
|
|
232383
232634
|
});
|
|
232384
232635
|
|
|
232636
|
+
// ../server/src/governance/agent-manage-guard.ts
|
|
232637
|
+
function isCompanyManager(role) {
|
|
232638
|
+
return role === "owner" || role === "admin";
|
|
232639
|
+
}
|
|
232640
|
+
function isAssistantAgent(subject) {
|
|
232641
|
+
return subject.positions.includes(ASSISTANT_POSITION);
|
|
232642
|
+
}
|
|
232643
|
+
function mayManageAgent(caller, subject) {
|
|
232644
|
+
if (!isAssistantAgent(subject)) return isCompanyManager(caller.role);
|
|
232645
|
+
if (subject.boundHumanActorId === void 0) return isCompanyManager(caller.role);
|
|
232646
|
+
if (subject.boundHumanActorId === caller.actorId) return true;
|
|
232647
|
+
if (subject.boundHumanReportsTo !== void 0 && subject.boundHumanReportsTo === caller.actorId) return true;
|
|
232648
|
+
return isCompanyManager(caller.role);
|
|
232649
|
+
}
|
|
232650
|
+
async function assertMayManageAgent(guard, ctx, agent) {
|
|
232651
|
+
if (!guard) return;
|
|
232652
|
+
const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
|
|
232653
|
+
const subject = { positions: agent.positions };
|
|
232654
|
+
if (isAssistantAgent(subject)) {
|
|
232655
|
+
const human = await guard.boundHuman(ctx.companyId, agent.id);
|
|
232656
|
+
if (human) {
|
|
232657
|
+
subject.boundHumanActorId = human;
|
|
232658
|
+
subject.boundHumanReportsTo = await guard.reportsTo(ctx.companyId, human) ?? void 0;
|
|
232659
|
+
}
|
|
232660
|
+
}
|
|
232661
|
+
if (mayManageAgent({ role, actorId: ctx.callerActorId }, subject)) return;
|
|
232662
|
+
throw new AgentManageForbiddenError(
|
|
232663
|
+
isAssistantAgent(subject) ? "\u53EA\u6709\u672C\u4EBA\u3001\u5176\u76F4\u63A5\u4E0A\u7EA7\u6216\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u8FD9\u4E2A\u52A9\u7406" : "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5\u7BA1\u7406\u4E13\u5BB6 Agent"
|
|
232664
|
+
);
|
|
232665
|
+
}
|
|
232666
|
+
async function assertCompanyManager(guard, ctx, what) {
|
|
232667
|
+
if (!guard) return;
|
|
232668
|
+
const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
|
|
232669
|
+
if (isCompanyManager(role)) return;
|
|
232670
|
+
throw new AgentManageForbiddenError(`\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u53EF\u4EE5${what}`);
|
|
232671
|
+
}
|
|
232672
|
+
async function assertMayCreateAgent(guard, ctx) {
|
|
232673
|
+
return assertCompanyManager(guard, ctx, "\u65B0\u5EFA Agent");
|
|
232674
|
+
}
|
|
232675
|
+
async function assertMaySelfOrManage(guard, ctx, targetActorId) {
|
|
232676
|
+
if (!guard) return;
|
|
232677
|
+
if (ctx.callerActorId === targetActorId) return;
|
|
232678
|
+
const role = await guard.callerRole(ctx.companyId, ctx.callerActorId);
|
|
232679
|
+
if (isCompanyManager(role)) return;
|
|
232680
|
+
throw new AgentManageForbiddenError("\u53EA\u80FD\u4FEE\u6539\u81EA\u5DF1\u7684\u6863\u6848\uFF1B\u6539\u4ED6\u4EBA\u7684\u9700\u8981\u7EC4\u7EC7\u7BA1\u7406\u5458 / \u7BA1\u7406\u5458\u6743\u9650");
|
|
232681
|
+
}
|
|
232682
|
+
var ASSISTANT_POSITION, AgentManageForbiddenError;
|
|
232683
|
+
var init_agent_manage_guard = __esm({
|
|
232684
|
+
"../server/src/governance/agent-manage-guard.ts"() {
|
|
232685
|
+
"use strict";
|
|
232686
|
+
ASSISTANT_POSITION = "assistant";
|
|
232687
|
+
AgentManageForbiddenError = class extends Error {
|
|
232688
|
+
status = 403;
|
|
232689
|
+
code = "FORBIDDEN";
|
|
232690
|
+
constructor(message) {
|
|
232691
|
+
super(message);
|
|
232692
|
+
this.name = "AgentManageForbiddenError";
|
|
232693
|
+
}
|
|
232694
|
+
};
|
|
232695
|
+
}
|
|
232696
|
+
});
|
|
232697
|
+
|
|
232385
232698
|
// ../server/src/domains/actors/routes.ts
|
|
232386
232699
|
async function asApiError(run) {
|
|
232387
232700
|
try {
|
|
@@ -232470,6 +232783,27 @@ function mapMarketError(err) {
|
|
|
232470
232783
|
}
|
|
232471
232784
|
function actorsDomain(opts) {
|
|
232472
232785
|
const { resolveCtx } = opts;
|
|
232786
|
+
const asForbidden = async (run) => {
|
|
232787
|
+
try {
|
|
232788
|
+
await run();
|
|
232789
|
+
} catch (e) {
|
|
232790
|
+
if (e instanceof AgentManageForbiddenError) throw new ApiError(e.status, e.code, e.message);
|
|
232791
|
+
throw e;
|
|
232792
|
+
}
|
|
232793
|
+
};
|
|
232794
|
+
const guardAgentWrite = async (req, actor) => {
|
|
232795
|
+
const ctx = { companyId: req.auth.companyId, callerActorId: req.auth.actor };
|
|
232796
|
+
if (actor.kind !== "agent") {
|
|
232797
|
+
await asForbidden(() => assertMaySelfOrManage(opts.agentManageGuard, ctx, actor.id));
|
|
232798
|
+
return;
|
|
232799
|
+
}
|
|
232800
|
+
await asForbidden(() => assertMayManageAgent(opts.agentManageGuard, ctx, { id: actor.id, positions: actor.roles }));
|
|
232801
|
+
};
|
|
232802
|
+
const guardExistingActorWrite = async (req, service, actorId) => {
|
|
232803
|
+
const existing = await service.getActor(actorId);
|
|
232804
|
+
if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${actorId}`);
|
|
232805
|
+
await guardAgentWrite(req, existing);
|
|
232806
|
+
};
|
|
232473
232807
|
const revealLimits = opts.credentialRevealLimits ?? { perSession: 200, perSessionPerKey: 20 };
|
|
232474
232808
|
const revealLimiter = new RevealRateLimiter(revealLimits.perSession, revealLimits.perSessionPerKey);
|
|
232475
232809
|
const consoleRevealLimits = opts.credentialRevealLimits?.console ?? { perSession: 60, perSessionPerKey: 10 };
|
|
@@ -232522,6 +232856,10 @@ function actorsDomain(opts) {
|
|
|
232522
232856
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232523
232857
|
const b2 = req.body;
|
|
232524
232858
|
if (!b2?.id || !b2.kind || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / kind / name");
|
|
232859
|
+
await asForbidden(() => assertMayCreateAgent(opts.agentManageGuard, {
|
|
232860
|
+
companyId: req.auth.companyId,
|
|
232861
|
+
callerActorId: req.auth.actor
|
|
232862
|
+
}));
|
|
232525
232863
|
const { id, kind, name } = b2;
|
|
232526
232864
|
const positions = positionsFromPayload(b2);
|
|
232527
232865
|
const avatar = avatarRefFromPayload(b2);
|
|
@@ -232548,6 +232886,7 @@ function actorsDomain(opts) {
|
|
|
232548
232886
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232549
232887
|
const existing = await service.getActor(req.params.id);
|
|
232550
232888
|
if (!existing) throw new ApiError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5\uFF1A${req.params.id}`);
|
|
232889
|
+
await guardAgentWrite(req, existing);
|
|
232551
232890
|
const b2 = req.body ?? {};
|
|
232552
232891
|
const positions = positionsFromPayload(b2);
|
|
232553
232892
|
const avatar = avatarRefFromPayload(b2);
|
|
@@ -232582,23 +232921,27 @@ function actorsDomain(opts) {
|
|
|
232582
232921
|
});
|
|
232583
232922
|
router.delete("/api/actors/:id", async (req) => {
|
|
232584
232923
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232924
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232585
232925
|
await service.disableActor(req.params.id, req.auth.actor);
|
|
232586
232926
|
return { status: 200, body: { ok: true } };
|
|
232587
232927
|
});
|
|
232588
232928
|
router.post("/api/actors/:id/avatar", async (req) => {
|
|
232589
232929
|
const { service, blobs } = await resolveCtx(req.auth.companyId);
|
|
232590
232930
|
if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
|
|
232931
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232591
232932
|
const ref2 = await storeImageBlob(blobs, req.body?.data);
|
|
232592
232933
|
const actor = await service.setActorAvatar(req.params.id, ref2, req.auth.actor);
|
|
232593
232934
|
return { status: 201, body: { avatar: actor.avatar } };
|
|
232594
232935
|
});
|
|
232595
232936
|
router.delete("/api/actors/:id/avatar", async (req) => {
|
|
232596
232937
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232938
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232597
232939
|
await service.setActorAvatar(req.params.id, null, req.auth.actor);
|
|
232598
232940
|
return { status: 200, body: { ok: true } };
|
|
232599
232941
|
});
|
|
232600
232942
|
router.post("/api/teams/:id/icon", async (req) => {
|
|
232601
232943
|
const { service, blobs } = await resolveCtx(req.auth.companyId);
|
|
232944
|
+
await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
|
|
232602
232945
|
if (!blobs) throw new ApiError(501, "NOT_IMPLEMENTED", "blob \u5B58\u50A8\u672A\u63A5\u5165");
|
|
232603
232946
|
const ref2 = await storeImageBlob(blobs, req.body?.data);
|
|
232604
232947
|
await service.setTeamIcon(req.params.id, ref2);
|
|
@@ -232606,6 +232949,7 @@ function actorsDomain(opts) {
|
|
|
232606
232949
|
});
|
|
232607
232950
|
router.delete("/api/teams/:id/icon", async (req) => {
|
|
232608
232951
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232952
|
+
await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u6539\u56E2\u961F\u56FE\u6807"));
|
|
232609
232953
|
await service.setTeamIcon(req.params.id, null);
|
|
232610
232954
|
return { status: 200, body: { ok: true } };
|
|
232611
232955
|
});
|
|
@@ -232617,6 +232961,7 @@ function actorsDomain(opts) {
|
|
|
232617
232961
|
});
|
|
232618
232962
|
router.post("/api/actors/:id/config", async (req) => {
|
|
232619
232963
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232964
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232620
232965
|
const b2 = req.body ?? {};
|
|
232621
232966
|
const checkString = (key2) => {
|
|
232622
232967
|
if (b2[key2] !== void 0 && typeof b2[key2] !== "string") {
|
|
@@ -232678,6 +233023,7 @@ function actorsDomain(opts) {
|
|
|
232678
233023
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232679
233024
|
const actorId = req.params.id;
|
|
232680
233025
|
if (!await service.getActor(actorId)) throw new ApiError(404, "NOT_FOUND", "\u6CA1\u6709\u8FD9\u4E2A\u5458\u5DE5");
|
|
233026
|
+
await guardExistingActorWrite(req, service, actorId);
|
|
232681
233027
|
const raw = req.body?.candidates;
|
|
232682
233028
|
if (!Array.isArray(raw)) throw new ApiError(400, "BAD_REQUEST", "candidates \u5FC5\u987B\u662F\u6570\u7EC4");
|
|
232683
233029
|
if (raw.length > 20) throw new ApiError(400, "BAD_REQUEST", "\u5019\u9009\u8FD0\u884C\u65F6\u6700\u591A 20 \u6761");
|
|
@@ -232712,6 +233058,7 @@ function actorsDomain(opts) {
|
|
|
232712
233058
|
});
|
|
232713
233059
|
router.post("/api/actors/:id/config/rollback", async (req) => {
|
|
232714
233060
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233061
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232715
233062
|
const b2 = req.body;
|
|
232716
233063
|
if (typeof b2?.version !== "number") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 version");
|
|
232717
233064
|
const cfg = await service.rollbackConfig(req.params.id, b2.version, req.auth.actor);
|
|
@@ -232723,6 +233070,7 @@ function actorsDomain(opts) {
|
|
|
232723
233070
|
});
|
|
232724
233071
|
router.patch("/api/actors/:id/settings", async (req) => {
|
|
232725
233072
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233073
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232726
233074
|
const b2 = req.body ?? {};
|
|
232727
233075
|
if (b2.memoryEnabled !== void 0 && typeof b2.memoryEnabled !== "boolean") {
|
|
232728
233076
|
throw new ApiError(400, "BAD_REQUEST", "memoryEnabled \u5FC5\u987B\u662F boolean");
|
|
@@ -232742,6 +233090,7 @@ function actorsDomain(opts) {
|
|
|
232742
233090
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232743
233091
|
const b2 = req.body;
|
|
232744
233092
|
if (!b2?.id || !b2.name) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 id / name");
|
|
233093
|
+
await asForbidden(() => assertCompanyManager(opts.agentManageGuard, { companyId: req.auth.companyId, callerActorId: req.auth.actor }, "\u65B0\u5EFA\u56E2\u961F"));
|
|
232745
233094
|
await service.upsertTeam({
|
|
232746
233095
|
id: b2.id,
|
|
232747
233096
|
name: b2.name,
|
|
@@ -232762,6 +233111,7 @@ function actorsDomain(opts) {
|
|
|
232762
233111
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
232763
233112
|
const b2 = req.body;
|
|
232764
233113
|
if (!b2?.actorId || !b2.nodeId || !b2.runtimeKind) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 actorId / nodeId / runtimeKind");
|
|
233114
|
+
await guardExistingActorWrite(req, service, b2.actorId);
|
|
232765
233115
|
const targetNode = await opts.resolveNodeTenancy?.(b2.nodeId).catch(() => null) ?? null;
|
|
232766
233116
|
const foreign = foreignNodeBlock(targetNode, req.auth.companyId);
|
|
232767
233117
|
if (foreign) throw new ApiError(400, "NODE_NOT_IN_COMPANY", foreign);
|
|
@@ -232770,6 +233120,7 @@ function actorsDomain(opts) {
|
|
|
232770
233120
|
});
|
|
232771
233121
|
router.delete("/api/bindings/:actorId", async (req) => {
|
|
232772
233122
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233123
|
+
await guardExistingActorWrite(req, service, req.params.actorId);
|
|
232773
233124
|
await service.unbind(req.params.actorId, req.auth.actor);
|
|
232774
233125
|
return { status: 200, body: { ok: true } };
|
|
232775
233126
|
});
|
|
@@ -232805,6 +233156,7 @@ function actorsDomain(opts) {
|
|
|
232805
233156
|
});
|
|
232806
233157
|
router.put("/api/actors/:id/connectors/:connectorId", async (req) => {
|
|
232807
233158
|
const { service } = await resolveCtx(req.auth.companyId);
|
|
233159
|
+
await guardExistingActorWrite(req, service, req.params.id);
|
|
232808
233160
|
const b2 = req.body;
|
|
232809
233161
|
if (typeof b2?.enabled !== "boolean") throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 enabled (boolean)");
|
|
232810
233162
|
const conn = b2.enabled ? await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, true, req.auth.actor) : await service.setActorConnectorEnabled(req.params.id, req.params.connectorId, false);
|
|
@@ -233973,6 +234325,7 @@ var init_routes3 = __esm({
|
|
|
233973
234325
|
init_skill_materializer();
|
|
233974
234326
|
init_connector_skills();
|
|
233975
234327
|
init_runtime_tenancy();
|
|
234328
|
+
init_agent_manage_guard();
|
|
233976
234329
|
CREDENTIAL_REVEAL_SOURCES = /* @__PURE__ */ new Set(["agent-cli", "agent-exec", "smoke-test", "oasis-internal", "console"]);
|
|
233977
234330
|
RevealRateLimiter = class {
|
|
233978
234331
|
constructor(perSession, perSessionPerKey, ttlMs = 2 * 60 * 6e4) {
|
|
@@ -234304,7 +234657,7 @@ function createActorsDomain(opts) {
|
|
|
234304
234657
|
return {
|
|
234305
234658
|
service: defaultCtx.service,
|
|
234306
234659
|
resolveCtx,
|
|
234307
|
-
register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.companyMemberRole ? { companyMemberRole: opts.companyMemberRole } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {} })
|
|
234660
|
+
register: actorsDomain({ resolveCtx, ...opts.resolveTrace ? { resolveTrace: opts.resolveTrace } : {}, ...opts.trace ? { trace: opts.trace } : {}, ...opts.listBuiltinSkills ? { listBuiltinSkills: opts.listBuiltinSkills } : {}, ...opts.getBuiltinSkills ? { getBuiltinSkills: opts.getBuiltinSkills } : {}, ...opts.getConnectorSkills ? { getConnectorSkills: opts.getConnectorSkills } : {}, ...opts.refreshConnectorSkills ? { refreshConnectorSkills: opts.refreshConnectorSkills } : {}, ...opts.memory ? { memory: opts.memory } : {}, ...opts.resolveDispatchScope ? { resolveDispatchScope: opts.resolveDispatchScope } : {}, ...opts.projectExists ? { projectExists: opts.projectExists } : {}, ...opts.companyMemberRole ? { companyMemberRole: opts.companyMemberRole } : {}, ...opts.credentialAudit ? { credentialAudit: opts.credentialAudit } : {}, ...opts.readCredentialAudit ? { readCredentialAudit: opts.readCredentialAudit } : {}, ...opts.credentialRevealLimits ? { credentialRevealLimits: opts.credentialRevealLimits } : {}, ...opts.skillMarket ? { skillMarket: opts.skillMarket } : {}, ...opts.resolveNodeTenancy ? { resolveNodeTenancy: opts.resolveNodeTenancy } : {}, ...opts.agentManageGuard ? { agentManageGuard: opts.agentManageGuard } : {} })
|
|
234308
234661
|
};
|
|
234309
234662
|
}
|
|
234310
234663
|
var import_node_crypto55;
|
|
@@ -241577,11 +241930,34 @@ var init_service6 = __esm({
|
|
|
241577
241930
|
async isEmployedElsewhere(companyId, accountId) {
|
|
241578
241931
|
return isEmployedElsewhere(this.store, companyId, accountId);
|
|
241579
241932
|
}
|
|
241580
|
-
/**
|
|
241933
|
+
/**
|
|
241934
|
+
* 当前用户所属的公司(按 membership 聚合)。**只返回 `status='active'` 的**。
|
|
241935
|
+
*
|
|
241936
|
+
* 🚨 过滤 status 是「解散组织」能真正生效的一半(组织三层身份 ADR D9.2):解散取的是可审计的
|
|
241937
|
+
* 停用语义(`status='suspended'` + `dissolved_at/by`),成员行**保留**。不在这里过滤的话,
|
|
241938
|
+
* 被解散的组织仍然会出现在顶栏切换器上、`CompanyGate` 也仍把这个人算作「有组织」——
|
|
241939
|
+
* 解散就只是换了个状态字,人照进不误。口径与 `resolveCurrentCompany` 的无 selector 分支对齐
|
|
241940
|
+
* (那一支本来就只挑 active)。某人全部组织都被解散后,按既有逻辑落到建团队页。
|
|
241941
|
+
*/
|
|
241581
241942
|
async listForUser(accountId) {
|
|
241582
241943
|
const memberships = await this.store.listMembershipsForAccount(accountId);
|
|
241583
241944
|
const fetched = await Promise.all(memberships.map((m2) => this.store.getCompany(m2.companyId)));
|
|
241584
|
-
return fetched.filter((c) => c !== null && c !== void 0);
|
|
241945
|
+
return fetched.filter((c) => c !== null && c !== void 0 && c.status === "active");
|
|
241946
|
+
}
|
|
241947
|
+
/**
|
|
241948
|
+
* 「我在哪些组织里还是组织管理员」——注销账号的跨组织检查(ADR D9.3)。
|
|
241949
|
+
* 只看 `status='active'` 的组织:已解散的那家里是不是 owner 已经无关紧要。
|
|
241950
|
+
* **不能只看当前组织**:注销是账号级动作,漏一家就等于把那家组织甩成无人可管。
|
|
241951
|
+
*/
|
|
241952
|
+
async listOwnedActiveCompanies(accountId) {
|
|
241953
|
+
const memberships = await this.store.listMembershipsForAccount(accountId);
|
|
241954
|
+
const out = [];
|
|
241955
|
+
for (const m2 of memberships) {
|
|
241956
|
+
if (m2.role !== "owner") continue;
|
|
241957
|
+
const c = await this.store.getCompany(m2.companyId);
|
|
241958
|
+
if (c?.status === "active") out.push(c);
|
|
241959
|
+
}
|
|
241960
|
+
return out;
|
|
241585
241961
|
}
|
|
241586
241962
|
async get(id) {
|
|
241587
241963
|
return this.store.getCompany(id);
|
|
@@ -241646,11 +242022,49 @@ var init_service6 = __esm({
|
|
|
241646
242022
|
return account;
|
|
241647
242023
|
}
|
|
241648
242024
|
/**
|
|
241649
|
-
*
|
|
241650
|
-
*
|
|
242025
|
+
* 授予某邮箱在某公司的组织管理员(ADR 0009 切片5:bootstrap 种子 + `grant-owner` 兜底)。
|
|
242026
|
+
*
|
|
242027
|
+
* 🚨 **它必须自己守 I1**(组织三层身份 ADR D2.4)。老实现是**无条件** upsert `role='owner'`,
|
|
242028
|
+
* 而调用点之一是 serve 的 bootstrap(`OASIS_BOOTSTRAP_OWNER`,在**启动路径**上)——该组织若已有
|
|
242029
|
+
* 组织管理员,它会写出第二位,在部分唯一索引 `company_members_one_owner_idx` 下直接抛错,
|
|
242030
|
+
* **等于 serve 起不来**。所以三分支:
|
|
242031
|
+
* - 目标已是该组织 owner → no-op,返回既有那行;
|
|
242032
|
+
* - 组织已有**别的** owner → **不写 owner**、`console.warn`,返回 `{ granted: false, existingOwner }`,
|
|
242033
|
+
* CLI 侧据此非零退出。但**仍然保证他是成员**:还不是成员就落一条 `admin`(已是成员的角色一个字不动)。
|
|
242034
|
+
* 为什么不是干脆「什么都不写」:这条通道的本意是 ADR 0009 的「第一个 owner 不会把自己锁在门外」
|
|
242035
|
+
* ——`OASIS_BOOTSTRAP_OWNER_EMAIL` 要让创始人**能登录并进得去管理面**。什么都不写的话,
|
|
242036
|
+
* 首装时若默认公司迁移已经指派过一位 owner,创始人就只有账号、没有任何 membership,
|
|
242037
|
+
* 登录后被 `CompanyGate` 扔到建团队页——兜底通道当场失效。管理面的闸是 `requireOwnerOrAdmin`,
|
|
242038
|
+
* admin 一样进得去;而「谁是唯一持有人」不该被一次装机悄悄换人。
|
|
242039
|
+
* - 组织无 owner(首次 bootstrap、或存量 0-owner 的脏数据)→ 照旧指派为 owner。
|
|
242040
|
+
* 「换人」一律走转交(`POST /api/companies/:id/owner`),不从这条兜底通道走。
|
|
241651
242041
|
*/
|
|
241652
242042
|
async grantOwner(companyId, email2, name) {
|
|
241653
242043
|
const account = await this.ensureAccount(email2, name);
|
|
242044
|
+
const members = await this.store.listMembers(companyId);
|
|
242045
|
+
const currentOwner = members.find((m2) => m2.role === "owner");
|
|
242046
|
+
if (currentOwner && currentOwner.accountId !== account.id) {
|
|
242047
|
+
const existingSelf = members.find((m2) => m2.accountId === account.id);
|
|
242048
|
+
if (existingSelf) {
|
|
242049
|
+
console.warn(
|
|
242050
|
+
`[companies] ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${currentOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${account.id}\uFF08\u4ED6\u5DF2\u662F\u672C\u7EC4\u7EC7 ${existingSelf.role}\uFF0C\u89D2\u8272\u672A\u6539\uFF09\uFF1B\u5982\u9700\u66F4\u6362\u8BF7\u8D70\u8F6C\u4EA4\uFF08POST /api/companies/${companyId}/owner\uFF09\u3002`
|
|
242051
|
+
);
|
|
242052
|
+
return { granted: false, membership: existingSelf, existingOwner: currentOwner };
|
|
242053
|
+
}
|
|
242054
|
+
const asAdmin = {
|
|
242055
|
+
companyId,
|
|
242056
|
+
accountId: account.id,
|
|
242057
|
+
actorId: account.id,
|
|
242058
|
+
role: "admin",
|
|
242059
|
+
onboardingCompletedAt: this.now()
|
|
242060
|
+
};
|
|
242061
|
+
await this.store.upsertMember(asAdmin);
|
|
242062
|
+
console.warn(
|
|
242063
|
+
`[companies] ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${currentOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${account.id}\uFF1B\u5DF2\u6539\u4E3A\u4EE5\u300C\u7BA1\u7406\u5458\u300D\u52A0\u5165\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09\u3002\u5982\u9700\u66F4\u6362\u6301\u6709\u4EBA\u8BF7\u8D70\u8F6C\u4EA4\uFF08POST /api/companies/${companyId}/owner\uFF09\u3002`
|
|
242064
|
+
);
|
|
242065
|
+
return { granted: false, membership: asAdmin, existingOwner: currentOwner };
|
|
242066
|
+
}
|
|
242067
|
+
if (currentOwner) return { granted: false, membership: currentOwner };
|
|
241654
242068
|
const membership = {
|
|
241655
242069
|
companyId,
|
|
241656
242070
|
accountId: account.id,
|
|
@@ -241659,7 +242073,7 @@ var init_service6 = __esm({
|
|
|
241659
242073
|
onboardingCompletedAt: this.now()
|
|
241660
242074
|
};
|
|
241661
242075
|
await this.store.upsertMember(membership);
|
|
241662
|
-
return membership;
|
|
242076
|
+
return { granted: true, membership };
|
|
241663
242077
|
}
|
|
241664
242078
|
/**
|
|
241665
242079
|
* 当前公司解析(动态、随登录用户走):
|
|
@@ -241672,6 +242086,7 @@ var init_service6 = __esm({
|
|
|
241672
242086
|
let company;
|
|
241673
242087
|
if (selector) {
|
|
241674
242088
|
company = await this.store.getCompany(selector) ?? await this.store.getCompanyBySlug(selector);
|
|
242089
|
+
if (company && company.status !== "active") company = null;
|
|
241675
242090
|
} else {
|
|
241676
242091
|
company = null;
|
|
241677
242092
|
for (const m2 of await this.store.listMembershipsForAccount(accountId)) {
|
|
@@ -241774,11 +242189,91 @@ var init_service6 = __esm({
|
|
|
241774
242189
|
*/
|
|
241775
242190
|
async changeMemberRole(companyId, accountId, role) {
|
|
241776
242191
|
if (!ROLES.includes(role)) throw new CompanyError(400, "BAD_ROLE", `role \u987B\u4E3A ${ROLES.join(" | ")}`);
|
|
242192
|
+
if (role === "owner") {
|
|
242193
|
+
throw new CompanyError(400, "OWNER_NOT_ASSIGNABLE", "\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EA\u80FD\u7ECF\u8F6C\u4EA4\u4EA7\u751F\uFF0C\u4E0D\u80FD\u76F4\u63A5\u6307\u6D3E");
|
|
242194
|
+
}
|
|
241777
242195
|
await this.requireCompany(companyId);
|
|
242196
|
+
const target = await this.store.getMember(companyId, accountId);
|
|
242197
|
+
if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242198
|
+
if (target.role === "owner") throw this.ownerProtected();
|
|
241778
242199
|
const res = await this.store.updateMemberRoleIfNotLastOwner(companyId, accountId, role);
|
|
241779
242200
|
if (res.ok) return res.member;
|
|
241780
242201
|
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
241781
|
-
throw
|
|
242202
|
+
throw this.ownerProtected();
|
|
242203
|
+
}
|
|
242204
|
+
/** 「组织管理员受保护」的唯一错误来源——成员管理面的四个入口共用,码与文案不各写一份。 */
|
|
242205
|
+
ownerProtected() {
|
|
242206
|
+
return new CompanyError(403, "OWNER_PROTECTED", "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u7F16\u8F91\u3001\u964D\u7EA7\u6216\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7");
|
|
242207
|
+
}
|
|
242208
|
+
/**
|
|
242209
|
+
* 转交组织管理员(`POST /api/companies/:id/owner`,ADR D4)。
|
|
242210
|
+
*
|
|
242211
|
+
* 调用者身份从会话解析、**不接受 body 里指定 from**。单事务、一把锁、**先降后升**,
|
|
242212
|
+
* 全部由 `store.setSoleCompanyOwner` 承担(并发语义见它的契约注释)。
|
|
242213
|
+
*
|
|
242214
|
+
* **刻意不做「已达成即 200」的幂等吞掉**:同一转交被重复提交时第二次同样得 409 `NOT_OWNER`。
|
|
242215
|
+
* 吞掉的话「我不是 owner 了」和「我刚转交成功」在响应里长得一样,前端分不出该显示
|
|
242216
|
+
* 「转交成功」还是「你的身份已变更」。
|
|
242217
|
+
*
|
|
242218
|
+
* **不 bump `tokenVersion`**:角色每请求现查(D7),转交不需要作废会话;bump 会把人从
|
|
242219
|
+
* **其它组织**一起踢下线(G10)。
|
|
242220
|
+
*/
|
|
242221
|
+
async transferOwner(companyId, fromAccountId, toAccountId) {
|
|
242222
|
+
if (!toAccountId) throw new CompanyError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId");
|
|
242223
|
+
if (fromAccountId === toAccountId) {
|
|
242224
|
+
throw new CompanyError(400, "SELF_TRANSFER", "\u4E0D\u80FD\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u81EA\u5DF1");
|
|
242225
|
+
}
|
|
242226
|
+
await this.requireActiveCompany(companyId);
|
|
242227
|
+
const res = await this.store.setSoleCompanyOwner(companyId, toAccountId, { requireCurrentOwner: fromAccountId });
|
|
242228
|
+
if (!res.ok) {
|
|
242229
|
+
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
|
|
242230
|
+
if (res.reason === "target_not_listed") {
|
|
242231
|
+
throw new CompanyError(409, "TARGET_NOT_LISTED", "\u5BF9\u65B9\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF08\u65B0\u624B\u6307\u5F15\u672A\u8D70\u5B8C\uFF09\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
|
|
242232
|
+
}
|
|
242233
|
+
if (res.reason === "target_inactive") {
|
|
242234
|
+
throw new CompanyError(409, "TARGET_INACTIVE", "\u5BF9\u65B9\u8D26\u53F7\u5DF2\u505C\u7528\uFF0C\u4E0D\u80FD\u63A5\u6536\u7EC4\u7EC7\u7BA1\u7406\u5458");
|
|
242235
|
+
}
|
|
242236
|
+
throw new CompanyError(409, "NOT_OWNER", "\u4F60\u5DF2\u4E0D\u662F\u8BE5\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8EAB\u4EFD\u5DF2\u53D8\u66F4");
|
|
242237
|
+
}
|
|
242238
|
+
const from = res.demoted.find((m2) => m2.accountId === fromAccountId);
|
|
242239
|
+
if (!from) throw new CompanyError(409, "NOT_OWNER", "\u4F60\u5DF2\u4E0D\u662F\u8BE5\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8EAB\u4EFD\u5DF2\u53D8\u66F4");
|
|
242240
|
+
return { from, to: res.owner };
|
|
242241
|
+
}
|
|
242242
|
+
/**
|
|
242243
|
+
* 存量映射用的「把这家组织收敛成唯一组织管理员」(ADR D3 回填面)。
|
|
242244
|
+
* 与转交**同一把锁、同一顺序**(先降后升)——两份实现必然漂,而漂出来的差异正好落在
|
|
242245
|
+
* 「中途有没有出现两位持有人」这种只有并发下才现形的地方。
|
|
242246
|
+
*/
|
|
242247
|
+
async setSoleOwner(companyId, toAccountId) {
|
|
242248
|
+
const res = await this.store.setSoleCompanyOwner(companyId, toAccountId);
|
|
242249
|
+
if (!res.ok) {
|
|
242250
|
+
if (res.reason === "target_not_listed") {
|
|
242251
|
+
throw new CompanyError(409, "TARGET_NOT_LISTED", `\u76EE\u6807\u5C1A\u672A\u5B8C\u6210\u52A0\u5165\uFF1A${toAccountId}`);
|
|
242252
|
+
}
|
|
242253
|
+
if (res.reason === "target_inactive") {
|
|
242254
|
+
throw new CompanyError(409, "TARGET_INACTIVE", `\u76EE\u6807\u8D26\u53F7\u5DF2\u505C\u7528\uFF1A${toAccountId}`);
|
|
242255
|
+
}
|
|
242256
|
+
throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${toAccountId}`);
|
|
242257
|
+
}
|
|
242258
|
+
return { owner: res.owner, demoted: res.demoted };
|
|
242259
|
+
}
|
|
242260
|
+
/**
|
|
242261
|
+
* 解散组织(`DELETE /api/companies/:id`,ADR D9.2)——**可审计的停用,不是物理销毁**。
|
|
242262
|
+
* 仅当前组织管理员;「我还是不是 owner」在**同一把锁内**复核(D7.2),不是入口查一次就写。
|
|
242263
|
+
* 断访问靠 `resolveCurrentCompany` 只认 active(D8.4)+ `listForUser` 只列 active(D9.2)。
|
|
242264
|
+
*/
|
|
242265
|
+
async dissolveCompany(companyId, callerAccountId) {
|
|
242266
|
+
await this.requireActiveCompany(companyId);
|
|
242267
|
+
const res = await this.store.dissolveCompanyIfOwner(companyId, callerAccountId, this.now());
|
|
242268
|
+
if (res.ok) return res.company;
|
|
242269
|
+
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
|
|
242270
|
+
throw new CompanyError(403, "FORBIDDEN", "\u53EA\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\u53EF\u4EE5\u89E3\u6563\u7EC4\u7EC7");
|
|
242271
|
+
}
|
|
242272
|
+
/** 公司必须存在**且 active**:已解散 / 已停用的组织上不接受任何写。 */
|
|
242273
|
+
async requireActiveCompany(companyId) {
|
|
242274
|
+
const c = await this.requireCompany(companyId);
|
|
242275
|
+
if (c.status !== "active") throw new CompanyError(404, "COMPANY_NOT_FOUND", `\u516C\u53F8\u4E0D\u53EF\u7528\uFF1A${companyId}`);
|
|
242276
|
+
return c;
|
|
241782
242277
|
}
|
|
241783
242278
|
/**
|
|
241784
242279
|
* 改成员在本公司的昵称(组织模型 ADR §4.1 `company_member.display_name`)。
|
|
@@ -241805,9 +242300,7 @@ var init_service6 = __esm({
|
|
|
241805
242300
|
await this.requireCompany(companyId);
|
|
241806
242301
|
const member = await this.store.getMember(companyId, accountId);
|
|
241807
242302
|
if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
241808
|
-
if (member.role === "owner"
|
|
241809
|
-
throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
|
|
241810
|
-
}
|
|
242303
|
+
if (member.role === "owner") throw this.ownerProtected();
|
|
241811
242304
|
return member;
|
|
241812
242305
|
}
|
|
241813
242306
|
/**
|
|
@@ -241825,10 +242318,82 @@ var init_service6 = __esm({
|
|
|
241825
242318
|
*/
|
|
241826
242319
|
async removeMember(companyId, accountId) {
|
|
241827
242320
|
await this.requireCompany(companyId);
|
|
242321
|
+
const target = await this.store.getMember(companyId, accountId);
|
|
242322
|
+
if (!target) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242323
|
+
if (target.role === "owner") throw this.ownerProtected();
|
|
241828
242324
|
const res = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
|
|
241829
242325
|
if (res.ok) return res.member;
|
|
241830
242326
|
if (res.reason === "not_found") throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
241831
|
-
throw
|
|
242327
|
+
throw this.ownerProtected();
|
|
242328
|
+
}
|
|
242329
|
+
/**
|
|
242330
|
+
* 退出组织(`DELETE /api/companies/:id/members/me`,ADR D9.1)——**本人限定**。
|
|
242331
|
+
*
|
|
242332
|
+
* 组织管理员退不出:先转交、或(只剩他一个人时)解散。响应体带 `canDissolve`,
|
|
242333
|
+
* 前端据它把人引到「解散组织」而不是「先转交给谁」(brief:单人团队路径)。
|
|
242334
|
+
* 真正的移除副作用由 `MemberOffboardingService.applyMemberRemoval` 承担——与管理员移除他人
|
|
242335
|
+
* **同一套**,不在这里另写一份。
|
|
242336
|
+
*/
|
|
242337
|
+
async peekSelfLeave(companyId, accountId) {
|
|
242338
|
+
await this.requireActiveCompany(companyId);
|
|
242339
|
+
const me = await this.store.getMember(companyId, accountId);
|
|
242340
|
+
if (!me) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242341
|
+
if (me.role === "owner") {
|
|
242342
|
+
const canDissolve = (await this.store.listMembers(companyId)).length <= 1;
|
|
242343
|
+
throw new CompanyError(
|
|
242344
|
+
409,
|
|
242345
|
+
"OWNER_MUST_TRANSFER",
|
|
242346
|
+
canDissolve ? "\u4F60\u662F\u8FD9\u4E2A\u7EC4\u7EC7\u552F\u4E00\u7684\u6210\u5458\uFF0C\u9000\u51FA\u8BF7\u6539\u4E3A\u89E3\u6563\u7EC4\u7EC7" : "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u80FD\u9000\u51FA\u7EC4\u7EC7\uFF0C\u8BF7\u5148\u628A\u7EC4\u7EC7\u7BA1\u7406\u5458\u8F6C\u4EA4\u7ED9\u4ED6\u4EBA",
|
|
242347
|
+
{ canDissolve }
|
|
242348
|
+
);
|
|
242349
|
+
}
|
|
242350
|
+
return me;
|
|
242351
|
+
}
|
|
242352
|
+
/**
|
|
242353
|
+
* 注销账号的**跨组织前置检查**(`DELETE /api/accounts/me`,ADR D9.3)。
|
|
242354
|
+
*
|
|
242355
|
+
* 遍历该账号**全部** membership,取其中 `status='active'` 的组织:只要**任意一家**里还是
|
|
242356
|
+
* 组织管理员就拒(409 `OWNER_IN_ORGS`,响应体列出这些组织的 `{id, name}`),要求先转交或解散。
|
|
242357
|
+
* 🚨 这条检查天然跨组织、与「当前公司」无关——**不能**只看当前组织,漏一家就等于把那家组织
|
|
242358
|
+
* 甩成无人可管,而那个人已经注销、再也转交不了。
|
|
242359
|
+
*
|
|
242360
|
+
* 返回值 = 要逐家执行退出副作用的 membership 清单。**只含 active 组织**:已解散组织里的成员行
|
|
242361
|
+
* 原样保留(账号停用后进不去,解散组织本身也解析不出来),不为了「删干净」去碰一家已经归档的
|
|
242362
|
+
* 组织的成员名单——那会让它的历史成员名册凭空少人。
|
|
242363
|
+
*/
|
|
242364
|
+
async peekAccountDeletion(accountId) {
|
|
242365
|
+
const account = await this.store.getAccount(accountId);
|
|
242366
|
+
if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
|
|
242367
|
+
const owned = await this.listOwnedActiveCompanies(accountId);
|
|
242368
|
+
if (owned.length > 0) {
|
|
242369
|
+
throw new CompanyError(
|
|
242370
|
+
409,
|
|
242371
|
+
"OWNER_IN_ORGS",
|
|
242372
|
+
"\u4F60\u8FD8\u662F\u4EE5\u4E0B\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8BF7\u5148\u8F6C\u4EA4\u6216\u89E3\u6563\u540E\u518D\u6CE8\u9500",
|
|
242373
|
+
{ companies: owned.map((c) => ({ id: c.id, name: c.name })) }
|
|
242374
|
+
);
|
|
242375
|
+
}
|
|
242376
|
+
const memberships = await this.store.listMembershipsForAccount(accountId);
|
|
242377
|
+
const out = [];
|
|
242378
|
+
for (const m2 of memberships) {
|
|
242379
|
+
const c = await this.store.getCompany(m2.companyId);
|
|
242380
|
+
if (c?.status === "active") out.push(m2);
|
|
242381
|
+
}
|
|
242382
|
+
return out;
|
|
242383
|
+
}
|
|
242384
|
+
/**
|
|
242385
|
+
* 注销的最后一步:账号置 `disabled` + **bump `tokenVersion`**,当场作废该账号全部会话。
|
|
242386
|
+
* 这是 `tokenVersion` 的**正确**用法(账号级总闸);转交与降级**不得**用它——那会把人从
|
|
242387
|
+
* 其它组织一起踢下线(G10)。
|
|
242388
|
+
*
|
|
242389
|
+
* **数据口径要诚实**:oplog 里的历史署名、已交付产物、任务轨迹**不删**(内核纯净红线)。
|
|
242390
|
+
* 「注销」= 这个人再也登不进来、不再是任何 active 组织的成员,不等于「个人数据被永久删除」。
|
|
242391
|
+
*/
|
|
242392
|
+
async finalizeAccountDeletion(accountId) {
|
|
242393
|
+
const account = await this.store.getAccount(accountId);
|
|
242394
|
+
if (!account) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u8D26\u53F7\uFF1A${accountId}`);
|
|
242395
|
+
await this.store.upsertAccount({ ...account, status: "disabled" });
|
|
242396
|
+
await this.store.bumpAccountTokenVersion(accountId);
|
|
241832
242397
|
}
|
|
241833
242398
|
// —— 邀请(本地模型,CO-005)——
|
|
241834
242399
|
/**
|
|
@@ -242263,21 +242828,18 @@ var init_service6 = __esm({
|
|
|
242263
242828
|
if (!c) throw new CompanyError(404, "NOT_FOUND", `\u6CA1\u6709\u8FD9\u4E2A\u516C\u53F8\uFF1A${companyId}`);
|
|
242264
242829
|
return c;
|
|
242265
242830
|
}
|
|
242266
|
-
async isLastOwner(companyId, accountId) {
|
|
242267
|
-
const owners = (await this.store.listMembers(companyId)).filter((m2) => m2.role === "owner");
|
|
242268
|
-
return owners.length === 1 && owners[0].accountId === accountId;
|
|
242269
|
-
}
|
|
242270
242831
|
};
|
|
242271
242832
|
}
|
|
242272
242833
|
});
|
|
242273
242834
|
|
|
242274
242835
|
// ../server/src/domains/companies/offboarding.ts
|
|
242275
|
-
var import_node_crypto59, MemberOffboardingService;
|
|
242836
|
+
var import_node_crypto59, OWNER_PROTECTED_MESSAGE, MemberOffboardingService;
|
|
242276
242837
|
var init_offboarding = __esm({
|
|
242277
242838
|
"../server/src/domains/companies/offboarding.ts"() {
|
|
242278
242839
|
"use strict";
|
|
242279
242840
|
import_node_crypto59 = require("node:crypto");
|
|
242280
242841
|
init_service6();
|
|
242842
|
+
OWNER_PROTECTED_MESSAGE = "\u7EC4\u7EC7\u7BA1\u7406\u5458\u4E0D\u53EF\u88AB\u79FB\u9664\uFF0C\u53EA\u80FD\u7531\u672C\u4EBA\u8F6C\u4EA4\u6216\u89E3\u6563\u7EC4\u7EC7";
|
|
242281
242843
|
MemberOffboardingService = class {
|
|
242282
242844
|
store;
|
|
242283
242845
|
now;
|
|
@@ -242319,7 +242881,9 @@ var init_offboarding = __esm({
|
|
|
242319
242881
|
if (!member) {
|
|
242320
242882
|
return [{ code: "NOT_FOUND", message: `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}` }];
|
|
242321
242883
|
}
|
|
242322
|
-
if (
|
|
242884
|
+
if (member.role === "owner") {
|
|
242885
|
+
blockers.push({ code: "OWNER_PROTECTED", message: OWNER_PROTECTED_MESSAGE });
|
|
242886
|
+
} else if (await this.isLastOwner(companyId, accountId)) {
|
|
242323
242887
|
blockers.push({ code: "LAST_OWNER", message: "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner" });
|
|
242324
242888
|
}
|
|
242325
242889
|
return blockers;
|
|
@@ -242342,6 +242906,7 @@ var init_offboarding = __esm({
|
|
|
242342
242906
|
async plan(companyId, accountId, initiatedBy) {
|
|
242343
242907
|
const member = await this.store.getMember(companyId, accountId);
|
|
242344
242908
|
if (!member) throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
242909
|
+
if (member.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
242345
242910
|
if (await this.isLastOwner(companyId, accountId)) {
|
|
242346
242911
|
throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
|
|
242347
242912
|
}
|
|
@@ -242388,6 +242953,8 @@ var init_offboarding = __esm({
|
|
|
242388
242953
|
if (op.version !== expectedVersion) {
|
|
242389
242954
|
throw new CompanyError(409, "STALE_PLAN", `\u8BA1\u5212\u7248\u672C\u5DF2\u53D8\uFF08\u5F53\u524D ${op.version}\uFF0C\u4F60\u7ED9\u7684\u662F ${expectedVersion}\uFF09\uFF0C\u8BF7\u91CD\u65B0\u53D6\u8BA1\u5212`);
|
|
242390
242955
|
}
|
|
242956
|
+
const target = await this.store.getMember(companyId, accountId);
|
|
242957
|
+
if (target?.role === "owner") throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
242391
242958
|
if (await this.isLastOwner(companyId, accountId)) {
|
|
242392
242959
|
throw new CompanyError(409, "LAST_OWNER", "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner\uFF0C\u4E0D\u80FD\u79FB\u9664\u6700\u540E\u4E00\u4E2A owner");
|
|
242393
242960
|
}
|
|
@@ -242403,19 +242970,7 @@ var init_offboarding = __esm({
|
|
|
242403
242970
|
);
|
|
242404
242971
|
throw new CompanyError(409, remaining[0].code, `\u4ECD\u6709\u672A\u4EA4\u63A5\u9879\uFF1A${remaining.map((b2) => b2.message).join("; ")}`);
|
|
242405
242972
|
}
|
|
242406
|
-
|
|
242407
|
-
if (member && !await isEmployedElsewhere(this.store, companyId, accountId)) {
|
|
242408
|
-
await this.disableActor?.(companyId, member.actorId);
|
|
242409
|
-
}
|
|
242410
|
-
await this.store.deletePrincipalBridge(companyId, "human", accountId);
|
|
242411
|
-
const removed = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
|
|
242412
|
-
if (!removed.ok) {
|
|
242413
|
-
throw new CompanyError(
|
|
242414
|
-
removed.reason === "last_owner" ? 409 : 404,
|
|
242415
|
-
removed.reason === "last_owner" ? "LAST_OWNER" : "NOT_FOUND",
|
|
242416
|
-
removed.reason === "last_owner" ? "\u516C\u53F8\u81F3\u5C11\u8981\u7559 1 \u4E2A owner" : `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`
|
|
242417
|
-
);
|
|
242418
|
-
}
|
|
242973
|
+
await this.applyMemberRemoval(companyId, accountId);
|
|
242419
242974
|
await this.store.updateMemberOffboardingIfVersion(
|
|
242420
242975
|
companyId,
|
|
242421
242976
|
operationId,
|
|
@@ -242425,6 +242980,37 @@ var init_offboarding = __esm({
|
|
|
242425
242980
|
);
|
|
242426
242981
|
return { status: "removed", transferred: [], left: released };
|
|
242427
242982
|
}
|
|
242983
|
+
/**
|
|
242984
|
+
* 移除一条成员关系的**全套副作用**,顺序固定,三条移除路径共用:
|
|
242985
|
+
* ① 清名下 agent 的负责人字段(`digital_employee.owner` 的复合 FK 是 ON DELETE RESTRICT,
|
|
242986
|
+
* 不清就是一个裸 FK 违例);**只清 owner**——不改状态、不撤凭证、不解绑助理(PRD v1.5);
|
|
242987
|
+
* ② 停用花名册里这个人(**还在别家公司在职就不停用**——停用打的是全局那一行);
|
|
242988
|
+
* ③ 删 human principal bridge(对 company_member 是 CASCADE,但文件/内存实现没有级联,
|
|
242989
|
+
* 显式删一次让「桥随主体走」在应用层也成立);
|
|
242990
|
+
* ④ 删成员行(走 `removeMemberIfNotLastOwner`,组织管理员那一位在这里被纵深防御挡住)。
|
|
242991
|
+
*
|
|
242992
|
+
* 顺序不能反:成员行一删,本公司的 scoped registry 就不再认这个 actor,②会静默 no-op。
|
|
242993
|
+
* 任何一步失败都停在「人已停用、成员还在」——可重试,每一步都幂等。
|
|
242994
|
+
*
|
|
242995
|
+
* 三个调用点:管理员移除他人(`commit`)、本人退出组织(`DELETE …/members/me`)、
|
|
242996
|
+
* 注销账号(逐家执行,`DELETE /api/accounts/me`)。
|
|
242997
|
+
*/
|
|
242998
|
+
async applyMemberRemoval(companyId, accountId) {
|
|
242999
|
+
const released = await this.releaseOwnedAgents(companyId, accountId);
|
|
243000
|
+
const member = await this.store.getMember(companyId, accountId);
|
|
243001
|
+
if (member && !await isEmployedElsewhere(this.store, companyId, accountId)) {
|
|
243002
|
+
await this.disableActor?.(companyId, member.actorId);
|
|
243003
|
+
}
|
|
243004
|
+
await this.store.deletePrincipalBridge(companyId, "human", accountId);
|
|
243005
|
+
const removed = await this.store.removeMemberIfNotLastOwner(companyId, accountId);
|
|
243006
|
+
if (!removed.ok) {
|
|
243007
|
+
if (removed.reason === "not_found") {
|
|
243008
|
+
throw new CompanyError(404, "NOT_FOUND", `\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF1A${accountId}`);
|
|
243009
|
+
}
|
|
243010
|
+
throw new CompanyError(403, "OWNER_PROTECTED", OWNER_PROTECTED_MESSAGE);
|
|
243011
|
+
}
|
|
243012
|
+
return released;
|
|
243013
|
+
}
|
|
242428
243014
|
/**
|
|
242429
243015
|
* 把该成员名下所有 agent 的负责人字段清空,返回被清的 agent id。
|
|
242430
243016
|
*
|
|
@@ -242519,6 +243105,12 @@ function companiesDomain(opts) {
|
|
|
242519
243105
|
throw new ApiError(403, "FORBIDDEN", "\u9700\u8981\u8BE5\u516C\u53F8\u7684 owner/admin \u6743\u9650");
|
|
242520
243106
|
}
|
|
242521
243107
|
};
|
|
243108
|
+
const requireMember = async (req, companyId) => {
|
|
243109
|
+
const me = await whoami(req);
|
|
243110
|
+
const member = await service.getMember(companyId, me.accountId);
|
|
243111
|
+
if (!member) throw new ApiError(403, "NOT_A_MEMBER", "\u4E0D\u662F\u8BE5\u516C\u53F8\u6210\u5458\uFF0C\u8BBF\u95EE\u88AB\u62D2");
|
|
243112
|
+
return me;
|
|
243113
|
+
};
|
|
242522
243114
|
return (router) => {
|
|
242523
243115
|
router.get("/api/companies", async (req) => {
|
|
242524
243116
|
const me = await whoami(req);
|
|
@@ -242578,7 +243170,9 @@ function companiesDomain(opts) {
|
|
|
242578
243170
|
return { status: 200, body: { ok: true } };
|
|
242579
243171
|
});
|
|
242580
243172
|
router.get("/api/companies/:id/members", async (req) => {
|
|
242581
|
-
const
|
|
243173
|
+
const cid = req.params.id;
|
|
243174
|
+
await requireMember(req, cid);
|
|
243175
|
+
const items = await service.listMemberViews(cid).catch(mapErr);
|
|
242582
243176
|
return { status: 200, body: { items } };
|
|
242583
243177
|
});
|
|
242584
243178
|
router.get("/api/companies/:id/members/me", async (req) => {
|
|
@@ -242608,6 +243202,9 @@ function companiesDomain(opts) {
|
|
|
242608
243202
|
if (b2.role === void 0 && b2.displayName === void 0) {
|
|
242609
243203
|
throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11\u53EF\u66F4\u65B0\u5B57\u6BB5\uFF08role / displayName\uFF09");
|
|
242610
243204
|
}
|
|
243205
|
+
const me = await whoami(req);
|
|
243206
|
+
if (b2.role !== void 0 || me.accountId !== aid) await requireOwnerOrAdmin(req, cid);
|
|
243207
|
+
else await requireMember(req, cid);
|
|
242611
243208
|
let member;
|
|
242612
243209
|
if (b2.role !== void 0) {
|
|
242613
243210
|
if (!b2.role) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 role");
|
|
@@ -242622,9 +243219,18 @@ function companiesDomain(opts) {
|
|
|
242622
243219
|
}
|
|
242623
243220
|
return { status: 200, body: member };
|
|
242624
243221
|
});
|
|
243222
|
+
router.delete("/api/companies/:id/members/me", async (req) => {
|
|
243223
|
+
const off = requireOffboarding();
|
|
243224
|
+
const cid = req.params.id;
|
|
243225
|
+
const me = await whoami(req);
|
|
243226
|
+
await service.peekSelfLeave(cid, me.accountId).catch(mapErr);
|
|
243227
|
+
const released = await off.applyMemberRemoval(cid, me.accountId).catch(mapErr);
|
|
243228
|
+
return { status: 200, body: { ok: true, releasedAgents: released } };
|
|
243229
|
+
});
|
|
242625
243230
|
router.delete("/api/companies/:id/members/:accountId", async (req) => {
|
|
242626
243231
|
const cid = req.params.id;
|
|
242627
243232
|
const aid = req.params.accountId;
|
|
243233
|
+
await requireOwnerOrAdmin(req, cid);
|
|
242628
243234
|
const member = await service.peekRemovableMember(cid, aid).catch(mapErr);
|
|
242629
243235
|
if (offboarding) await offboarding.releaseOwnedAgents(cid, aid).catch(mapErr);
|
|
242630
243236
|
let actorsSvc;
|
|
@@ -242666,6 +243272,32 @@ function companiesDomain(opts) {
|
|
|
242666
243272
|
}
|
|
242667
243273
|
return { status: 200, body: { ok: true } };
|
|
242668
243274
|
});
|
|
243275
|
+
router.post("/api/companies/:id/owner", async (req) => {
|
|
243276
|
+
const cid = req.params.id;
|
|
243277
|
+
const b2 = req.body ?? {};
|
|
243278
|
+
if (typeof b2.toAccountId !== "string" || !b2.toAccountId.trim()) {
|
|
243279
|
+
throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 toAccountId: string");
|
|
243280
|
+
}
|
|
243281
|
+
const me = await whoami(req);
|
|
243282
|
+
const res = await service.transferOwner(cid, me.accountId, b2.toAccountId.trim()).catch(mapErr);
|
|
243283
|
+
return { status: 200, body: res };
|
|
243284
|
+
});
|
|
243285
|
+
router.delete("/api/companies/:id", async (req) => {
|
|
243286
|
+
const cid = req.params.id;
|
|
243287
|
+
const me = await whoami(req);
|
|
243288
|
+
const company = await service.dissolveCompany(cid, me.accountId).catch(mapErr);
|
|
243289
|
+
return { status: 200, body: company };
|
|
243290
|
+
});
|
|
243291
|
+
router.delete("/api/accounts/me", async (req) => {
|
|
243292
|
+
const off = requireOffboarding();
|
|
243293
|
+
const me = await whoami(req);
|
|
243294
|
+
const memberships = await service.peekAccountDeletion(me.accountId).catch(mapErr);
|
|
243295
|
+
for (const m2 of memberships) {
|
|
243296
|
+
await off.applyMemberRemoval(m2.companyId, me.accountId).catch(mapErr);
|
|
243297
|
+
}
|
|
243298
|
+
await service.finalizeAccountDeletion(me.accountId).catch(mapErr);
|
|
243299
|
+
return { status: 200, body: { ok: true, leftCompanies: memberships.map((m2) => m2.companyId) } };
|
|
243300
|
+
});
|
|
242669
243301
|
router.get("/api/companies/:id/agent-credentials", async (req) => {
|
|
242670
243302
|
const creds = requireCredentials();
|
|
242671
243303
|
const cid = req.params.id;
|
|
@@ -242713,6 +243345,7 @@ function companiesDomain(opts) {
|
|
|
242713
243345
|
router.post("/api/companies/:id/invitations", async (req) => {
|
|
242714
243346
|
const b2 = req.body ?? {};
|
|
242715
243347
|
if (!b2.email) throw new ApiError(400, "BAD_REQUEST", "\u7F3A\u5C11 email");
|
|
243348
|
+
await requireOwnerOrAdmin(req, req.params.id);
|
|
242716
243349
|
const role = b2.role ?? "member";
|
|
242717
243350
|
const me = await whoami(req);
|
|
242718
243351
|
const invitation = await service.createInvitation(req.params.id, b2.email, role, me.accountId).catch(mapErr);
|
|
@@ -242852,7 +243485,7 @@ var init_routes6 = __esm({
|
|
|
242852
243485
|
init_router();
|
|
242853
243486
|
init_service6();
|
|
242854
243487
|
init_image_upload();
|
|
242855
|
-
COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links"];
|
|
243488
|
+
COMPANY_CROSS_TENANT_PREFIXES = ["/api/companies", "/api/invitations", "/api/invite-links", "/api/accounts"];
|
|
242856
243489
|
}
|
|
242857
243490
|
});
|
|
242858
243491
|
|
|
@@ -259991,7 +260624,35 @@ var init_pool = __esm({
|
|
|
259991
260624
|
});
|
|
259992
260625
|
|
|
259993
260626
|
// ../storage/src/postgres-control-plane.ts
|
|
259994
|
-
|
|
260627
|
+
async function ensureOneOwnerInvariant(pool, s2) {
|
|
260628
|
+
try {
|
|
260629
|
+
const bad = await pool.query(
|
|
260630
|
+
`SELECT company_id, count(*) FILTER (WHERE role = 'owner') AS owners
|
|
260631
|
+
FROM "${s2}".company_members
|
|
260632
|
+
GROUP BY company_id
|
|
260633
|
+
HAVING count(*) FILTER (WHERE role = 'owner') <> 1
|
|
260634
|
+
ORDER BY company_id`
|
|
260635
|
+
);
|
|
260636
|
+
if (bad.rowCount && bad.rowCount > 0) {
|
|
260637
|
+
console.warn(
|
|
260638
|
+
`[control-plane] ${bad.rowCount} \u5BB6\u7EC4\u7EC7\u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\u6570\u91CF\u4E0D\u662F 1\uFF0C\u5DF2\u8DF3\u8FC7\uFF08\u4E0D\u81EA\u52A8\u6311\u4EBA\uFF09\uFF1A` + bad.rows.map((r) => `${r.company_id}=${r.owners}`).join(", ") + `\u3002\u7528 \`pnpm migrate:org-identity inventory\` \u51FA\u76D8\u70B9\u3001\u6309\u51BB\u7ED3 manifest \u8DD1 backfill\u3002`
|
|
260639
|
+
);
|
|
260640
|
+
}
|
|
260641
|
+
} catch (e) {
|
|
260642
|
+
console.warn(`[control-plane] \u7EC4\u7EC7\u7BA1\u7406\u5458\u6570\u91CF\u590D\u6838\u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u542F\u52A8\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`);
|
|
260643
|
+
}
|
|
260644
|
+
try {
|
|
260645
|
+
await pool.query(
|
|
260646
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS company_members_one_owner_idx
|
|
260647
|
+
ON "${s2}".company_members (company_id) WHERE role = 'owner'`
|
|
260648
|
+
);
|
|
260649
|
+
} catch (e) {
|
|
260650
|
+
console.warn(
|
|
260651
|
+
`[control-plane] \u5EFA company_members_one_owner_idx \u5931\u8D25\uFF08\u4E0D\u963B\u65AD\u542F\u52A8\uFF0C\u5E94\u7528\u5C42\u5224\u636E\u4ECD\u5728\uFF09\uFF1A${e instanceof Error ? e.message : String(e)}`
|
|
260652
|
+
);
|
|
260653
|
+
}
|
|
260654
|
+
}
|
|
260655
|
+
var ident8, PostgresControlPlaneStore, rowToDigitalEmployee, rowToAgentCredential, rowToBridge, rowToOffboarding, toIso, rowToCompany, rowToRoleMigration, rowToMember, rowToInvitation, rowToInviteLink, rowToAccount, rowToVerificationCode;
|
|
259995
260656
|
var init_postgres_control_plane = __esm({
|
|
259996
260657
|
"../storage/src/postgres-control-plane.ts"() {
|
|
259997
260658
|
"use strict";
|
|
@@ -260021,6 +260682,8 @@ var init_postgres_control_plane = __esm({
|
|
|
260021
260682
|
onboarding_variant text -- \u767B\u5F55\u540E\u52A9\u7406\u5F15\u5BFC\u53D8\u4F53 A|B\uFF1BNULL=\u7F3A\u7701\u89C6\u4E3A A
|
|
260022
260683
|
)`);
|
|
260023
260684
|
await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS onboarding_variant text`);
|
|
260685
|
+
await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_at timestamptz`);
|
|
260686
|
+
await pool.query(`ALTER TABLE "${s2}".companies ADD COLUMN IF NOT EXISTS dissolved_by text`);
|
|
260024
260687
|
await pool.query(`
|
|
260025
260688
|
CREATE TABLE IF NOT EXISTS "${s2}".company_members (
|
|
260026
260689
|
company_id text NOT NULL,
|
|
@@ -260209,6 +260872,19 @@ var init_postgres_control_plane = __esm({
|
|
|
260209
260872
|
CHECK (status IN ('handoff_required','handoff_in_progress','ready_to_remove','removed'))
|
|
260210
260873
|
)`);
|
|
260211
260874
|
await pool.query(`CREATE UNIQUE INDEX IF NOT EXISTS member_offboardings_active_uq ON "${s2}".member_offboardings (company_id, account_id) WHERE status <> 'removed'`);
|
|
260875
|
+
await pool.query(`
|
|
260876
|
+
CREATE TABLE IF NOT EXISTS "${s2}".company_member_role_migrations (
|
|
260877
|
+
id bigserial PRIMARY KEY,
|
|
260878
|
+
company_id text NOT NULL,
|
|
260879
|
+
account_id text NOT NULL,
|
|
260880
|
+
role_before text NOT NULL,
|
|
260881
|
+
role_after text NOT NULL,
|
|
260882
|
+
evidence text NOT NULL, -- 'E1' | 'manifest'
|
|
260883
|
+
manifest_version text,
|
|
260884
|
+
applied_at timestamptz NOT NULL DEFAULT now()
|
|
260885
|
+
)`);
|
|
260886
|
+
await pool.query(`CREATE INDEX IF NOT EXISTS company_member_role_migrations_company_idx ON "${s2}".company_member_role_migrations (company_id, applied_at DESC)`);
|
|
260887
|
+
await ensureOneOwnerInvariant(pool, s2);
|
|
260212
260888
|
return new _PostgresControlPlaneStore(pool, schema);
|
|
260213
260889
|
}
|
|
260214
260890
|
// —— 公司 ——
|
|
@@ -260249,11 +260925,51 @@ var init_postgres_control_plane = __esm({
|
|
|
260249
260925
|
args.push(patch.onboardingVariant);
|
|
260250
260926
|
sets.push(`onboarding_variant = $${args.length}`);
|
|
260251
260927
|
}
|
|
260928
|
+
if (patch.dissolvedAt !== void 0) {
|
|
260929
|
+
args.push(patch.dissolvedAt);
|
|
260930
|
+
sets.push(`dissolved_at = $${args.length}`);
|
|
260931
|
+
}
|
|
260932
|
+
if (patch.dissolvedBy !== void 0) {
|
|
260933
|
+
args.push(patch.dissolvedBy);
|
|
260934
|
+
sets.push(`dissolved_by = $${args.length}`);
|
|
260935
|
+
}
|
|
260252
260936
|
args.push(id);
|
|
260253
260937
|
const sql = sets.length ? `UPDATE ${this.s}.companies SET ${sets.join(", ")} WHERE id = $${args.length}` : `SELECT 1 FROM ${this.s}.companies WHERE id = $${args.length}`;
|
|
260254
260938
|
const r = await this.pool.query(sql, args);
|
|
260255
260939
|
if (r.rowCount === 0) throw new Error(`company not found: ${id}`);
|
|
260256
260940
|
}
|
|
260941
|
+
async dissolveCompanyIfOwner(companyId, accountId, at2) {
|
|
260942
|
+
const client = await this.pool.connect();
|
|
260943
|
+
try {
|
|
260944
|
+
await client.query("BEGIN");
|
|
260945
|
+
const caller = await client.query(
|
|
260946
|
+
`SELECT role FROM ${this.s}.company_members WHERE company_id = $1 AND account_id = $2 FOR UPDATE`,
|
|
260947
|
+
[companyId, accountId]
|
|
260948
|
+
);
|
|
260949
|
+
if (!caller.rows[0] || caller.rows[0].role !== "owner") {
|
|
260950
|
+
await client.query("ROLLBACK");
|
|
260951
|
+
const exists = await this.pool.query(`SELECT 1 FROM ${this.s}.companies WHERE id = $1`, [companyId]);
|
|
260952
|
+
return { ok: false, reason: exists.rowCount ? "not_owner" : "not_found" };
|
|
260953
|
+
}
|
|
260954
|
+
const updated = await client.query(
|
|
260955
|
+
`UPDATE ${this.s}.companies SET status = 'suspended', dissolved_at = $2, dissolved_by = $3
|
|
260956
|
+
WHERE id = $1 RETURNING *`,
|
|
260957
|
+
[companyId, at2, accountId]
|
|
260958
|
+
);
|
|
260959
|
+
if (!updated.rows[0]) {
|
|
260960
|
+
await client.query("ROLLBACK");
|
|
260961
|
+
return { ok: false, reason: "not_found" };
|
|
260962
|
+
}
|
|
260963
|
+
await client.query("COMMIT");
|
|
260964
|
+
return { ok: true, company: rowToCompany(updated.rows[0]) };
|
|
260965
|
+
} catch (e) {
|
|
260966
|
+
await client.query("ROLLBACK").catch(() => {
|
|
260967
|
+
});
|
|
260968
|
+
throw e;
|
|
260969
|
+
} finally {
|
|
260970
|
+
client.release();
|
|
260971
|
+
}
|
|
260972
|
+
}
|
|
260257
260973
|
// —— 成员 ——
|
|
260258
260974
|
async upsertMember(m2) {
|
|
260259
260975
|
await this.pool.query(
|
|
@@ -260365,6 +261081,103 @@ var init_postgres_control_plane = __esm({
|
|
|
260365
261081
|
client.release();
|
|
260366
261082
|
}
|
|
260367
261083
|
}
|
|
261084
|
+
async setSoleCompanyOwner(companyId, toAccountId, opts) {
|
|
261085
|
+
const client = await this.pool.connect();
|
|
261086
|
+
try {
|
|
261087
|
+
await client.query("BEGIN");
|
|
261088
|
+
const all2 = await client.query(
|
|
261089
|
+
`SELECT company_id, account_id, actor_id, role, display_name, onboarding_completed_at, onboarding_step
|
|
261090
|
+
FROM ${this.s}.company_members WHERE company_id = $1 FOR UPDATE`,
|
|
261091
|
+
[companyId]
|
|
261092
|
+
);
|
|
261093
|
+
const rows = all2.rows.map(rowToMember);
|
|
261094
|
+
const target = rows.find((m2) => m2.accountId === toAccountId);
|
|
261095
|
+
if (!target) {
|
|
261096
|
+
await client.query("ROLLBACK");
|
|
261097
|
+
return { ok: false, reason: "not_found" };
|
|
261098
|
+
}
|
|
261099
|
+
if (opts?.requireCurrentOwner !== void 0) {
|
|
261100
|
+
const caller = rows.find((m2) => m2.accountId === opts.requireCurrentOwner);
|
|
261101
|
+
if (!caller || caller.role !== "owner") {
|
|
261102
|
+
await client.query("ROLLBACK");
|
|
261103
|
+
return { ok: false, reason: "not_owner" };
|
|
261104
|
+
}
|
|
261105
|
+
}
|
|
261106
|
+
if (!target.onboardingCompletedAt && target.role !== "owner") {
|
|
261107
|
+
await client.query("ROLLBACK");
|
|
261108
|
+
return { ok: false, reason: "target_not_listed" };
|
|
261109
|
+
}
|
|
261110
|
+
const acc = await client.query(
|
|
261111
|
+
`SELECT status FROM ${this.s}.accounts WHERE id = $1`,
|
|
261112
|
+
[toAccountId]
|
|
261113
|
+
);
|
|
261114
|
+
if (acc.rows[0] && acc.rows[0].status !== "active") {
|
|
261115
|
+
await client.query("ROLLBACK");
|
|
261116
|
+
return { ok: false, reason: "target_inactive" };
|
|
261117
|
+
}
|
|
261118
|
+
const others = rows.filter((m2) => m2.role === "owner" && m2.accountId !== toAccountId);
|
|
261119
|
+
if (others.length === 0 && target.role === "owner") {
|
|
261120
|
+
await client.query("COMMIT");
|
|
261121
|
+
return { ok: true, owner: target, demoted: [] };
|
|
261122
|
+
}
|
|
261123
|
+
if (others.length > 0) {
|
|
261124
|
+
await client.query(
|
|
261125
|
+
`UPDATE ${this.s}.company_members SET role = 'admin', updated_at = now()
|
|
261126
|
+
WHERE company_id = $1 AND role = 'owner' AND account_id <> $2`,
|
|
261127
|
+
[companyId, toAccountId]
|
|
261128
|
+
);
|
|
261129
|
+
}
|
|
261130
|
+
if (target.role !== "owner") {
|
|
261131
|
+
await client.query(
|
|
261132
|
+
`UPDATE ${this.s}.company_members SET role = 'owner', updated_at = now()
|
|
261133
|
+
WHERE company_id = $1 AND account_id = $2`,
|
|
261134
|
+
[companyId, toAccountId]
|
|
261135
|
+
);
|
|
261136
|
+
}
|
|
261137
|
+
await client.query("COMMIT");
|
|
261138
|
+
return {
|
|
261139
|
+
ok: true,
|
|
261140
|
+
owner: { ...target, role: "owner" },
|
|
261141
|
+
demoted: others.map((m2) => ({ ...m2, role: "admin" }))
|
|
261142
|
+
};
|
|
261143
|
+
} catch (e) {
|
|
261144
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261145
|
+
});
|
|
261146
|
+
throw e;
|
|
261147
|
+
} finally {
|
|
261148
|
+
client.release();
|
|
261149
|
+
}
|
|
261150
|
+
}
|
|
261151
|
+
// —— 存量身份映射审计(ADR D3.4)——
|
|
261152
|
+
async appendMemberRoleMigrations(rows) {
|
|
261153
|
+
if (rows.length === 0) return;
|
|
261154
|
+
const client = await this.pool.connect();
|
|
261155
|
+
try {
|
|
261156
|
+
await client.query("BEGIN");
|
|
261157
|
+
for (const r of rows) {
|
|
261158
|
+
await client.query(
|
|
261159
|
+
`INSERT INTO ${this.s}.company_member_role_migrations
|
|
261160
|
+
(company_id, account_id, role_before, role_after, evidence, manifest_version, applied_at)
|
|
261161
|
+
VALUES ($1,$2,$3,$4,$5,$6,$7)`,
|
|
261162
|
+
[r.companyId, r.accountId, r.roleBefore, r.roleAfter, r.evidence, r.manifestVersion ?? null, r.appliedAt]
|
|
261163
|
+
);
|
|
261164
|
+
}
|
|
261165
|
+
await client.query("COMMIT");
|
|
261166
|
+
} catch (e) {
|
|
261167
|
+
await client.query("ROLLBACK").catch(() => {
|
|
261168
|
+
});
|
|
261169
|
+
throw e;
|
|
261170
|
+
} finally {
|
|
261171
|
+
client.release();
|
|
261172
|
+
}
|
|
261173
|
+
}
|
|
261174
|
+
async listMemberRoleMigrations(companyId) {
|
|
261175
|
+
const r = companyId === void 0 ? await this.pool.query(`SELECT * FROM ${this.s}.company_member_role_migrations ORDER BY applied_at, id`) : await this.pool.query(
|
|
261176
|
+
`SELECT * FROM ${this.s}.company_member_role_migrations WHERE company_id = $1 ORDER BY applied_at, id`,
|
|
261177
|
+
[companyId]
|
|
261178
|
+
);
|
|
261179
|
+
return r.rows.map(rowToRoleMigration);
|
|
261180
|
+
}
|
|
260368
261181
|
// —— 邀请 ——
|
|
260369
261182
|
async createInvitation(inv) {
|
|
260370
261183
|
await this.pool.query(
|
|
@@ -260834,7 +261647,19 @@ var init_postgres_control_plane = __esm({
|
|
|
260834
261647
|
...row.avatar !== null && row.avatar !== void 0 ? { avatar: row.avatar } : {},
|
|
260835
261648
|
status: row.status,
|
|
260836
261649
|
createdAt: new Date(row.created_at).toISOString(),
|
|
260837
|
-
...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {}
|
|
261650
|
+
...row.onboarding_variant ? { onboardingVariant: row.onboarding_variant } : {},
|
|
261651
|
+
// 解散痕迹(组织三层身份 ADR D9.2):两列一起有、一起空。普通运维停用不写它们。
|
|
261652
|
+
...row.dissolved_at != null ? { dissolvedAt: toIso(row.dissolved_at) } : {},
|
|
261653
|
+
...row.dissolved_by != null ? { dissolvedBy: row.dissolved_by } : {}
|
|
261654
|
+
});
|
|
261655
|
+
rowToRoleMigration = (row) => ({
|
|
261656
|
+
companyId: row.company_id,
|
|
261657
|
+
accountId: row.account_id,
|
|
261658
|
+
roleBefore: row.role_before,
|
|
261659
|
+
roleAfter: row.role_after,
|
|
261660
|
+
evidence: row.evidence,
|
|
261661
|
+
...row.manifest_version != null ? { manifestVersion: row.manifest_version } : {},
|
|
261662
|
+
appliedAt: toIso(row.applied_at)
|
|
260838
261663
|
});
|
|
260839
261664
|
rowToMember = (row) => ({
|
|
260840
261665
|
companyId: row.company_id,
|
|
@@ -276559,6 +277384,7 @@ __export(src_exports2, {
|
|
|
276559
277384
|
ACTOR_DELETED_CHAT_MESSAGE: () => ACTOR_DELETED_CHAT_MESSAGE,
|
|
276560
277385
|
ACTOR_DELETED_ERROR_CODE: () => ACTOR_DELETED_ERROR_CODE,
|
|
276561
277386
|
ASSISTANT_ARCHIVED_CODE: () => ASSISTANT_ARCHIVED_CODE,
|
|
277387
|
+
ASSISTANT_POSITION: () => ASSISTANT_POSITION,
|
|
276562
277388
|
ASSISTANT_TIER_RULES: () => ASSISTANT_TIER_RULES,
|
|
276563
277389
|
AUTOMATION_TEMPLATES: () => AUTOMATION_TEMPLATES,
|
|
276564
277390
|
AUTONOMOUS_ETHOS: () => AUTONOMOUS_ETHOS,
|
|
@@ -276566,6 +277392,7 @@ __export(src_exports2, {
|
|
|
276566
277392
|
ActorMemoryService: () => ActorMemoryService,
|
|
276567
277393
|
ActorsService: () => ActorsService,
|
|
276568
277394
|
AgentCredentialService: () => AgentCredentialService,
|
|
277395
|
+
AgentManageForbiddenError: () => AgentManageForbiddenError,
|
|
276569
277396
|
ApiError: () => ApiError,
|
|
276570
277397
|
AssistantLazyCreateError: () => AssistantLazyCreateError,
|
|
276571
277398
|
AssistantsService: () => AssistantsService,
|
|
@@ -276703,6 +277530,7 @@ __export(src_exports2, {
|
|
|
276703
277530
|
OPERATOR_SCOPE: () => OPERATOR_SCOPE,
|
|
276704
277531
|
ORPHANED_TURN_ASSISTANT_NOTICE: () => ORPHANED_TURN_ASSISTANT_NOTICE,
|
|
276705
277532
|
OUTPUT_COPY_LIMITS: () => OUTPUT_COPY_LIMITS,
|
|
277533
|
+
OWNER_PROTECTED_MESSAGE: () => OWNER_PROTECTED_MESSAGE,
|
|
276706
277534
|
PARENT_CONVERSATION_PATH: () => PARENT_CONVERSATION_PATH,
|
|
276707
277535
|
PARENT_CONVERSATION_RECENT_TURNS: () => PARENT_CONVERSATION_RECENT_TURNS,
|
|
276708
277536
|
PostgresNotificationStore: () => PostgresNotificationStore,
|
|
@@ -276757,11 +277585,16 @@ __export(src_exports2, {
|
|
|
276757
277585
|
applyWorkspaceBundle: () => applyWorkspaceBundle,
|
|
276758
277586
|
assembleDelegationsDomain: () => assembleDelegationsDomain,
|
|
276759
277587
|
assertBindingWritePermission: () => assertBindingWritePermission,
|
|
277588
|
+
assertCompanyManager: () => assertCompanyManager,
|
|
276760
277589
|
assertCreatableVariable: () => assertCreatableVariable,
|
|
277590
|
+
assertMayCreateAgent: () => assertMayCreateAgent,
|
|
277591
|
+
assertMayManageAgent: () => assertMayManageAgent,
|
|
277592
|
+
assertMaySelfOrManage: () => assertMaySelfOrManage,
|
|
276761
277593
|
assistantRowRecovery: () => assistantRowRecovery,
|
|
276762
277594
|
assistantsDomain: () => assistantsDomain,
|
|
276763
277595
|
atCapacity: () => atCapacity,
|
|
276764
277596
|
automationsDomain: () => automationsDomain,
|
|
277597
|
+
backfillOrgIdentity: () => backfillOrgIdentity,
|
|
276765
277598
|
backfillOrgModel: () => backfillOrgModel,
|
|
276766
277599
|
backfillOrgModelOnStartup: () => backfillOrgModelOnStartup,
|
|
276767
277600
|
backfillResolvedLetterReadMarkers: () => backfillResolvedLetterReadMarkers,
|
|
@@ -276934,8 +277767,11 @@ __export(src_exports2, {
|
|
|
276934
277767
|
hideDeleted: () => hideDeleted,
|
|
276935
277768
|
humanSubmitSourceRunId: () => humanSubmitSourceRunId,
|
|
276936
277769
|
idlenessOf: () => idlenessOf,
|
|
277770
|
+
inventoryOrgIdentity: () => inventoryOrgIdentity,
|
|
276937
277771
|
isActiveCoordinatorAgent: () => isActiveCoordinatorAgent,
|
|
277772
|
+
isAssistantAgent: () => isAssistantAgent,
|
|
276938
277773
|
isCompanyCrossTenantPath: () => isCompanyCrossTenantPath,
|
|
277774
|
+
isCompanyManager: () => isCompanyManager,
|
|
276939
277775
|
isCoordinatorAgentName: () => isCoordinatorAgentName,
|
|
276940
277776
|
isCoordinatorRunReader: () => isCoordinatorRunReader,
|
|
276941
277777
|
isCredentialAuditKind: () => isCredentialAuditKind,
|
|
@@ -276995,6 +277831,7 @@ __export(src_exports2, {
|
|
|
276995
277831
|
matchedKnowledgeQueryTerms: () => matchedKnowledgeQueryTerms,
|
|
276996
277832
|
materializeBuiltinSkills: () => materializeBuiltinSkills,
|
|
276997
277833
|
materializeSkillFiles: () => materializeSkillFiles,
|
|
277834
|
+
mayManageAgent: () => mayManageAgent,
|
|
276998
277835
|
mayManageWorkorder: () => mayManageWorkorder,
|
|
276999
277836
|
mayReassignProjectItem: () => mayReassignProjectItem,
|
|
277000
277837
|
memberNameResolver: () => memberNameResolver,
|
|
@@ -277109,6 +277946,7 @@ __export(src_exports2, {
|
|
|
277109
277946
|
retireCdGateInTypeDef: () => retireCdGateInTypeDef,
|
|
277110
277947
|
retireCdGateInTypeStore: () => retireCdGateInTypeStore,
|
|
277111
277948
|
rolesDomain: () => rolesDomain,
|
|
277949
|
+
rollbackOrgIdentity: () => rollbackOrgIdentity,
|
|
277112
277950
|
routeFusedSignal: () => routeFusedSignal,
|
|
277113
277951
|
runtimeConnectionStatus: () => runtimeConnectionStatus,
|
|
277114
277952
|
runtimeDriftBlock: () => runtimeDriftBlock,
|
|
@@ -277200,6 +278038,7 @@ var init_src11 = __esm({
|
|
|
277200
278038
|
init_outputs_scan();
|
|
277201
278039
|
init_outputs_copy();
|
|
277202
278040
|
init_org_model_backfill();
|
|
278041
|
+
init_org_identity_migration();
|
|
277203
278042
|
init_types_admin();
|
|
277204
278043
|
init_roles_admin();
|
|
277205
278044
|
init_roles_seed();
|
|
@@ -277311,6 +278150,7 @@ var init_src11 = __esm({
|
|
|
277311
278150
|
init_chat_builtin_skills();
|
|
277312
278151
|
init_connector_skills();
|
|
277313
278152
|
init_connector_tool_versions();
|
|
278153
|
+
init_agent_manage_guard();
|
|
277314
278154
|
init_board_watch();
|
|
277315
278155
|
init_uncategorized_project();
|
|
277316
278156
|
init_run_settlement();
|
|
@@ -280989,6 +281829,28 @@ async function startServe(opts) {
|
|
|
280989
281829
|
const role = members.find((m2) => m2.actorId === actorId)?.role;
|
|
280990
281830
|
return role === "owner" || role === "admin" || role === "member" ? role : null;
|
|
280991
281831
|
},
|
|
281832
|
+
/* Agent 写面的行政权闸(组织三层身份 ADR D6,判据在 `governance/agent-manage-guard.ts`)。
|
|
281833
|
+
三条读取面都**每请求现查**、都按 `(组织, 账号)` 双键——禁止把角色塞进任何跨请求缓存
|
|
281834
|
+
(D7.1:`agentPrincipalByToken` 只缓存 `{companyId, actorId}`,不许顺手加 role)。
|
|
281835
|
+
`controlPlaneStore` / `getEngine` / `resolveAssistantsCtx` 在下方才就绪;这三个箭头只在
|
|
281836
|
+
请求时跑(同 `projectExists` 的惰性模式)。 */
|
|
281837
|
+
agentManageGuard: {
|
|
281838
|
+
callerRole: async (companyId, callerActorId) => {
|
|
281839
|
+
const cid = companyId ?? defaultCompanyId;
|
|
281840
|
+
const members = await controlPlaneStore.listMembers(cid);
|
|
281841
|
+
const row = members.find((m2) => m2.actorId === callerActorId || m2.accountId === callerActorId);
|
|
281842
|
+
return row?.role;
|
|
281843
|
+
},
|
|
281844
|
+
boundHuman: async (companyId, agentId) => {
|
|
281845
|
+
const ctx = await resolveAssistantsCtxLazy(companyId);
|
|
281846
|
+
if (!ctx) return null;
|
|
281847
|
+
return (await ctx.service.getByAgent(agentId))?.humanActorId ?? null;
|
|
281848
|
+
},
|
|
281849
|
+
reportsTo: async (companyId, humanActorId) => {
|
|
281850
|
+
const engine2 = await getEngine(companyId ?? defaultCompanyId);
|
|
281851
|
+
return (await engine2.registry.getActor(humanActorId))?.reportsTo ?? null;
|
|
281852
|
+
}
|
|
281853
|
+
},
|
|
280992
281854
|
// CO-302 数据面隔离(actors 域试点):按当前公司取其引擎,用各自 registry 服务该域请求。
|
|
280993
281855
|
// engineRouter 在下方(行 ~411)以 const 声明;此箭头只在请求时调用,那时已初始化,闭包引用合法
|
|
280994
281856
|
// (TDZ 只在构造时访问才报错,这里不访问)。默认公司命中现有单实例(registry===registryStore),
|
|
@@ -281051,6 +281913,12 @@ async function startServe(opts) {
|
|
|
281051
281913
|
isPlatformAdmin: async () => false
|
|
281052
281914
|
// 占位;controlPlaneStore 就绪后由下方 setPlatformAdminChecker 注入真判据
|
|
281053
281915
|
});
|
|
281916
|
+
let resolveAssistantsCtxImpl = null;
|
|
281917
|
+
const resolveAssistantsCtx = async (companyId) => {
|
|
281918
|
+
if (!resolveAssistantsCtxImpl) throw new Error("assistants ctx resolver \u5C1A\u672A\u88C5\u914D\uFF08\u542F\u52A8\u671F\u4E0D\u5E94\u6709\u8BF7\u6C42\uFF09");
|
|
281919
|
+
return resolveAssistantsCtxImpl(companyId);
|
|
281920
|
+
};
|
|
281921
|
+
const resolveAssistantsCtxLazy = async (companyId) => resolveAssistantsCtxImpl ? resolveAssistantsCtxImpl(companyId) : null;
|
|
281054
281922
|
let projectStateStore;
|
|
281055
281923
|
let artifactStateStore;
|
|
281056
281924
|
let chatSessionStore;
|
|
@@ -281780,8 +282648,7 @@ async function startServe(opts) {
|
|
|
281780
282648
|
const controlPlaneStore = pgDsn && pgPool ? await PostgresControlPlaneStore.open(pgPool, sharedSchema) : await FileControlPlaneStore.open(path30.join(opts.dir, "control-plane.json"));
|
|
281781
282649
|
assistantsService.setPlatformAdminChecker(async (caller) => {
|
|
281782
282650
|
const member = await controlPlaneStore.getMember(defaultCompanyId, caller);
|
|
281783
|
-
|
|
281784
|
-
return member.role === "owner";
|
|
282651
|
+
return member?.role === "owner" || member?.role === "admin";
|
|
281785
282652
|
});
|
|
281786
282653
|
if (pgDsn && pgPool) {
|
|
281787
282654
|
const priceStore = await PostgresModelPriceStore.open(pgPool, sharedSchema);
|
|
@@ -281879,16 +282746,29 @@ async function startServe(opts) {
|
|
|
281879
282746
|
}
|
|
281880
282747
|
}
|
|
281881
282748
|
if (!await controlPlaneStore.getMember(defaultCompanyId, operatorActor)) {
|
|
281882
|
-
await controlPlaneStore.
|
|
282749
|
+
const hasOwner = (await controlPlaneStore.listMembers(defaultCompanyId)).some((m2) => m2.role === "owner");
|
|
282750
|
+
await controlPlaneStore.upsertMember({
|
|
282751
|
+
companyId: defaultCompanyId,
|
|
282752
|
+
accountId: operatorActor,
|
|
282753
|
+
actorId: operatorActor,
|
|
282754
|
+
role: hasOwner ? "admin" : "owner",
|
|
282755
|
+
onboardingCompletedAt: seededAt
|
|
282756
|
+
});
|
|
282757
|
+
if (hasOwner) {
|
|
282758
|
+
console.log(`[serve] \u9ED8\u8BA4\u516C\u53F8\u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u8FD0\u7EF4\u8005 ${operatorActor} \u4EE5\u300C\u7BA1\u7406\u5458\u300D\u52A0\u5165\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09`);
|
|
282759
|
+
}
|
|
281883
282760
|
}
|
|
281884
282761
|
}
|
|
281885
282762
|
const bootstrapEmail = process.env["OASIS_BOOTSTRAP_OWNER_EMAIL"];
|
|
281886
282763
|
if (bootstrapEmail) {
|
|
281887
|
-
const
|
|
281888
|
-
|
|
282764
|
+
const res = await companies.service.grantOwner(defaultCompanyId, bootstrapEmail);
|
|
282765
|
+
const bootstrapAccount = await controlPlaneStore.getAccountByEmail(bootstrapEmail.trim().toLowerCase());
|
|
282766
|
+
console.log(
|
|
282767
|
+
res.granted ? `[serve] bootstrap owner\uFF1A${bootstrapEmail} \u2192 ${res.membership.accountId}\uFF08\u9ED8\u8BA4\u516C\u53F8 owner\uFF09` : `[serve] bootstrap owner \u8DF3\u8FC7\uFF1A\u9ED8\u8BA4\u516C\u53F8\u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${res.membership.accountId}\uFF08\u6362\u4EBA\u8D70\u8F6C\u4EA4\uFF09`
|
|
282768
|
+
);
|
|
281889
282769
|
const bootstrapPw = process.env["OASIS_BOOTSTRAP_OWNER_PASSWORD"];
|
|
281890
|
-
if (bootstrapPw) {
|
|
281891
|
-
await controlPlaneStore.setAccountPassword(
|
|
282770
|
+
if (bootstrapPw && bootstrapAccount) {
|
|
282771
|
+
await controlPlaneStore.setAccountPassword(bootstrapAccount.id, hashPassword(bootstrapPw));
|
|
281892
282772
|
console.log(`[serve] bootstrap owner \u5BC6\u7801\u5DF2\u8BBE\uFF08OASIS_BOOTSTRAP_OWNER_PASSWORD\uFF09`);
|
|
281893
282773
|
}
|
|
281894
282774
|
}
|
|
@@ -282060,6 +282940,36 @@ async function startServe(opts) {
|
|
|
282060
282940
|
planeInflight.set(companyId, building);
|
|
282061
282941
|
return building;
|
|
282062
282942
|
};
|
|
282943
|
+
resolveAssistantsCtxImpl = async (companyId) => {
|
|
282944
|
+
const cid = companyId ?? defaultCompanyId;
|
|
282945
|
+
const ctx = await actors.resolveCtx(cid);
|
|
282946
|
+
const engine2 = await getEngine(cid);
|
|
282947
|
+
const primary = Boolean(
|
|
282948
|
+
pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
|
|
282949
|
+
);
|
|
282950
|
+
const plane = primary ? await openCompanyPlane(cid) : void 0;
|
|
282951
|
+
return {
|
|
282952
|
+
service: new AssistantsService({
|
|
282953
|
+
bindStore: plane?.assistants ?? assistantBindStore,
|
|
282954
|
+
prefsStore: plane?.humanPrefs ?? humanPrefsStore,
|
|
282955
|
+
registry: engine2.registry,
|
|
282956
|
+
actors: ctx.service,
|
|
282957
|
+
// 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
|
|
282958
|
+
companyId: () => cid,
|
|
282959
|
+
listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
|
|
282960
|
+
// owner | admin 同档(组织三层身份 ADR D6.4):只认 owner 会让「管理员」失去今天
|
|
282961
|
+
// 「组织管理者」本就有的助理管理能力——那是本次改动造成的能力回归,不是新规则。
|
|
282962
|
+
isPlatformAdmin: async (caller) => {
|
|
282963
|
+
const member = await controlPlaneStore.getMember(cid, caller);
|
|
282964
|
+
return member?.role === "owner" || member?.role === "admin";
|
|
282965
|
+
},
|
|
282966
|
+
// 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
|
|
282967
|
+
// (新模型下它恰好返回一个元素,`ownerAssistantRuntime` 的「取第一个有着落的」语义不变)。
|
|
282968
|
+
listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
|
|
282969
|
+
}),
|
|
282970
|
+
getActor: (id) => engine2.registry.getActor(id)
|
|
282971
|
+
};
|
|
282972
|
+
};
|
|
282063
282973
|
const openCompanyPlaneUncached = async (companyId) => {
|
|
282064
282974
|
if (!pgPool || !locationStore || !cutoverStore) {
|
|
282065
282975
|
throw new Error("openCompanyPlane \u9700\u8981 Postgres");
|
|
@@ -283121,34 +284031,10 @@ async function startServe(opts) {
|
|
|
283121
284031
|
assistantsDomain({
|
|
283122
284032
|
service: assistantsService,
|
|
283123
284033
|
getActor: (id) => registryStore.getActor(id),
|
|
283124
|
-
|
|
283125
|
-
|
|
283126
|
-
|
|
283127
|
-
|
|
283128
|
-
const primary = Boolean(
|
|
283129
|
-
pgPool && cutoverStore && cutoverStore.readsCompanySchema(await cutoverStore.get(cid, "assistant"))
|
|
283130
|
-
);
|
|
283131
|
-
const plane = primary ? await openCompanyPlane(cid) : void 0;
|
|
283132
|
-
return {
|
|
283133
|
-
service: new AssistantsService({
|
|
283134
|
-
bindStore: plane?.assistants ?? assistantBindStore,
|
|
283135
|
-
prefsStore: plane?.humanPrefs ?? humanPrefsStore,
|
|
283136
|
-
registry: engine2.registry,
|
|
283137
|
-
actors: ctx.service,
|
|
283138
|
-
// 这一份服务的是**请求方的公司**——助理只在这家的在线 runtime 里挑机器(ADR-0164 §5)。
|
|
283139
|
-
companyId: () => cid,
|
|
283140
|
-
listRuntimes: (nodeId, scope2) => nodeStore.listRuntimes(nodeId, scope2),
|
|
283141
|
-
isPlatformAdmin: async (caller) => {
|
|
283142
|
-
const member = await controlPlaneStore.getMember(cid, caller);
|
|
283143
|
-
return member?.role === "owner";
|
|
283144
|
-
},
|
|
283145
|
-
// 「创建者的机器」要先知道谁是创建者:本公司的 owner 成员,保持 listMembers 的加入先后
|
|
283146
|
-
// (第一个 owner = 建团队的人;三种 store 都按加入先后返回,见控制面一致性套件)。
|
|
283147
|
-
listOwnerIds: async () => (await controlPlaneStore.listMembers(cid)).filter((m2) => m2.role === "owner").map((m2) => m2.actorId)
|
|
283148
|
-
}),
|
|
283149
|
-
getActor: (id) => engine2.registry.getActor(id)
|
|
283150
|
-
};
|
|
283151
|
-
}
|
|
284034
|
+
// 与 actors 域的 agentManageGuard.boundHuman **同一份**:「这个 agent 绑给了哪个真人」
|
|
284035
|
+
// 两处各解析一次助理 store 必然漂,而漂出来的差异正好是「删助理」与「解绑助理」两条路
|
|
284036
|
+
// 的闸又对不上(G4 的形状)。
|
|
284037
|
+
resolveService: resolveAssistantsCtx
|
|
283152
284038
|
}),
|
|
283153
284039
|
companies.register,
|
|
283154
284040
|
projectsDomain2.register,
|
|
@@ -292604,7 +293490,7 @@ async function waitForCommandAuthorization(base, headers, draftId, fallback) {
|
|
|
292604
293490
|
}
|
|
292605
293491
|
}
|
|
292606
293492
|
}
|
|
292607
|
-
var CLI_VERSION = "2.2.
|
|
293493
|
+
var CLI_VERSION = "2.2.81" ? "2.2.81" : "dev";
|
|
292608
293494
|
function apiHeaders(token, company) {
|
|
292609
293495
|
const headers = {
|
|
292610
293496
|
authorization: `Bearer ${token}`,
|
|
@@ -293001,8 +293887,19 @@ async function runCli(argv, println = console.log, progressln = console.error) {
|
|
|
293001
293887
|
const name = flags.get("name");
|
|
293002
293888
|
if (!await store.getCompany(companyId)) throw new Error(`\u516C\u53F8\u4E0D\u5B58\u5728\uFF1A${companyId}\uFF08\u5148\u8BA9 serve \u8DD1\u4E00\u6B21\u4EE5\u5EFA\u9ED8\u8BA4\u516C\u53F8\uFF0C\u6216\u7528 --company \u6307\u5B9A\u5DF2\u5B58\u5728\u516C\u53F8\uFF09`);
|
|
293003
293889
|
const svc = new CompaniesService({ store });
|
|
293004
|
-
const
|
|
293005
|
-
|
|
293890
|
+
const res = await svc.grantOwner(companyId, email2, name);
|
|
293891
|
+
if (!res.granted && res.existingOwner) {
|
|
293892
|
+
process.stderr.write(
|
|
293893
|
+
`\u516C\u53F8 ${companyId} \u5DF2\u6709\u7EC4\u7EC7\u7BA1\u7406\u5458 ${res.existingOwner.accountId}\uFF0C\u672A\u6388\u4E88 ${email2}\uFF1B\u4ED6\u73B0\u5728\u662F\u672C\u7EC4\u7EC7\u7684\u300C${res.membership.role === "admin" ? "\u7BA1\u7406\u5458" : res.membership.role}\u300D\uFF08\u7BA1\u7406\u9762\u540C\u6837\u53EF\u8FDB\uFF09\u3002
|
|
293894
|
+
\u6362\u4EBA\u8BF7\u8BA9\u73B0\u4EFB\u6301\u6709\u4EBA\u8C03 POST /api/companies/${companyId}/owner\uFF08\u8F6C\u4EA4\uFF09\u3002
|
|
293895
|
+
`
|
|
293896
|
+
);
|
|
293897
|
+
process.exitCode = 1;
|
|
293898
|
+
return;
|
|
293899
|
+
}
|
|
293900
|
+
println(
|
|
293901
|
+
res.granted ? `\u5DF2\u6388 owner\uFF1A${email2} \u2192 ${res.membership.accountId}\uFF08\u516C\u53F8 ${companyId}\uFF09` : `${email2} \u5DF2\u662F\u516C\u53F8 ${companyId} \u7684\u7EC4\u7EC7\u7BA1\u7406\u5458\uFF0C\u65E0\u9700\u6539\u52A8\uFF08\u5E42\u7B49\uFF09`
|
|
293902
|
+
);
|
|
293006
293903
|
process.stderr.write("\u63D0\u793A\uFF1A\u82E5 serve \u6B63\u5728\u8FD0\u884C\uFF0C\u63A7\u5236\u9762\u6587\u4EF6\u65E0\u9501\uFF0C\u5EFA\u8BAE\u505C\u6B62 serve \u540E\u518D\u6267\u884C\u4EE5\u514D\u5E76\u53D1\u5199\u3002\n");
|
|
293007
293904
|
return;
|
|
293008
293905
|
}
|
|
@@ -296503,7 +297400,7 @@ function shimScript() {
|
|
|
296503
297400
|
}
|
|
296504
297401
|
|
|
296505
297402
|
// src/index.ts
|
|
296506
|
-
var PKG_VERSION = true ? "2.2.
|
|
297403
|
+
var PKG_VERSION = true ? "2.2.81" : "dev";
|
|
296507
297404
|
var LOCAL_BIN = localBin();
|
|
296508
297405
|
var NPM_PREFIX = npmPrefix();
|
|
296509
297406
|
var INSTANCE = DEFAULT_INSTANCE;
|