@zackbart/connecta 0.39.0 → 0.41.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 +115 -0
- package/bin/connecta.mjs +4 -8
- package/bin/version.mjs +1 -1
- package/dist/access-tokens.js +6 -4
- package/dist/activity.d.ts +30 -4
- package/dist/activity.js +28 -2
- package/dist/agent-observation.js +24 -2
- package/dist/agent-routes.js +4 -41
- package/dist/auth/clerk.d.ts +6 -1
- package/dist/auth/clerk.js +16 -0
- package/dist/auth/cloudflare-access.js +49 -7
- package/dist/auth/downstream-oauth/consent.js +79 -29
- package/dist/auth/downstream-oauth/grant-store.js +58 -7
- package/dist/auth/downstream-oauth/refresh-records.js +6 -5
- package/dist/auth/downstream-oauth.js +12 -4
- package/dist/auth/native.js +10 -0
- package/dist/auth/oauth-request-not-sent.js +4 -0
- package/dist/auth/static-oauth.js +51 -21
- package/dist/catalog-cache.js +151 -37
- package/dist/catalog-freshness.js +13 -0
- package/dist/catalog-service.js +39 -12
- package/dist/catalog.js +103 -38
- package/dist/config.d.ts +10 -4
- package/dist/config.js +30 -7
- package/dist/connectors/remote-mcp/call.js +1 -1
- package/dist/connectors/remote-mcp/connection.js +52 -7
- package/dist/connectors/remote-mcp/oauth.js +6 -2
- package/dist/connectors/remote-mcp/resources.js +5 -2
- package/dist/connectors/remote-mcp/transport.js +37 -8
- package/dist/connectors/remote-mcp.js +8 -5
- package/dist/credential-contract.d.ts +3 -1
- package/dist/describe-config.d.ts +1 -0
- package/dist/describe-config.js +1 -0
- package/dist/downstream-input.js +11 -2
- package/dist/errors.d.ts +8 -1
- package/dist/errors.js +2 -0
- package/dist/execute.js +20 -7
- package/dist/executor-result.js +1 -1
- package/dist/executors/quickjs-child.js +1 -1
- package/dist/executors/quickjs-runtime.js +1 -1
- package/dist/executors/quickjs.js +1 -1
- package/dist/guarded-transport.js +2 -0
- package/dist/http/client.js +7 -0
- package/dist/http/hatch.js +2 -0
- package/dist/http/node-transport.js +110 -4
- package/dist/http/oauth.js +17 -6
- package/dist/index.js +10 -21
- package/dist/invocation-failure.js +28 -0
- package/dist/invocation.js +77 -35
- package/dist/membership.d.ts +1 -1
- package/dist/meta-tools.js +58 -36
- package/dist/module-contracts.d.ts +3 -1
- package/dist/native-tools.js +12 -5
- package/dist/node.d.ts +7 -0
- package/dist/node.js +82 -3
- package/dist/operation-activity.js +103 -0
- package/dist/operator-record.d.ts +8 -0
- package/dist/operator-record.js +15 -0
- package/dist/operator-ui/generated.js +2 -2
- package/dist/provider-call-facts.js +6 -2
- package/dist/provider.d.ts +1 -1
- package/dist/providers/_shared/google/workspace.d.ts +1 -1
- package/dist/providers/breeze/index.js +1 -1
- package/dist/providers/ccb/index.js +1 -1
- package/dist/providers/cloudflare/index.js +1 -1
- package/dist/providers/docs/index.js +1 -1
- package/dist/providers/drive/index.js +1 -1
- package/dist/providers/forms/index.js +1 -1
- package/dist/providers/github/index.js +1 -1
- package/dist/providers/gmail/index.js +1 -1
- package/dist/providers/granola/index.js +1 -1
- package/dist/providers/overflow/index.js +1 -1
- package/dist/providers/planning-center/index.js +1 -1
- package/dist/providers/planning-center/lib/constants.d.ts +1 -1
- package/dist/providers/quickbooks/api.js +28 -25
- package/dist/providers/quickbooks/index.js +1 -1
- package/dist/providers/quickbooks/lib/writes.js +55 -0
- package/dist/providers/quickbooks/safety.js +17 -5
- package/dist/providers/quickbooks/skill.generated.js +1 -1
- package/dist/providers/sheets/index.js +1 -1
- package/dist/providers/slides/index.js +1 -1
- package/dist/providers/stripe/index.js +2 -2
- package/dist/providers/tithely/index.js +1 -1
- package/dist/providers/vercel/api.js +1 -0
- package/dist/registry.js +16 -4
- package/dist/resource-read.js +21 -2
- package/dist/route-names.js +44 -0
- package/dist/routes/activity-context.js +1 -0
- package/dist/routes/activity-disclosure.js +26 -4
- package/dist/routes/activity.js +2 -1
- package/dist/routes/mcp.js +32 -13
- package/dist/routes/ui-config.js +1 -1
- package/dist/routes/ui-data.js +3 -2
- package/dist/runtime/native-storage.js +15 -0
- package/dist/runtime/request-reads.js +87 -10
- package/dist/runtime/request-scope.js +2 -1
- package/dist/skill-origin.js +16 -0
- package/dist/skills.js +132 -43
- package/dist/storage/d1-driver.js +33 -5
- package/dist/storage/keys.js +7 -1
- package/dist/storage/sql-native.js +85 -3
- package/dist/storage/sql.js +6 -0
- package/dist/timing-trace.js +84 -0
- package/dist/types.d.ts +5 -0
- package/dist/url-safety.js +38 -20
- package/dist/utility-recovery.js +33 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worker.js +33 -8
- package/documentation/architecture.md +83 -2
- package/documentation/auth.md +132 -37
- package/documentation/code-mode.md +61 -46
- package/documentation/deploying.md +25 -1
- package/documentation/integrating.md +2 -2
- package/documentation/meta-tools.md +134 -15
- package/documentation/operating.md +86 -1
- package/documentation/operator-ui.md +51 -2
- package/documentation/providers.md +16 -14
- package/package.json +10 -8
- package/templates/node/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,121 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this package are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.41.0 — 2026-11-03
|
|
6
|
+
|
|
7
|
+
This release fixes a production outage on Workers, tightens auth and network defaults, and makes warm requests cheaper. Three changes can stop an existing deployment at startup or at validation, so check them before upgrading.
|
|
8
|
+
|
|
9
|
+
**Upgrade if you run Workers on `@cloudflare/codemode` 0.5.5 or later.** On Connecta 0.40.0, those codemode releases make every `execute_code` host call fail with "The code execution finished before this tool call settled". `workerExecutor()` now works with them. Deployments that pinned codemode to 0.5.4 to avoid this can drop the pin after upgrading; deployments that stay on 0.40.0 must keep it.
|
|
10
|
+
|
|
11
|
+
**Breaking:** `clerkAuth` now requires a resolved `publicUrl`, set on `clerkAuth({ publicUrl })` or `ConnectaConfig.publicUrl`, and no longer falls back to the request origin. A configured vault must have `seal` and `open` when any connector has `startAuth`; use `encryptedCredentialVault(storage, key)` or add both methods, and existing plaintext grants are sealed on first read. Pool descriptions are limited to 500 UTF-16 code units after trimming. The public activity kind union adds `operation`, which affects exhaustive consumers of activity records.
|
|
12
|
+
|
|
13
|
+
**Security:** learned OAuth URLs on non-public addresses are refused, including transitional IPv6 forms carrying non-public IPv4 addresses, learned Node URL connections are pinned to validated DNS answers, and the Node adapter answers `400` to a malformed `Host`, `X-Forwarded-Proto` or request target before reading the body. Pending OAuth flow records store less: with a sealing vault the consent URL, binding and PKCE verifier are sealed together, and with no vault the record keeps no consent URL, so a repeated start mints fresh consent. Sealed consents pending from 0.40.0 still complete; a legacy no-vault consent restarts fresh.
|
|
14
|
+
|
|
15
|
+
**Performance:** warm catalog resolution locates manifests, negotiation and generation in one bounded read. Lexical indexes are reused across requests in an isolate, result-stash writes are batched, and last-seen observation skips redundant reads in its sampling window. Local-only skill catalogs now advertise a five-minute private TTL. Node gains a cap on request body bytes held in flight together, set with `listen(..., { maxInFlightBodyBytes })`. It defaults to the larger of 64 MiB and `maxBodyBytes`, and an explicit value must be at least `maxBodyBytes`; past it, requests get `503` with `Retry-After`.
|
|
16
|
+
|
|
17
|
+
**Reliability and agent guidance:** a rejected Cloudflare Access identity lookup retries once within its original deadline and records payload-free diagnostics. API OAuth status now comes from typed call outcomes fenced by grant revision. Endpoint instructions lead with pool scope and the guide pointer and state the effective write policy, native tools point at connector guides, skills have readable names with legacy aliases, and downstream skill text is labeled with its third-party origin. Typed failures carry recovery steps across host, availability, utility and result-handle errors.
|
|
18
|
+
|
|
19
|
+
**Opt-in, ignore unless you want them:** `activityHistory({ operations: true })` for payload-free operation history; `timingTraceSampleRate` for sampled payload-free MCP timing logs; QuickBooks `update_unsent_invoice`, enabled with `readOnly: false`, for sparse `DocNumber` updates on observed unsent US-company USD invoices, an inference from Intuit's documented autosend prerequisites and not a vendor no-email guarantee; and, in `@zackbart/connecta-eve`, a caller or turn `when` predicate on `connectaConnection` and `connectaInstructions`, plus `connectaSkill({ name, description, ... })` for deferred connector instructions as Eve skills.
|
|
20
|
+
|
|
21
|
+
The downstream Skills extension remains Beta, and unknown Skills revisions are now rejected.
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Add opt-in sparse DocNumber updates for observed unsent US-company USD QuickBooks invoices, refusing unknown delivery state, enabled payments and stale versions.
|
|
26
|
+
- **Breaking:** Opt in to payload-free operation history with activityHistory({ operations: true }). Search, describe, result, skill and whole-program rows use conservative scope and owner disclosure, remain detached from responses, and do not inflate call or friction counts. The public activity kind union adds operation, which may require updates to exhaustive consumers. Typed operation failures retain their checked codes, and activity reuses existing page byte measurements without serializing operation returns.
|
|
27
|
+
- The Eve helper accepts a caller/turn `when` predicate for instructions and connections. False or a policy failure omits the value before fetching guidance or selecting a pool, including when guidance is cached.
|
|
28
|
+
- `listen(..., { maxInFlightBodyBytes })` bounds the request body bytes all in-flight requests hold together on Node. It defaults to 64 MiB, or `maxBodyBytes` when that is larger. Past it a request is answered `503` with `Retry-After` and `Connection: close`. `maxBodyBytes` and its `413` are unchanged, and Workers are unaffected.
|
|
29
|
+
- Point guided native tool descriptions at the connector guide through the serving skills route.
|
|
30
|
+
- Add eval-only guidance delivery interventions for pointer, summary, inline convention and instruction-led fetch comparisons, with deterministic arm controls and guide-fetch metrics. Inline conventions use the advertised guideSummary field, and wire controls list tools first so the SDK validates every arm's output schema.
|
|
31
|
+
- Offer opt-in deferred connector instructions as Eve skills with caller selection before fetching. Network, timeout and malformed-response failures omit the skill while preserving turn cancellation. Successful content is reused only while Eve retains the endpoint/guide/turn memo; failed later turns serve no stale guide.
|
|
32
|
+
- Opt-in sampled payload-free MCP timing logs with request-read cache outcomes and D1 trip/statement totals. Workers durations are I/O-spanning elapsed observations, not CPU attribution; CPU-only catalog stages report counts and decoded bytes. Trace finalization survives throwing downstream cleanup.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- **Breaking:** `createConnecta` throws when `clerkAuth` has no resolved `publicUrl`: neither `clerkAuth({ publicUrl })` nor `ConnectaConfig.publicUrl`. Clerk's token `resource` and session `azp` checks no longer fall back to the request origin. To migrate, set either option to the URL clients reach, or name the loopback origin locally, such as `http://localhost:8787`. A `clerkAuth` `publicUrl` that is not an absolute http(s) URL without credentials also throws. The unset-`publicUrl` startup warning now also fires with inbound auth and describes every URL that follows the request origin.
|
|
37
|
+
- **Breaking:** `createConnecta` throws when a configured vault lacks `seal` or `open` and any connector has `startAuth`, replacing a startup warning that left downstream OAuth tokens, client registrations and PKCE verifiers as plaintext beside encrypted credentials. To migrate, use `encryptedCredentialVault(storage, key)` or add both methods to the custom vault; grants already stored as plaintext are sealed on first read. `encryptedCredentialVault`, deployments with no vault, and non-sealing vaults with no OAuth connector are unaffected.
|
|
38
|
+
- Skip redundant last-seen observation reads during the one-minute sampling window while continuing fresh token admission.
|
|
39
|
+
- Raise the root bundle cap to 422,500 B gzip and `examples/worker` to 806,500 B gzip for this release's measured growth, ending the batch's temporary headroom.
|
|
40
|
+
- Locate catalog manifests alongside negotiation and generation in one bounded SQL read, retaining complete chunk verification and a separate fresh acceptance fence.
|
|
41
|
+
|
|
42
|
+
Keep locator layout metadata and key construction in the shared key builders. Prove expired generation/manifest handling and literal punctuation against both SQL drivers, and pin measured combined warm agent read counts after admission observation sampling.
|
|
43
|
+
- Run CI static/package build phases and two isolated Node test shards in parallel, checking complete shard coverage and every matrix leg's completion at the aggregate gate.
|
|
44
|
+
- Prefix downstream skill text with its third-party connector origin on tool and program surfaces while preserving native manifest bytes.
|
|
45
|
+
- Reuse bounded immutable lexical tokens and postings across complete request catalogs while applying visibility on each request.
|
|
46
|
+
- Advertise a five-minute private TTL for local-only skill catalogs and reads; downstream-enabled caller views retain zero TTL.
|
|
47
|
+
- Use pinned native TypeScript for core and operator UI no-emit feedback, with explicit shared result types accepted by both compilers. Keep TypeScript 6 for AST analysis, emitted builds, declarations and installed consumers.
|
|
48
|
+
- Verify sealed pending OAuth consents from 0.40.0 through callback validation, binding, claim, exchange and token persistence. Cover fresh-start recovery for legacy no-vault consents whose discovery URLs include userinfo, queries or fragments.
|
|
49
|
+
- **Breaking:** Put pool scope and the guide pointer before routing rules in meta and native endpoint instructions, preserving unscoped instructions.
|
|
50
|
+
|
|
51
|
+
Pool descriptions are limited to 500 UTF-16 code units after trimming so accepted text fits the instruction budget and first 512 units in every mode. This tightens validation: previously accepted descriptions with at most 500 Unicode code points but more than 500 UTF-16 units (for example, 500 emoji) now fail construction. Shorten those descriptions before upgrading; required routing rules remain intact.
|
|
52
|
+
- Keep server orchestration and Effect out of the QuickJS child startup graph.
|
|
53
|
+
- Use readable skill slugs with legacy aliases, actionable provider summaries and advisory construction warnings for weak guide summaries.
|
|
54
|
+
|
|
55
|
+
Generated readable names resolve on every skill surface; canonical identities take priority over legacy aliases, ambiguous aliases fail explicitly, and generated names avoid configured connector IDs.
|
|
56
|
+
- Batch SQL result-stash trailing chunks before separate header publication, reducing storage trips while preserving expiry and uncertain-write accounting.
|
|
57
|
+
- Strengthen stash fault probes for delayed SQL batch commits and transactional expiry sweeps.
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
|
|
61
|
+
- Clarify that the redaction guarantee excludes occurrences made entirely of reserved replacement text, including runs across the seam between adjacent markers. Settling behavior is unchanged.
|
|
62
|
+
- QuickBooks write evals now explicitly grade the destination and final answer evidence, completing the selftest check classification without weakening outcome checks.
|
|
63
|
+
- Let `eval:selftest`'s D1 latency control accept a timer that fires just under its delay, so the full selftest no longer fails on a 49.8 ms round trip.
|
|
64
|
+
- QuickBooks eval setup now probes personal credential isolation through a program and checks read-only pools using the tools the selected eval surface serves, so code-only selftests retain their safety checks without calling hidden meta-tools.
|
|
65
|
+
- Attribute eval fake dispatches through a per-server transport marker, retaining unrelated loopback probes separately from vendor ledgers.
|
|
66
|
+
- Probe exact-once eval latency on both deployment targets, disabled fault membership, stopped listening sockets, and each D1 round trip. Count D1 batch waits instead of imposing a load-sensitive upper timing bound.
|
|
67
|
+
- Retry a rejected Cloudflare Access identity lookup once within its original deadline and record checked, payload-free lookup failure diagnostics. Identity failures continue to refuse admission.
|
|
68
|
+
- Restrict native eval workerd TCP connections with fail-closed Linux Landlock port rules and raw-socket controls for disallowed ports and the known allowed-port address limit. Add a model-free check of all eligible Worker references. Document the remaining address and UDP limits.
|
|
69
|
+
- Report API OAuth status from typed real-call outcomes fenced by the token grant's revision. Consent and refresh replace their observation atomically, and passive status reads do not write grant state. Later invocations can recover from transient API refusals; Connect starts fresh consent for a refused grant. Maintained provider middleware records initial and replayed outcomes, and unchanged observations do not rewrite grants. Normal Connect preserves concurrent recovery, and oversized 401 responses record refusal without reading their bodies, including after replay.
|
|
70
|
+
- `workerExecutor()` works with `@cloudflare/codemode` 0.5.5 and later. Those releases close a call gate when the SDK's `execute()` settles, and Connecta's inert loader settled it before the real program ran, so every `execute_code` host call failed with "The code execution finished before this tool call settled". The gate now stays open for the whole native run and closes when it ends or its lease is released. Development and CI use codemode 0.5.6.
|
|
71
|
+
- State effective write policy in endpoint instructions and connector guides, and explain that read-only refusals have no write route.
|
|
72
|
+
- Supply schema recovery for typed argument failures without validation details and explicit safe recovery limits for missing resources, unavailable connectors and uncertain writes.
|
|
73
|
+
- Reuse one observation age for stored connector details and their overlays, keeping both fields consistent across a clock rollover.
|
|
74
|
+
- Corrected the Cloudflare Access `identityLookup.inFlight` documentation: it counts lookup attempts still pending, so a hung lookup counts itself, and a rejected lookup or a deadline during retry backoff does not.
|
|
75
|
+
- Prevent eval startup port reservation races, retry child bind collisions before trials start, and protect a checkout's build with crash-released OS locks while allowing concurrent eval readers. Report failed cross-connector reference reads before attempting the answer post.
|
|
76
|
+
|
|
77
|
+
Restore the fake vendor transport before deployment startup yields, preserving containment and physical-write evidence across interleaved trials.
|
|
78
|
+
|
|
79
|
+
Use descriptor-mode locking on Linux and macOS, retaining the lease when the wrapper dies while its command runs. Keep explicit Wrangler inspector/proxy ports for Landlock and retry native startup collisions with fresh ports. Document the lock's reader/writer fairness limit.
|
|
80
|
+
- Retain remote MCP cleanup settlement across owner and connector close, including abandoned leases, so bounded session termination remains owned by runtime deferred work. Await explicit warmup termination in OAuth rotation tests, including cleanup gated before fetch accounting.
|
|
81
|
+
- `eval:selftest` exits after its last check instead of idling on the egress control's unclosed UDP probe socket.
|
|
82
|
+
- Reject unknown downstream Skills revisions and record the extension’s Beta lifecycle.
|
|
83
|
+
- Complete typed host, availability, utility and result-handle recovery, with continuation actions taking precedence over generic retry advice.
|
|
84
|
+
|
|
85
|
+
### Security
|
|
86
|
+
|
|
87
|
+
- Pending downstream OAuth flow records store less. With a sealing vault, the consent URL, server and client binding, and PKCE verifier are sealed together. With no vault, the record stores no consent URL and its binding keeps no URL userinfo, query or fragment, so a repeated non-forced start mints a fresh consent instead of reusing the pending URL. `documentation/auth.md` states the narrower no-vault guarantee. Sealed consents already in flight across the upgrade still complete. A no-vault consent whose discovery URL holds userinfo, a query or a fragment fails cleanly and needs a fresh start.
|
|
88
|
+
- Learned OAuth URLs naming reserved or transitional addresses are refused: IPv4 special-purpose ranges (`192.0.0/24`, `198.18/15`, multicast, reserved and broadcast among them), IPv6 outside global unicast, and IPv4 carried in compatible, translated, NAT64 or 6to4 form, judged by the address it carries. A public address in those forms stays public. A `publicUrl` on such an address no longer offers a self-hosted client metadata document.
|
|
89
|
+
- The Node adapter answers `400` for a `Host` that is not a host with an optional port, an `X-Forwarded-Proto` other than `http` or `https`, or a request target that does not start with `/`, before reading any body. No such value can change the path or host routes see. A malformed value used to fail as a `500` or be parsed into another URL.
|
|
90
|
+
- Pin learned Node URL connections to validated DNS addresses while preserving TLS hostname verification.
|
|
91
|
+
|
|
92
|
+
## 0.40.0 — 2026-11-02
|
|
93
|
+
|
|
94
|
+
This release makes warm Connecta calls on Workers cheaper in storage and closes a credential gap on OAuth remote MCP connectors. Deployments need no configuration change or migration, unless they use the Eve helper.
|
|
95
|
+
|
|
96
|
+
**Eve helper:** `@zackbart/connecta-eve` now requires exactly Eve 1.0.0-rc.1. Eve 0.90 apps must upgrade Eve together with the helper and, when installed, upgrade `@vercel/connect` to `^2.7.0`. The core package and non-Eve deployments are not affected by this change.
|
|
97
|
+
|
|
98
|
+
**Security:** an OAuth remote MCP connector no longer sends a passive handshake, catalog request or tool call without a credential if its tokens are removed mid-request. It returns OAuth recovery before sending instead. Before this, a server accepting anonymous requests could receive that request without the connector's credential.
|
|
99
|
+
|
|
100
|
+
**Native pools:** native pools now always list `authorize_connector`, so an agent on a pool without `execute_code` can get the operator a handoff for a connector that needs authorization. On those pools the repaired connector's tools appear only after the host refreshes its tool list, usually by reconnecting; pools with `execute_code` can reach them from a program straight away. Anything that compares a native pool's tool list exactly will see one more tool. Meta endpoints keep the same six tools.
|
|
101
|
+
|
|
102
|
+
**Storage work per call:** warm remote MCP catalog resolution now takes four D1 round trips instead of six on modern servers and five on legacy ones. Warm agent admission takes two reads instead of four. Warm OAuth calls read the grant 9 times instead of 11 on modern servers and 12 instead of 13 on legacy ones. A warm isolate reuses a stored catalog it has already validated, after the same freshness and partition checks. The catalog and admission round trips were measured with injected latency on local workerd, and the OAuth counts come from request-budget tests. None of this has been measured in production.
|
|
103
|
+
|
|
104
|
+
### Changed
|
|
105
|
+
|
|
106
|
+
- Read agent token admission state in one fresh D1 or SQLite statement, reducing warm admission and last-seen observation from four reads to two while preserving next-request rotation and revocation enforcement.
|
|
107
|
+
- **Breaking:** `@zackbart/connecta-eve` now requires exactly Eve 1.0.0-rc.1; Eve 0.90 apps must upgrade Eve together with the helper and, when installed, upgrade `@vercel/connect` to `^2.7.0`. Align installed-consumer probes and upgrade guidance with the tested release candidate. Later candidates and stable 1.0 require a compatibility check.
|
|
108
|
+
- Reduce grant reads on warm OAuth remote MCP calls while retaining fresh catalog acceptance, disconnect, token rotation and dispatch checks. Keep the separate invocation token read so same-epoch invalidation refuses writes with OAuth recovery before dispatch, and expiry changes are observed before early renewal.
|
|
109
|
+
- Reuse validated stored catalogs within a warm isolate after checking stored chunks, partition identity, expiry and the fresh generation fence. Retained facts are immutable and bounded; credential redaction still runs per request.
|
|
110
|
+
|
|
111
|
+
### Fixed
|
|
112
|
+
|
|
113
|
+
- Reduce warm remote MCP catalog storage round trips by batching locating reads and private/public manifest probes while preserving fresh generation and OAuth credential acceptance checks.
|
|
114
|
+
- Keep authorization recovery available on native pools without code by listing authorize_connector with the granted connector ids, even when a connector's catalog cannot load.
|
|
115
|
+
|
|
116
|
+
### Security
|
|
117
|
+
|
|
118
|
+
- Require a credential on passive OAuth remote MCP sends, including handshakes, catalog requests and tool dispatches, regardless of provider replacement. Return OAuth recovery before sending when the SDK cannot obtain one, and count a verified unsent write as failed without reconciliation. Explicit authorization flows retain their challenges; writes that may already have left remain unknown.
|
|
119
|
+
|
|
5
120
|
## 0.39.0 — 2026-10-30
|
|
6
121
|
|
|
7
122
|
This release makes Connecta faster and leaner for agents and operators on Cloudflare Workers. A warm search over remote MCP connectors with cached catalogs no longer reconnects to them: an all-legacy warm search makes no downstream requests. Repeated storage reads are memoized per request, and catalog chunks are read in batches. OAuth tokens refresh before they expire instead of failing the first write after expiry. Legacy negotiation verdicts last six hours, with bounded recovery when a server changes protocol era. A connector that recently hung or failed slowly is reported in `catalogErrors` and skipped by unscoped search for about a minute, and unscoped discovery has a total budget. With one hung connector, three consecutive searches went from 30 s each to 5 s, 11 ms and 8 ms.
|
package/bin/connecta.mjs
CHANGED
|
@@ -258,18 +258,14 @@ async function doctor() {
|
|
|
258
258
|
`Native catalogs unavailable: ${surface.catalogErrors.join(", ")}. Inspect connector status and authentication.`,
|
|
259
259
|
);
|
|
260
260
|
const expected = native
|
|
261
|
-
? ["skills", executeCode ? "execute_code" : "read_result"].sort()
|
|
261
|
+
? ["authorize_connector", "skills", executeCode ? "execute_code" : "read_result"].sort()
|
|
262
262
|
: [...META_TOOL_NAMES].sort();
|
|
263
263
|
if (
|
|
264
264
|
native
|
|
265
265
|
? expected.some((name) => !actual.includes(name)) ||
|
|
266
|
-
[
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"call_destructive_tool",
|
|
270
|
-
"authorize_connector",
|
|
271
|
-
executeCode ? "read_result" : "execute_code",
|
|
272
|
-
].some((name) => actual.includes(name))
|
|
266
|
+
["search_tools", "call_tool", "call_destructive_tool", executeCode ? "read_result" : "execute_code"].some(
|
|
267
|
+
(name) => actual.includes(name),
|
|
268
|
+
)
|
|
273
269
|
: JSON.stringify(actual) !== JSON.stringify(expected)
|
|
274
270
|
) {
|
|
275
271
|
throw new Error(
|
package/bin/version.mjs
CHANGED
package/dist/access-tokens.js
CHANGED
|
@@ -8,7 +8,7 @@ import { accessTokenKeys } from "./storage/keys.js";
|
|
|
8
8
|
import { AGENT_NAMESPACE, agentSubjectId, isAgentName, trustAgentProvider } from "./agent-identity.js";
|
|
9
9
|
import { keepErrorName } from "./redaction-identity.js";
|
|
10
10
|
import { agentConfig } from "./agent-config.js";
|
|
11
|
-
import { authCall, authCatch, authFlow, authInvoke, authMethod, authOwnMethod, authStorage, registerAuthMethod, } from "./auth/native.js";
|
|
11
|
+
import { authAccessToken, authCall, authCatch, authFlow, authInvoke, authMethod, authOwnMethod, authStorage, registerAuthMethod, } from "./auth/native.js";
|
|
12
12
|
import { runEdge } from "./runtime/run.js";
|
|
13
13
|
const TOKEN_PREFIX = "cta_";
|
|
14
14
|
const TOKEN_BYTES = 32;
|
|
@@ -802,7 +802,9 @@ export class AccessTokenManager {
|
|
|
802
802
|
signal.throwIfAborted();
|
|
803
803
|
const hash = (yield authCall(hashToken, [token]));
|
|
804
804
|
signal.throwIfAborted();
|
|
805
|
-
const
|
|
805
|
+
const snapshot = (yield authAccessToken(this.storage, hash));
|
|
806
|
+
const read = (key) => snapshot ? (snapshot.get(key) ?? null) : authStorage(this.storage, "get", [key]);
|
|
807
|
+
const lookupRaw = (yield authCall(read, [lookupKey(hash)]));
|
|
806
808
|
signal.throwIfAborted();
|
|
807
809
|
if (!lookupRaw)
|
|
808
810
|
return unauthorized();
|
|
@@ -810,7 +812,7 @@ export class AccessTokenManager {
|
|
|
810
812
|
if (!lookup)
|
|
811
813
|
return unauthorized();
|
|
812
814
|
const recordRaw = ID_RE.test(lookup.id)
|
|
813
|
-
? (yield
|
|
815
|
+
? (yield authCall(read, [recordKey(lookup.id)]))
|
|
814
816
|
: null;
|
|
815
817
|
const record = recordRaw ? parseRecord(recordRaw) : null;
|
|
816
818
|
if (record && record.id !== lookup.id)
|
|
@@ -827,7 +829,7 @@ export class AccessTokenManager {
|
|
|
827
829
|
// code, refuse exactly as an unknown token does.
|
|
828
830
|
if (record.principal || record.agent === undefined || !this.agents.has(record.agent))
|
|
829
831
|
return unauthorized();
|
|
830
|
-
const slotRaw = (yield
|
|
832
|
+
const slotRaw = (yield authCall(read, [slotKey(record.agent)]));
|
|
831
833
|
const slot = slotRaw === null ? null : parseSlot(slotRaw, record.agent);
|
|
832
834
|
signal.throwIfAborted();
|
|
833
835
|
if (slot?.state !== "issued" || slot.tokenId !== record.id)
|
package/dist/activity.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export interface ActivityActor {
|
|
|
27
27
|
namespace?: string;
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* Privacy-minimal history of
|
|
30
|
+
* Privacy-minimal history of a resolved call, catalog change or opt-in operation.
|
|
31
31
|
*
|
|
32
32
|
* Arguments, results, generated code, search text, and raw errors are excluded
|
|
33
33
|
* by construction. Deployments that need a safe human summary can add a
|
|
@@ -39,6 +39,19 @@ export interface ActivityCatalogChange {
|
|
|
39
39
|
removedTools: number;
|
|
40
40
|
changedTools: number;
|
|
41
41
|
}
|
|
42
|
+
export type ActivityOperationName = "search" | "describe" | "result" | "skill" | "program";
|
|
43
|
+
export interface ActivityOperation {
|
|
44
|
+
name: ActivityOperationName;
|
|
45
|
+
/** Conservative endpoint scope, never caller-supplied addresses. */
|
|
46
|
+
connectors: string[];
|
|
47
|
+
personal: boolean;
|
|
48
|
+
hostCalls?: {
|
|
49
|
+
attempted: number;
|
|
50
|
+
admitted: number;
|
|
51
|
+
succeeded: number;
|
|
52
|
+
failed: number;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
42
55
|
/** Integer metadata is checked at every write and read boundary. */
|
|
43
56
|
export declare function activityCount(value: unknown): number | undefined;
|
|
44
57
|
export declare function activityBehaviorFacts(event: {
|
|
@@ -46,16 +59,18 @@ export declare function activityBehaviorFacts(event: {
|
|
|
46
59
|
resultBytes?: unknown;
|
|
47
60
|
kind?: unknown;
|
|
48
61
|
drift?: unknown;
|
|
62
|
+
operation?: unknown;
|
|
49
63
|
pool?: unknown;
|
|
50
64
|
actorBasis?: unknown;
|
|
51
|
-
}): Pick<ToolCallActivityEvent, "classification" | "resultBytes" | "kind" | "drift" | "pool" | "actorBasis">;
|
|
65
|
+
}): Pick<ToolCallActivityEvent, "classification" | "resultBytes" | "kind" | "drift" | "operation" | "pool" | "actorBasis">;
|
|
52
66
|
export interface ToolCallActivityEvent {
|
|
53
67
|
schemaVersion: 1;
|
|
54
68
|
id: string;
|
|
55
69
|
occurredAt: string;
|
|
56
70
|
requestId: string;
|
|
57
71
|
/** Absent on legacy tool rows. Catalog changes use the same paging envelope. */
|
|
58
|
-
kind?: "catalog_drift";
|
|
72
|
+
kind?: "catalog_drift" | "operation";
|
|
73
|
+
operation?: ActivityOperation | undefined;
|
|
59
74
|
drift?: ActivityCatalogChange;
|
|
60
75
|
/** The final registry verdict captured when resolution succeeded. */
|
|
61
76
|
classification?: "read" | "write";
|
|
@@ -162,6 +177,7 @@ export interface CatalogDriftActivityContext {
|
|
|
162
177
|
}
|
|
163
178
|
/** Request-scoped context shared by direct, batch, and code-mode call paths. */
|
|
164
179
|
export interface ActivityRequestContext {
|
|
180
|
+
recordOperation?: typeof recordOperationActivity | undefined;
|
|
165
181
|
recordTool?: typeof recordToolActivity | undefined;
|
|
166
182
|
sink: ActivitySink;
|
|
167
183
|
actor: ActivityActor;
|
|
@@ -181,7 +197,7 @@ export interface ActivityRequestContext {
|
|
|
181
197
|
defer?: (promise: Promise<unknown>) => void;
|
|
182
198
|
logger: Logger;
|
|
183
199
|
}
|
|
184
|
-
export type ActivityEventInput = Pick<ToolCallActivityEvent, "connectorId" | "toolName" | "address" | "source" | "outcome" | "durationMs" | "attempts" | "friction" | "classification" | "resultBytes" | "kind" | "drift"> & {
|
|
200
|
+
export type ActivityEventInput = Pick<ToolCallActivityEvent, "connectorId" | "toolName" | "address" | "source" | "outcome" | "durationMs" | "attempts" | "friction" | "classification" | "resultBytes" | "kind" | "drift" | "operation"> & {
|
|
185
201
|
/**
|
|
186
202
|
* A code connecta assigns. Rows written by older deployments may hold
|
|
187
203
|
* others, so the stored event keeps `string`; a new row never does.
|
|
@@ -204,8 +220,18 @@ export declare function recordCatalogChangeActivity(context: CatalogDriftActivit
|
|
|
204
220
|
privateCatalog?: boolean;
|
|
205
221
|
}, request?: ActivityRequestContext): void;
|
|
206
222
|
export interface ActivityHistoryOptions {
|
|
223
|
+
/** Opt in to one detached storage write per meta operation and program. */
|
|
224
|
+
operations?: boolean;
|
|
207
225
|
store: ActivityStore;
|
|
208
226
|
deploymentId?: string;
|
|
209
227
|
readGate?: ActivityReadGate;
|
|
210
228
|
}
|
|
211
229
|
export declare function activityHistory(options: ActivityHistoryOptions): ActivityModule;
|
|
230
|
+
/** Operation records reuse the detached sink and accept no payload fields. */
|
|
231
|
+
export declare function recordOperationActivity(context: ActivityRequestContext, input: {
|
|
232
|
+
operation: ActivityOperation;
|
|
233
|
+
durationMs: number;
|
|
234
|
+
resultBytes?: number | undefined;
|
|
235
|
+
outcome: ActivityOutcome;
|
|
236
|
+
errorCode?: ClassificationCode | undefined;
|
|
237
|
+
}): void;
|
package/dist/activity.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkedActivityOperation } from "./operation-activity.js";
|
|
1
2
|
import { CONNECTA_VERSION } from "./version.js";
|
|
2
3
|
import { assertKnownOptions, keys, optionsOf } from "./config-schema.js";
|
|
3
4
|
import { routeActivity } from "./routes/activity.js";
|
|
@@ -82,6 +83,7 @@ export function activityBehaviorFacts(event) {
|
|
|
82
83
|
...(classification !== undefined ? { classification } : {}),
|
|
83
84
|
...(resultBytes !== undefined ? { resultBytes } : {}),
|
|
84
85
|
...(drift ? { kind: "catalog_drift", drift } : {}),
|
|
86
|
+
...(event.kind === "operation" ? { kind: "operation", operation: checkedActivityOperation(event.operation) } : {}),
|
|
85
87
|
...(pool !== undefined ? { pool } : {}),
|
|
86
88
|
};
|
|
87
89
|
}
|
|
@@ -106,7 +108,7 @@ export function recordToolActivity(context, input) {
|
|
|
106
108
|
// Checked here as well as by type: a handler can put any string in a
|
|
107
109
|
// `ConnectorCallError`'s code, and a downstream's own code is not recorded.
|
|
108
110
|
const errorCode = classificationCode(input.errorCode);
|
|
109
|
-
const friction = input.friction ?? agentFrictionForCode(errorCode);
|
|
111
|
+
const friction = input.kind === "operation" ? undefined : (input.friction ?? agentFrictionForCode(errorCode));
|
|
110
112
|
// Re-check client grammars at the record boundary; never spread the envelope.
|
|
111
113
|
const clientName = activityClientFact(context.clientInfo?.name, "name");
|
|
112
114
|
const clientVersion = activityClientFact(context.clientInfo?.version, "version");
|
|
@@ -176,16 +178,40 @@ export function recordCatalogChangeActivity(context, input, request) {
|
|
|
176
178
|
}
|
|
177
179
|
/** Attach payload-free history without changing tool results on store failure. */
|
|
178
180
|
/** The closed options activityHistory() accepts; see `assertKnownOptions`. */
|
|
179
|
-
const ACTIVITY_HISTORY_OPTIONS = optionsOf()(keys("store", "deploymentId", "readGate"));
|
|
181
|
+
const ACTIVITY_HISTORY_OPTIONS = optionsOf()(keys("store", "deploymentId", "readGate", "operations"));
|
|
180
182
|
export function activityHistory(options) {
|
|
181
183
|
options = assertKnownOptions(options, "activityHistory()", ACTIVITY_HISTORY_OPTIONS);
|
|
182
184
|
if (!options || typeof options.store?.record !== "function") {
|
|
183
185
|
throw new Error("activityHistory.store must implement record(event)");
|
|
184
186
|
}
|
|
187
|
+
if (options.operations !== undefined && typeof options.operations !== "boolean")
|
|
188
|
+
throw new Error("activityHistory.operations must be a boolean");
|
|
185
189
|
return {
|
|
186
190
|
...options,
|
|
191
|
+
...(options.operations ? { recordOperation: recordOperationActivity } : {}),
|
|
187
192
|
handle: routeActivity,
|
|
188
193
|
recordTool: recordToolActivity,
|
|
189
194
|
recordChange: recordCatalogChangeActivity,
|
|
190
195
|
};
|
|
191
196
|
}
|
|
197
|
+
/** Operation records reuse the detached sink and accept no payload fields. */
|
|
198
|
+
export function recordOperationActivity(context, input) {
|
|
199
|
+
const operation = checkedActivityOperation(input.operation);
|
|
200
|
+
if (!operation)
|
|
201
|
+
return;
|
|
202
|
+
const connectorId = operation.connectors.length === 1 ? operation.connectors[0] : "$operation";
|
|
203
|
+
recordToolActivity(context, {
|
|
204
|
+
kind: "operation",
|
|
205
|
+
operation,
|
|
206
|
+
connectorId,
|
|
207
|
+
toolName: operation.name,
|
|
208
|
+
address: `${connectorId}.${operation.name}`,
|
|
209
|
+
source: "execute_code",
|
|
210
|
+
outcome: input.outcome,
|
|
211
|
+
durationMs: input.durationMs,
|
|
212
|
+
...(input.resultBytes !== undefined ? { resultBytes: input.resultBytes } : {}),
|
|
213
|
+
attempts: 1,
|
|
214
|
+
...(input.errorCode ? { errorCode: input.errorCode } : {}),
|
|
215
|
+
...(operation.personal ? { privateCatalog: true } : {}),
|
|
216
|
+
});
|
|
217
|
+
}
|
|
@@ -5,8 +5,13 @@ import { isAgentName } from "./agent-identity.js";
|
|
|
5
5
|
import { agentObservationKeys } from "./storage/keys.js";
|
|
6
6
|
import { Storage } from "./runtime/services.js";
|
|
7
7
|
import { storageCompareAndSet, storageGet } from "./runtime/storage.js";
|
|
8
|
+
import { physicalStorageAddress } from "./runtime/request-reads.js";
|
|
8
9
|
const OBSERVATION_TIMEOUT_MS = 500;
|
|
9
10
|
const OBSERVATION_FRESHNESS_MS = 60_000;
|
|
11
|
+
// Only scalar telemetry deadlines survive requests. Weak physical-store ownership
|
|
12
|
+
// and a bounded map prevent agent/config churn from retaining unbounded state.
|
|
13
|
+
const observedUntil = new WeakMap();
|
|
14
|
+
const MAX_OBSERVED_AGENTS = 256;
|
|
10
15
|
function timestamp(raw) {
|
|
11
16
|
if (!raw || raw.length !== 24)
|
|
12
17
|
return null;
|
|
@@ -26,14 +31,31 @@ export function observeAgentAdmission(storage, name, at, timeoutMs) {
|
|
|
26
31
|
return Effect.void;
|
|
27
32
|
return Effect.gen(function* () {
|
|
28
33
|
const key = agentObservationKeys.agent(name);
|
|
34
|
+
const address = physicalStorageAddress(storage, key) ?? { store: storage, key };
|
|
35
|
+
const now = Date.parse(at);
|
|
36
|
+
let observations = observedUntil.get(address.store);
|
|
37
|
+
if (!observations)
|
|
38
|
+
observedUntil.set(address.store, (observations = new Map()));
|
|
39
|
+
if ((observations.get(address.key) ?? -Infinity) > now)
|
|
40
|
+
return;
|
|
41
|
+
const remember = (seen) => {
|
|
42
|
+
const until = Math.min(seen + OBSERVATION_FRESHNESS_MS, now + OBSERVATION_FRESHNESS_MS);
|
|
43
|
+
if (!observations.has(address.key) && observations.size >= MAX_OBSERVED_AGENTS)
|
|
44
|
+
observations.delete(observations.keys().next().value);
|
|
45
|
+
observations.set(address.key, Math.max(observations.get(address.key) ?? -Infinity, until));
|
|
46
|
+
};
|
|
29
47
|
for (let attempt = 0; attempt < 2; attempt++) {
|
|
30
48
|
const previous = yield* storageGet(key);
|
|
31
49
|
const seen = timestamp(previous);
|
|
32
50
|
// Future and recently sampled observations both need no write.
|
|
33
|
-
if (seen &&
|
|
51
|
+
if (seen && now - Date.parse(seen) < OBSERVATION_FRESHNESS_MS) {
|
|
52
|
+
remember(Date.parse(seen));
|
|
34
53
|
return;
|
|
35
|
-
|
|
54
|
+
}
|
|
55
|
+
if (yield* storageCompareAndSet(key, previous, at)) {
|
|
56
|
+
remember(now);
|
|
36
57
|
return;
|
|
58
|
+
}
|
|
37
59
|
}
|
|
38
60
|
}).pipe(Effect.provideService(Storage, storage), Effect.catchDefect((error) => Effect.fail(error)), Effect.catch(() => Effect.void), Effect.timeoutOption(Duration.millis(Math.min(timeoutMs, OBSERVATION_TIMEOUT_MS))), Effect.asVoid);
|
|
39
61
|
}
|
package/dist/agent-routes.js
CHANGED
|
@@ -26,45 +26,8 @@
|
|
|
26
26
|
* catalogs with the description built for its own surface.
|
|
27
27
|
*/
|
|
28
28
|
import { Context } from "effect";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
search: "search_tools",
|
|
32
|
-
read: "call_tool",
|
|
33
|
-
write: "call_destructive_tool",
|
|
34
|
-
program: "execute_code",
|
|
35
|
-
authorize: "authorize_connector",
|
|
36
|
-
skills: "skills",
|
|
37
|
-
}),
|
|
38
|
-
code: Object.freeze({
|
|
39
|
-
search: "connecta.search inside execute_code",
|
|
40
|
-
read: "connecta.call inside execute_code",
|
|
41
|
-
write: "connecta.call inside execute_code",
|
|
42
|
-
program: "execute_code",
|
|
43
|
-
authorize: "authorize_connector",
|
|
44
|
-
skills: "skills",
|
|
45
|
-
}),
|
|
46
|
-
native: Object.freeze({
|
|
47
|
-
search: "your host's tool search",
|
|
48
|
-
read: "the original native tool",
|
|
49
|
-
write: "the original native tool",
|
|
50
|
-
program: "your host's tool search",
|
|
51
|
-
authorize: "the returned authorization handoff",
|
|
52
|
-
skills: "skills",
|
|
53
|
-
}),
|
|
54
|
-
"native-code": Object.freeze({
|
|
55
|
-
search: "your host's tool search",
|
|
56
|
-
read: "the original native tool",
|
|
57
|
-
write: "the original native tool",
|
|
58
|
-
program: "execute_code",
|
|
59
|
-
authorize: "the returned authorization handoff",
|
|
60
|
-
skills: "skills",
|
|
61
|
-
}),
|
|
62
|
-
};
|
|
63
|
-
/** The six-tool surface's names: the default wherever no other surface applies. */
|
|
64
|
-
export const META_ROUTES = NAMES.meta;
|
|
65
|
-
export function routeNames(surface) {
|
|
66
|
-
return NAMES[surface];
|
|
67
|
-
}
|
|
29
|
+
import { META_ROUTES, routeNames } from "./route-names.js";
|
|
30
|
+
export { META_ROUTES, routeNames } from "./route-names.js";
|
|
68
31
|
/** The names for the surface a connector operation serves; meta for contexts Connecta did not build. */
|
|
69
32
|
export function routesOf(ctx) {
|
|
70
33
|
return ctx?.routes ?? META_ROUTES;
|
|
@@ -125,7 +88,7 @@ const PHRASES = {
|
|
|
125
88
|
...nativeAuthorize,
|
|
126
89
|
}),
|
|
127
90
|
};
|
|
128
|
-
const SURFACES = new Map(
|
|
91
|
+
const SURFACES = new Map(["meta", "code", "native", "native-code"].map((surface) => [routeNames(surface), surface]));
|
|
129
92
|
/** The surface a names table belongs to; meta for a table Connecta did not build. */
|
|
130
93
|
function routeSurface(routes) {
|
|
131
94
|
return (routes && SURFACES.get(routes)) ?? "meta";
|
|
@@ -227,5 +190,5 @@ export function renderRoutesAsPrograms(config) {
|
|
|
227
190
|
}
|
|
228
191
|
/** The names every reply of a deployment built from `config` uses outside native pools. */
|
|
229
192
|
export function deploymentRoutes(config) {
|
|
230
|
-
return programRouteConfigs.has(config) ?
|
|
193
|
+
return programRouteConfigs.has(config) ? routeNames("code") : META_ROUTES;
|
|
231
194
|
}
|
package/dist/auth/clerk.d.ts
CHANGED
|
@@ -4,7 +4,12 @@ export type { ClerkGateClient, ClerkUser } from "./clerk-sdk/client.js";
|
|
|
4
4
|
export interface ClerkAuthOptions {
|
|
5
5
|
publishableKey: string;
|
|
6
6
|
secretKey: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* Public base URL of this deployment: what tokens' resource and sessions'
|
|
9
|
+
* `azp` are checked against. Defaults to `ConnectaConfig.publicUrl`. One of
|
|
10
|
+
* the two is required; a request's Host never substitutes for it. A local
|
|
11
|
+
* deployment names its loopback origin, such as `http://localhost:8787`.
|
|
12
|
+
*/
|
|
8
13
|
publicUrl?: string;
|
|
9
14
|
/**
|
|
10
15
|
* OAuth client IDs dedicated to this deployment, used only when a verified
|
package/dist/auth/clerk.js
CHANGED
|
@@ -436,7 +436,22 @@ function mcpPoolSuffix(pathname) {
|
|
|
436
436
|
const match = /^\/mcp(\/[a-z0-9_-]+)$/.exec(pathname);
|
|
437
437
|
return match ? match[1] : null;
|
|
438
438
|
}
|
|
439
|
+
function assertPublicUrl(value) {
|
|
440
|
+
if (value === undefined)
|
|
441
|
+
return;
|
|
442
|
+
let url;
|
|
443
|
+
try {
|
|
444
|
+
url = typeof value === "string" ? new URL(value) : undefined;
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
url = undefined;
|
|
448
|
+
}
|
|
449
|
+
if (!url || (url.protocol !== "http:" && url.protocol !== "https:") || url.username || url.password) {
|
|
450
|
+
throw new Error("clerkAuth: `publicUrl` must be an absolute http(s) URL without credentials.");
|
|
451
|
+
}
|
|
452
|
+
}
|
|
439
453
|
export function clerkAuth(opts) {
|
|
454
|
+
assertPublicUrl(opts.publicUrl);
|
|
440
455
|
assertNoRetiredToolkitOptions("clerkAuth", opts);
|
|
441
456
|
// Before the Clerk client, so a malformed key fails as a connecta
|
|
442
457
|
// configuration error rather than however the SDK happens to treat it.
|
|
@@ -771,6 +786,7 @@ export function clerkAuth(opts) {
|
|
|
771
786
|
return {
|
|
772
787
|
kind: "clerk",
|
|
773
788
|
interactiveOperator: true,
|
|
789
|
+
...(opts.publicUrl === undefined ? { requiresPublicUrl: true } : {}),
|
|
774
790
|
recognizesCredential: (request) => {
|
|
775
791
|
if (authorizationCredential(request).kind !== "absent")
|
|
776
792
|
return !isMachineCredential(request);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
1
2
|
import { withAuthenticationDeadline } from "../auth-admission.js";
|
|
2
3
|
import { validIdentityReference } from "../identity.js";
|
|
3
4
|
import { authorizationCredential } from "../inbound-credential.js";
|
|
4
5
|
import { attachFailureFacts } from "../operator-record.js";
|
|
5
|
-
import { authCall, authFlow,
|
|
6
|
+
import { authCall, authFlow, authMethod } from "./native.js";
|
|
6
7
|
function identityString(identity, field) {
|
|
7
8
|
const value = identity[field];
|
|
8
9
|
return typeof value === "string" && validIdentityReference({ namespace: "cloudflare-access", id: value })
|
|
@@ -15,12 +16,30 @@ function unauthorized(reason) {
|
|
|
15
16
|
response: attachFailureFacts(Response.json({ error: "Cloudflare Access authentication required" }, { status: 401, headers: { "WWW-Authenticate": 'Bearer scope="openid email"', "Cache-Control": "no-store" } }), { reason }),
|
|
16
17
|
};
|
|
17
18
|
}
|
|
18
|
-
function unavailable(problem) {
|
|
19
|
+
function unavailable(problem, identityLookup) {
|
|
19
20
|
return {
|
|
20
21
|
ok: false,
|
|
21
|
-
response: attachFailureFacts(Response.json({ error: "Cloudflare Access identity unavailable", problem }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }), {
|
|
22
|
+
response: attachFailureFacts(Response.json({ error: "Cloudflare Access identity unavailable", problem }, { status: 503, headers: { "Cache-Control": "no-store", "Retry-After": "1" } }), {
|
|
23
|
+
reason: problem === "identity_unavailable" ? "access_identity_unavailable" : problem,
|
|
24
|
+
...(identityLookup ? { identityLookup } : {}),
|
|
25
|
+
}),
|
|
22
26
|
};
|
|
23
27
|
}
|
|
28
|
+
let lookups = 0;
|
|
29
|
+
let isolateCold = true;
|
|
30
|
+
const lookupSettled = () => {
|
|
31
|
+
lookups--;
|
|
32
|
+
};
|
|
33
|
+
function lookupIdentity(access) {
|
|
34
|
+
lookups++;
|
|
35
|
+
try {
|
|
36
|
+
return Promise.resolve(access.getIdentity()).finally(lookupSettled);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
lookups--;
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
24
43
|
/**
|
|
25
44
|
* Trust the identity Cloudflare Access attached to this direct Worker
|
|
26
45
|
* invocation. Access has already validated the browser session, Managed OAuth
|
|
@@ -50,16 +69,39 @@ export function cloudflareAccessAuth() {
|
|
|
50
69
|
if (typeof access.aud !== "string" || !/^[\x21-\x7e]{1,256}$/.test(access.aud))
|
|
51
70
|
return unavailable("access_audience_invalid");
|
|
52
71
|
let identity;
|
|
72
|
+
const started = performance.now();
|
|
73
|
+
const cold = isolateCold;
|
|
74
|
+
isolateCold = false;
|
|
75
|
+
let rejected = false;
|
|
53
76
|
try {
|
|
54
77
|
identity = (yield authCall(withAuthenticationDeadline, [
|
|
55
78
|
request,
|
|
56
|
-
authMethod(() => {
|
|
57
|
-
|
|
58
|
-
|
|
79
|
+
authMethod((signal) => authFlow(function* () {
|
|
80
|
+
for (let attempt = 0;; attempt++) {
|
|
81
|
+
signal.throwIfAborted();
|
|
82
|
+
try {
|
|
83
|
+
return (yield authCall(lookupIdentity, [access]));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (attempt === 1) {
|
|
87
|
+
rejected = true;
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Only a rejected identity lookup retries. A hung lookup never
|
|
92
|
+
// overlaps a second one; this sleep shares the original deadline.
|
|
93
|
+
yield Effect.sleep(100 + Math.floor(Math.random() * 151));
|
|
94
|
+
}
|
|
95
|
+
})),
|
|
59
96
|
]));
|
|
60
97
|
}
|
|
61
98
|
catch {
|
|
62
|
-
return unavailable("identity_unavailable"
|
|
99
|
+
return unavailable("identity_unavailable", {
|
|
100
|
+
outcome: rejected ? "rejected" : "timeout",
|
|
101
|
+
durationMs: Math.max(0, Math.round(performance.now() - started)),
|
|
102
|
+
cold,
|
|
103
|
+
inFlight: lookups,
|
|
104
|
+
});
|
|
63
105
|
}
|
|
64
106
|
// Access binds ctx.access.aud to its Worker application at the edge.
|
|
65
107
|
// Service credentials can pass that edge, but connecta machines must
|