@innspel/core 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -1
- package/dist/index.cjs +193 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +152 -2
- package/dist/index.d.ts +152 -2
- package/dist/index.js +186 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,12 +83,13 @@ Bruk hva du vil — MMKV, AsyncStorage, SQLite, et objekt i minnet.
|
|
|
83
83
|
## API
|
|
84
84
|
|
|
85
85
|
```ts
|
|
86
|
-
innspel.capture({ type, text, context?, correlation?, relatedKnownIssueId?, clientRef? })
|
|
86
|
+
innspel.capture({ type, text, attachment?, context?, correlation?, relatedKnownIssueId?, clientRef? })
|
|
87
87
|
innspel.knownIssues.for({ screen?, version?, platform? }) // kortene som vises FØR feltet
|
|
88
88
|
innspel.knownIssues.confirm(id) // «Ja, jeg også» — Reach +1, ingen ny sak
|
|
89
89
|
innspel.subjects.submissions() // «Mine innspill», inkl. det som venter på nett
|
|
90
90
|
innspel.subjects.status(submissionId) // status for én egen innsending
|
|
91
91
|
innspel.programs.list() // invitasjoner, deltakelser, oppfølginger
|
|
92
|
+
innspel.attachments.upload(submissionId, attachment) // skjermbilde til en innsending som alt er lagret
|
|
92
93
|
innspel.queue.pending() // det som ligger i offline-køen
|
|
93
94
|
innspel.queue.flush() // sendes ellers automatisk
|
|
94
95
|
innspel.scanText(text) // mønstervarsel, se under
|
|
@@ -109,6 +110,36 @@ er utløpt for lengst.
|
|
|
109
110
|
Vis køen for brukeren. `queue.pending()` gir deg radene, og «Venter på nett —
|
|
110
111
|
sendes automatisk» er teksten widgeten bruker.
|
|
111
112
|
|
|
113
|
+
### Vedlegg
|
|
114
|
+
|
|
115
|
+
Ett skjermbilde per innsending, **maskert på klienten før det forlater enheten**
|
|
116
|
+
(K4). Kjernen har ingen fangst og ingen maskering selv — `@innspel/react-native`
|
|
117
|
+
har begge; her tar du imot bildet som Blob, ArrayBuffer eller Uint8Array:
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
const res = await innspel.capture({
|
|
121
|
+
type: 'feil',
|
|
122
|
+
text: 'Dørlista er tom etter kl. 22.',
|
|
123
|
+
attachment: { mime: 'image/jpeg', body: blob, width: 1170, height: 2532 },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
if (res.status === 'sent' && res.attachment?.status === 'failed') {
|
|
127
|
+
// Teksten ER lagret. Si «Sendt — skjermbildet kunne ikke legges ved» (DD-42).
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Teksten sendes først, bildet etterpå i egen forespørsel direkte til lagringen, og
|
|
132
|
+
bildet kan feile alene — innsendingen står uansett. Uten nett køes teksten og
|
|
133
|
+
bildet forkastes (`attachment.status === 'dropped'`); et skjermbilde køes aldri.
|
|
134
|
+
|
|
135
|
+
Grensene sjekkes før noe går på nettet og kaster med feltnavn: PNG, JPEG eller
|
|
136
|
+
WebP (aldri SVG eller GIF), høyst 5 MB og 4096 × 4096 (K9). Plattformen sjekker
|
|
137
|
+
dem igjen, re-enkoder bildet i en isolert prosess og fjerner EXIF.
|
|
138
|
+
|
|
139
|
+
Geometrien bak maskeringen — fingerstrøk og trykk til rektangler i bildets
|
|
140
|
+
piksler — ligger her (`strokTilRekter`, `trykkTilRekt`, `skaler`, `klipp`), så
|
|
141
|
+
`@innspel/react-native` og `@innspel/web` maskerer likt.
|
|
142
|
+
|
|
112
143
|
### Mønstervarsel
|
|
113
144
|
|
|
114
145
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -780,6 +780,28 @@ var createClient = (config = {}) => {
|
|
|
780
780
|
var client = createClient(createConfig());
|
|
781
781
|
|
|
782
782
|
// src/generated/api/sdk.gen.ts
|
|
783
|
+
var attachmentsPresign = (options) => (options.client ?? client).post({
|
|
784
|
+
security: [{ name: "X-Innspel-Tenant-Key", type: "apiKey" }, {
|
|
785
|
+
key: "FeedbackTokenAuth",
|
|
786
|
+
scheme: "bearer",
|
|
787
|
+
type: "http"
|
|
788
|
+
}],
|
|
789
|
+
url: "/v1/ingest/attachments/presign",
|
|
790
|
+
...options,
|
|
791
|
+
headers: {
|
|
792
|
+
"Content-Type": "application/json",
|
|
793
|
+
...options.headers
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
var attachmentsComplete = (options) => (options.client ?? client).post({
|
|
797
|
+
security: [{ name: "X-Innspel-Tenant-Key", type: "apiKey" }, {
|
|
798
|
+
key: "FeedbackTokenAuth",
|
|
799
|
+
scheme: "bearer",
|
|
800
|
+
type: "http"
|
|
801
|
+
}],
|
|
802
|
+
url: "/v1/ingest/attachments/{id}/complete",
|
|
803
|
+
...options
|
|
804
|
+
});
|
|
783
805
|
var knownIssuesList2 = (options) => (options?.client ?? client).get({
|
|
784
806
|
security: [{ name: "X-Innspel-Tenant-Key", type: "apiKey" }, {
|
|
785
807
|
key: "FeedbackTokenAuth",
|
|
@@ -1129,8 +1151,66 @@ function nyClientRef() {
|
|
|
1129
1151
|
return hex.join("");
|
|
1130
1152
|
}
|
|
1131
1153
|
|
|
1154
|
+
// src/vedlegg.ts
|
|
1155
|
+
var VEDLEGG_GRENSER = {
|
|
1156
|
+
maxBytes: 5242880,
|
|
1157
|
+
maxDimension: 4096
|
|
1158
|
+
};
|
|
1159
|
+
var TILLATTE_MIME = ["image/png", "image/jpeg", "image/webp"];
|
|
1160
|
+
function byteLengde(body) {
|
|
1161
|
+
if (typeof body === "object" && body !== null && "size" in body && typeof body.size === "number") {
|
|
1162
|
+
return body.size;
|
|
1163
|
+
}
|
|
1164
|
+
return body.byteLength;
|
|
1165
|
+
}
|
|
1166
|
+
function validerVedlegg(input) {
|
|
1167
|
+
if (input === null || typeof input !== "object") {
|
|
1168
|
+
throw new InnspelConfigError("attachment", "m\xE5 v\xE6re et objekt med { submissionId?, mime, body, width, height }");
|
|
1169
|
+
}
|
|
1170
|
+
if (!TILLATTE_MIME.includes(input.mime)) {
|
|
1171
|
+
throw new InnspelConfigError(
|
|
1172
|
+
"attachment.mime",
|
|
1173
|
+
`m\xE5 v\xE6re en av ${TILLATTE_MIME.join(", ")}. SVG og GIF er aldri tillatt (K9)`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
if (input.body === null || input.body === void 0) {
|
|
1177
|
+
throw new InnspelConfigError("attachment.body", "mangler \u2014 send bildet som Blob, ArrayBuffer eller Uint8Array");
|
|
1178
|
+
}
|
|
1179
|
+
const bytes = byteLengde(input.body);
|
|
1180
|
+
if (!Number.isFinite(bytes) || bytes < 1) {
|
|
1181
|
+
throw new InnspelConfigError("attachment.body", "er tom");
|
|
1182
|
+
}
|
|
1183
|
+
if (bytes > VEDLEGG_GRENSER.maxBytes) {
|
|
1184
|
+
throw new InnspelConfigError(
|
|
1185
|
+
"attachment.body",
|
|
1186
|
+
`er ${bytes} byte, over taket p\xE5 ${VEDLEGG_GRENSER.maxBytes} (5 MB). Reduser kvaliteten eller oppl\xF8sningen ved fangst`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
for (const felt of ["width", "height"]) {
|
|
1190
|
+
const v = input[felt];
|
|
1191
|
+
if (!Number.isInteger(v) || v < 1 || v > VEDLEGG_GRENSER.maxDimension) {
|
|
1192
|
+
throw new InnspelConfigError(
|
|
1193
|
+
`attachment.${felt}`,
|
|
1194
|
+
`m\xE5 v\xE6re et heltall mellom 1 og ${VEDLEGG_GRENSER.maxDimension} (K9)`
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
async function lastOppTilLagring(fetchImpl, presign, body, signal) {
|
|
1200
|
+
const headers = {};
|
|
1201
|
+
for (const h of presign.headers) headers[h.name] = h.value;
|
|
1202
|
+
const res = await fetchImpl(new Request(presign.uploadUrl, { method: "PUT", headers, body, signal }));
|
|
1203
|
+
if (!res.ok) {
|
|
1204
|
+
throw new InnspelApiError(
|
|
1205
|
+
res.status,
|
|
1206
|
+
"unknown",
|
|
1207
|
+
`Lagringen avviste opplastingen med status ${res.status}. Bildet ble ikke lagt ved.`
|
|
1208
|
+
);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1132
1212
|
// src/versjon.ts
|
|
1133
|
-
var VERSJON = "0.
|
|
1213
|
+
var VERSJON = "0.2.0";
|
|
1134
1214
|
|
|
1135
1215
|
// src/klient.ts
|
|
1136
1216
|
var STANDARD_TIMEOUT_MS = 1e4;
|
|
@@ -1272,6 +1352,31 @@ function init(options) {
|
|
|
1272
1352
|
})
|
|
1273
1353
|
);
|
|
1274
1354
|
}
|
|
1355
|
+
async function lastOppVedlegg(submissionId, vedlegg) {
|
|
1356
|
+
const presign = await medToken(
|
|
1357
|
+
(h) => attachmentsPresign({
|
|
1358
|
+
client: klient,
|
|
1359
|
+
headers: h,
|
|
1360
|
+
signal: signal(),
|
|
1361
|
+
body: {
|
|
1362
|
+
submissionId,
|
|
1363
|
+
mime: vedlegg.mime,
|
|
1364
|
+
bytes: byteLengde(vedlegg.body),
|
|
1365
|
+
width: vedlegg.width,
|
|
1366
|
+
height: vedlegg.height
|
|
1367
|
+
}
|
|
1368
|
+
})
|
|
1369
|
+
);
|
|
1370
|
+
await lastOppTilLagring(options.fetch ?? fetch, presign, vedlegg.body, signal());
|
|
1371
|
+
return medToken(
|
|
1372
|
+
(h) => attachmentsComplete({
|
|
1373
|
+
client: klient,
|
|
1374
|
+
headers: h,
|
|
1375
|
+
signal: signal(),
|
|
1376
|
+
path: { id: presign.attachmentId }
|
|
1377
|
+
})
|
|
1378
|
+
);
|
|
1379
|
+
}
|
|
1275
1380
|
const flushK\u00F8 = () => flushKo(
|
|
1276
1381
|
k\u00F8,
|
|
1277
1382
|
(rad) => sendN\u00E5({ type: rad.type, text: rad.text }, rad.clientRef, kontekst),
|
|
@@ -1297,17 +1402,12 @@ function init(options) {
|
|
|
1297
1402
|
);
|
|
1298
1403
|
}
|
|
1299
1404
|
if (input.context) validerKontekst(input.context, "capture.context", false);
|
|
1405
|
+
if (input.attachment) validerVedlegg(input.attachment);
|
|
1300
1406
|
const clientRef = input.clientRef ?? nyClientRef();
|
|
1301
1407
|
const kontekstForKall = sl\u00E5SammenKontekst(options.context, input.context, VERSJON);
|
|
1408
|
+
let submission;
|
|
1302
1409
|
try {
|
|
1303
|
-
|
|
1304
|
-
telemetri.emit({
|
|
1305
|
-
name: "submission_sent",
|
|
1306
|
-
type: input.type,
|
|
1307
|
-
hasAttachment: false,
|
|
1308
|
-
offlineQueued: false
|
|
1309
|
-
});
|
|
1310
|
-
return { status: "sent", submission };
|
|
1410
|
+
submission = await sendN\u00E5(input, clientRef, kontekstForKall);
|
|
1311
1411
|
} catch (e) {
|
|
1312
1412
|
if (e instanceof InnspelApiError) {
|
|
1313
1413
|
telemetri.emit({ name: "submission_failed", reason: e.code });
|
|
@@ -1325,8 +1425,23 @@ function init(options) {
|
|
|
1325
1425
|
hasAttachment: false,
|
|
1326
1426
|
offlineQueued: true
|
|
1327
1427
|
});
|
|
1328
|
-
return { status: "queued", clientRef };
|
|
1428
|
+
return input.attachment ? { status: "queued", clientRef, attachment: { status: "dropped" } } : { status: "queued", clientRef };
|
|
1429
|
+
}
|
|
1430
|
+
let attachment;
|
|
1431
|
+
if (input.attachment) {
|
|
1432
|
+
try {
|
|
1433
|
+
attachment = { status: "uploaded", attachment: await lastOppVedlegg(submission.id, input.attachment) };
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
attachment = { status: "failed", error: e instanceof Error ? e : new Error(String(e)) };
|
|
1436
|
+
}
|
|
1329
1437
|
}
|
|
1438
|
+
telemetri.emit({
|
|
1439
|
+
name: "submission_sent",
|
|
1440
|
+
type: input.type,
|
|
1441
|
+
hasAttachment: attachment?.status === "uploaded",
|
|
1442
|
+
offlineQueued: false
|
|
1443
|
+
});
|
|
1444
|
+
return attachment ? { status: "sent", submission, attachment } : { status: "sent", submission };
|
|
1330
1445
|
},
|
|
1331
1446
|
knownIssues: {
|
|
1332
1447
|
async for(query = {}) {
|
|
@@ -1406,6 +1521,15 @@ function init(options) {
|
|
|
1406
1521
|
);
|
|
1407
1522
|
}
|
|
1408
1523
|
},
|
|
1524
|
+
attachments: {
|
|
1525
|
+
async upload(submissionId, attachment) {
|
|
1526
|
+
if (typeof submissionId !== "string" || submissionId.length === 0) {
|
|
1527
|
+
throw new InnspelConfigError("submissionId", "mangler \u2014 bruk id fra innsendingen capture() ga");
|
|
1528
|
+
}
|
|
1529
|
+
validerVedlegg(attachment);
|
|
1530
|
+
return lastOppVedlegg(submissionId, attachment);
|
|
1531
|
+
}
|
|
1532
|
+
},
|
|
1409
1533
|
queue: {
|
|
1410
1534
|
pending: () => k\u00F8.les(),
|
|
1411
1535
|
flush: flushK\u00F8
|
|
@@ -1429,13 +1553,72 @@ function init(options) {
|
|
|
1429
1553
|
return innspel;
|
|
1430
1554
|
}
|
|
1431
1555
|
|
|
1556
|
+
// src/maskering.ts
|
|
1557
|
+
function strokTilRekter(punkter, penselBredde, bilde) {
|
|
1558
|
+
if (penselBredde <= 0 || punkter.length === 0) return [];
|
|
1559
|
+
const halv = penselBredde / 2;
|
|
1560
|
+
const ut = [];
|
|
1561
|
+
const legg = (p) => {
|
|
1562
|
+
const r = klipp({ x: p.x - halv, y: p.y - halv, width: penselBredde, height: penselBredde }, bilde);
|
|
1563
|
+
if (r) ut.push(r);
|
|
1564
|
+
};
|
|
1565
|
+
legg(punkter[0]);
|
|
1566
|
+
for (let i = 1; i < punkter.length; i++) {
|
|
1567
|
+
const a = punkter[i - 1];
|
|
1568
|
+
const b = punkter[i];
|
|
1569
|
+
const avstand = Math.hypot(b.x - a.x, b.y - a.y);
|
|
1570
|
+
const steg = Math.max(1, Math.ceil(avstand / halv));
|
|
1571
|
+
for (let s = 1; s <= steg; s++) {
|
|
1572
|
+
legg({ x: a.x + (b.x - a.x) * s / steg, y: a.y + (b.y - a.y) * s / steg });
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
return ut;
|
|
1576
|
+
}
|
|
1577
|
+
function trykkTilRekt(punkt, boks, bilde) {
|
|
1578
|
+
return klipp({ x: punkt.x - boks.width / 2, y: punkt.y - boks.height / 2, width: boks.width, height: boks.height }, bilde);
|
|
1579
|
+
}
|
|
1580
|
+
function klipp(r, bilde) {
|
|
1581
|
+
const x1 = Math.max(0, r.x);
|
|
1582
|
+
const y1 = Math.max(0, r.y);
|
|
1583
|
+
const x2 = Math.min(bilde.width, r.x + r.width);
|
|
1584
|
+
const y2 = Math.min(bilde.height, r.y + r.height);
|
|
1585
|
+
if (x2 <= x1 || y2 <= y1) return null;
|
|
1586
|
+
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
|
1587
|
+
}
|
|
1588
|
+
function skaler(r, fra, til) {
|
|
1589
|
+
const sx = til.width / fra.width;
|
|
1590
|
+
const sy = til.height / fra.height;
|
|
1591
|
+
const x = Math.floor(r.x * sx);
|
|
1592
|
+
const y = Math.floor(r.y * sy);
|
|
1593
|
+
return {
|
|
1594
|
+
x,
|
|
1595
|
+
y,
|
|
1596
|
+
width: Math.ceil((r.x + r.width) * sx) - x,
|
|
1597
|
+
height: Math.ceil((r.y + r.height) * sy) - y
|
|
1598
|
+
};
|
|
1599
|
+
}
|
|
1600
|
+
function treffer(punkt, r) {
|
|
1601
|
+
return punkt.x >= r.x && punkt.x < r.x + r.width && punkt.y >= r.y && punkt.y < r.y + r.height;
|
|
1602
|
+
}
|
|
1603
|
+
function dekkerAlt(rekter, bilde) {
|
|
1604
|
+
return rekter.some((r) => r.x <= 0 && r.y <= 0 && r.x + r.width >= bilde.width && r.y + r.height >= bilde.height);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1432
1607
|
exports.InnspelApiError = InnspelApiError;
|
|
1433
1608
|
exports.InnspelConfigError = InnspelConfigError;
|
|
1434
1609
|
exports.InnspelQueueFullError = InnspelQueueFullError;
|
|
1435
1610
|
exports.KONTEKST_FELTER = KONTEKST_FELTER;
|
|
1436
1611
|
exports.KO_TAK = KO_TAK;
|
|
1612
|
+
exports.TILLATTE_MIME = TILLATTE_MIME;
|
|
1613
|
+
exports.VEDLEGG_GRENSER = VEDLEGG_GRENSER;
|
|
1437
1614
|
exports.VERSJON = VERSJON;
|
|
1615
|
+
exports.dekkerAlt = dekkerAlt;
|
|
1438
1616
|
exports.init = init;
|
|
1617
|
+
exports.klipp = klipp;
|
|
1439
1618
|
exports.scanText = scanText;
|
|
1619
|
+
exports.skaler = skaler;
|
|
1620
|
+
exports.strokTilRekter = strokTilRekter;
|
|
1621
|
+
exports.treffer = treffer;
|
|
1622
|
+
exports.trykkTilRekt = trykkTilRekt;
|
|
1440
1623
|
//# sourceMappingURL=index.cjs.map
|
|
1441
1624
|
//# sourceMappingURL=index.cjs.map
|