@geoly-ai/skills-hub 0.1.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/LICENSE +21 -0
- package/README.md +98 -0
- package/bin/skills-hub.mjs +26 -0
- package/package.json +44 -0
- package/src/adapters/index.mjs +832 -0
- package/src/artifact.mjs +376 -0
- package/src/atomic-fs.mjs +166 -0
- package/src/attestation.mjs +136 -0
- package/src/canonical-json.mjs +147 -0
- package/src/cli.mjs +208 -0
- package/src/commands/check.mjs +295 -0
- package/src/commands/context.mjs +235 -0
- package/src/commands/install.mjs +430 -0
- package/src/commands/locks.mjs +197 -0
- package/src/commands/output.mjs +127 -0
- package/src/commands/query.mjs +266 -0
- package/src/commands/recover.mjs +438 -0
- package/src/commands/registry.mjs +123 -0
- package/src/commands/resolve.mjs +171 -0
- package/src/commands/snapshot-access.mjs +91 -0
- package/src/commands/sync-lock.mjs +189 -0
- package/src/crc32c.mjs +27 -0
- package/src/exit-codes.mjs +265 -0
- package/src/fault-inject.mjs +379 -0
- package/src/install.mjs +732 -0
- package/src/journal.mjs +435 -0
- package/src/ledger.mjs +671 -0
- package/src/lock.mjs +98 -0
- package/src/lockfile.mjs +0 -0
- package/src/pack.mjs +792 -0
- package/src/packer.mjs +351 -0
- package/src/plan.mjs +519 -0
- package/src/recover.mjs +1345 -0
- package/src/safe-fs.mjs +252 -0
- package/src/sigstore.mjs +480 -0
- package/src/snapshot.mjs +528 -0
- package/src/stats.mjs +59 -0
- package/src/target.mjs +738 -0
- package/src/telemetry.mjs +393 -0
- package/src/tree-digest.mjs +103 -0
- package/src/trust-roots/README.md +31 -0
- package/src/trust-roots/sigstore-public-good.json +126 -0
- package/src/trust.mjs +563 -0
- package/src/untar.mjs +570 -0
- package/src/upload.mjs +268 -0
- package/src/vendor.mjs +465 -0
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// skill 埋点 —— 规范见 docs/telemetry/00-spec.md
|
|
2
|
+
// 🔴 隐私契约(硬约束,实现不得放宽):
|
|
3
|
+
// · 只收「哪个制品、什么结果」,不收路径、不收内容、不收用户名、不收目录清单
|
|
4
|
+
// · 事件先落本地,上报是**独立**动作;关掉上报不影响本地统计
|
|
5
|
+
// · install_id 是随机 UUID,与账号/机器名/用户名**无任何映射**
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { appendFileSync, existsSync, readFileSync, mkdirSync, openSync, closeSync, statSync, unlinkSync, renameSync, fstatSync, linkSync, fsyncSync } from 'node:fs';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import { homedir, platform, arch } from 'node:os';
|
|
10
|
+
import { writeAtomic, fsyncParentAfter } from './atomic-fs.mjs';
|
|
11
|
+
import { stringify, encodeString, parseStrict } from './canonical-json.mjs';
|
|
12
|
+
import { acquire } from './lock.mjs';
|
|
13
|
+
|
|
14
|
+
export const KINDS = new Set([
|
|
15
|
+
'install', 'update', 'remove', 'check', 'rollback', 'recover', 'sync-lock',
|
|
16
|
+
]);
|
|
17
|
+
export const RESULTS = new Set(['ok', 'skipped', 'failed', 'corrupt']);
|
|
18
|
+
|
|
19
|
+
export function stateDir() {
|
|
20
|
+
return process.env.GEOLY_STATE_DIR ?? join(homedir(), '.local', 'state', 'geoly-skills');
|
|
21
|
+
}
|
|
22
|
+
const queuePath = () => join(stateDir(), 'telemetry', 'queue.ndjson');
|
|
23
|
+
const idPath = () => join(stateDir(), 'telemetry', 'install-id');
|
|
24
|
+
|
|
25
|
+
/** 是否启用埋点:默认开,`GEOLY_TELEMETRY=0` 关。关掉后**一个字节都不写** */
|
|
26
|
+
export function enabled() {
|
|
27
|
+
const v = process.env.GEOLY_TELEMETRY;
|
|
28
|
+
return !(v === '0' || v === 'off' || v === 'false');
|
|
29
|
+
}
|
|
30
|
+
/** M0 的全局 `--offline`:置位后本 CLI 不得有任何网络出口,埋点也不例外 */
|
|
31
|
+
export const offline = () => process.env.GEOLY_OFFLINE === '1';
|
|
32
|
+
|
|
33
|
+
/** 是否上报:默认开;`GEOLY_TELEMETRY_UPLOAD=0` 或 `--offline` 只留本地 */
|
|
34
|
+
export function uploadEnabled() {
|
|
35
|
+
const v = process.env.GEOLY_TELEMETRY_UPLOAD;
|
|
36
|
+
return enabled() && !offline() && !(v === '0' || v === 'off' || v === 'false');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** 随机 install_id,只存本地;与身份无映射 */
|
|
40
|
+
export function installId() {
|
|
41
|
+
const p = idPath();
|
|
42
|
+
const readValid = () => {
|
|
43
|
+
try {
|
|
44
|
+
const v = readFileSync(p, 'utf8').trim();
|
|
45
|
+
return RE_UUID.test(v) ? v : null;
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const existing = readValid();
|
|
50
|
+
if (existing) return existing;
|
|
51
|
+
|
|
52
|
+
mkdirSync(join(stateDir(), 'telemetry'), { recursive: true });
|
|
53
|
+
|
|
54
|
+
// 🔴 **先写满,再让名字出现。**
|
|
55
|
+
//
|
|
56
|
+
// 早先这里是 `openSync(p, 'wx')` 然后往 fd 里写 —— 看着像原子抢占,其实不是:
|
|
57
|
+
// `wx` 一成功文件就存在了,但**内容还没写**。抢输的进程走 EEXIST 分支去读,
|
|
58
|
+
// 读到的是空串。实测(在 open 与 write 之间插 0.4s):4 个进程里 3 个拿到 ""。
|
|
59
|
+
//
|
|
60
|
+
// ⚠️ 又是「存在 ≠ 完整」。今天已经在墓碑、rmtree、这里各栽一次:
|
|
61
|
+
// **判据永远不能是「文件在不在」,要么是内容有效性,要么是只在完整后才出现的名字。**
|
|
62
|
+
//
|
|
63
|
+
// 所以:内容写进临时文件 → fsync → `link` 到正式名(原子 no-replace,
|
|
64
|
+
// 目标已存在就 EEXIST)→ 删临时文件。名字一出现,内容就一定是全的。
|
|
65
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
66
|
+
const id = randomUUID();
|
|
67
|
+
const tmp = `${p}.${process.pid}.${attempt}.tmp`;
|
|
68
|
+
try {
|
|
69
|
+
const fd = openSync(tmp, 'w', 0o644);
|
|
70
|
+
try { appendFileSync(fd, id + '\n'); fsyncSync(fd); } finally { closeSync(fd); }
|
|
71
|
+
try {
|
|
72
|
+
linkSync(tmp, p); // 抢到了
|
|
73
|
+
return id;
|
|
74
|
+
} catch {
|
|
75
|
+
const winner = readValid(); // 别人抢先,且此刻内容必然是完整的
|
|
76
|
+
if (winner) return winner;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
const winner = readValid();
|
|
80
|
+
if (winner) return winner;
|
|
81
|
+
} finally {
|
|
82
|
+
try { unlinkSync(tmp); } catch { /* 没建成 */ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// 反复抢不到又读不出有效值:不落盘,本次用一个临时身份,不要让埋点拖垮主命令
|
|
86
|
+
return randomUUID();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
90
|
+
// 严格 schema:这是隐私契约的**唯一**执行点
|
|
91
|
+
//
|
|
92
|
+
// 🔴 早先这里只做「扫字符串里有没有路径」,那是不够的(Codex 2026-08-26 指出):
|
|
93
|
+
// · `client: { path: '/Users/a' }` —— 值不是字符串,扫描直接 continue,整个对象漏出去
|
|
94
|
+
// · 用户名、邮箱、token、base64 过的路径都不含 `/`,扫描一律放行
|
|
95
|
+
// · 手改队列文件可以塞任意未知键,上报时原样 JSON.stringify 发走
|
|
96
|
+
// 正确做法是**穷举键 + 每个键一个值校验器**,而不是黑名单式地找坏东西。
|
|
97
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
// randomUUID() 产出的就是 v4;正则收窄到 v4,别写成 [1-5] 那样比规格宽
|
|
100
|
+
const RE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
|
|
101
|
+
const RE_AT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
|
|
102
|
+
const RE_SEMVERISH = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,31}$/;
|
|
103
|
+
const RE_ARTIFACT = /^(skill|pack):[a-z0-9][a-z0-9-]*\/[a-z0-9][a-z0-9._-]*@[0-9A-Za-z.+-]{1,32}$/;
|
|
104
|
+
|
|
105
|
+
export const CLIENTS = new Set(['claude', 'cursor', 'codex', 'agents']);
|
|
106
|
+
export const SCOPES = new Set(['global', 'project']);
|
|
107
|
+
// `os`/`arch` 只收已知取值 —— 未知值本身就是一种指纹
|
|
108
|
+
const OSES = new Set(['darwin', 'linux']);
|
|
109
|
+
const ARCHES = new Set(['x64', 'arm64']);
|
|
110
|
+
|
|
111
|
+
// 🔴 `reason` 是**有限代码表**,不是"符合某个形状的字符串"。
|
|
112
|
+
// 形状约束(小写 kebab)已经挡掉路径、邮箱、token,但挡不住 `reason: "alice"`
|
|
113
|
+
// 这种侧信道 —— 一个形状合法的自由字段迟早会被人拿来塞信息(Codex 2026-08-26 指出)。
|
|
114
|
+
// 加新原因就往这张表里加,**过评审**;不要退回成正则。
|
|
115
|
+
export const REASONS = new Set([
|
|
116
|
+
'signature-mismatch', 'digest-mismatch', 'trust-floor-violation', 'attestation-missing',
|
|
117
|
+
'target-busy', 'target-missing', 'target-not-writable', 'unsupported-client',
|
|
118
|
+
'not-found', 'already-installed', 'version-conflict', 'yanked',
|
|
119
|
+
'network-error', 'timeout', 'offline', 'lock-busy',
|
|
120
|
+
'journal-corrupt', 'ledger-corrupt', 'assertion-corrupt', 'user-abort',
|
|
121
|
+
'unknown',
|
|
122
|
+
]);
|
|
123
|
+
|
|
124
|
+
const str = (re) => (v) => typeof v === 'string' && re.test(v);
|
|
125
|
+
const oneOf = (set) => (v) => typeof v === 'string' && set.has(v);
|
|
126
|
+
|
|
127
|
+
// 用 __proto__: null 建表:否则 `FIELDS['__proto__']` 会命中 Object.prototype(真值),
|
|
128
|
+
// `FIELDS['constructor']` 会命中 Object —— 靠「取出来的东西没有 .ok 所以抛 TypeError」
|
|
129
|
+
// 挡住是巧合,不是设计。下面的 hasOwn 才是真正的判据。
|
|
130
|
+
/** 键的穷举表。**不在这张表里的键一律拒绝。** */
|
|
131
|
+
const FIELDS = {
|
|
132
|
+
__proto__: null,
|
|
133
|
+
schema: { required: true, ok: (v) => v === 'geoly.skills.telemetry/1' },
|
|
134
|
+
eid: { required: true, ok: str(RE_UUID) },
|
|
135
|
+
at: { required: true, ok: str(RE_AT) },
|
|
136
|
+
install_id: { required: true, ok: str(RE_UUID) },
|
|
137
|
+
cli: { required: true, ok: str(RE_SEMVERISH) },
|
|
138
|
+
os: { required: true, ok: oneOf(OSES) },
|
|
139
|
+
arch: { required: true, ok: oneOf(ARCHES) },
|
|
140
|
+
node: { required: true, ok: str(/^\d{1,3}\.\d{1,3}\.\d{1,3}$/) },
|
|
141
|
+
kind: { required: true, ok: (v) => KINDS.has(v) },
|
|
142
|
+
result: { required: true, ok: (v) => RESULTS.has(v) },
|
|
143
|
+
artifact: { required: false, ok: str(RE_ARTIFACT) },
|
|
144
|
+
version: { required: false, ok: str(RE_SEMVERISH) },
|
|
145
|
+
client: { required: false, ok: oneOf(CLIENTS) },
|
|
146
|
+
scope: { required: false, ok: oneOf(SCOPES) },
|
|
147
|
+
ms: { required: false, ok: (v) => Number.isInteger(v) && v >= 0 && v <= 86_400_000 },
|
|
148
|
+
reason: { required: false, ok: oneOf(REASONS) },
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 🔴 隐私契约的执行点。落盘、读队列、上报、导出 —— 四个边界都走这一个函数。
|
|
153
|
+
* 任何不合规的事件都**不得**离开本机。
|
|
154
|
+
*/
|
|
155
|
+
export function assertValidEvent(ev) {
|
|
156
|
+
if (ev === null || typeof ev !== 'object' || Array.isArray(ev)) {
|
|
157
|
+
throw new Error('telemetry: 事件必须是普通对象');
|
|
158
|
+
}
|
|
159
|
+
for (const k of Object.keys(ev)) {
|
|
160
|
+
if (!Object.hasOwn(FIELDS, k)) throw new Error(`telemetry: 未知字段 ${k},拒绝`);
|
|
161
|
+
const f = FIELDS[k];
|
|
162
|
+
const v = ev[k];
|
|
163
|
+
if (v !== null && typeof v === 'object') throw new Error(`telemetry: 字段 ${k} 是对象/数组,拒绝`);
|
|
164
|
+
if (!f.ok(v)) throw new Error(`telemetry: 字段 ${k} 的值不合规,拒绝`);
|
|
165
|
+
}
|
|
166
|
+
for (const [k, f] of Object.entries(FIELDS)) {
|
|
167
|
+
// Object.hasOwn 而非 `k in ev`:`in` 会走原型链,`{}` 也会「有」toString
|
|
168
|
+
if (f.required && !Object.hasOwn(ev, k)) throw new Error(`telemetry: 缺少必填字段 ${k}`);
|
|
169
|
+
}
|
|
170
|
+
return ev;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 校验但不抛:用于过滤被手改过的队列 */
|
|
174
|
+
export function isValidEvent(ev) {
|
|
175
|
+
try { assertValidEvent(ev); return true; } catch { return false; }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** 兼容旧名字;语义已并入 assertValidEvent */
|
|
179
|
+
export const assertNoPaths = assertValidEvent;
|
|
180
|
+
|
|
181
|
+
/** 🔴 白名单式构造:只有这些字段能进事件,别的一律丢弃 */
|
|
182
|
+
export function buildEvent({ kind, artifact, version, client, scope, result, ms, reason }) {
|
|
183
|
+
const ev = {
|
|
184
|
+
schema: 'geoly.skills.telemetry/1',
|
|
185
|
+
// 🔴 上报是 at-least-once(POST 成功后、游标落盘前崩溃会重发)。
|
|
186
|
+
// eid 是服务端唯一的去重依据 —— 随机 UUID,不含任何可反推的信息。
|
|
187
|
+
eid: randomUUID(),
|
|
188
|
+
at: new Date().toISOString().replace(/\.\d+Z$/, 'Z'),
|
|
189
|
+
install_id: installId(),
|
|
190
|
+
// 环境变量可被任意注入,所以它也要过 RE_SEMVERISH
|
|
191
|
+
cli: process.env.GEOLY_CLI_VERSION ?? '0.0.0-m1',
|
|
192
|
+
os: platform(), arch: arch(), node: process.versions.node,
|
|
193
|
+
kind, result,
|
|
194
|
+
};
|
|
195
|
+
if (artifact !== undefined) ev.artifact = artifact;
|
|
196
|
+
if (version !== undefined) ev.version = version;
|
|
197
|
+
if (client !== undefined) ev.client = client;
|
|
198
|
+
if (scope !== undefined) ev.scope = scope;
|
|
199
|
+
if (ms !== undefined) ev.ms = typeof ms === 'number' && Number.isFinite(ms) ? Math.max(0, Math.round(ms)) : ms;
|
|
200
|
+
if (reason !== undefined) ev.reason = reason;
|
|
201
|
+
return assertValidEvent(ev);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
// 本地队列
|
|
206
|
+
//
|
|
207
|
+
// 🔴 早先这里是「NDJSON 追加 + 已上报行数游标 + 超限就重写文件截半」。
|
|
208
|
+
// Codex 2026-08-26 指出这组合有**静默丢事件**的窗口,而且不止一个:
|
|
209
|
+
// · flush 在锁内读到快照后开始发;发的期间别的进程触发截断、行号含义变了;
|
|
210
|
+
// flush 成功后按旧快照写回一个偏大的游标 → 截断后保留的新事件被永久跳过
|
|
211
|
+
// · 截断写盘成功、删游标前崩溃 → 旧游标继续跳过保留下来的事件
|
|
212
|
+
// · 截断是「读全文再重写」,与另一个进程的 O_APPEND 相撞会吞掉刚追加的行
|
|
213
|
+
//
|
|
214
|
+
// 病根是**用位置去索引一个会被重写的文件**:位置的含义会变。
|
|
215
|
+
// 加 generation 只是给错误设计打补丁。这里改成:
|
|
216
|
+
// · 游标这个概念**整个删掉**
|
|
217
|
+
// · 淘汰旧数据只用 rename + unlink,**从不重写队列文件**(不会吞掉并发追加)
|
|
218
|
+
// · flush 是**消费式**的:把整代改名进 sending,发成功就删掉
|
|
219
|
+
//
|
|
220
|
+
// 队列是「两代环」:queue.ndjson(当前代)+ queue.1.ndjson(上一代)。
|
|
221
|
+
// 超过上限就 unlink 上一代、把当前代 rename 成上一代、开一个空的当前代。
|
|
222
|
+
// 「丢最旧的」= 那次 unlink,是有意的淘汰,不是意外丢失。
|
|
223
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
/** 单代上限;磁盘占用上界约为 2 倍(当前代 + 上一代) */
|
|
226
|
+
export const MAX_QUEUE_BYTES = 1024 * 1024;
|
|
227
|
+
|
|
228
|
+
const prevQueuePath = () => join(stateDir(), 'telemetry', 'queue.1.ndjson');
|
|
229
|
+
// 报表用的历史:与上报队列**分开**。上报是消费式的,发完队列就空了;
|
|
230
|
+
// 报表不能因为"上报成功"就没数据看,所以另存一份,flush 从不碰它。
|
|
231
|
+
const historyPath = () => join(stateDir(), 'telemetry', 'history.ndjson');
|
|
232
|
+
const prevHistoryPath = () => join(stateDir(), 'telemetry', 'history.1.ndjson');
|
|
233
|
+
export const historyFiles = () => [prevHistoryPath(), historyPath()];
|
|
234
|
+
const sendingPath = () => join(stateDir(), 'telemetry', 'sending.ndjson');
|
|
235
|
+
/** 已发出但推迟删除的墓碑 —— 必须被读到,见 upload.mjs 的 retire() */
|
|
236
|
+
const tombPath = () => join(stateDir(), 'telemetry', 'sending.tomb.ndjson');
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 读的顺序 = 时间顺序:在途 → 上一代 → 当前代。
|
|
240
|
+
*
|
|
241
|
+
* 🔴 这里**没有**中间文件(早先有过一个 `*.staged`,见 upload.mjs 里的说明)。
|
|
242
|
+
* 每多一个中间文件名,就多一个「崩在这一步谁来读它」和「同名覆盖」的问题。
|
|
243
|
+
*/
|
|
244
|
+
export const queueFiles = () => [sendingPath(), prevQueuePath(), queuePath()];
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 🔴 手写序列化,不走 JSON.stringify。
|
|
248
|
+
* JSON.stringify 会在每个值上查 `toJSON`(原语也会,经 GetV 走到原型),
|
|
249
|
+
* 于是被污染的 `Object.prototype.toJSON` / `String.prototype.toJSON`
|
|
250
|
+
* 能把**已经通过校验的对象**换成别的东西再写盘/上报。
|
|
251
|
+
* 事件的值只有受限字符集的字符串和整数,手写既安全又不复杂。
|
|
252
|
+
*/
|
|
253
|
+
export function serializeEvent(ev) {
|
|
254
|
+
const parts = [];
|
|
255
|
+
for (const k of Object.keys(FIELDS)) {
|
|
256
|
+
if (!Object.hasOwn(ev, k)) continue;
|
|
257
|
+
const v = ev[k];
|
|
258
|
+
parts.push(`${encodeString(k)}:${typeof v === 'number' ? String(v) : encodeString(v)}`);
|
|
259
|
+
}
|
|
260
|
+
return `{${parts.join(',')}}`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 追加到本地队列(NDJSON,一行一事件)。
|
|
265
|
+
*
|
|
266
|
+
* 🔴 **绝不向调用方抛错**(威胁模型 T-5:埋点不得让主命令变脆)。
|
|
267
|
+
* 校验失败、磁盘满、目录不可写 —— 一律返回 null,错误留在 `lastError()` 供诊断。
|
|
268
|
+
* 校验本身要严格,但"严格"体现在**不写出去**,不是体现在炸掉安装事务。
|
|
269
|
+
*
|
|
270
|
+
* 不 fsync:丢掉最后几条埋点无所谓,让主命令等一次 fsync 才是真的有害。
|
|
271
|
+
* 追加走 O_APPEND,单行远小于 PIPE_BUF,并发追加不会交错。
|
|
272
|
+
*/
|
|
273
|
+
let _lastError = null;
|
|
274
|
+
export const lastError = () => _lastError;
|
|
275
|
+
|
|
276
|
+
export function record(input) {
|
|
277
|
+
if (!enabled()) return null;
|
|
278
|
+
try {
|
|
279
|
+
const ev = buildEvent(input);
|
|
280
|
+
mkdirSync(join(stateDir(), 'telemetry'), { recursive: true });
|
|
281
|
+
const line = serializeEvent(ev) + '\n';
|
|
282
|
+
// 🔴 open 与 append 之间,别的进程可能把这个文件 unlink 掉(换代删上一代、
|
|
283
|
+
// retire 删 sending)。那样这一行就写进了一个没有目录项的 inode —— 谁都读不到,
|
|
284
|
+
// 静默丢事件。写完用 fstat 查 nlink:为 0 就说明中招了,换一个新 fd 重写一遍。
|
|
285
|
+
// 重试有限次,避免在病态并发下打转。
|
|
286
|
+
appendDurable(queuePath(), line);
|
|
287
|
+
appendDurable(historyPath(), line);
|
|
288
|
+
rotateIfNeeded(queuePath(), prevQueuePath());
|
|
289
|
+
rotateIfNeeded(historyPath(), prevHistoryPath());
|
|
290
|
+
_lastError = null;
|
|
291
|
+
return ev;
|
|
292
|
+
} catch (err) {
|
|
293
|
+
_lastError = err;
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* 追加一行,并确认它落进了一个**还有目录项**的 inode。
|
|
300
|
+
*
|
|
301
|
+
* open 与 append 之间,别的进程可能把这个文件删了(换代删上一代、retire 删墓碑)。
|
|
302
|
+
* 那样这一行就写进了孤儿 inode,谁都读不到。写完 fstat 查 nlink,中招就换 fd 重来。
|
|
303
|
+
*
|
|
304
|
+
* ⚠️ **这个检查是 TOCTOU 的**:它发现不了「fstat 之后、close 之前才被删」。
|
|
305
|
+
* 那个窗口靠上报侧的"延迟一个周期再删"来压缩,见 upload.mjs 的 retire()
|
|
306
|
+
* 与规格 §6 的 T-15 —— 那是**已知且接受**的残余风险,不是被闭合了。
|
|
307
|
+
*/
|
|
308
|
+
export function appendDurable(path, line) {
|
|
309
|
+
for (let attempt = 0; attempt < 4; attempt++) {
|
|
310
|
+
const fd = openSync(path, 'a', 0o644);
|
|
311
|
+
let orphaned;
|
|
312
|
+
try {
|
|
313
|
+
appendFileSync(fd, line);
|
|
314
|
+
orphaned = fstatSync(fd).nlink === 0;
|
|
315
|
+
} finally { closeSync(fd); }
|
|
316
|
+
if (!orphaned) return;
|
|
317
|
+
}
|
|
318
|
+
throw new Error('telemetry: 文件反复被并发删除,放弃这条事件');
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 换代。只做 unlink + rename,**不重写任何文件**,所以与并发追加无冲突:
|
|
323
|
+
* 别的进程用已打开的 fd 追加时,数据落进被改名后的那个 inode,仍在上一代里被读到。
|
|
324
|
+
*
|
|
325
|
+
* 拿不到锁就跳过 —— 换代晚一次完全无害(下次追加还会再判一遍),
|
|
326
|
+
* 而让 record 阻塞在锁上是有害的。
|
|
327
|
+
*/
|
|
328
|
+
function rotateIfNeeded(cur, prev) {
|
|
329
|
+
let size;
|
|
330
|
+
try { size = statSync(cur).size; } catch { return; }
|
|
331
|
+
if (size <= MAX_QUEUE_BYTES) return;
|
|
332
|
+
let release;
|
|
333
|
+
try { release = acquire(lockPath()); } catch { return; } // busy:下次再说
|
|
334
|
+
try {
|
|
335
|
+
// 再判一次:等锁的期间别的进程可能已经换过代了
|
|
336
|
+
try { if (statSync(cur).size <= MAX_QUEUE_BYTES) return; } catch { return; }
|
|
337
|
+
try { unlinkSync(prev); } catch { /* 本来就没有上一代 */ }
|
|
338
|
+
renameSync(cur, prev);
|
|
339
|
+
fsyncParentAfter(cur);
|
|
340
|
+
} finally {
|
|
341
|
+
release();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export const lockPath = () => join(stateDir(), 'telemetry', 'upload.lock.db');
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 读本地全部事件(在途 → 上一代 → 当前代,即时间顺序)。
|
|
349
|
+
* 🔴 队列文件是可被手改的,所以**读回来也要过一遍严格校验**:
|
|
350
|
+
* 解析失败或不合规的行一律丢弃,绝不让它们流到上报或导出。
|
|
351
|
+
* 同一 eid 只留第一次出现的那条(flush 失败重入时可能有重叠)。
|
|
352
|
+
*/
|
|
353
|
+
function readFiles(paths) {
|
|
354
|
+
const out = [];
|
|
355
|
+
const byEid = new Map();
|
|
356
|
+
for (const p of paths) {
|
|
357
|
+
let text;
|
|
358
|
+
// 不拿锁读,所以可能撞上 rename 的中间态:existsSync 与读之间文件就没了。
|
|
359
|
+
// 读不到就跳过这一个文件,不要把诊断命令炸掉。
|
|
360
|
+
try { text = readFileSync(p, 'utf8'); } catch { continue; }
|
|
361
|
+
for (const line of text.split('\n')) {
|
|
362
|
+
if (!line) continue;
|
|
363
|
+
let ev;
|
|
364
|
+
// parseStrict 而非 JSON.parse:重复 key 会被 JSON.parse 静默取最后一个
|
|
365
|
+
try { ev = parseStrict(line); } catch { continue; }
|
|
366
|
+
if (!isValidEvent(ev)) continue;
|
|
367
|
+
const prior = byEid.get(ev.eid);
|
|
368
|
+
if (prior) {
|
|
369
|
+
// 崩溃重发产生的是**同 eid 同内容**的副本,去重即可。
|
|
370
|
+
// 同 eid 不同内容说明有人改过文件或实现有 bug —— 不静默吞掉。
|
|
371
|
+
if (serializeEvent(prior) !== serializeEvent(ev)) {
|
|
372
|
+
_lastError = new Error(`telemetry: eid ${ev.eid} 出现内容不同的副本,已丢弃后者`);
|
|
373
|
+
}
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
byEid.set(ev.eid, ev);
|
|
377
|
+
out.push(ev);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** 待上报的事件(上报会消费掉它们) */
|
|
384
|
+
export function readAll() { return readFiles(queueFiles()); }
|
|
385
|
+
|
|
386
|
+
/** 报表用的历史。上报**不**消费它,所以发完了报表照样有数据看。 */
|
|
387
|
+
export function readHistory() { return readFiles(historyFiles()); }
|
|
388
|
+
|
|
389
|
+
/** 导出 canonical JSON(给静态页读)。导出也是一个出口,同样过校验。 */
|
|
390
|
+
export function exportJson(events = readHistory()) {
|
|
391
|
+
const clean = events.filter(isValidEvent);
|
|
392
|
+
return stringify({ schema: 'geoly.skills.telemetry-export/1', count: clean.length, events: clean });
|
|
393
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// geoly-tree-v1 / geoly-tx-v1 —— 规范见 01-artifacts.md §6、04-install.md §5.10
|
|
2
|
+
// 两者复用同一套 path 规范化与 leaf 编码,只是域分离前缀与覆盖面不同。
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
5
|
+
import { join, relative, sep } from 'node:path';
|
|
6
|
+
|
|
7
|
+
const ALLOWED_FILE_MODES = new Set([0o644, 0o755]);
|
|
8
|
+
const DIR_MODE = 0o755;
|
|
9
|
+
|
|
10
|
+
function leaf(content) {
|
|
11
|
+
const len = Buffer.alloc(8);
|
|
12
|
+
len.writeBigUInt64BE(BigInt(content.length));
|
|
13
|
+
return createHash('sha256').update('blob\0').update(len).update(content).digest('hex');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normPath(rel) {
|
|
17
|
+
const p = rel.split(sep).join('/');
|
|
18
|
+
return p.normalize('NFC');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 无跟随遍历;遇到不被允许的类型直接抛错(fail-closed) */
|
|
22
|
+
function walk(root, { includeDirs }) {
|
|
23
|
+
const files = [], dirs = [];
|
|
24
|
+
(function rec(dir) {
|
|
25
|
+
for (const name of readdirSync(dir)) {
|
|
26
|
+
const abs = join(dir, name);
|
|
27
|
+
const st = lstatSync(abs);
|
|
28
|
+
if (st.isSymbolicLink()) throw new Error(`tree-digest: 拒绝 symlink ${abs}`);
|
|
29
|
+
if (st.isDirectory()) {
|
|
30
|
+
if ((st.mode & 0o777) !== DIR_MODE && includeDirs) {
|
|
31
|
+
throw new Error(`tree-digest: 目录 mode 必须是 0755,${abs} 是 ${(st.mode & 0o777).toString(8)}`);
|
|
32
|
+
}
|
|
33
|
+
dirs.push(abs); rec(abs); continue;
|
|
34
|
+
}
|
|
35
|
+
if (!st.isFile()) throw new Error(`tree-digest: 拒绝非普通文件 ${abs}`);
|
|
36
|
+
// 🔴 nlink 检查只针对普通文件;对目录做会让每个正常 tx 都无法成像
|
|
37
|
+
if (st.nlink !== 1) throw new Error(`tree-digest: 拒绝 hardlink(nlink=${st.nlink})${abs}`);
|
|
38
|
+
const mode = st.mode & 0o777;
|
|
39
|
+
if (!ALLOWED_FILE_MODES.has(mode)) {
|
|
40
|
+
throw new Error(`tree-digest: 文件 mode 只允许 0644/0755,${abs} 是 ${mode.toString(8)}`);
|
|
41
|
+
}
|
|
42
|
+
files.push(abs);
|
|
43
|
+
}
|
|
44
|
+
})(root);
|
|
45
|
+
return { files, dirs };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** geoly-tree-v1:只覆盖文件叶子(制品禁止空目录,目录结构由文件路径隐含) */
|
|
49
|
+
export function treeDigest(root) {
|
|
50
|
+
const { files } = walk(root, { includeDirs: false });
|
|
51
|
+
const entries = files.map(abs => {
|
|
52
|
+
const st = lstatSync(abs);
|
|
53
|
+
const mode = (st.mode & 0o777) === 0o755 ? '0755' : '0644';
|
|
54
|
+
return { path: normPath(relative(root, abs)), mode, leaf: leaf(readFileSync(abs)) };
|
|
55
|
+
});
|
|
56
|
+
entries.sort((a, b) => Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8')));
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const h = createHash('sha256').update('geoly-tree-v1\n');
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
const fold = e.path.toLowerCase();
|
|
61
|
+
if (seen.has(fold)) throw new Error(`tree-digest: 大小写折叠后重名 ${e.path}`);
|
|
62
|
+
seen.add(fold);
|
|
63
|
+
h.update(Buffer.from(e.path, 'utf8')).update(Buffer.from([0]))
|
|
64
|
+
.update(e.mode).update(Buffer.from([0])).update(e.leaf).update('\n');
|
|
65
|
+
}
|
|
66
|
+
return 'geoly-tree-v1:sha256:' + h.digest('hex');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* geoly-tx-v1:目录项也进摘要(含空目录、含 tx 根本身)。
|
|
71
|
+
* 🔴 不能复用 geoly-tree-v1 —— 它只覆盖文件叶子,tx 的 stage/ retired/ 可以是空的,
|
|
72
|
+
* 空目录被删掉摘要不变,「落入同一等价类」就不成立。
|
|
73
|
+
*/
|
|
74
|
+
export function txDigest(root) {
|
|
75
|
+
const { files, dirs } = walk(root, { includeDirs: true });
|
|
76
|
+
const items = [];
|
|
77
|
+
items.push({ kind: 'd', path: '', mode: '0755' }); // 🔴 根目录本身也计入
|
|
78
|
+
for (const d of dirs) items.push({ kind: 'd', path: normPath(relative(root, d)), mode: '0755' });
|
|
79
|
+
for (const f of files) {
|
|
80
|
+
const st = lstatSync(f);
|
|
81
|
+
items.push({
|
|
82
|
+
kind: 'f', path: normPath(relative(root, f)),
|
|
83
|
+
mode: (st.mode & 0o777) === 0o755 ? '0755' : '0644',
|
|
84
|
+
leaf: leaf(readFileSync(f)),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
items.sort((a, b) => {
|
|
88
|
+
const c = Buffer.compare(Buffer.from(a.path, 'utf8'), Buffer.from(b.path, 'utf8'));
|
|
89
|
+
return c !== 0 ? c : (a.kind === b.kind ? 0 : a.kind === 'd' ? -1 : 1); // "d" < "f"
|
|
90
|
+
});
|
|
91
|
+
const seen = new Set();
|
|
92
|
+
const h = createHash('sha256').update('geoly-tx-v1\n');
|
|
93
|
+
for (const e of items) {
|
|
94
|
+
const key = e.kind + '\0' + e.path;
|
|
95
|
+
if (seen.has(key)) throw new Error(`tx-digest: 重复条目 ${key}`);
|
|
96
|
+
seen.add(key);
|
|
97
|
+
h.update(e.kind).update(Buffer.from([0]))
|
|
98
|
+
.update(Buffer.from(e.path, 'utf8')).update(Buffer.from([0])).update(e.mode);
|
|
99
|
+
if (e.kind === 'f') h.update(Buffer.from([0])).update(e.leaf);
|
|
100
|
+
h.update('\n');
|
|
101
|
+
}
|
|
102
|
+
return 'geoly-tx-v1:sha256:' + h.digest('hex');
|
|
103
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# 内置信任根
|
|
2
|
+
|
|
3
|
+
🔴 **这是整个验签体系唯一的信任输入。**
|
|
4
|
+
|
|
5
|
+
`sigstore-public-good.json` 是 Sigstore 公共实例的 TUF 根,**编译进包**(`package.json`
|
|
6
|
+
的 `files` 含 `src`),随 CLI 一起分发。
|
|
7
|
+
|
|
8
|
+
## 为什么必须内置
|
|
9
|
+
|
|
10
|
+
`docs/m0/02-registry.md` §8.1 要求信任根随 CLI 内置。理由是:验签器本身不认证信任根——
|
|
11
|
+
它只负责「按这个根去验」。**一旦这个根来自网络或缓存,对手可以自带 CA + CT log + Rekor,
|
|
12
|
+
整套验证会一次性失守,而且全程显示「验签通过」。**
|
|
13
|
+
|
|
14
|
+
所以:
|
|
15
|
+
|
|
16
|
+
- **绝不从网络读**,也不从用户可写的缓存读
|
|
17
|
+
- **绝不接受命令行/环境变量覆盖**
|
|
18
|
+
- `--offline` 下必须照常可用(本来就不出网)
|
|
19
|
+
|
|
20
|
+
⚠️ §8.2 自己承认过,「内置 + npm provenance」这一跳不是自洽闭环——
|
|
21
|
+
根的真伪最终依赖发包渠道。这是**已知的**信任边界,不要假装它不存在。
|
|
22
|
+
|
|
23
|
+
## 版本
|
|
24
|
+
|
|
25
|
+
| 字段 | 值 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| 来源 | Sigstore public-good instance TUF root |
|
|
28
|
+
| 取得时间 | 2026-08-26 |
|
|
29
|
+
|
|
30
|
+
🔴 **更新这个文件等于更新信任锚点**,必须走人工评审,且要在 commit message 里
|
|
31
|
+
写清新根的来源与校验方式。不要把它接进任何自动更新流程。
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
{
|
|
2
|
+
"mediaType": "application/vnd.dev.sigstore.trustedroot+json;version=0.1",
|
|
3
|
+
"tlogs": [
|
|
4
|
+
{
|
|
5
|
+
"baseUrl": "https://rekor.sigstore.dev",
|
|
6
|
+
"hashAlgorithm": "SHA2_256",
|
|
7
|
+
"publicKey": {
|
|
8
|
+
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE2G2Y+2tabdTV5BcGiBIx0a9fAFwrkBbmLSGtks4L3qX6yYY0zufBnhC8Ur/iy55GhWP/9A/bY2LhC30M9+RYtw==",
|
|
9
|
+
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
|
10
|
+
"validFor": {
|
|
11
|
+
"start": "2021-01-12T11:53:27Z"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"logId": {
|
|
15
|
+
"keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0="
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"baseUrl": "https://log2025-1.rekor.sigstore.dev",
|
|
20
|
+
"hashAlgorithm": "SHA2_256",
|
|
21
|
+
"publicKey": {
|
|
22
|
+
"rawBytes": "MCowBQYDK2VwAyEAt8rlp1knGwjfbcXAYPYAkn0XiLz1x8O4t0YkEhie244=",
|
|
23
|
+
"keyDetails": "PKIX_ED25519",
|
|
24
|
+
"validFor": {
|
|
25
|
+
"start": "2025-09-23T00:00:00Z"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"logId": {
|
|
29
|
+
"keyId": "zxGZFVvd0FEmjR8WrFwMdcAJ9vtaY/QXf44Y1wUeP6A="
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"certificateAuthorities": [
|
|
34
|
+
{
|
|
35
|
+
"subject": {
|
|
36
|
+
"organization": "sigstore.dev",
|
|
37
|
+
"commonName": "sigstore"
|
|
38
|
+
},
|
|
39
|
+
"uri": "https://fulcio.sigstore.dev",
|
|
40
|
+
"certChain": {
|
|
41
|
+
"certificates": [
|
|
42
|
+
{
|
|
43
|
+
"rawBytes": "MIIB+DCCAX6gAwIBAgITNVkDZoCiofPDsy7dfm6geLbuhzAKBggqhkjOPQQDAzAqMRUwEwYDVQQKEwxzaWdzdG9yZS5kZXYxETAPBgNVBAMTCHNpZ3N0b3JlMB4XDTIxMDMwNzAzMjAyOVoXDTMxMDIyMzAzMjAyOVowKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLSyA7Ii5k+pNO8ZEWY0ylemWDowOkNa3kL+GZE5Z5GWehL9/A9bRNA3RbrsZ5i0JcastaRL7Sp5fp/jD5dxqc/UdTVnlvS16an+2Yfswe/QuLolRUCrcOE2+2iA5+tzd6NmMGQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwHQYDVR0OBBYEFMjFHQBBmiQpMlEk6w2uSu1KBtPsMB8GA1UdIwQYMBaAFMjFHQBBmiQpMlEk6w2uSu1KBtPsMAoGCCqGSM49BAMDA2gAMGUCMH8liWJfMui6vXXBhjDgY4MwslmN/TJxVe/83WrFomwmNf056y1X48F9c4m3a3ozXAIxAKjRay5/aj/jsKKGIkmQatjI8uupHr/+CxFvaJWmpYqNkLDGRU+9orzh5hI2RrcuaQ=="
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
},
|
|
47
|
+
"validFor": {
|
|
48
|
+
"start": "2021-03-07T03:20:29Z",
|
|
49
|
+
"end": "2022-12-31T23:59:59.999Z"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"subject": {
|
|
54
|
+
"organization": "sigstore.dev",
|
|
55
|
+
"commonName": "sigstore"
|
|
56
|
+
},
|
|
57
|
+
"uri": "https://fulcio.sigstore.dev",
|
|
58
|
+
"certChain": {
|
|
59
|
+
"certificates": [
|
|
60
|
+
{
|
|
61
|
+
"rawBytes": "MIICGjCCAaGgAwIBAgIUALnViVfnU0brJasmRkHrn/UnfaQwCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMjA0MTMyMDA2MTVaFw0zMTEwMDUxMzU2NThaMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE8RVS/ysH+NOvuDZyPIZtilgUF9NlarYpAd9HP1vBBH1U5CV77LSS7s0ZiH4nE7Hv7ptS6LvvR/STk798LVgMzLlJ4HeIfF3tHSaexLcYpSASr1kS0N/RgBJz/9jWCiXno3sweTAOBgNVHQ8BAf8EBAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU39Ppz1YkEZb5qNjpKFWixi4YZD8wHwYDVR0jBBgwFoAUWMAeX5FFpWapesyQoZMi0CrFxfowCgYIKoZIzj0EAwMDZwAwZAIwPCsQK4DYiZYDPIaDi5HFKnfxXx6ASSVmERfsynYBiX2X6SJRnZU84/9DZdnFvvxmAjBOt6QpBlc4J/0DxvkTCqpclvziL6BCCPnjdlIB3Pu3BxsPmygUY7Ii2zbdCdliiow="
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"rawBytes": "MIIB9zCCAXygAwIBAgIUALZNAPFdxHPwjeDloDwyYChAO/4wCgYIKoZIzj0EAwMwKjEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MREwDwYDVQQDEwhzaWdzdG9yZTAeFw0yMTEwMDcxMzU2NTlaFw0zMTEwMDUxMzU2NThaMCoxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjERMA8GA1UEAxMIc2lnc3RvcmUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAT7XeFT4rb3PQGwS4IajtLk3/OlnpgangaBclYpsYBr5i+4ynB07ceb3LP0OIOZdxexX69c5iVuyJRQ+Hz05yi+UF3uBWAlHpiS5sh0+H2GHE7SXrk1EC5m1Tr19L9gg92jYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRYwB5fkUWlZql6zJChkyLQKsXF+jAfBgNVHSMEGDAWgBRYwB5fkUWlZql6zJChkyLQKsXF+jAKBggqhkjOPQQDAwNpADBmAjEAj1nHeXZp+13NWBNa+EDsDP8G1WWg1tCMWP/WHPqpaVo0jhsweNFZgSs0eE7wYI4qAjEA2WB9ot98sIkoF3vZYdd3/VtWB5b9TNMea7Ix/stJ5TfcLLeABLE4BNJOsQ4vnBHJ"
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
},
|
|
68
|
+
"validFor": {
|
|
69
|
+
"start": "2022-04-13T20:06:15Z"
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
],
|
|
73
|
+
"ctlogs": [
|
|
74
|
+
{
|
|
75
|
+
"baseUrl": "https://ctfe.sigstore.dev/test",
|
|
76
|
+
"hashAlgorithm": "SHA2_256",
|
|
77
|
+
"publicKey": {
|
|
78
|
+
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbfwR+RJudXscgRBRpKX1XFDy3PyudDxz/SfnRi1fT8ekpfBd2O1uoz7jr3Z8nKzxA69EUQ+eFCFI3zeubPWU7w==",
|
|
79
|
+
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
|
80
|
+
"validFor": {
|
|
81
|
+
"start": "2021-03-14T00:00:00Z",
|
|
82
|
+
"end": "2022-10-31T23:59:59.999Z"
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"logId": {
|
|
86
|
+
"keyId": "CGCS8ChS/2hF0dFrJ4ScRWcYrBY9wzjSbea8IgY2b3I="
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"baseUrl": "https://ctfe.sigstore.dev/2022",
|
|
91
|
+
"hashAlgorithm": "SHA2_256",
|
|
92
|
+
"publicKey": {
|
|
93
|
+
"rawBytes": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEiPSlFi0CmFTfEjCUqF9HuCEcYXNKAaYalIJmBZ8yyezPjTqhxrKBpMnaocVtLJBI1eM3uXnQzQGAJdJ4gs9Fyw==",
|
|
94
|
+
"keyDetails": "PKIX_ECDSA_P256_SHA_256",
|
|
95
|
+
"validFor": {
|
|
96
|
+
"start": "2022-10-20T00:00:00Z"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"logId": {
|
|
100
|
+
"keyId": "3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4="
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
],
|
|
104
|
+
"timestampAuthorities": [
|
|
105
|
+
{
|
|
106
|
+
"subject": {
|
|
107
|
+
"organization": "sigstore.dev",
|
|
108
|
+
"commonName": "sigstore-tsa-selfsigned"
|
|
109
|
+
},
|
|
110
|
+
"uri": "https://timestamp.sigstore.dev/api/v1/timestamp",
|
|
111
|
+
"certChain": {
|
|
112
|
+
"certificates": [
|
|
113
|
+
{
|
|
114
|
+
"rawBytes": "MIICEDCCAZagAwIBAgIUOhNULwyQYe68wUMvy4qOiyojiwwwCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMC4xFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEVMBMGA1UEAxMMc2lnc3RvcmUtdHNhMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE4ra2Z8hKNig2T9kFjCAToGG30jky+WQv3BzL+mKvh1SKNR/UwuwsfNCg4sryoYAd8E6isovVA3M4aoNdm9QDi50Z8nTEyvqgfDPtTIwXItfiW/AFf1V7uwkbkAoj0xxco2owaDAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0OBBYEFIn9eUOHz9BlRsMCRscsc1t9tOsDMB8GA1UdIwQYMBaAFJjsAe9/u1H/1JUeb4qImFMHic6/MBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMAoGCCqGSM49BAMDA2gAMGUCMDtpsV/6KaO0qyF/UMsX2aSUXKQFdoGTptQGc0ftq1csulHPGG6dsmyMNd3JB+G3EQIxAOajvBcjpJmKb4Nv+2Taoj8Uc5+b6ih6FXCCKraSqupe07zqswMcXJTe1cExvHvvlw=="
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"rawBytes": "MIIB9zCCAXygAwIBAgIUV7f0GLDOoEzIh8LXSW80OJiUp14wCgYIKoZIzj0EAwMwOTEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MSAwHgYDVQQDExdzaWdzdG9yZS10c2Etc2VsZnNpZ25lZDAeFw0yNTA0MDgwNjU5NDNaFw0zNTA0MDYwNjU5NDNaMDkxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEgMB4GA1UEAxMXc2lnc3RvcmUtdHNhLXNlbGZzaWduZWQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQUQNtfRT/ou3YATa6wB/kKTe70cfJwyRIBovMnt8RcJph/COE82uyS6FmppLLL1VBPGcPfpQPYJNXzWwi8icwhKQ6W/Qe2h3oebBb2FHpwNJDqo+TMaC/tdfkv/ElJB72jRTBDMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBSY7AHvf7tR/9SVHm+KiJhTB4nOvzAKBggqhkjOPQQDAwNpADBmAjEAwGEGrfGZR1cen1R8/DTVMI943LssZmJRtDp/i7SfGHmGRP6gRbuj9vOK3b67Z0QQAjEAuT2H673LQEaHTcyQSZrkp4mX7WwkmF+sVbkYY5mXN+RMH13KUEHHOqASaemYWK/E"
|
|
118
|
+
}
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
"validFor": {
|
|
122
|
+
"start": "2025-07-04T00:00:00Z"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
]
|
|
126
|
+
}
|