@sweberdev/witness 0.0.0-stage → 0.1.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.
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-DHX7SNWE.js +310 -0
  18. package/dist/chunk-DHX7SNWE.js.map +1 -0
  19. package/dist/chunk-L65R2SM5.js +228 -0
  20. package/dist/chunk-L65R2SM5.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 +642 -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 +639 -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 +356 -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,165 @@
1
+ /**
2
+ * An invisible, machine-readable marker for AI-generated text.
3
+ *
4
+ * The payload is written as Unicode variation selectors (U+FE00–U+FE0F and
5
+ * U+E0100–U+E01EF). They are default-ignorable code points: browsers do not render them and
6
+ * screen readers skip them, and they survive copy and paste in most applications. They do
7
+ * not survive sanitisers that strip unusual code points, retyping, or paraphrasing, so treat
8
+ * this as one marking layer next to visible labels and metadata, not as a robust watermark.
9
+ */
10
+
11
+ const MAGIC = [0x57, 0x31]; // "W1"
12
+ const MAX_PAYLOAD = 255;
13
+
14
+ export interface TextWatermark {
15
+ /** Name of the AI system. */
16
+ generator?: string;
17
+ /** ISO 8601 timestamp. */
18
+ createdAt?: string;
19
+ /** Your own reference, e.g. a message or log id. */
20
+ id?: string;
21
+ }
22
+
23
+ export interface ReadTextWatermark extends TextWatermark {
24
+ /** The raw payload as written. */
25
+ raw: string;
26
+ }
27
+
28
+ /**
29
+ * Appends an invisible watermark to `text`. An existing Witness watermark is replaced. Empty
30
+ * text is returned unchanged.
31
+ */
32
+ export function watermarkText(text: string, mark: TextWatermark = {}): string {
33
+ if (text.length === 0) return text;
34
+ return stripTextWatermark(text) + watermarkSuffix(mark);
35
+ }
36
+
37
+ /**
38
+ * Only the invisible characters, for appending to a stream. They attach to whatever character
39
+ * precedes them, so emit them after at least one visible character.
40
+ */
41
+ export function watermarkSuffix(mark: TextWatermark = {}): string {
42
+ const payload = encodePayload(mark);
43
+ return [...MAGIC, payload.length, ...payload].map(byteToSelector).join("");
44
+ }
45
+
46
+ /** Reads the first Witness watermark in `text`, or `null`. */
47
+ export function readTextWatermark(text: string): ReadTextWatermark | null {
48
+ for (const run of selectorRuns(text)) {
49
+ const found = findInRun(run.bytes);
50
+ if (found) {
51
+ const raw = new TextDecoder().decode(new Uint8Array(found.payload));
52
+ return { ...decodePayload(raw), raw };
53
+ }
54
+ }
55
+ return null;
56
+ }
57
+
58
+ /** True when `text` carries a Witness watermark. */
59
+ export function hasTextWatermark(text: string): boolean {
60
+ return readTextWatermark(text) !== null;
61
+ }
62
+
63
+ /**
64
+ * Removes Witness watermarks and leaves all other characters, including the variation
65
+ * selectors that belong to emoji, untouched.
66
+ */
67
+ export function stripTextWatermark(text: string): string {
68
+ const chars = [...text];
69
+ const remove = new Set<number>();
70
+ for (const run of selectorRuns(text)) {
71
+ let offset = 0;
72
+ while (offset < run.bytes.length) {
73
+ const found = findInRun(run.bytes.slice(offset));
74
+ if (!found) break;
75
+ const start = offset + found.start;
76
+ const end = offset + found.end;
77
+ for (let i = start; i < end; i++) remove.add(run.start + i);
78
+ offset = end;
79
+ }
80
+ }
81
+ if (remove.size === 0) return text;
82
+ return chars.filter((_, index) => !remove.has(index)).join("");
83
+ }
84
+
85
+ function encodePayload(mark: TextWatermark): number[] {
86
+ const fields: string[] = [];
87
+ if (mark.generator) fields.push(`g=${encodeURIComponent(mark.generator)}`);
88
+ if (mark.createdAt) fields.push(`t=${encodeURIComponent(mark.createdAt)}`);
89
+ if (mark.id) fields.push(`i=${encodeURIComponent(mark.id)}`);
90
+ let bytes = [...new TextEncoder().encode(fields.join(";"))];
91
+ if (bytes.length > MAX_PAYLOAD) {
92
+ // Keep whole fields only, so a truncated payload still parses.
93
+ while (fields.length > 0 && bytes.length > MAX_PAYLOAD) {
94
+ fields.pop();
95
+ bytes = [...new TextEncoder().encode(fields.join(";"))];
96
+ }
97
+ }
98
+ return bytes;
99
+ }
100
+
101
+ function decodePayload(raw: string): TextWatermark {
102
+ const result: TextWatermark = {};
103
+ for (const field of raw.split(";")) {
104
+ const index = field.indexOf("=");
105
+ if (index < 1) continue;
106
+ const key = field.slice(0, index);
107
+ let value: string;
108
+ try {
109
+ value = decodeURIComponent(field.slice(index + 1));
110
+ } catch {
111
+ continue;
112
+ }
113
+ if (key === "g") result.generator = value;
114
+ else if (key === "t") result.createdAt = value;
115
+ else if (key === "i") result.id = value;
116
+ }
117
+ return result;
118
+ }
119
+
120
+ function byteToSelector(byte: number): string {
121
+ return byte < 16
122
+ ? String.fromCodePoint(0xfe00 + byte)
123
+ : String.fromCodePoint(0xe0100 + (byte - 16));
124
+ }
125
+
126
+ function selectorToByte(codePoint: number): number | null {
127
+ if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) return codePoint - 0xfe00;
128
+ if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) return codePoint - 0xe0100 + 16;
129
+ return null;
130
+ }
131
+
132
+ /** Consecutive variation selectors, with their index in code points. */
133
+ function selectorRuns(text: string): Array<{ start: number; bytes: number[] }> {
134
+ const runs: Array<{ start: number; bytes: number[] }> = [];
135
+ let current: { start: number; bytes: number[] } | null = null;
136
+ let index = 0;
137
+ for (const char of text) {
138
+ const byte = selectorToByte(char.codePointAt(0) ?? 0);
139
+ if (byte === null) {
140
+ current = null;
141
+ } else {
142
+ if (!current) {
143
+ current = { start: index, bytes: [] };
144
+ runs.push(current);
145
+ }
146
+ current.bytes.push(byte);
147
+ }
148
+ index++;
149
+ }
150
+ return runs;
151
+ }
152
+
153
+ /** Finds MAGIC, length and payload inside a run. Positions are indexes into `bytes`. */
154
+ function findInRun(bytes: number[]): { start: number; end: number; payload: number[] } | null {
155
+ for (let i = 0; i + MAGIC.length < bytes.length; i++) {
156
+ if (bytes[i] !== MAGIC[0] || bytes[i + 1] !== MAGIC[1]) continue;
157
+ const length = bytes[i + 2];
158
+ if (length === undefined) continue;
159
+ const start = i + 3;
160
+ const end = start + length;
161
+ if (end > bytes.length) continue;
162
+ return { start: i, end, payload: bytes.slice(start, end) };
163
+ }
164
+ return null;
165
+ }
package/styles.css ADDED
@@ -0,0 +1,103 @@
1
+ /* Styles for labels rendered by labelHtml() and @sweberdev/witness-react.
2
+ Override the custom properties to match your design. */
3
+ :root {
4
+ --witness-accent: #003399;
5
+ --witness-accent-text: #ffffff;
6
+ --witness-bg: #ffffff;
7
+ --witness-fg: #1a1a1a;
8
+ --witness-muted: #555b66;
9
+ --witness-border: #d7dbe3;
10
+ --witness-radius: 8px;
11
+ }
12
+ @media (prefers-color-scheme: dark) {
13
+ :root {
14
+ --witness-accent: #8fb0ff;
15
+ --witness-accent-text: #0b1530;
16
+ --witness-bg: #15181f;
17
+ --witness-fg: #eef1f6;
18
+ --witness-muted: #a9b0bd;
19
+ --witness-border: #2c323d;
20
+ }
21
+ }
22
+ .witness-label {
23
+ display: inline-flex;
24
+ flex-wrap: wrap;
25
+ gap: 0.4em;
26
+ align-items: center;
27
+ margin: 0 0 0.75em;
28
+ padding: 0.2em 0.6em 0.2em 0.25em;
29
+ border: 1px solid var(--witness-border);
30
+ border-radius: 999px;
31
+ background: var(--witness-bg);
32
+ color: var(--witness-fg);
33
+ font-size: 0.875em;
34
+ line-height: 1.4;
35
+ }
36
+ .witness-label__icon {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ justify-content: center;
40
+ min-width: 1.75em;
41
+ height: 1.5em;
42
+ padding: 0 0.3em;
43
+ border-radius: 4px;
44
+ background: var(--witness-accent);
45
+ color: var(--witness-accent-text);
46
+ font-weight: 700;
47
+ font-size: 0.75em;
48
+ line-height: 1;
49
+ }
50
+ .witness-label__link {
51
+ color: inherit;
52
+ }
53
+ .witness-content > .witness-label:last-child {
54
+ margin: 0.75em 0 0;
55
+ }
56
+ .witness-notice {
57
+ display: flex;
58
+ gap: 0.75em;
59
+ align-items: flex-start;
60
+ padding: 0.875em 1em;
61
+ border: 1px solid var(--witness-border);
62
+ border-left: 4px solid var(--witness-accent);
63
+ border-radius: var(--witness-radius);
64
+ background: var(--witness-bg);
65
+ color: var(--witness-fg);
66
+ }
67
+ .witness-notice__title {
68
+ font-size: 1em;
69
+ margin: 0 0 0.25em;
70
+ }
71
+ .witness-notice__body {
72
+ margin: 0 0 0.75em;
73
+ line-height: 1.45;
74
+ }
75
+ .witness-notice__actions {
76
+ display: flex;
77
+ gap: 0.75em;
78
+ align-items: center;
79
+ flex-wrap: wrap;
80
+ }
81
+ .witness-notice__ack {
82
+ font: inherit;
83
+ cursor: pointer;
84
+ padding: 0.4em 0.9em;
85
+ border-radius: 6px;
86
+ border: 0;
87
+ background: var(--witness-accent);
88
+ color: var(--witness-accent-text);
89
+ font-weight: 600;
90
+ }
91
+ .witness-notice--compact {
92
+ display: inline-flex;
93
+ gap: 0.5em;
94
+ align-items: center;
95
+ font: inherit;
96
+ font-size: 0.875em;
97
+ cursor: pointer;
98
+ padding: 0.25em 0.5em;
99
+ border: 1px solid var(--witness-border);
100
+ border-radius: 999px;
101
+ background: var(--witness-bg);
102
+ color: var(--witness-fg);
103
+ }