@particle-academy/fancy-connector-core 0.10.0 → 0.11.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/CHANGELOG.md +49 -0
- package/dist/index.cjs +88 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +52 -13
- package/dist/index.d.ts +52 -13
- package/dist/index.js +83 -1
- package/dist/index.js.map +1 -1
- package/package.json +69 -69
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,55 @@ All notable changes to `fancy-connector-core` are documented here, in
|
|
|
6
6
|
**This package is pre-1.0, so breaking changes land in MINOR releases.** The
|
|
7
7
|
version number is not a promise it can keep yet; the entries below are.
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-10-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`decodeFormFieldJson` / `FormFieldJson::decode`: the form-field-json
|
|
14
|
+
payload transform.** It reads a form-encoded body whose one named field
|
|
15
|
+
holds JSON, which is how Slack posts every interactivity callback
|
|
16
|
+
(`payload=<json>`). It runs AFTER signature verification, over the same raw
|
|
17
|
+
bytes. `+` and `%20` are both spaces. Every `%` must be `%XX` across the
|
|
18
|
+
WHOLE body, and the bytes must be UTF-8. A missing, doubled or malformed
|
|
19
|
+
field, or one that is not JSON, is refused BY NAME (`missing-field`,
|
|
20
|
+
`duplicate-field`, `bad-encoding`, `not-json`; `FormFieldJsonError.reason` /
|
|
21
|
+
`FormFieldJsonException->reason`). Neither runtime uses its built-in
|
|
22
|
+
decoder, because `decodeURIComponent` throws on `%zz` where `urldecode`
|
|
23
|
+
silently keeps it, and a field sent twice is refused rather than resolved
|
|
24
|
+
first-wins or last-wins. Both runtimes reproduce the hand-authored corpus
|
|
25
|
+
under `fixtures/form-field-json/`. In PHP, JSON objects come back as
|
|
26
|
+
arrays, so an empty `{}` is `[]`, as in `Mime::toArray()`.
|
|
27
|
+
- **`TriggerDescriptor.decode` + `decodeDelivery` / `DeliveryDecode::apply`.**
|
|
28
|
+
A webhook trigger may declare how its verified body is read:
|
|
29
|
+
`{kind: "form-field-json", field: "payload"}`. A host verifies the raw
|
|
30
|
+
bytes, then calls the dispatcher with the declaration, then starts the run.
|
|
31
|
+
An unknown kind is refused by name, never passed through as raw bytes. The
|
|
32
|
+
kinds ship as data (`DELIVERY_DECODE_KINDS` / `DeliveryDecode::KINDS`).
|
|
33
|
+
Absent `decode` means a host reads the body as it always has.
|
|
34
|
+
|
|
35
|
+
`CONNECTOR_API_VERSION` is unchanged: an optional descriptor field changes
|
|
36
|
+
nothing a connector must implement.
|
|
37
|
+
|
|
38
|
+
## [0.10.1] - 2026-10-10
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`linkRanges`'s trailing-punctuation trim was quadratic (CodeQL
|
|
43
|
+
`js/polynomial-redos`, HIGH).** `match[0].replace(/[.,;:!?)\]}'"]+$/, "")`
|
|
44
|
+
is unanchored at the start, so V8 retries the trailing run from every
|
|
45
|
+
position inside it — measured at 344ms/20k, 1.2s/40k, 5.2s/80k adversarial
|
|
46
|
+
characters, clean quadratic. `linkRanges(text)` runs on arbitrary
|
|
47
|
+
post/message text, not an operator-typed value, so this is reachable from
|
|
48
|
+
untrusted input, not just a slow edge case. Replaced with a backward scan
|
|
49
|
+
over a `Set` of trailing punctuation characters — linear by construction,
|
|
50
|
+
each step only decrements an index. Trim behaviour is unchanged and pinned
|
|
51
|
+
by a 9-case table before the mechanism changed; a growth-ratio test (4x the
|
|
52
|
+
input must cost well under 10x, not the ~18x the old code showed) guards
|
|
53
|
+
against this coming back. PHP's mirror (`Text.php`) uses the same
|
|
54
|
+
character class but was checked directly and does not exhibit the
|
|
55
|
+
blowup — PCRE is linear here, so only the TypeScript side changes. Found
|
|
56
|
+
and measured by Fancy; `CONNECTOR_API_VERSION` is unchanged.
|
|
57
|
+
|
|
9
58
|
## [0.10.0] - 2026-09-16
|
|
10
59
|
|
|
11
60
|
Tynn #12 (MOIC's meeting bot): an RFC 5545 invite parser, written from
|
package/dist/index.cjs
CHANGED
|
@@ -38,8 +38,11 @@ __export(src_exports, {
|
|
|
38
38
|
ConnectorUnreachable: () => ConnectorUnreachable,
|
|
39
39
|
DEFAULT_IDEMPOTENCY_WINDOW_SECONDS: () => DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
|
|
40
40
|
DEFAULT_RETRY: () => DEFAULT_RETRY,
|
|
41
|
+
DELIVERY_DECODE_KINDS: () => DELIVERY_DECODE_KINDS,
|
|
41
42
|
EXPIRES_AT_UNITS: () => EXPIRES_AT_UNITS,
|
|
42
43
|
FAKE_EPOCH: () => FAKE_EPOCH,
|
|
44
|
+
FORM_FIELD_JSON_REASONS: () => FORM_FIELD_JSON_REASONS,
|
|
45
|
+
FormFieldJsonError: () => FormFieldJsonError,
|
|
43
46
|
MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
44
47
|
NO_IDEMPOTENCY_KEY_WARNING: () => NO_IDEMPOTENCY_KEY_WARNING,
|
|
45
48
|
PROBLEM_SEVERITIES: () => PROBLEM_SEVERITIES,
|
|
@@ -60,6 +63,8 @@ __export(src_exports, {
|
|
|
60
63
|
compareShape: () => compareShape,
|
|
61
64
|
connectionsFor: () => connectionsFor,
|
|
62
65
|
constantTimeEquals: () => constantTimeEquals,
|
|
66
|
+
decodeDelivery: () => decodeDelivery,
|
|
67
|
+
decodeFormFieldJson: () => decodeFormFieldJson,
|
|
63
68
|
deliver: () => deliver,
|
|
64
69
|
fakeRequest: () => fakeRequest,
|
|
65
70
|
fakeValues: () => fakeValues,
|
|
@@ -1789,6 +1794,77 @@ function parseMime(raw) {
|
|
|
1789
1794
|
};
|
|
1790
1795
|
}
|
|
1791
1796
|
|
|
1797
|
+
// src/form-field-json.ts
|
|
1798
|
+
var FORM_FIELD_JSON_REASONS = ["missing-field", "duplicate-field", "bad-encoding", "not-json"];
|
|
1799
|
+
var FormFieldJsonError = class extends Error {
|
|
1800
|
+
constructor(reason, field, message) {
|
|
1801
|
+
super(message);
|
|
1802
|
+
this.reason = reason;
|
|
1803
|
+
this.field = field;
|
|
1804
|
+
this.name = "FormFieldJsonError";
|
|
1805
|
+
}
|
|
1806
|
+
reason;
|
|
1807
|
+
field;
|
|
1808
|
+
};
|
|
1809
|
+
var UTF8 = new TextDecoder("utf-8", { fatal: true });
|
|
1810
|
+
var HEX = /^[0-9A-Fa-f]{2}$/;
|
|
1811
|
+
function decodeFormFieldJson(raw, field) {
|
|
1812
|
+
const text = typeof raw === "string" ? raw : latin12(raw);
|
|
1813
|
+
const found = [];
|
|
1814
|
+
for (const pair of text.split("&")) {
|
|
1815
|
+
if (pair === "") continue;
|
|
1816
|
+
const eq = pair.indexOf("=");
|
|
1817
|
+
const name = component(eq === -1 ? pair : pair.slice(0, eq), field);
|
|
1818
|
+
const value2 = component(eq === -1 ? "" : pair.slice(eq + 1), field);
|
|
1819
|
+
if (name === field) found.push(value2);
|
|
1820
|
+
}
|
|
1821
|
+
if (found.length === 0) {
|
|
1822
|
+
throw new FormFieldJsonError("missing-field", field, `the body has no "${field}" field`);
|
|
1823
|
+
}
|
|
1824
|
+
if (found.length > 1) {
|
|
1825
|
+
throw new FormFieldJsonError("duplicate-field", field, `the body has "${field}" ${found.length} times`);
|
|
1826
|
+
}
|
|
1827
|
+
const [value] = found;
|
|
1828
|
+
try {
|
|
1829
|
+
return JSON.parse(value);
|
|
1830
|
+
} catch (error) {
|
|
1831
|
+
throw new FormFieldJsonError("not-json", field, `"${field}" is not JSON: ${error.message}`);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
function component(encoded, field) {
|
|
1835
|
+
const bytes = [];
|
|
1836
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
1837
|
+
const char = encoded[i];
|
|
1838
|
+
if (char === "+") {
|
|
1839
|
+
bytes.push(32);
|
|
1840
|
+
} else if (char === "%") {
|
|
1841
|
+
const hex = encoded.slice(i + 1, i + 3);
|
|
1842
|
+
if (!HEX.test(hex)) {
|
|
1843
|
+
throw new FormFieldJsonError("bad-encoding", field, `a "%" at byte ${i} is not followed by two hex digits`);
|
|
1844
|
+
}
|
|
1845
|
+
bytes.push(parseInt(hex, 16));
|
|
1846
|
+
i += 2;
|
|
1847
|
+
} else {
|
|
1848
|
+
bytes.push(encoded.charCodeAt(i) & 255);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
try {
|
|
1852
|
+
return UTF8.decode(new Uint8Array(bytes));
|
|
1853
|
+
} catch {
|
|
1854
|
+
throw new FormFieldJsonError("bad-encoding", field, "the decoded bytes are not valid UTF-8");
|
|
1855
|
+
}
|
|
1856
|
+
}
|
|
1857
|
+
var DELIVERY_DECODE_KINDS = ["form-field-json"];
|
|
1858
|
+
function decodeDelivery(decode, raw) {
|
|
1859
|
+
if (decode.kind === "form-field-json") return decodeFormFieldJson(raw, decode.field);
|
|
1860
|
+
throw new Error(`unknown decode kind ${JSON.stringify(decode.kind)}`);
|
|
1861
|
+
}
|
|
1862
|
+
function latin12(bytes) {
|
|
1863
|
+
let out = "";
|
|
1864
|
+
for (const byte of bytes) out += String.fromCharCode(byte);
|
|
1865
|
+
return out;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1792
1868
|
// src/probe.ts
|
|
1793
1869
|
async function probe(spec) {
|
|
1794
1870
|
let response;
|
|
@@ -1866,11 +1942,17 @@ function sliceByteRange(text, range) {
|
|
|
1866
1942
|
const bytes = encoder.encode(text);
|
|
1867
1943
|
return new TextDecoder().decode(bytes.slice(range.byteStart, range.byteEnd));
|
|
1868
1944
|
}
|
|
1945
|
+
var TRAILING_PUNCTUATION = /* @__PURE__ */ new Set([".", ",", ";", ":", "!", "?", ")", "]", "}", "'", '"']);
|
|
1946
|
+
function trimTrailingPunctuation(raw) {
|
|
1947
|
+
let end = raw.length;
|
|
1948
|
+
while (end > 0 && TRAILING_PUNCTUATION.has(raw[end - 1])) end -= 1;
|
|
1949
|
+
return raw.slice(0, end);
|
|
1950
|
+
}
|
|
1869
1951
|
function linkRanges(text) {
|
|
1870
1952
|
const found = [];
|
|
1871
1953
|
for (const match of text.matchAll(/https?:\/\/[^\s]+/g)) {
|
|
1872
1954
|
if (match.index === void 0) continue;
|
|
1873
|
-
const url = match[0]
|
|
1955
|
+
const url = trimTrailingPunctuation(match[0]);
|
|
1874
1956
|
found.push({ ...byteRangeOf(text, match.index, url), url });
|
|
1875
1957
|
}
|
|
1876
1958
|
return found;
|
|
@@ -2205,8 +2287,11 @@ function hasDelivery(value) {
|
|
|
2205
2287
|
ConnectorUnreachable,
|
|
2206
2288
|
DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
|
|
2207
2289
|
DEFAULT_RETRY,
|
|
2290
|
+
DELIVERY_DECODE_KINDS,
|
|
2208
2291
|
EXPIRES_AT_UNITS,
|
|
2209
2292
|
FAKE_EPOCH,
|
|
2293
|
+
FORM_FIELD_JSON_REASONS,
|
|
2294
|
+
FormFieldJsonError,
|
|
2210
2295
|
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
2211
2296
|
NO_IDEMPOTENCY_KEY_WARNING,
|
|
2212
2297
|
PROBLEM_SEVERITIES,
|
|
@@ -2227,6 +2312,8 @@ function hasDelivery(value) {
|
|
|
2227
2312
|
compareShape,
|
|
2228
2313
|
connectionsFor,
|
|
2229
2314
|
constantTimeEquals,
|
|
2315
|
+
decodeDelivery,
|
|
2316
|
+
decodeFormFieldJson,
|
|
2230
2317
|
deliver,
|
|
2231
2318
|
fakeRequest,
|
|
2232
2319
|
fakeValues,
|