@zackbart/connecta 0.21.1 → 0.22.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +7 -0
  3. package/dist/access-tokens.d.ts +2 -2
  4. package/dist/access-tokens.js +14 -2
  5. package/dist/auth/downstream-oauth.d.ts +65 -2
  6. package/dist/auth/downstream-oauth.js +408 -20
  7. package/dist/connectors/api.d.ts +2 -0
  8. package/dist/connectors/api.js +1 -0
  9. package/dist/connectors/remote-mcp.d.ts +2 -0
  10. package/dist/connectors/remote-mcp.js +14 -4
  11. package/dist/credentials.d.ts +6 -6
  12. package/dist/credentials.js +25 -21
  13. package/dist/executors/quickjs.js +4 -0
  14. package/dist/identity.d.ts +4 -0
  15. package/dist/identity.js +17 -0
  16. package/dist/index.d.ts +16 -2
  17. package/dist/index.js +6 -1
  18. package/dist/meta-tools.js +7 -2
  19. package/dist/operator-ui/generated.js +1 -1
  20. package/dist/operator-ui/model.d.ts +4 -2
  21. package/dist/operator-ui/view.js +1 -1
  22. package/dist/providers/cloudflare.d.ts +2 -0
  23. package/dist/providers/cloudflare.js +1 -0
  24. package/dist/providers/linear.d.ts +2 -0
  25. package/dist/providers/linear.js +1 -0
  26. package/dist/providers/mixpanel.d.ts +2 -0
  27. package/dist/providers/mixpanel.js +1 -0
  28. package/dist/providers/notion.d.ts +2 -0
  29. package/dist/providers/notion.js +1 -0
  30. package/dist/providers/revenuecat.d.ts +2 -0
  31. package/dist/providers/revenuecat.js +1 -0
  32. package/dist/providers/stripe.d.ts +2 -0
  33. package/dist/providers/stripe.js +1 -0
  34. package/dist/registry.d.ts +25 -0
  35. package/dist/registry.js +200 -4
  36. package/dist/routes/access-tokens.js +2 -2
  37. package/dist/routes/activity.js +4 -1
  38. package/dist/routes/credentials.js +31 -12
  39. package/dist/routes/mcp.js +17 -2
  40. package/dist/routes/oauth.js +55 -11
  41. package/dist/routes/shared.d.ts +20 -4
  42. package/dist/routes/shared.js +92 -24
  43. package/dist/routes/ui.js +32 -13
  44. package/dist/types.d.ts +28 -2
  45. package/dist/ui.d.ts +3 -3
  46. package/dist/ui.js +18 -5
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/documentation/architecture.md +31 -8
  50. package/documentation/auth.md +90 -10
  51. package/documentation/code-mode.md +4 -4
  52. package/documentation/connectors.md +13 -0
  53. package/documentation/meta-tools.md +4 -3
  54. package/documentation/operations.md +5 -3
  55. package/documentation/operator-ui.md +13 -4
  56. package/documentation/request-admission.md +2 -1
  57. package/documentation/storage-and-credentials.md +77 -4
  58. package/documentation/upgrading.md +38 -7
  59. package/ethos.md +8 -8
  60. package/examples/worker/AGENTS.md +44 -0
  61. package/examples/worker/README.md +63 -14
  62. package/examples/worker/src/index.ts +26 -22
  63. package/package.json +1 -1
  64. package/templates/node/README.md +7 -0
  65. package/templates/node/package.json +1 -1
  66. package/templates/node/src/index.ts +13 -4
package/CHANGELOG.md CHANGED
@@ -2,6 +2,89 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.22.0 — 2026-08-31
6
+
7
+ This release lets one deployment serve several authenticated people without
8
+ becoming an account system. Connector visibility comes from deployment config,
9
+ and downstream auth may be shared across the tenant or isolated per human.
10
+ Existing connectors stay shared, every visible connector stays visible, and
11
+ every interactive human stays an operator unless the deployment opts into the
12
+ new identity rules. A signed-in human may manage authentication for every
13
+ connector their code-derived view includes; operator status separately controls
14
+ tokens and global activity. Worker deployments also gain the complete Managed
15
+ OAuth callback allowlist in the shipped example and agent instructions. The
16
+ one upgrade seam is deliberate: in-flight `get_result` ids created by a named
17
+ caller do not cross into the new subject partition, so finish paging them
18
+ before deployment. Single-user deployments need no identity configuration,
19
+ and non-Worker deployments can ignore the callback-policy change.
20
+
21
+ ### Added
22
+
23
+ - **Identity-derived connector views.** `identity.connectorAccess` selects
24
+ declared connector ids from the authenticated actor, subject, and principal.
25
+ The view reaches discovery, direct and program calls, status, catalogs,
26
+ observed output shapes, and paged results. Unknown ids and resolver failures
27
+ fail closed.
28
+ - **Shared and personal connector auth.** `authScope: "personal"` partitions
29
+ connector storage, encrypted credentials, OAuth state and tokens, catalogs,
30
+ and runtime observations by a hashed principal identity. Interactive members
31
+ manage auth for every visible connector: their own partition for personal
32
+ auth, or the deployment-wide grant for shared auth. `identity.operatorAccess`
33
+ reserves deployment access tokens and global activity for configured
34
+ operators.
35
+ - **Principal-bound access tokens and OAuth callbacks.** A new connecta access
36
+ token retains its creator's principal without gaining operator rights.
37
+ Personal OAuth handoffs bind a hash of state to the initiating principal for
38
+ 15 minutes before the public callback can exchange a code.
39
+
40
+ ### Changed
41
+
42
+ - **Result pages follow authenticated subjects.** `get_result` storage is now
43
+ partitioned for every namespaced subject, including existing Clerk, Access,
44
+ and connecta-token callers. In-flight result ids created before upgrading do
45
+ not cross that storage boundary; finish paging them before deployment when
46
+ that matters.
47
+ - **Worker Managed OAuth setup.** The Worker guide, source comment, upgrade
48
+ guide, and local `AGENTS.md` require Claude's fixed callback plus ChatGPT's
49
+ stable and callback-id forms in
50
+ `dynamic_client_registration.allowed_uris`.
51
+ - **One tenant, several people.** The ethos now refuses a connecta-owned account
52
+ model while allowing externally authenticated principals, config-derived
53
+ connector visibility, and personal downstream credentials inside one tenant.
54
+ - **Reviewed Cloudflare deletion contract.** Cloudflare removed a spurious
55
+ required empty request body from Worker script deletion. Connecta already
56
+ sent no body, so only the reviewed endpoint digest changes (#517).
57
+
58
+ ## 0.21.2 — 2026-08-31
59
+
60
+ This patch closes two runtime isolation gaps: concurrent request scopes now
61
+ share one downstream OAuth refresh inside a connector runtime, and QuickJS
62
+ children no longer inherit the deployment process environment. Existing
63
+ deployments need no configuration or storage migration. OAuth deployments get
64
+ the refresh fix automatically; Node deployments using QuickJS get the tighter
65
+ child boundary automatically. Deployments using neither path can ignore this
66
+ release.
67
+
68
+ ### Changed
69
+
70
+ - **Empty QuickJS child environments.** The Node process hosting the QuickJS
71
+ guest runtime starts with an explicit empty environment, so deployment
72
+ credentials and Node startup configuration such as `NODE_OPTIONS` never
73
+ cross the child-process boundary (#515).
74
+
75
+ ### Fixed
76
+
77
+ - **One rotating-token redemption per runtime.** Concurrent request scopes for
78
+ one OAuth connector generation share the owner's refresh result or bounded
79
+ failure instead of independently redeeming the same refresh token. The gate
80
+ remains request-safe across follower and owner cancellation, storage
81
+ failures, force reauthorization, and issuer-generation changes (#514).
82
+ - **Late and byte-identical refresh races.** Generation-scoped mutation and
83
+ success identities close token-save TOCTOU and complete-flight ABA windows,
84
+ including authorization servers that preserve the refresh token or return
85
+ byte-identical credentials. The guarantee is deliberately runtime-local;
86
+ `KVStorage` still has no cross-isolate compare-and-set primitive (#514).
87
+
5
88
  ## 0.21.1 — 2026-08-30
6
89
 
7
90
  This patch aligns the handwritten Cloudflare and Notion contracts and the four
package/README.md CHANGED
@@ -82,6 +82,13 @@ payload-free activity log. Worker deployments can use Cloudflare Access for
82
82
  both MCP and operator identity; Node deployments and existing Workers can use
83
83
  Clerk.
84
84
 
85
+ One deployment may serve several authenticated people inside the same tenant.
86
+ Configuration can derive connector visibility from the admitted identity, and
87
+ each connector may keep one shared downstream grant or a separate encrypted
88
+ grant per human. Connecta does not own accounts or groups; Clerk or Cloudflare
89
+ Access remains the identity provider. See [inbound auth](./documentation/auth.md#principals-visibility-and-operators)
90
+ and [shared and personal auth](./documentation/storage-and-credentials.md#shared-and-personal-auth).
91
+
85
92
  Connecta is not a platform, a marketplace, a policy engine, or a multi-tenant
86
93
  service. Those are decisions, and the [ethos](./ethos.md) records each one
87
94
  and why.
@@ -1,4 +1,4 @@
1
- import type { InboundAuth, KVStorage } from "./types.js";
1
+ import type { IdentityReference, InboundAuth, KVStorage } from "./types.js";
2
2
  export interface AccessTokenMetadata {
3
3
  id: string;
4
4
  name: string;
@@ -24,7 +24,7 @@ export declare class AccessTokenManager {
24
24
  });
25
25
  private read;
26
26
  list(): Promise<AccessTokenMetadata[]>;
27
- create(name: unknown, createdBy: string): Promise<CreatedAccessToken>;
27
+ create(name: unknown, createdBy: string | IdentityReference): Promise<CreatedAccessToken>;
28
28
  rename(id: string, name: unknown): Promise<AccessTokenMetadata | null>;
29
29
  revoke(id: string, revokedBy: string): Promise<AccessTokenMetadata | null>;
30
30
  private authorize;
@@ -1,3 +1,4 @@
1
+ import { validIdentityReference } from "./identity.js";
1
2
  const TOKEN_PREFIX = "cta_";
2
3
  const TOKEN_BYTES = 32;
3
4
  const TOKEN_VALUE_RE = /^cta_[A-Za-z0-9_-]{43}$/;
@@ -54,6 +55,8 @@ function parseRecord(raw) {
54
55
  typeof value.tokenPrefix !== "string" ||
55
56
  typeof value.createdAt !== "string" ||
56
57
  typeof value.createdBy !== "string" ||
58
+ (value.principal !== undefined &&
59
+ !validIdentityReference(value.principal)) ||
57
60
  (value.revokedAt !== undefined &&
58
61
  typeof value.revokedAt !== "string") ||
59
62
  (value.revokedBy !== undefined &&
@@ -167,7 +170,12 @@ export class AccessTokenManager {
167
170
  tokenHash: hash,
168
171
  tokenPrefix: token.slice(0, 12),
169
172
  createdAt: new Date().toISOString(),
170
- createdBy,
173
+ createdBy: typeof createdBy === "string"
174
+ ? createdBy
175
+ : `${createdBy.namespace}:${createdBy.id}`,
176
+ ...(typeof createdBy === "string"
177
+ ? {}
178
+ : { principal: { ...createdBy } }),
171
179
  };
172
180
  await this.storage.set(recordKey(record.id), JSON.stringify(record));
173
181
  try {
@@ -219,6 +227,10 @@ export class AccessTokenManager {
219
227
  if (!record || record.revokedAt || record.tokenHash !== hash) {
220
228
  return unauthorized();
221
229
  }
222
- return { ok: true, subjectId: record.id };
230
+ return {
231
+ ok: true,
232
+ subjectId: record.id,
233
+ ...(record.principal ? { principal: { ...record.principal } } : {}),
234
+ };
223
235
  }
224
236
  }
@@ -1,5 +1,55 @@
1
- import type { OAuthClientInformationContext, OAuthClientInformationMixed, OAuthClientMetadata, OAuthClientProvider, OAuthTokens } from "@modelcontextprotocol/client";
1
+ import type { FetchLike, OAuthClientInformationContext, OAuthClientInformationMixed, OAuthClientMetadata, OAuthClientProvider, OAuthTokens } from "@modelcontextprotocol/client";
2
2
  import type { KVStorage } from "../types.js";
3
+ type OAuthRefreshFlightOutcome = {
4
+ status: "refreshed";
5
+ } | {
6
+ status: "retired";
7
+ } | {
8
+ status: "failed";
9
+ error: unknown;
10
+ };
11
+ interface OAuthRefreshFlight {
12
+ done: Promise<OAuthRefreshFlightOutcome>;
13
+ release: (outcome: OAuthRefreshFlightOutcome) => void;
14
+ stopObservingOwnerAbort: () => void;
15
+ mutationId: object;
16
+ }
17
+ /**
18
+ * Share one rotating-token redemption within one connector runtime and OAuth
19
+ * generation. The first request still owns the real fetch and response. Its
20
+ * abort signal has one bounded listener until the exact flight settles, so a
21
+ * cancellation after the response cannot strand waiters during token storage.
22
+ * Followers wait for that provider to save tokens, then re-read storage. The
23
+ * map never retains a token response or transport.
24
+ *
25
+ * This is intentionally runtime-local. KVStorage has no atomic coordination
26
+ * operation, so a second isolate can still race the same refresh token.
27
+ */
28
+ export declare class OAuthRefreshCoordinator {
29
+ private readonly flights;
30
+ /** Opaque identities only: no request promise, signal, callback, or response. */
31
+ private readonly pendingMutations;
32
+ /** One bounded latest-success slot, containing only generation + identity. */
33
+ private successfulRefresh;
34
+ /** Replaced on every map mutation, closing flight/pending ABA across awaits. */
35
+ private stateRevision;
36
+ private advanceStateRevision;
37
+ private observeAuthoritativeGeneration;
38
+ private settle;
39
+ private markMutationPending;
40
+ private finishMutation;
41
+ /** @internal Opaque basis for issuer-aware provider token reads. */
42
+ successfulRefreshIdentity(generation: string): object | undefined;
43
+ coordinatedFetch(provider: KvOAuthProvider, baseFetch: FetchLike, requestSignal?: AbortSignal): FetchLike;
44
+ /** Publish one exact owner's successful save without disturbing a newer try. */
45
+ succeedMutation(generation: string, flight: OAuthRefreshFlight): void;
46
+ /** Give joined callers a fetch/flow failure, without rejecting the gate. */
47
+ fail(generation: string, flight: OAuthRefreshFlight, error: unknown): void;
48
+ /** Finish an exact failed credential write, then publish its failure. */
49
+ failMutation(generation: string, flight: OAuthRefreshFlight, error: unknown): void;
50
+ /** Force reauthorization fences and wakes every waiter on the retired epoch. */
51
+ retire(generation: string): void;
52
+ }
3
53
  /**
4
54
  * Physical key for an OAuth value in one authorization epoch. Legacy values
5
55
  * keep their historical names so upgrades can read an existing grant. Modern
@@ -20,19 +70,31 @@ export declare class KvOAuthProvider implements OAuthClientProvider {
20
70
  private readonly connectorId;
21
71
  private readonly storage;
22
72
  private readonly redirectUri;
73
+ private readonly refreshCoordinator?;
23
74
  /**
24
75
  * The reset generation this provider's flow started under. Every OAuth value
25
76
  * it writes carries this epoch, so a late write can land after a reset without
26
77
  * becoming readable under the new generation.
27
78
  */
28
79
  private capturedGeneration;
29
- constructor(connectorId: string, storage: KVStorage, redirectUri: string);
80
+ private refreshFlight;
81
+ /** Tokens this request's issuer-aware auth flow decided to refresh. */
82
+ private refreshBasis;
83
+ constructor(connectorId: string, storage: KVStorage, redirectUri: string, refreshCoordinator?: OAuthRefreshCoordinator | undefined);
30
84
  /**
31
85
  * Stamp the force-reauth generation the current connect flow started under.
32
86
  * Called by the connector once per connect, before c.connect(). The callback
33
87
  * path captures the generation stored beside its verified state instead.
34
88
  */
35
89
  captureGeneration(gen: string): void;
90
+ /** The generation captured for this flow, before a concurrent reset. */
91
+ flowGeneration(): Promise<string>;
92
+ /** @internal Record the refresh attempt this provider owns. */
93
+ captureRefreshFlight(generation: string, flight: OAuthRefreshFlight): void;
94
+ private succeedRefreshFlight;
95
+ private failRefreshFlight;
96
+ /** True when another request saved a refresh result after this flow's read. */
97
+ refreshBasisChanged(current: OAuthTokens, generation: string): boolean;
36
98
  /**
37
99
  * The epoch this provider writes under. Direct unit/custom use lazily captures
38
100
  * the current generation; connector-driven connect and callback paths stamp it
@@ -121,3 +183,4 @@ export declare class KvOAuthProvider implements OAuthClientProvider {
121
183
  resetAuthorization(operatorDisconnected?: boolean): Promise<void>;
122
184
  invalidateCredentials(scope: "all" | "client" | "tokens" | "verifier" | "discovery"): Promise<void>;
123
185
  }
186
+ export {};