@typeship-ax/mcp 0.6.0 → 0.9.1

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 (114) hide show
  1. package/AGENTS.md +31 -0
  2. package/README.md +67 -10
  3. package/api.json +6735 -3243
  4. package/api.md +8537 -248
  5. package/dist/api-identity.d.ts +40 -0
  6. package/dist/api-identity.d.ts.map +1 -0
  7. package/dist/api-identity.js +128 -0
  8. package/dist/auth-profiles.d.ts +30 -0
  9. package/dist/auth-profiles.d.ts.map +1 -0
  10. package/dist/auth-profiles.js +138 -0
  11. package/dist/core/http.d.ts +21 -92
  12. package/dist/core/http.d.ts.map +1 -1
  13. package/dist/core/http.js +143 -221
  14. package/dist/core/pagination.d.ts.map +1 -1
  15. package/dist/core/pagination.js +6 -34
  16. package/dist/credential-storage.d.ts +24 -0
  17. package/dist/credential-storage.d.ts.map +1 -0
  18. package/dist/credential-storage.js +207 -0
  19. package/dist/dates.d.ts +0 -2
  20. package/dist/dates.d.ts.map +1 -1
  21. package/dist/dates.js +0 -1
  22. package/dist/docs.d.ts +36 -0
  23. package/dist/docs.d.ts.map +1 -0
  24. package/dist/docs.js +258 -0
  25. package/dist/errors.d.ts +42 -34
  26. package/dist/errors.d.ts.map +1 -1
  27. package/dist/errors.js +30 -20
  28. package/dist/index.d.ts +27 -12
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +40 -14
  31. package/dist/mcp-authorization.d.ts +52 -0
  32. package/dist/mcp-authorization.d.ts.map +1 -0
  33. package/dist/mcp-authorization.js +232 -0
  34. package/dist/mcp-protocol.d.ts +69 -25
  35. package/dist/mcp-protocol.d.ts.map +1 -1
  36. package/dist/mcp-protocol.js +386 -138
  37. package/dist/mcp.d.ts +21 -3
  38. package/dist/mcp.d.ts.map +1 -1
  39. package/dist/mcp.js +199 -85
  40. package/dist/named-credentials.d.ts +21 -0
  41. package/dist/named-credentials.d.ts.map +1 -0
  42. package/dist/named-credentials.js +86 -0
  43. package/dist/oauth-request.d.ts +21 -0
  44. package/dist/oauth-request.d.ts.map +1 -0
  45. package/dist/oauth-request.js +119 -0
  46. package/dist/oauth-session.d.ts +106 -0
  47. package/dist/oauth-session.d.ts.map +1 -0
  48. package/dist/oauth-session.js +244 -0
  49. package/dist/ops.d.ts +18 -0
  50. package/dist/ops.d.ts.map +1 -1
  51. package/dist/ops.js +31 -17
  52. package/dist/resources/account.d.ts +4 -4
  53. package/dist/resources/account.d.ts.map +1 -1
  54. package/dist/resources/account.js +1 -0
  55. package/dist/resources/api-keys.d.ts +13 -8
  56. package/dist/resources/api-keys.d.ts.map +1 -1
  57. package/dist/resources/api-keys.js +5 -1
  58. package/dist/resources/definition-revisions.d.ts +58 -0
  59. package/dist/resources/definition-revisions.d.ts.map +1 -0
  60. package/dist/resources/definition-revisions.js +114 -0
  61. package/dist/resources/definitions.d.ts +35 -0
  62. package/dist/resources/definitions.d.ts.map +1 -0
  63. package/dist/resources/definitions.js +60 -0
  64. package/dist/resources/generate.d.ts +18 -7
  65. package/dist/resources/generate.d.ts.map +1 -1
  66. package/dist/resources/generate.js +13 -5
  67. package/dist/resources/generations.d.ts +6 -6
  68. package/dist/resources/generations.d.ts.map +1 -1
  69. package/dist/resources/generations.js +3 -1
  70. package/dist/resources/projects.d.ts +111 -35
  71. package/dist/resources/projects.d.ts.map +1 -1
  72. package/dist/resources/projects.js +125 -15
  73. package/dist/resources/targets.d.ts +97 -0
  74. package/dist/resources/targets.d.ts.map +1 -0
  75. package/dist/resources/targets.js +197 -0
  76. package/dist/schemas.d.ts.map +1 -1
  77. package/dist/schemas.js +135 -62
  78. package/dist/types.d.ts +2072 -267
  79. package/dist/types.d.ts.map +1 -1
  80. package/dist/types.js +20 -3
  81. package/dist/worker.js +4 -4
  82. package/package.json +11 -1
  83. package/server.json +42 -0
  84. package/src/api-identity.ts +98 -0
  85. package/src/auth-profiles.ts +114 -0
  86. package/src/core/http.ts +156 -305
  87. package/src/core/pagination.ts +6 -30
  88. package/src/credential-storage.ts +183 -0
  89. package/src/dates.ts +0 -1
  90. package/src/docs.ts +239 -0
  91. package/src/errors.ts +52 -41
  92. package/src/index.ts +49 -14
  93. package/src/mcp-authorization.ts +211 -0
  94. package/src/mcp-protocol.ts +432 -133
  95. package/src/mcp.ts +204 -90
  96. package/src/named-credentials.ts +74 -0
  97. package/src/oauth-request.ts +90 -0
  98. package/src/oauth-session.ts +258 -0
  99. package/src/ops.ts +56 -17
  100. package/src/resources/account.ts +6 -3
  101. package/src/resources/api-keys.ts +27 -7
  102. package/src/resources/definition-revisions.ts +207 -0
  103. package/src/resources/definitions.ts +122 -0
  104. package/src/resources/generate.ts +29 -6
  105. package/src/resources/generations.ts +9 -4
  106. package/src/resources/projects.ts +274 -41
  107. package/src/resources/targets.ts +378 -0
  108. package/src/schemas.ts +135 -62
  109. package/src/types.ts +2273 -322
  110. package/src/worker.ts +4 -4
  111. package/dist/resources/spec-revisions.d.ts +0 -47
  112. package/dist/resources/spec-revisions.d.ts.map +0 -1
  113. package/dist/resources/spec-revisions.js +0 -90
  114. package/src/resources/spec-revisions.ts +0 -150
package/dist/mcp.d.ts CHANGED
@@ -1,5 +1,23 @@
1
1
  #!/usr/bin/env node
2
- /** Fetch-style handler: mount in any runtime (workers, serverless, node).
3
- * Never rejects: every failure is an HTTP or JSON-RPC error response. */
4
- export declare function handleHttp(request: Request): Promise<Response>;
2
+ import { type ClientOptions } from "./index.js";
3
+ import { type McpAuthorizationConfiguration, type McpTokenIntrospectionConfiguration, type McpPrincipal } from "./mcp-authorization.js";
4
+ export type { McpAuthorizationConfiguration, McpTokenIntrospectionConfiguration, McpPrincipal } from "./mcp-authorization.js";
5
+ export { McpAccountLinkRequired } from "./mcp-protocol.js";
6
+ export interface McpHttpOptions {
7
+ authorization?: McpAuthorizationConfiguration;
8
+ /** Optional server-only introspection. When set, checks every token with the
9
+ * provider instead of local JWT verification. Never put secrets in config. */
10
+ introspection?: McpTokenIntrospectionConfiguration;
11
+ /** Application-owned lookup or token exchange for this verified identity.
12
+ * Keep it outside the generated directory so regeneration preserves it.
13
+ * Throw McpAccountLinkRequired with a public linking page when the user
14
+ * needs to connect an account. Always verify the link server-side on retry.
15
+ * The MCP access token and request headers are deliberately not provided. */
16
+ credentialsFor(principal: McpPrincipal): ClientOptions | Promise<ClientOptions>;
17
+ }
18
+ /** Create once at startup; metadata/key caching is isolated to this server. */
19
+ export declare function createMcpHandler(options: McpHttpOptions): (request: Request) => Promise<Response>;
20
+ /** A generated entry point cannot infer the host application's user-to-API
21
+ * credential mapping. It remains closed until the owner installs a handler. */
22
+ export declare function handleHttp(_request: Request): Promise<Response>;
5
23
  //# sourceMappingURL=mcp.d.ts.map
package/dist/mcp.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":";AA2SA;yEACyE;AACzE,wBAAsB,UAAU,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAsDpE"}
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":";AAuBA,OAAO,EAAoC,KAAK,aAAa,EAAmB,MAAM,YAAY,CAAC;AAcnG,OAAO,EAA8C,KAAK,6BAA6B,EAAE,KAAK,kCAAkC,EAAE,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACpL,YAAY,EAAE,6BAA6B,EAAE,kCAAkC,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC9H,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AA4U3D,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,6BAA6B,CAAC;IAC9C;kFAC8E;IAC9E,aAAa,CAAC,EAAE,kCAAkC,CAAC;IACnD;;;;iFAI6E;IAC7E,cAAc,CAAC,SAAS,EAAE,YAAY,GAAG,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;CACjF;AAED,+EAA+E;AAC/E,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,cAAc,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAMjG;AAED;+EAC+E;AAC/E,wBAAsB,UAAU,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAErE"}
package/dist/mcp.js CHANGED
@@ -5,27 +5,35 @@
5
5
  // live in ./mcp-protocol.js, shared with typeship's hosted endpoint. Two
6
6
  // transports, zero dependencies:
7
7
  // node mcp.js stdio (newline-delimited JSON-RPC 2.0)
8
- // node mcp.js --http Streamable HTTP on PORT (default 3000)
8
+ // createMcpHandler() configured Streamable HTTP handler
9
9
  // Surface switches (flags or environment):
10
10
  // --read-only / TYPESHIP_MCP_READ_ONLY=1 reads only; writes are not callable
11
11
  // --tools a,b / TYPESHIP_MCP_TOOLS=a,b only these resources or tools
12
12
  // TYPESHIP_MCP_MAX_RESULT_CHARS=<n> result size cap (default 64000)
13
- // handleHttp() is exported for serverless/worker runtimes.
14
- // In HTTP mode an incoming Authorization header is forwarded to the
15
- // upstream API (per-request passthrough); stdio resolves auth from the
13
+ // The bare handleHttp()/--http entry stays closed until a handler is configured.
14
+ // HTTP validates a dedicated MCP token and resolves API credentials through
15
+ // createMcpHandler's application-owned callback; stdio resolves auth from the
16
16
  // environment, then from credentials/config saved by the CLI's `login`
17
17
  // and `config` commands (same files, so one login covers both bins).
18
+ import { createHash } from "node:crypto";
18
19
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
19
20
  import { homedir, tmpdir } from "node:os";
20
21
  import { basename, join } from "node:path";
21
22
  import { fileURLToPath } from "node:url";
22
23
  import { TypeshipClient, formatDebugEvent } from "./index.js";
23
- import { GLOBALS, OPS, buildArgs } from "./ops.js";
24
- import { DEFAULT_MAX_RESULT_CHARS, SUPPORTED_PROTOCOL_VERSIONS, argumentsError, asJsonRpc, binaryOutcome, callSharedTool, checkRequestHeaders, dataOutcome, errorOutcome, fetchDocsText, handleRpc, isRpcOutcome, pageOutcome, parseIncludeList, prepareCall, serverInstructions, takeCancelled, textError, toolDefinitions, visibleOps, } from "./mcp-protocol.js";
24
+ import { GLOBALS, OMITTED_OPS, OPS, buildArgs } from "./ops.js";
25
+ import { DEFAULT_MAX_RESULT_CHARS, SUPPORTED_PROTOCOL_VERSIONS, McpAccountLinkRequired, argumentsError, asJsonRpc, binaryOutcome, callSharedTool, checkRequestHeaders, dataOutcome, errorOutcome, handleRpc, isRpcOutcome, pageOutcome, parseIncludeList, prepareCall, resolveReferences, serverInstructions, takeCancelled, textError, toolDefinitions, visibleOps, } from "./mcp-protocol.js";
26
+ import { fetchDocsText } from "./docs.js";
27
+ import { assertCredentialDestination, assertStoredIdentity } from "./oauth-session.js";
28
+ import { createCredentialStore } from "./credential-storage.js";
29
+ import { parseNamedCredentials, resolveNamedCredentials, namedCredentialAvailability } from "./named-credentials.js";
30
+ import { resolveProfile, profileFlag, readProfileConfig } from "./auth-profiles.js";
31
+ import { createMcpAuthorizer, McpAuthorizationError } from "./mcp-authorization.js";
32
+ export { McpAccountLinkRequired } from "./mcp-protocol.js";
25
33
  const BIN = "typeship";
26
34
  const PKG_NAME = "@typeship-ax/mcp";
27
35
  const SERVER_NAME = "typeship-mcp";
28
- const SERVER_VERSION = "0.6.0";
36
+ const SERVER_VERSION = "0.9.1";
29
37
  /** The MCP client's announced name (clientInfo in request _meta), for the User-Agent. */
30
38
  let MCP_CLIENT_NAME = null;
31
39
  function noteClientInfo(message) {
@@ -33,20 +41,18 @@ function noteClientInfo(message) {
33
41
  const name = meta?.["io.modelcontextprotocol/clientInfo"]?.name;
34
42
  if (typeof name === "string" && name && name !== MCP_CLIENT_NAME) {
35
43
  MCP_CLIENT_NAME = name.slice(0, 60);
36
- clientInstance = undefined;
37
44
  }
38
45
  }
39
46
  const DEFAULT_BASE_URL = "https://typeship.dev/api/v1";
47
+ const NAMED_SCHEMES = { "apiKey": { "kind": "bearer", "options": ["bearerToken"] } };
40
48
  const AUTH_SCALARS = [{ "option": "bearerToken", "flag": "token", "env": "TYPESHIP_TOKEN" }];
41
49
  const BASIC = null;
42
- const OAUTH_TOKEN_URL = null;
43
50
  const ENVIRONMENTS = {};
44
51
  const DOCS_URL_DEFAULT = "https://typeship.dev";
52
+ const DOCS_INDEX_URL_DEFAULT = null;
45
53
  /** "meta" collapses per-operation tools into search/read/execute so huge
46
54
  * APIs don't flood agent context with hundreds of tools. */
47
55
  const TOOL_MODE = "meta";
48
- /** Authorization server for OAuth discovery (RFC 9728), from the spec. */
49
- const OAUTH_ISSUER = null;
50
56
  /** Project-supplied guidance appended to the server instructions. */
51
57
  const CUSTOM_INSTRUCTIONS = null;
52
58
  /** The tool that returns the caller (the CLI's whoami target), named in the instructions. */
@@ -59,15 +65,18 @@ const INCLUDE = parseIncludeList(ARGV.includes("--tools") ? ARGV[ARGV.indexOf("-
59
65
  const MAX_RESULT_CHARS = Number(process.env["TYPESHIP_MCP_MAX_RESULT_CHARS"]) || DEFAULT_MAX_RESULT_CHARS;
60
66
  /** One sentence on where credentials come from on each transport; goes
61
67
  * into the instructions and into 401 results. */
62
- const AUTH_HINT_STDIO = "Credentials come from the MCP server's environment (TYPESHIP_TOKEN) or from 'typeship login'";
63
- const AUTH_HINT_HTTP = "Send the API credential as the Authorization header of each MCP request; it is forwarded to the API as is";
68
+ const AUTH_HINT_STDIO = "Credentials come from the MCP server's environment (TYPESHIP_CREDENTIALS, TYPESHIP_TOKEN) or from 'typeship login'";
69
+ const AUTH_HINT_HTTP = "Sign in to this MCP server. The server resolves your API credentials separately; its connection token is never forwarded to the API.";
64
70
  /** The tool list is fixed at generation, so clients may cache it for an
65
71
  * hour and shared caches may hold it (identical for every caller). */
66
72
  const TOOLS_TTL_MS = 60 * 60 * 1000;
67
- function configDir() {
73
+ function configRoot() {
68
74
  return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), BIN);
69
75
  }
76
+ function configDir() { return resolveProfile(configRoot(), { flag: profileFlag(ARGV), environment: process.env["TYPESHIP_PROFILE"] }).directory; }
70
77
  function readJson(file) {
78
+ if (!LOCAL_CREDENTIALS)
79
+ return null;
71
80
  try {
72
81
  return JSON.parse(readFileSync(join(configDir(), file), "utf8"));
73
82
  }
@@ -75,10 +84,22 @@ function readJson(file) {
75
84
  return null;
76
85
  }
77
86
  }
78
- function makeClient() {
79
- const stored = readJson("credentials.json");
80
- const config = readJson("config.json");
81
- const options = {};
87
+ function makeClient(op) {
88
+ const profile = LOCAL_CREDENTIALS ? resolveProfile(configRoot(), { flag: profileFlag(ARGV), environment: process.env["TYPESHIP_PROFILE"] }) : null;
89
+ const store = createCredentialStore(profile?.directory ?? configRoot(), process.env["TYPESHIP_CREDENTIAL_STORE"], "TYPESHIP_CREDENTIAL_STORE");
90
+ if (BASIC && (process.env[BASIC.envUser] !== undefined) !== (process.env[BASIC.envPass] !== undefined))
91
+ throw new Error("Supply both Basic-auth environment variables together, or use a complete named Basic credential.");
92
+ const envNamed = LOCAL_CREDENTIALS && process.env["TYPESHIP_CREDENTIALS"] !== undefined ? parseNamedCredentials(process.env["TYPESHIP_CREDENTIALS"], NAMED_SCHEMES) : {};
93
+ const explicitOptions = new Set(AUTH_SCALARS.filter((a) => process.env[a.env] !== undefined).map((a) => a.option));
94
+ if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
95
+ explicitOptions.add("basicAuth");
96
+ namedCredentialAvailability(NAMED_SCHEMES, explicitOptions, envNamed);
97
+ const identityPolicy = {};
98
+ const identityOp = OPS.find((candidate) => candidate.tool === IDENTITY_TOOL);
99
+ const identity = identityOp && Object.keys(identityPolicy).length ? { operation: identityOp.resource + "." + identityOp.method, fields: identityPolicy } : undefined;
100
+ const explicitCredentials = op.credentialOptions?.some((alternative) => alternative.length > 0 && alternative.every((option) => explicitOptions.has(option)));
101
+ const stored = op.auth !== "none" && LOCAL_CREDENTIALS && !explicitCredentials ? store.read() : null;
102
+ const config = profile ? readProfileConfig(profile.directory) : {};
82
103
  const baseUrl = process.env["TYPESHIP_BASE_URL"]
83
104
  ?? config?.baseUrl
84
105
  ?? (config?.environment !== undefined ? ENVIRONMENTS[config.environment] : undefined)
@@ -88,7 +109,11 @@ function makeClient() {
88
109
  // instead of a server that vanished mid-conversation.
89
110
  throw new Error("No base URL configured: set TYPESHIP_BASE_URL in the MCP server's environment, or run '" + BIN + " config set base-url <url>'.");
90
111
  }
91
- options.baseUrl = baseUrl;
112
+ if (stored) {
113
+ assertCredentialDestination(stored, { apiBaseUrl: baseUrl, environment: config.environment, profile: profile?.name });
114
+ assertStoredIdentity(stored, identity);
115
+ }
116
+ const options = { baseUrl };
92
117
  for (const a of AUTH_SCALARS) {
93
118
  const v = process.env[a.env] ?? stored?.scalars?.[a.option];
94
119
  if (v !== undefined)
@@ -100,20 +125,16 @@ function makeClient() {
100
125
  if (username !== undefined && password !== undefined)
101
126
  options.basicAuth = { username, password };
102
127
  }
103
- const oauthClientId = process.env["TYPESHIP_CLIENT_ID"];
104
- const oauthClientSecret = process.env["TYPESHIP_CLIENT_SECRET"];
105
- if ((oauthClientId === undefined) !== (oauthClientSecret === undefined)) {
106
- throw new Error("OAuth client credentials are incomplete: set both TYPESHIP_CLIENT_ID and TYPESHIP_CLIENT_SECRET in the MCP server's environment.");
107
- }
108
- if (oauthClientId !== undefined && oauthClientSecret !== undefined) {
109
- const tokenUrl = process.env["TYPESHIP_TOKEN_URL"] ?? OAUTH_TOKEN_URL ?? undefined;
110
- if (!tokenUrl)
111
- throw new Error("OAuth client credentials need a token URL: set TYPESHIP_TOKEN_URL in the MCP server's environment.");
112
- options.clientCredentials = { clientId: oauthClientId, clientSecret: oauthClientSecret, tokenUrl };
113
- }
114
- if (options.bearerToken === undefined && stored?.oauth?.accessToken !== undefined) {
115
- options.bearerToken = stored.oauth.accessToken;
116
- }
128
+ const envOptions = {};
129
+ for (const a of AUTH_SCALARS)
130
+ if (process.env[a.env] !== undefined)
131
+ envOptions[a.option] = process.env[a.env];
132
+ if (BASIC && process.env[BASIC.envUser] !== undefined && process.env[BASIC.envPass] !== undefined)
133
+ envOptions.basicAuth = { username: process.env[BASIC.envUser], password: process.env[BASIC.envPass] };
134
+ options.credentials = resolveNamedCredentials(NAMED_SCHEMES, [
135
+ { named: stored?.named, options: { ...stored?.scalars, ...(stored?.basic ? { basicAuth: stored.basic } : {}) } },
136
+ { named: envNamed, options: envOptions },
137
+ ]);
117
138
  for (const g of GLOBALS) {
118
139
  const value = process.env["TYPESHIP_" + g.envSuffix];
119
140
  if (value !== undefined)
@@ -124,15 +145,25 @@ function makeClient() {
124
145
  }
125
146
  // The local MCP server identifies itself (surface + the client it serves, when announced).
126
147
  options.defaultHeaders = { "User-Agent": PKG_NAME + "-mcp/" + SERVER_VERSION + " (typeship" + (MCP_CLIENT_NAME ? "; client=" + MCP_CLIENT_NAME : "") + ")" };
127
- return new TypeshipClient(options);
148
+ // Keep the SDK's machine-token cache while re-evaluating local configuration.
149
+ const key = createHash("sha256").update(JSON.stringify([options, config, profile?.name, stored?.oauth?.sessionId, null, process.env["TYPESHIP_DEBUG"]])).digest("hex");
150
+ if (clientInstance && clientKey === key)
151
+ return clientInstance;
152
+ const client = new TypeshipClient(options);
153
+ clientKey = key;
154
+ CLIENT_CREDENTIALS.set(client, Object.keys(options.credentials ?? {}).length > 0 || AUTH_SCALARS.some((a) => options[a.option] !== undefined) || options.basicAuth !== undefined || options.bearerToken !== undefined || options.clientCredentials !== undefined);
155
+ return (clientInstance = client);
128
156
  }
129
157
  let clientInstance;
130
- function getClient() {
131
- return (clientInstance ??= makeClient());
158
+ let clientKey = "";
159
+ const CLIENT_CREDENTIALS = new WeakMap();
160
+ /** Resolve configuration and credentials for each tool call, including login/logout changes. */
161
+ function getClient(op) {
162
+ return makeClient(op);
132
163
  }
133
164
  /** Run one operation through the generated SDK: the same client, the same
134
165
  * typed errors and pagination a hand-written caller would get. */
135
- async function callOperation(op, rawArgs, authHeader) {
166
+ async function callOperationRaw(op, rawArgs, remote, client) {
136
167
  // Arguments are checked against the tool's schema first: unknown names,
137
168
  // wrong types and missing requirements come back as one isError result,
138
169
  // nothing reaches the API half-formed and nothing is dropped silently.
@@ -149,7 +180,7 @@ async function callOperation(op, rawArgs, authHeader) {
149
180
  // argument errors, reported before anything is sent.
150
181
  const fileIssues = [];
151
182
  let rawBody = op.bodyStyle === "data" ? args.body : undefined;
152
- if (LOCAL_PROCESS) {
183
+ if (LOCAL_PROCESS && !remote) {
153
184
  for (const p of op.params) {
154
185
  if (p.type !== "file" || typeof values[p.name] !== "string")
155
186
  continue;
@@ -172,37 +203,80 @@ async function callOperation(op, rawArgs, authHeader) {
172
203
  if (fileIssues.length > 0)
173
204
  return argumentsError(op, fileIssues);
174
205
  const callArgs = buildArgs(op, values, rawBody, typeof args.select === "string" ? args.select : undefined);
175
- if (authHeader)
176
- callArgs.push({ headers: { Authorization: authHeader } });
177
206
  const errorContext = {
178
- authHint: authHeader !== undefined ? AUTH_HINT_HTTP : AUTH_HINT_STDIO,
207
+ authHint: remote ? AUTH_HINT_HTTP : AUTH_HINT_STDIO,
179
208
  docsUrl: docsSource.docsUrl(),
180
- hadCredential: authHeader !== undefined || AUTH_SCALARS.some((a) => process.env[a.env] !== undefined) || readJson("credentials.json") !== null,
209
+ hadCredential: !!remote || CLIENT_CREDENTIALS.get(client) === true,
181
210
  };
182
211
  const shape = { fields, maxChars, pagination: op.pagination, args };
183
212
  try {
184
- const target = getClient()[op.resource];
213
+ const target = client[op.resource];
185
214
  const result = await target[op.method](...callArgs);
186
215
  if (!result.ok)
187
216
  return errorOutcome(result.error, errorContext);
188
217
  if (op.paginated) {
189
218
  const page = result.data;
190
- return pageOutcome(page.items, page.nextPageParams(), shape);
219
+ return pageOutcome(page.items, page.nextPageParams(), { ...shape, requestId: page.response.requestId });
191
220
  }
192
221
  // A binary body (the SDK hands back a Blob): an image block, or a file
193
222
  // on disk, never "{}".
194
223
  if (result.data instanceof Blob)
195
- return binaryOutcome(result.data, { tool: op.tool, ...(LOCAL_PROCESS ? { saveBinary } : {}) });
224
+ return binaryOutcome(result.data, { tool: op.tool, ...(LOCAL_PROCESS && !remote ? { saveBinary } : {}) });
196
225
  return dataOutcome(result.data, shape);
197
226
  }
198
227
  catch (e) {
199
228
  return textError(e.message ?? "Tool call failed");
200
229
  }
201
230
  }
231
+ /** Name-or-ID resolution is the one wrapper around the ordinary executor,
232
+ * so a direct operation tool and compact execute take exactly the same path. */
233
+ const REFERENCE_CACHES = new Map();
234
+ function referenceCacheFor(authHeader) {
235
+ const key = createHash("sha256").update(JSON.stringify([authHeader ?? "stdio", clientKey])).digest("hex");
236
+ let cache = REFERENCE_CACHES.get(key);
237
+ if (!cache) {
238
+ cache = new Map();
239
+ REFERENCE_CACHES.set(key, cache);
240
+ while (REFERENCE_CACHES.size > 16)
241
+ REFERENCE_CACHES.delete(REFERENCE_CACHES.keys().next().value);
242
+ }
243
+ return cache;
244
+ }
245
+ async function callOperation(op, rawArgs, remote) {
246
+ const prepared = prepareCall(op, rawArgs, { maxChars: MAX_RESULT_CHARS });
247
+ if (!prepared.ok)
248
+ return prepared.outcome;
249
+ // One client/session for the entire tool, including name-to-ID lookups.
250
+ // If login changes during a lookup, the token callback rejects the old session.
251
+ let client;
252
+ try {
253
+ client = remote ? await remote.client() : getClient(op);
254
+ }
255
+ catch (error) {
256
+ if (remote && error instanceof McpAccountLinkRequired)
257
+ throw error;
258
+ return textError(remote ? "API access is unavailable for this MCP account. Reconnect your API account or contact the server owner." : error.message);
259
+ }
260
+ const resolved = await resolveReferences(op, prepared.call.args, {
261
+ ops: OPS,
262
+ identityTool: IDENTITY_TOOL,
263
+ cache: remote?.references ?? referenceCacheFor(),
264
+ runOperation: (source, args) => callOperationRaw(source, args, remote, client),
265
+ });
266
+ if (!resolved.ok)
267
+ return resolved.outcome;
268
+ const args = {
269
+ ...resolved.args,
270
+ ...(prepared.call.fields ? { fields: prepared.call.fields.map((path) => path.join(".")) } : {}),
271
+ };
272
+ return callOperationRaw(op, args, remote, client);
273
+ }
202
274
  /** Running as a process on this machine (stdio, or --http launched here),
203
275
  * as opposed to imported by a worker: the server can read and write local
204
276
  * files, so uploads are tools and binaries are saved to disk. */
205
277
  const LOCAL_PROCESS = invokedDirectly();
278
+ /** Saved human sessions are available only to local stdio, never HTTP/worker callers. */
279
+ const LOCAL_CREDENTIALS = LOCAL_PROCESS && !ARGV.includes("--http");
206
280
  /** The callable operations: event streams are CLI-only, uploads are tools
207
281
  * only on a local server (mcpExposed), writes are out under --read-only,
208
282
  * and --tools narrows to a subset. A hidden operation is unknown to
@@ -224,30 +298,40 @@ function saveBinary(bytes, _mediaType, suggestedName) {
224
298
  }
225
299
  const docsSource = {
226
300
  ops: MCP_OPS,
301
+ omittedOps: OMITTED_OPS,
302
+ generatedOperationCount: OPS.length,
227
303
  docsUrl: () => readJson("config.json")?.docsUrl ?? DOCS_URL_DEFAULT,
304
+ docsIndexUrl: () => readJson("config.json")?.docsUrl ? null : DOCS_INDEX_URL_DEFAULT,
228
305
  fetchText: fetchDocsText,
229
306
  };
230
- /** The MCP server for one request context (the HTTP transport passes the
231
- * caller's Authorization through; stdio has none). */
232
- function serverFor(authHeader) {
307
+ function serverFor(remote) {
308
+ const operations = remote ? visibleOps(MCP_OPS, { uploads: false }) : MCP_OPS;
309
+ const source = remote ? { ...docsSource, ops: operations } : docsSource;
233
310
  const website = docsSource.docsUrl();
234
311
  return {
235
312
  serverInfo: { name: SERVER_NAME, title: "typeship", version: SERVER_VERSION, ...(website ? { websiteUrl: website } : {}) },
236
313
  instructions: serverInstructions({
237
314
  title: "typeship",
238
- toolCount: MCP_OPS.length,
315
+ ops: operations,
316
+ toolCount: operations.length,
317
+ generatedOperationCount: OPS.length,
318
+ omittedOps: docsSource.omittedOps,
239
319
  mode: TOOL_MODE,
240
320
  readOnly: READ_ONLY,
241
321
  hiddenWrites: HIDDEN_WRITES,
242
- authHint: authHeader !== undefined ? AUTH_HINT_HTTP : AUTH_HINT_STDIO,
322
+ authHint: remote ? AUTH_HINT_HTTP : AUTH_HINT_STDIO,
243
323
  identityTool: MCP_OPS.some((o) => o.tool === IDENTITY_TOOL) ? IDENTITY_TOOL : null,
244
- uploads: HAS_UPLOADS,
324
+ referenceResolution: MCP_OPS.some((o) => o.params.some((p) => p.resolve && typeof p.resolve === "object")),
325
+ uploads: !remote && HAS_UPLOADS,
245
326
  custom: CUSTOM_INSTRUCTIONS,
246
327
  }),
247
328
  toolsTtlMs: TOOLS_TTL_MS,
248
- listTools: () => toolDefinitions(docsSource.ops, TOOL_MODE),
329
+ listTools: () => toolDefinitions(source.ops, TOOL_MODE, source.omittedOps),
330
+ unknownToolMessage: TOOL_MODE === "meta"
331
+ ? (name) => "Unknown tool: " + name + ". This server uses compact mode; call search_docs to discover an operation, then call execute with operation: \"" + name + "\"."
332
+ : undefined,
249
333
  callTool: async (name, args) => {
250
- const shared = await callSharedTool(name, args, docsSource, (op, opArgs) => callOperation(op, opArgs, authHeader));
334
+ const shared = await callSharedTool(name, args, source, (op, opArgs) => callOperation(op, opArgs, remote));
251
335
  if (shared !== undefined)
252
336
  return shared;
253
337
  // tools/list is the callable contract. In meta mode every operation
@@ -255,8 +339,8 @@ function serverFor(authHeader) {
255
339
  // confirmation gate; an unlisted operation name must stay unknown.
256
340
  if (TOOL_MODE !== "operations")
257
341
  return undefined;
258
- const op = MCP_OPS.find((o) => o.tool === name);
259
- return op ? callOperation(op, args, authHeader) : undefined;
342
+ const op = operations.find((o) => o.tool === name);
343
+ return op ? callOperation(op, args, remote) : undefined;
260
344
  },
261
345
  };
262
346
  }
@@ -266,7 +350,7 @@ function serverFor(authHeader) {
266
350
  * validating the header when present (DNS-rebinding defense), so browser
267
351
  * callers must be same-host, localhost, or listed in
268
352
  * TYPESHIP_MCP_ALLOWED_ORIGINS (comma-separated, "*" for any). */
269
- function originAllowed(request) {
353
+ function originAllowed(request, resourceOrigin) {
270
354
  const origin = request.headers.get("origin");
271
355
  if (origin === null)
272
356
  return true;
@@ -275,9 +359,9 @@ function originAllowed(request) {
275
359
  return true;
276
360
  try {
277
361
  const o = new URL(origin);
278
- if (o.host === new URL(request.url).host)
362
+ if (o.origin === resourceOrigin)
279
363
  return true;
280
- return ["localhost", "127.0.0.1", "[::1]"].includes(o.hostname);
364
+ return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(resourceOrigin).hostname) && ["localhost", "127.0.0.1", "[::1]"].includes(o.hostname);
281
365
  }
282
366
  catch {
283
367
  return false;
@@ -286,45 +370,75 @@ function originAllowed(request) {
286
370
  function corsHeaders(request) {
287
371
  return {
288
372
  "Access-Control-Allow-Origin": request.headers.get("origin") ?? "*",
289
- "Access-Control-Allow-Methods": "POST, OPTIONS",
373
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
374
+ "Access-Control-Expose-Headers": "WWW-Authenticate",
290
375
  "Access-Control-Allow-Headers": "Content-Type, Authorization, Accept, MCP-Protocol-Version, Mcp-Method, Mcp-Name",
291
376
  Vary: "Origin",
292
377
  };
293
378
  }
294
- function protectedResourceMetadata(origin) {
295
- return {
296
- resource: origin,
297
- ...(OAUTH_ISSUER ? { authorization_servers: [OAUTH_ISSUER] } : {}),
298
- bearer_methods_supported: ["header"],
299
- };
379
+ /** Create once at startup; metadata/key caching is isolated to this server. */
380
+ export function createMcpHandler(options) {
381
+ const configured = {};
382
+ const apiBaseUrl = process.env["TYPESHIP_BASE_URL"] ?? DEFAULT_BASE_URL ?? undefined;
383
+ const authorizer = createMcpAuthorizer(options.authorization ?? { issuer: configured.mcpIssuer ?? "", resource: configured.mcpResource ?? "", ...(configured.mcpJwksUrl ? { jwksUrl: configured.mcpJwksUrl } : {}), scopes: configured.mcpScopes ?? [] }, options.introspection);
384
+ if (typeof options.credentialsFor !== "function")
385
+ throw new Error("Provide credentialsFor to resolve each MCP caller's upstream API credentials.");
386
+ return (request) => authorizedHttp(request, authorizer, options.credentialsFor, apiBaseUrl).catch(() => new Response(JSON.stringify({ error: "MCP request failed." }), { status: 500, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } }));
300
387
  }
301
- /** Fetch-style handler: mount in any runtime (workers, serverless, node).
302
- * Never rejects: every failure is an HTTP or JSON-RPC error response. */
303
- export async function handleHttp(request) {
388
+ /** A generated entry point cannot infer the host application's user-to-API
389
+ * credential mapping. It remains closed until the owner installs a handler. */
390
+ export async function handleHttp(_request) {
391
+ return new Response(JSON.stringify({ error: "Configure createMcpHandler with MCP authorization and credentialsFor before serving remote requests." }), { status: 503, headers: { "Content-Type": "application/json", "Cache-Control": "no-store" } });
392
+ }
393
+ async function authorizedHttp(request, authorizer, credentialsFor, apiBaseUrl) {
304
394
  const url = new URL(request.url);
305
- const cors = corsHeaders(request);
306
- const json = (status, body, extra = {}) => new Response(JSON.stringify(body), { status, headers: { ...cors, "Content-Type": "application/json", ...extra } });
395
+ const resource = new URL(authorizer.configuration.resource);
396
+ const cors = originAllowed(request, resource.origin) ? corsHeaders(request) : {};
397
+ const json = (status, body, extra = {}) => new Response(JSON.stringify(body), { status, headers: { ...cors, "Content-Type": "application/json", "Cache-Control": "no-store", ...extra } });
307
398
  if (request.method === "OPTIONS") {
308
- return new Response(null, { status: originAllowed(request) ? 204 : 403, headers: cors });
399
+ return new Response(null, { status: originAllowed(request, resource.origin) ? 204 : 403, headers: cors });
309
400
  }
310
- if (!originAllowed(request)) {
401
+ if (!originAllowed(request, resource.origin)) {
311
402
  return json(403, { jsonrpc: "2.0", error: { code: -32600, message: "Origin not allowed. Set TYPESHIP_MCP_ALLOWED_ORIGINS to permit browser callers." } });
312
403
  }
313
- if (url.pathname.endsWith("/.well-known/oauth-protected-resource")) {
314
- return json(200, protectedResourceMetadata(url.origin));
315
- }
404
+ const metadataPath = "/.well-known/oauth-protected-resource" + resource.pathname.replace(/\/$/, "");
405
+ if (url.pathname === metadataPath && request.method === "GET")
406
+ return json(200, authorizer.metadata());
407
+ if (url.pathname !== resource.pathname)
408
+ return json(404, { error: "Unknown MCP endpoint." });
316
409
  if (request.method !== "POST") {
317
410
  // GET streams, DELETE (session end) and Mcp-Session-Id belong to earlier
318
411
  // revisions; the spec asks modern-only servers to answer 405.
319
412
  return json(405, { error: "POST JSON-RPC messages to this endpoint (MCP " + SUPPORTED_PROTOCOL_VERSIONS[0] + ", stateless)." }, { Allow: "POST, OPTIONS" });
320
413
  }
321
- const authHeader = request.headers.get("authorization") ?? undefined;
322
- const envHasAuth = AUTH_SCALARS.some((a) => process.env[a.env] !== undefined);
323
- if (OAUTH_ISSUER && !authHeader && !envHasAuth) {
324
- return json(401, { error: "Authorization required." }, {
325
- "WWW-Authenticate": 'Bearer resource_metadata="' + url.origin + '/.well-known/oauth-protected-resource"',
326
- });
327
- }
414
+ let principal;
415
+ try {
416
+ principal = await authorizer.authorize(request.headers.get("authorization"));
417
+ }
418
+ catch (error) {
419
+ const failure = error instanceof McpAuthorizationError ? error : new McpAuthorizationError(503, "authorization_unavailable");
420
+ return json(failure.status, { error: failure.code, message: failure.message }, failure.status === 503 ? {} : { "WWW-Authenticate": authorizer.challenge(failure.code === "insufficient_scope" ? "insufficient_scope" : "invalid_token") });
421
+ }
422
+ let client;
423
+ const remote = { references: new Map(), client: () => client ??= (async () => {
424
+ const credentials = await credentialsFor(principal);
425
+ if (!credentials || typeof credentials !== "object")
426
+ throw new Error("API credentials unavailable.");
427
+ const baseUrl = apiBaseUrl;
428
+ if (typeof baseUrl !== "string")
429
+ throw new Error("Configure the upstream API URL.");
430
+ if (credentials.baseUrl !== undefined && credentials.baseUrl !== baseUrl)
431
+ throw new Error("API credentials target a different API URL.");
432
+ const token = request.headers.get("authorization").slice(7);
433
+ const transport = credentials.fetch ?? fetch;
434
+ return new TypeshipClient({ ...credentials, baseUrl, fetch: async (input, init) => {
435
+ const headers = new Headers(init?.headers);
436
+ const destination = new URL(typeof input === "string" ? input : input instanceof URL ? input.href : input.url);
437
+ if ([...headers.values()].some(value => value === token || value === "Bearer " + token) || [...destination.searchParams.values()].includes(token))
438
+ throw new Error("An MCP connection token cannot authorize an upstream API request.");
439
+ return transport(input, init);
440
+ } });
441
+ })() };
328
442
  let incoming;
329
443
  try {
330
444
  incoming = await request.json();
@@ -345,10 +459,10 @@ export async function handleHttp(request) {
345
459
  noteClientInfo(parsed);
346
460
  let outcome;
347
461
  try {
348
- outcome = await handleRpc(serverFor(authHeader), parsed);
462
+ outcome = await handleRpc(serverFor(remote), parsed);
349
463
  }
350
464
  catch (e) {
351
- return json(500, { jsonrpc: "2.0", id: parsed.id ?? null, error: { code: -32603, message: e.message ?? "Internal error" } });
465
+ return json(500, { jsonrpc: "2.0", id: parsed.id ?? null, error: { code: -32603, message: "Internal error" } });
352
466
  }
353
467
  if (outcome.message === null) {
354
468
  return new Response(null, { status: outcome.status, headers: cors });
@@ -416,7 +530,7 @@ async function startStdio() {
416
530
  })
417
531
  .catch((e) => {
418
532
  if (id !== undefined) {
419
- write({ jsonrpc: "2.0", id: id ?? null, error: { code: -32603, message: e.message ?? "Internal error" } });
533
+ write({ jsonrpc: "2.0", id: id ?? null, error: { code: -32603, message: "Internal error" } });
420
534
  }
421
535
  });
422
536
  });
@@ -0,0 +1,21 @@
1
+ /** Runtime credentials keyed by the API's exact security-scheme names. */
2
+ export type NamedCredential = string | {
3
+ username: string;
4
+ password: string;
5
+ };
6
+ export type NamedCredentials = Record<string, NamedCredential>;
7
+ export type CredentialSchemes = Record<string, {
8
+ kind: string;
9
+ options: string[];
10
+ }>;
11
+ export declare function parseNamedCredentials(value: unknown, schemes: CredentialSchemes): NamedCredentials;
12
+ /** Low-to-high source order. Within a source, exact names beat convenience options. */
13
+ export declare function resolveNamedCredentials(schemes: CredentialSchemes, layers: {
14
+ named?: NamedCredentials;
15
+ options?: Record<string, unknown>;
16
+ }[]): NamedCredentials;
17
+ /** Make convenience and named inputs comparable without expanding combinations. */
18
+ export declare function namedCredentialAvailability(schemes: CredentialSchemes, options: Set<string>, named: NamedCredentials): Set<string>;
19
+ /** Bound file/stdin reads before allocating a full credential document. */
20
+ export declare function readNamedCredentialsFile(input: string, schemes: CredentialSchemes): NamedCredentials;
21
+ //# sourceMappingURL=named-credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"named-credentials.d.ts","sourceRoot":"","sources":["../src/named-credentials.ts"],"names":[],"mappings":"AAEA,0EAA0E;AAC1E,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAC9E,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC;AAC/D,MAAM,MAAM,iBAAiB,GAAG,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAAC;AAEpF,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAqBlG;AAED,uFAAuF;AACvF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,EAAE,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,gBAAgB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAAE,GAAG,gBAAgB,CAa/J;AAED,mFAAmF;AACnF,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,gBAAgB,GAAG,GAAG,CAAC,MAAM,CAAC,CAKlI;AAED,2EAA2E;AAC3E,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,CAkBpG"}
@@ -0,0 +1,86 @@
1
+ import { openSync, readSync, closeSync } from "node:fs";
2
+ export function parseNamedCredentials(value, schemes) {
3
+ if (typeof value === "string") {
4
+ if (new TextEncoder().encode(value).byteLength > 1_048_576)
5
+ throw new Error("Named credentials exceed the 1 MiB limit.");
6
+ try {
7
+ value = JSON.parse(value);
8
+ }
9
+ catch {
10
+ throw new Error("Named credentials must be a JSON object keyed by security scheme. Credential contents are not included in errors.");
11
+ }
12
+ }
13
+ if (!value || typeof value !== "object" || Array.isArray(value))
14
+ throw new Error("Named credentials must be a JSON object keyed by security scheme.");
15
+ const result = Object.create(null);
16
+ for (const [name, credential] of Object.entries(value)) {
17
+ if (!Object.hasOwn(schemes, name))
18
+ throw new Error("Named credentials contain an unknown or unsupported security scheme. Check the names listed by login --help.");
19
+ if (schemes[name].kind === "basic") {
20
+ if (!credential || typeof credential !== "object" || Array.isArray(credential) || Object.keys(credential).some((key) => key !== "username" && key !== "password") ||
21
+ typeof credential.username !== "string" || typeof credential.password !== "string" || !credential.username || !credential.password || credential.username.includes(":")) {
22
+ throw new Error("A named Basic credential requires a nonempty username and password; the username cannot contain a colon.");
23
+ }
24
+ result[name] = { username: credential.username, password: credential.password };
25
+ }
26
+ else {
27
+ if (typeof credential !== "string" || !credential || /[\x00-\x1f\x7f]/.test(credential) || (schemes[name].kind === "bearer" && credential.includes(" ")))
28
+ throw new Error("A named token or API key must be a nonempty string without control characters; bearer tokens cannot contain spaces.");
29
+ result[name] = credential;
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+ /** Low-to-high source order. Within a source, exact names beat convenience options. */
35
+ export function resolveNamedCredentials(schemes, layers) {
36
+ const resolved = Object.create(null);
37
+ for (const layer of layers) {
38
+ for (const [name, scheme] of Object.entries(schemes)) {
39
+ for (const option of scheme.options) {
40
+ if (option === "clientCredentials")
41
+ continue;
42
+ const value = layer.options?.[option];
43
+ if (value !== undefined)
44
+ resolved[name] = value;
45
+ }
46
+ }
47
+ if (layer.named)
48
+ Object.assign(resolved, parseNamedCredentials(layer.named, schemes));
49
+ }
50
+ return resolved;
51
+ }
52
+ /** Make convenience and named inputs comparable without expanding combinations. */
53
+ export function namedCredentialAvailability(schemes, options, named) {
54
+ for (const [name, scheme] of Object.entries(schemes)) {
55
+ if (Object.hasOwn(named, name) || scheme.options.some((option) => options.has(option)))
56
+ options.add("credentials." + name);
57
+ }
58
+ return options;
59
+ }
60
+ /** Bound file/stdin reads before allocating a full credential document. */
61
+ export function readNamedCredentialsFile(input, schemes) {
62
+ let fd;
63
+ const chunks = [];
64
+ let total = 0;
65
+ try {
66
+ fd = input === "-" ? 0 : openSync(input.slice(1), "r");
67
+ while (true) {
68
+ const buffer = Buffer.alloc(Math.min(32_768, 1_048_577 - total));
69
+ const size = readSync(fd, buffer, 0, buffer.length, null);
70
+ if (!size)
71
+ break;
72
+ total += size;
73
+ if (total > 1_048_576)
74
+ throw new Error("size");
75
+ chunks.push(buffer.subarray(0, size));
76
+ }
77
+ }
78
+ catch {
79
+ throw new Error("Cannot read named credentials. Supply a readable JSON file or stdin input of at most 1 MiB.");
80
+ }
81
+ finally {
82
+ if (fd !== undefined && input !== "-")
83
+ closeSync(fd);
84
+ }
85
+ return parseNamedCredentials(Buffer.concat(chunks).toString("utf8"), schemes);
86
+ }