现状
#4258(68dea0b,closes #4243 / #4236)把 sys_migration 的注册从 service-storage 挪到了 PlatformObjectsPlugin,确立了「平台系统对象由平台层注册」这个落脚点。同一形状的问题在 sys_secret 上还在,而且更尖锐。
对象定义住在 @objectstack/platform-objects/src/system/sys-secret.object.ts,注册权却在 settings 服务手里:
// packages/services/service-settings/src/manifest.ts:9
/** Objects owned by service-settings. Currently just the K/V store. */
export const settingsObjects: any[] = [SysSetting, SysSecret, SysSettingAudit];
为什么这次比 #4243 更该改
一、sys_secret 有三类生产者,代码里已经写明了,只有一类是 settings。 serve.ts:2295-2298 的注释自己点了名:
Credentials are bound through the SAME crypto provider used for secret fields below (sharedCryptoProvider), so every secret in sys_secret (settings, secret fields, datasource creds) shares one key.
三者对应:
SettingsService —— 加密设置项(正当属主);
- ObjectQL 引擎自身 ——
encryptSecretFields()(engine.ts:1662)直接对 driver 写:engine.ts:1708 取 driver、1716 create('sys_secret', …);读侧 resolveSecret()(engine.ts:1768)在 1774 取同一个 driver;
service-datasource —— datasource-secret-binder.ts:93/108/115 对 sys_secret 做 insert/delete/find。
二、引擎的调用点是通用写路径,不是 settings 内部路径。 encryptSecretFields 由 engine.ts:3115(insert)、3417/3443(update)调用,也就是任何业务对象的普通写入。而 secret 是可授权字段类型(Field.secret(),packages/spec/src/data/field.zod.ts:696;object.test.ts:1485 即 api_key: { type: 'secret' })。所以这条路径对任何写 secret 字段的 app 对象都成立,与 settings 无关。
三、失败姿态是 fail-closed,不是 fail-lenient。 这是与 #4243 最大的区别。sys_migration 缺席时门禁静默落回 legacy(安全但原因错了);sys_secret 缺席时直接抛错:
// packages/objectql/src/engine.ts:1706-1714
let secretDriver;
try {
secretDriver = this.getDriver('sys_secret');
} catch {
throw new Error(
`Cannot persist secret field "${object}.${field}": the sys_secret store is not available. `
+ 'Ensure the platform-objects (sys_secret) are registered before writing secret fields (fail-closed).',
);
}
注意这句报错的措辞:Ensure the platform-objects (sys_secret) are registered。引擎已经认为 sys_secret 该由 platform-objects 注册——实际注册者却是 service-settings。错的不只是耦合,连错误信息指认的责任方都是错的:运维照着这句去查 platform-objects 会一无所获。
四、两处头注释对属主的说法互相矛盾。 sys-secret.object.ts:27-28 写:
managedBy: 'engine-owned' — never edited from a generic Object grid. All writes flow through SettingsService and an ICryptoProvider.
但引擎是绕过 SettingsService 直接写 driver 的(engine.ts:1716)。「All writes flow through SettingsService」在 secret 字段这条路径上不成立。
影响面
和 #4243 一样,不是线上事故:os serve 的已知服务表里有 settings(serve.ts:433-436),所以被服务起来的部署总有这张表。
但「没装 settings 的内核」是这份代码明确承认存在的配置——serve.ts:2173-2174 自己写着「hosts without the settings service」。在那类内核上,任何带 secret 字段的对象一旦 insert/update 就硬失败,而失败信息指向一个并不负责注册它的包。
顺带一提,#4258 让 os migrate value-shapes 不再装配 settings。该命令严格只读(--apply 只写 sys_migration 标记本身),且 resolveSecret 是显式 API、普通 find 不触发,所以那条路径现在是安全的——但这恰好说明「不带 settings 的内核」已经从假设变成了仓库里跑着的现实。
建议
把 SysSecret 的注册从 settingsObjects 挪到 PlatformObjectsPlugin(packages/platform-objects/src/plugin.ts),与 #4258 对 SysMigration 的处理一致,让引擎那句报错变成真话。
实现时需要注意的:
关联
现状
#4258(
68dea0b,closes #4243 / #4236)把sys_migration的注册从 service-storage 挪到了PlatformObjectsPlugin,确立了「平台系统对象由平台层注册」这个落脚点。同一形状的问题在sys_secret上还在,而且更尖锐。对象定义住在
@objectstack/platform-objects/src/system/sys-secret.object.ts,注册权却在 settings 服务手里:为什么这次比 #4243 更该改
一、
sys_secret有三类生产者,代码里已经写明了,只有一类是 settings。serve.ts:2295-2298的注释自己点了名:三者对应:
SettingsService—— 加密设置项(正当属主);encryptSecretFields()(engine.ts:1662)直接对 driver 写:engine.ts:1708取 driver、1716create('sys_secret', …);读侧resolveSecret()(engine.ts:1768)在1774取同一个 driver;service-datasource——datasource-secret-binder.ts:93/108/115对sys_secret做 insert/delete/find。二、引擎的调用点是通用写路径,不是 settings 内部路径。
encryptSecretFields由engine.ts:3115(insert)、3417/3443(update)调用,也就是任何业务对象的普通写入。而secret是可授权字段类型(Field.secret(),packages/spec/src/data/field.zod.ts:696;object.test.ts:1485即api_key: { type: 'secret' })。所以这条路径对任何写secret字段的 app 对象都成立,与 settings 无关。三、失败姿态是 fail-closed,不是 fail-lenient。 这是与 #4243 最大的区别。
sys_migration缺席时门禁静默落回 legacy(安全但原因错了);sys_secret缺席时直接抛错:注意这句报错的措辞:
Ensure the platform-objects (sys_secret) are registered。引擎已经认为sys_secret该由 platform-objects 注册——实际注册者却是 service-settings。错的不只是耦合,连错误信息指认的责任方都是错的:运维照着这句去查 platform-objects 会一无所获。四、两处头注释对属主的说法互相矛盾。
sys-secret.object.ts:27-28写:但引擎是绕过
SettingsService直接写 driver 的(engine.ts:1716)。「All writes flow through SettingsService」在secret字段这条路径上不成立。影响面
和 #4243 一样,不是线上事故:
os serve的已知服务表里有 settings(serve.ts:433-436),所以被服务起来的部署总有这张表。但「没装 settings 的内核」是这份代码明确承认存在的配置——
serve.ts:2173-2174自己写着「hosts without the settings service」。在那类内核上,任何带secret字段的对象一旦 insert/update 就硬失败,而失败信息指向一个并不负责注册它的包。顺带一提,#4258 让
os migrate value-shapes不再装配 settings。该命令严格只读(--apply只写sys_migration标记本身),且resolveSecret是显式 API、普通find不触发,所以那条路径现在是安全的——但这恰好说明「不带 settings 的内核」已经从假设变成了仓库里跑着的现实。建议
把
SysSecret的注册从settingsObjects挪到PlatformObjectsPlugin(packages/platform-objects/src/plugin.ts),与 #4258 对SysMigration的处理一致,让引擎那句报错变成真话。实现时需要注意的:
sys_migration由 service-storage 持有,但它是平台级账本 —— 第二个消费者出现后这层耦合该解开 #4236 提醒过要确认「同一对象注册两次」的行为;feat(platform-objects,service-storage,cli): sys_migration 账本改由平台基础设施注册 (#4243) #4258 的做法是把原注册点删掉,只留一处。这里同理——从settingsObjects里摘掉SysSecret,不要两边都注册。settingsObjects是否还被别处当作「settings 拥有的对象集合」使用(权限、manifest 声明、迁移脚本等),摘除后语义是否仍自洽。sys_setting/sys_setting_audit不建议一并搬。 我查过,没找到 settings 服务之外的读者(sys_setting的引用全部落在 platform-objects 的对象定义自身、spec 的名字常量表和 service-settings 内部),settings 服务就是它们的正当属主。这次只动sys_secret。sys-secret.object.ts:28的「All writes flow through SettingsService」,以及manifest.ts:8的「Objects owned by service-settings」。关联
sys_migration由 service-storage 注册——账本是平台基础设施,不该由一个可选服务持有 #4243 /sys_migration由 service-storage 持有,但它是平台级账本 —— 第二个消费者出现后这层耦合该解开 #4236 —— 同一形状的前一例(sys_migration),已由 feat(platform-objects,service-storage,cli): sys_migration 账本改由平台基础设施注册 (#4243) #4258(68dea0b)解决,并确立了PlatformObjectsPlugin作为平台系统对象的注册点。sys_secret作为环境加密密钥库的定位(见sys-secret.object.ts:39-40)。