@sanity/runtime-cli 12.4.0 → 13.0.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/README.md +176 -63
- package/dist/actions/blueprints/assets.d.ts +3 -1
- package/dist/actions/blueprints/assets.js +8 -4
- package/dist/actions/blueprints/blueprint.d.ts +2 -1
- package/dist/actions/blueprints/blueprint.js +3 -1
- package/dist/actions/blueprints/config.d.ts +5 -2
- package/dist/actions/blueprints/config.js +4 -4
- package/dist/actions/blueprints/logs-streaming.d.ts +4 -2
- package/dist/actions/blueprints/logs-streaming.js +5 -2
- package/dist/actions/blueprints/logs.d.ts +2 -1
- package/dist/actions/blueprints/logs.js +4 -2
- package/dist/actions/blueprints/resources.d.ts +2 -1
- package/dist/actions/blueprints/resources.js +2 -2
- package/dist/actions/blueprints/stacks.d.ts +12 -6
- package/dist/actions/blueprints/stacks.js +18 -11
- package/dist/actions/functions/dev.d.ts +2 -1
- package/dist/actions/functions/dev.js +2 -2
- package/dist/actions/functions/env/list.d.ts +2 -1
- package/dist/actions/functions/env/list.js +4 -2
- package/dist/actions/functions/env/remove.d.ts +2 -1
- package/dist/actions/functions/env/remove.js +4 -2
- package/dist/actions/functions/env/update.d.ts +2 -1
- package/dist/actions/functions/env/update.js +4 -2
- package/dist/actions/functions/logs.d.ts +4 -3
- package/dist/actions/functions/logs.js +10 -6
- package/dist/actions/node.d.ts +2 -1
- package/dist/actions/node.js +2 -2
- package/dist/actions/sanity/examples.d.ts +5 -2
- package/dist/actions/sanity/examples.js +6 -6
- package/dist/actions/sanity/projects.d.ts +7 -3
- package/dist/actions/sanity/projects.js +11 -7
- package/dist/baseCommands.d.ts +4 -0
- package/dist/baseCommands.js +8 -2
- package/dist/commands/blueprints/add.d.ts +1 -0
- package/dist/commands/blueprints/add.js +12 -8
- package/dist/commands/blueprints/config.d.ts +1 -0
- package/dist/commands/blueprints/config.js +10 -4
- package/dist/commands/blueprints/deploy.d.ts +1 -0
- package/dist/commands/blueprints/deploy.js +8 -2
- package/dist/commands/blueprints/destroy.d.ts +1 -0
- package/dist/commands/blueprints/destroy.js +8 -2
- package/dist/commands/blueprints/doctor.d.ts +1 -0
- package/dist/commands/blueprints/doctor.js +7 -2
- package/dist/commands/blueprints/info.d.ts +1 -0
- package/dist/commands/blueprints/info.js +9 -3
- package/dist/commands/blueprints/init.d.ts +1 -0
- package/dist/commands/blueprints/init.js +20 -11
- package/dist/commands/blueprints/logs.d.ts +1 -0
- package/dist/commands/blueprints/logs.js +8 -2
- package/dist/commands/blueprints/plan.d.ts +1 -0
- package/dist/commands/blueprints/plan.js +6 -2
- package/dist/commands/blueprints/stacks.d.ts +1 -0
- package/dist/commands/blueprints/stacks.js +8 -4
- package/dist/commands/functions/add.d.ts +1 -0
- package/dist/commands/functions/add.js +8 -2
- package/dist/commands/functions/dev.d.ts +1 -0
- package/dist/commands/functions/dev.js +14 -3
- package/dist/commands/functions/env/add.d.ts +2 -1
- package/dist/commands/functions/env/add.js +6 -2
- package/dist/commands/functions/env/list.d.ts +2 -1
- package/dist/commands/functions/env/list.js +6 -2
- package/dist/commands/functions/env/remove.d.ts +2 -1
- package/dist/commands/functions/env/remove.js +6 -2
- package/dist/commands/functions/logs.d.ts +2 -1
- package/dist/commands/functions/logs.js +7 -4
- package/dist/commands/functions/test.d.ts +2 -1
- package/dist/commands/functions/test.js +6 -2
- package/dist/cores/blueprints/config.js +9 -9
- package/dist/cores/blueprints/deploy.js +14 -16
- package/dist/cores/blueprints/destroy.js +6 -6
- package/dist/cores/blueprints/doctor.js +20 -27
- package/dist/cores/blueprints/info.js +3 -3
- package/dist/cores/blueprints/init.d.ts +3 -3
- package/dist/cores/blueprints/init.js +15 -8
- package/dist/cores/blueprints/logs.js +6 -7
- package/dist/cores/blueprints/plan.js +1 -0
- package/dist/cores/blueprints/stacks.js +4 -4
- package/dist/cores/functions/add.js +8 -3
- package/dist/cores/functions/dev.js +2 -2
- package/dist/cores/functions/env/add.js +3 -4
- package/dist/cores/functions/env/list.js +3 -4
- package/dist/cores/functions/env/remove.js +3 -4
- package/dist/cores/functions/index.d.ts +3 -9
- package/dist/cores/functions/logs.js +8 -9
- package/dist/cores/functions/test.js +7 -8
- package/dist/cores/index.d.ts +4 -7
- package/dist/cores/index.js +3 -3
- package/dist/index.d.ts +18 -2
- package/dist/index.js +20 -2
- package/dist/server/app.d.ts +2 -1
- package/dist/server/app.js +4 -4
- package/dist/server/handlers/invoke.d.ts +2 -1
- package/dist/server/handlers/invoke.js +2 -2
- package/dist/server/static/components/response-panel.js +3 -0
- package/dist/server/static/components/rule-panel.js +9 -1
- package/dist/utils/display/prompt.d.ts +5 -2
- package/dist/utils/display/prompt.js +5 -4
- package/dist/utils/functions/fetch-document.d.ts +3 -2
- package/dist/utils/functions/fetch-document.js +7 -6
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +2 -0
- package/dist/utils/logger.d.ts +13 -0
- package/dist/utils/logger.js +61 -0
- package/dist/utils/other/github.d.ts +2 -1
- package/dist/utils/other/github.js +4 -2
- package/dist/utils/other/npmjs.d.ts +2 -1
- package/dist/utils/other/npmjs.js +4 -2
- package/dist/utils/traced-fetch.d.ts +35 -0
- package/dist/utils/traced-fetch.js +238 -0
- package/dist/utils/validated-token.d.ts +3 -2
- package/dist/utils/validated-token.js +6 -4
- package/oclif.manifest.json +175 -38
- package/package.json +13 -5
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient } from '@sanity/client';
|
|
2
|
-
import
|
|
3
|
-
export async function fetchDocument(documentId, { projectId, dataset, useCdn = true, apiVersion = '2025-02-06', apiHost, token }) {
|
|
4
|
-
const spinner = ora(`Fetching document ID ${documentId}...`).start();
|
|
2
|
+
import { createTracedFetch } from '../traced-fetch.js';
|
|
3
|
+
export async function fetchDocument(documentId, { projectId, dataset, useCdn = true, apiVersion = '2025-02-06', apiHost, token }, logger) {
|
|
4
|
+
const spinner = logger.ora(`Fetching document ID ${documentId}...`).start();
|
|
5
5
|
const client = createClient({ projectId, dataset, useCdn, apiVersion, apiHost, token });
|
|
6
6
|
const data = await client.fetch(`*[_id == "${documentId}"]`);
|
|
7
7
|
spinner.stop();
|
|
@@ -10,10 +10,11 @@ export async function fetchDocument(documentId, { projectId, dataset, useCdn = t
|
|
|
10
10
|
}
|
|
11
11
|
return data[0];
|
|
12
12
|
}
|
|
13
|
-
export async function fetchAsset(documentId, { mediaLibraryId, apiVersion = '2025-03-24', apiHost, token }) {
|
|
14
|
-
const spinner = ora(`Fetching document ID ${documentId}...`).start();
|
|
13
|
+
export async function fetchAsset(documentId, { mediaLibraryId, apiVersion = '2025-03-24', apiHost, token }, logger) {
|
|
14
|
+
const spinner = logger.ora(`Fetching document ID ${documentId}...`).start();
|
|
15
|
+
const fetchFn = createTracedFetch(logger);
|
|
15
16
|
const url = `${apiHost}/v${apiVersion}/media-libraries/${mediaLibraryId}/doc/${documentId}`;
|
|
16
|
-
const response = await
|
|
17
|
+
const response = await fetchFn(url, {
|
|
17
18
|
headers: {
|
|
18
19
|
Authorization: `Bearer ${token}`,
|
|
19
20
|
},
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * as display from './display/index.js';
|
|
2
2
|
export * as findFunction from './find-function.js';
|
|
3
3
|
export * as invokeLocal from './invoke-local.js';
|
|
4
|
+
export * as logger from './logger.js';
|
|
5
|
+
export * as tracedFetch from './traced-fetch.js';
|
|
4
6
|
export * as types from './types.js';
|
|
5
7
|
export * as validate from './validate/index.js';
|
|
6
8
|
export * as validatedToken from './validated-token.js';
|
package/dist/utils/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * as display from './display/index.js';
|
|
2
2
|
export * as findFunction from './find-function.js';
|
|
3
3
|
export * as invokeLocal from './invoke-local.js';
|
|
4
|
+
export * as logger from './logger.js';
|
|
5
|
+
export * as tracedFetch from './traced-fetch.js';
|
|
4
6
|
export * as types from './types.js';
|
|
5
7
|
export * as validate from './validate/index.js';
|
|
6
8
|
export * as validatedToken from './validated-token.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
export declare function Logger(log: (msg: string) => void, flags?: {
|
|
3
|
+
verbose?: boolean;
|
|
4
|
+
trace?: boolean;
|
|
5
|
+
}): {
|
|
6
|
+
(msg: string): void;
|
|
7
|
+
trace(formatter: unknown, ...args: unknown[]): false | void;
|
|
8
|
+
verbose(formatter: unknown, ...args: unknown[]): false | void;
|
|
9
|
+
info(formatter: unknown, ...args: unknown[]): false | void;
|
|
10
|
+
warn(formatter: unknown, ...args: unknown[]): false | void;
|
|
11
|
+
error(formatter: unknown, ...args: unknown[]): false | void;
|
|
12
|
+
ora: typeof ora;
|
|
13
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { format } from 'node:util';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
var LogLevel;
|
|
4
|
+
(function (LogLevel) {
|
|
5
|
+
LogLevel[LogLevel["TRACE"] = 0] = "TRACE";
|
|
6
|
+
LogLevel[LogLevel["VERBOSE"] = 1] = "VERBOSE";
|
|
7
|
+
LogLevel[LogLevel["INFO"] = 2] = "INFO";
|
|
8
|
+
LogLevel[LogLevel["WARN"] = 3] = "WARN";
|
|
9
|
+
LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
|
|
10
|
+
})(LogLevel || (LogLevel = {}));
|
|
11
|
+
export function Logger(log, flags = {}) {
|
|
12
|
+
const logger = (msg) => {
|
|
13
|
+
log(msg);
|
|
14
|
+
};
|
|
15
|
+
const level = flags.verbose ? LogLevel.VERBOSE : flags.trace ? LogLevel.TRACE : LogLevel.INFO;
|
|
16
|
+
logger.trace = (formatter, ...args) => level <= LogLevel.TRACE && logger(format(formatter, ...args));
|
|
17
|
+
logger.verbose = (formatter, ...args) => level <= LogLevel.VERBOSE && logger(format(formatter, ...args));
|
|
18
|
+
logger.info = (formatter, ...args) => level <= LogLevel.INFO && logger(format(formatter, ...args));
|
|
19
|
+
logger.warn = (formatter, ...args) => level <= LogLevel.WARN && logger(format(formatter, ...args));
|
|
20
|
+
logger.error = (formatter, ...args) => level <= LogLevel.ERROR && logger(format(formatter, ...args));
|
|
21
|
+
const oraWrapper = (opts) => {
|
|
22
|
+
if (level >= LogLevel.INFO)
|
|
23
|
+
return ora(opts);
|
|
24
|
+
return createOraLineLoggingWrapper(opts, logger);
|
|
25
|
+
};
|
|
26
|
+
logger.ora = oraWrapper;
|
|
27
|
+
return logger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* An ora-like wrapper that uses standard line output logging instead of rendering a spinner; used in verbose and trace modes.
|
|
31
|
+
*/
|
|
32
|
+
function createOraLineLoggingWrapper(opts, logger) {
|
|
33
|
+
const text = typeof opts === 'string' ? opts : (typeof opts === 'object' ? opts.text : '') || '';
|
|
34
|
+
const wrapper = {
|
|
35
|
+
clear() {
|
|
36
|
+
logger('');
|
|
37
|
+
return wrapper;
|
|
38
|
+
},
|
|
39
|
+
fail(subtext) {
|
|
40
|
+
logger.error(subtext);
|
|
41
|
+
return wrapper;
|
|
42
|
+
},
|
|
43
|
+
info(subtext) {
|
|
44
|
+
logger(subtext || wrapper.text);
|
|
45
|
+
return wrapper;
|
|
46
|
+
},
|
|
47
|
+
start(subtext) {
|
|
48
|
+
logger(subtext || wrapper.text);
|
|
49
|
+
return wrapper;
|
|
50
|
+
},
|
|
51
|
+
stop() {
|
|
52
|
+
return wrapper;
|
|
53
|
+
},
|
|
54
|
+
succeed(subtext) {
|
|
55
|
+
logger(subtext || wrapper.text);
|
|
56
|
+
return wrapper;
|
|
57
|
+
},
|
|
58
|
+
text,
|
|
59
|
+
};
|
|
60
|
+
return wrapper;
|
|
61
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
+
import type { Logger } from '../logger.js';
|
|
1
2
|
export declare const GITHUB_API_URL = "https://api.github.com";
|
|
2
|
-
export declare function gitHubRequest(path: string): Promise<Response>;
|
|
3
|
+
export declare function gitHubRequest(path: string, logger: ReturnType<typeof Logger>): Promise<Response>;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// ! Making requests to the GitHub API will be rate limited at 60 requests per hour per IP address
|
|
2
2
|
// https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#primary-rate-limit-for-unauthenticated-users
|
|
3
|
+
import { createTracedFetch } from '../traced-fetch.js';
|
|
3
4
|
export const GITHUB_API_URL = 'https://api.github.com';
|
|
4
|
-
export async function gitHubRequest(path) {
|
|
5
|
-
const
|
|
5
|
+
export async function gitHubRequest(path, logger) {
|
|
6
|
+
const fetchFn = createTracedFetch(logger);
|
|
7
|
+
const response = await fetchFn(`${GITHUB_API_URL}${path}`, {
|
|
6
8
|
headers: { Accept: 'application/vnd.github.v3+json' },
|
|
7
9
|
});
|
|
8
10
|
if (response.ok) {
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
import type { Logger } from '../logger.js';
|
|
2
|
+
export declare function getLatestNpmVersion(pkg: string, logger: ReturnType<typeof Logger>): Promise<string>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
import { createTracedFetch } from '../traced-fetch.js';
|
|
2
|
+
export async function getLatestNpmVersion(pkg, logger) {
|
|
3
|
+
const fetchFn = createTracedFetch(logger);
|
|
2
4
|
const url = `https://registry.npmjs.org/${pkg}/latest`;
|
|
3
5
|
try {
|
|
4
|
-
const res = await
|
|
6
|
+
const res = await fetchFn(url);
|
|
5
7
|
if (!res.ok)
|
|
6
8
|
throw new Error(`Failed to fetch version for ${pkg}`);
|
|
7
9
|
const data = await res.json();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Logger } from './logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for traced fetch
|
|
4
|
+
*/
|
|
5
|
+
export interface TracedFetchOptions {
|
|
6
|
+
/** Whether to log request headers. Default: true */
|
|
7
|
+
logRequestHeaders?: boolean;
|
|
8
|
+
/** Whether to log response headers. Default: true */
|
|
9
|
+
logResponseHeaders?: boolean;
|
|
10
|
+
/** Whether to log request body. Default: true */
|
|
11
|
+
logRequestBody?: boolean;
|
|
12
|
+
/** Whether to log response body. Default: true */
|
|
13
|
+
logResponseBody?: boolean;
|
|
14
|
+
/** Maximum length for body preview. Default: 500 */
|
|
15
|
+
maxBodyLength?: number;
|
|
16
|
+
/** Headers to redact (case-insensitive). Default: ['authorization', 'cookie', 'set-cookie', 'x-api-key', 'x-auth-token', 'proxy-authorization'] */
|
|
17
|
+
redactedHeaders?: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Creates a traced fetch function with HTTP request/response introspection.
|
|
21
|
+
* Logs HTTP details to the provided logger when set to TRACE log level.
|
|
22
|
+
* Includes request timing, headers, and optional body previews.
|
|
23
|
+
* Headers containing sensitive data are redacted by default.
|
|
24
|
+
* @param logger - Logger instance from Logger() factory
|
|
25
|
+
* @param options - Configuration options for logging behavior
|
|
26
|
+
* @returns A fetch-compatible function with tracing capabilities
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```typescript
|
|
30
|
+
* const logger = Logger(console.log, {trace: true})
|
|
31
|
+
* const tracedFetch = createTracedFetch(logger)
|
|
32
|
+
* const response = await tracedFetch('https://api.example.com/data')
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function createTracedFetch(logger: ReturnType<typeof Logger>, options?: TracedFetchOptions): typeof fetch;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { performance } from 'node:perf_hooks';
|
|
3
|
+
import { env } from 'node:process';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
let defaultMaxLength = env.SANITY_TRACE_MAX_LENGTH
|
|
6
|
+
? Number.parseInt(env.SANITY_TRACE_MAX_LENGTH, 10)
|
|
7
|
+
: 500;
|
|
8
|
+
if (Number.isNaN(defaultMaxLength))
|
|
9
|
+
defaultMaxLength = 500;
|
|
10
|
+
const DEFAULT_OPTIONS = {
|
|
11
|
+
logRequestHeaders: true,
|
|
12
|
+
logResponseHeaders: true,
|
|
13
|
+
logRequestBody: true,
|
|
14
|
+
logResponseBody: true,
|
|
15
|
+
maxBodyLength: defaultMaxLength,
|
|
16
|
+
redactedHeaders: [
|
|
17
|
+
'authorization',
|
|
18
|
+
'cookie',
|
|
19
|
+
'set-cookie',
|
|
20
|
+
'x-api-key',
|
|
21
|
+
'x-auth-token',
|
|
22
|
+
'proxy-authorization',
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Content types that are safe to log as text
|
|
27
|
+
*/
|
|
28
|
+
const SAFE_CONTENT_TYPES = [
|
|
29
|
+
'application/json',
|
|
30
|
+
'application/xml',
|
|
31
|
+
'application/x-www-form-urlencoded',
|
|
32
|
+
'text/',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Check if content type is safe to log
|
|
36
|
+
*/
|
|
37
|
+
function isSafeContentType(contentType) {
|
|
38
|
+
const lower = contentType.toLowerCase();
|
|
39
|
+
return SAFE_CONTENT_TYPES.some((safe) => lower.startsWith(safe));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Redact sensitive headers
|
|
43
|
+
*/
|
|
44
|
+
function redactHeaders(headers, redactedKeys) {
|
|
45
|
+
if (!headers)
|
|
46
|
+
return {};
|
|
47
|
+
const result = {};
|
|
48
|
+
const redactedLower = redactedKeys.map((k) => k.toLowerCase());
|
|
49
|
+
// Handle Headers object
|
|
50
|
+
if (headers instanceof Headers) {
|
|
51
|
+
headers.forEach((value, key) => {
|
|
52
|
+
result[key] = redactedLower.includes(key.toLowerCase()) ? '[REDACTED]' : value;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
// Handle array of tuples
|
|
56
|
+
else if (Array.isArray(headers)) {
|
|
57
|
+
for (const [key, value] of headers) {
|
|
58
|
+
result[key] = redactedLower.includes(key.toLowerCase()) ? '[REDACTED]' : value;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Handle plain object
|
|
62
|
+
else {
|
|
63
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
64
|
+
result[key] = redactedLower.includes(key.toLowerCase()) ? '[REDACTED]' : value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create a preview of body content
|
|
71
|
+
*/
|
|
72
|
+
function createBodyPreview(body, maxLength, contentType) {
|
|
73
|
+
if (body === null) {
|
|
74
|
+
return '[empty]';
|
|
75
|
+
}
|
|
76
|
+
// Handle string bodies
|
|
77
|
+
if (typeof body === 'string') {
|
|
78
|
+
if (!contentType || isSafeContentType(contentType)) {
|
|
79
|
+
if (body.length <= maxLength) {
|
|
80
|
+
return body;
|
|
81
|
+
}
|
|
82
|
+
return `${body.slice(0, maxLength)}... [truncated]`;
|
|
83
|
+
}
|
|
84
|
+
return `[non-text content, ${body.length} chars]`;
|
|
85
|
+
}
|
|
86
|
+
// Handle Buffer
|
|
87
|
+
if (Buffer.isBuffer(body)) {
|
|
88
|
+
if (contentType && isSafeContentType(contentType)) {
|
|
89
|
+
const text = body.toString('utf8');
|
|
90
|
+
if (text.length <= maxLength) {
|
|
91
|
+
return text;
|
|
92
|
+
}
|
|
93
|
+
return `${text.slice(0, maxLength)}... [truncated]`;
|
|
94
|
+
}
|
|
95
|
+
return `[${contentType} binary, ${body.length} bytes]`;
|
|
96
|
+
}
|
|
97
|
+
return '[unknown content type]';
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Extract body from RequestInit if present
|
|
101
|
+
*/
|
|
102
|
+
async function extractRequestBody(init) {
|
|
103
|
+
if (!init?.body) {
|
|
104
|
+
return { body: null, contentType: null };
|
|
105
|
+
}
|
|
106
|
+
const contentType = init.headers ? new Headers(init.headers).get('content-type') : null;
|
|
107
|
+
// init.body in node can be string, buffer, blob, formdata, url search params or a readable stream, so handle each case.
|
|
108
|
+
if (typeof init.body === 'string') {
|
|
109
|
+
return { body: init.body, contentType };
|
|
110
|
+
}
|
|
111
|
+
if (init.body instanceof FormData) {
|
|
112
|
+
return { body: '[FormData]', contentType };
|
|
113
|
+
}
|
|
114
|
+
if (init.body instanceof URLSearchParams) {
|
|
115
|
+
return { body: init.body.toString(), contentType };
|
|
116
|
+
}
|
|
117
|
+
if (init.body instanceof Blob) {
|
|
118
|
+
return { body: '[FormData]', contentType: contentType || init.body.type };
|
|
119
|
+
}
|
|
120
|
+
if (init.body instanceof ReadableStream) {
|
|
121
|
+
return { body: '[ReadableStream]', contentType };
|
|
122
|
+
}
|
|
123
|
+
if (Buffer.isBuffer(init.body)) {
|
|
124
|
+
return { body: init.body, contentType };
|
|
125
|
+
}
|
|
126
|
+
return { body: '[unknown body type]', contentType };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract body from Response
|
|
130
|
+
*/
|
|
131
|
+
async function extractResponseBody(response, maxLength) {
|
|
132
|
+
const contentType = response.headers.get('content-type');
|
|
133
|
+
// Don't try to read body for streaming responses (event-stream)
|
|
134
|
+
if (contentType?.includes('text/event-stream')) {
|
|
135
|
+
return '[streaming response]';
|
|
136
|
+
}
|
|
137
|
+
// Clone response to avoid consuming it
|
|
138
|
+
const cloned = response.clone();
|
|
139
|
+
try {
|
|
140
|
+
// Try to read as text
|
|
141
|
+
const text = await cloned.text();
|
|
142
|
+
if (!contentType || isSafeContentType(contentType)) {
|
|
143
|
+
if (text.length <= maxLength) {
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
return `${text.slice(0, maxLength)}... [truncated]`;
|
|
147
|
+
}
|
|
148
|
+
return `[binary content, ${text.length} bytes]`;
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return `[unable to read body: ${error instanceof Error ? error.message : String(error)}]`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Creates a traced fetch function with HTTP request/response introspection.
|
|
156
|
+
* Logs HTTP details to the provided logger when set to TRACE log level.
|
|
157
|
+
* Includes request timing, headers, and optional body previews.
|
|
158
|
+
* Headers containing sensitive data are redacted by default.
|
|
159
|
+
* @param logger - Logger instance from Logger() factory
|
|
160
|
+
* @param options - Configuration options for logging behavior
|
|
161
|
+
* @returns A fetch-compatible function with tracing capabilities
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* ```typescript
|
|
165
|
+
* const logger = Logger(console.log, {trace: true})
|
|
166
|
+
* const tracedFetch = createTracedFetch(logger)
|
|
167
|
+
* const response = await tracedFetch('https://api.example.com/data')
|
|
168
|
+
* ```
|
|
169
|
+
*/
|
|
170
|
+
export function createTracedFetch(logger, options) {
|
|
171
|
+
const opts = {
|
|
172
|
+
...DEFAULT_OPTIONS,
|
|
173
|
+
...options,
|
|
174
|
+
};
|
|
175
|
+
return async (input, init) => {
|
|
176
|
+
const requestId = randomUUID().slice(0, 3);
|
|
177
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
|
|
178
|
+
const method = init?.method || 'GET';
|
|
179
|
+
const startTime = performance.now();
|
|
180
|
+
function trace(type, msgTemplate, ...args) {
|
|
181
|
+
const arrow = type === 'req' ? '→' : type === 'res' ? '←' : chalk.red('✗');
|
|
182
|
+
logger.trace(`${chalk.dim('[%s]')} HTTP ${arrow} ${msgTemplate}`, requestId, ...args);
|
|
183
|
+
}
|
|
184
|
+
// Log request URL
|
|
185
|
+
trace('req', '%s %s', method, url);
|
|
186
|
+
// Log request headers
|
|
187
|
+
if (opts.logRequestHeaders && init?.headers) {
|
|
188
|
+
const redacted = redactHeaders(init.headers, opts.redactedHeaders);
|
|
189
|
+
trace('req', 'Headers: %j', redacted);
|
|
190
|
+
}
|
|
191
|
+
// Log request body
|
|
192
|
+
if (opts.logRequestBody && init?.body) {
|
|
193
|
+
try {
|
|
194
|
+
const { body, contentType } = await extractRequestBody(init);
|
|
195
|
+
const preview = createBodyPreview(body, opts.maxBodyLength, contentType);
|
|
196
|
+
trace('req', 'Body: %s', preview);
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
trace('err', 'Body: [error reading body: %s]', error instanceof Error ? error.message : String(error));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
// Make the actual fetch call
|
|
204
|
+
const response = await fetch(input, init);
|
|
205
|
+
const endTime = performance.now();
|
|
206
|
+
const duration = Math.round(endTime - startTime);
|
|
207
|
+
// Log response
|
|
208
|
+
trace('res', '%d %s (%dms)', response.status, response.statusText, duration);
|
|
209
|
+
// Log response headers
|
|
210
|
+
if (opts.logResponseHeaders) {
|
|
211
|
+
const headers = {};
|
|
212
|
+
response.headers.forEach((value, key) => {
|
|
213
|
+
headers[key] = value;
|
|
214
|
+
});
|
|
215
|
+
trace('res', 'Headers: %j', headers);
|
|
216
|
+
}
|
|
217
|
+
// Log response body
|
|
218
|
+
if (opts.logResponseBody) {
|
|
219
|
+
try {
|
|
220
|
+
const bodyPreview = await extractResponseBody(response, opts.maxBodyLength);
|
|
221
|
+
trace('res', 'Body: %s', bodyPreview);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
trace('err', 'Body: [error reading body: %s]', error instanceof Error ? error.message : String(error));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return response;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const endTime = performance.now();
|
|
231
|
+
const duration = Math.round(endTime - startTime);
|
|
232
|
+
// Log error
|
|
233
|
+
trace('err', '%s %s (%dms) - %s', method, url, duration, error instanceof Error ? error.message : String(error));
|
|
234
|
+
// Re-throw the error
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Logger } from './logger.js';
|
|
1
2
|
import type { Result } from './types.js';
|
|
2
|
-
export declare function validToken(maybeToken?: string): Promise<string>;
|
|
3
|
-
export declare function validTokenOrErrorMessage(maybeToken?: string): Promise<Result<string, {
|
|
3
|
+
export declare function validToken(logger: ReturnType<typeof Logger>, maybeToken?: string): Promise<string>;
|
|
4
|
+
export declare function validTokenOrErrorMessage(logger: ReturnType<typeof Logger>, maybeToken?: string): Promise<Result<string, {
|
|
4
5
|
e: Error | unknown;
|
|
5
6
|
message: string;
|
|
6
7
|
}>>;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import config from '../config.js';
|
|
2
|
-
|
|
2
|
+
import { createTracedFetch } from './traced-fetch.js';
|
|
3
|
+
export async function validToken(logger, maybeToken) {
|
|
3
4
|
if (config.isTest)
|
|
4
5
|
return maybeToken ?? 'token';
|
|
6
|
+
const fetchFn = createTracedFetch(logger);
|
|
5
7
|
const token = maybeToken ?? config.token;
|
|
6
8
|
if (!token)
|
|
7
9
|
throw new Error('NO_TOKEN');
|
|
8
10
|
const url = `${config.apiUrl}v2025-04-23/users/me`;
|
|
9
|
-
const response = await
|
|
11
|
+
const response = await fetchFn(url, {
|
|
10
12
|
method: 'GET',
|
|
11
13
|
headers: {
|
|
12
14
|
Accept: 'application/json',
|
|
@@ -21,9 +23,9 @@ export async function validToken(maybeToken) {
|
|
|
21
23
|
}
|
|
22
24
|
throw new Error('SERVER_ERROR', { cause: response.statusText });
|
|
23
25
|
}
|
|
24
|
-
export async function validTokenOrErrorMessage(maybeToken) {
|
|
26
|
+
export async function validTokenOrErrorMessage(logger, maybeToken) {
|
|
25
27
|
try {
|
|
26
|
-
const token = await validToken(maybeToken);
|
|
28
|
+
const token = await validToken(logger, maybeToken);
|
|
27
29
|
return { ok: true, value: token };
|
|
28
30
|
}
|
|
29
31
|
catch (e) {
|