@zackbart/connecta 0.37.0 → 0.38.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 (183) hide show
  1. package/CHANGELOG.md +80 -0
  2. package/README.md +2 -2
  3. package/bin/connecta.mjs +1 -39
  4. package/bin/version.mjs +1 -1
  5. package/dist/access-tokens.js +2 -4
  6. package/dist/agent-identity.js +1 -1
  7. package/dist/auth/clerk-sdk/generated.js +2 -1
  8. package/dist/auth/cloudflare-access.d.ts +3 -1
  9. package/dist/auth/cloudflare-access.js +22 -11
  10. package/dist/auth/downstream-oauth.js +195 -270
  11. package/dist/auth/downstream-revocation.js +1 -1
  12. package/dist/auth/oauth-grant.js +11 -0
  13. package/dist/auth/oauth-token-response.js +1 -1
  14. package/dist/auth/vercel-agent.js +2 -1
  15. package/dist/auth-elicitation.js +2 -2
  16. package/dist/branding.d.ts +1 -15
  17. package/dist/branding.js +2 -2
  18. package/dist/bytes.js +42 -0
  19. package/dist/call-admission-context.js +2 -5
  20. package/dist/call-admission.js +5 -0
  21. package/dist/call-recovery.js +0 -3
  22. package/dist/catalog-cache.js +26 -7
  23. package/dist/catalog-fingerprint.js +2 -18
  24. package/dist/catalog-revision.js +9 -0
  25. package/dist/catalog-service.js +51 -20
  26. package/dist/config-defaults.d.ts +1 -0
  27. package/dist/config-defaults.js +1 -0
  28. package/dist/config-schema.d.ts +1 -1
  29. package/dist/config-schema.js +6 -2
  30. package/dist/config.d.ts +41 -14
  31. package/dist/config.js +21 -71
  32. package/dist/connector-caller.js +1 -1
  33. package/dist/connectors/api-connector.js +4 -3
  34. package/dist/connectors/guarded-fetch.d.ts +0 -6
  35. package/dist/connectors/guarded-fetch.js +1 -11
  36. package/dist/connectors/remote-mcp.js +93 -69
  37. package/dist/connectors/resource-uri.js +0 -4
  38. package/dist/credentials.js +3 -0
  39. package/dist/d1.d.ts +0 -89
  40. package/dist/d1.js +0 -34
  41. package/dist/downstream-input.js +2 -2
  42. package/dist/execute.js +161 -116
  43. package/dist/executor-result.js +2 -1
  44. package/dist/executors/quickjs-runtime.js +2 -25
  45. package/dist/executors/quickjs.js +1 -1
  46. package/dist/guest-runtime.js +5 -3
  47. package/dist/guest-types.d.ts +5 -5
  48. package/dist/http/client.js +1 -1
  49. package/dist/http/errors.js +2 -2
  50. package/dist/http/json-schema.js +1 -1
  51. package/dist/http/node-transport.js +0 -1
  52. package/dist/http/oauth.js +1 -1
  53. package/dist/http/paging.js +6 -9
  54. package/dist/http/provider.js +1 -1
  55. package/dist/http/safety.js +1 -1
  56. package/dist/http/services.js +1 -8
  57. package/dist/http/tools.js +1 -7
  58. package/dist/http/vendor.js +7 -7
  59. package/dist/identity.js +2 -6
  60. package/dist/index.js +1 -2
  61. package/dist/invocation-auth.js +2 -1
  62. package/dist/invocation.js +8 -2
  63. package/dist/meta-tools.js +27 -27
  64. package/dist/native-tools.js +2 -1
  65. package/dist/oauth-handoff.js +2 -2
  66. package/dist/operation-vocabulary.js +1 -1
  67. package/dist/operator-record.d.ts +8 -2
  68. package/dist/operator-record.js +18 -50
  69. package/dist/operator-ui/contract.d.ts +4 -2
  70. package/dist/operator-ui/generated.js +3 -3
  71. package/dist/operator-ui/model.d.ts +7 -2
  72. package/dist/program-writes.js +3 -8
  73. package/dist/provider-call.js +0 -7
  74. package/dist/provider.d.ts +1 -1
  75. package/dist/providers/_shared/google/kit.js +1 -6
  76. package/dist/providers/_shared/google/service-account.js +1 -3
  77. package/dist/providers/_shared/google/workspace.d.ts +1 -1
  78. package/dist/providers/_shared/guide.js +3 -0
  79. package/dist/providers/_shared/json.js +12 -0
  80. package/dist/providers/_shared/rest/compact-index.js +1 -1
  81. package/dist/providers/_shared/rest/kit.js +1 -3
  82. package/dist/providers/_shared/rest/operation-index.js +1 -3
  83. package/dist/providers/_shared/rest/value-safety.js +26 -28
  84. package/dist/providers/breeze/api.js +2 -7
  85. package/dist/providers/breeze/index.js +4 -2
  86. package/dist/providers/breeze/tools.js +3 -4
  87. package/dist/providers/ccb/api.js +1 -6
  88. package/dist/providers/ccb/index.js +2 -1
  89. package/dist/providers/ccb/tools.js +1 -6
  90. package/dist/providers/cloudflare/api.js +2 -2
  91. package/dist/providers/cloudflare/index.js +2 -1
  92. package/dist/providers/cloudflare/safety.js +3 -3
  93. package/dist/providers/docs/tools.js +2 -10
  94. package/dist/providers/drive/tools.js +12 -25
  95. package/dist/providers/forms/tools.js +1 -9
  96. package/dist/providers/gmail/tools.js +5 -21
  97. package/dist/providers/overflow/api.js +1 -3
  98. package/dist/providers/overflow/index.js +2 -1
  99. package/dist/providers/overflow/tools.js +3 -7
  100. package/dist/providers/planning-center/api.js +2 -1
  101. package/dist/providers/planning-center/lib/constants.d.ts +1 -1
  102. package/dist/providers/planning-center/lib/json-api.js +1 -9
  103. package/dist/providers/planning-center/tools.js +2 -1
  104. package/dist/providers/sheets/tools.js +1 -9
  105. package/dist/providers/slides/tools.js +4 -13
  106. package/dist/providers/stripe/index.js +2 -1
  107. package/dist/providers/stripe/safety.js +2 -2
  108. package/dist/providers/tithely/api.js +2 -7
  109. package/dist/providers/tithely/index.js +3 -2
  110. package/dist/providers/tithely/tools.js +7 -5
  111. package/dist/providers/vercel/api.js +1 -6
  112. package/dist/providers/vercel/lib/pin.js +1 -1
  113. package/dist/providers/vercel/safety.js +1 -1
  114. package/dist/providers/vercel/tools.js +2 -1
  115. package/dist/redaction-marker.js +2 -2
  116. package/dist/registry.d.ts +4 -1
  117. package/dist/registry.js +35 -22
  118. package/dist/request-principal.js +15 -2
  119. package/dist/request-state.js +2 -2
  120. package/dist/routes/access-tokens.js +1 -1
  121. package/dist/routes/activity-disclosure.js +1 -1
  122. package/dist/routes/connect.js +2 -2
  123. package/dist/routes/credentials.js +1 -1
  124. package/dist/routes/dispatch.js +10 -0
  125. package/dist/routes/mcp.js +23 -21
  126. package/dist/routes/oauth-management.js +1 -1
  127. package/dist/routes/operator.js +53 -7
  128. package/dist/routes/shared.d.ts +8 -8
  129. package/dist/routes/shared.js +22 -15
  130. package/dist/routes/ui-config.js +6 -6
  131. package/dist/routes/ui-data.js +2 -2
  132. package/dist/routes/ui.js +9 -4
  133. package/dist/runtime/call-admission.js +1 -1
  134. package/dist/runtime/call.js +1 -1
  135. package/dist/runtime/settlement.js +1 -1
  136. package/dist/runtime/shared-read.js +2 -15
  137. package/dist/sent-secrets.d.ts +14 -6
  138. package/dist/sent-secrets.js +231 -85
  139. package/dist/server.js +4 -4
  140. package/dist/skills.js +8 -35
  141. package/dist/sqlite.d.ts +0 -19
  142. package/dist/sqlite.js +2 -83
  143. package/dist/storage/keys.js +29 -124
  144. package/dist/storage/sql-native.js +14 -111
  145. package/dist/storage/sql.d.ts +1 -24
  146. package/dist/storage/sql.js +1 -12
  147. package/dist/tool-error.js +4 -0
  148. package/dist/types.d.ts +4 -3
  149. package/dist/ui.js +3 -0
  150. package/dist/url-safety.js +0 -4
  151. package/dist/usage-guide.js +2 -2
  152. package/dist/version.d.ts +1 -1
  153. package/dist/version.js +1 -1
  154. package/dist/worker.js +1 -1
  155. package/documentation/README.md +6 -42
  156. package/documentation/architecture.md +190 -507
  157. package/documentation/auth.md +172 -58
  158. package/documentation/code-mode.md +98 -66
  159. package/documentation/deploying.md +19 -40
  160. package/documentation/integrating.md +8 -19
  161. package/documentation/meta-tools.md +23 -6
  162. package/documentation/migration-0.33.md +5 -5
  163. package/documentation/migration-0.34.md +2 -4
  164. package/documentation/migration-retired-layouts.md +74 -0
  165. package/documentation/operator-tests.md +19 -119
  166. package/documentation/operator-ui.md +46 -8
  167. package/documentation/provider-auth-migration.md +4 -10
  168. package/documentation/provider-migration-0.29.md +4 -15
  169. package/examples/worker/AGENTS.md +4 -6
  170. package/examples/worker/README.md +20 -115
  171. package/guides/connecting-agents.md +7 -30
  172. package/guides/how-it-works.md +2 -2
  173. package/guides/safety-and-trust.md +4 -1
  174. package/package.json +19 -19
  175. package/templates/node/README.md +9 -26
  176. package/templates/node/package.json +1 -1
  177. package/dist/auth/oauth-v2-migration.js +0 -135
  178. package/dist/provider-call-facts.d.ts +0 -115
  179. package/dist/provider-call.d.ts +0 -112
  180. package/dist/storage/d1-copy.js +0 -255
  181. package/dist/storage/sql-schema.d.ts +0 -6
  182. package/examples/worker/kv-to-d1.wrangler.jsonc +0 -26
  183. package/examples/worker/scripts/copy-kv-to-d1.mjs +0 -162
package/dist/config.js CHANGED
@@ -79,11 +79,12 @@ const discovery = {
79
79
  const calls = {
80
80
  /**
81
81
  * Deadline (ms) for `call_tool`/`call_destructive_tool` calls that pass no
82
- * `timeoutMs`. An explicit per-call value wins. Opt-in: unset by default, so
83
- * existing long-running calls gain no surprise deadline.
82
+ * `timeoutMs`. An explicit per-call value wins. Unset by default, when those
83
+ * calls fall back to `execute.hostCallTimeoutMs`.
84
84
  *
85
- * Each call makes one attempt. `execute_code` host calls are
86
- * unaffected because they already carry their own bound.
85
+ * Each call makes one attempt. `connecta.call` inside `execute_code` uses
86
+ * it too, ahead of `execute.hostCallTimeoutMs`; `connecta.result`,
87
+ * `connecta.skill` and `connecta.read` do not.
87
88
  */
88
89
  defaultTimeoutMs: whole({ min: 1 }),
89
90
  /**
@@ -111,9 +112,12 @@ const execute = {
111
112
  /** Host calls one program may make. Default 20. */
112
113
  maxHostCalls: whole({ min: 1, default: D.execute.maxHostCalls }),
113
114
  /**
114
- * Deadline for each host call a program makes, in milliseconds. Default
115
- * 15_000. Raise it for providers whose legitimate calls run longer, such as
116
- * analytics queries; `call_tool`'s own `timeoutMs` is unaffected.
115
+ * Deadline for a program's `connecta.result`, `connecta.skill` and
116
+ * `connecta.read`, in milliseconds. Default 15_000. Discovery uses
117
+ * `discovery.probeTimeoutMs`. `connecta.call` and the direct call tools use it only when
118
+ * `calls.defaultTimeoutMs` is unset, and an explicit per-call `timeoutMs`
119
+ * always wins. Raise it for providers whose legitimate calls run longer,
120
+ * such as analytics queries.
117
121
  */
118
122
  hostCallTimeoutMs: whole({ min: 1, default: D.execute.hostCallTimeoutMs }),
119
123
  /**
@@ -129,6 +133,14 @@ const execute = {
129
133
  * budget every call already spends. Default 10.
130
134
  */
131
135
  maxWrites: whole({ min: 1, default: D.execute.maxWrites }),
136
+ /**
137
+ * Serialized characters a program may return, shared with emitted text,
138
+ * before Connecta stashes the return and answers with a paging notice.
139
+ * Default 24_000 (~6k tokens). Raise it when agents need large results whole
140
+ * and the model's context can afford them: each paging round trip costs a
141
+ * model step. From 4_000 to 1_000_000.
142
+ */
143
+ maxResultChars: whole({ min: 4_000, max: 1_000_000, default: D.execute.maxResultChars }),
132
144
  };
133
145
  function admissionPool(defaults) {
134
146
  return {
@@ -492,48 +504,6 @@ const connectaConfig = {
492
504
  readOnly: required(opaque({ check: assertFlag }), (path) => READ_ONLY_REQUIRED(path)),
493
505
  };
494
506
  const CONFIG = object(connectaConfig);
495
- /** Pause-only options #672 removed with `resume_execution`. */
496
- const RETIRED_PAUSE_OPTIONS = ["ConnectaConfig.execute.resumableWrites", "ConnectaConfig.execute.pausedRunTtlSeconds"];
497
- /** Access options pools replaced (#837, decisions/0006). */
498
- const isRetiredPoolGrant = (path) => /^ConnectaConfig\.pools\.[^.]+\.grant$/.test(path);
499
- /** Management resolvers maintainers and pool membership replaced (#837, decisions/0006). */
500
- const RETIRED_IDENTITY = [
501
- ["credentialAdministration", "maintainers administer shared credentials and OAuth grants. List them in maintainers."],
502
- [
503
- "personalConnection",
504
- "pool members connect and disconnect their own personal connectors in their pools, and maintainers their own. Add people to a pool's members.",
505
- ],
506
- [
507
- "accessTokenManagement",
508
- "maintainers issue, rotate, and revoke agent tokens from an interactive session. List them in maintainers.",
509
- ],
510
- ["activityAccess", "maintainers read activity; activity.readGate can still narrow it. List them in maintainers."],
511
- ];
512
- /** `trust`, on the deployment or a pool, was replaced by `readOnly` (#820). */
513
- const isRetiredTrust = (path) => path === "ConnectaConfig.trust" || (path.startsWith("ConnectaConfig.pools.") && path.endsWith(".trust"));
514
- /**
515
- * `identity` was removed whole (#837), so the walk reports only the section.
516
- * Name each key it still holds, read by descriptor without running a getter
517
- * or echoing a value, so a retired resolver's message names its replacement.
518
- */
519
- function expandRetiredIdentity(config, paths) {
520
- if (!paths.includes("ConnectaConfig.identity"))
521
- return paths;
522
- let keys = [];
523
- try {
524
- const descriptor = Reflect.getOwnPropertyDescriptor(config, "identity");
525
- const value = descriptor && "value" in descriptor ? descriptor.value : undefined;
526
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
527
- keys = Reflect.ownKeys(value).filter((key) => typeof key === "string");
528
- }
529
- }
530
- catch {
531
- keys = [];
532
- }
533
- if (keys.length === 0)
534
- return paths;
535
- return paths.flatMap((path) => path === "ConnectaConfig.identity" ? keys.map((key) => `ConnectaConfig.identity.${key}`) : [path]);
536
- }
537
507
  function rejectUnknownOptions(paths) {
538
508
  if (paths.length === 0)
539
509
  return;
@@ -547,27 +517,6 @@ function rejectUnknownOptions(paths) {
547
517
  : "") +
548
518
  (paths.includes("ConnectaConfig.execute.approval")
549
519
  ? "\nexecute.approval was removed. The MCP client owns approval; set readOnly: true on the deployment or a pool to refuse writes."
550
- : "") +
551
- (paths.some((path) => RETIRED_PAUSE_OPTIONS.includes(path))
552
- ? "\nPrograms no longer pause at writes, so there is nothing to configure: writable endpoints let programs and call_destructive_tool write; readOnly endpoints refuse every write (issue #672). Delete the option."
553
- : "") +
554
- (paths.includes("ConnectaConfig.identity.connectorAccess")
555
- ? "\nidentity.connectorAccess was removed (#837): pools are the access model. List who may use plain /mcp in maintainers, and give each pool its tools and members. A guarded { tool, requireReadOnly: true } grant becomes a pool with readOnly: true."
556
- : "") +
557
- RETIRED_IDENTITY.filter(([key]) => paths.includes(`ConnectaConfig.identity.${key}`))
558
- .map(([key, replacement]) => `\nidentity.${key} was removed (#837): ${replacement}`)
559
- .join("") +
560
- (paths.some((path) => path === "ConnectaConfig.identity" || path.startsWith("ConnectaConfig.identity."))
561
- ? "\nThe identity section was removed (#837): maintainers and pool members decide access and management. Delete it."
562
- : "") +
563
- (paths.includes("ConnectaConfig.grant")
564
- ? "\nThe top-level grant was removed (#837); list the people who may use plain /mcp in maintainers."
565
- : "") +
566
- (paths.some(isRetiredPoolGrant)
567
- ? "\nPool grant was removed (#837); list who may open the pool in its members."
568
- : "") +
569
- (paths.some(isRetiredTrust)
570
- ? '\ntrust was removed (#820); set readOnly, which every deployment now chooses explicitly. trust: "read-only" becomes readOnly: true, which now also refuses call_destructive_tool. trust: "trusted" becomes readOnly: false. A pool inherits the deployment readOnly unless it sets its own.'
571
520
  : ""));
572
521
  }
573
522
  /** Checks that span fields; each value is already individually valid. */
@@ -598,7 +547,7 @@ export function resolveConfig(config) {
598
547
  */
599
548
  export function readConfig(config) {
600
549
  const read = readPlain(config, "ConnectaConfig", CONFIG);
601
- rejectUnknownOptions(expandRetiredIdentity(config, read.unknown));
550
+ rejectUnknownOptions(read.unknown);
602
551
  const input = read.value;
603
552
  const parsed = CONFIG.parse(input, "ConnectaConfig");
604
553
  assertCoherent(parsed);
@@ -648,5 +597,6 @@ export function executeLimits(config) {
648
597
  hostCallTimeoutMs: config.execute.hostCallTimeoutMs,
649
598
  watchdogMs: config.execute.watchdogMs,
650
599
  maxWrites: config.execute.maxWrites,
600
+ maxResultChars: config.execute.maxResultChars,
651
601
  };
652
602
  }
@@ -17,7 +17,7 @@
17
17
  // registry view that request receives, and `api()` hands handlers that same
18
18
  // context object; no argument or program can name it, and a header only
19
19
  // through an inbound-auth provider configured to read one (a bearer's asserted
20
- // principal). A context no request admitted, such as an operator probe, has no caller, and a delegated provider fails closed on it,
20
+ // principal). A context no request admitted has no caller, and a delegated provider fails closed on it,
21
21
  // as it does on the anonymous caller an open deployment admits.
22
22
  import { Caller, serviceFor, setViewService } from "./runtime/call.js";
23
23
  const callerIdentities = new WeakSet();
@@ -1,3 +1,4 @@
1
+ import { wrapViewCredentials } from "../runtime/call.js";
1
2
  import { storedAuthObservation, withStoredAuth } from "../auth/stored-observation.js";
2
3
  import { Effect, Exit } from "effect";
3
4
  import { registerConnectorVocabulary } from "../operation-vocabulary.js";
@@ -13,7 +14,7 @@ import { describedEndpoint, describedOrigin, describedTools } from "../described
13
14
  import { READ_ONLY, CALL_ADMISSION, CREDENTIAL, USAGE_GUIDE } from "./option-shapes.js";
14
15
  import { assertStaticToolNames } from "../tool-name.js";
15
16
  import { registerInvocationAuth } from "../invocation-auth.js";
16
- import { redactCatalog, redactSentSecrets, sentSecretsFetch, trackCredentialReads } from "../sent-secrets.js";
17
+ import { redactCatalog, redactSentSecrets, sentSecretsFetch } from "../sent-secrets.js";
17
18
  import { callClassification } from "../call-classification.js";
18
19
  import { classifyTool } from "../tool-safety.js";
19
20
  import { closeProviderCall, enterProviderBoundary, inLogicalCall, knownProviderFailure, nestedProviderRequests, providerBoundary, providerCallContext, providerFetch, providerRequests, tracksRequests, } from "../provider-call.js";
@@ -24,7 +25,7 @@ export function defined(value) {
24
25
  }
25
26
  /**
26
27
  * Three things a hand-written surface is refused for at construction rather
27
- * than in production (#340): no description (discovery has nothing to route on
28
+ * than in production: no description (discovery has nothing to route on
28
29
  * and a guess costs a call), no explicit `readOnlyHint` (connecta never infers
29
30
  * the safety class, so an unclassified tool is a deployment bug), and an
30
31
  * `inputSchema` the validator cannot compile (declaring one is optional;
@@ -173,7 +174,7 @@ export function apiConnector(id, opts, oauth) {
173
174
  callTool: (name, args, ctx, options) => runEdge(core(name, args, ctx, options)),
174
175
  };
175
176
  const core = (name, args, ctx) => Effect.gen(function* () {
176
- trackCredentialReads(ctx);
177
+ wrapViewCredentials(ctx);
177
178
  enterProviderBoundary(ctx, connector);
178
179
  const tool = byName.get(name);
179
180
  // Refusals before the handler runs are known failures: nothing was sent.
@@ -1,5 +1,4 @@
1
1
  export { retryAfterMs } from "../http-policy.js";
2
- import { type ProviderRequest } from "../provider-call.js";
3
2
  import type { ConnectorContext } from "../types.js";
4
3
  /** The methods a hand-written provider surface actually uses. */
5
4
  type GuardedMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
@@ -182,11 +181,6 @@ type GuardedFetcher = (url: string, init: RequestInit, ctx: ConnectorContext) =>
182
181
  export declare function oauthBearer(provider: string): Pick<GuardedFetchOptions, "authenticate" | "fetch">;
183
182
  /** Send one guarded request and map its response with provider knowledge. */
184
183
  export type GuardedTransport = <T>(request: GuardedRequest, ctx: ConnectorContext, map: GuardedResponseMapper<T>) => Promise<T>;
185
- /**
186
- * Test-only legacy response lookup for renewal-provenance fixtures outside
187
- * the kit. Production renewal uses the kit ledger. Undefined for other objects.
188
- */
189
- export declare function answeredRequest(response: object): ProviderRequest | undefined;
190
184
  /**
191
185
  * Build the transport a connector sends every request through. `maxResponseBytes`
192
186
  * is required rather than defaulted: what counts as an absurd response is a fact
@@ -3,7 +3,7 @@ export { retryAfterMs } from "../http-policy.js";
3
3
  import { callerSignal, duringViewCall, settleOnClose, viewSignal } from "../runtime/call.js";
4
4
  /**
5
5
  * The safety machinery every hand-written HTTP surface needs, extracted once
6
- * (#341): path confinement, encoded query and framed bodies, per-request
6
+ *: path confinement, encoded query and framed bodies, per-request
7
7
  * credential headers applied last, a refused redirect, a bounded read, and
8
8
  * network-failure normalization. It owns no vendor-specific meaning and never
9
9
  * invents an auth scheme, because the same 403 means an ungranted
@@ -280,15 +280,6 @@ async function jsonResult(read) {
280
280
  return { parseError: cause };
281
281
  }
282
282
  }
283
- // Each response a mapper reads, and the ledger entry of the request it answered.
284
- const answeredRequests = new WeakMap();
285
- /**
286
- * Test-only legacy response lookup for renewal-provenance fixtures outside
287
- * the kit. Production renewal uses the kit ledger. Undefined for other objects.
288
- */
289
- export function answeredRequest(response) {
290
- return answeredRequests.get(response);
291
- }
292
283
  /**
293
284
  * Build the transport a connector sends every request through. `maxResponseBytes`
294
285
  * is required rather than defaulted: what counts as an absurd response is a fact
@@ -463,7 +454,6 @@ export function guardedFetch(options) {
463
454
  throw oversized(provider, limit, `a declared ${declared} bytes`);
464
455
  }
465
456
  const answer = boundedResponse(provider, response, limit, signal);
466
- answeredRequests.set(answer, dispatch);
467
457
  const abort = () => {
468
458
  void response?.body?.cancel().catch(() => { });
469
459
  };
@@ -1,3 +1,5 @@
1
+ import { wrapViewCredentials } from "../runtime/call.js";
2
+ import { sha256Hex, utf8Length } from "../bytes.js";
1
3
  import { withStoredAuth } from "../auth/stored-observation.js";
2
4
  import { authorizationParamsFor } from "../auth/oauth-authorization-params.js";
3
5
  import { authView } from "../auth/native-call.js";
@@ -6,12 +8,12 @@ import { isOAuthCredentialRequest } from "../auth/oauth-token-response.js";
6
8
  import { deriveContext } from "../derived-context.js";
7
9
  import { operationContext, openRequest, viewServices, viewSignal } from "../runtime/call.js";
8
10
  import { assertRemoteOAuthClient, declareSelfHostedClient, downstreamClientMetadata, downstreamRedirectUri, remoteClientAuthMethod, selfHostedClientUrl, trackRemoteClientRequest, } from "../auth/downstream-client-metadata.js";
9
- import { AuthorizationServerMismatchError, Client, InsecureTokenEndpointError, InsufficientScopeError, IssuerMismatchError, MissingRequiredClientCapabilityError, OAuthClientFlowError, OAuthError, ProtocolError, RegistrationRejectedError, ResourceNotFoundError, SdkError, SdkErrorCode, SdkHttpError, SseError, UnsupportedProtocolVersionError, UrlElicitationRequiredError, isInputRequiredResult, fromJsonSchema, isJSONRPCErrorResponse, isJSONRPCNotification, specTypeSchemas, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
11
+ import { AuthorizationServerMismatchError, Client, InsecureTokenEndpointError, InsufficientScopeError, IssuerMismatchError, MissingRequiredClientCapabilityError, OAuthClientFlowError, OAuthError, ProtocolError, RegistrationRejectedError, ResourceNotFoundError, SdkError, SdkErrorCode, SdkHttpError, SseError, UnsupportedProtocolVersionError, UrlElicitationRequiredError, isInputRequiredResult, fromJsonSchema, isJSONRPCErrorResponse, isJSONRPCNotification, extractWWWAuthenticateParams, specTypeSchemas, StreamableHTTPClientTransport, UnauthorizedError, } from "@modelcontextprotocol/client";
10
12
  import { Deferred, Duration, Effect, Exit, Scope } from "effect";
11
13
  import { assertOAuthScope, authorizingContext, codeExchangeResponse, KvOAuthProvider, OAuthRequestNotSentError, refreshCoordinatorsByPartition, } from "../auth/downstream-oauth.js";
12
14
  import { trackOAuthStartReset } from "../auth/oauth-start-reset.js";
13
15
  import { byteReadResponse } from "../byte-read-response.js";
14
- import { redactCatalog, redactSentSecrets, requestSentSecrets, sentSecretsFor, shortSecretWarning, trackCredentialReads, } from "../sent-secrets.js";
16
+ import { redactCatalog, redactSentSecrets, requestSentSecrets, sentSecretsFor, shortSecretWarning, } from "../sent-secrets.js";
15
17
  import { MAX_CATALOG_TOOLS, MAX_SERIALIZED_CATALOG_BYTES } from "../catalog-limits.js";
16
18
  import { catalogClientOptions, catalogItems, observeCompletedCatalogRefresh, closeCatalogCacheScope, observeCatalogChange, observeCatalogFetch, observeCatalogExpiry, } from "../catalog-cache.js";
17
19
  import { connectorScopeCleanupClaimed } from "../connector-scope.js";
@@ -28,7 +30,7 @@ import { registerInvocationAuth } from "../invocation-auth.js";
28
30
  import { downstreamInputCapabilities, assertDownstreamOutputSafe, downstreamWriteContinuation, } from "../downstream-input-context.js";
29
31
  import { detach, runEdge } from "../runtime/run.js";
30
32
  import { assertKnownOptions, keys, optionsOf, strings } from "../config-schema.js";
31
- import { requestPrincipalFor } from "../request-principal.js";
33
+ import { requestPrincipalFor, tagPrincipalForwarding } from "../request-principal.js";
32
34
  import { describedEndpoint, describedUrl } from "../described.js";
33
35
  import { retryAfterMs } from "../http-policy.js";
34
36
  import { resourceUriMatchesTemplates } from "./resource-uri.js";
@@ -115,32 +117,11 @@ const MAX_SKILL_BYTES = 16 * 1024 * 1024;
115
117
  // Leave room for the envelope while accepting every file up to the spec limit.
116
118
  const MAX_SKILL_READ_RPC_BYTES = 6 * MAX_SKILL_BYTES + 1024 * 1024;
117
119
  /** Count UTF-8 without allocating another full copy of a bounded response. */
118
- function skillTextBytes(value) {
119
- let bytes = 0;
120
- for (let i = 0; i < value.length; i++) {
121
- const code = value.charCodeAt(i);
122
- if (code < 0x80)
123
- bytes++;
124
- else if (code < 0x800)
125
- bytes += 2;
126
- else if (code >= 0xd800 &&
127
- code <= 0xdbff &&
128
- i + 1 < value.length &&
129
- value.charCodeAt(i + 1) >= 0xdc00 &&
130
- value.charCodeAt(i + 1) <= 0xdfff) {
131
- bytes += 4;
132
- i++;
133
- }
134
- else
135
- bytes += 3;
136
- }
137
- return bytes;
138
- }
139
120
  function skillObject(value) {
140
121
  return typeof value === "object" && value !== null && !Array.isArray(value);
141
122
  }
142
123
  function skillJsonBytes(value) {
143
- return skillTextBytes(JSON.stringify(value));
124
+ return utf8Length(JSON.stringify(value));
144
125
  }
145
126
  function validSkill(value) {
146
127
  if (!skillObject(value) ||
@@ -229,9 +210,7 @@ function skillReadSchema(uri) {
229
210
  return { issues: [{ message: "Unexpected skill resource contents." }] };
230
211
  }
231
212
  const mimeType = typeof content.mimeType === "string" ? { mimeType: content.mimeType } : {};
232
- if (typeof content.text === "string" &&
233
- !("blob" in content) &&
234
- skillTextBytes(content.text) <= MAX_SKILL_BYTES) {
213
+ if (typeof content.text === "string" && !("blob" in content) && utf8Length(content.text) <= MAX_SKILL_BYTES) {
235
214
  return { value: [{ uri, ...mimeType, text: content.text }] };
236
215
  }
237
216
  if (typeof content.blob === "string" &&
@@ -668,18 +647,6 @@ function base64Utf8(value) {
668
647
  }
669
648
  return btoa(binary);
670
649
  }
671
- /**
672
- * Hex SHA-256 of a credential, used only to notice that a cached client
673
- * connected with a value the vault no longer holds. WebCrypto rather than a
674
- * `node:` hash: the whole core has to keep running unchanged on Workers.
675
- */
676
- async function digestOf(value) {
677
- const bytes = new Uint8Array(await crypto.subtle.digest("SHA-256", encoder.encode(value)));
678
- let hex = "";
679
- for (const byte of bytes)
680
- hex += byte.toString(16).padStart(2, "0");
681
- return hex;
682
- }
683
650
  /**
684
651
  * Assemble the one header a credential-auth connector sends.
685
652
  *
@@ -1132,13 +1099,16 @@ export function remoteMcp(id, opts) {
1132
1099
  // A closed scope keeps its (emptied) entry, so a late or future lookup finds
1133
1100
  // it closed rather than recreating an ownerless connection under it.
1134
1101
  const states = new WeakMap();
1102
+ // Post-connect auth checks keep the exact provider that bound this client,
1103
+ // even if another attempt has since replaced the request scope's state.
1104
+ const connectedProviders = new WeakMap();
1135
1105
  const cacheOwner = {};
1136
1106
  // A cached transport may dispatch concurrent calls and listings. Register
1137
1107
  // its actual sent auth with every operation using that client, including
1138
1108
  // handshake/discovery requests and OAuth rotations. Remove settled users.
1139
1109
  const activeContexts = new WeakMap();
1140
1110
  // Resource headers are non-secret context by contract. They never join the
1141
- // sent set, so arguments and results equal to them pass unchanged (#1108).
1111
+ // sent set, so arguments and results equal to them pass unchanged.
1142
1112
  // An argument mirrored as `Mcp-Param-*` carrying a resource value is the
1143
1113
  // same context under a name the tracker may read as credential-shaped.
1144
1114
  const resourceValues = new Set(Object.values(extraHeaders ?? {}));
@@ -1451,11 +1421,22 @@ export function remoteMcp(id, opts) {
1451
1421
  }
1452
1422
  }));
1453
1423
  adapted.onUnauthorized = authMethod((ctx) => authCall(flow, [
1454
- authMethod((trace) => authInvoke(Reflect, "apply", [
1455
- onUnauthorized,
1456
- adapted,
1457
- [{ ...ctx, fetchFn: trace(ctx.fetchFn ?? fetch) }],
1458
- ])),
1424
+ authMethod((trace) => authFlow(function* () {
1425
+ // This seam receives every 401, during status, starts, and live
1426
+ // calls. Use the SDK's parser so the check covers exactly the
1427
+ // challenged URL it would honor. The provider separately drops
1428
+ // foreign cached resource metadata on every SDK discovery read,
1429
+ // including callbacks and flows without a challenge.
1430
+ const { resourceMetadataUrl } = extractWWWAuthenticateParams(ctx.response);
1431
+ if (resourceMetadataUrl && resourceMetadataUrl.origin !== destination.origin) {
1432
+ throw new RemoteMcpDestinationError(id, "resource metadata outside the configured resource origin is not allowed");
1433
+ }
1434
+ yield authInvoke(Reflect, "apply", [
1435
+ onUnauthorized,
1436
+ adapted,
1437
+ [{ ...ctx, fetchFn: trace(ctx.fetchFn ?? fetch) }],
1438
+ ]);
1439
+ })),
1459
1440
  renewalSignal(ctx.response),
1460
1441
  ]));
1461
1442
  internals._stepUpAuthorize = authMethod((challenge, retries) => authCall(flow, [
@@ -1487,6 +1468,16 @@ export function remoteMcp(id, opts) {
1487
1468
  super("auth_required", message);
1488
1469
  }
1489
1470
  }
1471
+ /**
1472
+ * A request resolver threw something untyped. Calls still fail with
1473
+ * `auth_required`, but status reports an error: the resolver's own outage
1474
+ * is not an authorization the operator can grant.
1475
+ */
1476
+ class UnresolvedTokenError extends ConnectorCallError {
1477
+ constructor() {
1478
+ super("auth_required", "The request token could not be resolved.");
1479
+ }
1480
+ }
1490
1481
  /**
1491
1482
  * Read this request's credential. Called before every connect and before
1492
1483
  * trusting any cached client, so an operator's replacement is picked up
@@ -1502,7 +1493,7 @@ export function remoteMcp(id, opts) {
1502
1493
  catch (error) {
1503
1494
  if (error instanceof ConnectorCallError)
1504
1495
  throw error;
1505
- throw new ConnectorCallError("auth_required", "The request token could not be resolved.");
1496
+ throw new UnresolvedTokenError();
1506
1497
  }
1507
1498
  ctx.signal?.throwIfAborted();
1508
1499
  if (typeof value !== "string" ||
@@ -1781,7 +1772,13 @@ export function remoteMcp(id, opts) {
1781
1772
  },
1782
1773
  }
1783
1774
  : {}),
1784
- }), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), metadataUrl, oauthScope, undefined, { name: ctx.oauthClientName, client: staticClient, secrets: sentSecretsFor(ctx), authorizationParams });
1775
+ }), (reset) => trackOAuthStartReset(ctx.requestScope ?? ctx, reset), metadataUrl, oauthScope, undefined, {
1776
+ name: ctx.oauthClientName,
1777
+ client: staticClient,
1778
+ secrets: sentSecretsFor(ctx),
1779
+ authorizationParams,
1780
+ resourceOrigin: destination.origin,
1781
+ });
1785
1782
  if (state)
1786
1783
  state.provider = provider;
1787
1784
  return provider;
@@ -1811,6 +1808,7 @@ export function remoteMcp(id, opts) {
1811
1808
  headers.set(name, value);
1812
1809
  return { ...init, headers };
1813
1810
  };
1811
+ const responseOwner = Context.get(viewServices(ctx), RequestScope);
1814
1812
  const initiatingSignal = ctx.signal;
1815
1813
  ctx = deriveContext(operationContext(ctx), signal ? { signal } : {});
1816
1814
  if (opts._transportFactory)
@@ -1897,6 +1895,12 @@ export function remoteMcp(id, opts) {
1897
1895
  return writeContinuation ? send(input, { ...init, redirect: "manual" }) : send(input, init);
1898
1896
  })();
1899
1897
  return pending.then((response) => {
1898
+ // RPC fetch adapters can return a disposable Response. Own it at
1899
+ // the physical Promise leaf, including a late answer after close,
1900
+ // and keep its body live until the existing request boundary.
1901
+ const dispose = response[Symbol.dispose];
1902
+ if (dispose)
1903
+ responseOwner.own(() => dispose.call(response));
1900
1904
  if (entry)
1901
1905
  recordProviderResponse(entry, response.status);
1902
1906
  if (flight?.write) {
@@ -2140,7 +2144,7 @@ export function remoteMcp(id, opts) {
2140
2144
  credentialFramed = credentialHeaderValue(credentialScheme, credentialValue);
2141
2145
  // The principal joins the client's identity: another principal never
2142
2146
  // reuses this client, even if its resolver returns the same token.
2143
- credentialDigest = (yield authCall(digestOf, [
2147
+ credentialDigest = (yield authCall(sha256Hex, [
2144
2148
  principal ? JSON.stringify([principal, credentialValue]) : credentialValue,
2145
2149
  ]));
2146
2150
  if ((state.client || state.connecting) &&
@@ -2265,7 +2269,7 @@ export function remoteMcp(id, opts) {
2265
2269
  clientName: ctx.oauthClientName,
2266
2270
  }
2267
2271
  : undefined;
2268
- const negotiationDigest = yield* promised(authMethod(() => authCall(digestOf, [
2272
+ const negotiationDigest = yield* promised(authMethod(() => authCall(sha256Hex, [
2269
2273
  JSON.stringify([
2270
2274
  opts.url,
2271
2275
  opts.versionNegotiation ?? "auto",
@@ -2312,7 +2316,7 @@ export function remoteMcp(id, opts) {
2312
2316
  provider
2313
2317
  ? authMethod(() => authFlow(function* () {
2314
2318
  const tokens = yield authInvoke(provider, "tokens", []);
2315
- return yield authCall(digestOf, [JSON.stringify(tokens)]);
2319
+ return yield authCall(sha256Hex, [JSON.stringify(tokens)]);
2316
2320
  }))
2317
2321
  : undefined,
2318
2322
  ])));
@@ -2396,6 +2400,8 @@ export function remoteMcp(id, opts) {
2396
2400
  }
2397
2401
  }
2398
2402
  state.client = c;
2403
+ if (provider)
2404
+ connectedProviders.set(c, provider);
2399
2405
  state.connectedGeneration = genAtStart;
2400
2406
  state.authRequired = false;
2401
2407
  const discover = c.getDiscoverResult();
@@ -2464,8 +2470,8 @@ export function remoteMcp(id, opts) {
2464
2470
  * Read after connecting: connect has bound the epoch, migrated an older
2465
2471
  * layout, and refused a disconnected one within the scope's lifetime.
2466
2472
  */
2467
- const granted = authMethod((ctx) => authFlow(function* () {
2468
- return Boolean(yield authInvoke(newProvider(ctx), "tokens", []));
2473
+ const granted = authMethod((provider) => authFlow(function* () {
2474
+ return Boolean(yield authInvoke(provider, "tokens", []));
2469
2475
  }));
2470
2476
  /**
2471
2477
  * Start consent for a downstream that answered without challenging. The
@@ -2474,7 +2480,7 @@ export function remoteMcp(id, opts) {
2474
2480
  * well-known URLs, the same issuer and resource checks, consent state, and
2475
2481
  * CIMD/DCR/static client order. Nothing is sent to the MCP endpoint.
2476
2482
  */
2477
- const startUnchallenged = authMethod((ctx) => authFlow(function* () {
2483
+ const startUnchallenged = authMethod((ctx, connectedProvider) => authFlow(function* () {
2478
2484
  const required = {
2479
2485
  state: "auth_required",
2480
2486
  message: "Authorization required \u2014 open the URL to connect.",
@@ -2482,10 +2488,12 @@ export function remoteMcp(id, opts) {
2482
2488
  // A test transport carries no OAuth flow to start.
2483
2489
  if (opts._transportFactory)
2484
2490
  return required;
2485
- // Bound to the live epoch, which the connect just found connected: a
2486
- // reset or disconnect meanwhile fails this flow's writes.
2491
+ // A cached connection may belong to a passive status or call. Give this
2492
+ // explicit start its own authorization-capable provider without widening
2493
+ // the transport's authority or adopting a Restart's replacement epoch.
2494
+ const epoch = (yield authInvoke(connectedProvider, "flowEpoch", []));
2487
2495
  const provider = newProvider(ctx);
2488
- (yield authInvoke(provider, "beginFlow", []));
2496
+ (yield authInvoke(provider, "beginFlow", [epoch]));
2489
2497
  // Never started, this transport holds no session or stream to close.
2490
2498
  const transport = buildTransport(ctx, provider);
2491
2499
  const internals = transport;
@@ -2967,6 +2975,8 @@ export function remoteMcp(id, opts) {
2967
2975
  return yield authView(ctx, authMethod(() => authValue(() => closing)), true);
2968
2976
  })),
2969
2977
  status: authMethod((ctx) => authFlow(function* () {
2978
+ if (principalPolicy && !callerOf(ctx)?.authenticated)
2979
+ return ownStatus({ state: "per_caller", message: "Authenticates as each caller; verified on use." });
2970
2980
  const state = stateFor(ctx);
2971
2981
  const report = authMethod((status) => authFlow(function* () {
2972
2982
  const path = isOauth
@@ -2979,10 +2989,10 @@ export function remoteMcp(id, opts) {
2979
2989
  });
2980
2990
  }));
2981
2991
  try {
2982
- (yield authCall(ensureConnected, [ctx, state]));
2992
+ const client = (yield authCall(ensureConnected, [ctx, state]));
2983
2993
  // The grant decides, as it does for a call: a downstream that
2984
2994
  // answers without credentials is not connected for this owner.
2985
- if (isOauth && !(yield authCall(granted, [ctx])))
2995
+ if (isOauth && !(yield authCall(granted, [connectedProviders.get(client)])))
2986
2996
  return (yield authCall(report, [
2987
2997
  { state: "auth_required", message: "Authorization required \u2014 open the URL to connect." },
2988
2998
  ]));
@@ -2995,7 +3005,10 @@ export function remoteMcp(id, opts) {
2995
3005
  if (err instanceof OperatorDisconnectedError) {
2996
3006
  return (yield authCall(report, [{ state: "auth_required", message: err.message }]));
2997
3007
  }
2998
- if (state.authRequired) {
3008
+ if (state.authRequired ||
3009
+ (err instanceof ConnectorCallError &&
3010
+ err.code === "auth_required" &&
3011
+ !(err instanceof UnresolvedTokenError))) {
2999
3012
  return (yield authCall(report, [
3000
3013
  {
3001
3014
  state: "auth_required",
@@ -3008,7 +3021,9 @@ export function remoteMcp(id, opts) {
3008
3021
  return failureStatus(id, err);
3009
3022
  }
3010
3023
  })),
3011
- finishAuth: authMethod((code, ctx, callbackParams) => authView(ctx, authMethod((signal) => authFlow(function* () {
3024
+ };
3025
+ if (isOauth) {
3026
+ connector.finishAuth = authMethod((code, ctx, callbackParams) => authView(ctx, authMethod((signal) => authFlow(function* () {
3012
3027
  const state = stateFor(ctx);
3013
3028
  const provider = newProvider(ctx, state);
3014
3029
  const consent = callbackParams?.get("state") ?? null;
@@ -3041,9 +3056,7 @@ export function remoteMcp(id, opts) {
3041
3056
  finally {
3042
3057
  detach(closeConnection(null, t, ctx.logger));
3043
3058
  }
3044
- })), true)),
3045
- };
3046
- if (opts.auth?.type === "oauth") {
3059
+ })), true));
3047
3060
  connector.verifyState = authMethod((oauthState, ctx) => authFlow(function* () {
3048
3061
  const state = stateFor(ctx);
3049
3062
  return (yield authInvoke(newProvider(ctx, state), "verifyState", [oauthState]));
@@ -3090,18 +3103,27 @@ export function remoteMcp(id, opts) {
3090
3103
  if (ctx.signal?.aborted)
3091
3104
  throw ctx.signal.reason;
3092
3105
  try {
3106
+ // A passive call's refusal stays latched for status and calls. A
3107
+ // successful connection with no tokens still permits explicit
3108
+ // consent, bound to that connection rather than the live grant.
3109
+ if (state.authRequired && state.client) {
3110
+ const provider = connectedProviders.get(state.client);
3111
+ if (!(yield authCall(granted, [provider])))
3112
+ return (yield authCall(startUnchallenged, [ctx, provider]));
3113
+ }
3093
3114
  // The handshake runs first, so a 401 challenge's
3094
3115
  // `resource_metadata` starts consent as it always has.
3095
- (yield authCall(ensureConnected, [ctx, state]));
3096
- if (!(yield authCall(granted, [ctx])))
3097
- return (yield authCall(startUnchallenged, [ctx]));
3116
+ const client = (yield authCall(ensureConnected, [ctx, state]));
3117
+ const provider = connectedProviders.get(client);
3118
+ if (!(yield authCall(granted, [provider])))
3119
+ return (yield authCall(startUnchallenged, [ctx, provider]));
3098
3120
  return {
3099
3121
  state: "ok",
3100
3122
  message: "Already authorized \u2014 connection is healthy.",
3101
3123
  };
3102
3124
  }
3103
3125
  catch (err) {
3104
- if (state.authRequired) {
3126
+ if (state.authRequired && requiresAuthorization(err)) {
3105
3127
  let authorizationUrl;
3106
3128
  try {
3107
3129
  authorizationUrl = (yield state.attemptProvider
@@ -3131,7 +3153,7 @@ export function remoteMcp(id, opts) {
3131
3153
  }
3132
3154
  }));
3133
3155
  const withActiveSecrets = authMethod((ctx, run, redactResult = true, preserveInput = false) => authFlow(function* () {
3134
- trackCredentialReads(ctx);
3156
+ wrapViewCredentials(ctx);
3135
3157
  const secrets = sentSecretsFor(ctx);
3136
3158
  if (opts.auth?.type === "headers") {
3137
3159
  for (const value of Object.values(opts.auth.headers))
@@ -3272,5 +3294,7 @@ export function remoteMcp(id, opts) {
3272
3294
  })), 0), 0));
3273
3295
  if (isOauth)
3274
3296
  withStoredAuth(connector, (ctx) => newProvider(ctx).storedAuthorization());
3297
+ if (principalPolicy)
3298
+ tagPrincipalForwarding(connector);
3275
3299
  return connector;
3276
3300
  }
@@ -26,10 +26,6 @@ export function resourceUriMatchesTemplates(uri, templates, note = () => { }) {
26
26
  }
27
27
  return matched ? { matched } : { matched, ...(refusal ? { refusal } : {}) };
28
28
  }
29
- export function resourceUriMatchesTemplate(uri, template) {
30
- const parsed = parse(template);
31
- return typeof parsed === "object" && matches(uri, parsed);
32
- }
33
29
  function parse(template) {
34
30
  if (template.length > MAX_URI_LENGTH)
35
31
  return;
@@ -1,3 +1,4 @@
1
+ import { advanceCatalogRevision } from "./catalog-revision.js";
1
2
  import { deriveOAuthHandoffKey, deriveRequestStateKey } from "./oauth-sealing.js";
2
3
  import { credentialKeys } from "./storage/keys.js";
3
4
  const KEY_BYTES = 32;
@@ -201,10 +202,12 @@ export class CredentialVault {
201
202
  ciphertext: bytesToBase64(new Uint8Array(ciphertext)),
202
203
  };
203
204
  await this.storage.set(storageKey(connectorId, owner), JSON.stringify(envelope));
205
+ advanceCatalogRevision(this);
204
206
  return (await this.metadata(connectorId, owner));
205
207
  }
206
208
  async delete(connectorId, owner) {
207
209
  await this.storage.delete(storageKey(connectorId, owner));
210
+ advanceCatalogRevision(this);
208
211
  }
209
212
  /** Encrypt downstream OAuth state under the vault key; nothing is stored. */
210
213
  async seal(connectorId, purpose, plaintext, owner) {