@sudobility/sider_types 0.0.2 → 0.0.3
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/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/recipe-hash.d.ts +4 -0
- package/dist/recipe-hash.js +23 -0
- package/dist/templating.d.ts +13 -0
- package/dist/templating.js +67 -0
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/recipe-hash.ts +25 -0
- package/src/templating.ts +70 -0
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { InvocationRecipe } from "./registry";
|
|
2
|
+
/** Stable stringify (sorted keys) so two users' equivalent recipes hash equal. */
|
|
3
|
+
export declare function canonicalizeRecipe(recipe: InvocationRecipe): string;
|
|
4
|
+
export declare function hashRecipe(recipe: InvocationRecipe): string;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Recipe canonicalization + corroboration hashing. Pure domain logic shared by
|
|
2
|
+
// the backend (trust engine: distinct-recipe-hash quorum) and the frontend. A
|
|
3
|
+
// stable sorted-key stringify makes two users' equivalent recipes hash equal.
|
|
4
|
+
/** Stable stringify (sorted keys) so two users' equivalent recipes hash equal. */
|
|
5
|
+
export function canonicalizeRecipe(recipe) {
|
|
6
|
+
return stableStringify(recipe);
|
|
7
|
+
}
|
|
8
|
+
export function hashRecipe(recipe) {
|
|
9
|
+
const s = canonicalizeRecipe(recipe);
|
|
10
|
+
let h = 0;
|
|
11
|
+
for (let i = 0; i < s.length; i++)
|
|
12
|
+
h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
13
|
+
return (h >>> 0).toString(16);
|
|
14
|
+
}
|
|
15
|
+
function stableStringify(v) {
|
|
16
|
+
if (v === null || typeof v !== "object")
|
|
17
|
+
return JSON.stringify(v) ?? "null";
|
|
18
|
+
if (Array.isArray(v))
|
|
19
|
+
return `[${v.map(stableStringify).join(",")}]`;
|
|
20
|
+
const obj = v;
|
|
21
|
+
const keys = Object.keys(obj).sort();
|
|
22
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Observation } from "./runtime";
|
|
2
|
+
/** Replace id-like path segments with `{id}`: /sections/120/seats → /sections/{id}/seats */
|
|
3
|
+
export declare function templatePath(rawUrl: string): string;
|
|
4
|
+
/**
|
|
5
|
+
* GraphQL keying: one URL serves many operations, so cluster on operation name,
|
|
6
|
+
* not path. Returns undefined for non-GraphQL requests.
|
|
7
|
+
*/
|
|
8
|
+
export declare function graphqlOperationOf(url: string, body: unknown): string | undefined;
|
|
9
|
+
type Clusterable = Pick<Observation, "method" | "url" | "requestBody">;
|
|
10
|
+
/** Stable key identifying the endpoint template an observation belongs to. */
|
|
11
|
+
export declare function endpointKey(o: Clusterable): string;
|
|
12
|
+
export declare function clusterObservations<T extends Clusterable>(observations: T[]): Map<string, T[]>;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Deterministic clustering & path templating. Pure domain logic shared by the
|
|
2
|
+
// backend (distillation) and the frontend security lib — groups raw observations
|
|
3
|
+
// into endpoint templates so the brain infers semantics once per endpoint, not
|
|
4
|
+
// once per request. No runtime dependencies.
|
|
5
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
const LONGHEX_RE = /^[0-9a-f]{16,}$/i;
|
|
7
|
+
/** Replace id-like path segments with `{id}`: /sections/120/seats → /sections/{id}/seats */
|
|
8
|
+
export function templatePath(rawUrl) {
|
|
9
|
+
let pathname = rawUrl;
|
|
10
|
+
try {
|
|
11
|
+
pathname = new URL(rawUrl, "http://sider.local").pathname;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
/* keep raw */
|
|
15
|
+
}
|
|
16
|
+
return pathname
|
|
17
|
+
.split("/")
|
|
18
|
+
.map((seg) => {
|
|
19
|
+
if (seg === "")
|
|
20
|
+
return seg;
|
|
21
|
+
if (/^\d+$/.test(seg))
|
|
22
|
+
return "{id}";
|
|
23
|
+
if (UUID_RE.test(seg))
|
|
24
|
+
return "{id}";
|
|
25
|
+
if (LONGHEX_RE.test(seg))
|
|
26
|
+
return "{id}";
|
|
27
|
+
return seg;
|
|
28
|
+
})
|
|
29
|
+
.join("/");
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* GraphQL keying: one URL serves many operations, so cluster on operation name,
|
|
33
|
+
* not path. Returns undefined for non-GraphQL requests.
|
|
34
|
+
*/
|
|
35
|
+
export function graphqlOperationOf(url, body) {
|
|
36
|
+
if (body && typeof body === "object") {
|
|
37
|
+
const b = body;
|
|
38
|
+
if (typeof b["operationName"] === "string" && b["operationName"]) {
|
|
39
|
+
return b["operationName"];
|
|
40
|
+
}
|
|
41
|
+
if (typeof b["query"] === "string") {
|
|
42
|
+
const m = /\b(query|mutation|subscription)\s+([A-Za-z0-9_]+)/.exec(b["query"]);
|
|
43
|
+
if (m && m[2])
|
|
44
|
+
return m[2];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (/\/graphql\b/i.test(url))
|
|
48
|
+
return "anonymous";
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
/** Stable key identifying the endpoint template an observation belongs to. */
|
|
52
|
+
export function endpointKey(o) {
|
|
53
|
+
const op = graphqlOperationOf(o.url, o.requestBody);
|
|
54
|
+
return `${o.method} ${templatePath(o.url)}${op ? ` #${op}` : ""}`;
|
|
55
|
+
}
|
|
56
|
+
export function clusterObservations(observations) {
|
|
57
|
+
const groups = new Map();
|
|
58
|
+
for (const o of observations) {
|
|
59
|
+
const key = endpointKey(o);
|
|
60
|
+
const g = groups.get(key);
|
|
61
|
+
if (g)
|
|
62
|
+
g.push(o);
|
|
63
|
+
else
|
|
64
|
+
groups.set(key, [o]);
|
|
65
|
+
}
|
|
66
|
+
return groups;
|
|
67
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Recipe canonicalization + corroboration hashing. Pure domain logic shared by
|
|
2
|
+
// the backend (trust engine: distinct-recipe-hash quorum) and the frontend. A
|
|
3
|
+
// stable sorted-key stringify makes two users' equivalent recipes hash equal.
|
|
4
|
+
|
|
5
|
+
import type { InvocationRecipe } from "./registry";
|
|
6
|
+
|
|
7
|
+
/** Stable stringify (sorted keys) so two users' equivalent recipes hash equal. */
|
|
8
|
+
export function canonicalizeRecipe(recipe: InvocationRecipe): string {
|
|
9
|
+
return stableStringify(recipe);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hashRecipe(recipe: InvocationRecipe): string {
|
|
13
|
+
const s = canonicalizeRecipe(recipe);
|
|
14
|
+
let h = 0;
|
|
15
|
+
for (let i = 0; i < s.length; i++) h = (Math.imul(31, h) + s.charCodeAt(i)) | 0;
|
|
16
|
+
return (h >>> 0).toString(16);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function stableStringify(v: unknown): string {
|
|
20
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v) ?? "null";
|
|
21
|
+
if (Array.isArray(v)) return `[${v.map(stableStringify).join(",")}]`;
|
|
22
|
+
const obj = v as Record<string, unknown>;
|
|
23
|
+
const keys = Object.keys(obj).sort();
|
|
24
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Deterministic clustering & path templating. Pure domain logic shared by the
|
|
2
|
+
// backend (distillation) and the frontend security lib — groups raw observations
|
|
3
|
+
// into endpoint templates so the brain infers semantics once per endpoint, not
|
|
4
|
+
// once per request. No runtime dependencies.
|
|
5
|
+
|
|
6
|
+
import type { Observation } from "./runtime";
|
|
7
|
+
|
|
8
|
+
const UUID_RE =
|
|
9
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10
|
+
const LONGHEX_RE = /^[0-9a-f]{16,}$/i;
|
|
11
|
+
|
|
12
|
+
/** Replace id-like path segments with `{id}`: /sections/120/seats → /sections/{id}/seats */
|
|
13
|
+
export function templatePath(rawUrl: string): string {
|
|
14
|
+
let pathname = rawUrl;
|
|
15
|
+
try {
|
|
16
|
+
pathname = new URL(rawUrl, "http://sider.local").pathname;
|
|
17
|
+
} catch {
|
|
18
|
+
/* keep raw */
|
|
19
|
+
}
|
|
20
|
+
return pathname
|
|
21
|
+
.split("/")
|
|
22
|
+
.map((seg) => {
|
|
23
|
+
if (seg === "") return seg;
|
|
24
|
+
if (/^\d+$/.test(seg)) return "{id}";
|
|
25
|
+
if (UUID_RE.test(seg)) return "{id}";
|
|
26
|
+
if (LONGHEX_RE.test(seg)) return "{id}";
|
|
27
|
+
return seg;
|
|
28
|
+
})
|
|
29
|
+
.join("/");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GraphQL keying: one URL serves many operations, so cluster on operation name,
|
|
34
|
+
* not path. Returns undefined for non-GraphQL requests.
|
|
35
|
+
*/
|
|
36
|
+
export function graphqlOperationOf(url: string, body: unknown): string | undefined {
|
|
37
|
+
if (body && typeof body === "object") {
|
|
38
|
+
const b = body as Record<string, unknown>;
|
|
39
|
+
if (typeof b["operationName"] === "string" && b["operationName"]) {
|
|
40
|
+
return b["operationName"];
|
|
41
|
+
}
|
|
42
|
+
if (typeof b["query"] === "string") {
|
|
43
|
+
const m = /\b(query|mutation|subscription)\s+([A-Za-z0-9_]+)/.exec(b["query"]);
|
|
44
|
+
if (m && m[2]) return m[2];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (/\/graphql\b/i.test(url)) return "anonymous";
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type Clusterable = Pick<Observation, "method" | "url" | "requestBody">;
|
|
52
|
+
|
|
53
|
+
/** Stable key identifying the endpoint template an observation belongs to. */
|
|
54
|
+
export function endpointKey(o: Clusterable): string {
|
|
55
|
+
const op = graphqlOperationOf(o.url, o.requestBody);
|
|
56
|
+
return `${o.method} ${templatePath(o.url)}${op ? ` #${op}` : ""}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function clusterObservations<T extends Clusterable>(
|
|
60
|
+
observations: T[],
|
|
61
|
+
): Map<string, T[]> {
|
|
62
|
+
const groups = new Map<string, T[]>();
|
|
63
|
+
for (const o of observations) {
|
|
64
|
+
const key = endpointKey(o);
|
|
65
|
+
const g = groups.get(key);
|
|
66
|
+
if (g) g.push(o);
|
|
67
|
+
else groups.set(key, [o]);
|
|
68
|
+
}
|
|
69
|
+
return groups;
|
|
70
|
+
}
|