blun-king-cli 9.1.564 → 9.1.566

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 (88) hide show
  1. package/LIESMICH.txt +2 -2
  2. package/README.md +1 -1
  3. package/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
  4. package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
  5. package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
  6. package/agent-spine-plugin/.mcp.json +8 -8
  7. package/agent-spine-plugin/CONTRIBUTING.md +52 -0
  8. package/agent-spine-plugin/LICENSE +186 -186
  9. package/agent-spine-plugin/README.md +406 -0
  10. package/agent-spine-plugin/SECURITY.md +47 -0
  11. package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
  12. package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
  13. package/agent-spine-plugin/bin/agentspine.js +7 -7
  14. package/agent-spine-plugin/blun.plugin.json +33 -33
  15. package/agent-spine-plugin/hooks/codex.json +47 -47
  16. package/agent-spine-plugin/hooks/hooks.json +106 -106
  17. package/agent-spine-plugin/hooks/version.json +5 -5
  18. package/agent-spine-plugin/package.json +69 -69
  19. package/agent-spine-plugin/scripts/check-hosts.js +199 -199
  20. package/agent-spine-plugin/skill/SKILL.md +76 -76
  21. package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
  22. package/agent-spine-plugin/src/cli.js +1488 -1442
  23. package/agent-spine-plugin/src/hook.js +882 -861
  24. package/agent-spine-plugin/src/index.js +94 -93
  25. package/agent-spine-plugin/src/lib/acceptance.js +333 -333
  26. package/agent-spine-plugin/src/lib/attention.js +755 -755
  27. package/agent-spine-plugin/src/lib/audit.js +351 -342
  28. package/agent-spine-plugin/src/lib/authentication.js +515 -515
  29. package/agent-spine-plugin/src/lib/briefing.js +317 -317
  30. package/agent-spine-plugin/src/lib/catalog.js +167 -167
  31. package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
  32. package/agent-spine-plugin/src/lib/context.js +154 -154
  33. package/agent-spine-plugin/src/lib/continuity.js +338 -338
  34. package/agent-spine-plugin/src/lib/coordination.js +577 -577
  35. package/agent-spine-plugin/src/lib/documents.js +217 -217
  36. package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
  37. package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
  38. package/agent-spine-plugin/src/lib/gateway-runtime.js +1623 -760
  39. package/agent-spine-plugin/src/lib/graph.js +337 -337
  40. package/agent-spine-plugin/src/lib/hook-audit.js +29 -29
  41. package/agent-spine-plugin/src/lib/https-transport.js +392 -392
  42. package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
  43. package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
  44. package/agent-spine-plugin/src/lib/learning.js +6923 -6466
  45. package/agent-spine-plugin/src/lib/object-transport.js +206 -206
  46. package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
  47. package/agent-spine-plugin/src/lib/paths.js +109 -109
  48. package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
  49. package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
  50. package/agent-spine-plugin/src/lib/preflight.js +702 -702
  51. package/agent-spine-plugin/src/lib/runtime.js +13 -13
  52. package/agent-spine-plugin/src/lib/selfstarter.js +911 -866
  53. package/agent-spine-plugin/src/lib/sharing.js +969 -969
  54. package/agent-spine-plugin/src/lib/source-roots.js +530 -498
  55. package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
  56. package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
  57. package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
  58. package/agent-spine-plugin/src/mcp.js +597 -572
  59. package/agent-spine-plugin/src/version.js +1 -1
  60. package/agent-spine-plugin/src/worker.js +210 -195
  61. package/bin/active-steer-priority-policy.cjs +24 -0
  62. package/bin/launcher-runtime.js +8 -1
  63. package/bin/mnemo-tool-agent-policy.cjs +22 -0
  64. package/bin/native-runtime-cache.cjs +76 -0
  65. package/bin/thinking-activity-status-policy.cjs +132 -0
  66. package/bin/thinking-only-guard.cjs +75 -0
  67. package/bin/tool-call-loop-policy.cjs +53 -0
  68. package/bin/turn-thinking-policy.cjs +25 -1
  69. package/blun.mjs +556 -63
  70. package/package.json +4 -1
  71. package/standard-skills/translate-native/LICENSE +21 -21
  72. package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
  73. package/standard-skills/translate-native/references/native-orthography.md +79 -79
  74. package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
  75. package/standard-skills/translate-native/references/structured-content.md +72 -72
  76. package/standard-skills/translate-native/references/translationese-review.md +77 -77
  77. package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
  78. package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
  79. package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
  80. package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
  81. package/standard-skills/translate-native/scripts/language_quality.py +377 -377
  82. package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
  83. package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
  84. package/standard-tools/language-guard/blun_language_guard.py +697 -697
  85. package/standard-tools/language-guard/check_diacritics.py +353 -353
  86. package/standard-tools/language-guard/guard_service_client.py +264 -264
  87. package/standard-tools/language-guard/language_quality.py +377 -377
  88. 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
+ }