@particle-academy/fancy-connector-core 0.10.1 → 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 CHANGED
@@ -6,6 +6,35 @@ 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
+
9
38
  ## [0.10.1] - 2026-10-10
10
39
 
11
40
  ### Fixed
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;
@@ -2211,8 +2287,11 @@ function hasDelivery(value) {
2211
2287
  ConnectorUnreachable,
2212
2288
  DEFAULT_IDEMPOTENCY_WINDOW_SECONDS,
2213
2289
  DEFAULT_RETRY,
2290
+ DELIVERY_DECODE_KINDS,
2214
2291
  EXPIRES_AT_UNITS,
2215
2292
  FAKE_EPOCH,
2293
+ FORM_FIELD_JSON_REASONS,
2294
+ FormFieldJsonError,
2216
2295
  MAX_IDEMPOTENCY_KEY_LENGTH,
2217
2296
  NO_IDEMPOTENCY_KEY_WARNING,
2218
2297
  PROBLEM_SEVERITIES,
@@ -2233,6 +2312,8 @@ function hasDelivery(value) {
2233
2312
  compareShape,
2234
2313
  connectionsFor,
2235
2314
  constantTimeEquals,
2315
+ decodeDelivery,
2316
+ decodeFormFieldJson,
2236
2317
  deliver,
2237
2318
  fakeRequest,
2238
2319
  fakeValues,