@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.
package/dist/src/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
95
|
-
if (
|
|
96
|
-
return failure("insufficient-binding", "url-binding", "Snapshot URL is not
|
|
97
|
-
if (
|
|
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.
|
|
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.
|
|
50
|
+
"@kontourai/forage": "0.6.1",
|
|
51
51
|
"@kontourai/traverse": "0.25.1"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|