progmune-runtime 3.7.68 → 3.7.69

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/CHANGELOG.md CHANGED
@@ -1,5 +1,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [3.7.69] — 2026-10-17
4
+
5
+ ### 公开表面传播豁免(修冤枉第十刀)
6
+
7
+ **根因**:@Public()/@SkipAuth()/@AllowAnonymous 是开发者**显式声明**
8
+ 免鉴权(正向证据,R75),但提取器只在 E2 的 skip 方向认识它们
9
+ (「不注入鉴权标记」)——授权族规则照旧提问「缺鉴权」,公开端点
10
+ 自身(docmost 15 个 @Public 方法 17 条告警)与纯公开下游(10 条)
11
+ 全数误报。与第六刀(无请求面)同族,差在面向请求面——公开端点
12
+ 是请求面,只是免鉴权。
13
+
14
+ - **提取器**:类级/方法级 @Public/@SkipAuth/@AllowAnonymous ⇒ 注入
15
+ `__progmune_public_surface__`(与 E2/第六刀并列;类级作用于全方法)。
16
+ ⚠ 注入名只收三个 NestJS 惯例装饰器,与 E2 的 AUTH_SKIP_DECORATOR
17
+ **不对称是故意的**:skip 方向多收(IsPublic/NoAuth/ApiExcludeEndpoint)
18
+ 是防「公开判成有鉴权」,注入方向少收是防「受限判成公开」——两个
19
+ 方向的错判都是假阴性;@ApiExcludeEndpoint 是 Swagger 文档语义,
20
+ 绝不注入
21
+ - **规则侧(Part A,第六刀同架构)**:授权族四条(Access/Mutation/
22
+ Ownership/Resource Ownership)safeguard 接受标记——公开端点自身的
23
+ 告警直接消除(标记本身就是证据,与 no_request_surface 并列)
24
+ - **传播(Part B)**:computePublicSurfacePropagatedSet——F 的每个
25
+ 直接调用方 ∈ {公开面} ∪ {已守卫} ∪ {作业面} ⇒ 豁免;R1 调用方非空
26
+ / R2 边全解析 / R3 并集外照报 / R4 默认 1 轮 / R5 返回只含新增成员。
27
+ 引擎侧 suppressed 带 `suppressedByPublicSurfacePropagation` 标记(R9),
28
+ `PROGMUNE_PUBLIC_SURFACE_TOGGLE=off` 逃生门;并集含守卫∪作业 ⇒ 是
29
+ 第九刀超集,消费顺序 guard→job→mixed→public 保证不重复挂标记
30
+ - **三向 A/B(docmost,各 fresh IR)**:①control(传播关)vs 3.7.68
31
+ 基线 = **精确 −17 条 Part A**(全落 12 个 @Public 方法,新增 0,
32
+ suppressed 156=156 不变);②pre vs control = **精确 −10 条 Part B**
33
+ (全带 public 标记,suppressed 156→166);③主路径 violations
34
+ 11=11 / score 84=84 三臂一致。告警流累计 992→…→685→669→**642**
35
+ - **FN 风险逐条复核**:10 条传播候选全核调用方——搜索/分享/公开空间
36
+ 服务只被 @UseGuards(JwtAuthGuard) 类级守卫路由或 @Public 路由调用
37
+ (search.controller.ts 类级守卫 + 方法级 @Public 并存形态),TP 0;
38
+ 公开端点自身由装饰器显式声明
39
+ - **回归锁定**:提取器 5 例 + 规则侧 5 例 + 传播 10 例 + 引擎产品
40
+ 路径 2;全量 541/541(首跑 1 失败为 worker 超时环境噪音,复跑全绿);
41
+ 三闸门(taintpath 160 / webshape 72 / fr-007 pre5-post0 + fr-016
42
+ pre7-post0);TS 盲测 119 项目 LOST 0/ADDED 0(generated 语料无
43
+ @Public 形状,零漂移由构造保证 R23 家族)
44
+
3
45
  ## [3.7.68] — 2026-10-16
4
46
 
5
47
  ### 调用方混合并集豁免(修冤枉第九刀)
@@ -0,0 +1,152 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const vitest_1 = require("vitest");
37
+ const fs = __importStar(require("fs"));
38
+ const os = __importStar(require("os"));
39
+ const path = __importStar(require("path"));
40
+ const extract_ir_1 = require("./extract-ir");
41
+ /**
42
+ * 修冤枉第十刀(2026-10-16):公开设计端点 → 公开面标记锁定。
43
+ *
44
+ * 数据:docmost 15 个 @Public() 方法自身 17 条授权族告警全 FP——
45
+ * 公开是开发者显式声明(正向证据,R75),授权族「缺鉴权」提问不适用。
46
+ *
47
+ * 注入形态:类级/方法级 @Public/@SkipAuth/@AllowAnonymous(NestJS 惯例)。
48
+ * 反例(不注入):@ApiExcludeEndpoint 是 Swagger 文档语义;
49
+ * 与 AUTH_SKIP_DECORATOR 不对称是故意的(两个方向的错判都是假阴性)。
50
+ */
51
+ const MARK = "__progmune_public_surface__";
52
+ const TSCONFIG = JSON.stringify({
53
+ compilerOptions: { target: "ES2020", module: "commonjs", strict: false, skipLibCheck: true, noEmit: true },
54
+ include: ["**/*.ts"],
55
+ });
56
+ function makeProject(files) {
57
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pm-publicsurface-"));
58
+ fs.writeFileSync(path.join(dir, "tsconfig.json"), TSCONFIG);
59
+ fs.writeFileSync(path.join(dir, "package.json"), JSON.stringify({ name: "fixture", version: "0.0.0", private: true }));
60
+ for (const [rel, body] of Object.entries(files)) {
61
+ const abs = path.join(dir, rel);
62
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
63
+ fs.writeFileSync(abs, body);
64
+ }
65
+ return dir;
66
+ }
67
+ function callsOf(dir, fnName) {
68
+ const fns = (0, extract_ir_1.extractIR)(dir);
69
+ const f = fns.find((x) => x.name === fnName);
70
+ return f ? (f.calls ?? []) : [];
71
+ }
72
+ (0, vitest_1.describe)("extractIR:公开面标记(修冤枉第十刀)", () => {
73
+ (0, vitest_1.it)("@Public() 类级 ⇒ 全部方法注入", () => {
74
+ const dir = makeProject({
75
+ "a.ts": `
76
+ declare const Public: any; declare const Controller: any; declare const Get: any;
77
+ @Public()
78
+ @Controller("public-space")
79
+ export class PublicSpaceController {
80
+ @Get("info")
81
+ getInfo() { return 1; }
82
+ getTree() { return 2; }
83
+ }
84
+ `,
85
+ });
86
+ (0, vitest_1.expect)(callsOf(dir, "PublicSpaceController.getInfo")).toContain(MARK);
87
+ (0, vitest_1.expect)(callsOf(dir, "PublicSpaceController.getTree")).toContain(MARK);
88
+ });
89
+ (0, vitest_1.it)("@Public/@SkipAuth/@AllowAnonymous 方法级 ⇒ 仅该方法注入", () => {
90
+ const dir = makeProject({
91
+ "a.ts": `
92
+ declare const Public: any; declare const SkipAuth: any; declare const AllowAnonymous: any;
93
+ export class ShareController {
94
+ @Public()
95
+ getShare() {}
96
+ @SkipAuth()
97
+ getSharedPageInfo() {}
98
+ @AllowAnonymous()
99
+ transclusionLookup() {}
100
+ getPrivateShare() {}
101
+ }
102
+ `,
103
+ });
104
+ for (const fn of ["ShareController.getShare", "ShareController.getSharedPageInfo", "ShareController.transclusionLookup"]) {
105
+ (0, vitest_1.expect)(callsOf(dir, fn)).toContain(MARK);
106
+ }
107
+ (0, vitest_1.expect)(callsOf(dir, "ShareController.getPrivateShare")).not.toContain(MARK);
108
+ });
109
+ (0, vitest_1.it)("反例:@ApiExcludeEndpoint 是 Swagger 文档语义,不注入", () => {
110
+ const dir = makeProject({
111
+ "a.ts": `
112
+ declare const ApiExcludeEndpoint: any;
113
+ export class DocController {
114
+ @ApiExcludeEndpoint()
115
+ hidden() {}
116
+ }
117
+ `,
118
+ });
119
+ (0, vitest_1.expect)(callsOf(dir, "DocController.hidden")).not.toContain(MARK);
120
+ });
121
+ (0, vitest_1.it)("@UseGuards + @Public 并存 ⇒ public 注入、auth 不注入(显式公开优先)", () => {
122
+ const dir = makeProject({
123
+ "a.ts": `
124
+ declare const UseGuards: any; declare const Public: any;
125
+ declare class JwtAuthGuard {}
126
+ export class C {
127
+ @UseGuards(JwtAuthGuard)
128
+ @Public()
129
+ m() { return 1; }
130
+ }
131
+ `,
132
+ });
133
+ const calls = callsOf(dir, "C.m");
134
+ (0, vitest_1.expect)(calls).toContain(MARK);
135
+ (0, vitest_1.expect)(calls).not.toContain("__progmune_auth_machinery__");
136
+ });
137
+ (0, vitest_1.it)("普通守卫方法 ⇒ 不注入 public、注入 auth(两条通道互不污染)", () => {
138
+ const dir = makeProject({
139
+ "a.ts": `
140
+ declare const UseGuards: any;
141
+ declare class JwtAuthGuard {}
142
+ export class C {
143
+ @UseGuards(JwtAuthGuard)
144
+ m() { return 1; }
145
+ }
146
+ `,
147
+ });
148
+ const calls = callsOf(dir, "C.m");
149
+ (0, vitest_1.expect)(calls).not.toContain(MARK);
150
+ (0, vitest_1.expect)(calls).toContain("__progmune_auth_machinery__");
151
+ });
152
+ });
@@ -500,6 +500,45 @@ function noRequestSurfaceFromDecorators(decorators) {
500
500
  return noSurface ? [NO_REQUEST_SURFACE_MARKER] : [];
501
501
  }
502
502
  // ═══════════════════════════════════════════════════════════════
503
+ // 修冤枉第十刀(2026-10-16):公开设计端点 → 公开面标记
504
+ //
505
+ // 数据:docmost 15 个 @Public() 方法自身 17 条授权族告警 + 下游
506
+ // 纯公开可达 10 条(knife10-public-surface-probe.ts 量化)——公开是
507
+ // 开发者显式声明(正向证据,R75),授权族「缺鉴权」提问不适用。
508
+ //
509
+ // 语义与 E2(auth_machinery)相反:E2 是「有鉴权」,本标记是
510
+ // 「鉴权问题不适用」(与第六刀 no_request_surface 同族,但面向
511
+ // 请求面——公开端点是请求面,只是免鉴权)。
512
+ //
513
+ // 注入名只收三个 NestJS 惯例装饰器:Public / SkipAuth / AllowAnonymous。
514
+ // 与 AUTH_SKIP_DECORATOR(E2 的 skip 方向)不对称是**故意的**:
515
+ // skip 方向多收(IsPublic/NoAuth/ApiExcludeEndpoint)是防「把公开
516
+ // 端点判成有鉴权」的假阴性;注入方向少收是防「把受限端点判成
517
+ // 公开」的假阴性——两个方向的错判都是假阴性,都必须保守。
518
+ // @ApiExcludeEndpoint 是 Swagger 文档语义,绝不注入。
519
+ // ═══════════════════════════════════════════════════════════════
520
+ const PUBLIC_SURFACE_MARKER = "__progmune_public_surface__";
521
+ /** 显式公开装饰器 —— 出现即注入公开面标记(NestJS 惯例) */
522
+ const PUBLIC_SURFACE_DECORATOR = /^(?:Public|SkipAuth|AllowAnonymous)$/i;
523
+ /**
524
+ * 由类/方法装饰器判定是否注入公开面标记(与 E2/第六刀同款文本匹配)。
525
+ * @returns [] 或 [PUBLIC_SURFACE_MARKER]
526
+ */
527
+ function publicSurfaceFromDecorators(decorators) {
528
+ let publicSurface = false;
529
+ for (const d of decorators) {
530
+ const text = (d?.getText?.() ?? "").trim();
531
+ const name = /^@([A-Za-z_$][\w$]*)/.exec(text)?.[1];
532
+ if (!name)
533
+ continue;
534
+ if (PUBLIC_SURFACE_DECORATOR.test(name)) {
535
+ publicSurface = true;
536
+ break;
537
+ }
538
+ }
539
+ return publicSurface ? [PUBLIC_SURFACE_MARKER] : [];
540
+ }
541
+ // ═══════════════════════════════════════════════════════════════
503
542
  // E3(2026-09-22):TS 侧「入参已按 schema 校验」语义标记
504
543
  //
505
544
  // 判据(先抄准再写 expect,R22):
@@ -3303,6 +3342,15 @@ function _extractSingleProject(absRoot, tsconfigPath) {
3303
3342
  if (!mCalls.includes(mk))
3304
3343
  mCalls.push(mk);
3305
3344
  }
3345
+ // 修冤枉第十刀:@Public()/@SkipAuth()/@AllowAnonymous → 公开面标记
3346
+ // (与 E2/第六刀并列;类级公开装饰器作用于全部方法)
3347
+ for (const mk of publicSurfaceFromDecorators([
3348
+ ...(cls.getDecorators?.() ?? []),
3349
+ ...(m.getDecorators?.() ?? []),
3350
+ ])) {
3351
+ if (!mCalls.includes(mk))
3352
+ mCalls.push(mk);
3353
+ }
3306
3354
  funcs.push({
3307
3355
  name: `${cn}.${mn}`,
3308
3356
  params: mParams.map((p) => ({ name: p.getName(), type: getParamType(p), typeDetail: getParamTypeDetail(p) })),
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const protocol_detector_1 = require("./protocol-detector");
5
+ /**
6
+ * 修冤枉第十刀(2026-10-16):公开面标记的规则侧接受位锁定。
7
+ *
8
+ * 语义:@Public()/@SkipAuth()/@AllowAnonymous 是开发者显式声明免鉴权
9
+ * (正向证据,R75)——授权族四条的「缺鉴权/缺属主校验」提问在公开
10
+ * 端点上不适用。只抑制 authorization 类;其他族规则不受标记影响。
11
+ */
12
+ const M = "__progmune_public_surface__";
13
+ const has = (vs, rule) => vs.some((v) => v.rule === rule);
14
+ (0, vitest_1.describe)("public_surface 标记:授权族四条接受、其他族不受影响", () => {
15
+ const calls = ["getShareTree", M];
16
+ (0, vitest_1.it)("Unauthenticated Access 被抑制", () => {
17
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["getShareTree"], "ShareController.getShare", "typescript", ["shareId"], true);
18
+ (0, vitest_1.expect)(has(bare, "Authorization (Unauthenticated Access)")).toBe(true);
19
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(calls, "ShareController.getShare", "typescript", ["shareId"], true);
20
+ (0, vitest_1.expect)(has(withMark, "Authorization (Unauthenticated Access)")).toBe(false);
21
+ });
22
+ (0, vitest_1.it)("Unauthenticated Mutation 被抑制", () => {
23
+ // updateInvite 直接命中 Mutation 触发词;真实语料里的 acceptInvite
24
+ // 告警靠函数体内调用上下文触发,隔离夹具用直接形态
25
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["updateInvite"], "WorkspaceController.updateInvite", "typescript", ["token"], true);
26
+ (0, vitest_1.expect)(has(bare, "Authorization (Unauthenticated Mutation)")).toBe(true);
27
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["updateInvite", M], "WorkspaceController.updateInvite", "typescript", ["token"], true);
28
+ (0, vitest_1.expect)(has(withMark, "Authorization (Unauthenticated Mutation)")).toBe(false);
29
+ });
30
+ (0, vitest_1.it)("Ownership Check 被抑制(公开端点的属主校验提问不适用)", () => {
31
+ // sharePage 直接命中 Ownership 触发词(share+Page);真实语料里的
32
+ // getSharedPageInfo 类告警靠同文件级调用上下文触发,隔离夹具用直接形态
33
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["sharePage"], "ShareController.sharePage", "typescript", ["shareId"], true);
34
+ (0, vitest_1.expect)(has(bare, "Authorization (Ownership Check)")).toBe(true);
35
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["sharePage", M], "ShareController.sharePage", "typescript", ["shareId"], true);
36
+ (0, vitest_1.expect)(has(withMark, "Authorization (Ownership Check)")).toBe(false);
37
+ });
38
+ (0, vitest_1.it)("Resource Ownership 被抑制", () => {
39
+ const bare = (0, protocol_detector_1.detectSafeguardViolations)(["toggleShare"], "ShareController.toggleShare", "typescript", ["id"], true);
40
+ (0, vitest_1.expect)(has(bare, "Authorization (Resource Ownership)")).toBe(true);
41
+ const withMark = (0, protocol_detector_1.detectSafeguardViolations)(["toggleShare", M], "ShareController.toggleShare", "typescript", ["id"], true);
42
+ (0, vitest_1.expect)(has(withMark, "Authorization (Resource Ownership)")).toBe(false);
43
+ });
44
+ (0, vitest_1.it)("非授权族规则不受标记影响(渲染 sink 照报)", () => {
45
+ const vs = (0, protocol_detector_1.detectSafeguardViolations)(["render", M], "PublicController.renderTemplate", "typescript");
46
+ (0, vitest_1.expect)(has(vs, "No Input Sanitization")).toBe(true);
47
+ });
48
+ });
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const protocol_detector_1 = require("./protocol-detector");
5
+ /**
6
+ * 公开表面传播豁免(修冤枉第十刀,2026-10-16)单元测试。
7
+ *
8
+ * 判据(见 computePublicSurfacePropagatedSet 注释):
9
+ * 函数 F 的每个直接调用方都 ∈ {公开面} ∪ {已守卫传播集} ∪
10
+ * {作业面传播集} ⇒ F 无未鉴权请求面(公开=开发者显式声明,
11
+ * 已守卫=过了鉴权,作业=无请求面)。
12
+ * R1-R5:调用方非空 / 边全解析 / 全部落在并集内 / 快照轮次默认 1 /
13
+ * 返回集合只含并集新增成员。
14
+ * 直接公开标记函数自身的告警由规则侧 safeguard 接受标记消除(R1 注)。
15
+ */
16
+ const MG = "__progmune_auth_machinery__";
17
+ const MJ = "__progmune_no_request_surface__";
18
+ /** 便捷:guard/job/公开集 + 公开传播集一并算 */
19
+ function sets(fns, publicFns, opts) {
20
+ const guard = (0, protocol_detector_1.computeGuardPropagatedSet)(fns);
21
+ const job = (0, protocol_detector_1.computeJobSurfacePropagatedSet)(fns);
22
+ const pub = new Set(publicFns);
23
+ const propagated = (0, protocol_detector_1.computePublicSurfacePropagatedSet)(fns, pub, guard, job, opts);
24
+ return { guard, job, pub, propagated };
25
+ }
26
+ (0, vitest_1.describe)("computePublicSurfacePropagatedSet(公开表面传播,第十刀)", () => {
27
+ (0, vitest_1.it)("只被公开端点调用 ⇒ 豁免", () => {
28
+ const fns = [
29
+ { name: "ShareController.getShare", file: "c/share.controller.ts", calls: ["getShareTree"] },
30
+ { name: "ShareService.getShareTree", file: "s/share.service.ts", calls: [] },
31
+ ];
32
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
33
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
34
+ });
35
+ (0, vitest_1.it)("混合调用方(公开 + 已守卫)⇒ 豁免(第九刀够不着的形态)", () => {
36
+ const fns = [
37
+ { name: "GuardedController.create", file: "c/page.controller.ts", calls: [MG, "searchPage"] },
38
+ { name: "SearchController.searchShare", file: "c/search.controller.ts", calls: ["searchPage"] },
39
+ { name: "SearchService.searchPage", file: "s/search.service.ts", calls: [] },
40
+ ];
41
+ const { guard, propagated } = sets(fns, ["SearchController.searchShare"]);
42
+ (0, vitest_1.expect)(guard.has("SearchService.searchPage")).toBe(false); // 非全守卫 ⇒ 第五刀不豁免
43
+ (0, vitest_1.expect)(propagated.has("SearchService.searchPage")).toBe(true); // 公开∪守卫 并集 ⇒ 豁免
44
+ });
45
+ (0, vitest_1.it)("混合调用方(公开 + 作业)⇒ 豁免", () => {
46
+ const fns = [
47
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
48
+ { name: "DigestJob.run", file: "q.ts", calls: [MJ, "getShareTree"] },
49
+ { name: "ShareService.getShareTree", file: "s.ts", calls: [] },
50
+ ];
51
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
52
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
53
+ });
54
+ (0, vitest_1.it)("任一调用方在并集外(未守卫的非公开调用方)⇒ 不豁免(fail-safe)", () => {
55
+ const fns = [
56
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
57
+ { name: "InternalController.export", file: "c2.ts", calls: ["getShareTree"] }, // 无守卫无公开标记
58
+ { name: "ShareService.getShareTree", file: "s.ts", calls: [] },
59
+ ];
60
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
61
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(false);
62
+ });
63
+ (0, vitest_1.it)("无调用方(入口/未连通)⇒ 不豁免(R1)", () => {
64
+ const fns = [{ name: "orphanEntry", file: "m.ts", calls: [] }];
65
+ const { propagated } = sets(fns, ["orphanEntry"]);
66
+ (0, vitest_1.expect)(propagated.has("orphanEntry")).toBe(false);
67
+ });
68
+ (0, vitest_1.it)("歧义调用边 ⇒ 不豁免(R2 fail-safe)", () => {
69
+ const fns = [
70
+ { name: "PublicController.get", file: "c/ctrl.ts", calls: ["load"] },
71
+ { name: "XxxService.load", file: "x/x.service.ts", calls: [] },
72
+ { name: "YyyService.load", file: "y/y.service.ts", calls: [] },
73
+ ];
74
+ const { propagated } = sets(fns, ["PublicController.get"]);
75
+ (0, vitest_1.expect)(propagated.has("XxxService.load")).toBe(false);
76
+ (0, vitest_1.expect)(propagated.has("YyyService.load")).toBe(false);
77
+ });
78
+ (0, vitest_1.it)("默认 1 轮:公开下游的下游不豁免(传递闭包未开)", () => {
79
+ const fns = [
80
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
81
+ { name: "ShareService.getShareTree", file: "s.ts", calls: ["loadPageData"] },
82
+ { name: "loadPageData", file: "s.ts", calls: [] },
83
+ ];
84
+ const { propagated } = sets(fns, ["ShareController.getShare"]);
85
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
86
+ (0, vitest_1.expect)(propagated.has("loadPageData")).toBe(false);
87
+ });
88
+ (0, vitest_1.it)("maxRounds=2 ⇒ 传递两跳成立(快照轮次语义)", () => {
89
+ const fns = [
90
+ { name: "ShareController.getShare", file: "c.ts", calls: ["getShareTree"] },
91
+ { name: "ShareService.getShareTree", file: "s.ts", calls: ["loadPageData"] },
92
+ { name: "loadPageData", file: "s.ts", calls: [] },
93
+ ];
94
+ const { propagated } = sets(fns, ["ShareController.getShare"], { maxRounds: 2 });
95
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(true);
96
+ (0, vitest_1.expect)(propagated.has("loadPageData")).toBe(true);
97
+ });
98
+ (0, vitest_1.it)("返回集合只含并集新增成员(R5:公开种子/守卫/作业成员不重复进)", () => {
99
+ const fns = [
100
+ { name: "PublicController.show", file: "c.ts", calls: ["target"] },
101
+ { name: "GuardedController.create", file: "c.ts", calls: [MG, "service"] },
102
+ { name: "Job.run", file: "q.ts", calls: [MJ, "jobOnly"] },
103
+ { name: "service", file: "s.ts", calls: ["target"] },
104
+ { name: "jobOnly", file: "s.ts", calls: ["target"] },
105
+ { name: "target", file: "s.ts", calls: [] },
106
+ ];
107
+ const { propagated } = sets(fns, ["PublicController.show"]);
108
+ // 公开种子自身、守卫/作业成员都不重复进 propagated
109
+ (0, vitest_1.expect)(propagated.has("PublicController.show")).toBe(false);
110
+ (0, vitest_1.expect)(propagated.has("Job.run")).toBe(false);
111
+ (0, vitest_1.expect)(propagated.has("service")).toBe(false); // 守卫传播成员(调用方全守卫)
112
+ (0, vitest_1.expect)(propagated.has("jobOnly")).toBe(false); // 作业面成员
113
+ (0, vitest_1.expect)(propagated.has("target")).toBe(true); // 调用方 {public[pub], service[g], jobOnly[j]} ⊆ 并集
114
+ });
115
+ (0, vitest_1.it)("自调不提供豁免证据", () => {
116
+ const fns = [{ name: "ShareService.getShareTree", file: "s.ts", calls: ["getShareTree"] }];
117
+ const { propagated } = sets(fns, []);
118
+ (0, vitest_1.expect)(propagated.has("ShareService.getShareTree")).toBe(false);
119
+ });
120
+ });
@@ -18,6 +18,7 @@ exports.computeResolvedCallerMap = computeResolvedCallerMap;
18
18
  exports.computeGuardPropagatedSet = computeGuardPropagatedSet;
19
19
  exports.computeJobSurfacePropagatedSet = computeJobSurfacePropagatedSet;
20
20
  exports.computeMixedSurfacePropagatedSet = computeMixedSurfacePropagatedSet;
21
+ exports.computePublicSurfacePropagatedSet = computePublicSurfacePropagatedSet;
21
22
  exports.detectSafeguardViolationsV7 = detectSafeguardViolationsV7;
22
23
  exports.detectProtocolViolations = detectProtocolViolations;
23
24
  exports.validateProtocolState = validateProtocolState;
@@ -390,7 +391,7 @@ const SAFEGUARD_RULES = [
390
391
  { pattern: /\b(hasPermission|checkPermission|checkAccess|isAuthorized|checkRole|requireRole|adminCheck|isAdmin|canModify|canDelete|canEdit)\b/i, label: "authz_check" },
391
392
  // 修冤枉第六刀:队列/定时/事件处理器不面向请求面——鉴权发生在入队侧,
392
393
  // 「改资源没查属主」的提问在处理器层不适用(docmost 补标注 FP 类)
393
- { pattern: /\b(__progmune_no_request_surface__)\b/, label: "no_request_surface" },
394
+ { pattern: /\b(__progmune_no_request_surface__|__progmune_public_surface__)\b/, label: "no_request_surface" },
394
395
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
395
396
  // §46 ②:资源属主被**赋值为当前主体**(`{ userId: user.id }` / `where:{userId: user.id}`)。
396
397
  // 这类函数的被操作对象就是(或已被限定在)主体自己,谈不上「改别人的东西」。
@@ -434,7 +435,7 @@ const SAFEGUARD_RULES = [
434
435
  paramTypeSafeguards: [SESSION_BEARING_TYPE_RE],
435
436
  trigger: /\b(list|download|view|fetch)(?:[A-Z]\w*|_\w+)|get(?:[A-Z]\w+|_\w+)/i,
436
437
  safeguards: [
437
- { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__)\b/i, label: "auth_check" },
438
+ { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|jwt\.decode|decode_token|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__|__progmune_public_surface__)\b/i, label: "auth_check" },
438
439
  // §40 C1:裸 can —— verdaccio 的 can('publish') 中间件就是这里的授权判定
439
440
  { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
440
441
  ],
@@ -473,7 +474,7 @@ const SAFEGUARD_RULES = [
473
474
  // (listPosts/getPost/deletePost fire via identifier-parsed words).
474
475
  trigger: /\b(add|create|update|set|publish|insert|submit)(?:[A-Z]\w*|_\w+)|(?:[A-Z]\w*|_\w+)(Add|Create|Update|Set|Publish|Insert|Submit)\b/i,
475
476
  safeguards: [
476
- { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__)\b/i, label: "auth_check" },
477
+ { pattern: /\b(getUser|validateToken|verifyToken|verifySession|validateSession|getSessionUser|getSession\b|getCurrentUser|token\w*(Check|Verify|Valid)|session\w*(Check|Verify|Valid)|auth\w*(Check|Verify|Valid|Guard|Middleware|Required)|requireAuth|withAuth|authenticate\w*(User|Request|Token)?|checkAuth|isAuth|hasAuth|checkAccess|hasAccess|get_user|get_session_user|get_current_user|validate_session|verify_token|require_auth|with_auth|check_auth|auth_required|authenticate_user|authenticate_request|authenticate_token|token_check|token_verify|token_valid|session_check|session_verify|session_valid|auth_check|auth_guard|auth_middleware|get_current_user_authorizer|current_user_authorizer|login_required|permission_required|user_passes_test|check_authorization|check_permission|__progmune_auth_checked__|__progmune_credential_check__|__progmune_drf_permissions__|__progmune_auth_machinery__|__progmune_no_request_surface__|__progmune_public_surface__)\b/i, label: "auth_check" },
477
478
  // §40 C1:裸 can —— 同上
478
479
  { pattern: exports.AUTHZ_BARE_CAN_RE, label: "bare_can", callsOnly: true },
479
480
  // §46 ①:凭签发入口豁免 —— 独立 safeguard,**不并入 auth_check**。
@@ -643,7 +644,7 @@ const SAFEGUARD_RULES = [
643
644
  { pattern: /\b(ownerId\s*[!=]==?|authorId\s*[!=]==?|userId\s*[!=]==?|createdBy|\.owner\s*[!=]==?)/i, label: "ownership_comparison" },
644
645
  { pattern: /\b(__progmune_ownership_checked__)\b/, label: "inline_ownership_check" },
645
646
  // 修冤枉第六刀:同上(无请求面 ⇒ 属主校验提问不适用)
646
- { pattern: /\b(__progmune_no_request_surface__)\b/, label: "no_request_surface" },
647
+ { pattern: /\b(__progmune_no_request_surface__|__progmune_public_surface__)\b/, label: "no_request_surface" },
647
648
  // §38:同 Ownership Check,组合式谓语一并接上(这条规则更严,更需要)
648
649
  { pattern: exports.AUTHZ_PREDICATE_RE, label: "casl_predicate" },
649
650
  { pattern: exports.AUTHZ_CAN_HELPER_RE, label: "can_helper" },
@@ -1885,6 +1886,53 @@ function computeMixedSurfacePropagatedSet(funcs, guardSet, jobSet, opts) {
1885
1886
  }
1886
1887
  return mixed;
1887
1888
  }
1889
+ /**
1890
+ * 修冤枉第十刀(2026-10-16):公开表面传播豁免——
1891
+ * 「函数的每个直接调用方都 ∈ {公开面传播集} ∪ {已守卫传播集} ∪
1892
+ * {作业面传播集}」。
1893
+ *
1894
+ * 第九刀覆盖「守卫∪作业」混合;公开端点(@Public()/@SkipAuth()/
1895
+ * @AllowAnonymous 显式免鉴权装饰器)是第三条正当路径——公开是开发者
1896
+ * **显式声明**(正向证据,R75):经由公开端点可达的函数,授权族提问
1897
+ * 「缺鉴权」不适用(公开≠意外缺守卫;未公开却缺守卫照报)。
1898
+ *
1899
+ * 保守规则(与第五/九刀共享边解析,见 computeResolvedCallerMap R4):
1900
+ * R1 调用方集合非空——入口函数无项目调用方,永不豁免(公开入口
1901
+ * 自身的告警由规则侧 safeguard 接受标记直接消除,与第六刀同架构)
1902
+ * R2 每条调用边都必须可解析(resolved=true),歧义 fail-safe
1903
+ * R3 调用方必须 ∈ publicSet ∪ guardSet ∪ jobSet;任一在并集外 → 照报
1904
+ * R4 快照轮次语义;默认 1 轮
1905
+ * R5 返回集合只含并集【新增】成员
1906
+ *
1907
+ * 注意:并集含守卫∪作业 ⇒ 本集合是第九刀的超集;engine 消费顺序
1908
+ * guard→job→mixed→public(未豁免才挂 public 标记)保证第九刀告警
1909
+ * 不重复挂标记。
1910
+ */
1911
+ function computePublicSurfacePropagatedSet(funcs, publicSet, guardSet, jobSet, opts) {
1912
+ const maxRounds = opts?.maxRounds ?? 1;
1913
+ const calleeToCallers = computeResolvedCallerMap(funcs);
1914
+ const base = new Set([...publicSet, ...guardSet, ...jobSet]);
1915
+ const added = new Set(); // R5:只记本轮新增
1916
+ for (let round = 0; round < maxRounds; round++) {
1917
+ const toAdd = [];
1918
+ for (const f of funcs) {
1919
+ if (base.has(f.name))
1920
+ continue;
1921
+ const callers = calleeToCallers.get(f.name) || [];
1922
+ if (callers.length === 0)
1923
+ continue; // R1:无调用方(入口/未连通)→ 不豁免
1924
+ if (callers.every((c) => c.resolved && base.has(c.caller)))
1925
+ toAdd.push(f.name);
1926
+ }
1927
+ if (toAdd.length === 0)
1928
+ break;
1929
+ for (const n of toAdd) {
1930
+ base.add(n);
1931
+ added.add(n);
1932
+ }
1933
+ }
1934
+ return added;
1935
+ }
1888
1936
  /**
1889
1937
  * v7: Build safeguard context — own → same-file → caller chain.
1890
1938
  *
package/dist/sdk.js CHANGED
@@ -20,7 +20,7 @@ const risk_model_1 = require("./risk-model");
20
20
  const protocol_knowledge_1 = require("./protocol-knowledge");
21
21
  const evidence_repository_1 = require("./evidence-repository");
22
22
  /** Runtime version — stable public identifier. Internal layers evolve underneath. */
23
- exports.RUNTIME_VERSION = "3.7.68";
23
+ exports.RUNTIME_VERSION = "3.7.69";
24
24
  function verify(filePath) {
25
25
  const cert = (0, certify_1.certify)(filePath);
26
26
  const kb = (0, protocol_knowledge_1.buildKnowledgeBase)();
@@ -1520,6 +1520,21 @@ async function collectProtocolViolations(ctx, callGraph) {
1520
1520
  const mixedSurfacePropagated = process.env.PROGMUNE_MIXED_SURFACE_TOGGLE === "off"
1521
1521
  ? new Set()
1522
1522
  : (0, protocol_detector_1.computeMixedSurfacePropagatedSet)(functions, guardPropagated, jobSurfacePropagated);
1523
+ // 修冤枉第十刀(2026-10-16):公开表面传播豁免——「每个直接
1524
+ // 调用方都 ∈ {公开面} ∪ {已守卫} ∪ {作业面}」⇒ 无未鉴权请求面
1525
+ // (公开=开发者显式声明,见 computePublicSurfacePropagatedSet
1526
+ // R1-R5)。直接公开标记函数的告警由规则侧 safeguard 接受标记
1527
+ // 消除(与第六刀同架构);本传播只覆盖下游。并集含守卫∪作业
1528
+ // ⇒ 是第九刀超集——消费顺序 guard→job→mixed→public,已豁免
1529
+ // 的告警不重复挂 public 标记。
1530
+ const publicMarked = new Set();
1531
+ for (const f of functions) {
1532
+ if ((f.calls || []).includes("__progmune_public_surface__"))
1533
+ publicMarked.add(f.name);
1534
+ }
1535
+ const publicSurfacePropagated = process.env.PROGMUNE_PUBLIC_SURFACE_TOGGLE === "off"
1536
+ ? new Set()
1537
+ : (0, protocol_detector_1.computePublicSurfacePropagatedSet)(functions, publicMarked, guardPropagated, jobSurfacePropagated);
1523
1538
  for (const f of functions) {
1524
1539
  const fCalls = f.calls || [];
1525
1540
  let svs;
@@ -1534,9 +1549,11 @@ async function collectProtocolViolations(ctx, callGraph) {
1534
1549
  const params = (f.params || []).map((p) => ({ n: p.name, t: p.type || "" }));
1535
1550
  const directlyMarked = fCalls.includes("__progmune_auth_machinery__");
1536
1551
  const directlyJobMarked = fCalls.includes("__progmune_no_request_surface__");
1552
+ const directlyPublicMarked = fCalls.includes("__progmune_public_surface__");
1537
1553
  const guardExempt = !directlyMarked && guardPropagated.has(f.name);
1538
1554
  const jobExempt = !directlyJobMarked && jobSurfacePropagated.has(f.name);
1539
1555
  const mixedExempt = !directlyMarked && !directlyJobMarked && mixedSurfacePropagated.has(f.name);
1556
+ const publicExempt = !directlyPublicMarked && publicSurfacePropagated.has(f.name);
1540
1557
  for (const v of svs) {
1541
1558
  const alert = {
1542
1559
  rule: v.rule,
@@ -1560,6 +1577,10 @@ async function collectProtocolViolations(ctx, callGraph) {
1560
1577
  alert.suppressedByMixedSurfacePropagation = true;
1561
1578
  suppressed = true;
1562
1579
  }
1580
+ if (!suppressed && publicExempt) {
1581
+ alert.suppressedByPublicSurfacePropagation = true;
1582
+ suppressed = true;
1583
+ }
1563
1584
  if (suppressed) {
1564
1585
  safeguardAlertsSuppressed.push(alert);
1565
1586
  continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "progmune-runtime",
3
- "version": "3.7.68",
3
+ "version": "3.7.69",
4
4
  "description": "Progmune — AI Trust Decision Engine. Verify AI-generated code before it reaches production. Outputs APPROVED / NEEDS_REVIEW / BLOCKED with evidence.",
5
5
  "files": [
6
6
  "dist/",