@kontourai/lookout 0.5.0 → 0.5.1

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.
@@ -13,6 +13,8 @@ export { createLookoutSnapshotStore, resolveLookoutSnapshot, } from "./snapshot-
13
13
  export type { ResolveLookoutSnapshotOptions } from "./snapshot-store.js";
14
14
  export { admitProposalObservation } from "./observation-admission.js";
15
15
  export type { AdmitProposalObservationInput, AdmittedProposalObservation, AdmittedSnapshotIdentity, ObservationAdmissionError, ObservationAdmissionErrorKind, ObservationAdmissionResult } from "./observation-admission.js";
16
+ export { admitSourceCapture, admitSourceCheck } from "./source-admission.js";
17
+ export type { AdmittedSourceCapture, AdmittedSourceCheck, SourceAdmissionError, SourceAdmissionResult } from "./source-admission.js";
16
18
  export type { SnapshotSourceRefResolution } from "@kontourai/forage/fetch";
17
19
  export { canonicalValueKey } from "./canonical-value.js";
18
20
  export type { CanonicalValueKey, CanonicalValueFormatVersion, CanonicalValueResult, DiffKernelError, DiffKernelErrorKind, DiffResult, IdentityResult, } from "./canonical-value.js";
package/dist/src/index.js CHANGED
@@ -5,6 +5,7 @@ export { loadRegistry, LookoutRegistry, parseRegistry, RegistryValidationError,
5
5
  export { inMemorySourceStore } from "./source-store.js";
6
6
  export { createLookoutSnapshotStore, resolveLookoutSnapshot, } from "./snapshot-store.js";
7
7
  export { admitProposalObservation } from "./observation-admission.js";
8
+ export { admitSourceCapture, admitSourceCheck } from "./source-admission.js";
8
9
  export { canonicalValueKey } from "./canonical-value.js";
9
10
  export { compareStructural, diffKeyedMultiset } from "./structural-diff.js";
10
11
  export { diffProposalSets, extractionProposalIdentity } from "./proposal-diff.js";
@@ -30,6 +30,15 @@ export type ObservationAdmissionResult = {
30
30
  readonly ok: false;
31
31
  readonly error: ObservationAdmissionError;
32
32
  };
33
+ /** A stable exact-reader capability, captured before asynchronous admission begins. */
34
+ export declare function captureExactSnapshotReader(store: SnapshotStore): SnapshotStore | null;
35
+ export type SnapshotSourceBinding = "url-binding" | "redirect-binding";
36
+ /**
37
+ * Bind an authenticated snapshot to a registry source without exposing its
38
+ * body. Historical direct captures can predate a registry URL change, but a
39
+ * legacy reference cannot authenticate any redirect capture.
40
+ */
41
+ export declare function snapshotSourceBinding(registeredUrl: string, finalUrl: string, redirects: readonly string[] | undefined, integrity: "snapshot-envelope" | "body-and-identity", current: boolean): SnapshotSourceBinding | null;
33
42
  export interface AdmitProposalObservationInput {
34
43
  readonly source: LookoutSource;
35
44
  readonly current: ProposalSetObservation;
@@ -45,3 +54,6 @@ export interface AdmitProposalObservationInput {
45
54
  * snapshot bodies nor reads/writes the observation store.
46
55
  */
47
56
  export declare function admitProposalObservation(input: AdmitProposalObservationInput): Promise<ObservationAdmissionResult>;
57
+ export declare function normalizedHttpUrl(value: string): string | null;
58
+ /** Forage records visited redirect URLs before the final URL. */
59
+ export declare function validRedirectChain(redirects: readonly string[], finalUrl: string): boolean;
@@ -1,4 +1,32 @@
1
1
  import { resolveLookoutSnapshot } from "./snapshot-store.js";
2
+ /** A stable exact-reader capability, captured before asynchronous admission begins. */
3
+ export function captureExactSnapshotReader(store) {
4
+ try {
5
+ if (!store || typeof store !== "object" || typeof store.findExact !== "function")
6
+ return null;
7
+ const findExact = store.findExact.bind(store);
8
+ return { findExact };
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ /**
15
+ * Bind an authenticated snapshot to a registry source without exposing its
16
+ * body. Historical direct captures can predate a registry URL change, but a
17
+ * legacy reference cannot authenticate any redirect capture.
18
+ */
19
+ export function snapshotSourceBinding(registeredUrl, finalUrl, redirects, integrity, current) {
20
+ const registered = normalizedHttpUrl(registeredUrl);
21
+ const final = normalizedHttpUrl(finalUrl);
22
+ if (registered === null || final === null)
23
+ return "url-binding";
24
+ if (!redirects?.length)
25
+ return !current || final === registered ? null : "url-binding";
26
+ if (integrity !== "snapshot-envelope" || !validRedirectChain(redirects, finalUrl))
27
+ return "redirect-binding";
28
+ return !current || normalizedHttpUrl(redirects[0]) === registered ? null : "redirect-binding";
29
+ }
2
30
  /**
3
31
  * Authenticate proposal-observation snapshot references before they can be
4
32
  * diffed or committed. It is deliberately metadata-only: it neither exposes
@@ -72,13 +100,14 @@ function captureInvocation(input) {
72
100
  if (!input || typeof input !== "object")
73
101
  return null;
74
102
  const image = structuredClone({ source: input.source, current: input.current, check: input.check, prior: input.prior });
75
- return { ...image, snapshotStore: input.snapshotStore };
103
+ const snapshotStore = captureExactSnapshotReader(input.snapshotStore);
104
+ return snapshotStore === null ? null : { ...image, snapshotStore };
76
105
  }
77
106
  catch {
78
107
  return null;
79
108
  }
80
109
  }
81
- function normalizedHttpUrl(value) {
110
+ export function normalizedHttpUrl(value) {
82
111
  try {
83
112
  const url = new URL(value);
84
113
  if ((url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password)
@@ -91,19 +120,15 @@ function normalizedHttpUrl(value) {
91
120
  }
92
121
  }
93
122
  function bindCurrentSnapshot(finalUrl, redirects, integrity, registeredUrl) {
94
- const normalizedFinal = normalizedHttpUrl(finalUrl);
95
- if (normalizedFinal === null)
96
- return failure("insufficient-binding", "url-binding", "Snapshot URL is not an admissible HTTP URL");
97
- if (!redirects?.length)
98
- return normalizedFinal === registeredUrl ? null : failure("insufficient-binding", "url-binding", "Snapshot URL is not bound to the registered source");
99
- if (integrity !== "snapshot-envelope")
100
- return failure("insufficient-binding", "redirect-binding", "Legacy snapshot references cannot authenticate redirect captures");
101
- if (normalizedHttpUrl(redirects[0]) !== registeredUrl || !validRedirectChain(redirects, finalUrl))
123
+ const binding = snapshotSourceBinding(registeredUrl, finalUrl, redirects, integrity, true);
124
+ if (binding === "url-binding")
125
+ return failure("insufficient-binding", "url-binding", "Snapshot URL is not bound to the registered source");
126
+ if (binding === "redirect-binding")
102
127
  return failure("insufficient-binding", "redirect-binding", "Snapshot redirect capture is not admissibly bound");
103
128
  return null;
104
129
  }
105
130
  /** Forage records visited redirect URLs before the final URL. */
106
- function validRedirectChain(redirects, finalUrl) {
131
+ export function validRedirectChain(redirects, finalUrl) {
107
132
  const urls = [...redirects, finalUrl].map(normalizedHttpUrl);
108
133
  if (urls.some((url) => url === null))
109
134
  return false;
@@ -0,0 +1,37 @@
1
+ import type { SnapshotStore } from "@kontourai/forage";
2
+ import type { CheckResult } from "./check-result.js";
3
+ import { type AdmittedSnapshotIdentity } from "./observation-admission.js";
4
+ import { type LookoutSource } from "./registry.js";
5
+ export type SourceAdmissionError = {
6
+ readonly kind: "invalid-input" | "unresolved" | "insufficient-binding";
7
+ readonly message: string;
8
+ };
9
+ export type SourceAdmissionResult<T> = {
10
+ readonly ok: true;
11
+ readonly value: T;
12
+ } | {
13
+ readonly ok: false;
14
+ readonly error: SourceAdmissionError;
15
+ };
16
+ export interface AdmittedSourceCapture {
17
+ readonly capture: AdmittedSnapshotIdentity;
18
+ }
19
+ export interface AdmittedSourceCheck {
20
+ readonly prior: AdmittedSnapshotIdentity | null;
21
+ readonly current: AdmittedSnapshotIdentity;
22
+ readonly checkedAt: string;
23
+ readonly resultKind: Exclude<CheckResult["kind"], "error">;
24
+ }
25
+ /** Metadata-only capture admission; it neither fetches nor persists. */
26
+ export declare function admitSourceCapture(input: {
27
+ source: LookoutSource;
28
+ snapshotRef: string;
29
+ snapshotStore: SnapshotStore;
30
+ }): Promise<SourceAdmissionResult<AdmittedSourceCapture>>;
31
+ /** Admit a genuine CheckRunner outcome; this validates relationships, not HTTP provenance. */
32
+ export declare function admitSourceCheck(input: {
33
+ source: LookoutSource;
34
+ check: CheckResult;
35
+ expectedPriorSnapshotRef: string | null;
36
+ snapshotStore: SnapshotStore;
37
+ }): Promise<SourceAdmissionResult<AdmittedSourceCheck>>;
@@ -0,0 +1,138 @@
1
+ import { parseSnapshotSourceRef } from "@kontourai/forage/fetch";
2
+ import { captureExactSnapshotReader, normalizedHttpUrl, snapshotSourceBinding, } from "./observation-admission.js";
3
+ import { parseRegistry } from "./registry.js";
4
+ import { resolveLookoutSnapshot } from "./snapshot-store.js";
5
+ /** Metadata-only capture admission; it neither fetches nor persists. */
6
+ export async function admitSourceCapture(input) {
7
+ const captured = captureCapture(input);
8
+ if (!captured || !validSource(captured.source) || !canonicalRef(captured.snapshotRef))
9
+ return fail("invalid-input", "Capture admission input is malformed");
10
+ const value = await resolve(captured.source, captured.snapshotRef, captured.snapshotStore, true);
11
+ return value.ok ? { ok: true, value: { capture: value.value } } : value;
12
+ }
13
+ /** Admit a genuine CheckRunner outcome; this validates relationships, not HTTP provenance. */
14
+ export async function admitSourceCheck(input) {
15
+ const captured = captureCheck(input);
16
+ if (!captured || !validSource(captured.source) || !validCheck(captured.source, captured.check))
17
+ return fail("invalid-input", "Check admission input is malformed");
18
+ const check = captured.check;
19
+ if (check.kind === "error")
20
+ return fail("invalid-input", "An error result has no successful capture");
21
+ const refs = refsFor(check, captured.expectedPriorSnapshotRef);
22
+ // Validate every durable reference before the first exact lookup: admission
23
+ // never lets a malformed historical anchor cause partial capability I/O.
24
+ if (!refs || !canonicalRef(refs.current) || (refs.prior !== null && !canonicalRef(refs.prior)))
25
+ return fail("invalid-input", "Check references do not match the expected baseline");
26
+ const current = await resolve(captured.source, refs.current, captured.snapshotStore, true);
27
+ if (!current.ok)
28
+ return current;
29
+ const prior = refs.prior === null ? null : await resolve(captured.source, refs.prior, captured.snapshotStore, false);
30
+ if (prior !== null && !prior.ok)
31
+ return prior;
32
+ if (check.kind === "unchanged-hash" && prior !== null && prior.ok && (prior.value.bodyHash !== current.value.bodyHash || prior.value.url !== current.value.url))
33
+ return fail("insufficient-binding", "Same-hash result does not bind one resource capture");
34
+ if (check.kind === "changed" && check.changeBasis === "hash" && prior !== null && prior.ok && prior.value.bodyHash === current.value.bodyHash && prior.value.url === current.value.url)
35
+ return fail("insufficient-binding", "Changed result does not bind a changed capture");
36
+ return { ok: true, value: { prior: prior?.ok ? prior.value : null, current: current.value, checkedAt: check.checkedAt, resultKind: check.kind } };
37
+ }
38
+ function refsFor(check, expected) {
39
+ if (check.kind === "unchanged-304")
40
+ return expected !== null && check.snapshotRef === expected ? { prior: expected, current: check.snapshotRef } : null;
41
+ if (check.kind === "unchanged-hash")
42
+ return check.priorSnapshotRef === expected ? { prior: expected, current: check.currentSnapshotRef } : null;
43
+ return check.changeBasis === "initial"
44
+ ? expected === null ? { prior: null, current: check.currentSnapshotRef } : null
45
+ : check.priorSnapshotRef === expected && expected !== null ? { prior: expected, current: check.currentSnapshotRef } : null;
46
+ }
47
+ function validSource(source) {
48
+ try {
49
+ if (!source || typeof source !== "object" || normalizedHttpUrl(source.url) === null)
50
+ return false;
51
+ parseRegistry({ version: 1, sources: [source] });
52
+ return true;
53
+ }
54
+ catch {
55
+ return false;
56
+ }
57
+ }
58
+ function validCheck(source, check) {
59
+ if (!check || typeof check !== "object" || Array.isArray(check))
60
+ return false;
61
+ const common = () => check.sourceId === source.id && check.sourceUrl === source.url && validTimestamp(check.checkedAt) && validWarnings(check.warnings);
62
+ if (check.kind === "unchanged-304")
63
+ return closed(check, ["sourceId", "sourceUrl", "checkedAt", "warnings", "kind", "snapshotRef"]) && common() && typeof check.snapshotRef === "string";
64
+ if (check.kind === "unchanged-hash")
65
+ return closed(check, ["sourceId", "sourceUrl", "checkedAt", "warnings", "kind", "priorSnapshotRef", "currentSnapshotRef"]) && common() && typeof check.priorSnapshotRef === "string" && typeof check.currentSnapshotRef === "string";
66
+ if (check.kind === "changed")
67
+ return closed(check, ["sourceId", "sourceUrl", "checkedAt", "warnings", "kind", "priorSnapshotRef", "currentSnapshotRef", "changeBasis"]) && common() && typeof check.currentSnapshotRef === "string" && (check.changeBasis === "initial" || check.changeBasis === "hash") && (check.changeBasis === "initial" ? check.priorSnapshotRef === null : typeof check.priorSnapshotRef === "string");
68
+ return false;
69
+ }
70
+ function closed(value, keys) {
71
+ if (Object.getPrototypeOf(value) !== Object.prototype)
72
+ return false;
73
+ const actual = Object.keys(value);
74
+ return actual.length === keys.length && keys.every((key) => Object.hasOwn(value, key));
75
+ }
76
+ function validWarnings(value) {
77
+ if (!Array.isArray(value))
78
+ return false;
79
+ const keys = Object.keys(value);
80
+ if (keys.length !== value.length)
81
+ return false;
82
+ for (let index = 0; index < value.length; index++) {
83
+ if (!Object.hasOwn(value, index) || typeof value[index] !== "string")
84
+ return false;
85
+ }
86
+ return true;
87
+ }
88
+ function validTimestamp(value) {
89
+ if (typeof value !== "string")
90
+ return false;
91
+ const instant = new Date(value);
92
+ return !Number.isNaN(instant.valueOf()) && instant.toISOString() === value;
93
+ }
94
+ function canonicalRef(value) {
95
+ if (typeof value !== "string")
96
+ return false;
97
+ const parsed = parseSnapshotSourceRef(value);
98
+ if (!parsed || !/^[a-f0-9]{64}$/.test(parsed.bodyHash) || (parsed.snapshotDigest !== undefined && !/^[a-f0-9]{64}$/.test(parsed.snapshotDigest)))
99
+ return false;
100
+ const query = new URLSearchParams({ url: parsed.url, sha256: parsed.bodyHash, fetchedAt: parsed.fetchedAt });
101
+ if (parsed.snapshotDigest !== undefined)
102
+ query.set("snapshotSha256", parsed.snapshotDigest);
103
+ return value === `forage-snapshot:${encodeURIComponent(parsed.sourceId)}?${query.toString()}`;
104
+ }
105
+ function captureCapture(input) {
106
+ try {
107
+ if (!input || typeof input !== "object")
108
+ return null;
109
+ const snapshotStore = captureExactSnapshotReader(input.snapshotStore);
110
+ return snapshotStore === null ? null : { ...structuredClone({ source: input.source, snapshotRef: input.snapshotRef }), snapshotStore };
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function captureCheck(input) {
117
+ try {
118
+ if (!input || typeof input !== "object")
119
+ return null;
120
+ const snapshotStore = captureExactSnapshotReader(input.snapshotStore);
121
+ return snapshotStore === null ? null : { ...structuredClone({ source: input.source, check: input.check, expectedPriorSnapshotRef: input.expectedPriorSnapshotRef }), snapshotStore };
122
+ }
123
+ catch {
124
+ return null;
125
+ }
126
+ }
127
+ async function resolve(source, ref, store, current) {
128
+ const resolved = await resolveLookoutSnapshot(ref, { store });
129
+ if (!resolved.ok || resolved.snapshot.sourceId !== source.id)
130
+ return fail("unresolved", "Snapshot reference could not be admitted");
131
+ const binding = snapshotSourceBinding(source.url, resolved.snapshot.url, resolved.snapshot.redirects, resolved.integrity, current);
132
+ if (binding !== null)
133
+ return fail("insufficient-binding", binding === "redirect-binding" ? "Snapshot redirect capture is not admissibly bound" : "Snapshot URL is not bound to the registered source");
134
+ return { ok: true, value: { sourceId: source.id, snapshotRef: ref, url: resolved.snapshot.url, bodyHash: resolved.snapshot.bodyHash, fetchedAt: resolved.snapshot.fetchedAt, ...(resolved.reference.snapshotDigest ? { snapshotDigest: resolved.reference.snapshotDigest } : {}), integrity: resolved.integrity } };
135
+ }
136
+ function fail(kind, message) {
137
+ return { ok: false, error: { kind, message } };
138
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kontourai/lookout",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "A small source registry and drift-check runner built on Forage snapshots.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -47,7 +47,7 @@
47
47
  },
48
48
  "dependencies": {
49
49
  "@kontourai/datum": "0.7.0",
50
- "@kontourai/forage": "0.6.0",
50
+ "@kontourai/forage": "0.6.1",
51
51
  "@kontourai/traverse": "0.25.1"
52
52
  },
53
53
  "devDependencies": {