dsh-perm-gate 5.0.4 → 5.1.2-compat.10

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.
Files changed (52) hide show
  1. package/CHANGELOG.ja.md +0 -2
  2. package/CHANGELOG.ko.md +0 -2
  3. package/CHANGELOG.md +17 -2
  4. package/INSTALL.ja.md +0 -15
  5. package/INSTALL.ko.md +0 -15
  6. package/INSTALL.md +0 -15
  7. package/INSTALL.zh.md +0 -15
  8. package/README.ja.md +6 -18
  9. package/README.ko.md +6 -18
  10. package/README.md +40 -19
  11. package/README.zh.md +34 -18
  12. package/assets/appcontainer-launcher.cs +703 -0
  13. package/dsh.plugin.json +2 -2
  14. package/lib/appcontainer/compile.d.ts +17 -0
  15. package/lib/appcontainer/compile.js +74 -0
  16. package/lib/appcontainer/exempt.d.ts +18 -0
  17. package/lib/appcontainer/exempt.js +71 -0
  18. package/lib/appcontainer/index.d.ts +85 -0
  19. package/lib/appcontainer/index.js +286 -0
  20. package/lib/appcontainer/probe.d.ts +44 -0
  21. package/lib/appcontainer/probe.js +66 -0
  22. package/lib/appcontainer/sid.d.ts +23 -0
  23. package/lib/appcontainer/sid.js +33 -0
  24. package/lib/appcontainer/wrap.d.ts +22 -0
  25. package/lib/appcontainer/wrap.js +40 -0
  26. package/lib/bridge.d.ts +41 -0
  27. package/lib/bridge.js +128 -0
  28. package/lib/client.js +1563 -1109
  29. package/lib/client.js.map +1 -1
  30. package/lib/config.d.ts +41 -1
  31. package/lib/config.js +6 -0
  32. package/lib/debug-generation.d.ts +21 -0
  33. package/lib/debug-generation.js +44 -0
  34. package/lib/index.d.ts +1 -1
  35. package/lib/index.js +157 -7
  36. package/lib/runtime.d.ts +22 -0
  37. package/lib/runtime.js +24 -0
  38. package/locale/de.json +1 -0
  39. package/locale/en.json +1 -0
  40. package/locale/es.json +1 -0
  41. package/locale/fr.json +1 -0
  42. package/locale/it.json +1 -0
  43. package/locale/ja.json +1 -0
  44. package/locale/ko.json +1 -0
  45. package/locale/ru.json +1 -0
  46. package/locale/zh.json +1 -0
  47. package/package.json +25 -14
  48. package/README.de.md +0 -487
  49. package/README.es.md +0 -489
  50. package/README.fr.md +0 -498
  51. package/README.it.md +0 -490
  52. package/README.ru.md +0 -478
@@ -0,0 +1,66 @@
1
+ /**
2
+ * probe.ts —— launcher `--probe` 裁决的 Node 侧消费(解析 / 缓存键 / 归因)。
3
+ *
4
+ * 探针产物只做**诊断与归因**,不做放行依据:逐 launch 的 Region B 解析 +
5
+ * LookupMoniker 只读预检(launcher 进程内)才是唯一信任锚。缓存因此只在
6
+ * Node 进程内存中、按 launcher 哈希 + OS build 键控——**永不落盘共享**:
7
+ * 探测结论与「会话上下文」(提权/受限 job)强相关(同一台机器两种上下文
8
+ * 结论相反已实测),进程内缓存即天然一上下文一份,键里无需也无法诚实
9
+ * 表达上下文,落盘共享则必然跨上下文污染。
10
+ */
11
+ import { execFile } from 'node:child_process';
12
+ /**
13
+ * Parse the probe's single-line stdout. Returns undefined for anything that
14
+ * is not a recognizable verdict (crash output, truncated line, foreign text) —
15
+ * the controller treats undefined as UNAVAILABLE-with-retry.
16
+ */
17
+ export function parseProbeJson(text) {
18
+ const line = text.split('\n').map((l) => l.trim()).find((l) => l.startsWith('{'));
19
+ if (line === undefined)
20
+ return undefined;
21
+ let parsed;
22
+ try {
23
+ parsed = JSON.parse(line);
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ const p = parsed;
29
+ if (typeof p['era'] !== 'string' || typeof p['sid'] !== 'string' || typeof p['osBuild'] !== 'number')
30
+ return undefined;
31
+ if (typeof p['derive'] !== 'object' || p['derive'] === null)
32
+ return undefined;
33
+ if (typeof p['registered'] !== 'object' || p['registered'] === null)
34
+ return undefined;
35
+ if (typeof p['tryLaunch'] !== 'object' || p['tryLaunch'] === null)
36
+ return undefined;
37
+ return parsed;
38
+ }
39
+ /**
40
+ * The in-memory probe cache key. The launcher hash covers code changes, the
41
+ * OS build covers kernel-side behavior changes; context is deliberately NOT
42
+ * in the key because the cache never leaves the process (see header).
43
+ */
44
+ export function probeCacheKey(launcherHash, probe) {
45
+ return `${launcherHash}@${probe.osBuild}`;
46
+ }
47
+ /** Run `launcher --probe` and parse its verdict. Undefined = launcher unusable. */
48
+ export function runProbe(exePath, timeoutMs = 60_000) {
49
+ return new Promise((resolve) => {
50
+ execFile(exePath, ['--probe'], { windowsHide: true, timeout: timeoutMs, maxBuffer: 1 << 20 }, (error, stdout) => {
51
+ if (error !== undefined && error !== null) {
52
+ resolve(undefined); // crash/timeout: UNAVAILABLE, retried on next ensure
53
+ return;
54
+ }
55
+ resolve(parseProbeJson(String(stdout ?? '')));
56
+ });
57
+ });
58
+ }
59
+ /** Human-attributable one-liner for deny logs (which resolution level failed). */
60
+ export function probeAttribution(probe) {
61
+ const derive = `${probe.derive.name}(${probe.derive.module}) hr=0x${(probe.derive.hr >>> 0).toString(16)}`;
62
+ const launch = probe.tryLaunch.attempted
63
+ ? `tryLaunch win32Err=${probe.tryLaunch.win32Err}${probe.tryLaunch.childExit !== null ? ` childExit=${probe.tryLaunch.childExit}` : ''}`
64
+ : 'tryLaunch skipped';
65
+ return `era=${probe.era} derive=${derive} registered=${probe.registered.state} ${launch}`;
66
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * sid.ts —— AppContainer 容器 SID 的形状校验与容器名常量。
3
+ *
4
+ * 派生**不在 Node 侧做**:自造 S-1-15-2 SID(sha256 排序等任何手工形状)会被
5
+ * `NtCreateLowBoxToken` 以 STATUS_INVALID_SID (0xC0000030) 拒绝——容器 SID 必须由
6
+ * OS 自己的推导产生(launcher 的 `--print-sid` 走官方
7
+ * `DeriveAppContainerSidFromName`,Win8+,名字确定则 SID 确定,机器内稳定,
8
+ * 回环豁免与 ACL 授予因此终身复用)。本模块只保留:容器名(改名字 = 换 SID =
9
+ * 旧豁免/旧 ACE 全部脱靶)与 SID 字符串形状校验。
10
+ */
11
+ /** The fixed AppContainer container name — changing it invalidates every
12
+ * standing exemption/ACE (it derives a brand-new SID). */
13
+ export declare const APPCONTAINER_NAME = "dsh-perm-gate.ac";
14
+ export declare const APPCONTAINER_AUTHORITY = "S-1-15-2";
15
+ /**
16
+ * Structural check for an AppContainer SID string — an OPAQUE handle check.
17
+ *
18
+ * 实测(2026-10-05,26200):经典派生为 4 子权限,新代 moniker 派生与真实
19
+ * UWP 包 SID 为 7 子权限,且**非单调**——校验只认 `S-1-15-2` 权威前缀 +
20
+ * ≥4 个 uint32 子权限,绝不假设数量或排序(自造 SID 本就被内核拒收,
21
+ * 这里只是形状门,不是派生器)。
22
+ */
23
+ export declare function isAppContainerSid(sid: string): boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * sid.ts —— AppContainer 容器 SID 的形状校验与容器名常量。
3
+ *
4
+ * 派生**不在 Node 侧做**:自造 S-1-15-2 SID(sha256 排序等任何手工形状)会被
5
+ * `NtCreateLowBoxToken` 以 STATUS_INVALID_SID (0xC0000030) 拒绝——容器 SID 必须由
6
+ * OS 自己的推导产生(launcher 的 `--print-sid` 走官方
7
+ * `DeriveAppContainerSidFromName`,Win8+,名字确定则 SID 确定,机器内稳定,
8
+ * 回环豁免与 ACL 授予因此终身复用)。本模块只保留:容器名(改名字 = 换 SID =
9
+ * 旧豁免/旧 ACE 全部脱靶)与 SID 字符串形状校验。
10
+ */
11
+ /** The fixed AppContainer container name — changing it invalidates every
12
+ * standing exemption/ACE (it derives a brand-new SID). */
13
+ export const APPCONTAINER_NAME = 'dsh-perm-gate.ac';
14
+ export const APPCONTAINER_AUTHORITY = 'S-1-15-2';
15
+ /**
16
+ * Structural check for an AppContainer SID string — an OPAQUE handle check.
17
+ *
18
+ * 实测(2026-10-05,26200):经典派生为 4 子权限,新代 moniker 派生与真实
19
+ * UWP 包 SID 为 7 子权限,且**非单调**——校验只认 `S-1-15-2` 权威前缀 +
20
+ * ≥4 个 uint32 子权限,绝不假设数量或排序(自造 SID 本就被内核拒收,
21
+ * 这里只是形状门,不是派生器)。
22
+ */
23
+ export function isAppContainerSid(sid) {
24
+ const parts = sid.split('-');
25
+ if (parts.length < 8 || parts[0] !== 'S' || parts[1] !== '1' || parts[2] !== '15' || parts[3] !== '2')
26
+ return false;
27
+ for (const part of parts.slice(4)) {
28
+ const n = Number(part);
29
+ if (!Number.isInteger(n) || n < 0 || n > 0xFFFF_FFFF)
30
+ return false;
31
+ }
32
+ return true;
33
+ }
@@ -0,0 +1,22 @@
1
+ /** One sandboxed-execution plan: everything the launcher needs, on disk. */
2
+ export interface SandboxLaunchConfig {
3
+ /** The session workspace the command runs in (cwd + ACL grant root). */
4
+ readonly workspace: string;
5
+ /** Per-call scratch dir created by the Node side; the launcher gets it via env. */
6
+ readonly tempDir: string;
7
+ /** 'read-only' grants RX on the workspace; 'workspace-write' adds Modify. */
8
+ readonly mode: 'read-only' | 'workspace-write';
9
+ /** Exact env overrides for the child (HTTPS_PROXY etc.). Empty keys delete. */
10
+ readonly proxyEnv: Readonly<Record<string, string>>;
11
+ }
12
+ /** Serialize + persist one launch config, returning the path to hand the launcher. */
13
+ export declare function writeLaunchConfig(config: SandboxLaunchConfig, configDir: string): Promise<string>;
14
+ /** base64url-free standard base64 of the UTF-8 command text. */
15
+ export declare function encodeCommand(command: string): string;
16
+ export declare function decodeCommand(encoded: string): string;
17
+ /**
18
+ * The command line the outer shell executes instead of the original one.
19
+ * Single quoted-exe invocation; `=` padding and the alnum base64 body are
20
+ * inert in cmd.exe and POSIX shells alike.
21
+ */
22
+ export declare function buildWrappedCommand(launcherPath: string, configPath: string, command: string): string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * wrap.ts —— 允许调用 → AppContainer 启动器命令行的重写(纯逻辑,可单测)。
3
+ *
4
+ * 关键决策:原命令 **base64 编码**后交给启动器,外层 shell 只见
5
+ * `<launcher> --config <cfg> --exec-b64 <b64>` 一个普通进程调用——管道/
6
+ * `&&`/重定向是外层 shell 语法,若把原命令裸拼在后面,它们会在**容器外**
7
+ * 被解析执行,沙盒即被绕过。base64 字母表(A-Za-z0-9+/=)在 cmd 与 POSIX
8
+ * shell 里都是惰性字符,无需再转义。
9
+ *
10
+ * 配置文件每次调用落盘一份(JSON),启动器读它拿 workspace/temp/代理环境;
11
+ * 与命令行传参相比少一层引就地狱。env 覆盖采用宿主 windows-acl 验证过的
12
+ * 形态:**启动器先改自身环境再 CreateProcess(继承),不传显式 env block**
13
+ * (koffi 实测显式 env 在 CreateProcessAsUserW 上报 ERROR_INVALID_PARAMETER;
14
+ * C# 路径虽然标准,继承形态少一层封送,风险为零)。
15
+ */
16
+ import { mkdir, writeFile } from 'node:fs/promises';
17
+ import path from 'node:path';
18
+ import { randomUUID } from 'node:crypto';
19
+ /** Serialize + persist one launch config, returning the path to hand the launcher. */
20
+ export async function writeLaunchConfig(config, configDir) {
21
+ await mkdir(configDir, { recursive: true });
22
+ const file = path.join(configDir, `ac-${randomUUID()}.json`);
23
+ await writeFile(file, JSON.stringify(config, null, 2) + '\n', 'utf8');
24
+ return file;
25
+ }
26
+ /** base64url-free standard base64 of the UTF-8 command text. */
27
+ export function encodeCommand(command) {
28
+ return Buffer.from(command, 'utf8').toString('base64');
29
+ }
30
+ export function decodeCommand(encoded) {
31
+ return Buffer.from(encoded, 'base64').toString('utf8');
32
+ }
33
+ /**
34
+ * The command line the outer shell executes instead of the original one.
35
+ * Single quoted-exe invocation; `=` padding and the alnum base64 body are
36
+ * inert in cmd.exe and POSIX shells alike.
37
+ */
38
+ export function buildWrappedCommand(launcherPath, configPath, command) {
39
+ return `"${launcherPath}" --config "${configPath}" --exec-b64 ${encodeCommand(command)}`;
40
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * settings bridge — T12(spec webServer 数据桥规范):describe/mutate 两端点。
3
+ *
4
+ * 桥只是 HTTP 皮:读 = svc.describe 取自家 ns 描述符;写 = svc.mutate 转 ops 落
5
+ * 宿主 settings 服务(不落盘不缓存第二份,天然规避双写/锁竞争)。mutate 成功即
6
+ * describe 回填,带 expectedRevision 乐观并发。守卫:POST-only + ns 硬编码自家
7
+ * allowlist + 秘密不回读(redactSecrets)。
8
+ *
9
+ * 数据面消费方 = client BridgeDocHandle(T13b 双轨数据源:0.1.7+ 原生句柄优先,
10
+ * ≤0.1.5 settingsScope 不解析时落到本桥——free-search 同架构)。
11
+ *
12
+ * T20-b 泛化(2026-10-08):路由从 ns 参数派生(bridgeRoutesFor),一个宿主进程
13
+ * 可为多个 ns 各注册一对端点(steward 双 ns:会话管家 + 搜索索引)——客户端与
14
+ * 服务端各自持同一派生式,测试钉死两半一致,URL 契约不可能漂移。
15
+ */
16
+ interface SettingsServiceLike {
17
+ update(ns: string, patch: object): Promise<void>;
18
+ register?(ns: string, schema: unknown, options?: {
19
+ base?: unknown;
20
+ }): unknown;
21
+ installSection?(owner: unknown, ns: string, schema: unknown, entry: unknown, hooks: unknown): unknown;
22
+ describe?(o?: {
23
+ redactSecrets?: boolean;
24
+ }): Array<{
25
+ ns: unknown;
26
+ [k: string]: unknown;
27
+ }>;
28
+ mutate?(ns: string, ops: Array<{
29
+ op: string;
30
+ path: string[];
31
+ value?: unknown;
32
+ }>, expectedRevision?: number): Promise<unknown>;
33
+ writable?: boolean;
34
+ }
35
+ /** 一个 settings ns 的桥路由对——client 与 server 必须同式派生(测试钉死一致)。 */
36
+ export declare function bridgeRoutesFor(ns: string): {
37
+ describe: string;
38
+ mutate: string;
39
+ };
40
+ export declare function registerSettingsBridgeRoutes(server: unknown, getSettingsSvc: () => SettingsServiceLike | undefined, ns: string): Array<() => void>;
41
+ export {};
package/lib/bridge.js ADDED
@@ -0,0 +1,128 @@
1
+ /** 一个 settings ns 的桥路由对——client 与 server 必须同式派生(测试钉死一致)。 */
2
+ export function bridgeRoutesFor(ns) {
3
+ return { describe: `/api/${ns}/settings/describe`, mutate: `/api/${ns}/settings/mutate` };
4
+ }
5
+ function readJsonBody(reqRaw, limit = 256 * 1024) {
6
+ const req = reqRaw;
7
+ return new Promise((resolve, reject) => {
8
+ const chunks = [];
9
+ let size = 0;
10
+ req.on('data', (chunk) => {
11
+ const buf = chunk;
12
+ size += buf.length;
13
+ if (size > limit) {
14
+ reject(new Error('bridge body too large'));
15
+ req.resume?.();
16
+ return;
17
+ }
18
+ chunks.push(buf);
19
+ });
20
+ req.on('end', () => {
21
+ const raw = Buffer.concat(chunks).toString('utf8');
22
+ if (raw === '') {
23
+ resolve({});
24
+ return;
25
+ }
26
+ try {
27
+ resolve(JSON.parse(raw));
28
+ }
29
+ catch (e) {
30
+ reject(e instanceof Error ? e : new Error(String(e)));
31
+ }
32
+ });
33
+ req.on('error', reject);
34
+ });
35
+ }
36
+ function sendJson(res, code, body) {
37
+ res.writeHead(code, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-cache' });
38
+ res.end(JSON.stringify(body));
39
+ }
40
+ function isConflict(e) {
41
+ return e instanceof Error && /conflict|revision/i.test(e.message);
42
+ }
43
+ export function registerSettingsBridgeRoutes(server, getSettingsSvc, ns) {
44
+ if (typeof server !== 'object' || server === null)
45
+ return [];
46
+ const ws = typeof server.register === 'function' ? server : undefined;
47
+ if (ws === undefined)
48
+ return [];
49
+ const offs = [];
50
+ const routes = bridgeRoutesFor(ns);
51
+ offs.push(ws.register({
52
+ kind: 'exact',
53
+ path: routes.describe,
54
+ handler: (rawReq, rawRes) => {
55
+ const req = rawReq;
56
+ const res = rawRes;
57
+ if (req.method !== 'POST') {
58
+ sendJson(res, 405, { ok: false, code: 'method-not-allowed' });
59
+ return;
60
+ }
61
+ const svc = getSettingsSvc();
62
+ if (svc === undefined || typeof svc.describe !== 'function') {
63
+ sendJson(res, 200, { ok: false, code: 'settings-unavailable', message: 'settings service not resolved yet' });
64
+ return;
65
+ }
66
+ try {
67
+ const descriptor = svc
68
+ .describe({ redactSecrets: true })
69
+ .find((d) => d.ns === ns);
70
+ if (descriptor === undefined) {
71
+ sendJson(res, 200, { ok: false, code: 'ns-not-registered', message: `settings namespace "${ns}" is not registered` });
72
+ return;
73
+ }
74
+ sendJson(res, 200, { ok: true, value: { descriptor, writable: svc.writable !== false } });
75
+ }
76
+ catch (e) {
77
+ sendJson(res, 200, { ok: false, code: 'internal', message: e instanceof Error ? e.message : String(e) });
78
+ }
79
+ },
80
+ }));
81
+ offs.push(ws.register({
82
+ kind: 'exact',
83
+ path: routes.mutate,
84
+ handler: (rawReq, rawRes) => {
85
+ const req = rawReq;
86
+ const res = rawRes;
87
+ if (req.method !== 'POST') {
88
+ sendJson(res, 405, { ok: false, code: 'method-not-allowed' });
89
+ return;
90
+ }
91
+ const svc = getSettingsSvc();
92
+ const mutate = svc === undefined ? undefined : svc.mutate;
93
+ if (typeof mutate !== 'function') {
94
+ sendJson(res, 200, { ok: false, code: 'settings-unavailable', message: 'settings service mutate not available' });
95
+ return;
96
+ }
97
+ readJsonBody(req).then((body) => {
98
+ const ops = body.ops;
99
+ if (!Array.isArray(ops) || ops.some((op) => !op || typeof op !== 'object' || !(op.op in { set: 1, unset: 1 }))) {
100
+ sendJson(res, 200, { ok: false, code: 'settings-rejected', message: 'malformed bridge mutate ops' });
101
+ return;
102
+ }
103
+ const expectedRevision = typeof body.expectedRevision === 'number' ? body.expectedRevision : undefined;
104
+ mutate
105
+ .call(svc, ns, ops, expectedRevision)
106
+ .then(() => {
107
+ // mutate 成功即 describe 回填(规范:写路径仍归口宿主服务,无第二份状态)
108
+ const descriptor = svc
109
+ .describe({ redactSecrets: true })
110
+ .find((d) => d.ns === ns);
111
+ if (descriptor === undefined) {
112
+ sendJson(res, 200, { ok: false, code: 'internal', message: `namespace "${ns}" disposed after mutate` });
113
+ return;
114
+ }
115
+ sendJson(res, 200, { ok: true, value: { descriptor } });
116
+ })
117
+ .catch((e) => {
118
+ sendJson(res, 200, isConflict(e)
119
+ ? { ok: false, code: 'settings-conflict', message: e instanceof Error ? e.message : String(e) }
120
+ : { ok: false, code: 'internal', message: e instanceof Error ? e.message : String(e) });
121
+ });
122
+ }).catch((e) => {
123
+ sendJson(res, 200, { ok: false, code: 'bad-request', message: e instanceof Error ? e.message : String(e) });
124
+ });
125
+ },
126
+ }));
127
+ return offs;
128
+ }