@sema-agent/settings-schema 14.6.0 → 14.8.0
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 +475 -0
- package/README.md +89 -5
- package/conformance/VERSION +1 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +153 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/input.json +23 -0
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/meta.json +5 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +153 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/input.json +23 -0
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/meta.json +5 -0
- package/dist/api/auth-bridge.d.ts +16 -2
- package/dist/api/auth-bridge.js +59 -24
- package/dist/api/credentials.d.ts +281 -0
- package/dist/api/credentials.js +149 -0
- package/dist/api/scopes.js +5 -1
- package/dist/atomic-write.d.ts +15 -0
- package/dist/atomic-write.js +109 -5
- package/dist/catalog-manages.d.ts +15 -0
- package/dist/catalog-manages.js +60 -0
- package/dist/center-refs.d.ts +68 -0
- package/dist/center-refs.js +70 -0
- package/dist/config-d-file.js +33 -15
- package/dist/config-dir-migration.js +1 -0
- package/dist/config-fns.js +147 -81
- package/dist/config-root-documents.d.ts +106 -0
- package/dist/config-root-documents.js +93 -0
- package/dist/config-root-node.d.ts +117 -0
- package/dist/config-root-node.js +260 -0
- package/dist/conformance/canonical.js +4 -4
- package/dist/conformance/contract-adapter.js +26 -1
- package/dist/conformance/corpus.js +4 -4
- package/dist/conformance/derive.js +2 -2
- package/dist/conformance/run.d.ts +3 -0
- package/dist/conformance/run.js +53 -12
- package/dist/conformance/temp-roots.d.ts +31 -0
- package/dist/conformance/temp-roots.js +184 -0
- package/dist/cross-session.d.ts +4 -4
- package/dist/declared-capture.d.ts +41 -0
- package/dist/declared-capture.js +344 -0
- package/dist/default-model.d.ts +1 -1
- package/dist/default-model.js +5 -4
- package/dist/env-model-table.d.ts +163 -0
- package/dist/env-model-table.js +257 -0
- package/dist/file-edit.js +11 -15
- package/dist/fleet.d.ts +36 -7
- package/dist/fleet.js +83 -32
- package/dist/hash.d.ts +0 -9
- package/dist/hash.js +64 -7
- package/dist/hooks-config.js +18 -3
- package/dist/http-hook-allowlists.d.ts +22 -9
- package/dist/http-hook-allowlists.js +169 -91
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/mcp-request-entry.d.ts +67 -13
- package/dist/mcp-request-entry.js +132 -93
- package/dist/mcp-server-name.d.ts +17 -0
- package/dist/mcp-server-name.js +19 -0
- package/dist/migration-receipt.d.ts +10 -6
- package/dist/migration-receipt.js +5 -3
- package/dist/node.d.ts +1 -0
- package/dist/node.js +2 -0
- package/dist/one-time-migrations.d.ts +4 -2
- package/dist/one-time-migrations.js +236 -96
- package/dist/own-key-read.d.ts +247 -0
- package/dist/own-key-read.js +449 -0
- package/dist/plain-json-object.d.ts +11 -1
- package/dist/plain-json-object.js +37 -1
- package/dist/remote-exec.js +9 -1
- package/dist/retired-form.d.ts +19 -0
- package/dist/retired-form.js +24 -1
- package/dist/safety-merge-spec.d.ts +6 -6
- package/dist/safety-merge-spec.js +50 -37
- package/dist/sandbox-git.d.ts +64 -0
- package/dist/sandbox-git.js +89 -0
- package/dist/scheduler-store.js +19 -11
- package/dist/settings-units.js +14 -12
- package/dist/tier-words.d.ts +33 -0
- package/dist/tier-words.js +43 -0
- package/dist/types.d.ts +12 -2
- package/dist/types.js +35 -6
- package/dist/url-shape.d.ts +16 -0
- package/dist/url-shape.js +18 -0
- package/dist/web-admin-credential.js +5 -2
- package/dist/web-admin-session-url.js +2 -2
- package/dist/worker-row-verdict.d.ts +11 -0
- package/dist/worker-row-verdict.js +58 -2
- package/dist/workers-rows.js +38 -10
- package/dist/zod-unwrap.d.ts +6 -0
- package/dist/zod-unwrap.js +14 -7
- package/docs/API-SURFACE.md +100 -5
- package/docs/CONSUMER-INTEGRATION.md +250 -2
- package/llms-full.txt +26 -4
- package/llms.txt +11 -4
- package/package.json +2 -2
- package/dist/definition-copy.d.ts +0 -23
- package/dist/definition-copy.js +0 -52
package/dist/api/auth-bridge.js
CHANGED
|
@@ -54,10 +54,14 @@
|
|
|
54
54
|
* signature checks). (Decision function: `jwksWithinOfflineGrace`.)
|
|
55
55
|
*
|
|
56
56
|
* R9 OUTPUT. Success → `VerifiedIdentity { principal: sub, role: token.role, grantId: token.jti,
|
|
57
|
-
* scope: token.scope, iat: token.iat }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
|
|
57
|
+
* scope: token.scope, iat: token.iat, principalInc: token.sub_inc }` (`scope` = the multi-tenant active scope claim, `api/scopes`; read it with
|
|
58
58
|
* `readTokenScope` — an absent claim is refused, not read as "global"; `iat` = issued-at, integer seconds, carried
|
|
59
59
|
* verbatim — a reader that learns a principal was deleted at T cuts off its tokens issued before T; absent claim ⇒ no
|
|
60
|
-
* `iat` key; present but not a non-negative integer ⇒ the token is `malformed`
|
|
60
|
+
* `iat` key; present but not a non-negative integer ⇒ the token is `malformed`; `principalInc` = the incarnation claim
|
|
61
|
+
* {@link PRINCIPAL_INC_CLAIM} carried verbatim — the same name deleted and re-created is ANOTHER person, and this is what
|
|
62
|
+
* tells the two apart; absent claim ⇒ no `principalInc` key — never a placeholder: what a reader does with a token that names
|
|
63
|
+
* no incarnation is that reader's decision, not a refusal here; present but not a non-empty string ⇒ the token is `malformed`,
|
|
64
|
+
* the same law as `sub`).
|
|
61
65
|
* `role` is web-admin RBAC role claim (string, informational for the worker's own authz mapping);
|
|
62
66
|
* `grantId` is the jti when web-admin stamps one (absent today — optional forever). Failure → one
|
|
63
67
|
* of AUTH_BRIDGE_ERROR_CODES; the HTTP surface answers 401 with a GENERIC message (no error detail
|
|
@@ -72,6 +76,8 @@
|
|
|
72
76
|
*/
|
|
73
77
|
import { z } from "zod";
|
|
74
78
|
import { INTEGER } from "../integer.js";
|
|
79
|
+
import { inspect, orWhenUninspectable, ownJsonOrThrow, ownOrThrow } from "../own-key-read.js";
|
|
80
|
+
import { isJsonArray } from "../plain-json-object.js";
|
|
75
81
|
// ── constants ────────────────────────────────────────────────────────────────────────────────────────────
|
|
76
82
|
/** The web-admin JWKS endpoint, relative to the web-admin origin (shipped; public by design, RFC 7517 §5). */
|
|
77
83
|
export const WEB_ADMIN_JWKS_PATH = "/api/auth/sso/jwks";
|
|
@@ -88,6 +94,13 @@ export const DEFAULT_OFFLINE_GRACE_SEC = 24 * 60 * 60;
|
|
|
88
94
|
* split one identity into two. Anyone validating or minting a principal (a token `sub`, a membership row, an outbound
|
|
89
95
|
* principal header) checks against THIS value. */
|
|
90
96
|
export const PRINCIPAL_MAX_LENGTH = 190;
|
|
97
|
+
/** R9 — the token claim that carries the principal's INCARNATION: a deleted-and-re-created user keeps the name (`sub`) and gets a new
|
|
98
|
+
* incarnation, so `sub` alone names a slot, not a person. One spelling for whoever mints the claim and whoever reads it
|
|
99
|
+
* ({@link toVerifiedIdentity} reads it by this constant), the way `SCOPE_CLAIM` (`api/scopes`) is spelled once for the scope claim. */
|
|
100
|
+
export const PRINCIPAL_INC_CLAIM = "sub_inc";
|
|
101
|
+
/** One incarnation id: a non-empty string, opaque (never trimmed, folded or parsed). The token claim, {@link VerifiedIdentity.principalInc},
|
|
102
|
+
* and every wire field that names an incarnation (the credential port's request and answer) are THIS schema — one rule, read by object. */
|
|
103
|
+
export const PrincipalIncarnation = z.string().min(1);
|
|
91
104
|
// ── JWKS wire shape (what GET /api/auth/sso/jwks returns) ────────────────────────────────────────────────
|
|
92
105
|
/** One RSA public JWK as web-admin publishes it (public members ONLY — a `d`/`p`/`q` here is a web-admin
|
|
93
106
|
* bug, but the verifier MUST still never use/echo private members). */
|
|
@@ -159,6 +172,11 @@ export const VerifiedIdentity = z.object({
|
|
|
159
172
|
* a claim that is not a non-negative integer makes the token `malformed` (R9), and one issued later than now + the clock
|
|
160
173
|
* skew is `expired` (R5, the same tolerance as `exp` / `nbf`). */
|
|
161
174
|
iat: INTEGER.nonnegative().optional(),
|
|
175
|
+
/** = token {@link PRINCIPAL_INC_CLAIM} (`sub_inc`) when present, verbatim — the principal's incarnation id ({@link PrincipalIncarnation}).
|
|
176
|
+
* Absent claim ⇒ the key is absent (never minted; never `0`, `""` or any placeholder): a reader that needs an incarnation and finds none
|
|
177
|
+
* answers "incarnation unrecorded" itself — that is not a refusal of the token. A claim that is not a non-empty string makes the token
|
|
178
|
+
* `malformed` (R9): a broken issuer is not read as an old one. */
|
|
179
|
+
principalInc: PrincipalIncarnation.optional(),
|
|
162
180
|
});
|
|
163
181
|
/**
|
|
164
182
|
* R3 — should a token bearing `kid` trigger a forced JWKS refetch?
|
|
@@ -203,29 +221,46 @@ export function classifyTimeClaims(claims, nowSec, clockSkewSec) {
|
|
|
203
221
|
* library has verified signature (R1/R2/R3), issuer (R4) and time (R5).
|
|
204
222
|
*/
|
|
205
223
|
export function toVerifiedIdentity(payload, cfg) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return { ok: false, error: "malformed" };
|
|
212
|
-
const iat = iatRead.data;
|
|
213
|
-
if (cfg?.audience) {
|
|
214
|
-
const aud = payload.aud;
|
|
215
|
-
const match = typeof aud === "string" ? aud === cfg.audience : Array.isArray(aud) && aud.includes(cfg.audience);
|
|
216
|
-
if (!match)
|
|
224
|
+
// Every claim is read from the payload's OWN keys (identity reads only what the token carries: a polluted `Object.prototype.role` is not a role the token was minted with,
|
|
225
|
+
// and a token with no `aud` is not given one); a claim that cannot be read ⇒ the token is malformed (fail-closed), never an engine error.
|
|
226
|
+
return orWhenUninspectable(() => {
|
|
227
|
+
const sub = ownOrThrow(payload, "sub"); // 只问「是不是非空串」:读一次值,不深拷
|
|
228
|
+
if (typeof sub !== "string" || sub.length === 0)
|
|
217
229
|
return { ok: false, error: "malformed" };
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
230
|
+
// The iat and incarnation rules are the output fields' own schemas (one rule each, not a second hand-written check).
|
|
231
|
+
// Each schema call runs under `inspect`: the validator reads a snapshot THIS package took, and a throw there (a length check coercing an object value to a primitive) is not the caller's
|
|
232
|
+
// object being unreadable — but it is still no identity: the claim is malformed, and no engine error escapes the judge.
|
|
233
|
+
const iatRead = inspect(() => VerifiedIdentity.shape.iat.safeParse(ownJsonOrThrow(payload, "iat")));
|
|
234
|
+
if (!iatRead.ok || !iatRead.value.success)
|
|
235
|
+
return { ok: false, error: "malformed" };
|
|
236
|
+
const iat = iatRead.value.data;
|
|
237
|
+
const incRead = inspect(() => VerifiedIdentity.shape.principalInc.safeParse(ownJsonOrThrow(payload, PRINCIPAL_INC_CLAIM)));
|
|
238
|
+
if (!incRead.ok || !incRead.value.success)
|
|
239
|
+
return { ok: false, error: "malformed" };
|
|
240
|
+
const principalInc = incRead.value.data;
|
|
241
|
+
if (cfg?.audience) {
|
|
242
|
+
const aud = ownJsonOrThrow(payload, "aud");
|
|
243
|
+
const match = typeof aud === "string" ? aud === cfg.audience : isJsonArray(aud) && aud.includes(cfg.audience);
|
|
244
|
+
if (!match)
|
|
245
|
+
return { ok: false, error: "malformed" };
|
|
246
|
+
}
|
|
247
|
+
// role / jti / scope 只问「是不是非空串」(别的值读成缺省):读一次值,不深拷 —— 深拷会让「值是个带 Date 的对象」把整枚令牌拖成 malformed(v14.6.0 只做 typeof 判)。
|
|
248
|
+
// 要深拷的只有交给会反射它的代码的值:`aud`(数组上 `includes`)与 `iat` / `sub_inc`(交给校验器)
|
|
249
|
+
const role = ownOrThrow(payload, "role");
|
|
250
|
+
const jti = ownOrThrow(payload, "jti");
|
|
251
|
+
const scope = ownOrThrow(payload, "scope");
|
|
252
|
+
return {
|
|
253
|
+
ok: true,
|
|
254
|
+
identity: {
|
|
255
|
+
principal: sub,
|
|
256
|
+
role: typeof role === "string" && role.length > 0 ? role : undefined,
|
|
257
|
+
grantId: typeof jti === "string" && jti.length > 0 ? jti : undefined,
|
|
258
|
+
scope: typeof scope === "string" && scope.length > 0 ? scope : undefined,
|
|
259
|
+
...(iat !== undefined ? { iat } : {}),
|
|
260
|
+
...(principalInc !== undefined ? { principalInc } : {}),
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
}, () => ({ ok: false, error: "malformed" }));
|
|
229
264
|
}
|
|
230
265
|
/** Cheap pre-filter: does this bearer LOOK like a JWT (three non-empty dot-separated segments)? Use it to
|
|
231
266
|
* route between the existing static-token branch and this verifier — NOT as any kind of validation. */
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api/credentials` —— 凭据取值口(`POST /api/v1/credentials/handles` → `POST /api/v1/credentials/resolve`)里 **`sandbox_git`** 这一种凭据的 wire 型(14.7.0)。
|
|
3
|
+
* 一份 zod,发这些形的一方(web-admin)与收这些形的一方(引擎的数据面)同 import。纯、浏览器安全。
|
|
4
|
+
*
|
|
5
|
+
* 两步:① 带调用方凭据申领一枚短期、一次性的**句柄**(请求 {@link SandboxGitCredentialRequest},应答 {@link CredentialHandleAnswer});
|
|
6
|
+
* ② 凭句柄兑换(请求 {@link CredentialResolveRequest},应答 {@link SandboxGitCredentialAnswer};不合格则整次拒答 {@link SandboxGitCredentialRefusal})。
|
|
7
|
+
* 本模块只有**形与词表**:调用方鉴权、委托、活表复核、行 MAC、租约算术、续期状态机、「应答与请求逐条对应 / 回显相等」的核对都是各方自己的事,不在这里。
|
|
8
|
+
* 取值口的另两种凭据(`model` / `mcp_static`)的请求与应答形不在本模块;它们落地时与本模块的 `sandbox_git` 形并成按 `kind` 判别的联合,
|
|
9
|
+
* 本模块的形不动(`kind` 字面量已经在)。
|
|
10
|
+
*
|
|
11
|
+
* ## 三张词表,各一张,每个词只在本文件里拼一次
|
|
12
|
+
* · {@link SANDBOX_GIT_ORIGIN_VERDICTS} —— **逐 origin 判定序表**,11 行,序即判定序(首条命中即答;一个 origin 的结果不影响别的 origin)。
|
|
13
|
+
* 状态词({@link SANDBOX_GIT_ORIGIN_STATES},六个)与原因词({@link SANDBOX_GIT_ORIGIN_REASONS},十个)由它派生,没有第二张词表;`active` 行没有原因。
|
|
14
|
+
* 应答行({@link SandboxGitOriginAnswer})每个状态一个臂,臂里 `reason` 的值域 = 表里该状态的行 —— 状态与原因的配对由表定,臂不手写。
|
|
15
|
+
* 每一行的**条件**(什么事实使它命中)是 web-admin 的判定逻辑,写在 `docs/CONSUMER-INTEGRATION.md` 里,不进运行期表。
|
|
16
|
+
* · {@link SANDBOX_GIT_REFUSALS} —— **整次拒答词**(十个)→ HTTP 状态集与 `permanence`;拒体里的 `permanence` 是错误词的函数,必须等于这一行的。
|
|
17
|
+
* · {@link SANDBOX_GIT_PERMANENCE} —— `terminal`(不按定时重试)/ `transient`(退避重试)两词,语义在本表定义。
|
|
18
|
+
*
|
|
19
|
+
* ## 解析一律剥未知键
|
|
20
|
+
* 与包内 api 契约的前向兼容同律(新键恒为加性)。同时这也是一条安全律:`secret` 只在 `active` 臂的形里 —— 别的臂即使收到也剥掉,
|
|
21
|
+
* 解析出的值里不会带着谁多塞的秘密。`secret` 是应答里唯一的秘密位;`credentialId` / `version` / `auditId` 是不带值的审计位。
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
/**
|
|
25
|
+
* 逐 origin 判定序表(11 行,序即判定序)。每行 `{ state, reason? }`:命中这一行的 origin 的应答是这个状态与这个原因。
|
|
26
|
+
* 条件(各行什么事实命中)见 `docs/CONSUMER-INTEGRATION.md` 的判定序表;`active` 是兜底行(行 `active` 且刷新不成问题),没有原因。
|
|
27
|
+
* 加一行 / 改序都是改契约:状态词、原因词、应答臂的原因值域、类型全部随这一张表走。
|
|
28
|
+
*/
|
|
29
|
+
export declare const SANDBOX_GIT_ORIGIN_VERDICTS: readonly [{
|
|
30
|
+
readonly state: "insecure_transport";
|
|
31
|
+
readonly reason: "not_https";
|
|
32
|
+
}, {
|
|
33
|
+
readonly state: "not_offered";
|
|
34
|
+
readonly reason: "not_in_registry";
|
|
35
|
+
}, {
|
|
36
|
+
readonly state: "not_connected";
|
|
37
|
+
readonly reason: "no_row";
|
|
38
|
+
}, {
|
|
39
|
+
readonly state: "not_connected";
|
|
40
|
+
readonly reason: "revoked";
|
|
41
|
+
}, {
|
|
42
|
+
readonly state: "needs_reauth";
|
|
43
|
+
readonly reason: "revoking";
|
|
44
|
+
}, {
|
|
45
|
+
readonly state: "needs_reauth";
|
|
46
|
+
readonly reason: "suspended";
|
|
47
|
+
}, {
|
|
48
|
+
readonly state: "needs_reauth";
|
|
49
|
+
readonly reason: "row_needs_reauth";
|
|
50
|
+
}, {
|
|
51
|
+
readonly state: "needs_reauth";
|
|
52
|
+
readonly reason: "integrity_failed";
|
|
53
|
+
}, {
|
|
54
|
+
readonly state: "needs_reauth";
|
|
55
|
+
readonly reason: "refresh_rejected";
|
|
56
|
+
}, {
|
|
57
|
+
readonly state: "temporarily_unavailable";
|
|
58
|
+
readonly reason: "refresh_unavailable";
|
|
59
|
+
}, {
|
|
60
|
+
readonly state: "active";
|
|
61
|
+
}];
|
|
62
|
+
type Verdict = (typeof SANDBOX_GIT_ORIGIN_VERDICTS)[number];
|
|
63
|
+
/** 逐 origin 状态词(闭集,六个)—— 类型由判定序表派生。 */
|
|
64
|
+
export type SandboxGitOriginState = Verdict["state"];
|
|
65
|
+
/** 逐 origin 原因词(闭集,十个)—— 类型由判定序表派生;`active` 行没有原因。 */
|
|
66
|
+
export type SandboxGitOriginReason = Extract<Verdict, {
|
|
67
|
+
readonly reason: string;
|
|
68
|
+
}>["reason"];
|
|
69
|
+
/** 状态词表:判定序表里状态首次出现的次序。 */
|
|
70
|
+
export declare const SANDBOX_GIT_ORIGIN_STATES: readonly [SandboxGitOriginState, ...SandboxGitOriginState[]];
|
|
71
|
+
/** 原因词表:判定序表里原因出现的次序(= 判定序)。 */
|
|
72
|
+
export declare const SANDBOX_GIT_ORIGIN_REASONS: readonly [SandboxGitOriginReason, ...SandboxGitOriginReason[]];
|
|
73
|
+
/** 申领 `sandbox_git` 句柄的请求体。`origins` = 要取凭据的 origin(规范形、1–16 个、两两不同;http origin 也收 —— 中心对它答判定序第 1 行);`sid` = 会话 id(审计标签,不是安全边界)。 */
|
|
74
|
+
export declare const SandboxGitCredentialRequest: z.ZodObject<{
|
|
75
|
+
origins: z.ZodArray<z.ZodString>;
|
|
76
|
+
sid: z.ZodString;
|
|
77
|
+
principal: z.ZodString;
|
|
78
|
+
principalInc: z.ZodString;
|
|
79
|
+
scope: z.ZodString;
|
|
80
|
+
kind: z.ZodLiteral<"sandbox_git">;
|
|
81
|
+
}, z.core.$strip>;
|
|
82
|
+
export type SandboxGitCredentialRequest = z.infer<typeof SandboxGitCredentialRequest>;
|
|
83
|
+
/** 申领成功的应答:句柄(不透明串,一次性)与它的到期时刻(毫秒)。与凭据种类无关。 */
|
|
84
|
+
export declare const CredentialHandleAnswer: z.ZodObject<{
|
|
85
|
+
handle: z.ZodString;
|
|
86
|
+
expiresAt: z.ZodNumber;
|
|
87
|
+
}, z.core.$strip>;
|
|
88
|
+
export type CredentialHandleAnswer = z.infer<typeof CredentialHandleAnswer>;
|
|
89
|
+
/** 兑换请求:只有句柄 —— 调用方凭据在传输层,不在体里。与凭据种类无关。 */
|
|
90
|
+
export declare const CredentialResolveRequest: z.ZodObject<{
|
|
91
|
+
handle: z.ZodString;
|
|
92
|
+
}, z.core.$strip>;
|
|
93
|
+
export type CredentialResolveRequest = z.infer<typeof CredentialResolveRequest>;
|
|
94
|
+
/**
|
|
95
|
+
* 一个 origin 的应答行,按 `state` 判别。键集(判定序表之上的形):
|
|
96
|
+
* · `active` 独有 `credentialId` `version` `username` `secret` `expiresAt`(OAuth 令牌:到期毫秒;个人访问令牌:`null`);没有 `reason`;
|
|
97
|
+
* · `needs_reauth`(序 5–9)带 `credentialId`(不带值的审计位);
|
|
98
|
+
* · 其余只有 `origin` / `state` / `reason`。
|
|
99
|
+
* 多给的键一律剥掉(见模块头注)。
|
|
100
|
+
*/
|
|
101
|
+
export declare const SandboxGitOriginAnswer: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
102
|
+
origin: z.ZodString;
|
|
103
|
+
state: z.ZodLiteral<"insecure_transport">;
|
|
104
|
+
reason: z.ZodEnum<{
|
|
105
|
+
not_https: "not_https";
|
|
106
|
+
}>;
|
|
107
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
108
|
+
origin: z.ZodString;
|
|
109
|
+
state: z.ZodLiteral<"not_offered">;
|
|
110
|
+
reason: z.ZodEnum<{
|
|
111
|
+
not_in_registry: "not_in_registry";
|
|
112
|
+
}>;
|
|
113
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
114
|
+
origin: z.ZodString;
|
|
115
|
+
state: z.ZodLiteral<"not_connected">;
|
|
116
|
+
reason: z.ZodEnum<{
|
|
117
|
+
no_row: "no_row";
|
|
118
|
+
revoked: "revoked";
|
|
119
|
+
}>;
|
|
120
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
121
|
+
origin: z.ZodString;
|
|
122
|
+
state: z.ZodLiteral<"needs_reauth">;
|
|
123
|
+
reason: z.ZodEnum<{
|
|
124
|
+
revoking: "revoking";
|
|
125
|
+
suspended: "suspended";
|
|
126
|
+
row_needs_reauth: "row_needs_reauth";
|
|
127
|
+
integrity_failed: "integrity_failed";
|
|
128
|
+
refresh_rejected: "refresh_rejected";
|
|
129
|
+
}>;
|
|
130
|
+
credentialId: z.ZodString;
|
|
131
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
132
|
+
origin: z.ZodString;
|
|
133
|
+
state: z.ZodLiteral<"temporarily_unavailable">;
|
|
134
|
+
reason: z.ZodEnum<{
|
|
135
|
+
refresh_unavailable: "refresh_unavailable";
|
|
136
|
+
}>;
|
|
137
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
138
|
+
origin: z.ZodString;
|
|
139
|
+
state: z.ZodLiteral<"active">;
|
|
140
|
+
credentialId: z.ZodString;
|
|
141
|
+
version: z.ZodNumber;
|
|
142
|
+
username: z.ZodString;
|
|
143
|
+
secret: z.ZodString;
|
|
144
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
145
|
+
}, z.core.$strip>], "state">;
|
|
146
|
+
export type SandboxGitOriginAnswer = z.infer<typeof SandboxGitOriginAnswer>;
|
|
147
|
+
/**
|
|
148
|
+
* 取值应答(200)。`principal` / `principalInc` / `scope` 是请求的**回显**;`answeredAt`(应答时刻)、`cacheUntil`(句柄到期,收方不得把凭据缓存过它)与行里的 `expiresAt`
|
|
149
|
+
* 是**同一只钟**(中心的)给的毫秒 —— 收方只该用它们的**差**,不拿它们和自己的钟比。`auditId` = 审计行 id(不带值)。
|
|
150
|
+
* `origins` 与请求逐条对应(同序同长)是收方要核的事,本形只钉 1–{@link SANDBOX_GIT_MAX_ORIGINS} 条。
|
|
151
|
+
*/
|
|
152
|
+
export declare const SandboxGitCredentialAnswer: z.ZodObject<{
|
|
153
|
+
auditId: z.ZodString;
|
|
154
|
+
answeredAt: z.ZodNumber;
|
|
155
|
+
cacheUntil: z.ZodNumber;
|
|
156
|
+
origins: z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
157
|
+
origin: z.ZodString;
|
|
158
|
+
state: z.ZodLiteral<"insecure_transport">;
|
|
159
|
+
reason: z.ZodEnum<{
|
|
160
|
+
not_https: "not_https";
|
|
161
|
+
}>;
|
|
162
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
163
|
+
origin: z.ZodString;
|
|
164
|
+
state: z.ZodLiteral<"not_offered">;
|
|
165
|
+
reason: z.ZodEnum<{
|
|
166
|
+
not_in_registry: "not_in_registry";
|
|
167
|
+
}>;
|
|
168
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
169
|
+
origin: z.ZodString;
|
|
170
|
+
state: z.ZodLiteral<"not_connected">;
|
|
171
|
+
reason: z.ZodEnum<{
|
|
172
|
+
no_row: "no_row";
|
|
173
|
+
revoked: "revoked";
|
|
174
|
+
}>;
|
|
175
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
176
|
+
origin: z.ZodString;
|
|
177
|
+
state: z.ZodLiteral<"needs_reauth">;
|
|
178
|
+
reason: z.ZodEnum<{
|
|
179
|
+
revoking: "revoking";
|
|
180
|
+
suspended: "suspended";
|
|
181
|
+
row_needs_reauth: "row_needs_reauth";
|
|
182
|
+
integrity_failed: "integrity_failed";
|
|
183
|
+
refresh_rejected: "refresh_rejected";
|
|
184
|
+
}>;
|
|
185
|
+
credentialId: z.ZodString;
|
|
186
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
187
|
+
origin: z.ZodString;
|
|
188
|
+
state: z.ZodLiteral<"temporarily_unavailable">;
|
|
189
|
+
reason: z.ZodEnum<{
|
|
190
|
+
refresh_unavailable: "refresh_unavailable";
|
|
191
|
+
}>;
|
|
192
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
193
|
+
origin: z.ZodString;
|
|
194
|
+
state: z.ZodLiteral<"active">;
|
|
195
|
+
credentialId: z.ZodString;
|
|
196
|
+
version: z.ZodNumber;
|
|
197
|
+
username: z.ZodString;
|
|
198
|
+
secret: z.ZodString;
|
|
199
|
+
expiresAt: z.ZodNullable<z.ZodNumber>;
|
|
200
|
+
}, z.core.$strip>], "state">>;
|
|
201
|
+
principal: z.ZodString;
|
|
202
|
+
principalInc: z.ZodString;
|
|
203
|
+
scope: z.ZodString;
|
|
204
|
+
kind: z.ZodLiteral<"sandbox_git">;
|
|
205
|
+
}, z.core.$strip>;
|
|
206
|
+
export type SandboxGitCredentialAnswer = z.infer<typeof SandboxGitCredentialAnswer>;
|
|
207
|
+
/** `permanence` 两词:`terminal` = 不按定时重试;`transient` = 退避重试。 */
|
|
208
|
+
export declare const SANDBOX_GIT_PERMANENCE: readonly ["terminal", "transient"];
|
|
209
|
+
export type SandboxGitPermanence = (typeof SANDBOX_GIT_PERMANENCE)[number];
|
|
210
|
+
/**
|
|
211
|
+
* 整次拒答词 → 它的 HTTP 状态集(中心答这个词时用其一)与 `permanence`。**一个错误词的 permanence 是固定的**,拒体里再带一份只为让收方不必查表 ——
|
|
212
|
+
* 两处不一致就是畸形应答({@link SandboxGitCredentialRefusal} 拒)。收方怎么处置某个词(例如哪些 terminal 可由用户动作恢复)是收方的状态机,不在这里。
|
|
213
|
+
*/
|
|
214
|
+
export declare const SANDBOX_GIT_REFUSALS: {
|
|
215
|
+
readonly caller_not_admitted: {
|
|
216
|
+
readonly statuses: readonly [401, 403];
|
|
217
|
+
readonly permanence: "terminal";
|
|
218
|
+
};
|
|
219
|
+
readonly scope_mismatch: {
|
|
220
|
+
readonly statuses: readonly [403];
|
|
221
|
+
readonly permanence: "terminal";
|
|
222
|
+
};
|
|
223
|
+
readonly incarnation_mismatch: {
|
|
224
|
+
readonly statuses: readonly [403];
|
|
225
|
+
readonly permanence: "terminal";
|
|
226
|
+
};
|
|
227
|
+
readonly request_invalid: {
|
|
228
|
+
readonly statuses: readonly [400];
|
|
229
|
+
readonly permanence: "terminal";
|
|
230
|
+
};
|
|
231
|
+
readonly principal_not_live: {
|
|
232
|
+
readonly statuses: readonly [403];
|
|
233
|
+
readonly permanence: "terminal";
|
|
234
|
+
};
|
|
235
|
+
readonly delegation_required: {
|
|
236
|
+
readonly statuses: readonly [403];
|
|
237
|
+
readonly permanence: "terminal";
|
|
238
|
+
};
|
|
239
|
+
readonly vault_disabled: {
|
|
240
|
+
readonly statuses: readonly [503];
|
|
241
|
+
readonly permanence: "terminal";
|
|
242
|
+
};
|
|
243
|
+
readonly vault_frozen: {
|
|
244
|
+
readonly statuses: readonly [503];
|
|
245
|
+
readonly permanence: "terminal";
|
|
246
|
+
};
|
|
247
|
+
readonly handle_invalid: {
|
|
248
|
+
readonly statuses: readonly [401];
|
|
249
|
+
readonly permanence: "transient";
|
|
250
|
+
};
|
|
251
|
+
readonly vault_unavailable: {
|
|
252
|
+
readonly statuses: readonly [503];
|
|
253
|
+
readonly permanence: "transient";
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
/** 整次拒答词(闭集,十个)—— 类型由 {@link SANDBOX_GIT_REFUSALS} 派生。 */
|
|
257
|
+
export type SandboxGitRefusalError = keyof typeof SANDBOX_GIT_REFUSALS;
|
|
258
|
+
/** 整次拒答词表(运行期;表的键,按表序)。 */
|
|
259
|
+
export declare const SANDBOX_GIT_REFUSAL_ERRORS: readonly [SandboxGitRefusalError, ...SandboxGitRefusalError[]];
|
|
260
|
+
/** 整次拒答的体:`{ error, permanence, message }`。`message` 是中心的诊断句(收方不据它判,可为空串)。 */
|
|
261
|
+
export declare const SandboxGitCredentialRefusal: z.ZodObject<{
|
|
262
|
+
error: z.ZodEnum<{
|
|
263
|
+
caller_not_admitted: "caller_not_admitted";
|
|
264
|
+
scope_mismatch: "scope_mismatch";
|
|
265
|
+
incarnation_mismatch: "incarnation_mismatch";
|
|
266
|
+
request_invalid: "request_invalid";
|
|
267
|
+
principal_not_live: "principal_not_live";
|
|
268
|
+
delegation_required: "delegation_required";
|
|
269
|
+
vault_disabled: "vault_disabled";
|
|
270
|
+
vault_frozen: "vault_frozen";
|
|
271
|
+
handle_invalid: "handle_invalid";
|
|
272
|
+
vault_unavailable: "vault_unavailable";
|
|
273
|
+
}>;
|
|
274
|
+
permanence: z.ZodEnum<{
|
|
275
|
+
terminal: "terminal";
|
|
276
|
+
transient: "transient";
|
|
277
|
+
}>;
|
|
278
|
+
message: z.ZodString;
|
|
279
|
+
}, z.core.$strip>;
|
|
280
|
+
export type SandboxGitCredentialRefusal = z.infer<typeof SandboxGitCredentialRefusal>;
|
|
281
|
+
export {};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `api/credentials` —— 凭据取值口(`POST /api/v1/credentials/handles` → `POST /api/v1/credentials/resolve`)里 **`sandbox_git`** 这一种凭据的 wire 型(14.7.0)。
|
|
3
|
+
* 一份 zod,发这些形的一方(web-admin)与收这些形的一方(引擎的数据面)同 import。纯、浏览器安全。
|
|
4
|
+
*
|
|
5
|
+
* 两步:① 带调用方凭据申领一枚短期、一次性的**句柄**(请求 {@link SandboxGitCredentialRequest},应答 {@link CredentialHandleAnswer});
|
|
6
|
+
* ② 凭句柄兑换(请求 {@link CredentialResolveRequest},应答 {@link SandboxGitCredentialAnswer};不合格则整次拒答 {@link SandboxGitCredentialRefusal})。
|
|
7
|
+
* 本模块只有**形与词表**:调用方鉴权、委托、活表复核、行 MAC、租约算术、续期状态机、「应答与请求逐条对应 / 回显相等」的核对都是各方自己的事,不在这里。
|
|
8
|
+
* 取值口的另两种凭据(`model` / `mcp_static`)的请求与应答形不在本模块;它们落地时与本模块的 `sandbox_git` 形并成按 `kind` 判别的联合,
|
|
9
|
+
* 本模块的形不动(`kind` 字面量已经在)。
|
|
10
|
+
*
|
|
11
|
+
* ## 三张词表,各一张,每个词只在本文件里拼一次
|
|
12
|
+
* · {@link SANDBOX_GIT_ORIGIN_VERDICTS} —— **逐 origin 判定序表**,11 行,序即判定序(首条命中即答;一个 origin 的结果不影响别的 origin)。
|
|
13
|
+
* 状态词({@link SANDBOX_GIT_ORIGIN_STATES},六个)与原因词({@link SANDBOX_GIT_ORIGIN_REASONS},十个)由它派生,没有第二张词表;`active` 行没有原因。
|
|
14
|
+
* 应答行({@link SandboxGitOriginAnswer})每个状态一个臂,臂里 `reason` 的值域 = 表里该状态的行 —— 状态与原因的配对由表定,臂不手写。
|
|
15
|
+
* 每一行的**条件**(什么事实使它命中)是 web-admin 的判定逻辑,写在 `docs/CONSUMER-INTEGRATION.md` 里,不进运行期表。
|
|
16
|
+
* · {@link SANDBOX_GIT_REFUSALS} —— **整次拒答词**(十个)→ HTTP 状态集与 `permanence`;拒体里的 `permanence` 是错误词的函数,必须等于这一行的。
|
|
17
|
+
* · {@link SANDBOX_GIT_PERMANENCE} —— `terminal`(不按定时重试)/ `transient`(退避重试)两词,语义在本表定义。
|
|
18
|
+
*
|
|
19
|
+
* ## 解析一律剥未知键
|
|
20
|
+
* 与包内 api 契约的前向兼容同律(新键恒为加性)。同时这也是一条安全律:`secret` 只在 `active` 臂的形里 —— 别的臂即使收到也剥掉,
|
|
21
|
+
* 解析出的值里不会带着谁多塞的秘密。`secret` 是应答里唯一的秘密位;`credentialId` / `version` / `auditId` 是不带值的审计位。
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { INTEGER } from "../integer.js";
|
|
25
|
+
import { buildQuotedIssue } from "../quoted-issue.js";
|
|
26
|
+
import { SANDBOX_GIT_MAX_ORIGINS, SandboxGitOrigin, SandboxGitOriginList } from "../sandbox-git.js";
|
|
27
|
+
import { PRINCIPAL_MAX_LENGTH, PrincipalIncarnation } from "./auth-bridge.js";
|
|
28
|
+
// ── 逐 origin 判定序表 ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
29
|
+
/**
|
|
30
|
+
* 逐 origin 判定序表(11 行,序即判定序)。每行 `{ state, reason? }`:命中这一行的 origin 的应答是这个状态与这个原因。
|
|
31
|
+
* 条件(各行什么事实命中)见 `docs/CONSUMER-INTEGRATION.md` 的判定序表;`active` 是兜底行(行 `active` 且刷新不成问题),没有原因。
|
|
32
|
+
* 加一行 / 改序都是改契约:状态词、原因词、应答臂的原因值域、类型全部随这一张表走。
|
|
33
|
+
*/
|
|
34
|
+
export const SANDBOX_GIT_ORIGIN_VERDICTS = [
|
|
35
|
+
{ state: "insecure_transport", reason: "not_https" },
|
|
36
|
+
{ state: "not_offered", reason: "not_in_registry" },
|
|
37
|
+
{ state: "not_connected", reason: "no_row" },
|
|
38
|
+
{ state: "not_connected", reason: "revoked" },
|
|
39
|
+
{ state: "needs_reauth", reason: "revoking" },
|
|
40
|
+
{ state: "needs_reauth", reason: "suspended" },
|
|
41
|
+
{ state: "needs_reauth", reason: "row_needs_reauth" },
|
|
42
|
+
{ state: "needs_reauth", reason: "integrity_failed" },
|
|
43
|
+
{ state: "needs_reauth", reason: "refresh_rejected" },
|
|
44
|
+
{ state: "temporarily_unavailable", reason: "refresh_unavailable" },
|
|
45
|
+
{ state: "active" },
|
|
46
|
+
];
|
|
47
|
+
/** 状态词表:判定序表里状态首次出现的次序。 */
|
|
48
|
+
export const SANDBOX_GIT_ORIGIN_STATES = [...new Set(SANDBOX_GIT_ORIGIN_VERDICTS.map((v) => v.state))];
|
|
49
|
+
/** 原因词表:判定序表里原因出现的次序(= 判定序)。 */
|
|
50
|
+
export const SANDBOX_GIT_ORIGIN_REASONS = SANDBOX_GIT_ORIGIN_VERDICTS.flatMap((v) => ("reason" in v ? [v.reason] : []));
|
|
51
|
+
/** 判定序表里某个状态下的原因词,按表序。 */
|
|
52
|
+
function reasonsOf(state) {
|
|
53
|
+
return SANDBOX_GIT_ORIGIN_VERDICTS.flatMap((v) => (v.state === state && "reason" in v ? [v.reason] : []));
|
|
54
|
+
}
|
|
55
|
+
// ── 公共字段 ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
56
|
+
/** 本模块里每个数值位(毫秒时刻、凭据行版本)的形:非负安全整数 —— 包内的整数判据 + 安全整数上界;浮点、负数、超出安全整数的都不是。 */
|
|
57
|
+
const SafeCount = INTEGER.nonnegative().max(Number.MAX_SAFE_INTEGER);
|
|
58
|
+
const NonEmptyString = z.string().min(1);
|
|
59
|
+
/**
|
|
60
|
+
* 请求与应答都带的「这是谁的 run」字段:主体(长度界同令牌 `sub` 的界,{@link PRINCIPAL_MAX_LENGTH})、化身号({@link PrincipalIncarnation},与令牌 `sub_inc` 同一只 schema)、
|
|
61
|
+
* scope(非空串 —— 与 `readTokenScope` 读 token 声明的规则同一条,不另立 slug 规则)。应答里这三键是**回显**,等不等于请求由收方核。
|
|
62
|
+
*/
|
|
63
|
+
const RunIdentity = {
|
|
64
|
+
principal: z.string().min(1).max(PRINCIPAL_MAX_LENGTH),
|
|
65
|
+
principalInc: PrincipalIncarnation,
|
|
66
|
+
scope: NonEmptyString,
|
|
67
|
+
};
|
|
68
|
+
// ── ① 申领句柄、② 兑换:请求与句柄应答 ───────────────────────────────────────────────────────────────────────────
|
|
69
|
+
/** 申领 `sandbox_git` 句柄的请求体。`origins` = 要取凭据的 origin(规范形、1–16 个、两两不同;http origin 也收 —— 中心对它答判定序第 1 行);`sid` = 会话 id(审计标签,不是安全边界)。 */
|
|
70
|
+
export const SandboxGitCredentialRequest = z.object({
|
|
71
|
+
kind: z.literal("sandbox_git"),
|
|
72
|
+
...RunIdentity,
|
|
73
|
+
origins: SandboxGitOriginList.min(1),
|
|
74
|
+
sid: NonEmptyString,
|
|
75
|
+
});
|
|
76
|
+
/** 申领成功的应答:句柄(不透明串,一次性)与它的到期时刻(毫秒)。与凭据种类无关。 */
|
|
77
|
+
export const CredentialHandleAnswer = z.object({ handle: NonEmptyString, expiresAt: SafeCount });
|
|
78
|
+
/** 兑换请求:只有句柄 —— 调用方凭据在传输层,不在体里。与凭据种类无关。 */
|
|
79
|
+
export const CredentialResolveRequest = z.object({ handle: NonEmptyString });
|
|
80
|
+
// ── 应答:逐 origin 一行 ─────────────────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
/** 非 `active` 状态的应答臂:`{ origin, state, reason }`,`reason` 的值域 = 判定序表里该状态的行。 */
|
|
82
|
+
function verdictArm(state) {
|
|
83
|
+
return z.object({ origin: SandboxGitOrigin, state: z.literal(state), reason: z.enum(reasonsOf(state)) });
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* 一个 origin 的应答行,按 `state` 判别。键集(判定序表之上的形):
|
|
87
|
+
* · `active` 独有 `credentialId` `version` `username` `secret` `expiresAt`(OAuth 令牌:到期毫秒;个人访问令牌:`null`);没有 `reason`;
|
|
88
|
+
* · `needs_reauth`(序 5–9)带 `credentialId`(不带值的审计位);
|
|
89
|
+
* · 其余只有 `origin` / `state` / `reason`。
|
|
90
|
+
* 多给的键一律剥掉(见模块头注)。
|
|
91
|
+
*/
|
|
92
|
+
export const SandboxGitOriginAnswer = z.discriminatedUnion("state", [
|
|
93
|
+
verdictArm("insecure_transport"),
|
|
94
|
+
verdictArm("not_offered"),
|
|
95
|
+
verdictArm("not_connected"),
|
|
96
|
+
verdictArm("needs_reauth").extend({ credentialId: NonEmptyString }),
|
|
97
|
+
verdictArm("temporarily_unavailable"),
|
|
98
|
+
z.object({
|
|
99
|
+
origin: SandboxGitOrigin,
|
|
100
|
+
state: z.literal("active"),
|
|
101
|
+
credentialId: NonEmptyString,
|
|
102
|
+
version: SafeCount,
|
|
103
|
+
username: NonEmptyString,
|
|
104
|
+
secret: NonEmptyString,
|
|
105
|
+
expiresAt: SafeCount.nullable(),
|
|
106
|
+
}),
|
|
107
|
+
]);
|
|
108
|
+
/**
|
|
109
|
+
* 取值应答(200)。`principal` / `principalInc` / `scope` 是请求的**回显**;`answeredAt`(应答时刻)、`cacheUntil`(句柄到期,收方不得把凭据缓存过它)与行里的 `expiresAt`
|
|
110
|
+
* 是**同一只钟**(中心的)给的毫秒 —— 收方只该用它们的**差**,不拿它们和自己的钟比。`auditId` = 审计行 id(不带值)。
|
|
111
|
+
* `origins` 与请求逐条对应(同序同长)是收方要核的事,本形只钉 1–{@link SANDBOX_GIT_MAX_ORIGINS} 条。
|
|
112
|
+
*/
|
|
113
|
+
export const SandboxGitCredentialAnswer = z.object({
|
|
114
|
+
kind: z.literal("sandbox_git"),
|
|
115
|
+
...RunIdentity,
|
|
116
|
+
auditId: NonEmptyString,
|
|
117
|
+
answeredAt: SafeCount,
|
|
118
|
+
cacheUntil: SafeCount,
|
|
119
|
+
origins: z.array(SandboxGitOriginAnswer).min(1).max(SANDBOX_GIT_MAX_ORIGINS),
|
|
120
|
+
});
|
|
121
|
+
// ── 整次拒答 ───────────────────────────────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
/** `permanence` 两词:`terminal` = 不按定时重试;`transient` = 退避重试。 */
|
|
123
|
+
export const SANDBOX_GIT_PERMANENCE = ["terminal", "transient"];
|
|
124
|
+
/**
|
|
125
|
+
* 整次拒答词 → 它的 HTTP 状态集(中心答这个词时用其一)与 `permanence`。**一个错误词的 permanence 是固定的**,拒体里再带一份只为让收方不必查表 ——
|
|
126
|
+
* 两处不一致就是畸形应答({@link SandboxGitCredentialRefusal} 拒)。收方怎么处置某个词(例如哪些 terminal 可由用户动作恢复)是收方的状态机,不在这里。
|
|
127
|
+
*/
|
|
128
|
+
export const SANDBOX_GIT_REFUSALS = {
|
|
129
|
+
caller_not_admitted: { statuses: [401, 403], permanence: "terminal" },
|
|
130
|
+
scope_mismatch: { statuses: [403], permanence: "terminal" },
|
|
131
|
+
incarnation_mismatch: { statuses: [403], permanence: "terminal" },
|
|
132
|
+
request_invalid: { statuses: [400], permanence: "terminal" },
|
|
133
|
+
principal_not_live: { statuses: [403], permanence: "terminal" },
|
|
134
|
+
delegation_required: { statuses: [403], permanence: "terminal" },
|
|
135
|
+
vault_disabled: { statuses: [503], permanence: "terminal" },
|
|
136
|
+
vault_frozen: { statuses: [503], permanence: "terminal" },
|
|
137
|
+
handle_invalid: { statuses: [401], permanence: "transient" },
|
|
138
|
+
vault_unavailable: { statuses: [503], permanence: "transient" },
|
|
139
|
+
};
|
|
140
|
+
/** 整次拒答词表(运行期;表的键,按表序)。 */
|
|
141
|
+
export const SANDBOX_GIT_REFUSAL_ERRORS = Object.keys(SANDBOX_GIT_REFUSALS);
|
|
142
|
+
/** 整次拒答的体:`{ error, permanence, message }`。`message` 是中心的诊断句(收方不据它判,可为空串)。 */
|
|
143
|
+
export const SandboxGitCredentialRefusal = z
|
|
144
|
+
.object({
|
|
145
|
+
error: z.enum(SANDBOX_GIT_REFUSAL_ERRORS),
|
|
146
|
+
permanence: z.enum(SANDBOX_GIT_PERMANENCE),
|
|
147
|
+
message: z.string(),
|
|
148
|
+
})
|
|
149
|
+
.refine((body) => SANDBOX_GIT_REFUSALS[body.error].permanence === body.permanence, buildQuotedIssue `permanence must be the one the refusal table gives for this error`);
|
package/dist/api/scopes.js
CHANGED
|
@@ -47,6 +47,7 @@ import { z } from "zod";
|
|
|
47
47
|
import { INTEGER } from "../integer.js";
|
|
48
48
|
import { buildQuotedIssue, fixed } from "../quoted-issue.js";
|
|
49
49
|
import { USER_ROLES } from "../config-fns.js";
|
|
50
|
+
import { ownDeclaredValueOf } from "../own-key-read.js";
|
|
50
51
|
import { ACCESS_TOKEN_TTL_SECONDS } from "./auth.js";
|
|
51
52
|
// ── constants ────────────────────────────────────────────────────────────────────────────────────────────
|
|
52
53
|
/** The reserved default scope. Always exists. */
|
|
@@ -230,5 +231,8 @@ export const TOKEN_SCOPE_REFUSALS = ["scope_claim_missing"];
|
|
|
230
231
|
* re-mints one with the claim — a token without it is not a token this contract reads (it used to be read as `"global"`).
|
|
231
232
|
*/
|
|
232
233
|
export function readTokenScope(claims) {
|
|
233
|
-
|
|
234
|
+
// the claim is read from the claims' OWN keys (a polluted `Object.prototype.scope` is not a scope the token carries); a claim that cannot be read is a claim that is missing
|
|
235
|
+
const read = ownDeclaredValueOf(claims, "scope");
|
|
236
|
+
const scope = read.ok ? read.value : undefined;
|
|
237
|
+
return typeof scope === "string" && scope.length > 0 ? { scope } : { refused: "scope_claim_missing" };
|
|
234
238
|
}
|
package/dist/atomic-write.d.ts
CHANGED
|
@@ -29,3 +29,18 @@ export declare function writeFileAtomic(target: string, text: string, kind: Atom
|
|
|
29
29
|
* 入参 `dir` 是已解析到真目录的路径;返回目录的权限位是否让本进程能增删其中的目录项。
|
|
30
30
|
*/
|
|
31
31
|
export declare function directoryAdmitsWrite(dir: string): boolean;
|
|
32
|
+
/** 新建目录的模式:仅属主可读写进(引擎的目录信任判官要求路径上的组件不得组 / 他人可写;0o700 在任何 umask 下都过)。 */
|
|
33
|
+
export declare const OWNER_ONLY_DIRECTORY_MODE = 448;
|
|
34
|
+
/**
|
|
35
|
+
* 原子地**不存在才建** `target`(见文件头「不存在才建」):建成 ⇒ `"created"`;目标名已在(只有 `EEXIST`)⇒ `"present"`(一字不动)。
|
|
36
|
+
* 临时文件独占建出、写完、fsync({@link writeTempExclusive})后 `link` 成目标名。模式 = `kind` 在表里的新建模式经进程 umask(目标按定义不在,没有「已有的模式」可沿用)。
|
|
37
|
+
* 临时文件建不出 / 写不完 / fsync 不了 ⇒ 抛 {@link FsFailure}(落点「往目录里写临时文件」,与 {@link writeFileAtomic} 建临时文件失败同落点同句);`link` 的别的失败原样抛
|
|
38
|
+
* (调用方经 `fsFailureMessage` 取句)。父目录须已存在。
|
|
39
|
+
*/
|
|
40
|
+
export declare function createFileExclusive(target: string, text: string, kind: AtomicFileClass): "created" | "present";
|
|
41
|
+
/**
|
|
42
|
+
* 目录不在就建(连同缺的上级):**逐级** mkdir、每建一级立刻 chmod 到 {@link OWNER_ONLY_DIRECTORY_MODE} —— 模式恰是它,不经 umask(umask 0777 下一次性的递归 mkdir 会先建出
|
|
43
|
+
* 一级 0000 的目录、再也进不去建下一级)。本进程建了 `dir` ⇒ `"created"`;`dir` 已在(或恰被别的进程同时建成)⇒ `"present"`(它的模式一位不动)。
|
|
44
|
+
* 路径上有一节不是目录 ⇒ 抛。
|
|
45
|
+
*/
|
|
46
|
+
export declare function ensureOwnerOnlyDirectory(dir: string): "created" | "present";
|