@particle-academy/fancy-connector-core 0.0.0 → 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/CHANGELOG.md +179 -103
- package/LICENSE +21 -21
- package/README.md +200 -175
- package/dist/index.cjs +95 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +143 -9
- package/dist/index.d.ts +143 -9
- package/dist/index.js +85 -10
- package/dist/index.js.map +1 -1
- package/package.json +71 -71
package/README.md
CHANGED
|
@@ -1,175 +1,200 @@
|
|
|
1
|
-
# fancy-connector-core
|
|
2
|
-
|
|
3
|
-
**The runtime under every Fancy connector.** Modes and estates, connections, one
|
|
4
|
-
call path, a failure taxonomy that knows the difference between *it never
|
|
5
|
-
arrived* and *nobody can tell*, deterministic fakers, webhook verification, pure
|
|
6
|
-
versioned rendering, chain posting, declared metric shapes, credential-free
|
|
7
|
-
probes, and API-drift reporting.
|
|
8
|
-
|
|
9
|
-
Matched **TypeScript + PHP**, one repository, **zero runtime dependencies in
|
|
10
|
-
either ecosystem**.
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
npm install @particle-academy/fancy-connector-core
|
|
14
|
-
composer require particle-academy/fancy-connector-core
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
---
|
|
18
|
-
|
|
19
|
-
## It owns the wire. It never owns a gate.
|
|
20
|
-
|
|
21
|
-
Approval, liveness, the approved-bytes comparison, consent, second review and
|
|
22
|
-
every journal belong to the **host** — because each is enforced in one place and
|
|
23
|
-
every connector inherits it from the host's dispatch path rather than
|
|
24
|
-
implementing it.
|
|
25
|
-
|
|
26
|
-
```ts
|
|
27
|
-
await connector.call(target, { dryRun, credentials });
|
|
28
|
-
// ^^^^^^ ^^^^^^^^^^^
|
|
29
|
-
// yours yours
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Three of those promises are **tests**, not intentions:
|
|
33
|
-
|
|
34
|
-
1. **Nothing here reads the environment.** No `process.env`, no `getenv`. Your
|
|
35
|
-
credential storage discipline — a gitignored file behind a whitelist, a secret
|
|
36
|
-
manager, Laravel config — stays yours, because the package cannot reach past
|
|
37
|
-
it.
|
|
38
|
-
2. **Nothing here contacts a URL of its own.** No telemetry, no phone-home, no
|
|
39
|
-
default endpoint. Even the drift checker does not fetch: you fetch, and pass
|
|
40
|
-
the document in.
|
|
41
|
-
3. **Nothing here retries an ambiguous failure** unless the connector declared
|
|
42
|
-
that repeating a request is harmless.
|
|
43
|
-
|
|
44
|
-
---
|
|
45
|
-
|
|
46
|
-
## The one that matters: retry, without ever sending twice
|
|
47
|
-
|
|
48
|
-
A network hiccup should not lose a call. A retry after a request that succeeded
|
|
49
|
-
and whose response was lost is a **double write** — the exact failure an
|
|
50
|
-
idempotency gate exists to prevent, arriving through the door marked
|
|
51
|
-
*reliability*.
|
|
52
|
-
|
|
53
|
-
Nearly every retry helper reconciles those the same way and loses: it catches an
|
|
54
|
-
error, waits, and tries again, because from the caller's side all failures look
|
|
55
|
-
alike.
|
|
56
|
-
|
|
57
|
-
| what happened | did the provider receive it? | retry? |
|
|
58
|
-
|---|---|---|
|
|
59
|
-
| DNS failure, connection refused | **no** | always safe |
|
|
60
|
-
| HTTP 429 / 5xx | **yes**, and it explicitly refused | safe — it says it did nothing |
|
|
61
|
-
| timeout, abort, **anything unrecognised** | **unknown** | only where the connector is idempotent |
|
|
62
|
-
| other 4xx | yes, and the answer was a real no | never |
|
|
63
|
-
|
|
64
|
-
```ts
|
|
65
|
-
import { classifyError, shouldRetry } from "@particle-academy/fancy-connector-core";
|
|
66
|
-
|
|
67
|
-
shouldRetry(classifyError(err).kind, { idempotent: connector.delivery.idempotent });
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`idempotent` defaults to **false**, and `delivery.why` must cite the mechanism —
|
|
71
|
-
`Idempotency-Key`, or the absence of one — because `idempotent: true` is the one
|
|
72
|
-
claim whose failure is a public duplicate.
|
|
73
|
-
|
|
74
|
-
---
|
|
75
|
-
|
|
76
|
-
## Text, and two one-line bugs that ASCII hides
|
|
77
|
-
|
|
78
|
-
```ts
|
|
79
|
-
import { measure, linkRanges, sliceByteRange } from "@particle-academy/fancy-connector-core";
|
|
80
|
-
|
|
81
|
-
measure("👍", "graphemes"); // 1 — "👍".length is 2
|
|
82
|
-
linkRanges("👍 https://example.test"); // UTF-8 BYTE ranges, not char offsets
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
`ByteRange` is a **type**, producible only by a function that encoded the string.
|
|
86
|
-
A character-offset implementation is correct for ASCII and silently corrupts the
|
|
87
|
-
*link* on any post containing an emoji — the post looks fine and goes somewhere
|
|
88
|
-
wrong.
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Rendering: pure, versioned, honest about loss
|
|
93
|
-
|
|
94
|
-
```ts
|
|
95
|
-
const payload = render(copy, {
|
|
96
|
-
limit: 300, unit: "graphemes", thread: true, links: true, label: "Bluesky",
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
payload.segments; // one entry per message; more than one means a thread
|
|
100
|
-
payload.problems; // what could NOT be fitted. Never silently applied.
|
|
101
|
-
await payloadHash(payload); // RENDERER_VERSION is INSIDE the hash
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Show the payload before you approve it, recompute it at dispatch, refuse on
|
|
105
|
-
mismatch. That is the only way "what was approved is what was sent" survives the
|
|
106
|
-
existence of adaptation — and the renderer version being inside the hash is what
|
|
107
|
-
stops it being checked separately and forgotten.
|
|
108
|
-
|
|
109
|
-
---
|
|
110
|
-
|
|
111
|
-
## Threading
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
await postChain(payload.segments.map((s) => s.text), answering, post);
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Root fixed at the top of the chain; parent advancing to whatever was just posted.
|
|
118
|
-
Reverse them and every message attaches to the first — a fan, not a thread — and
|
|
119
|
-
the provider's response looks identical either way. The post function is an
|
|
120
|
-
**argument** so that is reachable by a test.
|
|
121
|
-
|
|
122
|
-
---
|
|
123
|
-
|
|
124
|
-
## Probes: an auth error proves the transport
|
|
125
|
-
|
|
126
|
-
A connector written from documentation can be wrong in three ways that look
|
|
127
|
-
identical until a credential arrives: the URL is wrong, the request shape is
|
|
128
|
-
wrong, or the error handling never runs. **All three are invisible to a fake
|
|
129
|
-
server, because a fake server agrees with whatever your code does.**
|
|
130
|
-
|
|
131
|
-
So call the real API with a deliberately invalid credential and require an
|
|
132
|
-
auth-shaped refusal.
|
|
133
|
-
|
|
134
|
-
- `authStatuses` is declared per provider — a Discord webhook answers `404` for
|
|
135
|
-
an unknown id, so 404 IS its auth answer.
|
|
136
|
-
- **A 2xx is a failure.** The credential is reaching nothing.
|
|
137
|
-
- **Offline is `skip`, never `fail`.** A check that goes red on a train gets
|
|
138
|
-
ignored, and is then worth nothing when it goes red for real.
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## Drift: it reports, it never adapts
|
|
143
|
-
|
|
144
|
-
A connector's worst failure is silent — the provider changes its API and nothing
|
|
145
|
-
says so until something breaks.
|
|
146
|
-
|
|
147
|
-
**And the fix is not a connector that reshapes itself.** That connector cannot be
|
|
148
|
-
reviewed, its behaviour depends on when it ran, and adaptation is only ever a
|
|
149
|
-
guess at intent. So:
|
|
150
|
-
|
|
151
|
-
> Drift reports. The provider decides. A person acts.
|
|
152
|
-
|
|
153
|
-
Drift also never changes runtime behaviour: it runs out of band, and a check that
|
|
154
|
-
failed a working call because a *document* changed would be a self-inflicted
|
|
155
|
-
outage.
|
|
156
|
-
|
|
157
|
-
What is checked is a narrow projection, not a diff — *for the operations this
|
|
158
|
-
connector actually calls, do the path, method, sent fields and read fields still
|
|
159
|
-
exist?* Where a provider publishes no spec (most do not), the fallback is a
|
|
160
|
-
recorded shape: field **names** only.
|
|
161
|
-
|
|
162
|
-
`outcome` is `clean` | `drifted` | **`unchecked`**. The third is never collapsed
|
|
163
|
-
into the first — a checker that could not see is not a checker that saw nothing
|
|
164
|
-
wrong.
|
|
165
|
-
|
|
166
|
-
---
|
|
167
|
-
|
|
168
|
-
##
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
1
|
+
# fancy-connector-core
|
|
2
|
+
|
|
3
|
+
**The runtime under every Fancy connector.** Modes and estates, connections, one
|
|
4
|
+
call path, a failure taxonomy that knows the difference between *it never
|
|
5
|
+
arrived* and *nobody can tell*, deterministic fakers, webhook verification, pure
|
|
6
|
+
versioned rendering, chain posting, declared metric shapes, credential-free
|
|
7
|
+
probes, and API-drift reporting.
|
|
8
|
+
|
|
9
|
+
Matched **TypeScript + PHP**, one repository, **zero runtime dependencies in
|
|
10
|
+
either ecosystem**.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @particle-academy/fancy-connector-core
|
|
14
|
+
composer require particle-academy/fancy-connector-core
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## It owns the wire. It never owns a gate.
|
|
20
|
+
|
|
21
|
+
Approval, liveness, the approved-bytes comparison, consent, second review and
|
|
22
|
+
every journal belong to the **host** — because each is enforced in one place and
|
|
23
|
+
every connector inherits it from the host's dispatch path rather than
|
|
24
|
+
implementing it.
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
await connector.call(target, { dryRun, credentials });
|
|
28
|
+
// ^^^^^^ ^^^^^^^^^^^
|
|
29
|
+
// yours yours
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Three of those promises are **tests**, not intentions:
|
|
33
|
+
|
|
34
|
+
1. **Nothing here reads the environment.** No `process.env`, no `getenv`. Your
|
|
35
|
+
credential storage discipline — a gitignored file behind a whitelist, a secret
|
|
36
|
+
manager, Laravel config — stays yours, because the package cannot reach past
|
|
37
|
+
it.
|
|
38
|
+
2. **Nothing here contacts a URL of its own.** No telemetry, no phone-home, no
|
|
39
|
+
default endpoint. Even the drift checker does not fetch: you fetch, and pass
|
|
40
|
+
the document in.
|
|
41
|
+
3. **Nothing here retries an ambiguous failure** unless the connector declared
|
|
42
|
+
that repeating a request is harmless.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## The one that matters: retry, without ever sending twice
|
|
47
|
+
|
|
48
|
+
A network hiccup should not lose a call. A retry after a request that succeeded
|
|
49
|
+
and whose response was lost is a **double write** — the exact failure an
|
|
50
|
+
idempotency gate exists to prevent, arriving through the door marked
|
|
51
|
+
*reliability*.
|
|
52
|
+
|
|
53
|
+
Nearly every retry helper reconciles those the same way and loses: it catches an
|
|
54
|
+
error, waits, and tries again, because from the caller's side all failures look
|
|
55
|
+
alike.
|
|
56
|
+
|
|
57
|
+
| what happened | did the provider receive it? | retry? |
|
|
58
|
+
|---|---|---|
|
|
59
|
+
| DNS failure, connection refused | **no** | always safe |
|
|
60
|
+
| HTTP 429 / 5xx | **yes**, and it explicitly refused | safe — it says it did nothing |
|
|
61
|
+
| timeout, abort, **anything unrecognised** | **unknown** | only where the connector is idempotent |
|
|
62
|
+
| other 4xx | yes, and the answer was a real no | never |
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { classifyError, shouldRetry } from "@particle-academy/fancy-connector-core";
|
|
66
|
+
|
|
67
|
+
shouldRetry(classifyError(err).kind, { idempotent: connector.delivery.idempotent });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`idempotent` defaults to **false**, and `delivery.why` must cite the mechanism —
|
|
71
|
+
`Idempotency-Key`, or the absence of one — because `idempotent: true` is the one
|
|
72
|
+
claim whose failure is a public duplicate.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Text, and two one-line bugs that ASCII hides
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { measure, linkRanges, sliceByteRange } from "@particle-academy/fancy-connector-core";
|
|
80
|
+
|
|
81
|
+
measure("👍", "graphemes"); // 1 — "👍".length is 2
|
|
82
|
+
linkRanges("👍 https://example.test"); // UTF-8 BYTE ranges, not char offsets
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`ByteRange` is a **type**, producible only by a function that encoded the string.
|
|
86
|
+
A character-offset implementation is correct for ASCII and silently corrupts the
|
|
87
|
+
*link* on any post containing an emoji — the post looks fine and goes somewhere
|
|
88
|
+
wrong.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Rendering: pure, versioned, honest about loss
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const payload = render(copy, {
|
|
96
|
+
limit: 300, unit: "graphemes", thread: true, links: true, label: "Bluesky",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
payload.segments; // one entry per message; more than one means a thread
|
|
100
|
+
payload.problems; // what could NOT be fitted. Never silently applied.
|
|
101
|
+
await payloadHash(payload); // RENDERER_VERSION is INSIDE the hash
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Show the payload before you approve it, recompute it at dispatch, refuse on
|
|
105
|
+
mismatch. That is the only way "what was approved is what was sent" survives the
|
|
106
|
+
existence of adaptation — and the renderer version being inside the hash is what
|
|
107
|
+
stops it being checked separately and forgotten.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Threading
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
await postChain(payload.segments.map((s) => s.text), answering, post);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Root fixed at the top of the chain; parent advancing to whatever was just posted.
|
|
118
|
+
Reverse them and every message attaches to the first — a fan, not a thread — and
|
|
119
|
+
the provider's response looks identical either way. The post function is an
|
|
120
|
+
**argument** so that is reachable by a test.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Probes: an auth error proves the transport
|
|
125
|
+
|
|
126
|
+
A connector written from documentation can be wrong in three ways that look
|
|
127
|
+
identical until a credential arrives: the URL is wrong, the request shape is
|
|
128
|
+
wrong, or the error handling never runs. **All three are invisible to a fake
|
|
129
|
+
server, because a fake server agrees with whatever your code does.**
|
|
130
|
+
|
|
131
|
+
So call the real API with a deliberately invalid credential and require an
|
|
132
|
+
auth-shaped refusal.
|
|
133
|
+
|
|
134
|
+
- `authStatuses` is declared per provider — a Discord webhook answers `404` for
|
|
135
|
+
an unknown id, so 404 IS its auth answer.
|
|
136
|
+
- **A 2xx is a failure.** The credential is reaching nothing.
|
|
137
|
+
- **Offline is `skip`, never `fail`.** A check that goes red on a train gets
|
|
138
|
+
ignored, and is then worth nothing when it goes red for real.
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Drift: it reports, it never adapts
|
|
143
|
+
|
|
144
|
+
A connector's worst failure is silent — the provider changes its API and nothing
|
|
145
|
+
says so until something breaks.
|
|
146
|
+
|
|
147
|
+
**And the fix is not a connector that reshapes itself.** That connector cannot be
|
|
148
|
+
reviewed, its behaviour depends on when it ran, and adaptation is only ever a
|
|
149
|
+
guess at intent. So:
|
|
150
|
+
|
|
151
|
+
> Drift reports. The provider decides. A person acts.
|
|
152
|
+
|
|
153
|
+
Drift also never changes runtime behaviour: it runs out of band, and a check that
|
|
154
|
+
failed a working call because a *document* changed would be a self-inflicted
|
|
155
|
+
outage.
|
|
156
|
+
|
|
157
|
+
What is checked is a narrow projection, not a diff — *for the operations this
|
|
158
|
+
connector actually calls, do the path, method, sent fields and read fields still
|
|
159
|
+
exist?* Where a provider publishes no spec (most do not), the fallback is a
|
|
160
|
+
recorded shape: field **names** only.
|
|
161
|
+
|
|
162
|
+
`outcome` is `clean` | `drifted` | **`unchecked`**. The third is never collapsed
|
|
163
|
+
into the first — a checker that could not see is not a checker that saw nothing
|
|
164
|
+
wrong.
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## Depend on this with an open range, not a caret
|
|
169
|
+
|
|
170
|
+
```jsonc
|
|
171
|
+
// package.json
|
|
172
|
+
"@particle-academy/fancy-connector-core": ">=0.2.0 <2.0.0"
|
|
173
|
+
```
|
|
174
|
+
```jsonc
|
|
175
|
+
// composer.json
|
|
176
|
+
"particle-academy/fancy-connector-core": ">=0.2.0 <2.0.0"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**Not `^0.2.0`.** A caret on a `0.x` locks the MINOR — npm and Composer both read
|
|
180
|
+
`^0.2.0` as `>=0.2.0 <0.3.0` — so the next release is out of range and
|
|
181
|
+
**`npm update` reports success having installed nothing.** A resolver quietly
|
|
182
|
+
keeping the old version is indistinguishable from a resolver that had nothing to
|
|
183
|
+
do, which is why this is worth stating rather than leaving to convention.
|
|
184
|
+
|
|
185
|
+
This package intends the open range: **pre-1.0 minors may change what a connector
|
|
186
|
+
sees, and `CONNECTOR_API_VERSION` is what tells you when** — loudly, at
|
|
187
|
+
registration, naming which side is behind. That is a stronger guarantee than a
|
|
188
|
+
caret gives you, and it is the reason the caret is not needed here.
|
|
189
|
+
|
|
190
|
+
The first consumer of this package pinned `^0.1.0` and would have missed 0.2.0
|
|
191
|
+
entirely. That is the whole argument.
|
|
192
|
+
|
|
193
|
+
## Documentation
|
|
194
|
+
|
|
195
|
+
- [`AGENTS.md`](./AGENTS.md) — the invariants, the traps, and what a change here
|
|
196
|
+
breaks.
|
|
197
|
+
- `.ai/plans/fancy-connectors.md` in the envelope — the architecture, the
|
|
198
|
+
Socialite seam, the drift research, and every decision with its reason.
|
|
199
|
+
|
|
200
|
+
MIT.
|
package/dist/index.cjs
CHANGED
|
@@ -21,8 +21,10 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
23
|
AMBIGUOUS_REFUSAL: () => AMBIGUOUS_REFUSAL,
|
|
24
|
+
CANONICAL_METRICS: () => CANONICAL_METRICS,
|
|
24
25
|
CONNECTOR_API_VERSION: () => CONNECTOR_API_VERSION,
|
|
25
26
|
CONNECTOR_MODES: () => CONNECTOR_MODES,
|
|
27
|
+
CREDENTIAL_SCOPES: () => CREDENTIAL_SCOPES,
|
|
26
28
|
ConnectorAmbiguous: () => ConnectorAmbiguous,
|
|
27
29
|
ConnectorApiMismatch: () => ConnectorApiMismatch,
|
|
28
30
|
ConnectorAuthError: () => ConnectorAuthError,
|
|
@@ -39,8 +41,10 @@ __export(src_exports, {
|
|
|
39
41
|
FAKE_EPOCH: () => FAKE_EPOCH,
|
|
40
42
|
MAX_IDEMPOTENCY_KEY_LENGTH: () => MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
41
43
|
NO_IDEMPOTENCY_KEY_WARNING: () => NO_IDEMPOTENCY_KEY_WARNING,
|
|
44
|
+
PROBLEM_SEVERITIES: () => PROBLEM_SEVERITIES,
|
|
42
45
|
RENDERER_VERSION: () => RENDERER_VERSION,
|
|
43
46
|
REVIEW_STALE_DAYS: () => REVIEW_STALE_DAYS,
|
|
47
|
+
SANDBOX_KINDS: () => SANDBOX_KINDS,
|
|
44
48
|
SUPPORTED_CONNECTOR_API: () => SUPPORTED_CONNECTOR_API,
|
|
45
49
|
assertConnectorApi: () => assertConnectorApi,
|
|
46
50
|
byteRangeOf: () => byteRangeOf,
|
|
@@ -72,6 +76,8 @@ __export(src_exports, {
|
|
|
72
76
|
payloadHash: () => payloadHash,
|
|
73
77
|
postChain: () => postChain,
|
|
74
78
|
probe: () => probe,
|
|
79
|
+
providerProblems: () => providerProblems,
|
|
80
|
+
providerSandboxIsSelectable: () => providerSandboxIsSelectable,
|
|
75
81
|
registerConnectionHost: () => registerConnectionHost,
|
|
76
82
|
registerTransport: () => registerTransport,
|
|
77
83
|
render: () => render,
|
|
@@ -83,6 +89,8 @@ __export(src_exports, {
|
|
|
83
89
|
runIdentityFrom: () => runIdentityFrom,
|
|
84
90
|
runKeyFrom: () => runKeyFrom,
|
|
85
91
|
runProbes: () => runProbes,
|
|
92
|
+
sandboxIsSelectable: () => sandboxIsSelectable,
|
|
93
|
+
sandboxRefusal: () => sandboxRefusal,
|
|
86
94
|
satisfiesMinimum: () => satisfiesMinimum,
|
|
87
95
|
seedFrom: () => seedFrom,
|
|
88
96
|
shapeOf: () => shapeOf,
|
|
@@ -93,7 +101,8 @@ __export(src_exports, {
|
|
|
93
101
|
unchecked: () => unchecked,
|
|
94
102
|
utf8Length: () => utf8Length,
|
|
95
103
|
verifyDelivery: () => verifyDelivery,
|
|
96
|
-
verifyHmac: () => verifyHmac
|
|
104
|
+
verifyHmac: () => verifyHmac,
|
|
105
|
+
withResolvedLimit: () => withResolvedLimit
|
|
97
106
|
});
|
|
98
107
|
module.exports = __toCommonJS(src_exports);
|
|
99
108
|
|
|
@@ -166,6 +175,8 @@ var AMBIGUOUS_CODES = /* @__PURE__ */ new Set([
|
|
|
166
175
|
]);
|
|
167
176
|
var AMBIGUOUS_NAMES = /* @__PURE__ */ new Set(["AbortError", "TimeoutError"]);
|
|
168
177
|
function classifyError(error) {
|
|
178
|
+
const attached = error?.classified;
|
|
179
|
+
if (attached && typeof attached === "object" && typeof attached.kind === "string") return attached;
|
|
169
180
|
const detail = error instanceof Error ? error.message : String(error);
|
|
170
181
|
const code = error?.code ?? error?.cause?.code ?? "";
|
|
171
182
|
const name = error instanceof Error ? error.name : "";
|
|
@@ -403,19 +414,42 @@ function fakeRequest(service, operation, config, input) {
|
|
|
403
414
|
|
|
404
415
|
// src/mode.ts
|
|
405
416
|
var CONNECTOR_MODES = ["fake", "sandbox", "live"];
|
|
417
|
+
var SANDBOX_KINDS = [
|
|
418
|
+
"credential",
|
|
419
|
+
"base-url",
|
|
420
|
+
"separate-account",
|
|
421
|
+
"restricted-reach",
|
|
422
|
+
"none",
|
|
423
|
+
"unverified"
|
|
424
|
+
];
|
|
425
|
+
var SELECTABLE_SANDBOX = /* @__PURE__ */ new Set([
|
|
426
|
+
"credential",
|
|
427
|
+
"base-url",
|
|
428
|
+
"separate-account"
|
|
429
|
+
]);
|
|
430
|
+
function sandboxIsSelectable(kind) {
|
|
431
|
+
return SELECTABLE_SANDBOX.has(kind);
|
|
432
|
+
}
|
|
433
|
+
function sandboxRefusal(service, kind) {
|
|
434
|
+
if (kind === "restricted-reach") {
|
|
435
|
+
return service + ` has no separate test estate. What it has instead is an ACCOUNT-LEVEL reach restriction - an unreviewed app that posts only to its own developers, or only privately. Same credentials, same endpoints, same estate: only the audience changes, and nothing in a request selects it. That makes it the sandbox shape most likely to be mistaken for a real one, because a restricted call looks exactly like a successful one - ok, a real id, and no audience. Use "fake" to develop without touching the provider, or "live" and confirm reach on the provider's own surface, which is the only place it is visible.`;
|
|
436
|
+
}
|
|
437
|
+
if (kind === "unverified") {
|
|
438
|
+
return "Nobody has verified what test estate " + service + ' offers, so "sandbox" cannot be honoured - the resolver would be guessing, and the cost of guessing wrong here is a workflow pointed at a live estate while somebody believes it is a test one. Find out and declare it, or use "fake".';
|
|
439
|
+
}
|
|
440
|
+
return service + ' has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.';
|
|
441
|
+
}
|
|
406
442
|
function resolveConnectorMode(inputs) {
|
|
407
443
|
const { requested, connectionMode, sandbox, hasSandboxCredentials, environment } = inputs;
|
|
408
444
|
if (requested && requested !== "auto") {
|
|
409
|
-
if (requested === "sandbox" && sandbox
|
|
410
|
-
throw new ConnectorModeError(
|
|
411
|
-
`This connector's provider has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.`
|
|
412
|
-
);
|
|
445
|
+
if (requested === "sandbox" && !sandboxIsSelectable(sandbox)) {
|
|
446
|
+
throw new ConnectorModeError(sandboxRefusal("This connector's provider", sandbox));
|
|
413
447
|
}
|
|
414
448
|
return requested;
|
|
415
449
|
}
|
|
416
450
|
if (connectionMode) return connectionMode;
|
|
417
451
|
if (environment.production) return "live";
|
|
418
|
-
return sandbox
|
|
452
|
+
return sandboxIsSelectable(sandbox) && hasSandboxCredentials ? "sandbox" : "fake";
|
|
419
453
|
}
|
|
420
454
|
var ConnectorModeError = class extends Error {
|
|
421
455
|
constructor(message) {
|
|
@@ -446,10 +480,8 @@ function connectionsFor(service) {
|
|
|
446
480
|
function resolveConnection(options) {
|
|
447
481
|
const { service, operation, sandbox } = options;
|
|
448
482
|
const requestedMode = options.requested ?? "auto";
|
|
449
|
-
if (requestedMode === "sandbox" && sandbox
|
|
450
|
-
throw new ConnectorModeError(
|
|
451
|
-
`${service} has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.`
|
|
452
|
-
);
|
|
483
|
+
if (requestedMode === "sandbox" && !sandboxIsSelectable(sandbox)) {
|
|
484
|
+
throw new ConnectorModeError(sandboxRefusal(service, sandbox));
|
|
453
485
|
}
|
|
454
486
|
const id = options.connectionId?.trim() || service;
|
|
455
487
|
if (options.credentials) {
|
|
@@ -976,6 +1008,41 @@ function reported(values) {
|
|
|
976
1008
|
}
|
|
977
1009
|
return out;
|
|
978
1010
|
}
|
|
1011
|
+
function providerProblems(provider) {
|
|
1012
|
+
const problems = [];
|
|
1013
|
+
if (provider.implemented && provider.sandbox === "unverified") {
|
|
1014
|
+
problems.push(
|
|
1015
|
+
`${provider.id} says it is implemented while its sandbox shape is still "unverified". Something a person can run today must not be ambiguous about which estate it reaches \u2014 verify it, or set implemented: false.`
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
if (provider.implemented && provider.fields.length === 0) {
|
|
1019
|
+
problems.push(`${provider.id} says it is implemented but declares no credential fields.`);
|
|
1020
|
+
}
|
|
1021
|
+
if (provider.implemented && provider.setup.length === 0) {
|
|
1022
|
+
problems.push(
|
|
1023
|
+
`${provider.id} says it is implemented and names no setup steps. The code is never the expensive part; the setup is, and a provider whose setup is undocumented is one nobody can stand up.`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
for (const field of provider.fields) {
|
|
1027
|
+
if (field.help.trim().length < 10) {
|
|
1028
|
+
problems.push(`${provider.id}.${field.key} has no help text worth reading.`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
if (provider.verify && !provider.proves) {
|
|
1032
|
+
problems.push(
|
|
1033
|
+
`${provider.id} ships a verify but does not say what it PROVES. A green tick that means more than it should is worse than no tick \u2014 name the step it does not cover.`
|
|
1034
|
+
);
|
|
1035
|
+
}
|
|
1036
|
+
if (provider.sandbox === "restricted-reach" && !provider.summary.toLowerCase().includes("reach")) {
|
|
1037
|
+
problems.push(
|
|
1038
|
+
`${provider.id} is declared restricted-reach and its summary never mentions reach. That shape looks exactly like a successful post nobody can see, so the surface has to say so before anyone runs it.`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
return problems;
|
|
1042
|
+
}
|
|
1043
|
+
function providerSandboxIsSelectable(provider) {
|
|
1044
|
+
return sandboxIsSelectable(provider.sandbox);
|
|
1045
|
+
}
|
|
979
1046
|
|
|
980
1047
|
// src/probe.ts
|
|
981
1048
|
async function probe(spec) {
|
|
@@ -1177,6 +1244,14 @@ async function payloadHash(payload) {
|
|
|
1177
1244
|
function isEmptyPayload(payload) {
|
|
1178
1245
|
return payload.segments.every((segment) => segment.text.trim() === "");
|
|
1179
1246
|
}
|
|
1247
|
+
function withResolvedLimit(rules, limit) {
|
|
1248
|
+
return limit === void 0 ? rules : { ...rules, limit };
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
// src/seam.ts
|
|
1252
|
+
var CREDENTIAL_SCOPES = ["provider", "account"];
|
|
1253
|
+
var PROBLEM_SEVERITIES = ["block", "warn"];
|
|
1254
|
+
var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
|
|
1180
1255
|
|
|
1181
1256
|
// src/webhook.ts
|
|
1182
1257
|
async function verifyHmac(options) {
|
|
@@ -1279,8 +1354,10 @@ function hasDelivery(value) {
|
|
|
1279
1354
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1280
1355
|
0 && (module.exports = {
|
|
1281
1356
|
AMBIGUOUS_REFUSAL,
|
|
1357
|
+
CANONICAL_METRICS,
|
|
1282
1358
|
CONNECTOR_API_VERSION,
|
|
1283
1359
|
CONNECTOR_MODES,
|
|
1360
|
+
CREDENTIAL_SCOPES,
|
|
1284
1361
|
ConnectorAmbiguous,
|
|
1285
1362
|
ConnectorApiMismatch,
|
|
1286
1363
|
ConnectorAuthError,
|
|
@@ -1297,8 +1374,10 @@ function hasDelivery(value) {
|
|
|
1297
1374
|
FAKE_EPOCH,
|
|
1298
1375
|
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
1299
1376
|
NO_IDEMPOTENCY_KEY_WARNING,
|
|
1377
|
+
PROBLEM_SEVERITIES,
|
|
1300
1378
|
RENDERER_VERSION,
|
|
1301
1379
|
REVIEW_STALE_DAYS,
|
|
1380
|
+
SANDBOX_KINDS,
|
|
1302
1381
|
SUPPORTED_CONNECTOR_API,
|
|
1303
1382
|
assertConnectorApi,
|
|
1304
1383
|
byteRangeOf,
|
|
@@ -1330,6 +1409,8 @@ function hasDelivery(value) {
|
|
|
1330
1409
|
payloadHash,
|
|
1331
1410
|
postChain,
|
|
1332
1411
|
probe,
|
|
1412
|
+
providerProblems,
|
|
1413
|
+
providerSandboxIsSelectable,
|
|
1333
1414
|
registerConnectionHost,
|
|
1334
1415
|
registerTransport,
|
|
1335
1416
|
render,
|
|
@@ -1341,6 +1422,8 @@ function hasDelivery(value) {
|
|
|
1341
1422
|
runIdentityFrom,
|
|
1342
1423
|
runKeyFrom,
|
|
1343
1424
|
runProbes,
|
|
1425
|
+
sandboxIsSelectable,
|
|
1426
|
+
sandboxRefusal,
|
|
1344
1427
|
satisfiesMinimum,
|
|
1345
1428
|
seedFrom,
|
|
1346
1429
|
shapeOf,
|
|
@@ -1351,6 +1434,7 @@ function hasDelivery(value) {
|
|
|
1351
1434
|
unchecked,
|
|
1352
1435
|
utf8Length,
|
|
1353
1436
|
verifyDelivery,
|
|
1354
|
-
verifyHmac
|
|
1437
|
+
verifyHmac,
|
|
1438
|
+
withResolvedLimit
|
|
1355
1439
|
});
|
|
1356
1440
|
//# sourceMappingURL=index.cjs.map
|