@sweberdev/witness 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +21 -2
- package/dist/ai-sdk.cjs +921 -0
- package/dist/ai-sdk.cjs.map +1 -0
- package/dist/ai-sdk.d.cts +95 -0
- package/dist/ai-sdk.d.ts +95 -0
- package/dist/ai-sdk.js +150 -0
- package/dist/ai-sdk.js.map +1 -0
- package/dist/bin.cjs +944 -0
- package/dist/bin.cjs.map +1 -0
- package/dist/bin.d.cts +1 -0
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +12 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-APXU3MTX.js +958 -0
- package/dist/chunk-APXU3MTX.js.map +1 -0
- package/dist/chunk-L65R2SM5.js +228 -0
- package/dist/chunk-L65R2SM5.js.map +1 -0
- package/dist/chunk-O6TADCND.js +317 -0
- package/dist/chunk-O6TADCND.js.map +1 -0
- package/dist/chunk-UBRK4CTM.js +156 -0
- package/dist/chunk-UBRK4CTM.js.map +1 -0
- package/dist/chunk-XJOQ5IUT.js +110 -0
- package/dist/chunk-XJOQ5IUT.js.map +1 -0
- package/dist/cli.cjs +964 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +7 -0
- package/dist/cli.d.ts +7 -0
- package/dist/cli.js +9 -0
- package/dist/cli.js.map +1 -0
- package/dist/disclosure-5a3UIVQZ.d.cts +70 -0
- package/dist/disclosure-C7Y_uY73.d.ts +70 -0
- package/dist/elements-auto.cjs +649 -0
- package/dist/elements-auto.cjs.map +1 -0
- package/dist/elements-auto.d.cts +3 -0
- package/dist/elements-auto.d.ts +3 -0
- package/dist/elements-auto.js +16 -0
- package/dist/elements-auto.js.map +1 -0
- package/dist/elements.cjs +646 -0
- package/dist/elements.cjs.map +1 -0
- package/dist/elements.d.cts +47 -0
- package/dist/elements.d.ts +47 -0
- package/dist/elements.js +13 -0
- package/dist/elements.js.map +1 -0
- package/dist/image-B7YvdITx.d.ts +49 -0
- package/dist/image-BNrEO-hj.d.cts +49 -0
- package/dist/index.cjs +1344 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +124 -0
- package/dist/index.d.ts +124 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/types-WWHzrNlc.d.cts +90 -0
- package/dist/types-WWHzrNlc.d.ts +90 -0
- package/package.json +123 -3
- package/src/ai-sdk.ts +252 -0
- package/src/bin.ts +6 -0
- package/src/cli.ts +160 -0
- package/src/disclosure.ts +172 -0
- package/src/elements-auto.ts +5 -0
- package/src/elements.ts +363 -0
- package/src/i18n.ts +259 -0
- package/src/image.ts +552 -0
- package/src/index.ts +66 -0
- package/src/inflate.ts +189 -0
- package/src/marking.ts +237 -0
- package/src/types.ts +115 -0
- package/src/watermark.ts +165 -0
- package/styles.css +103 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DISCLOSURE_KINDS,
|
|
3
|
+
createDisclosure
|
|
4
|
+
} from "./chunk-XJOQ5IUT.js";
|
|
5
|
+
import {
|
|
6
|
+
format,
|
|
7
|
+
getMessages
|
|
8
|
+
} from "./chunk-L65R2SM5.js";
|
|
9
|
+
|
|
10
|
+
// src/elements.ts
|
|
11
|
+
var BaseElement = typeof HTMLElement === "undefined" ? class {
|
|
12
|
+
} : HTMLElement;
|
|
13
|
+
var SHARED_CSS = `
|
|
14
|
+
:host { --witness-accent: #003399; --witness-accent-text: #ffffff; --witness-bg: #ffffff;
|
|
15
|
+
--witness-fg: #1a1a1a; --witness-muted: #555b66; --witness-border: #d7dbe3; --witness-radius: 8px;
|
|
16
|
+
font: inherit; color: var(--witness-fg); }
|
|
17
|
+
@media (prefers-color-scheme: dark) {
|
|
18
|
+
:host { --witness-accent: #8fb0ff; --witness-accent-text: #0b1530; --witness-bg: #15181f;
|
|
19
|
+
--witness-fg: #eef1f6; --witness-muted: #a9b0bd; --witness-border: #2c323d; }
|
|
20
|
+
}
|
|
21
|
+
.icon { display: inline-flex; align-items: center; justify-content: center; min-width: 1.75em;
|
|
22
|
+
height: 1.5em; padding: 0 .3em; border-radius: 4px; background: var(--witness-accent);
|
|
23
|
+
color: var(--witness-accent-text); font-weight: 700; font-size: .75em; letter-spacing: .02em;
|
|
24
|
+
line-height: 1; flex: none; }
|
|
25
|
+
button { font: inherit; cursor: pointer; }
|
|
26
|
+
button:focus-visible, a:focus-visible { outline: 2px solid var(--witness-accent); outline-offset: 2px; }
|
|
27
|
+
a { color: inherit; }
|
|
28
|
+
.muted { color: var(--witness-muted); }
|
|
29
|
+
`;
|
|
30
|
+
var NOTICE_CSS = `${SHARED_CSS}
|
|
31
|
+
:host { display: block; }
|
|
32
|
+
.notice { display: flex; gap: .75em; align-items: flex-start; padding: .875em 1em;
|
|
33
|
+
border: 1px solid var(--witness-border); border-left: 4px solid var(--witness-accent);
|
|
34
|
+
border-radius: var(--witness-radius); background: var(--witness-bg); }
|
|
35
|
+
.notice h2 { font-size: 1em; margin: 0 0 .25em; }
|
|
36
|
+
.notice p { margin: 0 0 .75em; line-height: 1.45; }
|
|
37
|
+
.actions { display: flex; gap: .75em; align-items: center; flex-wrap: wrap; }
|
|
38
|
+
.ack { padding: .4em .9em; border-radius: 6px; border: 0; background: var(--witness-accent);
|
|
39
|
+
color: var(--witness-accent-text); font-weight: 600; }
|
|
40
|
+
.compact { display: inline-flex; gap: .5em; align-items: center; padding: .25em .5em;
|
|
41
|
+
border: 1px solid var(--witness-border); border-radius: 999px; background: var(--witness-bg);
|
|
42
|
+
color: var(--witness-fg); font-size: .875em; }
|
|
43
|
+
`;
|
|
44
|
+
var LABEL_CSS = `${SHARED_CSS}
|
|
45
|
+
:host { display: inline-block; position: relative; font-size: .875em; }
|
|
46
|
+
:host([variant="overlay"]) { position: absolute; inset-block-start: .5em; inset-inline-end: .5em; z-index: 1; }
|
|
47
|
+
.badge { display: inline-flex; gap: .4em; align-items: center; padding: .2em .55em .2em .25em;
|
|
48
|
+
border: 1px solid var(--witness-border); border-radius: 999px; background: var(--witness-bg);
|
|
49
|
+
color: var(--witness-fg); }
|
|
50
|
+
.panel { position: absolute; inset-block-start: calc(100% + .4em); inset-inline-end: 0; z-index: 10;
|
|
51
|
+
width: max-content; max-width: min(22em, 80vw); padding: .75em .875em; text-align: start;
|
|
52
|
+
border: 1px solid var(--witness-border); border-radius: var(--witness-radius);
|
|
53
|
+
background: var(--witness-bg); box-shadow: 0 6px 24px rgb(0 0 0 / .15); line-height: 1.45; }
|
|
54
|
+
:host([variant="inline"]) .panel, :host(:not([variant])) .panel { inset-inline-end: auto; inset-inline-start: 0; }
|
|
55
|
+
.panel strong { display: block; margin-bottom: .25em; }
|
|
56
|
+
.panel p { margin: 0 0 .4em; }
|
|
57
|
+
.panel ul { margin: .4em 0; padding-inline-start: 1.1em; }
|
|
58
|
+
.close { position: absolute; inset-block-start: .35em; inset-inline-end: .35em; border: 0;
|
|
59
|
+
background: none; color: var(--witness-muted); font-size: 1.1em; line-height: 1; padding: .2em .35em; }
|
|
60
|
+
[hidden] { display: none !important; }
|
|
61
|
+
`;
|
|
62
|
+
function locale(el) {
|
|
63
|
+
const own = el.getAttribute("locale") ?? el.closest("[lang]")?.getAttribute("lang");
|
|
64
|
+
if (own) return [own];
|
|
65
|
+
return typeof navigator !== "undefined" ? [...navigator.languages] : [];
|
|
66
|
+
}
|
|
67
|
+
function kindOf(el, fallback) {
|
|
68
|
+
const value = el.getAttribute("kind");
|
|
69
|
+
return value && DISCLOSURE_KINDS.includes(value) ? value : fallback;
|
|
70
|
+
}
|
|
71
|
+
function h(doc, tag, attrs = {}, ...children) {
|
|
72
|
+
const el = doc.createElement(tag);
|
|
73
|
+
for (const [name, value] of Object.entries(attrs)) el.setAttribute(name, value);
|
|
74
|
+
for (const child of children) {
|
|
75
|
+
if (child !== null) el.append(child);
|
|
76
|
+
}
|
|
77
|
+
return el;
|
|
78
|
+
}
|
|
79
|
+
function icon(doc) {
|
|
80
|
+
return h(doc, "span", { class: "icon", "aria-hidden": "true", part: "icon" }, "AI");
|
|
81
|
+
}
|
|
82
|
+
var WitnessNoticeElement = class extends BaseElement {
|
|
83
|
+
static {
|
|
84
|
+
this.observedAttributes = ["kind", "locale", "disclosure-id", "version", "href", "storage"];
|
|
85
|
+
}
|
|
86
|
+
#disclosure = null;
|
|
87
|
+
#unsubscribe = null;
|
|
88
|
+
#expanded = false;
|
|
89
|
+
/** `id@version` of the disclosure a `shown` event was sent for, so attribute changes don't repeat it. */
|
|
90
|
+
#shownFor = null;
|
|
91
|
+
connectedCallback() {
|
|
92
|
+
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
93
|
+
this.#setup();
|
|
94
|
+
}
|
|
95
|
+
disconnectedCallback() {
|
|
96
|
+
this.#unsubscribe?.();
|
|
97
|
+
this.#unsubscribe = null;
|
|
98
|
+
}
|
|
99
|
+
attributeChangedCallback(_name, oldValue, newValue) {
|
|
100
|
+
if (oldValue === newValue) return;
|
|
101
|
+
if (this.isConnected && this.shadowRoot) this.#setup();
|
|
102
|
+
}
|
|
103
|
+
/** Forgets the acknowledgement and shows the full notice again. */
|
|
104
|
+
reset() {
|
|
105
|
+
this.#disclosure?.reset();
|
|
106
|
+
}
|
|
107
|
+
get disclosure() {
|
|
108
|
+
return this.#disclosure;
|
|
109
|
+
}
|
|
110
|
+
#setup() {
|
|
111
|
+
this.#unsubscribe?.();
|
|
112
|
+
const storage = this.getAttribute("storage");
|
|
113
|
+
const kind = kindOf(this, "chatbot");
|
|
114
|
+
const tags = locale(this);
|
|
115
|
+
this.#disclosure = createDisclosure({
|
|
116
|
+
kind,
|
|
117
|
+
id: this.getAttribute("disclosure-id") ?? kind,
|
|
118
|
+
version: this.getAttribute("version") ?? "1",
|
|
119
|
+
locale: getLocaleCode(tags),
|
|
120
|
+
...storage === "session" || storage === "memory" || storage === "local" ? { storage } : {},
|
|
121
|
+
onEvent: (event) => this.#dispatch(event)
|
|
122
|
+
});
|
|
123
|
+
this.#expanded = false;
|
|
124
|
+
this.#unsubscribe = this.#disclosure.subscribe(() => {
|
|
125
|
+
this.#expanded = false;
|
|
126
|
+
this.#render();
|
|
127
|
+
});
|
|
128
|
+
this.#render();
|
|
129
|
+
}
|
|
130
|
+
#dispatch(event) {
|
|
131
|
+
this.dispatchEvent(
|
|
132
|
+
new CustomEvent(`witness-${event.type}`, { detail: event, bubbles: true, composed: true })
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
#render() {
|
|
136
|
+
const root = this.shadowRoot;
|
|
137
|
+
const disclosure = this.#disclosure;
|
|
138
|
+
if (!root || !disclosure) return;
|
|
139
|
+
const doc = this.ownerDocument;
|
|
140
|
+
const messages = getMessages(locale(this));
|
|
141
|
+
const text = messages.kinds[disclosure.kind];
|
|
142
|
+
const href = this.getAttribute("href");
|
|
143
|
+
const full = this.#expanded || disclosure.needsAcknowledgement();
|
|
144
|
+
const style = h(doc, "style", {}, NOTICE_CSS);
|
|
145
|
+
if (full) {
|
|
146
|
+
const ack = h(
|
|
147
|
+
doc,
|
|
148
|
+
"button",
|
|
149
|
+
{ type: "button", class: "ack", part: "button" },
|
|
150
|
+
messages.ui.acknowledge
|
|
151
|
+
);
|
|
152
|
+
ack.addEventListener("click", () => disclosure.acknowledge());
|
|
153
|
+
const link = href ? h(doc, "a", { href, part: "link" }, messages.ui.moreInfo) : null;
|
|
154
|
+
const box = h(
|
|
155
|
+
doc,
|
|
156
|
+
"section",
|
|
157
|
+
{ class: "notice", role: "note", "aria-labelledby": "title", part: "notice" },
|
|
158
|
+
icon(doc),
|
|
159
|
+
h(
|
|
160
|
+
doc,
|
|
161
|
+
"div",
|
|
162
|
+
{},
|
|
163
|
+
h(doc, "h2", { id: "title", part: "title" }, text.title),
|
|
164
|
+
h(doc, "p", { part: "body" }, h(doc, "slot", {}, text.body)),
|
|
165
|
+
h(doc, "div", { class: "actions" }, ack, link)
|
|
166
|
+
)
|
|
167
|
+
);
|
|
168
|
+
root.replaceChildren(style, box);
|
|
169
|
+
const shownFor = `${disclosure.id}@${disclosure.version}`;
|
|
170
|
+
if (this.#shownFor !== shownFor) {
|
|
171
|
+
this.#shownFor = shownFor;
|
|
172
|
+
disclosure.markShown();
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
const reopen = h(
|
|
176
|
+
doc,
|
|
177
|
+
"button",
|
|
178
|
+
{
|
|
179
|
+
type: "button",
|
|
180
|
+
class: "compact",
|
|
181
|
+
"aria-label": `${text.label}: ${messages.ui.details}`,
|
|
182
|
+
part: "compact"
|
|
183
|
+
},
|
|
184
|
+
icon(doc),
|
|
185
|
+
text.label
|
|
186
|
+
);
|
|
187
|
+
reopen.addEventListener("click", () => {
|
|
188
|
+
this.#expanded = true;
|
|
189
|
+
this.#render();
|
|
190
|
+
});
|
|
191
|
+
root.replaceChildren(style, reopen);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
var WitnessLabelElement = class extends BaseElement {
|
|
196
|
+
static {
|
|
197
|
+
this.observedAttributes = [
|
|
198
|
+
"kind",
|
|
199
|
+
"locale",
|
|
200
|
+
"generator",
|
|
201
|
+
"generator-version",
|
|
202
|
+
"created",
|
|
203
|
+
"reviewed",
|
|
204
|
+
"href",
|
|
205
|
+
"variant"
|
|
206
|
+
];
|
|
207
|
+
}
|
|
208
|
+
#open = false;
|
|
209
|
+
#onKey = (event) => {
|
|
210
|
+
if (event.key === "Escape" && this.#open) {
|
|
211
|
+
this.#toggle(false);
|
|
212
|
+
this.shadowRoot?.querySelector(".badge")?.focus();
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
#onOutside = (event) => {
|
|
216
|
+
if (this.#open && !event.composedPath().includes(this)) this.#toggle(false);
|
|
217
|
+
};
|
|
218
|
+
connectedCallback() {
|
|
219
|
+
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
220
|
+
this.ownerDocument.addEventListener("keydown", this.#onKey);
|
|
221
|
+
this.ownerDocument.addEventListener("click", this.#onOutside);
|
|
222
|
+
this.#render();
|
|
223
|
+
}
|
|
224
|
+
disconnectedCallback() {
|
|
225
|
+
this.ownerDocument.removeEventListener("keydown", this.#onKey);
|
|
226
|
+
this.ownerDocument.removeEventListener("click", this.#onOutside);
|
|
227
|
+
}
|
|
228
|
+
attributeChangedCallback() {
|
|
229
|
+
if (this.isConnected && this.shadowRoot) this.#render();
|
|
230
|
+
}
|
|
231
|
+
#toggle(open) {
|
|
232
|
+
this.#open = open;
|
|
233
|
+
this.#render();
|
|
234
|
+
}
|
|
235
|
+
#render() {
|
|
236
|
+
const root = this.shadowRoot;
|
|
237
|
+
if (!root) return;
|
|
238
|
+
const doc = this.ownerDocument;
|
|
239
|
+
const tags = locale(this);
|
|
240
|
+
const messages = getMessages(tags);
|
|
241
|
+
const kind = kindOf(this, "generated");
|
|
242
|
+
const text = messages.kinds[kind];
|
|
243
|
+
const generator = [this.getAttribute("generator"), this.getAttribute("generator-version")].filter(Boolean).join(" ");
|
|
244
|
+
const created = this.getAttribute("created");
|
|
245
|
+
const href = this.getAttribute("href");
|
|
246
|
+
const badge = h(
|
|
247
|
+
doc,
|
|
248
|
+
"button",
|
|
249
|
+
{
|
|
250
|
+
type: "button",
|
|
251
|
+
class: "badge",
|
|
252
|
+
part: "badge",
|
|
253
|
+
"aria-expanded": String(this.#open),
|
|
254
|
+
"aria-controls": "panel",
|
|
255
|
+
title: messages.ui.details
|
|
256
|
+
},
|
|
257
|
+
icon(doc),
|
|
258
|
+
h(doc, "span", { part: "label" }, text.label)
|
|
259
|
+
);
|
|
260
|
+
badge.addEventListener("click", () => this.#toggle(!this.#open));
|
|
261
|
+
const facts = h(doc, "ul", {});
|
|
262
|
+
if (generator) facts.append(h(doc, "li", {}, format(messages.ui.generatedWith, { generator })));
|
|
263
|
+
if (created) {
|
|
264
|
+
facts.append(
|
|
265
|
+
h(doc, "li", {}, format(messages.ui.createdOn, { date: formatDate(created, tags) }))
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (this.hasAttribute("reviewed")) facts.append(h(doc, "li", {}, messages.ui.humanReviewed));
|
|
269
|
+
const close = h(
|
|
270
|
+
doc,
|
|
271
|
+
"button",
|
|
272
|
+
{ type: "button", class: "close", "aria-label": messages.ui.close },
|
|
273
|
+
"\xD7"
|
|
274
|
+
);
|
|
275
|
+
close.addEventListener("click", () => this.#toggle(false));
|
|
276
|
+
const panel = h(
|
|
277
|
+
doc,
|
|
278
|
+
"div",
|
|
279
|
+
{ id: "panel", class: "panel", role: "dialog", "aria-label": text.title, part: "panel" },
|
|
280
|
+
close,
|
|
281
|
+
h(doc, "strong", {}, text.title),
|
|
282
|
+
h(doc, "p", {}, text.body),
|
|
283
|
+
facts.childElementCount > 0 ? facts : null,
|
|
284
|
+
href ? h(doc, "a", { href, part: "link" }, messages.ui.moreInfo) : null
|
|
285
|
+
);
|
|
286
|
+
if (!this.#open) panel.hidden = true;
|
|
287
|
+
root.replaceChildren(h(doc, "style", {}, LABEL_CSS), badge, panel);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
function formatDate(value, tags) {
|
|
291
|
+
const date = new Date(value);
|
|
292
|
+
if (Number.isNaN(date.getTime())) return value;
|
|
293
|
+
try {
|
|
294
|
+
return new Intl.DateTimeFormat(tags.length > 0 ? tags : void 0, {
|
|
295
|
+
dateStyle: "medium"
|
|
296
|
+
}).format(date);
|
|
297
|
+
} catch {
|
|
298
|
+
return value;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function getLocaleCode(tags) {
|
|
302
|
+
return tags[0] ?? "en";
|
|
303
|
+
}
|
|
304
|
+
function defineWitnessElements() {
|
|
305
|
+
if (typeof customElements === "undefined") return;
|
|
306
|
+
if (!customElements.get("witness-notice"))
|
|
307
|
+
customElements.define("witness-notice", WitnessNoticeElement);
|
|
308
|
+
if (!customElements.get("witness-label"))
|
|
309
|
+
customElements.define("witness-label", WitnessLabelElement);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export {
|
|
313
|
+
WitnessNoticeElement,
|
|
314
|
+
WitnessLabelElement,
|
|
315
|
+
defineWitnessElements
|
|
316
|
+
};
|
|
317
|
+
//# sourceMappingURL=chunk-O6TADCND.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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 /** `id@version` of the disclosure a `shown` event was sent for, so attribute changes don't repeat it. */\n #shownFor: string | null = null;\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(_name: string, oldValue: string | null, newValue: string | null): void {\n if (oldValue === newValue) return;\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 const shownFor = `${disclosure.id}@${disclosure.version}`;\n if (this.#shownFor !== shownFor) {\n this.#shownFor = shownFor;\n disclosure.markShown();\n }\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":[]}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectImageFormat,
|
|
3
|
+
iptcSourceType,
|
|
4
|
+
markImage,
|
|
5
|
+
readImageMarking,
|
|
6
|
+
readTextWatermark
|
|
7
|
+
} from "./chunk-APXU3MTX.js";
|
|
8
|
+
|
|
9
|
+
// src/cli.ts
|
|
10
|
+
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
11
|
+
import { basename, extname, join } from "path";
|
|
12
|
+
import { parseArgs } from "util";
|
|
13
|
+
var HELP = `witness: AI Act Article 50 marking for images and text
|
|
14
|
+
|
|
15
|
+
Usage
|
|
16
|
+
witness mark <files...> (--out <dir> | --in-place) [options]
|
|
17
|
+
witness inspect <files...> [--json] [--require]
|
|
18
|
+
|
|
19
|
+
mark options
|
|
20
|
+
--generator <name> AI system, e.g. "gpt-image-2"
|
|
21
|
+
--generator-version <v> its version
|
|
22
|
+
--kind <kind> generated (default), edited or deepfake
|
|
23
|
+
--provider <name> your company or product
|
|
24
|
+
--url <url> page that explains how you use AI
|
|
25
|
+
--description <text> free text written to dc:description
|
|
26
|
+
--overwrite-c2pa also rewrite files that carry a C2PA manifest
|
|
27
|
+
|
|
28
|
+
inspect options
|
|
29
|
+
--json one JSON object per file
|
|
30
|
+
--require exit 1 if a file carries no AI marking
|
|
31
|
+
|
|
32
|
+
Images: PNG, JPEG, WebP (XMP with IPTC Digital Source Type).
|
|
33
|
+
Text files are checked for the Witness text watermark.`;
|
|
34
|
+
var KINDS = ["generated", "edited", "deepfake"];
|
|
35
|
+
async function runCli(argv, io = defaultIo) {
|
|
36
|
+
const [command, ...rest] = argv;
|
|
37
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
38
|
+
io.out(HELP);
|
|
39
|
+
return command ? 0 : 1;
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
if (command === "mark") return await mark(rest, io);
|
|
43
|
+
if (command === "inspect") return await inspect(rest, io);
|
|
44
|
+
io.err(`Unknown command "${command}". Run "witness --help".`);
|
|
45
|
+
return 1;
|
|
46
|
+
} catch (error) {
|
|
47
|
+
io.err(error instanceof Error ? error.message : String(error));
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
async function mark(argv, io) {
|
|
52
|
+
const { values, positionals } = parseArgs({
|
|
53
|
+
args: argv,
|
|
54
|
+
allowPositionals: true,
|
|
55
|
+
options: {
|
|
56
|
+
out: { type: "string" },
|
|
57
|
+
"in-place": { type: "boolean" },
|
|
58
|
+
generator: { type: "string" },
|
|
59
|
+
"generator-version": { type: "string" },
|
|
60
|
+
kind: { type: "string" },
|
|
61
|
+
provider: { type: "string" },
|
|
62
|
+
url: { type: "string" },
|
|
63
|
+
description: { type: "string" },
|
|
64
|
+
"overwrite-c2pa": { type: "boolean" }
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
if (positionals.length === 0) throw new Error("witness mark: no files given.");
|
|
68
|
+
if (!values.out && !values["in-place"]) {
|
|
69
|
+
throw new Error("witness mark: pass --out <dir> or --in-place.");
|
|
70
|
+
}
|
|
71
|
+
const kind = values.kind ?? "generated";
|
|
72
|
+
if (!KINDS.includes(kind))
|
|
73
|
+
throw new Error(`witness mark: --kind must be one of ${KINDS.join(", ")}.`);
|
|
74
|
+
const input = { kind };
|
|
75
|
+
if (values.generator) input.generator = values.generator;
|
|
76
|
+
if (values["generator-version"]) input.generatorVersion = values["generator-version"];
|
|
77
|
+
if (values.provider) input.provider = values.provider;
|
|
78
|
+
if (values.url) input.disclosureUrl = values.url;
|
|
79
|
+
if (values.description) input.description = values.description;
|
|
80
|
+
if (values.out) await mkdir(values.out, { recursive: true });
|
|
81
|
+
let failed = 0;
|
|
82
|
+
for (const file of positionals) {
|
|
83
|
+
const bytes = new Uint8Array(await readFile(file));
|
|
84
|
+
const result = markImage(bytes, input, {
|
|
85
|
+
c2pa: values["overwrite-c2pa"] ? "overwrite" : "skip"
|
|
86
|
+
});
|
|
87
|
+
if (result.status === "unsupported") {
|
|
88
|
+
io.err(`${file}: not a PNG, JPEG or WebP file, skipped`);
|
|
89
|
+
failed++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (result.status === "skipped-c2pa") {
|
|
93
|
+
io.err(`${file}: carries a C2PA manifest, left unchanged (use --overwrite-c2pa to force)`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const target = values.out ? join(values.out, basename(file)) : file;
|
|
97
|
+
await writeFile(target, result.bytes);
|
|
98
|
+
io.out(`${file}: marked (${result.format}) -> ${target}`);
|
|
99
|
+
}
|
|
100
|
+
return failed > 0 ? 1 : 0;
|
|
101
|
+
}
|
|
102
|
+
async function inspect(argv, io) {
|
|
103
|
+
const { values, positionals } = parseArgs({
|
|
104
|
+
args: argv,
|
|
105
|
+
allowPositionals: true,
|
|
106
|
+
options: { json: { type: "boolean" }, require: { type: "boolean" } }
|
|
107
|
+
});
|
|
108
|
+
if (positionals.length === 0) throw new Error("witness inspect: no files given.");
|
|
109
|
+
let unmarked = 0;
|
|
110
|
+
for (const file of positionals) {
|
|
111
|
+
const bytes = new Uint8Array(await readFile(file));
|
|
112
|
+
const format = detectImageFormat(bytes);
|
|
113
|
+
if (format) {
|
|
114
|
+
const info = readImageMarking(bytes);
|
|
115
|
+
const marked = info.aiGenerated || info.c2pa;
|
|
116
|
+
if (!marked) unmarked++;
|
|
117
|
+
if (values.json) {
|
|
118
|
+
const { xmp: _xmp, ...rest } = info;
|
|
119
|
+
io.out(JSON.stringify({ file, ...rest }));
|
|
120
|
+
} else {
|
|
121
|
+
const parts = [
|
|
122
|
+
`${file}: ${format}`,
|
|
123
|
+
info.sourceType ? `source type ${iptcSourceType(info.sourceType)}` : "no IPTC source type",
|
|
124
|
+
info.generator ? `generator ${info.generator}` : null,
|
|
125
|
+
info.c2pa ? "C2PA manifest present (not verified)" : null,
|
|
126
|
+
marked ? "AI-marked" : "NOT AI-marked"
|
|
127
|
+
];
|
|
128
|
+
io.out(parts.filter(Boolean).join(", "));
|
|
129
|
+
}
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const ext = extname(file).toLowerCase();
|
|
133
|
+
const text = new TextDecoder().decode(bytes);
|
|
134
|
+
const watermark = readTextWatermark(text);
|
|
135
|
+
if (!watermark) unmarked++;
|
|
136
|
+
if (values.json) {
|
|
137
|
+
io.out(JSON.stringify({ file, format: ext.slice(1) || "text", watermark }));
|
|
138
|
+
} else {
|
|
139
|
+
io.out(
|
|
140
|
+
watermark ? `${file}: text watermark found${watermark.generator ? `, generator ${watermark.generator}` : ""}${watermark.createdAt ? `, created ${watermark.createdAt}` : ""}` : `${file}: no Witness text watermark`
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return values.require && unmarked > 0 ? 1 : 0;
|
|
145
|
+
}
|
|
146
|
+
var defaultIo = {
|
|
147
|
+
out: (line) => process.stdout.write(`${line}
|
|
148
|
+
`),
|
|
149
|
+
err: (line) => process.stderr.write(`${line}
|
|
150
|
+
`)
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
runCli
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=chunk-UBRK4CTM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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":[]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/disclosure.ts
|
|
2
|
+
var PREFIX = "witness:ack:";
|
|
3
|
+
function createDisclosure(options = {}) {
|
|
4
|
+
const kind = options.kind ?? "chatbot";
|
|
5
|
+
const id = options.id ?? kind;
|
|
6
|
+
const version = options.version ?? "1";
|
|
7
|
+
const locale = options.locale ?? "en";
|
|
8
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
9
|
+
const storage = resolveStorage(options.storage);
|
|
10
|
+
const key = `${PREFIX}${id}`;
|
|
11
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12
|
+
let shown = false;
|
|
13
|
+
const read = () => {
|
|
14
|
+
try {
|
|
15
|
+
const raw = storage.getItem(key);
|
|
16
|
+
if (!raw) return null;
|
|
17
|
+
const parsed = JSON.parse(raw);
|
|
18
|
+
if (typeof parsed.version !== "string" || typeof parsed.at !== "string") return null;
|
|
19
|
+
return { version: parsed.version, at: parsed.at };
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const emit = (type, at) => {
|
|
25
|
+
options.onEvent?.({ type, id, kind, version, locale, at });
|
|
26
|
+
};
|
|
27
|
+
const notify = () => {
|
|
28
|
+
for (const listener of listeners) listener();
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
id,
|
|
32
|
+
kind,
|
|
33
|
+
version,
|
|
34
|
+
needsAcknowledgement: () => read()?.version !== version,
|
|
35
|
+
acknowledgedAt: () => {
|
|
36
|
+
const entry = read();
|
|
37
|
+
return entry?.version === version ? entry.at : null;
|
|
38
|
+
},
|
|
39
|
+
markShown() {
|
|
40
|
+
if (shown) return;
|
|
41
|
+
shown = true;
|
|
42
|
+
emit("shown", now().toISOString());
|
|
43
|
+
},
|
|
44
|
+
acknowledge() {
|
|
45
|
+
const at = now().toISOString();
|
|
46
|
+
try {
|
|
47
|
+
storage.setItem(key, JSON.stringify({ version, at }));
|
|
48
|
+
} catch {
|
|
49
|
+
}
|
|
50
|
+
emit("acknowledged", at);
|
|
51
|
+
notify();
|
|
52
|
+
},
|
|
53
|
+
reset() {
|
|
54
|
+
try {
|
|
55
|
+
storage.removeItem(key);
|
|
56
|
+
} catch {
|
|
57
|
+
}
|
|
58
|
+
notify();
|
|
59
|
+
},
|
|
60
|
+
subscribe(listener) {
|
|
61
|
+
listeners.add(listener);
|
|
62
|
+
return () => listeners.delete(listener);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function resolveStorage(option) {
|
|
67
|
+
if (option && typeof option === "object") return option;
|
|
68
|
+
if (option !== "memory") {
|
|
69
|
+
try {
|
|
70
|
+
const candidate = option === "session" ? globalThis.sessionStorage : globalThis.localStorage;
|
|
71
|
+
if (candidate) {
|
|
72
|
+
const probe = `${PREFIX}probe`;
|
|
73
|
+
candidate.setItem(probe, "1");
|
|
74
|
+
candidate.removeItem(probe);
|
|
75
|
+
return candidate;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return memoryStorage();
|
|
81
|
+
}
|
|
82
|
+
function memoryStorage() {
|
|
83
|
+
const map = /* @__PURE__ */ new Map();
|
|
84
|
+
return {
|
|
85
|
+
getItem: (key) => map.get(key) ?? null,
|
|
86
|
+
setItem: (key, value) => {
|
|
87
|
+
map.set(key, value);
|
|
88
|
+
},
|
|
89
|
+
removeItem: (key) => {
|
|
90
|
+
map.delete(key);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// src/types.ts
|
|
96
|
+
var DISCLOSURE_KINDS = [
|
|
97
|
+
"chatbot",
|
|
98
|
+
"generated",
|
|
99
|
+
"edited",
|
|
100
|
+
"deepfake",
|
|
101
|
+
"emotion-recognition",
|
|
102
|
+
"biometric-categorisation"
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
export {
|
|
106
|
+
createDisclosure,
|
|
107
|
+
memoryStorage,
|
|
108
|
+
DISCLOSURE_KINDS
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=chunk-XJOQ5IUT.js.map
|