@dorsk/hmm 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.
- package/LICENSE +21 -0
- package/README.md +162 -2
- package/dist/Hmm.svelte +650 -0
- package/dist/Hmm.svelte.d.ts +11 -0
- package/dist/core.d.ts +33 -0
- package/dist/core.js +50 -0
- package/dist/draft.d.ts +12 -0
- package/dist/draft.js +44 -0
- package/dist/envelope.d.ts +103 -0
- package/dist/envelope.js +218 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +32 -0
- package/dist/inspect.d.ts +4 -0
- package/dist/inspect.js +75 -0
- package/dist/labels.d.ts +36 -0
- package/dist/labels.js +35 -0
- package/dist/picker.d.ts +2 -0
- package/dist/picker.js +4 -0
- package/dist/redact.d.ts +5 -0
- package/dist/redact.js +64 -0
- package/dist/shot.d.ts +10 -0
- package/dist/shot.js +61 -0
- package/dist/transport.d.ts +11 -0
- package/dist/transport.js +71 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +82 -5
- package/schema/envelope.v1.json +163 -0
package/dist/redact.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const RULES = [
|
|
2
|
+
[
|
|
3
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
4
|
+
'[redacted-private-key]',
|
|
5
|
+
],
|
|
6
|
+
[/\beyJ[\w-]{8,}\.[\w-]{8,}\.[\w-]{8,}/g, '[redacted-jwt]'],
|
|
7
|
+
[/\b(Bearer|Basic)\s+[\w.~+/=-]{8,}/gi, '$1 [redacted]'],
|
|
8
|
+
[/\b(?:sk|pk|rk)-[\w-]{20,}/g, '[redacted-key]'],
|
|
9
|
+
[/\bgh[pousr]_\w{30,}/g, '[redacted-key]'],
|
|
10
|
+
[/\bgithub_pat_\w{30,}/g, '[redacted-key]'],
|
|
11
|
+
[/\bxox[abposr]-[\w-]{10,}/g, '[redacted-key]'],
|
|
12
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, '[redacted-key]'],
|
|
13
|
+
[
|
|
14
|
+
/([\w-]*(?:pass(?:word|wd)?|secret|token|api[_-]?key|access[_-]?key|authorization))(["']?\s*[:=]\s*["']?)(?!\[?redacted)([^\s"'&,;]{4,})/gi,
|
|
15
|
+
'$1$2[redacted]',
|
|
16
|
+
],
|
|
17
|
+
];
|
|
18
|
+
const SECRET_PARAM = /token|key|secret|pass|code|sig|auth|session/i;
|
|
19
|
+
export function redactText(text) {
|
|
20
|
+
let out = text;
|
|
21
|
+
for (const [pattern, replacement] of RULES)
|
|
22
|
+
out = out.replace(pattern, replacement);
|
|
23
|
+
return out;
|
|
24
|
+
}
|
|
25
|
+
export function redactUrl(url) {
|
|
26
|
+
let parsed;
|
|
27
|
+
try {
|
|
28
|
+
parsed = new URL(url);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return redactText(url);
|
|
32
|
+
}
|
|
33
|
+
parsed.username = '';
|
|
34
|
+
parsed.password = '';
|
|
35
|
+
for (const name of [...parsed.searchParams.keys()]) {
|
|
36
|
+
if (SECRET_PARAM.test(name))
|
|
37
|
+
parsed.searchParams.set(name, 'redacted');
|
|
38
|
+
}
|
|
39
|
+
if (parsed.hash.includes('='))
|
|
40
|
+
parsed.hash = redactText(parsed.hash);
|
|
41
|
+
return redactText(parsed.toString());
|
|
42
|
+
}
|
|
43
|
+
function deep(value) {
|
|
44
|
+
if (typeof value === 'string')
|
|
45
|
+
return redactText(value);
|
|
46
|
+
if (Array.isArray(value))
|
|
47
|
+
return value.map(deep);
|
|
48
|
+
if (typeof value === 'object' && value !== null) {
|
|
49
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deep(v)]));
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
/** Scrubs every string except attachment image data; images cannot be scrubbed. */
|
|
54
|
+
export function redactEnvelope(envelope) {
|
|
55
|
+
const { attachments, ...rest } = envelope;
|
|
56
|
+
const withUrls = {
|
|
57
|
+
...rest,
|
|
58
|
+
page: rest.page ? { ...rest.page, url: redactUrl(rest.page.url) } : undefined,
|
|
59
|
+
items: rest.items.map((item) => ({ ...item, url: redactUrl(item.url) })),
|
|
60
|
+
};
|
|
61
|
+
if (!withUrls.page)
|
|
62
|
+
delete withUrls.page;
|
|
63
|
+
return { ...deep(withUrls), attachments };
|
|
64
|
+
}
|
package/dist/shot.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Shot = {
|
|
2
|
+
data: string;
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Rasterises the DOM, so cross-origin images without CORS, video and tainted canvases come out
|
|
8
|
+
* blank. `target` null captures the visible viewport.
|
|
9
|
+
*/
|
|
10
|
+
export declare function capture(win: Window, target: Element | null, exclude: Element): Promise<Shot>;
|
package/dist/shot.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { LIMITS } from "./envelope.js";
|
|
2
|
+
const MAX_SIDE = 1600;
|
|
3
|
+
const PREFIX = 'data:image/png;base64,';
|
|
4
|
+
const CLEAR = new Set(['', 'transparent', 'rgba(0, 0, 0, 0)']);
|
|
5
|
+
function paintOf(win, el) {
|
|
6
|
+
const color = win.getComputedStyle(el).backgroundColor;
|
|
7
|
+
return CLEAR.has(color) ? null : color;
|
|
8
|
+
}
|
|
9
|
+
/** The renderer paints `backgroundColor` on the captured node itself, so an element that has its own background must not get one. */
|
|
10
|
+
function backdropOf(win, target) {
|
|
11
|
+
if (paintOf(win, target))
|
|
12
|
+
return {};
|
|
13
|
+
for (let node = target.parentElement; node; node = node.parentElement) {
|
|
14
|
+
const color = paintOf(win, node);
|
|
15
|
+
if (color)
|
|
16
|
+
return { backgroundColor: color };
|
|
17
|
+
}
|
|
18
|
+
return { backgroundColor: '#ffffff' };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Rasterises the DOM, so cross-origin images without CORS, video and tainted canvases come out
|
|
22
|
+
* blank. `target` null captures the visible viewport.
|
|
23
|
+
*/
|
|
24
|
+
export async function capture(win, target, exclude) {
|
|
25
|
+
const { domToPng } = await import('modern-screenshot');
|
|
26
|
+
const doc = win.document;
|
|
27
|
+
const size = target
|
|
28
|
+
? target.getBoundingClientRect()
|
|
29
|
+
: { width: win.innerWidth, height: win.innerHeight };
|
|
30
|
+
if (size.width < 1 || size.height < 1)
|
|
31
|
+
throw new Error('nothing to capture');
|
|
32
|
+
const base = {
|
|
33
|
+
filter: (node) => node !== exclude,
|
|
34
|
+
...(target
|
|
35
|
+
? backdropOf(win, target)
|
|
36
|
+
: {
|
|
37
|
+
backgroundColor: paintOf(win, doc.body) ?? '#ffffff',
|
|
38
|
+
width: size.width,
|
|
39
|
+
height: size.height,
|
|
40
|
+
style: {
|
|
41
|
+
overflow: 'hidden',
|
|
42
|
+
transform: `translate(${-win.scrollX}px, ${-win.scrollY}px)`,
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
let scale = Math.min(1, MAX_SIDE / Math.max(size.width, size.height));
|
|
47
|
+
for (let attempt = 0; attempt < 3; attempt++, scale /= 2) {
|
|
48
|
+
const url = await domToPng(target ?? doc.documentElement, { ...base, scale });
|
|
49
|
+
if (!url.startsWith(PREFIX))
|
|
50
|
+
break;
|
|
51
|
+
const data = url.slice(PREFIX.length);
|
|
52
|
+
if (data.length <= LIMITS.attachmentChars) {
|
|
53
|
+
return {
|
|
54
|
+
data,
|
|
55
|
+
width: Math.max(1, Math.round(size.width * scale)),
|
|
56
|
+
height: Math.max(1, Math.round(size.height * scale)),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error('the screenshot is too large');
|
|
61
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Envelope } from './envelope.ts';
|
|
2
|
+
export type Transport = (envelope: Envelope) => Promise<void>;
|
|
3
|
+
export declare class SendError extends Error {
|
|
4
|
+
retryable: boolean;
|
|
5
|
+
constructor(message: string, retryable: boolean);
|
|
6
|
+
}
|
|
7
|
+
export declare function httpTransport(endpoint: string, projectKey: string): Transport;
|
|
8
|
+
export declare const isRetryable: (err: unknown) => boolean;
|
|
9
|
+
/** False when the report could not be stored, for example when the quota is exceeded. */
|
|
10
|
+
export declare function enqueue(storage: Storage, key: string, envelope: Envelope): boolean;
|
|
11
|
+
export declare function flush(storage: Storage, key: string, transport: Transport): Promise<number>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { validateEnvelope } from "./envelope.js";
|
|
2
|
+
export class SendError extends Error {
|
|
3
|
+
retryable;
|
|
4
|
+
constructor(message, retryable) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = 'SendError';
|
|
7
|
+
this.retryable = retryable;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function httpTransport(endpoint, projectKey) {
|
|
11
|
+
const url = `${endpoint.replace(/\/+$/, '')}/v1/reports`;
|
|
12
|
+
return async (envelope) => {
|
|
13
|
+
const res = await fetch(url, {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'content-type': 'application/json', 'x-hmm-key': projectKey },
|
|
16
|
+
body: JSON.stringify(envelope),
|
|
17
|
+
credentials: 'omit',
|
|
18
|
+
});
|
|
19
|
+
if (res.ok)
|
|
20
|
+
return;
|
|
21
|
+
const retryable = res.status >= 500 || res.status === 408 || res.status === 429;
|
|
22
|
+
throw new SendError(`the server answered ${res.status}`, retryable);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
export const isRetryable = (err) => err?.retryable !== false;
|
|
26
|
+
const OUTBOX_MAX = 5;
|
|
27
|
+
function read(storage, key) {
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(storage.getItem(key) ?? '[]');
|
|
30
|
+
if (!Array.isArray(parsed))
|
|
31
|
+
return [];
|
|
32
|
+
return parsed.filter((e) => validateEnvelope(e).ok);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function write(storage, key, queue) {
|
|
39
|
+
try {
|
|
40
|
+
if (queue.length)
|
|
41
|
+
storage.setItem(key, JSON.stringify(queue));
|
|
42
|
+
else
|
|
43
|
+
storage.removeItem(key);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** False when the report could not be stored, for example when the quota is exceeded. */
|
|
51
|
+
export function enqueue(storage, key, envelope) {
|
|
52
|
+
const queue = read(storage, key);
|
|
53
|
+
if (queue.length >= OUTBOX_MAX)
|
|
54
|
+
return false;
|
|
55
|
+
return write(storage, key, [...queue, envelope]);
|
|
56
|
+
}
|
|
57
|
+
export async function flush(storage, key, transport) {
|
|
58
|
+
let sent = 0;
|
|
59
|
+
for (const envelope of read(storage, key)) {
|
|
60
|
+
try {
|
|
61
|
+
await transport(envelope);
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
if (isRetryable(err))
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
write(storage, key, read(storage, key).filter((e) => e.id !== envelope.id));
|
|
68
|
+
sent++;
|
|
69
|
+
}
|
|
70
|
+
return sent;
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.1.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = '0.1.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,83 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@dorsk/hmm",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Feedback panel for Svelte apps built on tsumikit: select an element, comment, attach a screenshot, review what will be sent, and post it to your own endpoint.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Dorsk <dorsk.dev@gmail.com>",
|
|
8
|
+
"homepage": "https://github.com/DorskFR/hmm",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/DorskFR/hmm.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/DorskFR/hmm/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"feedback",
|
|
18
|
+
"widget",
|
|
19
|
+
"screenshot",
|
|
20
|
+
"picker",
|
|
21
|
+
"bug-report",
|
|
22
|
+
"svelte",
|
|
23
|
+
"svelte5",
|
|
24
|
+
"tsumikit"
|
|
25
|
+
],
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"schema"
|
|
29
|
+
],
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"svelte": "./dist/index.js",
|
|
35
|
+
"default": "./dist/index.js"
|
|
36
|
+
},
|
|
37
|
+
"./envelope": {
|
|
38
|
+
"types": "./dist/envelope.d.ts",
|
|
39
|
+
"default": "./dist/envelope.js"
|
|
40
|
+
},
|
|
41
|
+
"./schema/envelope.v1.json": "./schema/envelope.v1.json",
|
|
42
|
+
"./package.json": "./package.json"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"provenance": true
|
|
47
|
+
},
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "node scripts/version.mjs && svelte-package -i src -o dist && publint && node scripts/bundle.mjs",
|
|
50
|
+
"check": "node scripts/version.mjs && svelte-check --tsconfig ./tsconfig.json",
|
|
51
|
+
"lint": "biome check . && tsumikit-lint src",
|
|
52
|
+
"format": "biome check --write .",
|
|
53
|
+
"demo": "npm run build && node demo/serve.mjs 4188",
|
|
54
|
+
"test": "npm run build && playwright test",
|
|
55
|
+
"test:unit": "playwright test --project unit",
|
|
56
|
+
"prepare": "lefthook install || true",
|
|
57
|
+
"prepublishOnly": "npm run build"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"modern-screenshot": "^4.7.0"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@dorsk/tsumikit": ">=0.82.0 <1",
|
|
64
|
+
"svelte": "^5.0.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@biomejs/biome": "^2.5.12",
|
|
68
|
+
"@dorsk/tsumikit": "^0.83.0",
|
|
69
|
+
"@playwright/test": "^1.64.1",
|
|
70
|
+
"@sveltejs/package": "^3.0.2",
|
|
71
|
+
"@types/node": "^26.4.1",
|
|
72
|
+
"ajv": "^8.17.1",
|
|
73
|
+
"esbuild": "^0.28.2",
|
|
74
|
+
"esbuild-svelte": "^0.9.5",
|
|
75
|
+
"lefthook": "^2.1.12",
|
|
76
|
+
"publint": "^0.3.25",
|
|
77
|
+
"svelte": "^5.57.3",
|
|
78
|
+
"svelte-check": "^4.7.8",
|
|
79
|
+
"typescript": "^6.0.3"
|
|
80
|
+
},
|
|
81
|
+
"svelte": "./dist/index.js",
|
|
82
|
+
"sideEffects": false
|
|
83
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
3
|
+
"$id": "https://github.com/DorskFR/hmm/blob/main/schema/envelope.v1.json",
|
|
4
|
+
"title": "hmm report envelope v1",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"required": [
|
|
8
|
+
"v",
|
|
9
|
+
"id",
|
|
10
|
+
"kind",
|
|
11
|
+
"project",
|
|
12
|
+
"createdAt",
|
|
13
|
+
"client",
|
|
14
|
+
"context",
|
|
15
|
+
"comment",
|
|
16
|
+
"items",
|
|
17
|
+
"attachments"
|
|
18
|
+
],
|
|
19
|
+
"properties": {
|
|
20
|
+
"v": { "const": 1 },
|
|
21
|
+
"id": { "$ref": "#/definitions/id" },
|
|
22
|
+
"kind": { "enum": ["feedback", "error", "alert"] },
|
|
23
|
+
"category": { "enum": ["idea", "problem", "question", "other"] },
|
|
24
|
+
"fingerprint": { "type": "string", "maxLength": 500 },
|
|
25
|
+
"project": { "type": "string", "pattern": "^[A-Za-z0-9._-]{1,128}$" },
|
|
26
|
+
"createdAt": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"maxLength": 40,
|
|
29
|
+
"pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z$"
|
|
30
|
+
},
|
|
31
|
+
"client": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"additionalProperties": false,
|
|
34
|
+
"required": ["name", "version"],
|
|
35
|
+
"properties": {
|
|
36
|
+
"name": { "type": "string", "maxLength": 100 },
|
|
37
|
+
"version": { "type": "string", "maxLength": 100 }
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"page": {
|
|
41
|
+
"type": "object",
|
|
42
|
+
"additionalProperties": false,
|
|
43
|
+
"required": ["url", "title", "viewport", "userAgent", "language"],
|
|
44
|
+
"properties": {
|
|
45
|
+
"url": { "type": "string", "maxLength": 2000 },
|
|
46
|
+
"title": { "type": "string", "maxLength": 500 },
|
|
47
|
+
"userAgent": { "type": "string", "maxLength": 500 },
|
|
48
|
+
"language": { "type": "string", "maxLength": 100 },
|
|
49
|
+
"viewport": {
|
|
50
|
+
"type": "object",
|
|
51
|
+
"additionalProperties": false,
|
|
52
|
+
"required": ["width", "height"],
|
|
53
|
+
"properties": {
|
|
54
|
+
"width": { "type": "integer", "minimum": 0, "maximum": 1000000 },
|
|
55
|
+
"height": { "type": "integer", "minimum": 0, "maximum": 1000000 }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"context": {
|
|
61
|
+
"description": "Free-form host data. Must also serialise to at most 16000 characters.",
|
|
62
|
+
"type": "object",
|
|
63
|
+
"maxProperties": 50
|
|
64
|
+
},
|
|
65
|
+
"comment": { "type": "string", "maxLength": 5000 },
|
|
66
|
+
"items": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"maxItems": 20,
|
|
69
|
+
"items": {
|
|
70
|
+
"oneOf": [{ "$ref": "#/definitions/elementItem" }, { "$ref": "#/definitions/screenshotItem" }]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"attachments": {
|
|
74
|
+
"description": "Ids must be unique, and every item attachment must name one of them.",
|
|
75
|
+
"type": "array",
|
|
76
|
+
"maxItems": 10,
|
|
77
|
+
"items": { "$ref": "#/definitions/attachment" }
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
"definitions": {
|
|
81
|
+
"id": { "type": "string", "pattern": "^[A-Za-z0-9_-]{1,64}$" },
|
|
82
|
+
"elementItem": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"required": ["id", "type", "url", "comment", "target"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"id": { "$ref": "#/definitions/id" },
|
|
88
|
+
"type": { "const": "element" },
|
|
89
|
+
"url": { "type": "string", "maxLength": 2000 },
|
|
90
|
+
"comment": { "type": "string", "maxLength": 5000 },
|
|
91
|
+
"target": { "$ref": "#/definitions/target" },
|
|
92
|
+
"attachment": { "$ref": "#/definitions/id" }
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"screenshotItem": {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"additionalProperties": false,
|
|
98
|
+
"required": ["id", "type", "url", "comment", "attachment"],
|
|
99
|
+
"properties": {
|
|
100
|
+
"id": { "$ref": "#/definitions/id" },
|
|
101
|
+
"type": { "const": "screenshot" },
|
|
102
|
+
"url": { "type": "string", "maxLength": 2000 },
|
|
103
|
+
"comment": { "type": "string", "maxLength": 5000 },
|
|
104
|
+
"attachment": { "$ref": "#/definitions/id" }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"target": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"additionalProperties": false,
|
|
110
|
+
"required": ["selector", "tag", "text", "html", "attrs", "rect"],
|
|
111
|
+
"properties": {
|
|
112
|
+
"selector": { "type": "string", "maxLength": 1000 },
|
|
113
|
+
"tag": { "type": "string", "maxLength": 100 },
|
|
114
|
+
"text": { "type": "string", "maxLength": 400 },
|
|
115
|
+
"html": { "type": "string", "maxLength": 2000 },
|
|
116
|
+
"attrs": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"maxProperties": 40,
|
|
119
|
+
"propertyNames": { "maxLength": 100 },
|
|
120
|
+
"additionalProperties": { "type": "string", "maxLength": 400 }
|
|
121
|
+
},
|
|
122
|
+
"rect": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"additionalProperties": false,
|
|
125
|
+
"required": ["x", "y", "width", "height"],
|
|
126
|
+
"properties": {
|
|
127
|
+
"x": { "type": "integer", "minimum": -1000000, "maximum": 1000000 },
|
|
128
|
+
"y": { "type": "integer", "minimum": -1000000, "maximum": 1000000 },
|
|
129
|
+
"width": { "type": "integer", "minimum": 0, "maximum": 1000000 },
|
|
130
|
+
"height": { "type": "integer", "minimum": 0, "maximum": 1000000 }
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"source": {
|
|
134
|
+
"type": "object",
|
|
135
|
+
"additionalProperties": false,
|
|
136
|
+
"required": ["file", "line", "column"],
|
|
137
|
+
"properties": {
|
|
138
|
+
"file": { "type": "string", "maxLength": 500 },
|
|
139
|
+
"line": { "type": "integer", "minimum": 0, "maximum": 1000000 },
|
|
140
|
+
"column": { "type": "integer", "minimum": 0, "maximum": 1000000 }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"attachment": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"additionalProperties": false,
|
|
148
|
+
"required": ["id", "mime", "width", "height", "data"],
|
|
149
|
+
"properties": {
|
|
150
|
+
"id": { "$ref": "#/definitions/id" },
|
|
151
|
+
"mime": { "const": "image/png" },
|
|
152
|
+
"width": { "type": "integer", "minimum": 1, "maximum": 8192 },
|
|
153
|
+
"height": { "type": "integer", "minimum": 1, "maximum": 8192 },
|
|
154
|
+
"data": {
|
|
155
|
+
"description": "Base64 without a data URL prefix.",
|
|
156
|
+
"type": "string",
|
|
157
|
+
"maxLength": 2800000,
|
|
158
|
+
"pattern": "^[A-Za-z0-9+/]+={0,2}$"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|