@sweberdev/witness 0.1.0 → 0.2.0

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.
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/elements.ts"],"sourcesContent":["/**\n * Framework-free custom elements:\n *\n * - `<witness-notice>`: the chatbot notice (Art. 50(1)). Expanded until acknowledged, then a\n * compact label that can be reopened.\n * - `<witness-label>`: a badge for AI-generated or AI-edited content with a details popover.\n *\n * Import `@sweberdev/witness/elements` to register both, or call `defineWitnessElements()`.\n */\n\nimport { createDisclosure, type Disclosure, type DisclosureEvent } from \"./disclosure.js\";\nimport { format, getMessages } from \"./i18n.js\";\nimport { DISCLOSURE_KINDS, type DisclosureKind } from \"./types.js\";\n\nconst BaseElement: typeof HTMLElement =\n typeof HTMLElement === \"undefined\" ? (class {} as unknown as typeof HTMLElement) : HTMLElement;\n\nconst SHARED_CSS = `\n:host { --witness-accent: #003399; --witness-accent-text: #ffffff; --witness-bg: #ffffff;\n --witness-fg: #1a1a1a; --witness-muted: #555b66; --witness-border: #d7dbe3; --witness-radius: 8px;\n font: inherit; color: var(--witness-fg); }\n@media (prefers-color-scheme: dark) {\n :host { --witness-accent: #8fb0ff; --witness-accent-text: #0b1530; --witness-bg: #15181f;\n --witness-fg: #eef1f6; --witness-muted: #a9b0bd; --witness-border: #2c323d; }\n}\n.icon { display: inline-flex; align-items: center; justify-content: center; min-width: 1.75em;\n height: 1.5em; padding: 0 .3em; border-radius: 4px; background: var(--witness-accent);\n color: var(--witness-accent-text); font-weight: 700; font-size: .75em; letter-spacing: .02em;\n line-height: 1; flex: none; }\nbutton { font: inherit; cursor: pointer; }\nbutton:focus-visible, a:focus-visible { outline: 2px solid var(--witness-accent); outline-offset: 2px; }\na { color: inherit; }\n.muted { color: var(--witness-muted); }\n`;\n\nconst NOTICE_CSS = `${SHARED_CSS}\n:host { display: block; }\n.notice { display: flex; gap: .75em; align-items: flex-start; padding: .875em 1em;\n border: 1px solid var(--witness-border); border-left: 4px solid var(--witness-accent);\n border-radius: var(--witness-radius); background: var(--witness-bg); }\n.notice h2 { font-size: 1em; margin: 0 0 .25em; }\n.notice p { margin: 0 0 .75em; line-height: 1.45; }\n.actions { display: flex; gap: .75em; align-items: center; flex-wrap: wrap; }\n.ack { padding: .4em .9em; border-radius: 6px; border: 0; background: var(--witness-accent);\n color: var(--witness-accent-text); font-weight: 600; }\n.compact { display: inline-flex; gap: .5em; align-items: center; padding: .25em .5em;\n border: 1px solid var(--witness-border); border-radius: 999px; background: var(--witness-bg);\n color: var(--witness-fg); font-size: .875em; }\n`;\n\nconst LABEL_CSS = `${SHARED_CSS}\n:host { display: inline-block; position: relative; font-size: .875em; }\n:host([variant=\"overlay\"]) { position: absolute; inset-block-start: .5em; inset-inline-end: .5em; z-index: 1; }\n.badge { display: inline-flex; gap: .4em; align-items: center; padding: .2em .55em .2em .25em;\n border: 1px solid var(--witness-border); border-radius: 999px; background: var(--witness-bg);\n color: var(--witness-fg); }\n.panel { position: absolute; inset-block-start: calc(100% + .4em); inset-inline-end: 0; z-index: 10;\n width: max-content; max-width: min(22em, 80vw); padding: .75em .875em; text-align: start;\n border: 1px solid var(--witness-border); border-radius: var(--witness-radius);\n background: var(--witness-bg); box-shadow: 0 6px 24px rgb(0 0 0 / .15); line-height: 1.45; }\n:host([variant=\"inline\"]) .panel, :host(:not([variant])) .panel { inset-inline-end: auto; inset-inline-start: 0; }\n.panel strong { display: block; margin-bottom: .25em; }\n.panel p { margin: 0 0 .4em; }\n.panel ul { margin: .4em 0; padding-inline-start: 1.1em; }\n.close { position: absolute; inset-block-start: .35em; inset-inline-end: .35em; border: 0;\n background: none; color: var(--witness-muted); font-size: 1.1em; line-height: 1; padding: .2em .35em; }\n[hidden] { display: none !important; }\n`;\n\nfunction locale(el: HTMLElement): string[] {\n const own = el.getAttribute(\"locale\") ?? el.closest(\"[lang]\")?.getAttribute(\"lang\");\n if (own) return [own];\n return typeof navigator !== \"undefined\" ? [...navigator.languages] : [];\n}\n\nfunction kindOf(el: HTMLElement, fallback: DisclosureKind): DisclosureKind {\n const value = el.getAttribute(\"kind\") as DisclosureKind | null;\n return value && DISCLOSURE_KINDS.includes(value) ? value : fallback;\n}\n\nfunction h<K extends keyof HTMLElementTagNameMap>(\n doc: Document,\n tag: K,\n attrs: Record<string, string> = {},\n ...children: Array<Node | string | null>\n): HTMLElementTagNameMap[K] {\n const el = doc.createElement(tag);\n for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);\n for (const child of children) {\n if (child !== null) el.append(child);\n }\n return el;\n}\n\nfunction icon(doc: Document): HTMLElement {\n return h(doc, \"span\", { class: \"icon\", \"aria-hidden\": \"true\", part: \"icon\" }, \"AI\");\n}\n\n/** `<witness-notice kind=\"chatbot\" locale=\"de\" disclosure-id=\"support\" version=\"2\" href=\"/ki\">` */\nexport class WitnessNoticeElement extends BaseElement {\n static observedAttributes = [\"kind\", \"locale\", \"disclosure-id\", \"version\", \"href\", \"storage\"];\n\n #disclosure: Disclosure | null = null;\n #unsubscribe: (() => void) | null = null;\n #expanded = false;\n\n connectedCallback(): void {\n if (!this.shadowRoot) this.attachShadow({ mode: \"open\" });\n this.#setup();\n }\n\n disconnectedCallback(): void {\n this.#unsubscribe?.();\n this.#unsubscribe = null;\n }\n\n attributeChangedCallback(): void {\n if (this.isConnected && this.shadowRoot) this.#setup();\n }\n\n /** Forgets the acknowledgement and shows the full notice again. */\n reset(): void {\n this.#disclosure?.reset();\n }\n\n get disclosure(): Disclosure | null {\n return this.#disclosure;\n }\n\n #setup(): void {\n this.#unsubscribe?.();\n const storage = this.getAttribute(\"storage\");\n const kind = kindOf(this, \"chatbot\");\n const tags = locale(this);\n this.#disclosure = createDisclosure({\n kind,\n id: this.getAttribute(\"disclosure-id\") ?? kind,\n version: this.getAttribute(\"version\") ?? \"1\",\n locale: getLocaleCode(tags),\n ...(storage === \"session\" || storage === \"memory\" || storage === \"local\" ? { storage } : {}),\n onEvent: (event) => this.#dispatch(event),\n });\n this.#expanded = false;\n this.#unsubscribe = this.#disclosure.subscribe(() => {\n this.#expanded = false;\n this.#render();\n });\n this.#render();\n }\n\n #dispatch(event: DisclosureEvent): void {\n this.dispatchEvent(\n new CustomEvent(`witness-${event.type}`, { detail: event, bubbles: true, composed: true }),\n );\n }\n\n #render(): void {\n const root = this.shadowRoot;\n const disclosure = this.#disclosure;\n if (!root || !disclosure) return;\n const doc = this.ownerDocument;\n const messages = getMessages(locale(this));\n const text = messages.kinds[disclosure.kind];\n const href = this.getAttribute(\"href\");\n const full = this.#expanded || disclosure.needsAcknowledgement();\n\n const style = h(doc, \"style\", {}, NOTICE_CSS);\n if (full) {\n const ack = h(\n doc,\n \"button\",\n { type: \"button\", class: \"ack\", part: \"button\" },\n messages.ui.acknowledge,\n );\n ack.addEventListener(\"click\", () => disclosure.acknowledge());\n const link = href ? h(doc, \"a\", { href, part: \"link\" }, messages.ui.moreInfo) : null;\n const box = h(\n doc,\n \"section\",\n { class: \"notice\", role: \"note\", \"aria-labelledby\": \"title\", part: \"notice\" },\n icon(doc),\n h(\n doc,\n \"div\",\n {},\n h(doc, \"h2\", { id: \"title\", part: \"title\" }, text.title),\n h(doc, \"p\", { part: \"body\" }, h(doc, \"slot\", {}, text.body)),\n h(doc, \"div\", { class: \"actions\" }, ack, link),\n ),\n );\n root.replaceChildren(style, box);\n disclosure.markShown();\n } else {\n const reopen = h(\n doc,\n \"button\",\n {\n type: \"button\",\n class: \"compact\",\n \"aria-label\": `${text.label}: ${messages.ui.details}`,\n part: \"compact\",\n },\n icon(doc),\n text.label,\n );\n reopen.addEventListener(\"click\", () => {\n this.#expanded = true;\n this.#render();\n });\n root.replaceChildren(style, reopen);\n }\n }\n}\n\n/** `<witness-label kind=\"generated\" generator=\"Claude\" created=\"2026-10-05\" reviewed href=\"/ki\">` */\nexport class WitnessLabelElement extends BaseElement {\n static observedAttributes = [\n \"kind\",\n \"locale\",\n \"generator\",\n \"generator-version\",\n \"created\",\n \"reviewed\",\n \"href\",\n \"variant\",\n ];\n\n #open = false;\n #onKey = (event: KeyboardEvent) => {\n if (event.key === \"Escape\" && this.#open) {\n this.#toggle(false);\n this.shadowRoot?.querySelector<HTMLButtonElement>(\".badge\")?.focus();\n }\n };\n #onOutside = (event: Event) => {\n if (this.#open && !event.composedPath().includes(this)) this.#toggle(false);\n };\n\n connectedCallback(): void {\n if (!this.shadowRoot) this.attachShadow({ mode: \"open\" });\n this.ownerDocument.addEventListener(\"keydown\", this.#onKey);\n this.ownerDocument.addEventListener(\"click\", this.#onOutside);\n this.#render();\n }\n\n disconnectedCallback(): void {\n this.ownerDocument.removeEventListener(\"keydown\", this.#onKey);\n this.ownerDocument.removeEventListener(\"click\", this.#onOutside);\n }\n\n attributeChangedCallback(): void {\n if (this.isConnected && this.shadowRoot) this.#render();\n }\n\n #toggle(open: boolean): void {\n this.#open = open;\n this.#render();\n }\n\n #render(): void {\n const root = this.shadowRoot;\n if (!root) return;\n const doc = this.ownerDocument;\n const tags = locale(this);\n const messages = getMessages(tags);\n const kind = kindOf(this, \"generated\");\n const text = messages.kinds[kind];\n const generator = [this.getAttribute(\"generator\"), this.getAttribute(\"generator-version\")]\n .filter(Boolean)\n .join(\" \");\n const created = this.getAttribute(\"created\");\n const href = this.getAttribute(\"href\");\n\n const badge = h(\n doc,\n \"button\",\n {\n type: \"button\",\n class: \"badge\",\n part: \"badge\",\n \"aria-expanded\": String(this.#open),\n \"aria-controls\": \"panel\",\n title: messages.ui.details,\n },\n icon(doc),\n h(doc, \"span\", { part: \"label\" }, text.label),\n );\n badge.addEventListener(\"click\", () => this.#toggle(!this.#open));\n\n const facts = h(doc, \"ul\", {});\n if (generator) facts.append(h(doc, \"li\", {}, format(messages.ui.generatedWith, { generator })));\n if (created) {\n facts.append(\n h(doc, \"li\", {}, format(messages.ui.createdOn, { date: formatDate(created, tags) })),\n );\n }\n if (this.hasAttribute(\"reviewed\")) facts.append(h(doc, \"li\", {}, messages.ui.humanReviewed));\n\n const close = h(\n doc,\n \"button\",\n { type: \"button\", class: \"close\", \"aria-label\": messages.ui.close },\n \"×\",\n );\n close.addEventListener(\"click\", () => this.#toggle(false));\n\n const panel = h(\n doc,\n \"div\",\n { id: \"panel\", class: \"panel\", role: \"dialog\", \"aria-label\": text.title, part: \"panel\" },\n close,\n h(doc, \"strong\", {}, text.title),\n h(doc, \"p\", {}, text.body),\n facts.childElementCount > 0 ? facts : null,\n href ? h(doc, \"a\", { href, part: \"link\" }, messages.ui.moreInfo) : null,\n );\n if (!this.#open) panel.hidden = true;\n root.replaceChildren(h(doc, \"style\", {}, LABEL_CSS), badge, panel);\n }\n}\n\nfunction formatDate(value: string, tags: string[]): string {\n const date = new Date(value);\n if (Number.isNaN(date.getTime())) return value;\n try {\n return new Intl.DateTimeFormat(tags.length > 0 ? tags : undefined, {\n dateStyle: \"medium\",\n }).format(date);\n } catch {\n return value;\n }\n}\n\nfunction getLocaleCode(tags: string[]): string {\n return tags[0] ?? \"en\";\n}\n\n/** Registers `<witness-notice>` and `<witness-label>` once. Safe to call during SSR. */\nexport function defineWitnessElements(): void {\n if (typeof customElements === \"undefined\") return;\n if (!customElements.get(\"witness-notice\"))\n customElements.define(\"witness-notice\", WitnessNoticeElement);\n if (!customElements.get(\"witness-label\"))\n customElements.define(\"witness-label\", WitnessLabelElement);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n \"witness-notice\": WitnessNoticeElement;\n \"witness-label\": WitnessLabelElement;\n }\n interface HTMLElementEventMap {\n \"witness-shown\": CustomEvent<DisclosureEvent>;\n \"witness-acknowledged\": CustomEvent<DisclosureEvent>;\n }\n}\n"],"mappings":"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","names":[]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, extname, join } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { detectImageFormat, markImage, readImageMarking } from \"./image.js\";\nimport { iptcSourceType } from \"./marking.js\";\nimport type { ContentKind, MarkingInput } from \"./types.js\";\nimport { readTextWatermark } from \"./watermark.js\";\n\nexport interface CliIo {\n out: (line: string) => void;\n err: (line: string) => void;\n}\n\nconst HELP = `witness: AI Act Article 50 marking for images and text\n\nUsage\n witness mark <files...> (--out <dir> | --in-place) [options]\n witness inspect <files...> [--json] [--require]\n\nmark options\n --generator <name> AI system, e.g. \"gpt-image-2\"\n --generator-version <v> its version\n --kind <kind> generated (default), edited or deepfake\n --provider <name> your company or product\n --url <url> page that explains how you use AI\n --description <text> free text written to dc:description\n --overwrite-c2pa also rewrite files that carry a C2PA manifest\n\ninspect options\n --json one JSON object per file\n --require exit 1 if a file carries no AI marking\n\nImages: PNG, JPEG, WebP (XMP with IPTC Digital Source Type).\nText files are checked for the Witness text watermark.`;\n\nconst KINDS: ContentKind[] = [\"generated\", \"edited\", \"deepfake\"];\n\nexport async function runCli(argv: string[], io: CliIo = defaultIo): Promise<number> {\n const [command, ...rest] = argv;\n if (!command || command === \"--help\" || command === \"-h\" || command === \"help\") {\n io.out(HELP);\n return command ? 0 : 1;\n }\n try {\n if (command === \"mark\") return await mark(rest, io);\n if (command === \"inspect\") return await inspect(rest, io);\n io.err(`Unknown command \"${command}\". Run \"witness --help\".`);\n return 1;\n } catch (error) {\n io.err(error instanceof Error ? error.message : String(error));\n return 1;\n }\n}\n\nasync function mark(argv: string[], io: CliIo): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n out: { type: \"string\" },\n \"in-place\": { type: \"boolean\" },\n generator: { type: \"string\" },\n \"generator-version\": { type: \"string\" },\n kind: { type: \"string\" },\n provider: { type: \"string\" },\n url: { type: \"string\" },\n description: { type: \"string\" },\n \"overwrite-c2pa\": { type: \"boolean\" },\n },\n });\n if (positionals.length === 0) throw new Error(\"witness mark: no files given.\");\n if (!values.out && !values[\"in-place\"]) {\n throw new Error(\"witness mark: pass --out <dir> or --in-place.\");\n }\n const kind = (values.kind ?? \"generated\") as ContentKind;\n if (!KINDS.includes(kind))\n throw new Error(`witness mark: --kind must be one of ${KINDS.join(\", \")}.`);\n const input: MarkingInput = { kind };\n if (values.generator) input.generator = values.generator;\n if (values[\"generator-version\"]) input.generatorVersion = values[\"generator-version\"];\n if (values.provider) input.provider = values.provider;\n if (values.url) input.disclosureUrl = values.url;\n if (values.description) input.description = values.description;\n if (values.out) await mkdir(values.out, { recursive: true });\n\n let failed = 0;\n for (const file of positionals) {\n const bytes = new Uint8Array(await readFile(file));\n const result = markImage(bytes, input, {\n c2pa: values[\"overwrite-c2pa\"] ? \"overwrite\" : \"skip\",\n });\n if (result.status === \"unsupported\") {\n io.err(`${file}: not a PNG, JPEG or WebP file, skipped`);\n failed++;\n continue;\n }\n if (result.status === \"skipped-c2pa\") {\n io.err(`${file}: carries a C2PA manifest, left unchanged (use --overwrite-c2pa to force)`);\n continue;\n }\n const target = values.out ? join(values.out, basename(file)) : file;\n await writeFile(target, result.bytes);\n io.out(`${file}: marked (${result.format}) -> ${target}`);\n }\n return failed > 0 ? 1 : 0;\n}\n\nasync function inspect(argv: string[], io: CliIo): Promise<number> {\n const { values, positionals } = parseArgs({\n args: argv,\n allowPositionals: true,\n options: { json: { type: \"boolean\" }, require: { type: \"boolean\" } },\n });\n if (positionals.length === 0) throw new Error(\"witness inspect: no files given.\");\n let unmarked = 0;\n for (const file of positionals) {\n const bytes = new Uint8Array(await readFile(file));\n const format = detectImageFormat(bytes);\n if (format) {\n const info = readImageMarking(bytes);\n const marked = info.aiGenerated || info.c2pa;\n if (!marked) unmarked++;\n if (values.json) {\n const { xmp: _xmp, ...rest } = info;\n io.out(JSON.stringify({ file, ...rest }));\n } else {\n const parts = [\n `${file}: ${format}`,\n info.sourceType\n ? `source type ${iptcSourceType(info.sourceType)}`\n : \"no IPTC source type\",\n info.generator ? `generator ${info.generator}` : null,\n info.c2pa ? \"C2PA manifest present (not verified)\" : null,\n marked ? \"AI-marked\" : \"NOT AI-marked\",\n ];\n io.out(parts.filter(Boolean).join(\", \"));\n }\n continue;\n }\n const ext = extname(file).toLowerCase();\n const text = new TextDecoder().decode(bytes);\n const watermark = readTextWatermark(text);\n if (!watermark) unmarked++;\n if (values.json) {\n io.out(JSON.stringify({ file, format: ext.slice(1) || \"text\", watermark }));\n } else {\n io.out(\n watermark\n ? `${file}: text watermark found${watermark.generator ? `, generator ${watermark.generator}` : \"\"}${watermark.createdAt ? `, created ${watermark.createdAt}` : \"\"}`\n : `${file}: no Witness text watermark`,\n );\n }\n }\n return values.require && unmarked > 0 ? 1 : 0;\n}\n\nconst defaultIo: CliIo = {\n out: (line) => process.stdout.write(`${line}\\n`),\n err: (line) => process.stderr.write(`${line}\\n`),\n};\n"],"mappings":"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","names":[]}