@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.
Files changed (69) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +21 -2
  3. package/dist/ai-sdk.cjs +921 -0
  4. package/dist/ai-sdk.cjs.map +1 -0
  5. package/dist/ai-sdk.d.cts +95 -0
  6. package/dist/ai-sdk.d.ts +95 -0
  7. package/dist/ai-sdk.js +150 -0
  8. package/dist/ai-sdk.js.map +1 -0
  9. package/dist/bin.cjs +944 -0
  10. package/dist/bin.cjs.map +1 -0
  11. package/dist/bin.d.cts +1 -0
  12. package/dist/bin.d.ts +1 -0
  13. package/dist/bin.js +12 -0
  14. package/dist/bin.js.map +1 -0
  15. package/dist/chunk-APXU3MTX.js +958 -0
  16. package/dist/chunk-APXU3MTX.js.map +1 -0
  17. package/dist/chunk-L65R2SM5.js +228 -0
  18. package/dist/chunk-L65R2SM5.js.map +1 -0
  19. package/dist/chunk-O6TADCND.js +317 -0
  20. package/dist/chunk-O6TADCND.js.map +1 -0
  21. package/dist/chunk-UBRK4CTM.js +156 -0
  22. package/dist/chunk-UBRK4CTM.js.map +1 -0
  23. package/dist/chunk-XJOQ5IUT.js +110 -0
  24. package/dist/chunk-XJOQ5IUT.js.map +1 -0
  25. package/dist/cli.cjs +964 -0
  26. package/dist/cli.cjs.map +1 -0
  27. package/dist/cli.d.cts +7 -0
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.js +9 -0
  30. package/dist/cli.js.map +1 -0
  31. package/dist/disclosure-5a3UIVQZ.d.cts +70 -0
  32. package/dist/disclosure-C7Y_uY73.d.ts +70 -0
  33. package/dist/elements-auto.cjs +649 -0
  34. package/dist/elements-auto.cjs.map +1 -0
  35. package/dist/elements-auto.d.cts +3 -0
  36. package/dist/elements-auto.d.ts +3 -0
  37. package/dist/elements-auto.js +16 -0
  38. package/dist/elements-auto.js.map +1 -0
  39. package/dist/elements.cjs +646 -0
  40. package/dist/elements.cjs.map +1 -0
  41. package/dist/elements.d.cts +47 -0
  42. package/dist/elements.d.ts +47 -0
  43. package/dist/elements.js +13 -0
  44. package/dist/elements.js.map +1 -0
  45. package/dist/image-B7YvdITx.d.ts +49 -0
  46. package/dist/image-BNrEO-hj.d.cts +49 -0
  47. package/dist/index.cjs +1344 -0
  48. package/dist/index.cjs.map +1 -0
  49. package/dist/index.d.cts +124 -0
  50. package/dist/index.d.ts +124 -0
  51. package/dist/index.js +77 -0
  52. package/dist/index.js.map +1 -0
  53. package/dist/types-WWHzrNlc.d.cts +90 -0
  54. package/dist/types-WWHzrNlc.d.ts +90 -0
  55. package/package.json +123 -3
  56. package/src/ai-sdk.ts +252 -0
  57. package/src/bin.ts +6 -0
  58. package/src/cli.ts +160 -0
  59. package/src/disclosure.ts +172 -0
  60. package/src/elements-auto.ts +5 -0
  61. package/src/elements.ts +363 -0
  62. package/src/i18n.ts +259 -0
  63. package/src/image.ts +552 -0
  64. package/src/index.ts +66 -0
  65. package/src/inflate.ts +189 -0
  66. package/src/marking.ts +237 -0
  67. package/src/types.ts +115 -0
  68. package/src/watermark.ts +165 -0
  69. package/styles.css +103 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The transparency situations Article 50 of the EU AI Act describes.
3
+ *
4
+ * - `chatbot`: a person interacts directly with an AI system (Art. 50(1)).
5
+ * - `generated`: image, audio, video or text produced by an AI system (Art. 50(2) and 50(4)).
6
+ * - `edited`: existing content altered with an AI system (Art. 50(2) and 50(4)).
7
+ * - `deepfake`: generated or manipulated content that resembles real people, objects,
8
+ * places or events and could falsely appear authentic (Art. 50(4)).
9
+ * - `emotion-recognition` and `biometric-categorisation`: people are exposed to such a
10
+ * system (Art. 50(3)).
11
+ */
12
+ type DisclosureKind = "chatbot" | "generated" | "edited" | "deepfake" | "emotion-recognition" | "biometric-categorisation";
13
+ declare const DISCLOSURE_KINDS: readonly DisclosureKind[];
14
+ /** The kinds that describe a piece of content and can be written into a marking. */
15
+ type ContentKind = "generated" | "edited" | "deepfake";
16
+ /**
17
+ * IPTC Digital Source Type values relevant to AI. See
18
+ * https://cv.iptc.org/newscodes/digitalsourcetype/
19
+ */
20
+ type SourceType = "trainedAlgorithmicMedia" | "compositeWithTrainedAlgorithmicMedia" | "algorithmicallyEnhanced" | "algorithmicMedia" | "compositeSynthetic";
21
+ /** Wording for one disclosure kind in one language. */
22
+ interface DisclosureText {
23
+ /** Two or three words for a badge, e.g. "AI-generated". */
24
+ label: string;
25
+ /** One line for the heading of a notice. */
26
+ title: string;
27
+ /** One or two plain sentences that explain the label. */
28
+ body: string;
29
+ }
30
+ /** Interface strings shared by the notice and label components. */
31
+ interface UiText {
32
+ /** Button that collapses the chatbot notice. */
33
+ acknowledge: string;
34
+ /** Accessible name of the button that opens label details. */
35
+ details: string;
36
+ /** "Generated with {generator}". `{generator}` is replaced. */
37
+ generatedWith: string;
38
+ /** "Created on {date}". `{date}` is replaced. */
39
+ createdOn: string;
40
+ /** Shown when a person reviewed the content and took editorial responsibility. */
41
+ humanReviewed: string;
42
+ /** Link text to a page with more information. */
43
+ moreInfo: string;
44
+ /** Closes the details popover. */
45
+ close: string;
46
+ }
47
+ interface LocaleMessages {
48
+ kinds: Record<DisclosureKind, DisclosureText>;
49
+ ui: UiText;
50
+ }
51
+ /** A partial override of a locale, e.g. to switch German to the informal "du". */
52
+ interface LocaleOverride {
53
+ kinds?: Partial<Record<DisclosureKind, Partial<DisclosureText>>>;
54
+ ui?: Partial<UiText>;
55
+ }
56
+ /** What a marking says about a piece of content. Input for {@link createMarking}. */
57
+ interface MarkingInput {
58
+ /** Defaults to `generated`. */
59
+ kind?: ContentKind;
60
+ /** Defaults from `kind`: `trainedAlgorithmicMedia`, or the composite type for `edited`. */
61
+ sourceType?: SourceType;
62
+ /** Name of the AI system or model, e.g. "gpt-image-2" or "Claude". */
63
+ generator?: string;
64
+ /** Version of the AI system, if known. */
65
+ generatorVersion?: string;
66
+ /** Who provides or operates the AI system, e.g. your company. */
67
+ provider?: string;
68
+ /** ISO 8601 timestamp. Defaults to now. */
69
+ createdAt?: string | Date;
70
+ /** A page that explains how and why you use AI. */
71
+ disclosureUrl?: string;
72
+ /** A person reviewed the content and holds editorial responsibility (Art. 50(4), text). */
73
+ humanReviewed?: boolean;
74
+ /** Free text, e.g. "Image generated from the prompt by the newsroom". */
75
+ description?: string;
76
+ }
77
+ /** A normalised marking with all defaults applied. */
78
+ interface Marking {
79
+ kind: ContentKind;
80
+ sourceType: SourceType;
81
+ generator?: string;
82
+ generatorVersion?: string;
83
+ provider?: string;
84
+ createdAt: string;
85
+ disclosureUrl?: string;
86
+ humanReviewed: boolean;
87
+ description?: string;
88
+ }
89
+
90
+ export { type ContentKind as C, type DisclosureKind as D, type LocaleOverride as L, type Marking as M, type SourceType as S, type UiText as U, type MarkingInput as a, type DisclosureText as b, type LocaleMessages as c, DISCLOSURE_KINDS as d };
@@ -0,0 +1,90 @@
1
+ /**
2
+ * The transparency situations Article 50 of the EU AI Act describes.
3
+ *
4
+ * - `chatbot`: a person interacts directly with an AI system (Art. 50(1)).
5
+ * - `generated`: image, audio, video or text produced by an AI system (Art. 50(2) and 50(4)).
6
+ * - `edited`: existing content altered with an AI system (Art. 50(2) and 50(4)).
7
+ * - `deepfake`: generated or manipulated content that resembles real people, objects,
8
+ * places or events and could falsely appear authentic (Art. 50(4)).
9
+ * - `emotion-recognition` and `biometric-categorisation`: people are exposed to such a
10
+ * system (Art. 50(3)).
11
+ */
12
+ type DisclosureKind = "chatbot" | "generated" | "edited" | "deepfake" | "emotion-recognition" | "biometric-categorisation";
13
+ declare const DISCLOSURE_KINDS: readonly DisclosureKind[];
14
+ /** The kinds that describe a piece of content and can be written into a marking. */
15
+ type ContentKind = "generated" | "edited" | "deepfake";
16
+ /**
17
+ * IPTC Digital Source Type values relevant to AI. See
18
+ * https://cv.iptc.org/newscodes/digitalsourcetype/
19
+ */
20
+ type SourceType = "trainedAlgorithmicMedia" | "compositeWithTrainedAlgorithmicMedia" | "algorithmicallyEnhanced" | "algorithmicMedia" | "compositeSynthetic";
21
+ /** Wording for one disclosure kind in one language. */
22
+ interface DisclosureText {
23
+ /** Two or three words for a badge, e.g. "AI-generated". */
24
+ label: string;
25
+ /** One line for the heading of a notice. */
26
+ title: string;
27
+ /** One or two plain sentences that explain the label. */
28
+ body: string;
29
+ }
30
+ /** Interface strings shared by the notice and label components. */
31
+ interface UiText {
32
+ /** Button that collapses the chatbot notice. */
33
+ acknowledge: string;
34
+ /** Accessible name of the button that opens label details. */
35
+ details: string;
36
+ /** "Generated with {generator}". `{generator}` is replaced. */
37
+ generatedWith: string;
38
+ /** "Created on {date}". `{date}` is replaced. */
39
+ createdOn: string;
40
+ /** Shown when a person reviewed the content and took editorial responsibility. */
41
+ humanReviewed: string;
42
+ /** Link text to a page with more information. */
43
+ moreInfo: string;
44
+ /** Closes the details popover. */
45
+ close: string;
46
+ }
47
+ interface LocaleMessages {
48
+ kinds: Record<DisclosureKind, DisclosureText>;
49
+ ui: UiText;
50
+ }
51
+ /** A partial override of a locale, e.g. to switch German to the informal "du". */
52
+ interface LocaleOverride {
53
+ kinds?: Partial<Record<DisclosureKind, Partial<DisclosureText>>>;
54
+ ui?: Partial<UiText>;
55
+ }
56
+ /** What a marking says about a piece of content. Input for {@link createMarking}. */
57
+ interface MarkingInput {
58
+ /** Defaults to `generated`. */
59
+ kind?: ContentKind;
60
+ /** Defaults from `kind`: `trainedAlgorithmicMedia`, or the composite type for `edited`. */
61
+ sourceType?: SourceType;
62
+ /** Name of the AI system or model, e.g. "gpt-image-2" or "Claude". */
63
+ generator?: string;
64
+ /** Version of the AI system, if known. */
65
+ generatorVersion?: string;
66
+ /** Who provides or operates the AI system, e.g. your company. */
67
+ provider?: string;
68
+ /** ISO 8601 timestamp. Defaults to now. */
69
+ createdAt?: string | Date;
70
+ /** A page that explains how and why you use AI. */
71
+ disclosureUrl?: string;
72
+ /** A person reviewed the content and holds editorial responsibility (Art. 50(4), text). */
73
+ humanReviewed?: boolean;
74
+ /** Free text, e.g. "Image generated from the prompt by the newsroom". */
75
+ description?: string;
76
+ }
77
+ /** A normalised marking with all defaults applied. */
78
+ interface Marking {
79
+ kind: ContentKind;
80
+ sourceType: SourceType;
81
+ generator?: string;
82
+ generatorVersion?: string;
83
+ provider?: string;
84
+ createdAt: string;
85
+ disclosureUrl?: string;
86
+ humanReviewed: boolean;
87
+ description?: string;
88
+ }
89
+
90
+ export { type ContentKind as C, type DisclosureKind as D, type LocaleOverride as L, type Marking as M, type SourceType as S, type UiText as U, type MarkingInput as a, type DisclosureText as b, type LocaleMessages as c, DISCLOSURE_KINDS as d };
package/package.json CHANGED
@@ -1,6 +1,126 @@
1
1
  {
2
2
  "name": "@sweberdev/witness",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.1",
4
+ "description": "AI transparency kit for Article 50 of the EU AI Act: chatbot notices, AI labels in DE/EN/FR/IT, IPTC/XMP marking for images, an invisible text watermark and Vercel AI SDK middleware.",
5
+ "license": "MIT",
6
+ "author": "Seya Weber (https://github.com/sxwxbxr)",
7
+ "homepage": "https://packages.sweber.dev/witness/docs",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Weber-Development/witness.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/Weber-Development/witness/issues"
15
+ },
16
+ "type": "module",
17
+ "sideEffects": [
18
+ "./dist/elements-auto.js",
19
+ "./dist/elements-auto.cjs",
20
+ "./src/elements-auto.ts",
21
+ "./styles.css"
22
+ ],
23
+ "main": "./dist/index.cjs",
24
+ "module": "./dist/index.js",
25
+ "types": "./dist/index.d.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": {
29
+ "types": "./dist/index.d.ts",
30
+ "default": "./dist/index.js"
31
+ },
32
+ "require": {
33
+ "types": "./dist/index.d.cts",
34
+ "default": "./dist/index.cjs"
35
+ }
36
+ },
37
+ "./elements": {
38
+ "import": {
39
+ "types": "./dist/elements-auto.d.ts",
40
+ "default": "./dist/elements-auto.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/elements-auto.d.cts",
44
+ "default": "./dist/elements-auto.cjs"
45
+ }
46
+ },
47
+ "./elements/define": {
48
+ "import": {
49
+ "types": "./dist/elements.d.ts",
50
+ "default": "./dist/elements.js"
51
+ },
52
+ "require": {
53
+ "types": "./dist/elements.d.cts",
54
+ "default": "./dist/elements.cjs"
55
+ }
56
+ },
57
+ "./ai-sdk": {
58
+ "import": {
59
+ "types": "./dist/ai-sdk.d.ts",
60
+ "default": "./dist/ai-sdk.js"
61
+ },
62
+ "require": {
63
+ "types": "./dist/ai-sdk.d.cts",
64
+ "default": "./dist/ai-sdk.cjs"
65
+ }
66
+ },
67
+ "./cli": {
68
+ "import": {
69
+ "types": "./dist/cli.d.ts",
70
+ "default": "./dist/cli.js"
71
+ },
72
+ "require": {
73
+ "types": "./dist/cli.d.cts",
74
+ "default": "./dist/cli.cjs"
75
+ }
76
+ },
77
+ "./styles.css": "./styles.css",
78
+ "./package.json": "./package.json"
79
+ },
80
+ "bin": {
81
+ "witness": "./dist/bin.js"
82
+ },
83
+ "files": [
84
+ "dist",
85
+ "src",
86
+ "styles.css",
87
+ "README.md",
88
+ "LICENSE"
89
+ ],
90
+ "keywords": [
91
+ "ai-act",
92
+ "eu-ai-act",
93
+ "article-50",
94
+ "ai-transparency",
95
+ "ai-label",
96
+ "ai-disclosure",
97
+ "chatbot",
98
+ "watermark",
99
+ "xmp",
100
+ "iptc",
101
+ "digital-source-type",
102
+ "vercel-ai-sdk",
103
+ "web-component"
104
+ ],
105
+ "engines": {
106
+ "node": ">=20"
107
+ },
108
+ "publishConfig": {
109
+ "access": "public",
110
+ "provenance": true
111
+ },
112
+ "devDependencies": {
113
+ "@types/node": "^22.20.5",
114
+ "ai": "^7.0.127",
115
+ "jsdom": "^27.0.0",
116
+ "sharp": "^0.35.5",
117
+ "tsup": "^8.5.1",
118
+ "typescript": "^5.9.0",
119
+ "vitest": "^5.0.3"
120
+ },
121
+ "scripts": {
122
+ "build": "tsup",
123
+ "test": "vitest run",
124
+ "typecheck": "tsc -p tsconfig.json"
125
+ }
6
126
  }
package/src/ai-sdk.ts ADDED
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Middleware for the Vercel AI SDK (`ai` 5, 6 and 7). It marks what a model produces:
3
+ *
4
+ * - `witnessMiddleware` appends the invisible text watermark to every text part, for
5
+ * `generateText` and `streamText`, and adds `providerMetadata.witness`.
6
+ * - `witnessImageMiddleware` writes XMP with the IPTC Digital Source Type into every image
7
+ * from `generateImage`.
8
+ *
9
+ * The types are structural, so this module has no dependency on `ai`.
10
+ */
11
+
12
+ import { type MarkImageOptions, markImage } from "./image.js";
13
+ import { createMarking, iptcSourceType } from "./marking.js";
14
+ import type { ContentKind, Marking } from "./types.js";
15
+ import { watermarkSuffix, watermarkText } from "./watermark.js";
16
+
17
+ type SpecVersion = "v2" | "v3" | "v4";
18
+
19
+ interface ModelInfo {
20
+ provider: string;
21
+ modelId: string;
22
+ }
23
+
24
+ export interface WitnessMarkedEvent {
25
+ type: "text" | "image";
26
+ marking: Marking;
27
+ /** For text: the id of the text part in a stream, if any. */
28
+ partId?: string;
29
+ /** For images: what happened to each image. */
30
+ status?: string;
31
+ }
32
+
33
+ export interface WitnessMiddlewareOptions {
34
+ /**
35
+ * The AI SDK middleware specification. `v4` for `ai` 7 (default), `v3` for `ai` 6, `v2`
36
+ * for `ai` 5.
37
+ */
38
+ specificationVersion?: SpecVersion;
39
+ /** Name written into the marking. Default: `provider/modelId` of the wrapped model. */
40
+ generator?: string | ((model: ModelInfo) => string);
41
+ /** Default `generated`. */
42
+ kind?: ContentKind;
43
+ /** Your company or product, written into the marking. */
44
+ provider?: string;
45
+ /** Append the invisible text watermark. Default `true`. */
46
+ watermark?: boolean;
47
+ /** A reference written into the watermark, e.g. a log id. */
48
+ id?: () => string;
49
+ /** Called once per marked text part or image, e.g. to write an audit log entry. */
50
+ onMarked?: (event: WitnessMarkedEvent) => void;
51
+ }
52
+
53
+ interface TextPart {
54
+ type: string;
55
+ text?: string;
56
+ }
57
+
58
+ interface GenerateResult {
59
+ content: TextPart[];
60
+ providerMetadata?: Record<string, unknown>;
61
+ }
62
+
63
+ interface StreamPart {
64
+ type: string;
65
+ id?: string;
66
+ delta?: string;
67
+ providerMetadata?: Record<string, unknown>;
68
+ }
69
+
70
+ interface StreamResult {
71
+ stream: ReadableStream<StreamPart>;
72
+ }
73
+
74
+ /** The `witness` entry added to `providerMetadata`. */
75
+ export function witnessProviderMetadata(marking: Marking): Record<string, string | boolean> {
76
+ return {
77
+ aiGenerated: true,
78
+ kind: marking.kind,
79
+ sourceType: iptcSourceType(marking.sourceType),
80
+ createdAt: marking.createdAt,
81
+ ...(marking.generator ? { generator: marking.generator } : {}),
82
+ ...(marking.provider ? { provider: marking.provider } : {}),
83
+ };
84
+ }
85
+
86
+ /** Language model middleware. Pass it to `wrapLanguageModel`. */
87
+ export function witnessMiddleware(options: WitnessMiddlewareOptions = {}) {
88
+ const watermark = options.watermark ?? true;
89
+
90
+ const markingFor = (model: ModelInfo): Marking =>
91
+ createMarking({
92
+ kind: options.kind ?? "generated",
93
+ generator: generatorFor(options.generator, model),
94
+ ...(options.provider ? { provider: options.provider } : {}),
95
+ });
96
+
97
+ const watermarkFor = (marking: Marking) => ({
98
+ ...(marking.generator ? { generator: marking.generator } : {}),
99
+ createdAt: marking.createdAt,
100
+ ...(options.id ? { id: options.id() } : {}),
101
+ });
102
+
103
+ return {
104
+ specificationVersion: options.specificationVersion ?? "v4",
105
+
106
+ async wrapGenerate<R extends GenerateResult>({
107
+ doGenerate,
108
+ model,
109
+ }: {
110
+ doGenerate: () => PromiseLike<R>;
111
+ model: ModelInfo;
112
+ }): Promise<R> {
113
+ const result = await doGenerate();
114
+ const marking = markingFor(model);
115
+ const content = result.content.map((part) => {
116
+ if (part.type !== "text" || typeof part.text !== "string" || part.text.length === 0) {
117
+ return part;
118
+ }
119
+ options.onMarked?.({ type: "text", marking });
120
+ return watermark
121
+ ? { ...part, text: watermarkText(part.text, watermarkFor(marking)) }
122
+ : part;
123
+ });
124
+ return {
125
+ ...result,
126
+ content,
127
+ providerMetadata: { ...result.providerMetadata, witness: witnessProviderMetadata(marking) },
128
+ };
129
+ },
130
+
131
+ async wrapStream<R extends StreamResult>({
132
+ doStream,
133
+ model,
134
+ }: {
135
+ doStream: () => PromiseLike<R>;
136
+ model: ModelInfo;
137
+ }): Promise<R> {
138
+ const result = await doStream();
139
+ const marking = markingFor(model);
140
+ const withText = new Set<string>();
141
+ const transform = new TransformStream<StreamPart, StreamPart>({
142
+ transform(part, controller) {
143
+ const id = part.id ?? "";
144
+ if (
145
+ part.type === "text-delta" &&
146
+ typeof part.delta === "string" &&
147
+ part.delta.length > 0
148
+ ) {
149
+ withText.add(id);
150
+ }
151
+ if (part.type === "text-end" && withText.has(id)) {
152
+ withText.delete(id);
153
+ options.onMarked?.({ type: "text", marking, partId: id });
154
+ if (watermark) {
155
+ controller.enqueue({
156
+ type: "text-delta",
157
+ id,
158
+ delta: watermarkSuffix(watermarkFor(marking)),
159
+ });
160
+ }
161
+ }
162
+ if (part.type === "finish") {
163
+ controller.enqueue({
164
+ ...part,
165
+ providerMetadata: {
166
+ ...part.providerMetadata,
167
+ witness: witnessProviderMetadata(marking),
168
+ },
169
+ });
170
+ return;
171
+ }
172
+ controller.enqueue(part);
173
+ },
174
+ });
175
+ return { ...result, stream: result.stream.pipeThrough(transform) };
176
+ },
177
+ };
178
+ }
179
+
180
+ export interface WitnessImageMiddlewareOptions
181
+ extends Pick<WitnessMiddlewareOptions, "generator" | "kind" | "provider" | "onMarked">,
182
+ MarkImageOptions {
183
+ /** `v4` for `ai` 7 (default), `v3` for `ai` 6. */
184
+ specificationVersion?: "v3" | "v4";
185
+ }
186
+
187
+ interface ImageResult {
188
+ images: string[] | Uint8Array[];
189
+ warnings: unknown[];
190
+ }
191
+
192
+ /** Image model middleware. Pass it to `wrapImageModel`. */
193
+ export function witnessImageMiddleware(options: WitnessImageMiddlewareOptions = {}) {
194
+ return {
195
+ specificationVersion: options.specificationVersion ?? "v4",
196
+
197
+ async wrapGenerate<R extends ImageResult>({
198
+ doGenerate,
199
+ model,
200
+ }: {
201
+ doGenerate: () => PromiseLike<R>;
202
+ model: ModelInfo;
203
+ }): Promise<R> {
204
+ const result = await doGenerate();
205
+ const marking = createMarking({
206
+ kind: options.kind ?? "generated",
207
+ generator: generatorFor(options.generator, model),
208
+ ...(options.provider ? { provider: options.provider } : {}),
209
+ });
210
+ const warnings = [...result.warnings];
211
+ const markOne = (bytes: Uint8Array): Uint8Array => {
212
+ const marked = markImage(bytes, marking, options.c2pa ? { c2pa: options.c2pa } : {});
213
+ options.onMarked?.({ type: "image", marking, status: marked.status });
214
+ if (marked.status !== "marked") {
215
+ warnings.push({
216
+ type: "other",
217
+ message:
218
+ marked.status === "skipped-c2pa"
219
+ ? "Witness: image already carries a C2PA manifest and was left unchanged."
220
+ : "Witness: image format not supported (PNG, JPEG, WebP); image is not marked.",
221
+ });
222
+ }
223
+ return marked.bytes;
224
+ };
225
+ const images = result.images.map((image: string | Uint8Array) =>
226
+ typeof image === "string" ? toBase64(markOne(fromBase64(image))) : markOne(image),
227
+ ) as R["images"];
228
+ return { ...result, images, warnings };
229
+ },
230
+ };
231
+ }
232
+
233
+ function generatorFor(option: WitnessMiddlewareOptions["generator"], model: ModelInfo): string {
234
+ if (typeof option === "function") return option(model);
235
+ return option ?? `${model.provider}/${model.modelId}`;
236
+ }
237
+
238
+ function fromBase64(value: string): Uint8Array {
239
+ const binary = atob(value);
240
+ const bytes = new Uint8Array(binary.length);
241
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
242
+ return bytes;
243
+ }
244
+
245
+ function toBase64(bytes: Uint8Array): string {
246
+ let binary = "";
247
+ const step = 0x8000;
248
+ for (let i = 0; i < bytes.length; i += step) {
249
+ binary += String.fromCharCode(...bytes.subarray(i, i + step));
250
+ }
251
+ return btoa(binary);
252
+ }
package/src/bin.ts ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "./cli.js";
3
+
4
+ runCli(process.argv.slice(2)).then((code) => {
5
+ process.exitCode = code;
6
+ });