@particle-academy/fancy-connector-core 0.1.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/dist/index.d.cts CHANGED
@@ -37,8 +37,22 @@
37
37
  * one status id, Telegram needs a message id and a chat. A shape naming one
38
38
  * provider's model would be wrong for the next one, so the chain carries
39
39
  * whatever the poster returned and never reads inside it.
40
- */
41
- type ChainRef = Record<string, string | number>;
40
+ *
41
+ * **`object`, not `Record<string, string | number>`** — and the difference is
42
+ * not pedantry. An ordinary interface does not satisfy an index signature
43
+ * unless it declares one, so `interface PostRef { uri: string; cid: string }`
44
+ * did NOT satisfy the old constraint. The reference consumer worked around it
45
+ * with `postChain<T & ChainRef>` plus two `as unknown as` casts, and could not
46
+ * use `ChainOutcome<T>` in their own signatures at all — because the only other
47
+ * fix was to add an index signature to a type used across their codebase in
48
+ * order to satisfy a generic used in one place, which weakens the type
49
+ * everywhere to please one call site.
50
+ *
51
+ * A constraint everyone casts past is not enforcing anything; it is just making
52
+ * the casts. `object` still rejects the mistake worth rejecting — a primitive
53
+ * as a reference — and accepts every real one.
54
+ */
55
+ type ChainRef = object;
42
56
  type ChainLinks<Ref extends ChainRef> = {
43
57
  /** The top of the conversation. Fixed for the whole chain. */
44
58
  root: Ref;
@@ -496,10 +510,39 @@ declare const CONNECTOR_MODES: ConnectorMode[];
496
510
  * - `base-url` — a different host entirely (PayPal's `api-m.sandbox.paypal.com`).
497
511
  * - `separate-account` — a distinct tenant you must create, often with its own
498
512
  * login host (Salesforce sandbox orgs on `test.salesforce.com`).
513
+ * - `restricted-reach` — there is no separate estate at all; what the provider
514
+ * offers instead is an **account-level restriction on who can see the
515
+ * result**. A Meta app in Development Mode; an unaudited TikTok app that can
516
+ * only post `SELF_ONLY`. Same credentials, same endpoints, same estate — only
517
+ * the audience changes, and nothing in a request selects it.
518
+ *
519
+ * **This is the dangerous one**, and it is why it is not folded into `none`.
520
+ * It is the shape most likely to be mistaken for a sandbox, because it looks
521
+ * exactly like a successful post that nobody can see: `ok: true`, a real id,
522
+ * no audience. `none` says "there is nothing"; this says "there is something,
523
+ * it is not selectable from here, and success does not mean reach".
499
524
  * - `none` — the provider has no test estate. `sandbox` is then not offered at
500
- * all, and the node's honest choices are `fake` and `live`.
501
- */
502
- type SandboxKind = "credential" | "base-url" | "separate-account" | "none";
525
+ * all, and the honest choices are `fake` and `live`.
526
+ * - `unverified` — **nobody has checked yet.** A real state, and the type has to
527
+ * carry it: this is the field where being wrong sends someone to a live
528
+ * estate believing it is a test one, so "researched but not verified" must not
529
+ * be forced to pick one of the other five. It belongs on a provider declared
530
+ * `implemented: false`; on one declared `implemented: true` it is a finding,
531
+ * and `providerProblems()` reports it.
532
+ */
533
+ type SandboxKind = "credential" | "base-url" | "separate-account" | "restricted-reach" | "none" | "unverified";
534
+ /** Every value, as data, for a host validating input that crossed a JSON boundary. */
535
+ declare const SANDBOX_KINDS: readonly SandboxKind[];
536
+ declare function sandboxIsSelectable(kind: SandboxKind): boolean;
537
+ /**
538
+ * Why `mode: "sandbox"` cannot be honoured for this kind, in the words the
539
+ * person reading it needs.
540
+ *
541
+ * Three reasons, three messages. Collapsing them into "no sandbox available"
542
+ * would hide the one that matters: `restricted-reach` is not an absence, it is
543
+ * a different mechanism with a trap in it.
544
+ */
545
+ declare function sandboxRefusal(service: string, kind: SandboxKind): string;
503
546
  /** The host's view of where it is running. Kept tiny so it is trivial to fake. */
504
547
  type HostEnvironment = {
505
548
  /** True only in a real production deployment. Everything else is "local". */
@@ -1461,7 +1504,29 @@ type RenderedPayload = {
1461
1504
  * ignores this; the point of the rule set is that most do not have to.
1462
1505
  */
1463
1506
  type RenderRules = {
1464
- /** The provider's limit per message, or null where it has none. */
1507
+ /**
1508
+ * The provider's limit per message, or `null` where it imposes none.
1509
+ *
1510
+ * **`null` means UNCOUNTED, and it is a real answer** — distinct from "we do
1511
+ * not know", which is not a renderable state at all. A caller that does not
1512
+ * know the limit must find out before rendering, because every alternative
1513
+ * silently produces the wrong bytes.
1514
+ *
1515
+ * ## This must stay RESOLVABLE PER CONNECTION, and never become static
1516
+ *
1517
+ * A Mastodon instance's character limit comes from the server it is running:
1518
+ * one instance allows 500, another 5000, and the same connector talks to
1519
+ * both. Baking the limit into the connector breaks in the quietest way
1520
+ * available — rendering to 500 on an instance that allows 5000 wastes most of
1521
+ * a post, and rendering to 5000 on one that allows 500 produces a refusal the
1522
+ * author cannot see coming from the preview.
1523
+ *
1524
+ * So the limit is resolved by the CALLER, from the connection, and passed IN
1525
+ * — which is also what makes it part of what was approved rather than
1526
+ * something decided later behind the approver's back. `withResolvedLimit()`
1527
+ * is the supported way to do that, and a test pins that per-call rules beat
1528
+ * declared ones.
1529
+ */
1465
1530
  limit: number | null;
1466
1531
  unit: TextUnit;
1467
1532
  /**
@@ -1529,6 +1594,19 @@ declare function payloadHash(payload: RenderedPayload): Promise<string>;
1529
1594
  * every one of those checks from being hollow.
1530
1595
  */
1531
1596
  declare function isEmptyPayload(payload: RenderedPayload): boolean;
1597
+ /**
1598
+ * Apply a per-connection limit over a connector's declared rules.
1599
+ *
1600
+ * The one supported way to do the thing `RenderRules.limit` insists on: a
1601
+ * Mastodon instance publishes its own `max_toot_chars`, a Discord webhook does
1602
+ * not, and the same connector serves both. Resolving happens at the call site,
1603
+ * with the connection in hand, and the resolved value travels INTO `render` so
1604
+ * it is part of the payload the approver saw.
1605
+ *
1606
+ * `undefined` keeps whatever the connector declared — "I did not look" is not
1607
+ * the same as "there is no limit", and only the second is `null`.
1608
+ */
1609
+ declare function withResolvedLimit(rules: RenderRules, limit: number | null | undefined): RenderRules;
1532
1610
 
1533
1611
  /**
1534
1612
  * The seam — two objects, deliberately separate.
@@ -1576,6 +1654,28 @@ declare function isEmptyPayload(payload: RenderedPayload): boolean;
1576
1654
  * path — and the reference implementation very nearly stored it as
1577
1655
  * configuration because it looked like a URL.
1578
1656
  */
1657
+ /**
1658
+ * Who a credential belongs to.
1659
+ *
1660
+ * Exported as a runtime constant as well as a type — see `CREDENTIAL_SCOPES`.
1661
+ */
1662
+ type CredentialScope = "provider" | "account";
1663
+ /**
1664
+ * The scopes, as DATA.
1665
+ *
1666
+ * Because this union crosses a JSON boundary and the compiler cannot follow it
1667
+ * there. When these values were renamed from `app`/`brand`, a consumer that
1668
+ * re-declared the field shape on its client kept compiling — their
1669
+ * `f.scope === "brand"` silently became never-true, and every credential field
1670
+ * would have rendered as shared. `tsc` gives a host mirroring these types no
1671
+ * help at all, so the values ship as data a host can validate against at the
1672
+ * boundary where the compiler stops.
1673
+ */
1674
+ declare const CREDENTIAL_SCOPES: readonly CredentialScope[];
1675
+ /** Severities, as data, for the same reason. */
1676
+ declare const PROBLEM_SEVERITIES: readonly ["block", "warn"];
1677
+ /** Canonical metric names, as data, for the same reason. */
1678
+ declare const CANONICAL_METRICS: readonly ["like", "share", "reply", "quote", "view"];
1579
1679
  type CredentialField = {
1580
1680
  /** Stable key. The host maps it to wherever it keeps values. */
1581
1681
  key: string;
@@ -1589,7 +1689,7 @@ type CredentialField = {
1589
1689
  * The distinction exists because getting it wrong means either asking for the
1590
1690
  * same app secret five times or letting one account's token reach another's.
1591
1691
  */
1592
- scope: "provider" | "account";
1692
+ scope: CredentialScope;
1593
1693
  secret: boolean;
1594
1694
  required: boolean;
1595
1695
  /** A hint for an empty field. NEVER a real value. */
@@ -1642,8 +1742,26 @@ type ProviderAdapter = {
1642
1742
  * that anything can be revoked. "No known expiry" is not "cannot stop working".
1643
1743
  */
1644
1744
  credentialLifetimeDays?: number;
1645
- /** How the provider exposes a test estate, in the four shapes that exist. */
1745
+ /**
1746
+ * How the provider exposes a test estate.
1747
+ *
1748
+ * **`"unverified"` is a real answer and is the right one until somebody has
1749
+ * actually checked.** Being wrong here points a workflow at a live estate
1750
+ * while a person believes it is a test one, so the type carries "nobody
1751
+ * looked" rather than forcing a guess — a comment saying so is not a type, and
1752
+ * `providerProblems()` reports the combination `implemented: true` +
1753
+ * `unverified`.
1754
+ */
1646
1755
  sandbox: SandboxKind;
1756
+ /**
1757
+ * What `verify` proves, and what it does NOT.
1758
+ *
1759
+ * Required whenever `verify` exists, and checked by `providerProblems()`.
1760
+ * Telegram's `getMe` validates the token and says nothing about whether the
1761
+ * bot was added to the target chat — which is the step everyone actually gets
1762
+ * stuck on. A green tick that means more than it should is worse than no tick.
1763
+ */
1764
+ proves?: string;
1647
1765
  /**
1648
1766
  * A read-only call proving the credential works AND reaches the right account.
1649
1767
  *
@@ -1860,6 +1978,22 @@ declare function capabilityProblems(connector: Connector<never>): string[];
1860
1978
  * `undefined` and non-numbers do not.
1861
1979
  */
1862
1980
  declare function reported(values: Record<string, unknown>): Record<string, number>;
1981
+ /**
1982
+ * Every way a PROVIDER's declaration can outrun what anyone actually checked.
1983
+ *
1984
+ * The sibling of `capabilityProblems`, one level up. `capabilities.metrics: true`
1985
+ * with no shape turns an unimplemented feature into a reported zero; the
1986
+ * equivalent here turns *nobody looked* into a statement about a test estate —
1987
+ * and that field is the one where being wrong sends a person to a live estate
1988
+ * believing it is a test one.
1989
+ *
1990
+ * Strings rather than a throw, so a host reports all of them at once. A check
1991
+ * that stops at the first problem trains people to fix one thing and re-run,
1992
+ * which is how a list of six becomes six rounds.
1993
+ */
1994
+ declare function providerProblems(provider: ProviderAdapter): string[];
1995
+ /** True when a provider's declared sandbox can actually be selected at run time. */
1996
+ declare function providerSandboxIsSelectable(provider: ProviderAdapter): boolean;
1863
1997
 
1864
1998
  /**
1865
1999
  * Dry-verify — proving the transport with a credential that cannot work.
@@ -2126,4 +2260,4 @@ declare function verifyDelivery(trigger: TriggerDescriptor, delivery: InboundDel
2126
2260
  */
2127
2261
  declare function triggerEvent(trigger: TriggerDescriptor, connection: ResolvedConnection, injected: unknown, config: Record<string, unknown>): unknown;
2128
2262
 
2129
- export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CONNECTOR_API_VERSION, CONNECTOR_MODES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, linkRanges, measure, payloadHash, postChain, probe, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac };
2263
+ export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CANONICAL_METRICS, CONNECTOR_API_VERSION, CONNECTOR_MODES, CREDENTIAL_SCOPES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, type CredentialScope, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, PROBLEM_SEVERITIES, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SANDBOX_KINDS, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, linkRanges, measure, payloadHash, postChain, probe, providerProblems, providerSandboxIsSelectable, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, sandboxIsSelectable, sandboxRefusal, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac, withResolvedLimit };
package/dist/index.d.ts CHANGED
@@ -37,8 +37,22 @@
37
37
  * one status id, Telegram needs a message id and a chat. A shape naming one
38
38
  * provider's model would be wrong for the next one, so the chain carries
39
39
  * whatever the poster returned and never reads inside it.
40
- */
41
- type ChainRef = Record<string, string | number>;
40
+ *
41
+ * **`object`, not `Record<string, string | number>`** — and the difference is
42
+ * not pedantry. An ordinary interface does not satisfy an index signature
43
+ * unless it declares one, so `interface PostRef { uri: string; cid: string }`
44
+ * did NOT satisfy the old constraint. The reference consumer worked around it
45
+ * with `postChain<T & ChainRef>` plus two `as unknown as` casts, and could not
46
+ * use `ChainOutcome<T>` in their own signatures at all — because the only other
47
+ * fix was to add an index signature to a type used across their codebase in
48
+ * order to satisfy a generic used in one place, which weakens the type
49
+ * everywhere to please one call site.
50
+ *
51
+ * A constraint everyone casts past is not enforcing anything; it is just making
52
+ * the casts. `object` still rejects the mistake worth rejecting — a primitive
53
+ * as a reference — and accepts every real one.
54
+ */
55
+ type ChainRef = object;
42
56
  type ChainLinks<Ref extends ChainRef> = {
43
57
  /** The top of the conversation. Fixed for the whole chain. */
44
58
  root: Ref;
@@ -496,10 +510,39 @@ declare const CONNECTOR_MODES: ConnectorMode[];
496
510
  * - `base-url` — a different host entirely (PayPal's `api-m.sandbox.paypal.com`).
497
511
  * - `separate-account` — a distinct tenant you must create, often with its own
498
512
  * login host (Salesforce sandbox orgs on `test.salesforce.com`).
513
+ * - `restricted-reach` — there is no separate estate at all; what the provider
514
+ * offers instead is an **account-level restriction on who can see the
515
+ * result**. A Meta app in Development Mode; an unaudited TikTok app that can
516
+ * only post `SELF_ONLY`. Same credentials, same endpoints, same estate — only
517
+ * the audience changes, and nothing in a request selects it.
518
+ *
519
+ * **This is the dangerous one**, and it is why it is not folded into `none`.
520
+ * It is the shape most likely to be mistaken for a sandbox, because it looks
521
+ * exactly like a successful post that nobody can see: `ok: true`, a real id,
522
+ * no audience. `none` says "there is nothing"; this says "there is something,
523
+ * it is not selectable from here, and success does not mean reach".
499
524
  * - `none` — the provider has no test estate. `sandbox` is then not offered at
500
- * all, and the node's honest choices are `fake` and `live`.
501
- */
502
- type SandboxKind = "credential" | "base-url" | "separate-account" | "none";
525
+ * all, and the honest choices are `fake` and `live`.
526
+ * - `unverified` — **nobody has checked yet.** A real state, and the type has to
527
+ * carry it: this is the field where being wrong sends someone to a live
528
+ * estate believing it is a test one, so "researched but not verified" must not
529
+ * be forced to pick one of the other five. It belongs on a provider declared
530
+ * `implemented: false`; on one declared `implemented: true` it is a finding,
531
+ * and `providerProblems()` reports it.
532
+ */
533
+ type SandboxKind = "credential" | "base-url" | "separate-account" | "restricted-reach" | "none" | "unverified";
534
+ /** Every value, as data, for a host validating input that crossed a JSON boundary. */
535
+ declare const SANDBOX_KINDS: readonly SandboxKind[];
536
+ declare function sandboxIsSelectable(kind: SandboxKind): boolean;
537
+ /**
538
+ * Why `mode: "sandbox"` cannot be honoured for this kind, in the words the
539
+ * person reading it needs.
540
+ *
541
+ * Three reasons, three messages. Collapsing them into "no sandbox available"
542
+ * would hide the one that matters: `restricted-reach` is not an absence, it is
543
+ * a different mechanism with a trap in it.
544
+ */
545
+ declare function sandboxRefusal(service: string, kind: SandboxKind): string;
503
546
  /** The host's view of where it is running. Kept tiny so it is trivial to fake. */
504
547
  type HostEnvironment = {
505
548
  /** True only in a real production deployment. Everything else is "local". */
@@ -1461,7 +1504,29 @@ type RenderedPayload = {
1461
1504
  * ignores this; the point of the rule set is that most do not have to.
1462
1505
  */
1463
1506
  type RenderRules = {
1464
- /** The provider's limit per message, or null where it has none. */
1507
+ /**
1508
+ * The provider's limit per message, or `null` where it imposes none.
1509
+ *
1510
+ * **`null` means UNCOUNTED, and it is a real answer** — distinct from "we do
1511
+ * not know", which is not a renderable state at all. A caller that does not
1512
+ * know the limit must find out before rendering, because every alternative
1513
+ * silently produces the wrong bytes.
1514
+ *
1515
+ * ## This must stay RESOLVABLE PER CONNECTION, and never become static
1516
+ *
1517
+ * A Mastodon instance's character limit comes from the server it is running:
1518
+ * one instance allows 500, another 5000, and the same connector talks to
1519
+ * both. Baking the limit into the connector breaks in the quietest way
1520
+ * available — rendering to 500 on an instance that allows 5000 wastes most of
1521
+ * a post, and rendering to 5000 on one that allows 500 produces a refusal the
1522
+ * author cannot see coming from the preview.
1523
+ *
1524
+ * So the limit is resolved by the CALLER, from the connection, and passed IN
1525
+ * — which is also what makes it part of what was approved rather than
1526
+ * something decided later behind the approver's back. `withResolvedLimit()`
1527
+ * is the supported way to do that, and a test pins that per-call rules beat
1528
+ * declared ones.
1529
+ */
1465
1530
  limit: number | null;
1466
1531
  unit: TextUnit;
1467
1532
  /**
@@ -1529,6 +1594,19 @@ declare function payloadHash(payload: RenderedPayload): Promise<string>;
1529
1594
  * every one of those checks from being hollow.
1530
1595
  */
1531
1596
  declare function isEmptyPayload(payload: RenderedPayload): boolean;
1597
+ /**
1598
+ * Apply a per-connection limit over a connector's declared rules.
1599
+ *
1600
+ * The one supported way to do the thing `RenderRules.limit` insists on: a
1601
+ * Mastodon instance publishes its own `max_toot_chars`, a Discord webhook does
1602
+ * not, and the same connector serves both. Resolving happens at the call site,
1603
+ * with the connection in hand, and the resolved value travels INTO `render` so
1604
+ * it is part of the payload the approver saw.
1605
+ *
1606
+ * `undefined` keeps whatever the connector declared — "I did not look" is not
1607
+ * the same as "there is no limit", and only the second is `null`.
1608
+ */
1609
+ declare function withResolvedLimit(rules: RenderRules, limit: number | null | undefined): RenderRules;
1532
1610
 
1533
1611
  /**
1534
1612
  * The seam — two objects, deliberately separate.
@@ -1576,6 +1654,28 @@ declare function isEmptyPayload(payload: RenderedPayload): boolean;
1576
1654
  * path — and the reference implementation very nearly stored it as
1577
1655
  * configuration because it looked like a URL.
1578
1656
  */
1657
+ /**
1658
+ * Who a credential belongs to.
1659
+ *
1660
+ * Exported as a runtime constant as well as a type — see `CREDENTIAL_SCOPES`.
1661
+ */
1662
+ type CredentialScope = "provider" | "account";
1663
+ /**
1664
+ * The scopes, as DATA.
1665
+ *
1666
+ * Because this union crosses a JSON boundary and the compiler cannot follow it
1667
+ * there. When these values were renamed from `app`/`brand`, a consumer that
1668
+ * re-declared the field shape on its client kept compiling — their
1669
+ * `f.scope === "brand"` silently became never-true, and every credential field
1670
+ * would have rendered as shared. `tsc` gives a host mirroring these types no
1671
+ * help at all, so the values ship as data a host can validate against at the
1672
+ * boundary where the compiler stops.
1673
+ */
1674
+ declare const CREDENTIAL_SCOPES: readonly CredentialScope[];
1675
+ /** Severities, as data, for the same reason. */
1676
+ declare const PROBLEM_SEVERITIES: readonly ["block", "warn"];
1677
+ /** Canonical metric names, as data, for the same reason. */
1678
+ declare const CANONICAL_METRICS: readonly ["like", "share", "reply", "quote", "view"];
1579
1679
  type CredentialField = {
1580
1680
  /** Stable key. The host maps it to wherever it keeps values. */
1581
1681
  key: string;
@@ -1589,7 +1689,7 @@ type CredentialField = {
1589
1689
  * The distinction exists because getting it wrong means either asking for the
1590
1690
  * same app secret five times or letting one account's token reach another's.
1591
1691
  */
1592
- scope: "provider" | "account";
1692
+ scope: CredentialScope;
1593
1693
  secret: boolean;
1594
1694
  required: boolean;
1595
1695
  /** A hint for an empty field. NEVER a real value. */
@@ -1642,8 +1742,26 @@ type ProviderAdapter = {
1642
1742
  * that anything can be revoked. "No known expiry" is not "cannot stop working".
1643
1743
  */
1644
1744
  credentialLifetimeDays?: number;
1645
- /** How the provider exposes a test estate, in the four shapes that exist. */
1745
+ /**
1746
+ * How the provider exposes a test estate.
1747
+ *
1748
+ * **`"unverified"` is a real answer and is the right one until somebody has
1749
+ * actually checked.** Being wrong here points a workflow at a live estate
1750
+ * while a person believes it is a test one, so the type carries "nobody
1751
+ * looked" rather than forcing a guess — a comment saying so is not a type, and
1752
+ * `providerProblems()` reports the combination `implemented: true` +
1753
+ * `unverified`.
1754
+ */
1646
1755
  sandbox: SandboxKind;
1756
+ /**
1757
+ * What `verify` proves, and what it does NOT.
1758
+ *
1759
+ * Required whenever `verify` exists, and checked by `providerProblems()`.
1760
+ * Telegram's `getMe` validates the token and says nothing about whether the
1761
+ * bot was added to the target chat — which is the step everyone actually gets
1762
+ * stuck on. A green tick that means more than it should is worse than no tick.
1763
+ */
1764
+ proves?: string;
1647
1765
  /**
1648
1766
  * A read-only call proving the credential works AND reaches the right account.
1649
1767
  *
@@ -1860,6 +1978,22 @@ declare function capabilityProblems(connector: Connector<never>): string[];
1860
1978
  * `undefined` and non-numbers do not.
1861
1979
  */
1862
1980
  declare function reported(values: Record<string, unknown>): Record<string, number>;
1981
+ /**
1982
+ * Every way a PROVIDER's declaration can outrun what anyone actually checked.
1983
+ *
1984
+ * The sibling of `capabilityProblems`, one level up. `capabilities.metrics: true`
1985
+ * with no shape turns an unimplemented feature into a reported zero; the
1986
+ * equivalent here turns *nobody looked* into a statement about a test estate —
1987
+ * and that field is the one where being wrong sends a person to a live estate
1988
+ * believing it is a test one.
1989
+ *
1990
+ * Strings rather than a throw, so a host reports all of them at once. A check
1991
+ * that stops at the first problem trains people to fix one thing and re-run,
1992
+ * which is how a list of six becomes six rounds.
1993
+ */
1994
+ declare function providerProblems(provider: ProviderAdapter): string[];
1995
+ /** True when a provider's declared sandbox can actually be selected at run time. */
1996
+ declare function providerSandboxIsSelectable(provider: ProviderAdapter): boolean;
1863
1997
 
1864
1998
  /**
1865
1999
  * Dry-verify — proving the transport with a credential that cannot work.
@@ -2126,4 +2260,4 @@ declare function verifyDelivery(trigger: TriggerDescriptor, delivery: InboundDel
2126
2260
  */
2127
2261
  declare function triggerEvent(trigger: TriggerDescriptor, connection: ResolvedConnection, injected: unknown, config: Record<string, unknown>): unknown;
2128
2262
 
2129
- export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CONNECTOR_API_VERSION, CONNECTOR_MODES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, linkRanges, measure, payloadHash, postChain, probe, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac };
2263
+ export { AMBIGUOUS_REFUSAL, type ApiContract, type Attempt, type ByteRange, CANONICAL_METRICS, CONNECTOR_API_VERSION, CONNECTOR_MODES, CREDENTIAL_SCOPES, type CallOptions, type CallResult, type Capabilities, type Catalogue, type ChainLinks, type ChainOutcome, type ChainRef, type Citation, type Classified, type ConnectionCredentials, type ConnectionHost, type ConnectionSpec, type Connector, ConnectorAmbiguous, ConnectorApiMismatch, ConnectorAuthError, ConnectorConfigError, ConnectorError, type ConnectorErrorContext, type ConnectorFaker, ConnectorIdempotencyExpiredError, type ConnectorMode, ConnectorModeError, ConnectorRateLimited, type ConnectorRequest, ConnectorRequestError, type ConnectorResult, ConnectorTransient, ConnectorUnreachable, type ContractOperation, type CredentialField, type CredentialScope, DEFAULT_IDEMPOTENCY_WINDOW_SECONDS, DEFAULT_RETRY, type DeliveryDeclaration, type DeliveryMechanism, type DeliveryOutcome, type DriftFinding, type DriftReport, FAKE_EPOCH, type FailureKind, type FakeRequest, type FakeValues, type HmacScheme, type HostEnvironment, type IdempotencyOptions, type InboundDelivery, MAX_IDEMPOTENCY_KEY_LENGTH, type MetricDescriptor, type MetricSample, type ModeInputs, NO_IDEMPOTENCY_KEY_WARNING, PROBLEM_SEVERITIES, type PreparedRequest, type ProbeOutcome, type ProbeReport, type ProbeResult, type ProbeSpec, type Problem, type ProviderAdapter, RENDERER_VERSION, REVIEW_STALE_DAYS, type RecordedShape, type RenderRules, type RenderedPayload, type RequestedMode, type ResolveOptions, type ResolvedConnection, type RetryPolicy, type RunIdentity, SANDBOX_KINDS, SUPPORTED_CONNECTOR_API, type SandboxKind, type Segment, type ServiceDescriptor, type SetupStep, type ShapeMismatch, type SpecSource, type TextUnit, type Transport, type TransportResponse, type TriggerDescriptor, type VerifyResult, type WebhookVerification, type WebhookVerificationSpec, assertConnectorApi, byteRangeOf, callConnector, capabilityProblems, checkAgainstOpenApi, checkAgainstRecordedShape, classifyError, classifyHttp, classifyStatus, classifyThrown, compareShape, connectionsFor, constantTimeEquals, deliver, fakeRequest, fakeValues, getConnectionHost, graphemes, header, hmac, httpFailure, idempotencyKeyFor, isEmptyPayload, isRemote, isUnconditionallyRetryable, linkRanges, measure, payloadHash, postChain, probe, providerProblems, providerSandboxIsSelectable, registerConnectionHost, registerTransport, render, reported, resetRateState, resolveConnection, resolveConnectorMode, respectRate, runIdentityFrom, runKeyFrom, runProbes, sandboxIsSelectable, sandboxRefusal, satisfiesMinimum, seedFrom, shapeOf, shouldRetry, sliceByteRange, splitToFit, triggerEvent, unchecked, utf8Length, verifyDelivery, verifyHmac, withResolvedLimit };
package/dist/index.js CHANGED
@@ -67,6 +67,8 @@ var AMBIGUOUS_CODES = /* @__PURE__ */ new Set([
67
67
  ]);
68
68
  var AMBIGUOUS_NAMES = /* @__PURE__ */ new Set(["AbortError", "TimeoutError"]);
69
69
  function classifyError(error) {
70
+ const attached = error?.classified;
71
+ if (attached && typeof attached === "object" && typeof attached.kind === "string") return attached;
70
72
  const detail = error instanceof Error ? error.message : String(error);
71
73
  const code = error?.code ?? error?.cause?.code ?? "";
72
74
  const name = error instanceof Error ? error.name : "";
@@ -304,19 +306,42 @@ function fakeRequest(service, operation, config, input) {
304
306
 
305
307
  // src/mode.ts
306
308
  var CONNECTOR_MODES = ["fake", "sandbox", "live"];
309
+ var SANDBOX_KINDS = [
310
+ "credential",
311
+ "base-url",
312
+ "separate-account",
313
+ "restricted-reach",
314
+ "none",
315
+ "unverified"
316
+ ];
317
+ var SELECTABLE_SANDBOX = /* @__PURE__ */ new Set([
318
+ "credential",
319
+ "base-url",
320
+ "separate-account"
321
+ ]);
322
+ function sandboxIsSelectable(kind) {
323
+ return SELECTABLE_SANDBOX.has(kind);
324
+ }
325
+ function sandboxRefusal(service, kind) {
326
+ if (kind === "restricted-reach") {
327
+ 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.`;
328
+ }
329
+ if (kind === "unverified") {
330
+ 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".';
331
+ }
332
+ return service + ' has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.';
333
+ }
307
334
  function resolveConnectorMode(inputs) {
308
335
  const { requested, connectionMode, sandbox, hasSandboxCredentials, environment } = inputs;
309
336
  if (requested && requested !== "auto") {
310
- if (requested === "sandbox" && sandbox === "none") {
311
- throw new ConnectorModeError(
312
- `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.`
313
- );
337
+ if (requested === "sandbox" && !sandboxIsSelectable(sandbox)) {
338
+ throw new ConnectorModeError(sandboxRefusal("This connector's provider", sandbox));
314
339
  }
315
340
  return requested;
316
341
  }
317
342
  if (connectionMode) return connectionMode;
318
343
  if (environment.production) return "live";
319
- return sandbox !== "none" && hasSandboxCredentials ? "sandbox" : "fake";
344
+ return sandboxIsSelectable(sandbox) && hasSandboxCredentials ? "sandbox" : "fake";
320
345
  }
321
346
  var ConnectorModeError = class extends Error {
322
347
  constructor(message) {
@@ -347,10 +372,8 @@ function connectionsFor(service) {
347
372
  function resolveConnection(options) {
348
373
  const { service, operation, sandbox } = options;
349
374
  const requestedMode = options.requested ?? "auto";
350
- if (requestedMode === "sandbox" && sandbox === "none") {
351
- throw new ConnectorModeError(
352
- `${service} has no sandbox estate, so "sandbox" cannot be honoured. Use "fake" to develop without credentials, or "live" to talk to the provider.`
353
- );
375
+ if (requestedMode === "sandbox" && !sandboxIsSelectable(sandbox)) {
376
+ throw new ConnectorModeError(sandboxRefusal(service, sandbox));
354
377
  }
355
378
  const id = options.connectionId?.trim() || service;
356
379
  if (options.credentials) {
@@ -877,6 +900,41 @@ function reported(values) {
877
900
  }
878
901
  return out;
879
902
  }
903
+ function providerProblems(provider) {
904
+ const problems = [];
905
+ if (provider.implemented && provider.sandbox === "unverified") {
906
+ problems.push(
907
+ `${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.`
908
+ );
909
+ }
910
+ if (provider.implemented && provider.fields.length === 0) {
911
+ problems.push(`${provider.id} says it is implemented but declares no credential fields.`);
912
+ }
913
+ if (provider.implemented && provider.setup.length === 0) {
914
+ problems.push(
915
+ `${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.`
916
+ );
917
+ }
918
+ for (const field of provider.fields) {
919
+ if (field.help.trim().length < 10) {
920
+ problems.push(`${provider.id}.${field.key} has no help text worth reading.`);
921
+ }
922
+ }
923
+ if (provider.verify && !provider.proves) {
924
+ problems.push(
925
+ `${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.`
926
+ );
927
+ }
928
+ if (provider.sandbox === "restricted-reach" && !provider.summary.toLowerCase().includes("reach")) {
929
+ problems.push(
930
+ `${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.`
931
+ );
932
+ }
933
+ return problems;
934
+ }
935
+ function providerSandboxIsSelectable(provider) {
936
+ return sandboxIsSelectable(provider.sandbox);
937
+ }
880
938
 
881
939
  // src/probe.ts
882
940
  async function probe(spec) {
@@ -1078,6 +1136,14 @@ async function payloadHash(payload) {
1078
1136
  function isEmptyPayload(payload) {
1079
1137
  return payload.segments.every((segment) => segment.text.trim() === "");
1080
1138
  }
1139
+ function withResolvedLimit(rules, limit) {
1140
+ return limit === void 0 ? rules : { ...rules, limit };
1141
+ }
1142
+
1143
+ // src/seam.ts
1144
+ var CREDENTIAL_SCOPES = ["provider", "account"];
1145
+ var PROBLEM_SEVERITIES = ["block", "warn"];
1146
+ var CANONICAL_METRICS = ["like", "share", "reply", "quote", "view"];
1081
1147
 
1082
1148
  // src/webhook.ts
1083
1149
  async function verifyHmac(options) {
@@ -1179,8 +1245,10 @@ function hasDelivery(value) {
1179
1245
  }
1180
1246
  export {
1181
1247
  AMBIGUOUS_REFUSAL,
1248
+ CANONICAL_METRICS,
1182
1249
  CONNECTOR_API_VERSION,
1183
1250
  CONNECTOR_MODES,
1251
+ CREDENTIAL_SCOPES,
1184
1252
  ConnectorAmbiguous,
1185
1253
  ConnectorApiMismatch,
1186
1254
  ConnectorAuthError,
@@ -1197,8 +1265,10 @@ export {
1197
1265
  FAKE_EPOCH,
1198
1266
  MAX_IDEMPOTENCY_KEY_LENGTH,
1199
1267
  NO_IDEMPOTENCY_KEY_WARNING,
1268
+ PROBLEM_SEVERITIES,
1200
1269
  RENDERER_VERSION,
1201
1270
  REVIEW_STALE_DAYS,
1271
+ SANDBOX_KINDS,
1202
1272
  SUPPORTED_CONNECTOR_API,
1203
1273
  assertConnectorApi,
1204
1274
  byteRangeOf,
@@ -1230,6 +1300,8 @@ export {
1230
1300
  payloadHash,
1231
1301
  postChain,
1232
1302
  probe,
1303
+ providerProblems,
1304
+ providerSandboxIsSelectable,
1233
1305
  registerConnectionHost,
1234
1306
  registerTransport,
1235
1307
  render,
@@ -1241,6 +1313,8 @@ export {
1241
1313
  runIdentityFrom,
1242
1314
  runKeyFrom,
1243
1315
  runProbes,
1316
+ sandboxIsSelectable,
1317
+ sandboxRefusal,
1244
1318
  satisfiesMinimum,
1245
1319
  seedFrom,
1246
1320
  shapeOf,
@@ -1251,6 +1325,7 @@ export {
1251
1325
  unchecked,
1252
1326
  utf8Length,
1253
1327
  verifyDelivery,
1254
- verifyHmac
1328
+ verifyHmac,
1329
+ withResolvedLimit
1255
1330
  };
1256
1331
  //# sourceMappingURL=index.js.map