@dontfeedmachines/fieldkit 0.0.0-stage → 0.4.2
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/CHANGELOG.md +11 -0
- package/LICENSE +203 -0
- package/NOTICE +131 -0
- package/README.md +12 -2
- package/config/menu.default.json +6 -0
- package/config/wiki.default.json +32 -0
- package/css/base.css +150 -0
- package/package.json +44 -3
- package/pouches/sutures/astro/AnsiLogo.astro +51 -0
- package/pouches/sutures/astro/Barcode.astro +19 -0
- package/pouches/sutures/astro/BomTable.astro +44 -0
- package/pouches/sutures/astro/Breadcrumb.astro +56 -0
- package/pouches/sutures/astro/BuildStep.astro +42 -0
- package/pouches/sutures/astro/BuildSteps.astro +63 -0
- package/pouches/sutures/astro/Button.astro +5 -0
- package/pouches/sutures/astro/CTA.astro +58 -0
- package/pouches/sutures/astro/Difficulty.astro +27 -0
- package/pouches/sutures/astro/Footer.astro +80 -0
- package/pouches/sutures/astro/Glossary.astro +64 -0
- package/pouches/sutures/astro/Grid.astro +54 -0
- package/pouches/sutures/astro/Header.astro +270 -0
- package/pouches/sutures/astro/Hero.astro +38 -0
- package/pouches/sutures/astro/ImageMod.astro +66 -0
- package/pouches/sutures/astro/InteractiveCircuit.astro +192 -0
- package/pouches/sutures/astro/LegalWarning.astro +18 -0
- package/pouches/sutures/astro/MobileMenuToggle.astro +105 -0
- package/pouches/sutures/astro/PageFrame.astro +166 -0
- package/pouches/sutures/astro/Search.astro +541 -0
- package/pouches/sutures/astro/Section.astro +14 -0
- package/pouches/sutures/astro/SidebarNav.astro +106 -0
- package/pouches/sutures/astro/SidebarRestorePoint.astro +10 -0
- package/pouches/sutures/astro/SidebarSublist.astro +257 -0
- package/pouches/sutures/astro/SiteTitle.astro +65 -0
- package/pouches/sutures/astro/SpecTable.astro +64 -0
- package/pouches/sutures/astro/TeletextChar.astro +51 -0
- package/pouches/sutures/astro/ThemeSwitch.astro +93 -0
- package/pouches/sutures/astro/ThreatLevel.astro +77 -0
- package/pouches/sutures/astro/ThreatModel.astro +32 -0
- package/pouches/sutures/astro/TwoColumnContent.astro +84 -0
- package/pouches/sutures/astro/Win.astro +13 -0
- package/pouches/sutures/astro/index.ts +21 -0
- package/src/elements.ts +404 -0
- package/src/lib/scrapeview.ts +61 -0
- package/src/lib/seal.ts +157 -0
- package/src/lib/teletext-chars.ts +723 -0
- package/src/node/astro.ts +121 -0
- package/src/node/registry.ts +221 -0
- package/src/node/seal.ts +7 -0
- package/src/node/wiki.ts +45 -0
- package/src/wiki/ansi.ts +15 -0
- package/src/wiki/config.ts +5 -0
- package/src/wiki/teletext.ts +21 -0
- package/src/wiki/textConverter.ts +16 -0
- package/src/wiki/virtual.d.ts +7 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type ScrapeResult = {
|
|
2
|
+
readable: string;
|
|
3
|
+
structured: {
|
|
4
|
+
title: string;
|
|
5
|
+
meta: { name: string; content: string }[];
|
|
6
|
+
jsonld: string[];
|
|
7
|
+
og: { property: string; content: string }[];
|
|
8
|
+
};
|
|
9
|
+
canaries: string[];
|
|
10
|
+
poison: string[];
|
|
11
|
+
chars: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function stripScripts(html: string) {
|
|
15
|
+
return html
|
|
16
|
+
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
|
17
|
+
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
|
18
|
+
.replace(/ on[a-z]+="[^"]*"/gi, "")
|
|
19
|
+
.replace(/ on[a-z]+='[^']*'/gi, "");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function tagText(html: string) {
|
|
23
|
+
return html
|
|
24
|
+
.replace(/<[^>]+>/g, " ")
|
|
25
|
+
.replace(/ /g, " ")
|
|
26
|
+
.replace(/&/g, "&")
|
|
27
|
+
.replace(/</g, "<")
|
|
28
|
+
.replace(/>/g, ">")
|
|
29
|
+
.replace(/"/g, '"')
|
|
30
|
+
.replace(/'/g, "'")
|
|
31
|
+
.replace(/\s+/g, " ")
|
|
32
|
+
.trim();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function pick(html: string, tag: string) {
|
|
36
|
+
const m = html.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, "i"));
|
|
37
|
+
return m ? m[1] : "";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function scrape(html: string): ScrapeResult {
|
|
41
|
+
const noScript = stripScripts(html);
|
|
42
|
+
const region = pick(noScript, "article") || pick(noScript, "main") || pick(noScript, "body") || noScript;
|
|
43
|
+
const readable = tagText(region);
|
|
44
|
+
const structured: ScrapeResult["structured"] = { title: tagText(pick(html, "title")), meta: [], jsonld: [], og: [] };
|
|
45
|
+
let m: RegExpExecArray | null;
|
|
46
|
+
const metaRe = /<meta\s+([^>]+)>/gi;
|
|
47
|
+
while ((m = metaRe.exec(html))) {
|
|
48
|
+
const name = (m[1].match(/name=["']([^"']+)["']/i) || [])[1];
|
|
49
|
+
const prop = (m[1].match(/property=["']([^"']+)["']/i) || [])[1];
|
|
50
|
+
const content = (m[1].match(/content=["']([^"']*)["']/i) || [])[1] ?? "";
|
|
51
|
+
if (prop && prop.startsWith("og:")) structured.og.push({ property: prop, content });
|
|
52
|
+
else if (name) structured.meta.push({ name, content });
|
|
53
|
+
}
|
|
54
|
+
const ldRe = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
|
|
55
|
+
while ((m = ldRe.exec(html))) structured.jsonld.push(m[1].trim());
|
|
56
|
+
const canaries = (html.match(/c[0-9a-f]{16,32}/gi) || []).filter((t) => /^c[0-9a-f]{16}/.test(t));
|
|
57
|
+
const poison: string[] = [];
|
|
58
|
+
const poisonRe = /<div[^>]*data-dfm-poison[^>]*>([\s\S]*?)<\/div>/gi;
|
|
59
|
+
while ((m = poisonRe.exec(html))) poison.push(tagText(m[1]));
|
|
60
|
+
return { readable, structured, canaries: [...new Set(canaries)], poison, chars: readable.length };
|
|
61
|
+
}
|
package/src/lib/seal.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as ed from "@noble/ed25519";
|
|
2
|
+
import { stripScripts, tagText } from "./scrapeview.ts";
|
|
3
|
+
|
|
4
|
+
export const BLOODOATH_V = "bloodoath/1";
|
|
5
|
+
export const DFM_SEAL_V1 = "dfm-seal-v1";
|
|
6
|
+
|
|
7
|
+
export const toHex = (b: Uint8Array) => ed.etc.bytesToHex(b);
|
|
8
|
+
export const fromHex = (h: string) => ed.etc.hexToBytes(h.trim().toLowerCase());
|
|
9
|
+
const enc = new TextEncoder();
|
|
10
|
+
|
|
11
|
+
export async function sha256Hex(data: string | Uint8Array): Promise<string> {
|
|
12
|
+
const bytes = typeof data === "string" ? enc.encode(data) : data;
|
|
13
|
+
const d = await crypto.subtle.digest("SHA-256", bytes as BufferSource);
|
|
14
|
+
return toHex(new Uint8Array(d));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SealKind = "page" | "text" | "file" | "oath" | "rung" | "post";
|
|
18
|
+
|
|
19
|
+
export type Cert = {
|
|
20
|
+
v: typeof BLOODOATH_V;
|
|
21
|
+
kind: SealKind;
|
|
22
|
+
pubkey: string;
|
|
23
|
+
hash: string;
|
|
24
|
+
issued: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
title?: string;
|
|
27
|
+
handle?: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function signingMessage(c: Pick<Cert, "kind" | "pubkey" | "hash" | "issued">) {
|
|
31
|
+
return enc.encode(`${BLOODOATH_V}|${c.kind}|${c.pubkey}|${c.hash}|${c.issued}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function newKey() {
|
|
35
|
+
const { secretKey, publicKey } = await ed.keygenAsync();
|
|
36
|
+
return { secret: toHex(secretKey), pubkey: toHex(publicKey) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function pubFromSecret(secretHex: string) {
|
|
40
|
+
return toHex(await ed.getPublicKeyAsync(fromHex(secretHex)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function signRaw(message: Uint8Array, secretHex: string) {
|
|
44
|
+
return toHex(await ed.signAsync(message, fromHex(secretHex)));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function verifyRaw(message: Uint8Array, sigHex: string, pubHex: string) {
|
|
48
|
+
try {
|
|
49
|
+
return await ed.verifyAsync(fromHex(sigHex), message, fromHex(pubHex));
|
|
50
|
+
} catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function seal(
|
|
56
|
+
content: string | Uint8Array,
|
|
57
|
+
kind: SealKind,
|
|
58
|
+
secretHex: string,
|
|
59
|
+
extra: { title?: string; handle?: string; issued?: string } = {},
|
|
60
|
+
): Promise<Cert> {
|
|
61
|
+
const pubkey = await pubFromSecret(secretHex);
|
|
62
|
+
const hash = await sha256Hex(content);
|
|
63
|
+
const issued = extra.issued ?? new Date().toISOString();
|
|
64
|
+
const signature = await signRaw(signingMessage({ kind, pubkey, hash, issued }), secretHex);
|
|
65
|
+
const c: Cert = { v: BLOODOATH_V, kind, pubkey, hash, issued, signature };
|
|
66
|
+
if (extra.title) c.title = extra.title;
|
|
67
|
+
if (extra.handle) c.handle = extra.handle;
|
|
68
|
+
return c;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type CheckState = "valid" | "tampered-content" | "bad-signature" | "unsealed";
|
|
72
|
+
|
|
73
|
+
export async function checkCert(cert: Partial<Cert> | null, content?: string | Uint8Array): Promise<CheckState> {
|
|
74
|
+
if (!cert || cert.v !== BLOODOATH_V || !cert.pubkey || !cert.signature || !cert.hash || !cert.issued || !cert.kind) {
|
|
75
|
+
return "unsealed";
|
|
76
|
+
}
|
|
77
|
+
const ok = await verifyRaw(signingMessage(cert as Cert), cert.signature, cert.pubkey);
|
|
78
|
+
if (!ok) return "bad-signature";
|
|
79
|
+
if (content !== undefined && (await sha256Hex(content)) !== cert.hash) return "tampered-content";
|
|
80
|
+
return "valid";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// the page seal covers the readable text of <main>, so adding the meta tags afterwards never changes the hash
|
|
84
|
+
export function canonText(html: string): string {
|
|
85
|
+
const noScript = stripScripts(html);
|
|
86
|
+
const m = noScript.match(/<main[^>]*>([\s\S]*?)<\/main>/i) ?? noScript.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
|
87
|
+
return tagText(m ? m[1] : noScript);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function certMeta(c: Cert): string {
|
|
91
|
+
const esc = (s: string) => s.replace(/&/g, "&").replace(/"/g, """).replace(/</g, "<");
|
|
92
|
+
const rows: [string, string][] = [
|
|
93
|
+
["bloodoath:v", c.v],
|
|
94
|
+
["bloodoath:kind", c.kind],
|
|
95
|
+
["bloodoath:pubkey", c.pubkey],
|
|
96
|
+
["bloodoath:hash", c.hash],
|
|
97
|
+
["bloodoath:issued", c.issued],
|
|
98
|
+
["bloodoath:signature", c.signature],
|
|
99
|
+
];
|
|
100
|
+
if (c.handle) rows.push(["bloodoath:handle", c.handle]);
|
|
101
|
+
return rows.map(([k, v]) => `<meta name="${k}" content="${esc(v)}">`).join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function readMeta(html: string): Partial<Cert> | null {
|
|
105
|
+
const get = (k: string) => {
|
|
106
|
+
const re = new RegExp(`<meta\\s+[^>]*name=["']${k.replace(":", "\\:")}["'][^>]*>`, "i");
|
|
107
|
+
const tag = html.match(re)?.[0];
|
|
108
|
+
return tag ? (tag.match(/content=["']([^"']*)["']/i)?.[1] ?? "").replace(/"/g, '"').replace(/&/g, "&") : undefined;
|
|
109
|
+
};
|
|
110
|
+
const v = get("bloodoath:v");
|
|
111
|
+
if (!v) return null;
|
|
112
|
+
return {
|
|
113
|
+
v: v as Cert["v"],
|
|
114
|
+
kind: get("bloodoath:kind") as SealKind,
|
|
115
|
+
pubkey: get("bloodoath:pubkey"),
|
|
116
|
+
hash: get("bloodoath:hash"),
|
|
117
|
+
issued: get("bloodoath:issued"),
|
|
118
|
+
signature: get("bloodoath:signature"),
|
|
119
|
+
handle: get("bloodoath:handle"),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type DfmSealV1 = { seal: string; author: string; title: string; issued: string; contentHash: string };
|
|
124
|
+
|
|
125
|
+
export function readDfmSealV1(html: string): DfmSealV1 | null {
|
|
126
|
+
const get = (k: string) => html.match(new RegExp(`<meta\\s+name=["']${k}["']\\s+content=["']([^"']*)["']`, "i"))?.[1];
|
|
127
|
+
const seal = get("dfm-seal");
|
|
128
|
+
if (!seal) return null;
|
|
129
|
+
return {
|
|
130
|
+
seal,
|
|
131
|
+
author: get("dfm-seal:author") ?? "",
|
|
132
|
+
title: get("dfm-seal:title") ?? "",
|
|
133
|
+
issued: get("dfm-seal:issued") ?? "",
|
|
134
|
+
contentHash: get("dfm-seal:content") ?? "",
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function checkDfmSealV1(s: DfmSealV1): Promise<boolean> {
|
|
139
|
+
const expect = await sha256Hex(`${DFM_SEAL_V1}|${s.author}|${s.title}|${s.issued}|${s.contentHash}`);
|
|
140
|
+
return expect === s.seal;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function detectC2PA(bytes: Uint8Array) {
|
|
144
|
+
const has = (s: string) => {
|
|
145
|
+
const n = [...s].map((c) => c.charCodeAt(0));
|
|
146
|
+
outer: for (let i = 0; i <= bytes.length - n.length; i++) {
|
|
147
|
+
for (let j = 0; j < n.length; j++) if (bytes[i + j] !== n[j]) continue outer;
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
};
|
|
152
|
+
return has("c2pa") || has("jumbf");
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function detectPGP(text: string) {
|
|
156
|
+
return /-----BEGIN PGP SIGNED MESSAGE-----/.test(text) || /-----BEGIN PGP SIGNATURE-----/.test(text);
|
|
157
|
+
}
|