feat(linq): converge fork with upstream v0.4.0 - #1
Merged
Conversation
…ntials feat(adapter): support managed credentials and trusted webhook forwarding
The adapter pinned ^0.22.1, eighteen minor versions behind the published SDK. Catching up is a prerequisite for verifying webhooks with the SDK's own `webhooks.unwrap()`, which does not exist before 0.40.0. One type moved: `EventsWebhookEvent` is now `UnwrapWebhookEvent`. It is the same union of webhook event shapes, and the envelope it carries is unchanged, so the four references substitute directly. No behavior change. Tests, typecheck, lint, and build all match the pre-bump baseline (62 tests passing). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt
chore(adapter-linq): track @linqapp/sdk 0.40.0
* ci: run typecheck, lint, test, and build on every PR This repo publishes @linqapp/chat-sdk-adapter to npm with nothing running its test suite on a pull request. The checks already exist as package scripts; nothing was invoking them. Adds a GitHub Actions workflow that installs with a frozen lockfile and runs all four across the workspace, plus the root `test` and `lint` scripts and matching turbo tasks they need. Node 24, and pnpm resolved from the repo's own packageManager pin, so CI matches local dev. Packages without a given script are skipped by turbo, so apps/api runs typecheck and build while the adapter runs all four. Verified locally against this branch: typecheck, lint, test (62), and build all pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt * ci: build workspace dependencies before typechecking The first CI run failed where local runs passed: apps/api could not resolve `@linqapp/chat-sdk-adapter`. The adapter's `types` point at `dist/index.d.ts`, and a clean checkout has no `dist/`. Local runs only passed because an earlier build had left one behind. `typecheck` declared `dependsOn: ["^typecheck"]`, but typechecking a dependency does not emit its declarations. It needs `^build`, same as `test`. Reproduced by deleting `dist/` and rerunning, then confirmed fixed the same way. Also moves checkout and setup-node to v5; v4 targets Node 20, which the runners now force onto Node 24 with a deprecation warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The integrating-linq skill told agents to verify deliveries with
HMAC-SHA256 over `{timestamp}.{raw_body}` against the `X-Webhook-*`
headers. Linq signs with Standard Webhooks — `webhook-id`,
`webhook-timestamp`, `webhook-signature`, HMAC over
`{id}.{timestamp}.{body}`, keyed on the base64-decoded secret — and its
API docs mark the older headers deprecated.
A skill that documents the deprecated scheme propagates it into every
integration an agent writes from it, and the resulting code fails
whenever the legacy headers are withdrawn.
Points both the skill and its webhook reference at
`client.webhooks.unwrap`, and spells out the two mistakes that make a
hand-rolled implementation reject every real delivery: signing the body
alone, and keying on the raw secret bytes instead of the decoded ones.
Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(adapter-linq): verify webhooks with Standard Webhooks
Linq signs deliveries with Standard Webhooks — `webhook-id`,
`webhook-timestamp`, `webhook-signature`, HMAC over
`{id}.{timestamp}.{body}`, keyed on the base64-decoded secret. It also
still emits the older `X-Webhook-*` headers, which its API docs mark
deprecated.
This adapter verified only the deprecated pair, and reimplemented the
older scheme by hand: `{timestamp}.{body}` hex HMAC keyed on the raw
secret bytes. It works today only because Linq keeps sending the legacy
headers for backwards compatibility, so it fails whenever those are
withdrawn.
Verification now delegates to `standardwebhooks`, the reference
implementation of the spec and the same library `@linqapp/sdk` uses
behind `client.webhooks.unwrap`. That removes ~100 lines of hand-rolled
HMAC, hex decoding, constant-time comparison, and replay-window checks.
Verification takes the signing secret alone rather than an API client.
Routing it through the SDK client would have resolved lazy credentials
twice per delivery and coupled webhook auth to API credentials; the
existing suite caught both.
`handleWebhook` now receives an already-parsed envelope from either
verification path, so the separate JSON.parse step is gone.
Tests: 67 passing (62 existing, re-signed for the new scheme, plus 5
covering signature mismatch, id substitution, missing headers, and the
replay window).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt
* test(adapter-linq): verify the signing path against a real Linq secret
The unit tests sign with a secret this repo invented. That proves the
code path works with an assumed format, not Linq's actual one — and the
format matters: `new Webhook(secret)` strips `whsec_` and base64-decodes
the rest, so a non-base64 secret would reject every real delivery.
Adds `smoke-live.mjs verify`. It creates a throwaway webhook
subscription, takes the `signing_secret` Linq issues, signs a synthetic
`message.received` with it, and runs it through the real adapter, then
deletes the subscription. No messages are sent, so it needs no phone
number and no tunnel.
It asserts the secret is `whsec_` + base64 of a 32-byte key, that a
correctly signed delivery both verifies *and* reaches the Chat SDK
handler, and that three things are rejected: a tampered body, a delivery
signed with a different secret, and the deprecated `X-Webhook-*` scheme.
Run against a live account: all five checks pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt
* feat(adapter-linq): implement openDM with pending threads
Chat SDK's `openDM(handle)` returns a thread ID before any message is
sent. Linq has no empty-chat primitive — `POST /v3/chats` and
`POST /v3/messages` both create a chat *by* sending its first message —
so the adapter left `openDM` unimplemented and could only reply to chats
that already existed. Anything proactive was impossible: a scheduled job
could not open a conversation with someone who had not texted first.
`openDM` now returns a pending thread ID, `linq:pending:{handle}`, and
the chat is created on that thread's first post. The ID is deterministic,
so a caller can address a handle it has never messaged without a round
trip, and a consumer that persists thread IDs can construct one directly.
The first post goes through `messages.create`, which lets Linq select the
sending line and fail over off flagged ones, rather than pinning a `from`.
It also reuses an existing chat with the same recipients, so a repeated
first post lands in one conversation instead of forking it. The returned
`RawMessage` carries the real `linq:{chatId}`.
Operations that need an existing chat — fetch, edit, typing, reactions —
now fail on a pending thread with the remedy instead of a confusing API
error.
Tests: 73 passing (6 new covering the pending ID, determinism, blank
handle, chat creation on first post, the existing-chat path, and the
guard).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt
* test(adapter-linq): live smoke coverage for openDM
Adds `smoke-live.mjs opendm`, which exercises the proactive path against
the real API: openDM a handle with no existing chat, post to the pending
thread, and confirm a real chat comes back.
It asserts the pending thread ID shape, that the first post returns a
real `linq:{uuid}` thread, that a second post reuses that chat rather
than forking a parallel conversation, and that an operation needing a
chat still fails with the remedy on a pending thread.
Unlike `verify`, this one sends real messages, so it needs
`LINQ_TEST_TO` and a line allowed to reach it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`pnpm typecheck` already guards the SDK surface — every call and every response field the adapter reads is typed, which is why the 0.22→0.40 bump surfaced exactly one break. The gaps are where types cannot reach. Typed test doubles. The suite injected `apiClient` through sixteen `as unknown as` casts, which detach the double from the real API: a fake response could drift from the SDK's shape and every test still passed. `test/support/mock-client.ts` types the doubles against `LinqAPIV3["chats"]["messages"]["send"]` and friends, so an upgrade that reshapes a request or response now fails typecheck in the tests too. All sixteen casts are gone. The webhook fixture asserted `as LinqAPIV3.MessageReceivedWebhookEvent`, suppressing exactly the errors worth having. It satisfies the type on its own, so the cast only cost protection. Error paths. None of Linq's documented failures were covered, and none are visible to typecheck — the call compiles and the server refuses at runtime. Adds coverage for 403/2008 (recipient not allowed), 409/2015 (no eligible sending line, hit for real while smoke-testing openDM), 429, and the nested `error.code` / `error.doc_url` shape a caller has to read. Also pins that a rejected send is not retried behind the caller's back, since a silent retry to a disallowed recipient is a compliance problem rather than resilience. Restores `smoke-live.mjs live`, which verifies a webhook Linq's own server produced and delivered over the network. It was written against the webhook branch and did not survive that branch's route to main. Tests: 78 passing, up from 73. Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* run oxfmt so format:check can gate CI Three files had drifted from the repo's own formatter, so oxfmt --check failed. Reformatting them lets CI enforce it. Co-Authored-By: ghsmc <george@milolabs.com> * add CI and automated npm publishing CI runs lint, format:check, typecheck and test on every PR and on main. Publishing keys off the version in packages/adapter-linq/package.json. Merging a bump to main reruns the checks, publishes to npm with provenance, pushes a v<version> tag and cuts a GitHub release. It no-ops if the version is already on npm, and pushes only a tag, never a commit to main. turbo typecheck now depends on ^build. This fixes apps/api failing to typecheck from a clean checkout because the adapter dist types were missing. Root scripts proxy turbo, plus pnpm version:adapter for the bump. RELEASING.md documents the flow, the NPM_TOKEN secret and the manual fallback. Co-Authored-By: ghsmc <george@milolabs.com> * style: apply oxfmt so format:check can gate CI The branch's original formatting commit reformatted the tree as it stood before linq-team#15, linq-team#18, and linq-team#20. Those PRs rewrote most of the same files, so the commit no longer applied. Re-running `oxfmt .` against current main produces the equivalent result for the current content. Whitespace only — no logic, no behavior. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt * ci: align publish to Node 24 and correct the dependency note The publish workflow ran on Node 22 while CI verifies on 24, so a release could ship from a runtime nothing tested on. Both are 24 now. The README claimed the adapter has no runtime dependencies beyond @linqapp/sdk. linq-team#15 added standardwebhooks, so that is no longer true. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt * ci: publish over OIDC instead of an npm token The package is now configured on npm with a trusted publisher pointing at this workflow, so publishing authenticates over OIDC and needs no secret. Removes `NODE_AUTH_TOKEN` from the publish step. This is deliberate, not an oversight: the npm CLI prefers OIDC when it detects the environment and falls back to a token otherwise, so leaving one set would shadow trusted publishing — including a stale or empty `NPM_TOKEN` secret. Drops `--provenance`. Provenance is generated automatically when publishing through a trusted publisher, and the flag is redundant. Trusted publishing requires npm >= 11.5.1. Node 24 bundles npm 11.x, but the patch depends on which 24.x setup-node resolves, so the workflow installs the latest npm before publishing rather than inheriting it. RELEASING.md documented an `NPM_TOKEN` secret that is no longer used, and now records that renaming or moving this workflow file breaks publishing until the trusted publisher entry on npm is updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: ghsmc <george@milolabs.com> Co-authored-by: Josh Flayhart <josh.flayhart@linqapp.com>
Managed credentials and trusted webhook forwarding from linq-team#10. New public types on LinqAdapterConfig, so a minor bump. Co-authored-by: ghsmc <george@milolabs.com> Co-authored-by: Josh Flayhart <jflayhart@users.noreply.github.com>
* chore: adopt changesets for versioning Releases were driven by hand-editing the adapter's version. Nothing enforced that a merged change got released, and nothing recorded what each version contained — 0.2.0 shipped to npm with no changelog at all. Changesets proposes the version; publishing stays where it is. A PR carries a changeset describing its user-visible change, merging to main opens a Version Packages PR that applies the accumulated changesets and writes CHANGELOG.md, and merging that PR edits the adapter's package.json, which publish.yml already triggers on. The Version Packages PR becomes the release gate. The Version workflow deliberately does not publish. changesets/action spawns the publish command as a child process without passing the OIDC request token through, so `npm publish` fails ENEEDAUTH (npm/cli#8976). Keeping publishing in publish.yml, where npm publish runs as a top-level step, sidesteps that and preserves the trusted-publishing path already verified working. `nitro-starter` is ignored — it is an example app, not a published package, and is not marked private. CHANGELOG.md is seeded by hand for 0.2.0, which predates this, so the version currently on npm is not left undocumented. It is also added to the package's `files`, so the notes ship to npm rather than living only in the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt * chore: pin the changesets schema to the installed version The config referenced schema 3.0.0 while @changesets/config resolves to 4.0.0, so editors validated against the wrong schema. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(adapter): mark Linq chats as read * chore: rebase onto main, adopt typed mocks, add changeset Rebased off `fix/standard-webhooks-verification`, which was a leftover branch already superseded by main and has since been deleted. Two follow-ups from changes that landed after this branched: linq-team#20 replaced every `as unknown as` API-client cast with typed helpers, so the markRead test now uses `withApiClient`. Its double is checked against the SDK rather than detached from it. linq-team#22 moved releases onto changesets, so `markRead` needs one to ship. It is new user-visible behavior, hence minor. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt --------- Co-authored-by: Josh Flayhart <josh.flayhart@linqapp.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Josh Flayhart <jflayhart@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
…q-team#28) The adapter acts on three of the forty-four event types Linq can send. That is a reasonable scope for a Chat SDK adapter, but nothing recorded which events were deliberate omissions and which were simply unnoticed. `@linqapp/sdk` went from 25 event types in 0.22.1 to 44 in 0.40.0. Nineteen events arrived in a single upgrade and nothing surfaced it. Adds a record keyed by the SDK's own `WebhookEventType`, so it must name every event Linq can send. An upgrade that adds one leaves the record missing a key and typecheck fails — the drift appears on the upgrade's own pull request rather than as silence in production. Verified by simulating a new event type: `Property '"message.recalled"' is missing`. Each ignored event carries its reason, separating "Linq supports it and Chat SDK can express it, we have not wired it up" from "no Chat SDK primitive exists" — payments, polls, location, and calls fall in the latter and belong behind `bot.getAdapter("linq")`. Tests assert the record describes the code: an event marked handled must reach the Chat SDK, an ignored one must not, and every ignored entry must state a reason. Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…q-team#29) Linq sends message.sent, message.delivered, message.read, and message.failed. The adapter dropped all four, so a caller had no way to tell a delivered message from one the carrier rejected — every send looked like a success, and a failed one was indistinguishable from a message sitting unread on someone's phone. Chat SDK has no delivery-status dispatch: ChatInstance exposes processMessage, processReaction, processAction and friends, none of which model a receipt. So this surfaces on the adapter through `onDeliveryStatus`, reachable from a Chat SDK app with `getAdapter("linq")` — the same escape hatch eve's own first-party channels use for platform-specific calls. A listener that throws is logged rather than propagated. Linq retries any non-2xx delivery, so letting one bad listener fail the webhook would replay the whole event. The coverage record moves those four events from ignored to handled, and caught a real imprecision while doing it: the ledger test asserted that a handled event reaches processMessage or processReaction, which delivery status deliberately does not. It now counts adapter-owned surfaces too, so "handled" means the adapter acts on the event rather than that it reaches one specific dispatcher. Claude-Session: https://claude.ai/code/session_01MEVd5BThtsmgTVkoojSkLt Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Validation