@vigilbase/cli 0.2.3 → 0.2.5
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/CLOUDFLARE.md +2 -2
- package/COMMANDS.md +111 -44
- package/LOCAL.md +99 -38
- package/ONE.md +2 -2
- package/README.md +215 -67
- package/dist/auth/acl.d.ts +6 -0
- package/dist/auth/acl.js +45 -0
- package/dist/auth/device-auth.js +1 -0
- package/dist/auth/provider.js +4 -4
- package/dist/auth/refresh.d.ts +5 -4
- package/dist/auth/refresh.js +82 -41
- package/dist/auth/store.d.ts +25 -0
- package/dist/auth/store.js +91 -11
- package/dist/bin.js +13 -1
- package/dist/cli.js +205 -81
- package/dist/commands/account.js +11 -7
- package/dist/commands/auth.d.ts +6 -0
- package/dist/commands/auth.js +75 -24
- package/dist/commands/billing.js +2 -2
- package/dist/commands/cloudflare.d.ts +1 -1
- package/dist/commands/cloudflare.js +31 -17
- package/dist/commands/helpers.js +3 -3
- package/dist/commands/index.d.ts +1 -0
- package/dist/commands/index.js +2 -1
- package/dist/commands/legacy-validation.d.ts +20 -0
- package/dist/commands/legacy-validation.js +77 -3
- package/dist/commands/local.js +186 -65
- package/dist/commands/one.js +16 -13
- package/dist/commands/public-tools.js +52 -27
- package/dist/commands/runs.js +5 -5
- package/dist/commands/security.d.ts +1 -1
- package/dist/commands/security.js +41 -37
- package/dist/commands/watchdog.js +5 -5
- package/dist/lib/banner.d.ts +5 -11
- package/dist/lib/banner.js +11 -31
- package/dist/lib/client.js +19 -1
- package/dist/lib/cloudflare-client.d.ts +2 -2
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +77 -19
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +75 -24
- package/dist/lib/help.js +57 -6
- package/dist/lib/network.d.ts +12 -3
- package/dist/lib/network.js +73 -9
- package/dist/lib/one-client.js +9 -2
- package/dist/lib/one-error-hints.d.ts +1 -0
- package/dist/lib/one-error-hints.js +10 -0
- package/dist/lib/open-url.js +2 -1
- package/dist/lib/platform.d.ts +2 -0
- package/dist/lib/platform.js +5 -0
- package/dist/lib/progress.d.ts +18 -3
- package/dist/lib/progress.js +72 -10
- package/dist/lib/system-trust.d.ts +2 -0
- package/dist/lib/system-trust.js +6 -0
- package/dist/lib/tui.d.ts +1 -1
- package/dist/lib/tui.js +5 -7
- package/dist/local/cloudflare-checkup/cf/child-env.js +2 -4
- package/dist/local/cloudflare-checkup/cf/install.d.ts +4 -2
- package/dist/local/cloudflare-checkup/cf/install.js +55 -24
- package/dist/local/cloudflare-checkup/cf/locate.d.ts +2 -0
- package/dist/local/cloudflare-checkup/cf/locate.js +10 -1
- package/dist/local/cloudflare-checkup/cf/run-cf.d.ts +18 -3
- package/dist/local/cloudflare-checkup/cf/run-cf.js +75 -17
- package/dist/local/cloudflare-checkup/contract.d.ts +16 -9
- package/dist/local/cloudflare-checkup/contract.js +80 -14
- package/dist/local/cloudflare-checkup/errors.d.ts +34 -1
- package/dist/local/cloudflare-checkup/errors.js +36 -5
- package/dist/local/cloudflare-checkup/output.d.ts +6 -0
- package/dist/local/cloudflare-checkup/output.js +9 -0
- package/dist/local/cloudflare-checkup/snapshot-file.d.ts +6 -2
- package/dist/local/cloudflare-checkup/snapshot-file.js +25 -11
- package/dist/local/cloudflare-checkup/submit-client.js +7 -1
- package/dist/types.d.ts +2 -1
- package/esm-only.cjs +3 -0
- package/package.json +7 -4
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { CloudflareClient, accountResourceSchema, cloudflareScopeSchema, workspaceSectionSchema, } from '../lib/cloudflare-client.js';
|
|
3
3
|
import { assertCommandFlags, asArray, asRecord, ok, outputJson, requireOneClient, withSpinner, } from './helpers.js';
|
|
4
|
-
|
|
4
|
+
import { InvalidArgumentError, inRange, oneOf, unknownSubcommand } from './legacy-validation.js';
|
|
5
5
|
const checkupUsage = 'Usage: vigilbase cf checkup [status|start|progress <run-id>|result <run-id>] [--customer-id …] [--account-id …] [--json]';
|
|
6
6
|
export const CLOUDFLARE_HELP = `Usage: vigilbase cf <command> [options]
|
|
7
7
|
Alias: vigilbase cloudflare
|
|
8
8
|
|
|
9
9
|
Inventory and workspace:
|
|
10
|
-
zones List zones (default)
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
zones List zones (default)
|
|
11
|
+
--customer-id --account-id --zone-id
|
|
12
|
+
zone <zone-id> Zone detail; --customer-id for live details,
|
|
13
|
+
optional --account-id
|
|
14
|
+
accounts List accessible accounts
|
|
15
|
+
--customer-id --account-id
|
|
13
16
|
account <account-id> Stored account detail; --customer-id
|
|
14
|
-
workspace [section] Workspace bootstrap or section
|
|
15
|
-
|
|
17
|
+
workspace [section] Workspace bootstrap or one section
|
|
18
|
+
--customer-id --account-id --zone-id
|
|
19
|
+
Sections: overview, reports, zones, rule-analyzer,
|
|
20
|
+
accounts, checkups, settings
|
|
16
21
|
|
|
17
22
|
Zone reads (--customer-id <uuid> required; IDs can use --zone-id):
|
|
18
23
|
dns <zone-id> DNS records, TTL and proxy state
|
|
19
|
-
waf <zone-id> WAF rules, expressions, analytics and
|
|
24
|
+
waf <zone-id> WAF rules, expressions, analytics and
|
|
25
|
+
suggestions
|
|
20
26
|
speed-settings <zone-id> Performance settings (internal users only)
|
|
21
27
|
|
|
22
28
|
Account reads (optional --customer-id; IDs can use --account-id):
|
|
23
|
-
audit-logs <account-id> Latest provider audit events (up to 100
|
|
24
|
-
|
|
29
|
+
audit-logs <account-id> Latest provider audit events (up to 100)
|
|
30
|
+
Needs service-read permission.
|
|
31
|
+
zero-trust <account-id> Users, Access applications, tunnels and
|
|
32
|
+
authentication analytics
|
|
25
33
|
developer-platform <account-id>
|
|
26
34
|
Workers, Workers analytics and Pages
|
|
27
35
|
magic-transit <account-id> Prefixes, routes and traffic analytics
|
|
28
36
|
Zero Trust, Developer Platform and Magic Transit require deployment-read permission.
|
|
29
37
|
|
|
30
38
|
Analytics and checkups:
|
|
31
|
-
analytics --customer-id <uuid> [--account-id <id> | --zone-id <id>]
|
|
39
|
+
analytics --customer-id <uuid> [--account-id <id> | --zone-id <id>]
|
|
40
|
+
[--days 1..30]
|
|
32
41
|
Default 7 days; live zone analytics uses the most recent 23 hours.
|
|
33
|
-
checkup [status] Latest completed checkup
|
|
34
|
-
|
|
42
|
+
checkup [status] Latest completed checkup
|
|
43
|
+
--customer-id --account-id
|
|
44
|
+
checkup start Start and persist a scan
|
|
45
|
+
--customer-id --account-id
|
|
35
46
|
checkup progress <run-uuid> Read saved progress once
|
|
36
47
|
checkup result <run-uuid> Read the stored Cloudflare checkup result
|
|
37
48
|
checkups aliases checkup; list/show alias status; run aliases start.
|
|
@@ -44,7 +55,7 @@ See CLOUDFLARE.md for full grammar, API mapping, data limits and examples.`;
|
|
|
44
55
|
function validate(schema, input) {
|
|
45
56
|
const result = schema.safeParse(input);
|
|
46
57
|
if (!result.success)
|
|
47
|
-
throw new
|
|
58
|
+
throw new InvalidArgumentError(result.error.issues.map((issue) => issue.message).join(' '));
|
|
48
59
|
return result.data;
|
|
49
60
|
}
|
|
50
61
|
function dataOf(payload) {
|
|
@@ -85,7 +96,7 @@ export const handleCloudflare = async (ctx) => {
|
|
|
85
96
|
if (scope.zoneId && ['accounts', 'account', ...accountResourceSchema.options, 'checkup', 'checkups'].includes(subcommand))
|
|
86
97
|
throw new Error(`cf ${subcommand} does not support --zone-id.`);
|
|
87
98
|
const args = ctx.rest.slice(1);
|
|
88
|
-
const noExtraArgs = (count, message = usage) => {
|
|
99
|
+
const noExtraArgs = (count, message = `Unexpected arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`) => {
|
|
89
100
|
if (args.length > count)
|
|
90
101
|
throw new Error(message);
|
|
91
102
|
};
|
|
@@ -100,7 +111,7 @@ export const handleCloudflare = async (ctx) => {
|
|
|
100
111
|
};
|
|
101
112
|
// Validate commands before requesting credentials or making API calls.
|
|
102
113
|
if (!['zones', 'zone', 'accounts', 'workspace', 'dns', 'waf', 'speed-settings', 'account', ...accountResourceSchema.options, 'analytics', 'checkup', 'checkups'].includes(subcommand)) {
|
|
103
|
-
throw
|
|
114
|
+
throw unknownSubcommand(ctx.command, ctx.rest[0] ?? '');
|
|
104
115
|
}
|
|
105
116
|
let zoneId;
|
|
106
117
|
let accountId;
|
|
@@ -132,7 +143,7 @@ export const handleCloudflare = async (ctx) => {
|
|
|
132
143
|
noExtraArgs(0);
|
|
133
144
|
}
|
|
134
145
|
const section = subcommand === 'workspace'
|
|
135
|
-
? (noExtraArgs(1),
|
|
146
|
+
? (noExtraArgs(1), oneOf('Workspace section', workspaceSectionSchema.options, args[0]))
|
|
136
147
|
: undefined;
|
|
137
148
|
let days = 7;
|
|
138
149
|
if (subcommand === 'analytics') {
|
|
@@ -140,7 +151,10 @@ export const handleCloudflare = async (ctx) => {
|
|
|
140
151
|
throw new Error('Cloudflare analytics requires --customer-id <uuid>. Find it with `vigilbase cf accounts --json`.');
|
|
141
152
|
if (scope.zoneId && scope.accountId)
|
|
142
153
|
throw new Error('Choose either --zone-id or --account-id for analytics.');
|
|
143
|
-
|
|
154
|
+
const raw = ctx.values.days ?? '7';
|
|
155
|
+
if (!/^\d+$/.test(raw))
|
|
156
|
+
throw new InvalidArgumentError('--days must be an integer.');
|
|
157
|
+
days = inRange('--days', Number(raw), 1, 30);
|
|
144
158
|
}
|
|
145
159
|
const action = (args[0] ?? 'status').toLowerCase();
|
|
146
160
|
let runId;
|
package/dist/commands/helpers.js
CHANGED
|
@@ -2,6 +2,7 @@ import { printJson as writeJson } from '../lib/format.js';
|
|
|
2
2
|
import { withProgress } from '../lib/progress.js';
|
|
3
3
|
import { EXIT } from '../types.js';
|
|
4
4
|
import { getOneAccessToken } from '../auth/index.js';
|
|
5
|
+
import { NotSignedInError } from '../auth/store.js';
|
|
5
6
|
import { VigilbaseOneClient as OneClient } from '../lib/one-client.js';
|
|
6
7
|
/** Reject known flags from other command groups instead of silently ignoring filters. */
|
|
7
8
|
export function assertCommandFlags(ctx, allowed) {
|
|
@@ -33,9 +34,8 @@ export function asArray(value) {
|
|
|
33
34
|
}
|
|
34
35
|
export async function requireOneClient(ctx) {
|
|
35
36
|
const accessToken = await getOneAccessToken(ctx.config);
|
|
36
|
-
if (!accessToken)
|
|
37
|
-
throw new
|
|
38
|
-
}
|
|
37
|
+
if (!accessToken)
|
|
38
|
+
throw new NotSignedInError();
|
|
39
39
|
return new OneClient(ctx.config, accessToken, true);
|
|
40
40
|
}
|
|
41
41
|
export async function withSpinner(ctx, label, work) {
|
package/dist/commands/index.d.ts
CHANGED
|
@@ -7,4 +7,5 @@ import type { CommandHandler } from './helpers.js';
|
|
|
7
7
|
* `cloudflare-checkup` is the top-level alias of `local cloudflare-checkup`.
|
|
8
8
|
*/
|
|
9
9
|
export declare const COMMAND_HANDLERS: Record<string, CommandHandler>;
|
|
10
|
+
/** Own entries only: names such as constructor or __proto__ are not commands. */
|
|
10
11
|
export declare function getCommandHandler(command: string): CommandHandler | null;
|
package/dist/commands/index.js
CHANGED
|
@@ -43,6 +43,7 @@ export const COMMAND_HANDLERS = {
|
|
|
43
43
|
local: handleLocal,
|
|
44
44
|
'cloudflare-checkup': handleCloudflareCheckup,
|
|
45
45
|
};
|
|
46
|
+
/** Own entries only: names such as constructor or __proto__ are not commands. */
|
|
46
47
|
export function getCommandHandler(command) {
|
|
47
|
-
return COMMAND_HANDLERS[command]
|
|
48
|
+
return Object.hasOwn(COMMAND_HANDLERS, command) ? COMMAND_HANDLERS[command] : null;
|
|
48
49
|
}
|
|
@@ -1,12 +1,28 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { type CommandContext } from './helpers.js';
|
|
3
|
+
/** A flag or argument the CLI rejected before any request; `--json` reports it as code INVALID_ARGUMENT. */
|
|
4
|
+
export declare class InvalidArgumentError extends Error {
|
|
5
|
+
readonly code = "INVALID_ARGUMENT";
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* Exactly `count` positional arguments. When some are missing, they are named from the trailing `<placeholders>`
|
|
5
10
|
* of `usage` (for example `vigilbase run <tool> <target>`), so `usage` lists only positional placeholders.
|
|
11
|
+
* An empty or whitespace-only argument counts as missing.
|
|
6
12
|
*/
|
|
7
13
|
export declare function validateArguments(ctx: CommandContext, count: number, flags?: readonly string[], usage?: string): void;
|
|
8
14
|
/** "Missing <target>. Usage: vigilbase headers <url>", or a pointer to --help without a usage. */
|
|
9
15
|
export declare function missingArguments(command: string, missingCount: number, usage?: string): string;
|
|
16
|
+
/** 'Unknown security command "x". Run `vigilbase security --help` for usage.' The pointer names the top-level group. */
|
|
17
|
+
export declare function unknownSubcommand(group: string, subcommand: string): Error;
|
|
18
|
+
/** Parses user input with `schema`, throwing `message` instead of Zod's issue list. */
|
|
19
|
+
export declare function parseArgument<T>(schema: z.ZodType<T>, value: unknown, message: string): T;
|
|
20
|
+
/** An integer already checked as digits, within [min, max]. */
|
|
21
|
+
export declare function inRange(label: string, value: number, min: number, max: number): number;
|
|
22
|
+
export declare function oneOf<const T extends readonly string[]>(label: string, values: T, value: string | undefined): T[number] | undefined;
|
|
23
|
+
/** A trimmed, non-empty flag value of at most `max` characters. */
|
|
24
|
+
export declare function textValue(label: string, value: string | undefined, max: number): string | undefined;
|
|
25
|
+
export declare function isoDateTime(label: string, value: string): string;
|
|
10
26
|
export declare function limitValue(raw: string | undefined, fallback: number, max: number): number;
|
|
11
27
|
export declare const invitationSchema: z.ZodObject<{
|
|
12
28
|
email: z.ZodString;
|
|
@@ -23,4 +39,8 @@ export declare const invitationSchema: z.ZodObject<{
|
|
|
23
39
|
sales_only: "sales_only";
|
|
24
40
|
}>>;
|
|
25
41
|
}, z.core.$strip>;
|
|
42
|
+
export declare function invitationValue(email: string | undefined, role: string | undefined): z.infer<typeof invitationSchema>;
|
|
26
43
|
export declare const toolNameSchema: z.ZodString;
|
|
44
|
+
export declare function toolNameValue(value: string): string;
|
|
45
|
+
/** A scan target: trimmed, 1-2048 characters, no control characters. */
|
|
46
|
+
export declare function targetValue(value: string): string;
|
|
@@ -1,14 +1,31 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { assertCommandFlags } from './helpers.js';
|
|
3
|
+
/** A flag or argument the CLI rejected before any request; `--json` reports it as code INVALID_ARGUMENT. */
|
|
4
|
+
export class InvalidArgumentError extends Error {
|
|
5
|
+
code = 'INVALID_ARGUMENT';
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'InvalidArgumentError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Exactly `count` positional arguments. When some are missing, they are named from the trailing `<placeholders>`
|
|
5
13
|
* of `usage` (for example `vigilbase run <tool> <target>`), so `usage` lists only positional placeholders.
|
|
14
|
+
* An empty or whitespace-only argument counts as missing.
|
|
6
15
|
*/
|
|
7
16
|
export function validateArguments(ctx, count, flags = [], usage) {
|
|
8
17
|
if (ctx.rest.length < count)
|
|
9
18
|
throw new Error(missingArguments(ctx.command, count - ctx.rest.length, usage));
|
|
10
19
|
if (ctx.rest.length > count)
|
|
11
20
|
throw new Error(`Unexpected arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`);
|
|
21
|
+
const blank = ctx.rest.flatMap((value, index) => value.trim() ? [] : [index]);
|
|
22
|
+
if (blank.length) {
|
|
23
|
+
const placeholders = usage?.match(/<[^>]+>/g) ?? [];
|
|
24
|
+
const missing = blank.map((index) => placeholders[index - (count - placeholders.length)]);
|
|
25
|
+
throw new Error(missing.every(Boolean) && usage
|
|
26
|
+
? `Missing ${missing.join(' and ')}. Usage: ${usage}`
|
|
27
|
+
: `Missing arguments. Run \`vigilbase ${ctx.command} --help\` for usage.`);
|
|
28
|
+
}
|
|
12
29
|
assertCommandFlags(ctx, flags);
|
|
13
30
|
}
|
|
14
31
|
/** "Missing <target>. Usage: vigilbase headers <url>", or a pointer to --help without a usage. */
|
|
@@ -18,15 +35,72 @@ export function missingArguments(command, missingCount, usage) {
|
|
|
18
35
|
return `Missing arguments. Run \`vigilbase ${command} --help\` for usage.`;
|
|
19
36
|
return `Missing ${missing.join(' and ')}. Usage: ${usage}`;
|
|
20
37
|
}
|
|
38
|
+
/** 'Unknown security command "x". Run `vigilbase security --help` for usage.' The pointer names the top-level group. */
|
|
39
|
+
export function unknownSubcommand(group, subcommand) {
|
|
40
|
+
return new InvalidArgumentError(`Unknown ${group} command "${subcommand.slice(0, 80)}". Run \`vigilbase ${group.split(' ')[0]} --help\` for usage.`);
|
|
41
|
+
}
|
|
42
|
+
/** Parses user input with `schema`, throwing `message` instead of Zod's issue list. */
|
|
43
|
+
export function parseArgument(schema, value, message) {
|
|
44
|
+
const result = schema.safeParse(value);
|
|
45
|
+
if (!result.success)
|
|
46
|
+
throw new InvalidArgumentError(message);
|
|
47
|
+
return result.data;
|
|
48
|
+
}
|
|
49
|
+
/** An integer already checked as digits, within [min, max]. */
|
|
50
|
+
export function inRange(label, value, min, max) {
|
|
51
|
+
if (!Number.isSafeInteger(value) || value < min || value > max)
|
|
52
|
+
throw new InvalidArgumentError(`${label} must be between ${min} and ${max}.`);
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
export function oneOf(label, values, value) {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
if (!values.includes(value))
|
|
59
|
+
throw new InvalidArgumentError(`${label} must be one of: ${values.join(', ')}.`);
|
|
60
|
+
return value;
|
|
61
|
+
}
|
|
62
|
+
/** A trimmed, non-empty flag value of at most `max` characters. */
|
|
63
|
+
export function textValue(label, value, max) {
|
|
64
|
+
if (value === undefined)
|
|
65
|
+
return undefined;
|
|
66
|
+
const trimmed = value.trim();
|
|
67
|
+
if (!trimmed)
|
|
68
|
+
throw new InvalidArgumentError(`${label} must not be empty.`);
|
|
69
|
+
if (trimmed.length > max)
|
|
70
|
+
throw new InvalidArgumentError(`${label} must be at most ${max} characters.`);
|
|
71
|
+
return trimmed;
|
|
72
|
+
}
|
|
73
|
+
export function isoDateTime(label, value) {
|
|
74
|
+
return parseArgument(z.iso.datetime({ offset: true }), value, `${label} must be an ISO 8601 date-time with an offset, for example 2026-10-12T00:00:00Z.`);
|
|
75
|
+
}
|
|
21
76
|
export function limitValue(raw, fallback, max) {
|
|
22
77
|
if (raw === undefined)
|
|
23
78
|
return fallback;
|
|
24
79
|
if (!/^\d+$/.test(raw))
|
|
25
|
-
throw new
|
|
26
|
-
return
|
|
80
|
+
throw new InvalidArgumentError('--limit must be an integer.');
|
|
81
|
+
return inRange('--limit', Number(raw), 1, max);
|
|
27
82
|
}
|
|
83
|
+
const invitationRoles = ['admin', 'member', 'readonly', 'partner_owner', 'partner_sales', 'partner_operations', 'partner_finance', 'partner_readonly', 'billing_only', 'sales_only'];
|
|
28
84
|
export const invitationSchema = z.object({
|
|
29
85
|
email: z.string().trim().email(),
|
|
30
|
-
role: z.enum(
|
|
86
|
+
role: z.enum(invitationRoles).default('member'),
|
|
31
87
|
});
|
|
88
|
+
export function invitationValue(email, role) {
|
|
89
|
+
return {
|
|
90
|
+
email: parseArgument(invitationSchema.shape.email, email, 'Enter a valid email address for the invitation.'),
|
|
91
|
+
role: oneOf('--role', invitationRoles, role) ?? 'member',
|
|
92
|
+
};
|
|
93
|
+
}
|
|
32
94
|
export const toolNameSchema = z.string().trim().min(1).max(128).regex(/^[a-z0-9-]+$/, 'Tool names must contain lowercase letters, digits, or hyphens.');
|
|
95
|
+
export function toolNameValue(value) {
|
|
96
|
+
return parseArgument(toolNameSchema, value, 'Tool names must contain lowercase letters, digits, or hyphens.');
|
|
97
|
+
}
|
|
98
|
+
/** A scan target: trimmed, 1-2048 characters, no control characters. */
|
|
99
|
+
export function targetValue(value) {
|
|
100
|
+
const trimmed = value.trim();
|
|
101
|
+
if (/[\u0000-\u001f\u007f]/.test(trimmed))
|
|
102
|
+
throw new InvalidArgumentError('Target must not contain control characters.');
|
|
103
|
+
if (!trimmed || trimmed.length > 2048)
|
|
104
|
+
throw new InvalidArgumentError('Target must be 1-2048 characters.');
|
|
105
|
+
return trimmed;
|
|
106
|
+
}
|