blun-king-cli 9.1.563 → 9.1.565

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 (85) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  4. package/agent-spine-plugin/.mcp.json +8 -8
  5. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  6. package/agent-spine-plugin/LICENSE +186 -186
  7. package/agent-spine-plugin/README.md +394 -0
  8. package/agent-spine-plugin/SECURITY.md +47 -0
  9. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  10. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  11. package/agent-spine-plugin/bin/agentspine.js +7 -7
  12. package/agent-spine-plugin/blun.plugin.json +33 -33
  13. package/agent-spine-plugin/hooks/codex.json +47 -47
  14. package/agent-spine-plugin/hooks/hooks.json +106 -106
  15. package/agent-spine-plugin/hooks/version.json +5 -5
  16. package/agent-spine-plugin/package.json +69 -69
  17. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  18. package/agent-spine-plugin/skill/SKILL.md +76 -76
  19. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  20. package/agent-spine-plugin/src/cli.js +1488 -1442
  21. package/agent-spine-plugin/src/hook.js +886 -812
  22. package/agent-spine-plugin/src/index.js +93 -93
  23. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  24. package/agent-spine-plugin/src/lib/attention.js +755 -755
  25. package/agent-spine-plugin/src/lib/audit.js +351 -342
  26. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  27. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  28. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  29. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  30. package/agent-spine-plugin/src/lib/context.js +154 -154
  31. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  32. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  33. package/agent-spine-plugin/src/lib/documents.js +217 -217
  34. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  35. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  36. package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
  37. package/agent-spine-plugin/src/lib/graph.js +337 -337
  38. package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
  39. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  40. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  41. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  42. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  43. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  44. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  45. package/agent-spine-plugin/src/lib/paths.js +109 -109
  46. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  47. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  48. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  49. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  50. package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
  51. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  52. package/agent-spine-plugin/src/lib/source-roots.js +529 -482
  53. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  54. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  55. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  56. package/agent-spine-plugin/src/mcp.js +597 -572
  57. package/agent-spine-plugin/src/version.js +1 -1
  58. package/agent-spine-plugin/src/worker.js +202 -195
  59. package/bin/active-steer-priority-policy.cjs +24 -0
  60. package/bin/launcher-runtime.js +8 -1
  61. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  62. package/bin/thinking-activity-status-policy.cjs +132 -0
  63. package/bin/thinking-only-guard.cjs +75 -0
  64. package/bin/tool-call-loop-policy.cjs +53 -0
  65. package/bin/turn-thinking-policy.cjs +25 -1
  66. package/blun.mjs +554 -64
  67. package/package.json +4 -1
  68. package/standard-skills/translate-native/LICENSE +21 -21
  69. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  70. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  71. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  72. package/standard-skills/translate-native/references/structured-content.md +72 -72
  73. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  74. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  75. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  76. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  77. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  78. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  79. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  80. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  81. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  82. package/standard-tools/language-guard/check_diacritics.py +353 -353
  83. package/standard-tools/language-guard/guard_service_client.py +264 -264
  84. package/standard-tools/language-guard/language_quality.py +377 -377
  85. package/standard-tools/language-guard/translation_guard.py +916 -916
@@ -1,499 +1,499 @@
1
- import { createHash, randomUUID } from "node:crypto";
2
- import { lookup as dnsLookup } from "node:dns/promises";
3
- import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
4
- import { request as httpsRequest } from "node:https";
5
- import { isIP } from "node:net";
6
- import { join } from "node:path";
7
- import {
8
- assertTrustedIdentity, loadTrust, signEnvelope, verifyEnvelope
9
- } from "./authentication.js";
10
- import { buildCatalog } from "./catalog.js";
11
- import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
12
- import {
13
- buildHttpsSnapshot, fetchHttpsSnapshot, importHttpsSnapshot, resolveHttpsEndpoint
14
- } from "./https-transport.js";
15
- import {
16
- httpsObjectUrl, putHttpsSnapshot, validateHttpsObjectBase
17
- } from "./object-transport.js";
18
- import { projectStateDir, statePathIsScanExcluded } from "./paths.js";
19
-
20
- const SCHEMA = "agentspine.feed/v1";
21
- const STATE_SCHEMA = "agentspine.feed-state/v1";
22
- const CONFIRMATION = "local-share-confirmed";
23
- const MAX_ENTRIES = 256;
24
- const MAX_FEED_BYTES = 256 * 1024;
25
- const MAX_STATE_BYTES = 4 * 1024 * 1024;
26
- const MAX_TOKEN_BYTES = 8192;
27
- const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
28
- const DIGEST_RE = /^[a-f0-9]{64}$/;
29
- const ETAG_RE = /^"[^"\r\n]{1,128}"$/;
30
- const TOKEN_ENV_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
31
-
32
- function canonical(value) {
33
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
34
- if (value && typeof value === "object") {
35
- return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
36
- }
37
- return JSON.stringify(value);
38
- }
39
-
40
- function digest(value) {
41
- return createHash("sha256").update(canonical(value)).digest("hex");
42
- }
43
-
44
- function exactKeys(value, expected) {
45
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
46
- const actual = Object.keys(value).sort();
47
- const wanted = [...expected].sort();
48
- return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
49
- }
50
-
51
- function validDate(value) {
52
- return typeof value === "string" && Number.isFinite(new Date(value).getTime());
53
- }
54
-
55
- function isoDate(value, field = "date") {
56
- const parsed = value instanceof Date ? value : new Date(value);
57
- if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
58
- return parsed.toISOString();
59
- }
60
-
61
- function bearerToken(tokenEnv, environment) {
62
- if (tokenEnv === null || tokenEnv === undefined) return null;
63
- if (!TOKEN_ENV_RE.test(tokenEnv)) throw new Error("tokenEnv must be an uppercase environment variable name");
64
- const value = environment[tokenEnv];
65
- if (typeof value !== "string" || !value) throw new Error(`HTTPS feed token environment variable is missing: ${tokenEnv}`);
66
- if (Buffer.byteLength(value) > MAX_TOKEN_BYTES || /[\0-\x1f\x7f]/.test(value)) {
67
- throw new Error("HTTPS feed bearer token is invalid");
68
- }
69
- return value;
70
- }
71
-
72
- function timeout(value) {
73
- if (!Number.isInteger(value) || value < 1000 || value > 30000) {
74
- throw new Error("HTTPS feed timeout must be between 1000 and 30000 milliseconds");
75
- }
76
- return value;
77
- }
78
-
79
- async function within(promise, milliseconds, message) {
80
- let timer;
81
- try {
82
- return await Promise.race([
83
- promise,
84
- new Promise((_resolve, reject) => {
85
- timer = setTimeout(() => reject(new Error(message)), milliseconds);
86
- })
87
- ]);
88
- } finally {
89
- clearTimeout(timer);
90
- }
91
- }
92
-
93
- export function httpsFeedUrl(baseUrl, feedId) {
94
- if (!ID_RE.test(feedId || "")) throw new Error("feedId must be a stable, whitespace-free identifier");
95
- const endpoint = validateHttpsObjectBase(baseUrl);
96
- const name = createHash("sha256").update(feedId).digest("hex");
97
- endpoint.pathname = `${endpoint.pathname}/feeds/${name}.json`;
98
- return endpoint.toString();
99
- }
100
-
101
- function validateEntry(entry) {
102
- if (!exactKeys(entry, [
103
- "sequence", "snapshotId", "snapshotDigest", "previousDigest", "publishedAt", "authority", "digest"
104
- ]) || !Number.isSafeInteger(entry.sequence) || entry.sequence < 1
105
- || !ID_RE.test(entry.snapshotId || "") || !DIGEST_RE.test(entry.snapshotDigest || "")
106
- || !(entry.previousDigest === null || DIGEST_RE.test(entry.previousDigest))
107
- || !validDate(entry.publishedAt) || entry.authority !== "context-only"
108
- || !DIGEST_RE.test(entry.digest || "")) {
109
- throw new Error("HTTPS feed entry is invalid");
110
- }
111
- const { digest: _digest, ...body } = entry;
112
- if (digest(body) !== entry.digest) throw new Error("HTTPS feed entry failed its integrity check");
113
- return entry;
114
- }
115
-
116
- export function validateHttpsFeed(envelope) {
117
- const verified = verifyEnvelope(envelope, "manifest");
118
- const feed = verified.payload;
119
- if (!exactKeys(feed, [
120
- "schema", "feedId", "scopeId", "adapterId", "sequence", "entries", "authority", "digest"
121
- ]) || feed.schema !== SCHEMA || !ID_RE.test(feed.feedId || "")
122
- || !ID_RE.test(feed.scopeId || "") || !ID_RE.test(feed.adapterId || "")
123
- || !Number.isSafeInteger(feed.sequence) || feed.sequence < 1
124
- || !Array.isArray(feed.entries) || feed.entries.length < 1 || feed.entries.length > MAX_ENTRIES
125
- || feed.authority !== "context-only" || !DIGEST_RE.test(feed.digest || "")) {
126
- throw new Error("HTTPS feed is invalid");
127
- }
128
- if (Buffer.byteLength(JSON.stringify(envelope)) > MAX_FEED_BYTES) throw new Error("HTTPS feed exceeds 256 KiB");
129
- const { digest: _digest, ...body } = feed;
130
- if (digest(body) !== feed.digest) throw new Error("HTTPS feed failed its integrity check");
131
- let previous = null;
132
- for (const entry of feed.entries) {
133
- validateEntry(entry);
134
- if (previous) {
135
- if (entry.sequence !== previous.sequence + 1 || entry.previousDigest !== previous.digest) {
136
- throw new Error("HTTPS feed hash chain is broken");
137
- }
138
- } else if (entry.sequence === 1 && entry.previousDigest !== null) {
139
- throw new Error("HTTPS feed genesis entry cannot have a predecessor");
140
- } else if (entry.sequence > 1 && entry.previousDigest === null) {
141
- throw new Error("truncated HTTPS feed window must retain its predecessor digest");
142
- }
143
- previous = entry;
144
- }
145
- if (previous.sequence !== feed.sequence) throw new Error("HTTPS feed sequence does not match its latest entry");
146
- return { envelope, feed, authentication: verified.authentication, publicIdentity: verified.publicIdentity };
147
- }
148
-
149
- function nextFeedBody({ current, feedId, snapshot, publishedAt }) {
150
- if (current && (current.feedId !== feedId || current.scopeId !== snapshot.scopeId
151
- || current.adapterId !== snapshot.adapterId)) {
152
- throw new Error("HTTPS feed identity does not match the snapshot");
153
- }
154
- const latest = current?.entries.at(-1) || null;
155
- const entryBody = {
156
- sequence: (latest?.sequence || 0) + 1,
157
- snapshotId: snapshot.snapshotId,
158
- snapshotDigest: snapshot.digest,
159
- previousDigest: latest?.digest || null,
160
- publishedAt,
161
- authority: "context-only"
162
- };
163
- const entry = { ...entryBody, digest: digest(entryBody) };
164
- const entries = [...(current?.entries || []), entry].slice(-MAX_ENTRIES);
165
- const body = {
166
- schema: SCHEMA, feedId, scopeId: snapshot.scopeId, adapterId: snapshot.adapterId,
167
- sequence: entry.sequence, entries, authority: "context-only"
168
- };
169
- return { ...body, digest: digest(body) };
170
- }
171
-
172
- async function resolvedRequest({
173
- url, method, headers, body = null, timeoutMs, allowPrivateNetwork, lookup, request, accepted
174
- }) {
175
- const resolved = await within(
176
- resolveHttpsEndpoint(url, { allowPrivateNetwork, lookup }), timeoutMs,
177
- "HTTPS feed DNS resolution timed out"
178
- );
179
- const selected = resolved.addresses[0];
180
- return new Promise((resolvePromise, rejectPromise) => {
181
- let deadline;
182
- let settled = false;
183
- const finish = (operation, value) => {
184
- if (settled) return;
185
- settled = true;
186
- clearTimeout(deadline);
187
- operation(value);
188
- };
189
- const options = {
190
- protocol: "https:", hostname: resolved.lookupHostname,
191
- port: resolved.endpoint.port || 443, path: resolved.endpoint.pathname,
192
- method, headers, agent: false, autoSelectFamily: false,
193
- servername: isIP(resolved.lookupHostname) ? undefined : resolved.lookupHostname,
194
- lookup: (_hostname, lookupOptions, callback) => {
195
- const family = typeof lookupOptions === "object" && lookupOptions.family
196
- ? Number(lookupOptions.family) : 0;
197
- const usable = family ? resolved.addresses.find((item) => item.family === family) : selected;
198
- if (!usable) return callback(new Error("no vetted DNS address matches the requested family"));
199
- return callback(null, usable.address, usable.family);
200
- }
201
- };
202
- const active = request(options, (response) => {
203
- const status = Number(response.statusCode || 0);
204
- if (status >= 300 && status < 400 && status !== 304) {
205
- response.resume();
206
- finish(rejectPromise, new Error("HTTPS feed redirects are not followed"));
207
- return;
208
- }
209
- const chunks = [];
210
- let bytes = 0;
211
- response.on("data", (chunk) => {
212
- bytes += chunk.length;
213
- if (bytes > MAX_FEED_BYTES) active.destroy(new Error("HTTPS feed exceeds 256 KiB"));
214
- else chunks.push(chunk);
215
- });
216
- response.on("end", () => {
217
- if (!accepted.includes(status)) {
218
- finish(rejectPromise, new Error(`HTTPS feed request failed with status ${status}`));
219
- return;
220
- }
221
- finish(resolvePromise, { status, headers: response.headers, body: Buffer.concat(chunks) });
222
- });
223
- response.on("error", (error) => finish(rejectPromise, error));
224
- });
225
- active.setTimeout(timeoutMs, () => active.destroy(new Error("HTTPS feed request timed out")));
226
- deadline = setTimeout(() => active.destroy(new Error("HTTPS feed request timed out")), timeoutMs);
227
- active.on("error", (error) => finish(rejectPromise, error));
228
- active.end(body);
229
- });
230
- }
231
-
232
- export async function fetchHttpsFeed({
233
- baseUrl, feedId, tokenEnv = null, timeoutMs = 10000, allowPrivateNetwork = false,
234
- confirmation = null, environment = process.env, lookup = dnsLookup, request = httpsRequest,
235
- allowMissing = false
236
- }) {
237
- timeout(timeoutMs);
238
- if (allowPrivateNetwork && confirmation !== CONFIRMATION) {
239
- throw new Error("private-network HTTPS feed access requires explicit local owner confirmation");
240
- }
241
- const token = bearerToken(tokenEnv, environment);
242
- const url = httpsFeedUrl(baseUrl, feedId);
243
- const headers = {
244
- accept: "application/vnd.agentspine.feed+json", "accept-encoding": "identity",
245
- "user-agent": "AgentSpine/0.1 HTTPS feed transport"
246
- };
247
- if (token) headers.authorization = `Bearer ${token}`;
248
- const response = await resolvedRequest({
249
- url, method: "GET", headers, timeoutMs, allowPrivateNetwork, lookup, request,
250
- accepted: allowMissing ? [200, 404] : [200]
251
- });
252
- if (response.status === 404) return { missing: true, url, authenticatedRequest: Boolean(token) };
253
- const contentType = String(response.headers["content-type"] || "").toLowerCase();
254
- if (!/^application\/(?:json|[a-z0-9.+-]+\+json)(?:\s*;|$)/.test(contentType)) {
255
- throw new Error("HTTPS feed response must use an application/json content type");
256
- }
257
- if (String(response.headers["content-encoding"] || "identity").toLowerCase() !== "identity") {
258
- throw new Error("compressed HTTPS feeds are rejected");
259
- }
260
- const etag = String(response.headers.etag || "");
261
- if (!ETAG_RE.test(etag)) throw new Error("HTTPS feed response requires one strong ETag");
262
- let envelope;
263
- try { envelope = JSON.parse(response.body.toString("utf8")); } catch { throw new Error("HTTPS feed response is not valid JSON"); }
264
- const validated = validateHttpsFeed(envelope);
265
- if (validated.feed.feedId !== feedId) throw new Error("HTTPS feed resource has the wrong feedId");
266
- return { ...validated, missing: false, etag, url, authenticatedRequest: Boolean(token) };
267
- }
268
-
269
- async function putFeed({ baseUrl, feedId, envelope, etag, options }) {
270
- const token = bearerToken(options.tokenEnv, options.environment);
271
- const body = Buffer.from(JSON.stringify(envelope));
272
- if (body.length > MAX_FEED_BYTES) throw new Error("HTTPS feed exceeds 256 KiB");
273
- const headers = {
274
- accept: "application/json", "accept-encoding": "identity",
275
- "content-length": String(body.length), "content-type": "application/vnd.agentspine.feed+json",
276
- "user-agent": "AgentSpine/0.1 HTTPS feed transport"
277
- };
278
- if (etag) headers["if-match"] = etag;
279
- else headers["if-none-match"] = "*";
280
- if (token) headers.authorization = `Bearer ${token}`;
281
- try {
282
- return await resolvedRequest({
283
- url: httpsFeedUrl(baseUrl, feedId), method: "PUT", headers, body,
284
- timeoutMs: options.timeoutMs, allowPrivateNetwork: options.allowPrivateNetwork,
285
- lookup: options.lookup, request: options.request, accepted: etag ? [200, 204] : [201, 204]
286
- });
287
- } catch (error) {
288
- if (/status 412/.test(error.message)) throw new Error("HTTPS feed changed concurrently; fetch and retry");
289
- throw error;
290
- }
291
- }
292
-
293
- export async function publishHttpsFeed({
294
- root = process.cwd(), directory, baseUrl, feedId, signerId,
295
- snapshotId = `snapshot:${randomUUID()}`, now = new Date(), tokenEnv = null,
296
- timeoutMs = 10000, allowPrivateNetwork = false, confirmation = null,
297
- environment = process.env, lookup = dnsLookup, request = httpsRequest
298
- }) {
299
- if (confirmation !== CONFIRMATION) throw new Error("HTTPS feed publishing requires explicit local owner confirmation");
300
- if (!ID_RE.test(signerId || "")) throw new Error("signerId is required for HTTPS feed publishing");
301
- timeout(timeoutMs);
302
- const publishedAt = isoDate(now, "now");
303
- const snapshot = await buildHttpsSnapshot({ root, directory, snapshotId, now });
304
- const transportOptions = {
305
- tokenEnv, timeoutMs, allowPrivateNetwork, confirmation, environment, lookup, request
306
- };
307
- const object = await putHttpsSnapshot({ baseUrl, snapshot, ...transportOptions });
308
- const current = await fetchHttpsFeed({ baseUrl, feedId, allowMissing: true, ...transportOptions });
309
- if (!current.missing && current.authentication.signerId !== signerId) {
310
- throw new Error("HTTPS feed signer cannot change during publication");
311
- }
312
- const feed = nextFeedBody({ current: current.feed, feedId, snapshot, publishedAt });
313
- const envelope = await signEnvelope({ root, signerId, kind: "manifest", payload: feed, now });
314
- if (!current.missing && current.publicIdentity.keyId !== envelope.signer.keyId) {
315
- throw new Error("HTTPS feed signing key cannot change; use a new feedId after key rotation");
316
- }
317
- await putFeed({ baseUrl, feedId, envelope, etag: current.etag || null, options: transportOptions });
318
- const verified = await fetchHttpsFeed({ baseUrl, feedId, ...transportOptions });
319
- if (verified.feed.digest !== feed.digest || verified.authentication.signerId !== signerId) {
320
- throw new Error("HTTPS feed read-back does not match the published chain tip");
321
- }
322
- return {
323
- createdFeed: current.missing, sequence: feed.sequence, feedId, feedDigest: feed.digest,
324
- entryDigest: feed.entries.at(-1).digest, snapshotId: snapshot.snapshotId,
325
- snapshotDigest: snapshot.digest, objectUrl: object.objectUrl, feedUrl: verified.url,
326
- objectCreated: object.created, verified: true, authenticatedWrite: Boolean(bearerToken(tokenEnv, environment)),
327
- authority: "context-only"
328
- };
329
- }
330
-
331
- function emptyState(root) {
332
- return { schema: STATE_SCHEMA, root, feeds: [], history: [] };
333
- }
334
-
335
- function validateReceipt(record) {
336
- return exactKeys(record, [
337
- "feedId", "scopeId", "adapterId", "sequence", "entryDigest", "snapshotDigest",
338
- "signerId", "keyId", "observedAt", "authority"
339
- ]) && ID_RE.test(record.feedId || "") && ID_RE.test(record.scopeId || "")
340
- && ID_RE.test(record.adapterId || "") && Number.isSafeInteger(record.sequence) && record.sequence >= 1
341
- && DIGEST_RE.test(record.entryDigest || "") && DIGEST_RE.test(record.snapshotDigest || "")
342
- && ID_RE.test(record.signerId || "") && ID_RE.test(record.keyId || "")
343
- && validDate(record.observedAt) && record.authority === "context-only";
344
- }
345
-
346
- function validateState(state, root) {
347
- if (!exactKeys(state, ["schema", "root", "feeds", "history"]) || state.schema !== STATE_SCHEMA
348
- || state.root !== root || !Array.isArray(state.feeds) || !Array.isArray(state.history)
349
- || state.feeds.some((item) => !validateReceipt(item)) || state.history.some((item) => !validateReceipt(item))) {
350
- throw new Error("HTTPS feed state is invalid");
351
- }
352
- if (new Set(state.feeds.map((item) => item.feedId)).size !== state.feeds.length) {
353
- throw new Error("HTTPS feed state contains duplicate feeds");
354
- }
355
- return state;
356
- }
357
-
358
- async function feedStatePaths(root) {
359
- const catalog = await buildCatalog(root);
360
- const directory = await projectStateDir(catalog.root);
361
- if (!statePathIsScanExcluded(catalog, directory)) {
362
- throw new Error("HTTPS feed state must remain outside the scanned project or in an excluded home-state root");
363
- }
364
- return { root: catalog.root, path: join(directory, "feed-state.json") };
365
- }
366
-
367
- async function readState(path, root) {
368
- try {
369
- const metadata = await stat(path);
370
- if (metadata.size > MAX_STATE_BYTES) throw new Error("HTTPS feed state exceeds 4 MiB");
371
- return validateState(JSON.parse(await readFile(path, "utf8")), root);
372
- } catch (error) {
373
- if (error.code !== "ENOENT") throw error;
374
- return emptyState(root);
375
- }
376
- }
377
-
378
- async function acquireLock(path) {
379
- const lockPath = `${path}.lock`;
380
- for (let attempt = 0; attempt < 120; attempt += 1) {
381
- try { return { handle: await open(lockPath, "wx", 0o600), lockPath }; } catch (error) {
382
- if (!isFileLockContention(error)) throw error;
383
- try {
384
- const metadata = await stat(lockPath);
385
- if (Date.now() - metadata.mtimeMs > 90000) await unlink(lockPath);
386
- } catch (lockError) { if (lockError.code !== "ENOENT") throw lockError; }
387
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
388
- }
389
- }
390
- throw new Error("HTTPS feed state is busy; retry shortly");
391
- }
392
-
393
- async function saveState(path, state) {
394
- validateState(state, state.root);
395
- const content = `${JSON.stringify(state, null, 2)}\n`;
396
- if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("HTTPS feed state exceeds 4 MiB");
397
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
398
- await writeFile(temporary, content, { mode: 0o600 });
399
- await replaceFileWithRetry(temporary, path);
400
- }
401
-
402
- function assessFeed(feed, receipt) {
403
- const latest = feed.entries.at(-1);
404
- if (!receipt) return { latest, unchanged: false };
405
- if (receipt.scopeId !== feed.scopeId || receipt.adapterId !== feed.adapterId) {
406
- throw new Error("HTTPS feed scope or adapter changed");
407
- }
408
- if (latest.sequence < receipt.sequence) throw new Error("HTTPS feed rollback detected");
409
- if (latest.sequence === receipt.sequence) {
410
- if (latest.digest !== receipt.entryDigest || latest.snapshotDigest !== receipt.snapshotDigest) {
411
- throw new Error("HTTPS feed equivocation detected at an observed sequence");
412
- }
413
- return { latest, unchanged: true };
414
- }
415
- const anchor = feed.entries.findIndex((entry) => (
416
- entry.sequence === receipt.sequence && entry.digest === receipt.entryDigest
417
- ));
418
- if (anchor < 0) throw new Error("HTTPS feed continuity window no longer contains the last observed entry");
419
- return { latest, unchanged: false };
420
- }
421
-
422
- export async function loadHttpsFeedState(root = process.cwd()) {
423
- const paths = await feedStatePaths(root);
424
- return { state: await readState(paths.path, paths.root), statePath: paths.path };
425
- }
426
-
427
- export async function inspectHttpsFeedState(root = process.cwd()) {
428
- const paths = await feedStatePaths(root);
429
- try {
430
- return { state: await readState(paths.path, paths.root), statePath: paths.path, error: null };
431
- } catch (error) {
432
- return { state: emptyState(paths.root), statePath: paths.path, error: error.message };
433
- }
434
- }
435
-
436
- export async function pullHttpsFeed({
437
- root = process.cwd(), baseUrl, feedId, now = new Date(), tokenEnv = null,
438
- timeoutMs = 10000, allowPrivateNetwork = false, confirmation = null,
439
- environment = process.env, lookup = dnsLookup, request = httpsRequest
440
- }) {
441
- const observedAt = isoDate(now, "now");
442
- const options = {
443
- baseUrl, feedId, tokenEnv, timeoutMs, allowPrivateNetwork,
444
- confirmation, environment, lookup, request
445
- };
446
- const remote = await fetchHttpsFeed(options);
447
- const { trust } = await loadTrust(root);
448
- assertTrustedIdentity(remote.publicIdentity, trust);
449
- const paths = await feedStatePaths(root);
450
- const { handle, lockPath } = await acquireLock(paths.path);
451
- try {
452
- const state = await readState(paths.path, paths.root);
453
- const existing = state.feeds.find((item) => item.feedId === feedId) || null;
454
- if (existing && (existing.signerId !== remote.publicIdentity.signerId
455
- || existing.keyId !== remote.publicIdentity.keyId)) {
456
- throw new Error("HTTPS feed signing identity changed; use a new feedId after key rotation");
457
- }
458
- const assessment = assessFeed(remote.feed, existing);
459
- if (assessment.unchanged) {
460
- return {
461
- changed: false, feedId, sequence: existing.sequence,
462
- snapshotDigest: existing.snapshotDigest, statePath: paths.path,
463
- authenticatedRequest: remote.authenticatedRequest, authority: "context-only"
464
- };
465
- }
466
- const objectUrl = httpsObjectUrl(baseUrl, assessment.latest.snapshotDigest);
467
- const fetched = await fetchHttpsSnapshot({
468
- url: objectUrl, tokenEnv, timeoutMs, allowPrivateNetwork,
469
- confirmation, environment, lookup, request
470
- });
471
- if (fetched.snapshot.digest !== assessment.latest.snapshotDigest
472
- || fetched.snapshot.snapshotId !== assessment.latest.snapshotId
473
- || fetched.snapshot.scopeId !== remote.feed.scopeId
474
- || fetched.snapshot.adapterId !== remote.feed.adapterId) {
475
- throw new Error("HTTPS feed snapshot does not match its signed entry");
476
- }
477
- const imported = await importHttpsSnapshot({ root: paths.root, snapshot: fetched.snapshot, now });
478
- const receipt = {
479
- feedId, scopeId: remote.feed.scopeId, adapterId: remote.feed.adapterId,
480
- sequence: assessment.latest.sequence, entryDigest: assessment.latest.digest,
481
- snapshotDigest: assessment.latest.snapshotDigest,
482
- signerId: remote.publicIdentity.signerId, keyId: remote.publicIdentity.keyId,
483
- observedAt, authority: "context-only"
484
- };
485
- if (existing) state.history.push(existing);
486
- state.feeds = [...state.feeds.filter((item) => item.feedId !== feedId), receipt]
487
- .sort((a, b) => a.feedId.localeCompare(b.feedId));
488
- await saveState(paths.path, state);
489
- return {
490
- ...imported, changed: true, feedId, sequence: receipt.sequence,
491
- entryDigest: receipt.entryDigest, snapshotDigest: receipt.snapshotDigest,
492
- feedUrl: remote.url, objectUrl, statePath: paths.path,
493
- authenticatedRequest: remote.authenticatedRequest, authority: "context-only"
494
- };
495
- } finally {
496
- await handle.close();
497
- await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
498
- }
499
- }
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { lookup as dnsLookup } from "node:dns/promises";
3
+ import { open, readFile, stat, unlink, writeFile } from "node:fs/promises";
4
+ import { request as httpsRequest } from "node:https";
5
+ import { isIP } from "node:net";
6
+ import { join } from "node:path";
7
+ import {
8
+ assertTrustedIdentity, loadTrust, signEnvelope, verifyEnvelope
9
+ } from "./authentication.js";
10
+ import { buildCatalog } from "./catalog.js";
11
+ import { isFileLockContention, replaceFileWithRetry } from "./filesystem-retry.js";
12
+ import {
13
+ buildHttpsSnapshot, fetchHttpsSnapshot, importHttpsSnapshot, resolveHttpsEndpoint
14
+ } from "./https-transport.js";
15
+ import {
16
+ httpsObjectUrl, putHttpsSnapshot, validateHttpsObjectBase
17
+ } from "./object-transport.js";
18
+ import { projectStateDir, statePathIsScanExcluded } from "./paths.js";
19
+
20
+ const SCHEMA = "agentspine.feed/v1";
21
+ const STATE_SCHEMA = "agentspine.feed-state/v1";
22
+ const CONFIRMATION = "local-share-confirmed";
23
+ const MAX_ENTRIES = 256;
24
+ const MAX_FEED_BYTES = 256 * 1024;
25
+ const MAX_STATE_BYTES = 4 * 1024 * 1024;
26
+ const MAX_TOKEN_BYTES = 8192;
27
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
28
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
29
+ const ETAG_RE = /^"[^"\r\n]{1,128}"$/;
30
+ const TOKEN_ENV_RE = /^[A-Z_][A-Z0-9_]{0,127}$/;
31
+
32
+ function canonical(value) {
33
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
34
+ if (value && typeof value === "object") {
35
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
36
+ }
37
+ return JSON.stringify(value);
38
+ }
39
+
40
+ function digest(value) {
41
+ return createHash("sha256").update(canonical(value)).digest("hex");
42
+ }
43
+
44
+ function exactKeys(value, expected) {
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
46
+ const actual = Object.keys(value).sort();
47
+ const wanted = [...expected].sort();
48
+ return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
49
+ }
50
+
51
+ function validDate(value) {
52
+ return typeof value === "string" && Number.isFinite(new Date(value).getTime());
53
+ }
54
+
55
+ function isoDate(value, field = "date") {
56
+ const parsed = value instanceof Date ? value : new Date(value);
57
+ if (!Number.isFinite(parsed.getTime())) throw new Error(`${field} must be a valid date`);
58
+ return parsed.toISOString();
59
+ }
60
+
61
+ function bearerToken(tokenEnv, environment) {
62
+ if (tokenEnv === null || tokenEnv === undefined) return null;
63
+ if (!TOKEN_ENV_RE.test(tokenEnv)) throw new Error("tokenEnv must be an uppercase environment variable name");
64
+ const value = environment[tokenEnv];
65
+ if (typeof value !== "string" || !value) throw new Error(`HTTPS feed token environment variable is missing: ${tokenEnv}`);
66
+ if (Buffer.byteLength(value) > MAX_TOKEN_BYTES || /[\0-\x1f\x7f]/.test(value)) {
67
+ throw new Error("HTTPS feed bearer token is invalid");
68
+ }
69
+ return value;
70
+ }
71
+
72
+ function timeout(value) {
73
+ if (!Number.isInteger(value) || value < 1000 || value > 30000) {
74
+ throw new Error("HTTPS feed timeout must be between 1000 and 30000 milliseconds");
75
+ }
76
+ return value;
77
+ }
78
+
79
+ async function within(promise, milliseconds, message) {
80
+ let timer;
81
+ try {
82
+ return await Promise.race([
83
+ promise,
84
+ new Promise((_resolve, reject) => {
85
+ timer = setTimeout(() => reject(new Error(message)), milliseconds);
86
+ })
87
+ ]);
88
+ } finally {
89
+ clearTimeout(timer);
90
+ }
91
+ }
92
+
93
+ export function httpsFeedUrl(baseUrl, feedId) {
94
+ if (!ID_RE.test(feedId || "")) throw new Error("feedId must be a stable, whitespace-free identifier");
95
+ const endpoint = validateHttpsObjectBase(baseUrl);
96
+ const name = createHash("sha256").update(feedId).digest("hex");
97
+ endpoint.pathname = `${endpoint.pathname}/feeds/${name}.json`;
98
+ return endpoint.toString();
99
+ }
100
+
101
+ function validateEntry(entry) {
102
+ if (!exactKeys(entry, [
103
+ "sequence", "snapshotId", "snapshotDigest", "previousDigest", "publishedAt", "authority", "digest"
104
+ ]) || !Number.isSafeInteger(entry.sequence) || entry.sequence < 1
105
+ || !ID_RE.test(entry.snapshotId || "") || !DIGEST_RE.test(entry.snapshotDigest || "")
106
+ || !(entry.previousDigest === null || DIGEST_RE.test(entry.previousDigest))
107
+ || !validDate(entry.publishedAt) || entry.authority !== "context-only"
108
+ || !DIGEST_RE.test(entry.digest || "")) {
109
+ throw new Error("HTTPS feed entry is invalid");
110
+ }
111
+ const { digest: _digest, ...body } = entry;
112
+ if (digest(body) !== entry.digest) throw new Error("HTTPS feed entry failed its integrity check");
113
+ return entry;
114
+ }
115
+
116
+ export function validateHttpsFeed(envelope) {
117
+ const verified = verifyEnvelope(envelope, "manifest");
118
+ const feed = verified.payload;
119
+ if (!exactKeys(feed, [
120
+ "schema", "feedId", "scopeId", "adapterId", "sequence", "entries", "authority", "digest"
121
+ ]) || feed.schema !== SCHEMA || !ID_RE.test(feed.feedId || "")
122
+ || !ID_RE.test(feed.scopeId || "") || !ID_RE.test(feed.adapterId || "")
123
+ || !Number.isSafeInteger(feed.sequence) || feed.sequence < 1
124
+ || !Array.isArray(feed.entries) || feed.entries.length < 1 || feed.entries.length > MAX_ENTRIES
125
+ || feed.authority !== "context-only" || !DIGEST_RE.test(feed.digest || "")) {
126
+ throw new Error("HTTPS feed is invalid");
127
+ }
128
+ if (Buffer.byteLength(JSON.stringify(envelope)) > MAX_FEED_BYTES) throw new Error("HTTPS feed exceeds 256 KiB");
129
+ const { digest: _digest, ...body } = feed;
130
+ if (digest(body) !== feed.digest) throw new Error("HTTPS feed failed its integrity check");
131
+ let previous = null;
132
+ for (const entry of feed.entries) {
133
+ validateEntry(entry);
134
+ if (previous) {
135
+ if (entry.sequence !== previous.sequence + 1 || entry.previousDigest !== previous.digest) {
136
+ throw new Error("HTTPS feed hash chain is broken");
137
+ }
138
+ } else if (entry.sequence === 1 && entry.previousDigest !== null) {
139
+ throw new Error("HTTPS feed genesis entry cannot have a predecessor");
140
+ } else if (entry.sequence > 1 && entry.previousDigest === null) {
141
+ throw new Error("truncated HTTPS feed window must retain its predecessor digest");
142
+ }
143
+ previous = entry;
144
+ }
145
+ if (previous.sequence !== feed.sequence) throw new Error("HTTPS feed sequence does not match its latest entry");
146
+ return { envelope, feed, authentication: verified.authentication, publicIdentity: verified.publicIdentity };
147
+ }
148
+
149
+ function nextFeedBody({ current, feedId, snapshot, publishedAt }) {
150
+ if (current && (current.feedId !== feedId || current.scopeId !== snapshot.scopeId
151
+ || current.adapterId !== snapshot.adapterId)) {
152
+ throw new Error("HTTPS feed identity does not match the snapshot");
153
+ }
154
+ const latest = current?.entries.at(-1) || null;
155
+ const entryBody = {
156
+ sequence: (latest?.sequence || 0) + 1,
157
+ snapshotId: snapshot.snapshotId,
158
+ snapshotDigest: snapshot.digest,
159
+ previousDigest: latest?.digest || null,
160
+ publishedAt,
161
+ authority: "context-only"
162
+ };
163
+ const entry = { ...entryBody, digest: digest(entryBody) };
164
+ const entries = [...(current?.entries || []), entry].slice(-MAX_ENTRIES);
165
+ const body = {
166
+ schema: SCHEMA, feedId, scopeId: snapshot.scopeId, adapterId: snapshot.adapterId,
167
+ sequence: entry.sequence, entries, authority: "context-only"
168
+ };
169
+ return { ...body, digest: digest(body) };
170
+ }
171
+
172
+ async function resolvedRequest({
173
+ url, method, headers, body = null, timeoutMs, allowPrivateNetwork, lookup, request, accepted
174
+ }) {
175
+ const resolved = await within(
176
+ resolveHttpsEndpoint(url, { allowPrivateNetwork, lookup }), timeoutMs,
177
+ "HTTPS feed DNS resolution timed out"
178
+ );
179
+ const selected = resolved.addresses[0];
180
+ return new Promise((resolvePromise, rejectPromise) => {
181
+ let deadline;
182
+ let settled = false;
183
+ const finish = (operation, value) => {
184
+ if (settled) return;
185
+ settled = true;
186
+ clearTimeout(deadline);
187
+ operation(value);
188
+ };
189
+ const options = {
190
+ protocol: "https:", hostname: resolved.lookupHostname,
191
+ port: resolved.endpoint.port || 443, path: resolved.endpoint.pathname,
192
+ method, headers, agent: false, autoSelectFamily: false,
193
+ servername: isIP(resolved.lookupHostname) ? undefined : resolved.lookupHostname,
194
+ lookup: (_hostname, lookupOptions, callback) => {
195
+ const family = typeof lookupOptions === "object" && lookupOptions.family
196
+ ? Number(lookupOptions.family) : 0;
197
+ const usable = family ? resolved.addresses.find((item) => item.family === family) : selected;
198
+ if (!usable) return callback(new Error("no vetted DNS address matches the requested family"));
199
+ return callback(null, usable.address, usable.family);
200
+ }
201
+ };
202
+ const active = request(options, (response) => {
203
+ const status = Number(response.statusCode || 0);
204
+ if (status >= 300 && status < 400 && status !== 304) {
205
+ response.resume();
206
+ finish(rejectPromise, new Error("HTTPS feed redirects are not followed"));
207
+ return;
208
+ }
209
+ const chunks = [];
210
+ let bytes = 0;
211
+ response.on("data", (chunk) => {
212
+ bytes += chunk.length;
213
+ if (bytes > MAX_FEED_BYTES) active.destroy(new Error("HTTPS feed exceeds 256 KiB"));
214
+ else chunks.push(chunk);
215
+ });
216
+ response.on("end", () => {
217
+ if (!accepted.includes(status)) {
218
+ finish(rejectPromise, new Error(`HTTPS feed request failed with status ${status}`));
219
+ return;
220
+ }
221
+ finish(resolvePromise, { status, headers: response.headers, body: Buffer.concat(chunks) });
222
+ });
223
+ response.on("error", (error) => finish(rejectPromise, error));
224
+ });
225
+ active.setTimeout(timeoutMs, () => active.destroy(new Error("HTTPS feed request timed out")));
226
+ deadline = setTimeout(() => active.destroy(new Error("HTTPS feed request timed out")), timeoutMs);
227
+ active.on("error", (error) => finish(rejectPromise, error));
228
+ active.end(body);
229
+ });
230
+ }
231
+
232
+ export async function fetchHttpsFeed({
233
+ baseUrl, feedId, tokenEnv = null, timeoutMs = 10000, allowPrivateNetwork = false,
234
+ confirmation = null, environment = process.env, lookup = dnsLookup, request = httpsRequest,
235
+ allowMissing = false
236
+ }) {
237
+ timeout(timeoutMs);
238
+ if (allowPrivateNetwork && confirmation !== CONFIRMATION) {
239
+ throw new Error("private-network HTTPS feed access requires explicit local owner confirmation");
240
+ }
241
+ const token = bearerToken(tokenEnv, environment);
242
+ const url = httpsFeedUrl(baseUrl, feedId);
243
+ const headers = {
244
+ accept: "application/vnd.agentspine.feed+json", "accept-encoding": "identity",
245
+ "user-agent": "AgentSpine/0.1 HTTPS feed transport"
246
+ };
247
+ if (token) headers.authorization = `Bearer ${token}`;
248
+ const response = await resolvedRequest({
249
+ url, method: "GET", headers, timeoutMs, allowPrivateNetwork, lookup, request,
250
+ accepted: allowMissing ? [200, 404] : [200]
251
+ });
252
+ if (response.status === 404) return { missing: true, url, authenticatedRequest: Boolean(token) };
253
+ const contentType = String(response.headers["content-type"] || "").toLowerCase();
254
+ if (!/^application\/(?:json|[a-z0-9.+-]+\+json)(?:\s*;|$)/.test(contentType)) {
255
+ throw new Error("HTTPS feed response must use an application/json content type");
256
+ }
257
+ if (String(response.headers["content-encoding"] || "identity").toLowerCase() !== "identity") {
258
+ throw new Error("compressed HTTPS feeds are rejected");
259
+ }
260
+ const etag = String(response.headers.etag || "");
261
+ if (!ETAG_RE.test(etag)) throw new Error("HTTPS feed response requires one strong ETag");
262
+ let envelope;
263
+ try { envelope = JSON.parse(response.body.toString("utf8")); } catch { throw new Error("HTTPS feed response is not valid JSON"); }
264
+ const validated = validateHttpsFeed(envelope);
265
+ if (validated.feed.feedId !== feedId) throw new Error("HTTPS feed resource has the wrong feedId");
266
+ return { ...validated, missing: false, etag, url, authenticatedRequest: Boolean(token) };
267
+ }
268
+
269
+ async function putFeed({ baseUrl, feedId, envelope, etag, options }) {
270
+ const token = bearerToken(options.tokenEnv, options.environment);
271
+ const body = Buffer.from(JSON.stringify(envelope));
272
+ if (body.length > MAX_FEED_BYTES) throw new Error("HTTPS feed exceeds 256 KiB");
273
+ const headers = {
274
+ accept: "application/json", "accept-encoding": "identity",
275
+ "content-length": String(body.length), "content-type": "application/vnd.agentspine.feed+json",
276
+ "user-agent": "AgentSpine/0.1 HTTPS feed transport"
277
+ };
278
+ if (etag) headers["if-match"] = etag;
279
+ else headers["if-none-match"] = "*";
280
+ if (token) headers.authorization = `Bearer ${token}`;
281
+ try {
282
+ return await resolvedRequest({
283
+ url: httpsFeedUrl(baseUrl, feedId), method: "PUT", headers, body,
284
+ timeoutMs: options.timeoutMs, allowPrivateNetwork: options.allowPrivateNetwork,
285
+ lookup: options.lookup, request: options.request, accepted: etag ? [200, 204] : [201, 204]
286
+ });
287
+ } catch (error) {
288
+ if (/status 412/.test(error.message)) throw new Error("HTTPS feed changed concurrently; fetch and retry");
289
+ throw error;
290
+ }
291
+ }
292
+
293
+ export async function publishHttpsFeed({
294
+ root = process.cwd(), directory, baseUrl, feedId, signerId,
295
+ snapshotId = `snapshot:${randomUUID()}`, now = new Date(), tokenEnv = null,
296
+ timeoutMs = 10000, allowPrivateNetwork = false, confirmation = null,
297
+ environment = process.env, lookup = dnsLookup, request = httpsRequest
298
+ }) {
299
+ if (confirmation !== CONFIRMATION) throw new Error("HTTPS feed publishing requires explicit local owner confirmation");
300
+ if (!ID_RE.test(signerId || "")) throw new Error("signerId is required for HTTPS feed publishing");
301
+ timeout(timeoutMs);
302
+ const publishedAt = isoDate(now, "now");
303
+ const snapshot = await buildHttpsSnapshot({ root, directory, snapshotId, now });
304
+ const transportOptions = {
305
+ tokenEnv, timeoutMs, allowPrivateNetwork, confirmation, environment, lookup, request
306
+ };
307
+ const object = await putHttpsSnapshot({ baseUrl, snapshot, ...transportOptions });
308
+ const current = await fetchHttpsFeed({ baseUrl, feedId, allowMissing: true, ...transportOptions });
309
+ if (!current.missing && current.authentication.signerId !== signerId) {
310
+ throw new Error("HTTPS feed signer cannot change during publication");
311
+ }
312
+ const feed = nextFeedBody({ current: current.feed, feedId, snapshot, publishedAt });
313
+ const envelope = await signEnvelope({ root, signerId, kind: "manifest", payload: feed, now });
314
+ if (!current.missing && current.publicIdentity.keyId !== envelope.signer.keyId) {
315
+ throw new Error("HTTPS feed signing key cannot change; use a new feedId after key rotation");
316
+ }
317
+ await putFeed({ baseUrl, feedId, envelope, etag: current.etag || null, options: transportOptions });
318
+ const verified = await fetchHttpsFeed({ baseUrl, feedId, ...transportOptions });
319
+ if (verified.feed.digest !== feed.digest || verified.authentication.signerId !== signerId) {
320
+ throw new Error("HTTPS feed read-back does not match the published chain tip");
321
+ }
322
+ return {
323
+ createdFeed: current.missing, sequence: feed.sequence, feedId, feedDigest: feed.digest,
324
+ entryDigest: feed.entries.at(-1).digest, snapshotId: snapshot.snapshotId,
325
+ snapshotDigest: snapshot.digest, objectUrl: object.objectUrl, feedUrl: verified.url,
326
+ objectCreated: object.created, verified: true, authenticatedWrite: Boolean(bearerToken(tokenEnv, environment)),
327
+ authority: "context-only"
328
+ };
329
+ }
330
+
331
+ function emptyState(root) {
332
+ return { schema: STATE_SCHEMA, root, feeds: [], history: [] };
333
+ }
334
+
335
+ function validateReceipt(record) {
336
+ return exactKeys(record, [
337
+ "feedId", "scopeId", "adapterId", "sequence", "entryDigest", "snapshotDigest",
338
+ "signerId", "keyId", "observedAt", "authority"
339
+ ]) && ID_RE.test(record.feedId || "") && ID_RE.test(record.scopeId || "")
340
+ && ID_RE.test(record.adapterId || "") && Number.isSafeInteger(record.sequence) && record.sequence >= 1
341
+ && DIGEST_RE.test(record.entryDigest || "") && DIGEST_RE.test(record.snapshotDigest || "")
342
+ && ID_RE.test(record.signerId || "") && ID_RE.test(record.keyId || "")
343
+ && validDate(record.observedAt) && record.authority === "context-only";
344
+ }
345
+
346
+ function validateState(state, root) {
347
+ if (!exactKeys(state, ["schema", "root", "feeds", "history"]) || state.schema !== STATE_SCHEMA
348
+ || state.root !== root || !Array.isArray(state.feeds) || !Array.isArray(state.history)
349
+ || state.feeds.some((item) => !validateReceipt(item)) || state.history.some((item) => !validateReceipt(item))) {
350
+ throw new Error("HTTPS feed state is invalid");
351
+ }
352
+ if (new Set(state.feeds.map((item) => item.feedId)).size !== state.feeds.length) {
353
+ throw new Error("HTTPS feed state contains duplicate feeds");
354
+ }
355
+ return state;
356
+ }
357
+
358
+ async function feedStatePaths(root) {
359
+ const catalog = await buildCatalog(root);
360
+ const directory = await projectStateDir(catalog.root);
361
+ if (!statePathIsScanExcluded(catalog, directory)) {
362
+ throw new Error("HTTPS feed state must remain outside the scanned project or in an excluded home-state root");
363
+ }
364
+ return { root: catalog.root, path: join(directory, "feed-state.json") };
365
+ }
366
+
367
+ async function readState(path, root) {
368
+ try {
369
+ const metadata = await stat(path);
370
+ if (metadata.size > MAX_STATE_BYTES) throw new Error("HTTPS feed state exceeds 4 MiB");
371
+ return validateState(JSON.parse(await readFile(path, "utf8")), root);
372
+ } catch (error) {
373
+ if (error.code !== "ENOENT") throw error;
374
+ return emptyState(root);
375
+ }
376
+ }
377
+
378
+ async function acquireLock(path) {
379
+ const lockPath = `${path}.lock`;
380
+ for (let attempt = 0; attempt < 120; attempt += 1) {
381
+ try { return { handle: await open(lockPath, "wx", 0o600), lockPath }; } catch (error) {
382
+ if (!isFileLockContention(error)) throw error;
383
+ try {
384
+ const metadata = await stat(lockPath);
385
+ if (Date.now() - metadata.mtimeMs > 90000) await unlink(lockPath);
386
+ } catch (lockError) { if (lockError.code !== "ENOENT") throw lockError; }
387
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 50));
388
+ }
389
+ }
390
+ throw new Error("HTTPS feed state is busy; retry shortly");
391
+ }
392
+
393
+ async function saveState(path, state) {
394
+ validateState(state, state.root);
395
+ const content = `${JSON.stringify(state, null, 2)}\n`;
396
+ if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("HTTPS feed state exceeds 4 MiB");
397
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
398
+ await writeFile(temporary, content, { mode: 0o600 });
399
+ await replaceFileWithRetry(temporary, path);
400
+ }
401
+
402
+ function assessFeed(feed, receipt) {
403
+ const latest = feed.entries.at(-1);
404
+ if (!receipt) return { latest, unchanged: false };
405
+ if (receipt.scopeId !== feed.scopeId || receipt.adapterId !== feed.adapterId) {
406
+ throw new Error("HTTPS feed scope or adapter changed");
407
+ }
408
+ if (latest.sequence < receipt.sequence) throw new Error("HTTPS feed rollback detected");
409
+ if (latest.sequence === receipt.sequence) {
410
+ if (latest.digest !== receipt.entryDigest || latest.snapshotDigest !== receipt.snapshotDigest) {
411
+ throw new Error("HTTPS feed equivocation detected at an observed sequence");
412
+ }
413
+ return { latest, unchanged: true };
414
+ }
415
+ const anchor = feed.entries.findIndex((entry) => (
416
+ entry.sequence === receipt.sequence && entry.digest === receipt.entryDigest
417
+ ));
418
+ if (anchor < 0) throw new Error("HTTPS feed continuity window no longer contains the last observed entry");
419
+ return { latest, unchanged: false };
420
+ }
421
+
422
+ export async function loadHttpsFeedState(root = process.cwd()) {
423
+ const paths = await feedStatePaths(root);
424
+ return { state: await readState(paths.path, paths.root), statePath: paths.path };
425
+ }
426
+
427
+ export async function inspectHttpsFeedState(root = process.cwd()) {
428
+ const paths = await feedStatePaths(root);
429
+ try {
430
+ return { state: await readState(paths.path, paths.root), statePath: paths.path, error: null };
431
+ } catch (error) {
432
+ return { state: emptyState(paths.root), statePath: paths.path, error: error.message };
433
+ }
434
+ }
435
+
436
+ export async function pullHttpsFeed({
437
+ root = process.cwd(), baseUrl, feedId, now = new Date(), tokenEnv = null,
438
+ timeoutMs = 10000, allowPrivateNetwork = false, confirmation = null,
439
+ environment = process.env, lookup = dnsLookup, request = httpsRequest
440
+ }) {
441
+ const observedAt = isoDate(now, "now");
442
+ const options = {
443
+ baseUrl, feedId, tokenEnv, timeoutMs, allowPrivateNetwork,
444
+ confirmation, environment, lookup, request
445
+ };
446
+ const remote = await fetchHttpsFeed(options);
447
+ const { trust } = await loadTrust(root);
448
+ assertTrustedIdentity(remote.publicIdentity, trust);
449
+ const paths = await feedStatePaths(root);
450
+ const { handle, lockPath } = await acquireLock(paths.path);
451
+ try {
452
+ const state = await readState(paths.path, paths.root);
453
+ const existing = state.feeds.find((item) => item.feedId === feedId) || null;
454
+ if (existing && (existing.signerId !== remote.publicIdentity.signerId
455
+ || existing.keyId !== remote.publicIdentity.keyId)) {
456
+ throw new Error("HTTPS feed signing identity changed; use a new feedId after key rotation");
457
+ }
458
+ const assessment = assessFeed(remote.feed, existing);
459
+ if (assessment.unchanged) {
460
+ return {
461
+ changed: false, feedId, sequence: existing.sequence,
462
+ snapshotDigest: existing.snapshotDigest, statePath: paths.path,
463
+ authenticatedRequest: remote.authenticatedRequest, authority: "context-only"
464
+ };
465
+ }
466
+ const objectUrl = httpsObjectUrl(baseUrl, assessment.latest.snapshotDigest);
467
+ const fetched = await fetchHttpsSnapshot({
468
+ url: objectUrl, tokenEnv, timeoutMs, allowPrivateNetwork,
469
+ confirmation, environment, lookup, request
470
+ });
471
+ if (fetched.snapshot.digest !== assessment.latest.snapshotDigest
472
+ || fetched.snapshot.snapshotId !== assessment.latest.snapshotId
473
+ || fetched.snapshot.scopeId !== remote.feed.scopeId
474
+ || fetched.snapshot.adapterId !== remote.feed.adapterId) {
475
+ throw new Error("HTTPS feed snapshot does not match its signed entry");
476
+ }
477
+ const imported = await importHttpsSnapshot({ root: paths.root, snapshot: fetched.snapshot, now });
478
+ const receipt = {
479
+ feedId, scopeId: remote.feed.scopeId, adapterId: remote.feed.adapterId,
480
+ sequence: assessment.latest.sequence, entryDigest: assessment.latest.digest,
481
+ snapshotDigest: assessment.latest.snapshotDigest,
482
+ signerId: remote.publicIdentity.signerId, keyId: remote.publicIdentity.keyId,
483
+ observedAt, authority: "context-only"
484
+ };
485
+ if (existing) state.history.push(existing);
486
+ state.feeds = [...state.feeds.filter((item) => item.feedId !== feedId), receipt]
487
+ .sort((a, b) => a.feedId.localeCompare(b.feedId));
488
+ await saveState(paths.path, state);
489
+ return {
490
+ ...imported, changed: true, feedId, sequence: receipt.sequence,
491
+ entryDigest: receipt.entryDigest, snapshotDigest: receipt.snapshotDigest,
492
+ feedUrl: remote.url, objectUrl, statePath: paths.path,
493
+ authenticatedRequest: remote.authenticatedRequest, authority: "context-only"
494
+ };
495
+ } finally {
496
+ await handle.close();
497
+ await unlink(lockPath).catch((error) => { if (error.code !== "ENOENT") throw error; });
498
+ }
499
+ }