@typeship-ax/mcp 0.8.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.
- package/AGENTS.md +31 -0
- package/README.md +66 -9
- package/api.json +1617 -712
- package/api.md +8292 -382
- package/dist/api-identity.d.ts +40 -0
- package/dist/api-identity.d.ts.map +1 -0
- package/dist/api-identity.js +128 -0
- package/dist/auth-profiles.d.ts +30 -0
- package/dist/auth-profiles.d.ts.map +1 -0
- package/dist/auth-profiles.js +138 -0
- package/dist/core/http.d.ts +17 -2
- package/dist/core/http.d.ts.map +1 -1
- package/dist/core/http.js +78 -17
- package/dist/credential-storage.d.ts +24 -0
- package/dist/credential-storage.d.ts.map +1 -0
- package/dist/credential-storage.js +207 -0
- package/dist/docs.d.ts +25 -0
- package/dist/docs.d.ts.map +1 -1
- package/dist/docs.js +144 -0
- package/dist/errors.d.ts +18 -10
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +24 -14
- package/dist/index.d.ts +10 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -4
- package/dist/mcp-authorization.d.ts +52 -0
- package/dist/mcp-authorization.d.ts.map +1 -0
- package/dist/mcp-authorization.js +232 -0
- package/dist/mcp-protocol.d.ts +51 -2
- package/dist/mcp-protocol.d.ts.map +1 -1
- package/dist/mcp-protocol.js +249 -37
- package/dist/mcp.d.ts +21 -3
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +185 -68
- package/dist/named-credentials.d.ts +21 -0
- package/dist/named-credentials.d.ts.map +1 -0
- package/dist/named-credentials.js +86 -0
- package/dist/oauth-request.d.ts +21 -0
- package/dist/oauth-request.d.ts.map +1 -0
- package/dist/oauth-request.js +119 -0
- package/dist/oauth-session.d.ts +106 -0
- package/dist/oauth-session.d.ts.map +1 -0
- package/dist/oauth-session.js +244 -0
- package/dist/ops.d.ts +14 -1
- package/dist/ops.d.ts.map +1 -1
- package/dist/ops.js +30 -30
- package/dist/resources/account.d.ts +2 -2
- package/dist/resources/account.d.ts.map +1 -1
- package/dist/resources/account.js +1 -0
- package/dist/resources/api-keys.d.ts +3 -3
- package/dist/resources/api-keys.d.ts.map +1 -1
- package/dist/resources/api-keys.js +2 -0
- package/dist/resources/definition-revisions.d.ts +5 -5
- package/dist/resources/definition-revisions.d.ts.map +1 -1
- package/dist/resources/definition-revisions.js +4 -0
- package/dist/resources/definitions.d.ts +15 -4
- package/dist/resources/definitions.d.ts.map +1 -1
- package/dist/resources/definitions.js +11 -2
- package/dist/resources/generate.d.ts +14 -3
- package/dist/resources/generate.d.ts.map +1 -1
- package/dist/resources/generate.js +10 -2
- package/dist/resources/generations.d.ts +3 -3
- package/dist/resources/generations.d.ts.map +1 -1
- package/dist/resources/generations.js +2 -0
- package/dist/resources/projects.d.ts +56 -20
- package/dist/resources/projects.d.ts.map +1 -1
- package/dist/resources/projects.js +40 -4
- package/dist/resources/targets.d.ts +20 -9
- package/dist/resources/targets.d.ts.map +1 -1
- package/dist/resources/targets.js +14 -1
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +38 -22
- package/dist/types.d.ts +385 -119
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +11 -0
- package/dist/worker.js +4 -4
- package/package.json +11 -1
- package/server.json +42 -0
- package/src/api-identity.ts +98 -0
- package/src/auth-profiles.ts +114 -0
- package/src/core/http.ts +88 -19
- package/src/credential-storage.ts +183 -0
- package/src/docs.ts +138 -0
- package/src/errors.ts +26 -15
- package/src/index.ts +29 -4
- package/src/mcp-authorization.ts +211 -0
- package/src/mcp-protocol.ts +287 -38
- package/src/mcp.ts +186 -72
- package/src/named-credentials.ts +74 -0
- package/src/oauth-request.ts +90 -0
- package/src/oauth-session.ts +258 -0
- package/src/ops.ts +44 -31
- package/src/resources/account.ts +3 -0
- package/src/resources/api-keys.ts +5 -0
- package/src/resources/definition-revisions.ts +9 -0
- package/src/resources/definitions.ts +25 -0
- package/src/resources/generate.ts +23 -0
- package/src/resources/generations.ts +5 -0
- package/src/resources/projects.ts +95 -7
- package/src/resources/targets.ts +32 -0
- package/src/schemas.ts +38 -22
- package/src/types.ts +404 -119
- package/src/worker.ts +4 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Identity comes from the authenticated API response, including opaque-token APIs. */
|
|
2
|
+
export type IdentityKind = "subject" | "account" | "organization";
|
|
3
|
+
export type IdentityPolicy = Partial<Record<IdentityKind, string>>;
|
|
4
|
+
export type ApiIdentity = Partial<Record<IdentityKind, string | null>>;
|
|
5
|
+
export interface VerifiedIdentity {
|
|
6
|
+
values: ApiIdentity;
|
|
7
|
+
checkedAt: string;
|
|
8
|
+
}
|
|
9
|
+
export interface IdentityConfiguration {
|
|
10
|
+
operation: string;
|
|
11
|
+
fields: IdentityPolicy;
|
|
12
|
+
}
|
|
13
|
+
export declare function validateIdentityPointer(pointer: unknown): asserts pointer is string;
|
|
14
|
+
export declare function identityPolicyOf(auth?: {
|
|
15
|
+
identitySubject?: string | null;
|
|
16
|
+
identityAccount?: string | null;
|
|
17
|
+
identityOrganization?: string | null;
|
|
18
|
+
}): IdentityPolicy;
|
|
19
|
+
export declare function readApiIdentity(data: unknown, policy: IdentityPolicy): ApiIdentity;
|
|
20
|
+
export declare function assertApiIdentity(expected: ApiIdentity, actual: ApiIdentity): void;
|
|
21
|
+
/** No redirects, bounded decoded response, and one deadline including body reads. */
|
|
22
|
+
export declare function identityFetch(apiBaseUrl: string): typeof fetch;
|
|
23
|
+
type IdentityResult = {
|
|
24
|
+
ok: boolean;
|
|
25
|
+
data?: unknown;
|
|
26
|
+
error?: {
|
|
27
|
+
status?: number;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export declare function identityResult(client: unknown, operation: {
|
|
31
|
+
resource: string;
|
|
32
|
+
method: string;
|
|
33
|
+
}): Promise<IdentityResult>;
|
|
34
|
+
export declare function verifyClientIdentity(createClient: (options: Record<string, unknown>) => unknown, options: Record<string, unknown>, operation: {
|
|
35
|
+
resource: string;
|
|
36
|
+
method: string;
|
|
37
|
+
}, policy: IdentityPolicy, expected?: ApiIdentity): Promise<VerifiedIdentity>;
|
|
38
|
+
export declare function verifyApiIdentity(read: (anonymous: boolean) => Promise<IdentityResult>, policy: IdentityPolicy, expected?: ApiIdentity): Promise<VerifiedIdentity>;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=api-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-identity.d.ts","sourceRoot":"","sources":["../src/api-identity.ts"],"names":[],"mappings":"AAAA,uFAAuF;AACvF,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,GAAG,cAAc,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;AACnE,MAAM,MAAM,WAAW,GAAG,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC;AACvE,MAAM,WAAW,gBAAgB;IAAG,MAAM,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AAC5E,MAAM,WAAW,qBAAqB;IAAG,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,cAAc,CAAA;CAAE;AAEpF,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAInF;AAED,wBAAgB,gBAAgB,CAAC,IAAI,GAAE;IAAE,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,cAAc,CAKtK;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,WAAW,CAelF;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,MAAM,EAAE,WAAW,GAAG,IAAI,CAIlF;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,KAAK,CA8B9D;AAED,KAAK,cAAc,GAAG;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AACnF,wBAAsB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,cAAc,CAAC,CAI9H;AAED,wBAAgB,oBAAoB,CAAC,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAI9P;AACD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,cAAc,EAAE,QAAQ,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,gBAAgB,CAAC,CASxK"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
export function validateIdentityPointer(pointer) {
|
|
2
|
+
if (typeof pointer !== "string" || !pointer.startsWith("/") || pointer.length > 512 || /[\x00-\x1f\x7f]/.test(pointer) || /~(?:[^01]|$)/.test(pointer)) {
|
|
3
|
+
throw new Error("Identity fields must be JSON Pointers starting with /, at most 512 characters, with ~0 and ~1 escapes.");
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export function identityPolicyOf(auth = {}) {
|
|
7
|
+
const entries = [["subject", auth.identitySubject], ["account", auth.identityAccount], ["organization", auth.identityOrganization]];
|
|
8
|
+
const policy = {};
|
|
9
|
+
for (const [kind, pointer] of entries)
|
|
10
|
+
if (pointer !== undefined && pointer !== null) {
|
|
11
|
+
validateIdentityPointer(pointer);
|
|
12
|
+
policy[kind] = pointer;
|
|
13
|
+
}
|
|
14
|
+
return policy;
|
|
15
|
+
}
|
|
16
|
+
export function readApiIdentity(data, policy) {
|
|
17
|
+
const result = {};
|
|
18
|
+
if (!Object.keys(policy).length)
|
|
19
|
+
throw new Error("Configure at least one identity field before verifying a session.");
|
|
20
|
+
for (const [kind, pointer] of Object.entries(policy)) {
|
|
21
|
+
validateIdentityPointer(pointer);
|
|
22
|
+
let value = data;
|
|
23
|
+
for (const part of pointer.slice(1).split("/").map((part) => part.replaceAll("~1", "/").replaceAll("~0", "~"))) {
|
|
24
|
+
if (!value || typeof value !== "object" || !Object.hasOwn(value, part))
|
|
25
|
+
throw new Error("The API identity response is missing a configured field. Check the identity field mapping.");
|
|
26
|
+
value = value[part];
|
|
27
|
+
}
|
|
28
|
+
if (value === null && kind === "organization")
|
|
29
|
+
result[kind] = null;
|
|
30
|
+
else if ((typeof value === "string" && value.length > 0 && value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value)) || (typeof value === "number" && Number.isSafeInteger(value)))
|
|
31
|
+
result[kind] = String(value);
|
|
32
|
+
else
|
|
33
|
+
throw new Error("The API identity field must return a nonempty ID string or safe integer. Only an organization may be null.");
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export function assertApiIdentity(expected, actual) {
|
|
38
|
+
if (!Object.keys(expected).length || Object.entries(expected).some(([key, value]) => !Object.hasOwn(actual, key) || actual[key] !== value)) {
|
|
39
|
+
throw new Error("The authenticated account, subject or organization does not match this login. Sign in again with the intended account and organization.");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** No redirects, bounded decoded response, and one deadline including body reads. */
|
|
43
|
+
export function identityFetch(apiBaseUrl) {
|
|
44
|
+
const base = new URL(apiBaseUrl);
|
|
45
|
+
if ((base.protocol !== "https:" && !(base.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(base.hostname))) || base.username || base.password || base.search || base.hash)
|
|
46
|
+
throw new Error("Identity verification requires an HTTPS API URL; loopback HTTP is allowed for development.");
|
|
47
|
+
const basePath = base.pathname.replace(/\/+$/, "");
|
|
48
|
+
return async (input, init) => {
|
|
49
|
+
const url = new URL(input instanceof Request ? input.url : String(input));
|
|
50
|
+
if (url.origin !== base.origin || (url.pathname !== basePath && !url.pathname.startsWith(basePath + "/")) || url.username || url.password || url.hash)
|
|
51
|
+
throw new Error("The identity read must stay on the configured API.");
|
|
52
|
+
const abort = new AbortController();
|
|
53
|
+
const timeout = setTimeout(() => abort.abort(), 10_000);
|
|
54
|
+
try {
|
|
55
|
+
const signal = init?.signal ? AbortSignal.any([init.signal, abort.signal]) : abort.signal;
|
|
56
|
+
const response = await fetch(input, { ...init, redirect: "error", signal });
|
|
57
|
+
const reader = response.body?.getReader();
|
|
58
|
+
const chunks = [];
|
|
59
|
+
let size = 0;
|
|
60
|
+
if (reader) {
|
|
61
|
+
try {
|
|
62
|
+
while (true) {
|
|
63
|
+
const chunk = await reader.read();
|
|
64
|
+
if (chunk.done)
|
|
65
|
+
break;
|
|
66
|
+
size += chunk.value.byteLength;
|
|
67
|
+
if (size > 1_048_576) {
|
|
68
|
+
abort.abort();
|
|
69
|
+
throw new Error();
|
|
70
|
+
}
|
|
71
|
+
chunks.push(chunk.value);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
reader.releaseLock();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const bytes = new Uint8Array(size);
|
|
79
|
+
let offset = 0;
|
|
80
|
+
for (const chunk of chunks) {
|
|
81
|
+
bytes.set(chunk, offset);
|
|
82
|
+
offset += chunk.byteLength;
|
|
83
|
+
}
|
|
84
|
+
return new Response([204, 205, 304].includes(response.status) ? null : bytes, { status: response.status, statusText: response.statusText, headers: response.headers });
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw new Error("The API identity read failed. Check the API URL, connectivity and response size before signing in again.");
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
clearTimeout(timeout);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
export async function identityResult(client, operation) {
|
|
95
|
+
const target = client[operation.resource];
|
|
96
|
+
if (!target?.[operation.method])
|
|
97
|
+
throw new Error("The configured identity operation is not available in this package.");
|
|
98
|
+
return target[operation.method]();
|
|
99
|
+
}
|
|
100
|
+
export function verifyClientIdentity(createClient, options, operation, policy, expected) {
|
|
101
|
+
if (typeof options.baseUrl !== "string")
|
|
102
|
+
throw new Error("Configure the API base URL before verifying identity.");
|
|
103
|
+
const shared = { baseUrl: options.baseUrl, environment: options.environment, fetch: identityFetch(options.baseUrl), maxRetries: 0, timeoutMs: 10_000 };
|
|
104
|
+
return verifyApiIdentity((anonymous) => identityResult(createClient(anonymous ? shared : { ...options, ...shared }), operation), policy, expected);
|
|
105
|
+
}
|
|
106
|
+
export async function verifyApiIdentity(read, policy, expected) {
|
|
107
|
+
let anonymous, authenticated;
|
|
108
|
+
try {
|
|
109
|
+
anonymous = await read(true);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new Error("Could not verify that the identity read rejects anonymous access.");
|
|
113
|
+
}
|
|
114
|
+
if (anonymous.ok || ![401, 403].includes(anonymous.error?.status ?? 0))
|
|
115
|
+
throw new Error("The identity read must reject anonymous access with 401 or 403 before login can be verified.");
|
|
116
|
+
try {
|
|
117
|
+
authenticated = await read(false);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
throw new Error("The API identity read failed with the new credential. Check the API configuration and granted permissions.");
|
|
121
|
+
}
|
|
122
|
+
if (!authenticated.ok)
|
|
123
|
+
throw new Error("The API did not accept the new login credential for its identity read. Check the audience, scopes and account access.");
|
|
124
|
+
const values = readApiIdentity(authenticated.data, policy);
|
|
125
|
+
if (expected)
|
|
126
|
+
assertApiIdentity(expected, values);
|
|
127
|
+
return { values, checkedAt: new Date().toISOString() };
|
|
128
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export interface ProfileConfig {
|
|
2
|
+
baseUrl?: string;
|
|
3
|
+
environment?: string;
|
|
4
|
+
docsUrl?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ProfileContext {
|
|
7
|
+
name: string;
|
|
8
|
+
directory: string;
|
|
9
|
+
source: "flag" | "environment" | "selected" | "default";
|
|
10
|
+
}
|
|
11
|
+
export declare function profileName(value: string): string;
|
|
12
|
+
export declare function profileDirectory(root: string, name: string): string;
|
|
13
|
+
export declare function resolveProfile(root: string, options?: {
|
|
14
|
+
flag?: string;
|
|
15
|
+
environment?: string;
|
|
16
|
+
allowMissing?: boolean;
|
|
17
|
+
}): ProfileContext;
|
|
18
|
+
export declare function selectProfile(root: string, name: string): void;
|
|
19
|
+
export declare function readProfileConfig(directory: string): ProfileConfig;
|
|
20
|
+
export declare function updateProfileConfig(directory: string, change: (current: ProfileConfig) => ProfileConfig): Promise<void>;
|
|
21
|
+
export declare function listProfiles(root: string): {
|
|
22
|
+
name: string;
|
|
23
|
+
config: ProfileConfig;
|
|
24
|
+
hasSavedCredentials: boolean;
|
|
25
|
+
}[];
|
|
26
|
+
/** Removing a profile requires logout first. Hold the settings and both storage
|
|
27
|
+
* locks so a concurrent login cannot be deleted underneath its transaction. */
|
|
28
|
+
export declare function removeProfile(root: string, name: string): Promise<void>;
|
|
29
|
+
export declare function profileFlag(args: string[]): string | undefined;
|
|
30
|
+
//# sourceMappingURL=auth-profiles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-profiles.d.ts","sourceRoot":"","sources":["../src/auth-profiles.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAAG,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE;AAC3F,MAAM,WAAW,cAAc;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,GAAG,aAAa,GAAG,UAAU,GAAG,SAAS,CAAA;CAAE;AAE5H,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAGjD;AAqBD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAOnE;AAED,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,cAAc,CAa1I;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAI9D;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,CAIlE;AAED,wBAAsB,mBAAmB,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7H;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,aAAa,CAAC;IAAC,mBAAmB,EAAE,OAAO,CAAA;CAAE,EAAE,CAYlH;AAED;+EAC+E;AAC/E,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc7E;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAS9D"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { FileCredentialStore } from "./oauth-session.js";
|
|
5
|
+
export function profileName(value) {
|
|
6
|
+
if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(value) || /^(con|prn|aux|nul|com[0-9]|lpt[0-9])$/.test(value))
|
|
7
|
+
throw new Error("Profile names use 1–64 lowercase letters, numbers, hyphens or underscores and cannot be reserved OS names.");
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function json(path) {
|
|
11
|
+
try {
|
|
12
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
13
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
14
|
+
throw new Error();
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === "ENOENT")
|
|
19
|
+
return null;
|
|
20
|
+
throw new Error("Cannot read profile settings. Repair the settings file before retrying.");
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function write(directory, file, value) {
|
|
24
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
25
|
+
const temporary = join(directory, ".profile-" + randomBytes(16).toString("hex") + ".tmp");
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(temporary, JSON.stringify(value, null, 2) + "\n", { flag: "wx", mode: 0o600, flush: true });
|
|
28
|
+
renameSync(temporary, join(directory, file));
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
rmSync(temporary, { force: true });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function profileDirectory(root, name) {
|
|
35
|
+
const directory = join(root, "profiles", profileName(name));
|
|
36
|
+
try {
|
|
37
|
+
const entry = lstatSync(directory);
|
|
38
|
+
if (entry.isSymbolicLink() || !entry.isDirectory())
|
|
39
|
+
throw new Error("A profile must be a directory, not a link or another file.");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== "ENOENT")
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return directory;
|
|
46
|
+
}
|
|
47
|
+
export function resolveProfile(root, options = {}) {
|
|
48
|
+
let name, source;
|
|
49
|
+
if (options.flag !== undefined) {
|
|
50
|
+
name = profileName(options.flag);
|
|
51
|
+
source = "flag";
|
|
52
|
+
}
|
|
53
|
+
else if (options.environment !== undefined) {
|
|
54
|
+
name = profileName(options.environment);
|
|
55
|
+
source = "environment";
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const settings = json(join(root, "profiles.json"));
|
|
59
|
+
if (settings && typeof settings.selected !== "string")
|
|
60
|
+
throw new Error("The selected profile setting is invalid. Pass --profile default to recover.");
|
|
61
|
+
name = settings ? profileName(settings.selected) : "default";
|
|
62
|
+
source = settings ? "selected" : "default";
|
|
63
|
+
}
|
|
64
|
+
const directory = profileDirectory(root, name);
|
|
65
|
+
if (name !== "default" && !options.allowMissing && !existsSync(directory))
|
|
66
|
+
throw new Error("Profile '" + name + "' does not exist. Run login --profile " + name + " or configure that profile first.");
|
|
67
|
+
return { name, directory, source };
|
|
68
|
+
}
|
|
69
|
+
export function selectProfile(root, name) {
|
|
70
|
+
const directory = profileDirectory(root, name);
|
|
71
|
+
if (name !== "default" && !existsSync(directory))
|
|
72
|
+
throw new Error("Profile '" + name + "' does not exist. Log in or configure it before selecting it.");
|
|
73
|
+
write(root, "profiles.json", { selected: name });
|
|
74
|
+
}
|
|
75
|
+
export function readProfileConfig(directory) {
|
|
76
|
+
const value = json(join(directory, "config.json")) ?? {};
|
|
77
|
+
for (const [key, field] of Object.entries(value))
|
|
78
|
+
if (!["baseUrl", "environment", "docsUrl"].includes(key) || typeof field !== "string")
|
|
79
|
+
throw new Error("Profile settings contain an unknown field or a non-string value.");
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
export async function updateProfileConfig(directory, change) {
|
|
83
|
+
await new FileCredentialStore(join(directory, "config.json")).withLock(async () => { write(directory, "config.json", change(readProfileConfig(directory))); });
|
|
84
|
+
}
|
|
85
|
+
export function listProfiles(root) {
|
|
86
|
+
const names = new Set(["default"]);
|
|
87
|
+
try {
|
|
88
|
+
for (const entry of readdirSync(join(root, "profiles"), { withFileTypes: true })) {
|
|
89
|
+
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
90
|
+
continue;
|
|
91
|
+
try {
|
|
92
|
+
names.add(profileName(entry.name));
|
|
93
|
+
}
|
|
94
|
+
catch { /* unrelated directories are not profiles */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code !== "ENOENT")
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
return [...names].sort().map((name) => {
|
|
102
|
+
const directory = profileDirectory(root, name);
|
|
103
|
+
return { name, config: readProfileConfig(directory), hasSavedCredentials: ["credentials.enc", "credentials.json"].some((file) => existsSync(join(directory, file))) };
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
/** Removing a profile requires logout first. Hold the settings and both storage
|
|
107
|
+
* locks so a concurrent login cannot be deleted underneath its transaction. */
|
|
108
|
+
export async function removeProfile(root, name) {
|
|
109
|
+
const directory = profileDirectory(root, name);
|
|
110
|
+
if (!existsSync(directory))
|
|
111
|
+
throw new Error("Profile '" + name + "' does not exist.");
|
|
112
|
+
const paths = ["config.json", "credentials.enc", "credentials.json"].map((file) => join(directory, file));
|
|
113
|
+
await new FileCredentialStore(paths[0]).withLock(async () => {
|
|
114
|
+
await new FileCredentialStore(paths[1]).withLock(async () => {
|
|
115
|
+
await new FileCredentialStore(paths[2]).withLock(async () => {
|
|
116
|
+
if (paths.slice(1).some((path) => existsSync(path)))
|
|
117
|
+
throw new Error("Log out of both credential stores for profile '" + name + "' before removing it. Use logout --profile " + name + ".");
|
|
118
|
+
const selected = json(join(root, "profiles.json"));
|
|
119
|
+
if (selected?.selected === name)
|
|
120
|
+
selectProfile(root, "default");
|
|
121
|
+
rmSync(directory, { recursive: true });
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
export function profileFlag(args) {
|
|
127
|
+
let value;
|
|
128
|
+
for (let i = 0; i < args.length; i++) {
|
|
129
|
+
if (args[i] === "--profile") {
|
|
130
|
+
value = args[++i];
|
|
131
|
+
if (value === undefined || value.startsWith("--"))
|
|
132
|
+
throw new Error("--profile requires a profile name.");
|
|
133
|
+
}
|
|
134
|
+
else if (args[i].startsWith("--profile="))
|
|
135
|
+
value = args[i].slice("--profile=".length);
|
|
136
|
+
}
|
|
137
|
+
return value === undefined ? undefined : profileName(value);
|
|
138
|
+
}
|
package/dist/core/http.d.ts
CHANGED
|
@@ -64,6 +64,14 @@ export declare class ApiError<S extends number = number, B = unknown> extends Er
|
|
|
64
64
|
export declare class UnexpectedApiError extends ApiError<number, unknown> {
|
|
65
65
|
constructor(status: number, body: unknown, response: ResponseMeta);
|
|
66
66
|
}
|
|
67
|
+
/** A successful response declared JSON but carried a body that could not be parsed. */
|
|
68
|
+
export declare class ResponseParseError extends Error {
|
|
69
|
+
readonly status: number;
|
|
70
|
+
readonly body: string;
|
|
71
|
+
readonly response: ResponseMeta;
|
|
72
|
+
readonly cause?: unknown;
|
|
73
|
+
constructor(body: string, response: ResponseMeta, cause?: unknown);
|
|
74
|
+
}
|
|
67
75
|
export interface Violation {
|
|
68
76
|
path: string;
|
|
69
77
|
message: string;
|
|
@@ -82,13 +90,14 @@ export declare class ValidationError extends Error {
|
|
|
82
90
|
* are ignored: validation can miss drift but never false-alarms.
|
|
83
91
|
*/
|
|
84
92
|
export declare function validateAgainstSchema(value: unknown, schema: unknown, path: string, out: Violation[], defs?: Record<string, unknown>): void;
|
|
85
|
-
/** The request
|
|
93
|
+
/** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body). */
|
|
86
94
|
export declare class TransportError extends Error {
|
|
87
95
|
readonly cause?: unknown;
|
|
88
96
|
constructor(message: string, cause?: unknown);
|
|
89
97
|
}
|
|
90
98
|
type ErrorCtor = new (body: any, response: ResponseMeta) => ApiError<number, unknown>;
|
|
91
99
|
export interface CoreRequest {
|
|
100
|
+
security?: Record<string, string[]>[];
|
|
92
101
|
method: string;
|
|
93
102
|
path: string;
|
|
94
103
|
query?: Record<string, unknown>;
|
|
@@ -119,7 +128,13 @@ export interface RetryPolicy {
|
|
|
119
128
|
/** Also retry non-idempotent methods (POST/PATCH). */
|
|
120
129
|
retryNonIdempotent?: boolean;
|
|
121
130
|
}
|
|
131
|
+
export interface SecurityCredential {
|
|
132
|
+
headers?: Record<string, AuthValue>;
|
|
133
|
+
query?: Record<string, AuthValue>;
|
|
134
|
+
}
|
|
122
135
|
export interface CoreConfig {
|
|
136
|
+
/** Named credentials are selected using each operation's requirements. */
|
|
137
|
+
credentials?: Record<string, SecurityCredential>;
|
|
123
138
|
baseUrl: string;
|
|
124
139
|
headers: Record<string, AuthValue>;
|
|
125
140
|
/** Auth carried as query parameters (apiKey-in-query schemes). */
|
|
@@ -177,7 +192,7 @@ export interface DebugEvent {
|
|
|
177
192
|
/** 1-based; >1 means this was a retry */
|
|
178
193
|
attempt: number;
|
|
179
194
|
requestId?: string;
|
|
180
|
-
/** transport failure message, when
|
|
195
|
+
/** transport failure message, when the request failed before a complete response */
|
|
181
196
|
error?: string;
|
|
182
197
|
}
|
|
183
198
|
export declare class HttpCore {
|
package/dist/core/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mCAAmC;IACnC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC7C,yEAAyE;IACzE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,OAAO,CAAC;IACjB;;iCAE6B;IAC7B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;qCACiC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,CAAC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;AAElE;8CAC8C;AAC9C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;CACjB;AAGD;;;;GAIG;AACH,MAAM,MAAM,SAAS,CAAC,CAAC,EAAE,CAAC,IACtB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,CAAC,CAAC;IAAC,QAAQ,EAAE,YAAY,CAAA;CAAE,GAC7C;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE,YAAY,CAAA;CAAE,CAAC;AAErD,0EAA0E;AAC1E,wBAAgB,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAIvD;AAED,gDAAgD;AAChD,qBAAa,QAAQ,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,CAAC,GAAG,OAAO,CAAE,SAAQ,KAAK;IACzE,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IACnB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACjB,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;gBAEpB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,YAAY;CAOxE;AAED,kDAAkD;AAClD,qBAAa,kBAAmB,SAAQ,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;gBACnD,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY;CAGlE;AAOD,MAAM,WAAW,SAAS;IAAG,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE;AAE5D;;0CAE0C;AAC1C,qBAAa,eAAgB,SAAQ,KAAK;IACxC,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,UAAU,CAAC;IAC3C,QAAQ,CAAC,UAAU,EAAE,SAAS,EAAE,CAAC;gBACrB,SAAS,EAAE,SAAS,GAAG,UAAU,EAAE,UAAU,EAAE,SAAS,EAAE;CAQvE;AAeD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CA+E3I;AAqBD,
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/core/http.ts"],"names":[],"mappings":"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"}
|
package/dist/core/http.js
CHANGED
|
@@ -29,6 +29,21 @@ export class UnexpectedApiError extends ApiError {
|
|
|
29
29
|
super("Unexpected HTTP status " + status, status, body, response);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
/** A successful response declared JSON but carried a body that could not be parsed. */
|
|
33
|
+
export class ResponseParseError extends Error {
|
|
34
|
+
status;
|
|
35
|
+
body;
|
|
36
|
+
response;
|
|
37
|
+
cause;
|
|
38
|
+
constructor(body, response, cause) {
|
|
39
|
+
super("HTTP " + response.status + " response body was not valid JSON");
|
|
40
|
+
this.name = "ResponseParseError";
|
|
41
|
+
this.status = response.status;
|
|
42
|
+
this.body = body;
|
|
43
|
+
this.response = response;
|
|
44
|
+
this.cause = cause;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
32
47
|
/** Request or response data did not match the spec's schema (opt-in via the
|
|
33
48
|
* client's validate option). Never thrown: returned as the error side of
|
|
34
49
|
* ApiResult, like every other failure. */
|
|
@@ -174,7 +189,7 @@ function transportFailureMessage(method, url, cause) {
|
|
|
174
189
|
}
|
|
175
190
|
return method + " " + url + " failed: " + detail;
|
|
176
191
|
}
|
|
177
|
-
/** The request
|
|
192
|
+
/** The request failed before a complete HTTP response arrived (network failure, timeout, abort, or truncated body). */
|
|
178
193
|
export class TransportError extends Error {
|
|
179
194
|
cause;
|
|
180
195
|
constructor(message, cause) {
|
|
@@ -183,6 +198,33 @@ export class TransportError extends Error {
|
|
|
183
198
|
this.cause = cause;
|
|
184
199
|
}
|
|
185
200
|
}
|
|
201
|
+
/** Resolve one complete alternative in spec order. Missing or incompatible
|
|
202
|
+
* combinations contribute no partial credentials. Anonymous operations send
|
|
203
|
+
* no generated credentials; explicit headers/hooks remain application owned. */
|
|
204
|
+
function selectSecurity(requirements, credentials) {
|
|
205
|
+
for (const requirement of requirements) {
|
|
206
|
+
const names = Object.keys(requirement);
|
|
207
|
+
if (!names.length || names.some((name) => !Object.hasOwn(credentials, name)))
|
|
208
|
+
continue;
|
|
209
|
+
const selected = { headers: Object.create(null), query: Object.create(null) };
|
|
210
|
+
const destinations = new Set();
|
|
211
|
+
let conflict = false;
|
|
212
|
+
for (const name of names) {
|
|
213
|
+
for (const location of ["headers", "query"]) {
|
|
214
|
+
for (const [wire, value] of Object.entries(credentials[name][location] ?? {})) {
|
|
215
|
+
const destination = location + ":" + (location === "headers" ? wire.toLowerCase() : wire);
|
|
216
|
+
if (destinations.has(destination))
|
|
217
|
+
conflict = true;
|
|
218
|
+
destinations.add(destination);
|
|
219
|
+
selected[location][wire] = value;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (!conflict)
|
|
224
|
+
return selected;
|
|
225
|
+
}
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
186
228
|
const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
187
229
|
/** Credential-bearing headers fetch itself drops when a redirect crosses to
|
|
188
230
|
* another origin. A spec's own API-key headers join them below. */
|
|
@@ -232,8 +274,13 @@ export class HttpCore {
|
|
|
232
274
|
const policy = { ...this.config.retry, ...req.retry };
|
|
233
275
|
const maxRetries = req.options?.maxRetries ?? policy.maxRetries ?? this.config.maxRetries;
|
|
234
276
|
const timeoutMs = req.options?.timeoutMs ?? this.config.timeoutMs;
|
|
235
|
-
const retryAllowed = req.idempotent === true ||
|
|
236
|
-
|
|
277
|
+
const retryAllowed = req.idempotent === true ||
|
|
278
|
+
req.method === "GET" ||
|
|
279
|
+
req.idempotencyKey !== undefined ||
|
|
280
|
+
policy.retryNonIdempotent === true;
|
|
281
|
+
const retryableStatuses = policy.statuses
|
|
282
|
+
? new Set(policy.statuses)
|
|
283
|
+
: RETRYABLE_STATUSES;
|
|
237
284
|
// One key per logical call, reused on every retry — that's the point
|
|
238
285
|
// of idempotency keys.
|
|
239
286
|
const autoIdempotencyKey = req.idempotencyKey !== undefined &&
|
|
@@ -299,10 +346,11 @@ export class HttpCore {
|
|
|
299
346
|
data = (await parseBody(response, req.method));
|
|
300
347
|
}
|
|
301
348
|
catch (cause) {
|
|
302
|
-
|
|
303
|
-
|
|
349
|
+
const parseError = cause instanceof ResponseParseError ? cause : undefined;
|
|
350
|
+
emitResponseDebug(response, parseError?.body);
|
|
351
|
+
const error = (parseError ?? new TransportError("The response body read failed before completing", cause));
|
|
304
352
|
await this.config.onError?.(error, { method: req.method, path: req.path });
|
|
305
|
-
return { ok: false, error, response: meta(response) };
|
|
353
|
+
return { ok: false, error, response: parseError?.response ?? meta(response) };
|
|
306
354
|
}
|
|
307
355
|
emitResponseDebug(response, data);
|
|
308
356
|
const responseMeta = meta(response, data);
|
|
@@ -366,8 +414,9 @@ export class HttpCore {
|
|
|
366
414
|
return { ok: false, error };
|
|
367
415
|
}
|
|
368
416
|
async send(req, timeoutMs, attempt, autoIdempotencyKey) {
|
|
417
|
+
const selected = req.security && this.config.credentials ? selectSecurity(req.security, this.config.credentials) : {};
|
|
369
418
|
const headers = {};
|
|
370
|
-
for (const [k, v] of Object.entries(this.config.headers)) {
|
|
419
|
+
for (const [k, v] of Object.entries({ ...this.config.headers, ...selected.headers })) {
|
|
371
420
|
headers[k] = await resolveAuthValue(v);
|
|
372
421
|
}
|
|
373
422
|
if (req.idempotencyKey && autoIdempotencyKey) {
|
|
@@ -384,7 +433,7 @@ export class HttpCore {
|
|
|
384
433
|
}
|
|
385
434
|
const context = {
|
|
386
435
|
method: req.method,
|
|
387
|
-
url: await this.buildUrl(req),
|
|
436
|
+
url: await this.buildUrl(req, selected.query),
|
|
388
437
|
headers,
|
|
389
438
|
attempt,
|
|
390
439
|
};
|
|
@@ -469,7 +518,7 @@ export class HttpCore {
|
|
|
469
518
|
origin = target.origin;
|
|
470
519
|
}
|
|
471
520
|
}
|
|
472
|
-
async buildUrl(req) {
|
|
521
|
+
async buildUrl(req, authQuery) {
|
|
473
522
|
const base = this.config.baseUrl.replace(/\/+$/, "");
|
|
474
523
|
const url = new URL(base + req.path);
|
|
475
524
|
for (const [k, v] of Object.entries(req.query ?? {})) {
|
|
@@ -484,7 +533,7 @@ export class HttpCore {
|
|
|
484
533
|
appendDeep(url.searchParams, k, v);
|
|
485
534
|
}
|
|
486
535
|
}
|
|
487
|
-
for (const [k, v] of Object.entries(this.config.query)) {
|
|
536
|
+
for (const [k, v] of Object.entries({ ...this.config.query, ...authQuery })) {
|
|
488
537
|
url.searchParams.append(k, await resolveAuthValue(v));
|
|
489
538
|
}
|
|
490
539
|
return url.toString();
|
|
@@ -541,10 +590,22 @@ function serializeBody(req) {
|
|
|
541
590
|
async function parseBody(response, method) {
|
|
542
591
|
if (method === "HEAD" || response.status === 204 || response.status === 205)
|
|
543
592
|
return undefined;
|
|
544
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
593
|
+
const contentType = (response.headers.get("content-type") ?? "").toLowerCase();
|
|
594
|
+
const mediaType = contentType.split(";", 1)[0].trim();
|
|
595
|
+
const slash = mediaType.indexOf("/");
|
|
596
|
+
const subtype = slash === -1 ? "" : mediaType.slice(slash + 1);
|
|
545
597
|
try {
|
|
546
|
-
if (
|
|
547
|
-
|
|
598
|
+
if (subtype === "json" || subtype.endsWith("+json")) {
|
|
599
|
+
const body = await response.text();
|
|
600
|
+
if (body.length === 0)
|
|
601
|
+
return undefined;
|
|
602
|
+
try {
|
|
603
|
+
return JSON.parse(body);
|
|
604
|
+
}
|
|
605
|
+
catch (cause) {
|
|
606
|
+
throw new ResponseParseError(body, meta(response, body), cause);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
548
609
|
if (contentType.startsWith("text/"))
|
|
549
610
|
return await response.text();
|
|
550
611
|
if (response.body === null)
|
|
@@ -552,11 +613,11 @@ async function parseBody(response, method) {
|
|
|
552
613
|
return await response.blob();
|
|
553
614
|
}
|
|
554
615
|
catch (cause) {
|
|
555
|
-
|
|
556
|
-
// empty body — surface it instead of faking success.
|
|
557
|
-
if (cause instanceof Error && (cause.name === "AbortError" || cause.name === "TimeoutError"))
|
|
616
|
+
if (cause instanceof ResponseParseError)
|
|
558
617
|
throw cause;
|
|
559
|
-
|
|
618
|
+
// A timed-out, aborted, or prematurely terminated body is a transport
|
|
619
|
+
// failure, not an empty body — surface it instead of faking success.
|
|
620
|
+
throw cause;
|
|
560
621
|
}
|
|
561
622
|
}
|
|
562
623
|
function requestIdFromBody(body) {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { FileCredentialStore, type CredentialCodec } from "./oauth-session.js";
|
|
2
|
+
export interface NativeCommandResult {
|
|
3
|
+
status: number | null;
|
|
4
|
+
stdout: string;
|
|
5
|
+
stderr: string;
|
|
6
|
+
error?: unknown;
|
|
7
|
+
}
|
|
8
|
+
export type NativeCommand = (executable: string, args: string[], input?: string) => NativeCommandResult;
|
|
9
|
+
export interface CredentialKeyStore {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
read(): Buffer | null;
|
|
12
|
+
write(key: Buffer): void;
|
|
13
|
+
/** Used only when explicitly removing this wrapping key, not ordinary logout. */
|
|
14
|
+
remove(): void;
|
|
15
|
+
}
|
|
16
|
+
/** A small wrapping key avoids OS item-size limits; session files are encrypted
|
|
17
|
+
* separately so refresh rotation can retain the atomic file/lock transaction. */
|
|
18
|
+
export declare function nativeCredentialKeyStore(path: string, platform?: NodeJS.Platform, run?: NativeCommand, environmentName?: string): CredentialKeyStore;
|
|
19
|
+
export declare function encryptedCredentialCodec(path: string, keyStore: CredentialKeyStore): CredentialCodec;
|
|
20
|
+
/** OS protection is the default. Plaintext storage is an explicit, separate
|
|
21
|
+
* store for environments where the owner accepts that tradeoff. Never migrate
|
|
22
|
+
* or fall back silently when an OS service is unavailable. */
|
|
23
|
+
export declare function createCredentialStore(directory: string, mode?: string, environmentName?: string): FileCredentialStore;
|
|
24
|
+
//# sourceMappingURL=credential-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credential-storage.d.ts","sourceRoot":"","sources":["../src/credential-storage.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,mBAAmB,EAA0B,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAEvG,MAAM,WAAW,mBAAmB;IAAG,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAC/G,MAAM,MAAM,aAAa,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,mBAAmB,CAAC;AACxG,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,IAAI,IAAI,MAAM,GAAG,IAAI,CAAC;IACtB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,iFAAiF;IACjF,MAAM,IAAI,IAAI,CAAC;CAChB;AAkBD;iFACiF;AACjF,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAAE,GAAG,GAAE,aAA6B,EAAE,eAAe,SAAqB,GAAG,kBAAkB,CAsGjM;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,eAAe,CAkCpG;AAED;;8DAE8D;AAC9D,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,GAAE,MAAa,EAAE,eAAe,SAAqB,GAAG,mBAAmB,CAKvI"}
|