@particle-academy/fancy-connector-core 0.11.0 → 0.12.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 +32 -0
- package/dist/index.cjs +43 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -2
- package/dist/index.d.ts +58 -2
- package/dist/index.js +41 -2
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ 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.12.0] - 2026-11-01
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **A signed REQUEST LINE and millisecond timestamps (HubSpot v3).** HubSpot
|
|
14
|
+
signs `base64(HMAC-SHA256(client secret, method + uri + body + timestamp))`,
|
|
15
|
+
sends `X-HubSpot-Request-Timestamp` in MILLISECONDS, and signs the URI with
|
|
16
|
+
only its QUERY portion percent-decoded for twelve escapes (never the leading
|
|
17
|
+
`?`, any `#fragment` dropped). The pieces:
|
|
18
|
+
- `InboundDelivery` gains optional `method` and `url`; pass the request
|
|
19
|
+
line exactly as it arrived.
|
|
20
|
+
- `HmacScheme` gains `timestampUnit` (`"s"` | `"ms"`), `signsRequestLine`
|
|
21
|
+
(a delivery without its method or URL is refused by name: `delivery
|
|
22
|
+
carried no request method or url`), and `uri: "hubspot-v3"`.
|
|
23
|
+
- The CORE normalises the URI (`normaliseSignedUri` /
|
|
24
|
+
`WebhookVerifier::normaliseSignedUri`; the rules ship as data, in
|
|
25
|
+
`SIGNED_URI_RULES`), and a scheme's `payload` receives the request line
|
|
26
|
+
as a fourth argument.
|
|
27
|
+
- PHP's new `verify()` parameters are appended, so existing calls are
|
|
28
|
+
unchanged.
|
|
29
|
+
|
|
30
|
+
The signed-delivery corpus gains four HubSpot rows, led by HubSpot's
|
|
31
|
+
PUBLISHED worked example, which reproduces from its inputs.
|
|
32
|
+
- **`TriggerDescriptor.route` `{by, path}`.** It names the one key a delivery
|
|
33
|
+
that reaches ONE endpoint for many tenants may be routed by (HubSpot's
|
|
34
|
+
`portalId`; Gmail's `emailAddress`). The rule is fixed: route only after
|
|
35
|
+
verification, only to a connection or watch the host minted, and drop an
|
|
36
|
+
unmapped key with a 2xx and a log line, creating nothing. The package
|
|
37
|
+
declares; the host routes.
|
|
38
|
+
|
|
39
|
+
`CONNECTOR_API_VERSION` is unchanged: everything here is optional.
|
|
40
|
+
|
|
9
41
|
## [0.11.0] - 2026-10-28
|
|
10
42
|
|
|
11
43
|
### Added
|
package/dist/index.cjs
CHANGED
|
@@ -49,6 +49,7 @@ __export(src_exports, {
|
|
|
49
49
|
RENDERER_VERSION: () => RENDERER_VERSION,
|
|
50
50
|
REVIEW_STALE_DAYS: () => REVIEW_STALE_DAYS,
|
|
51
51
|
SANDBOX_KINDS: () => SANDBOX_KINDS,
|
|
52
|
+
SIGNED_URI_RULES: () => SIGNED_URI_RULES,
|
|
52
53
|
SUPPORTED_CONNECTOR_API: () => SUPPORTED_CONNECTOR_API,
|
|
53
54
|
assertConnectorApi: () => assertConnectorApi,
|
|
54
55
|
byteRangeOf: () => byteRangeOf,
|
|
@@ -88,6 +89,7 @@ __export(src_exports, {
|
|
|
88
89
|
linkRanges: () => linkRanges,
|
|
89
90
|
measure: () => measure,
|
|
90
91
|
nextOccurrence: () => nextOccurrence,
|
|
92
|
+
normaliseSignedUri: () => normaliseSignedUri,
|
|
91
93
|
parseInvite: () => parseInvite,
|
|
92
94
|
parseMime: () => parseMime,
|
|
93
95
|
payloadHash: () => payloadHash,
|
|
@@ -2081,6 +2083,29 @@ var PROBLEM_SEVERITIES = ["block", "warn"];
|
|
|
2081
2083
|
var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
|
|
2082
2084
|
|
|
2083
2085
|
// src/webhook.ts
|
|
2086
|
+
var SIGNED_URI_RULES = ["hubspot-v3"];
|
|
2087
|
+
var HUBSPOT_V3_DECODE = {
|
|
2088
|
+
"%3A": ":",
|
|
2089
|
+
"%2F": "/",
|
|
2090
|
+
"%3F": "?",
|
|
2091
|
+
"%40": "@",
|
|
2092
|
+
"%21": "!",
|
|
2093
|
+
"%24": "$",
|
|
2094
|
+
"%27": "'",
|
|
2095
|
+
"%28": "(",
|
|
2096
|
+
"%29": ")",
|
|
2097
|
+
"%2A": "*",
|
|
2098
|
+
"%2C": ",",
|
|
2099
|
+
"%3B": ";"
|
|
2100
|
+
};
|
|
2101
|
+
function normaliseSignedUri(url, rule) {
|
|
2102
|
+
if (rule !== "hubspot-v3") throw new Error(`unknown signed-uri rule ${JSON.stringify(rule)}`);
|
|
2103
|
+
const withoutFragment = url.split("#", 1)[0] ?? "";
|
|
2104
|
+
const q = withoutFragment.indexOf("?");
|
|
2105
|
+
if (q === -1) return withoutFragment;
|
|
2106
|
+
const query = withoutFragment.slice(q + 1).replace(/%[0-9A-Fa-f]{2}/g, (e) => HUBSPOT_V3_DECODE[e.toUpperCase()] ?? e);
|
|
2107
|
+
return `${withoutFragment.slice(0, q)}?${query}`;
|
|
2108
|
+
}
|
|
2084
2109
|
function secretKeyBytes(secret, options = {}) {
|
|
2085
2110
|
let material = secret;
|
|
2086
2111
|
if (options.secretPrefix !== void 0) {
|
|
@@ -2115,13 +2140,26 @@ async function verifyHmac(options) {
|
|
|
2115
2140
|
const sent = Number(timestamp);
|
|
2116
2141
|
if (!Number.isFinite(sent)) return { ok: false, reason: "timestamp header is not a number" };
|
|
2117
2142
|
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2118
|
-
|
|
2143
|
+
const sentSeconds = scheme.timestampUnit === "ms" ? sent / 1e3 : sent;
|
|
2144
|
+
if (Math.abs(now - sentSeconds) > scheme.tolerance) {
|
|
2119
2145
|
return { ok: false, reason: `delivery is outside the ${scheme.tolerance}s replay window` };
|
|
2120
2146
|
}
|
|
2121
2147
|
}
|
|
2122
2148
|
const key = secretKeyBytes(secret, scheme);
|
|
2123
2149
|
if ("reason" in key) return { ok: false, reason: key.reason };
|
|
2124
|
-
|
|
2150
|
+
let line;
|
|
2151
|
+
if (scheme.signsRequestLine) {
|
|
2152
|
+
const { method, url } = options.request ?? {};
|
|
2153
|
+
if (!method || !url) return { ok: false, reason: "delivery carried no request method or url" };
|
|
2154
|
+
line = { method, uri: scheme.uri ? normaliseSignedUri(url, scheme.uri) : url };
|
|
2155
|
+
}
|
|
2156
|
+
const expected = await hmac(
|
|
2157
|
+
secret,
|
|
2158
|
+
scheme.payload(raw, timestamp, id, line),
|
|
2159
|
+
scheme.algorithm,
|
|
2160
|
+
scheme.encoding ?? "hex",
|
|
2161
|
+
scheme
|
|
2162
|
+
);
|
|
2125
2163
|
return signatures.some((candidate) => constantTimeEquals(expected, candidate)) ? { ok: true } : { ok: false, reason: "signature did not match" };
|
|
2126
2164
|
}
|
|
2127
2165
|
async function hmac(secret, payload, algorithm, encoding = "hex", options = {}) {
|
|
@@ -2246,6 +2284,7 @@ async function verifyDelivery(trigger, delivery, secret, now) {
|
|
|
2246
2284
|
scheme: spec.scheme,
|
|
2247
2285
|
timestamp,
|
|
2248
2286
|
id,
|
|
2287
|
+
...delivery.method === void 0 && delivery.url === void 0 ? {} : { request: { method: delivery.method ?? "", url: delivery.url ?? "" } },
|
|
2249
2288
|
...now === void 0 ? {} : { now }
|
|
2250
2289
|
});
|
|
2251
2290
|
}
|
|
@@ -2298,6 +2337,7 @@ function hasDelivery(value) {
|
|
|
2298
2337
|
RENDERER_VERSION,
|
|
2299
2338
|
REVIEW_STALE_DAYS,
|
|
2300
2339
|
SANDBOX_KINDS,
|
|
2340
|
+
SIGNED_URI_RULES,
|
|
2301
2341
|
SUPPORTED_CONNECTOR_API,
|
|
2302
2342
|
assertConnectorApi,
|
|
2303
2343
|
byteRangeOf,
|
|
@@ -2337,6 +2377,7 @@ function hasDelivery(value) {
|
|
|
2337
2377
|
linkRanges,
|
|
2338
2378
|
measure,
|
|
2339
2379
|
nextOccurrence,
|
|
2380
|
+
normaliseSignedUri,
|
|
2340
2381
|
parseInvite,
|
|
2341
2382
|
parseMime,
|
|
2342
2383
|
payloadHash,
|