requestshield 0.1.3 → 0.1.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.
@@ -0,0 +1,90 @@
1
+ // @ts-check
2
+
3
+ import { CliError } from "../errors.mjs";
4
+ import { objectString } from "../api-client.mjs";
5
+
6
+ const HEADERS = ["APP KEY", "NAME", "STATUS"];
7
+
8
+ /** @typedef {{ appKey: string, name: string, status: string, createdAt: string, updatedAt: string }} App */
9
+
10
+ /**
11
+ * @param {{ json: boolean }} options
12
+ * @param {{ api: { listApps(accessToken: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
13
+ */
14
+ export async function listApps(options, deps) {
15
+ const accessToken =
16
+ await deps.sessions.loadToken();
17
+
18
+ const result =
19
+ await deps.api.listApps(accessToken);
20
+
21
+ const { data: apps, nextCursor } = parseApps(result.body);
22
+
23
+ if (options.json) {
24
+ deps.log(JSON.stringify({ data: apps, nextCursor }, null, 2));
25
+ return;
26
+ }
27
+
28
+ if (apps.length === 0) {
29
+ deps.log("No applications are available to this account.");
30
+ return;
31
+ }
32
+
33
+ for (const line of formatTable(apps)) deps.log(line);
34
+ }
35
+
36
+ /** @param {unknown} body @returns {{ data: App[], nextCursor: string | null }} */
37
+ function parseApps(body) {
38
+ const data = body && typeof body === "object" ?
39
+ Reflect.get(body, "data") : undefined;
40
+
41
+ const nextCursor = body && typeof body === "object" ?
42
+ Reflect.get(body, "nextCursor") : undefined;
43
+
44
+ if (!Array.isArray(data)) {
45
+ throw new CliError("The applications response did not contain a data array", {
46
+ code: "INVALID_RESPONSE",
47
+ });
48
+ }
49
+
50
+ if (nextCursor !== null && (typeof nextCursor !== "string" || nextCursor.length === 0)) {
51
+ throw new CliError("The applications response contained an invalid nextCursor", {
52
+ code: "INVALID_RESPONSE",
53
+ });
54
+ }
55
+
56
+ // Rebuild every item from approved metadata so unexpected API fields never reach output.
57
+ const apps = data.map((entry, index) => {
58
+ const appKey = objectString(entry, "appKey");
59
+ const name = objectString(entry, "name");
60
+ const status = objectString(entry, "status");
61
+ const createdAt = objectString(entry, "createdAt");
62
+ const updatedAt = objectString(entry, "updatedAt");
63
+
64
+ if (!appKey || !name || !status || !createdAt || !updatedAt) {
65
+ throw new CliError(
66
+ `Application at position ${index + 1} is missing appKey, name, status, createdAt, or updatedAt`,
67
+ { code: "INVALID_RESPONSE" },
68
+ );
69
+ }
70
+
71
+ return { appKey, name, status, createdAt, updatedAt };
72
+ });
73
+
74
+ return { data: apps, nextCursor };
75
+ }
76
+
77
+ /** @param {App[]} apps @returns {string[]} */
78
+ function formatTable(apps) {
79
+ const rows = [HEADERS, ...apps.map((app) =>
80
+ [app.appKey, app.name, app.status])];
81
+
82
+ const widths = HEADERS.map((_, column) =>
83
+ Math.max(...rows.map((row) => row[column].length)));
84
+
85
+ return rows.map((row) =>
86
+ row
87
+ .map((cell, column) => (column === HEADERS.length - 1 ? cell : cell.padEnd(widths[column])))
88
+ .join(" "),
89
+ );
90
+ }
@@ -0,0 +1,110 @@
1
+ // @ts-check
2
+
3
+ import { CliError } from "../errors.mjs";
4
+ import { objectString } from "../api-client.mjs";
5
+
6
+ /**
7
+ * @param {{ appKey: string }} options
8
+ * @param {{ api: { getBilling(accessToken: string, appKey: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
9
+ */
10
+ export async function showBilling(options, deps) {
11
+ const accessToken =
12
+ await deps.sessions.loadToken();
13
+
14
+ const result =
15
+ await deps.api.getBilling(accessToken, options.appKey);
16
+
17
+ const billing = parseBilling(result.body);
18
+
19
+ deps.log(JSON.stringify({ ok: true, data: billing }, null, 2));
20
+ }
21
+
22
+ /** @param {unknown} body */
23
+ function parseBilling(body) {
24
+ const ok = body && typeof body === "object" ?
25
+ Reflect.get(body, "ok") : undefined;
26
+
27
+ const data = objectValue(body, "data");
28
+
29
+ const plan = objectValue(data, "plan");
30
+
31
+ const usage = objectValue(data, "usage");
32
+
33
+ if (ok !== true || !data || !plan || !usage) {
34
+ throw invalidResponse("The billing response did not contain ok=true, data.plan, and data.usage");
35
+ }
36
+
37
+ const tier = objectString(plan, "tier");
38
+ const currentCycle = Reflect.get(plan, "current_cycle");
39
+ const billing = Reflect.get(plan, "billing");
40
+ const nextCharge = objectString(plan, "next_charge");
41
+ const monthlyQuota = Reflect.get(usage, "monthly_quota");
42
+ const currentUsage = Reflect.get(usage, "current_usage");
43
+ const overageCharge = Reflect.get(usage, "overage_charge");
44
+
45
+ if (!tier) throw invalidResponse("The billing response did not contain plan.tier");
46
+ if (
47
+ !Array.isArray(currentCycle)
48
+ || currentCycle.length !== 2
49
+ || !currentCycle.every((value) =>
50
+ typeof value === "string" && isIsoDateTime(value))
51
+ || Date.parse(currentCycle[0]) > Date.parse(currentCycle[1])
52
+ ) {
53
+ throw invalidResponse("The billing response contained an invalid plan.current_cycle");
54
+ }
55
+ if (!nonNegativeNumber(billing)) {
56
+ throw invalidResponse("The billing response contained an invalid plan.billing");
57
+ }
58
+ if (!nextCharge || !isIsoDateTime(nextCharge)) {
59
+ throw invalidResponse("The billing response contained an invalid plan.next_charge");
60
+ }
61
+ if (!nonNegativeInteger(monthlyQuota) || !nonNegativeInteger(currentUsage)) {
62
+ throw invalidResponse("The billing response contained invalid quota or usage values");
63
+ }
64
+ if (!nonNegativeNumber(overageCharge)) {
65
+ throw invalidResponse("The billing response contained an invalid usage.overage_charge");
66
+ }
67
+
68
+ // Rebuild nested output so unexpected response fields cannot reach the terminal.
69
+ return {
70
+ plan: {
71
+ tier,
72
+ current_cycle: currentCycle,
73
+ billing,
74
+ next_charge: nextCharge,
75
+ },
76
+ usage: {
77
+ monthly_quota: monthlyQuota,
78
+ current_usage: currentUsage,
79
+ overage_charge: overageCharge,
80
+ },
81
+ };
82
+ }
83
+
84
+ /** @param {unknown} value @param {string} property */
85
+ function objectValue(value, property) {
86
+ if (!value || typeof value !== "object") return undefined;
87
+ const found = Reflect.get(value, property);
88
+ return found && typeof found === "object" && !Array.isArray(found) ? found : undefined;
89
+ }
90
+
91
+ /** @param {unknown} value */
92
+ function nonNegativeNumber(value) {
93
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
94
+ }
95
+
96
+ /** @param {unknown} value */
97
+ function nonNegativeInteger(value) {
98
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
99
+ }
100
+
101
+ /** @param {string} value */
102
+ function isIsoDateTime(value) {
103
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?(?:Z|[+-]\d{2}:\d{2})?$/.test(value)
104
+ && Number.isFinite(Date.parse(value));
105
+ }
106
+
107
+ /** @param {string} message */
108
+ function invalidResponse(message) {
109
+ return new CliError(message, { code: "INVALID_RESPONSE" });
110
+ }
@@ -0,0 +1,81 @@
1
+ // @ts-check
2
+
3
+ import { CliError } from "../errors.mjs";
4
+ import { objectString } from "../api-client.mjs";
5
+
6
+ /**
7
+ * @param {{ appKey: string, from?: string, to?: string, granularity?: string }} options
8
+ * @param {{ api: { challengeVolume(accessToken: string, appKey: string, options: {from?: string, to?: string, granularity?: string}): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
9
+ */
10
+ export async function showChallengeVolume(options, deps) {
11
+ const accessToken = await deps.sessions.loadToken();
12
+ const filters = {
13
+ ...(options.from ? { from: options.from } : {}),
14
+ ...(options.to ? { to: options.to } : {}),
15
+ ...(options.granularity ? { granularity: options.granularity } : {}),
16
+ };
17
+ const result = await deps.api.challengeVolume(accessToken, options.appKey, filters);
18
+ const volume = parseChallengeVolume(result.body, options);
19
+ deps.log(JSON.stringify({ ok: true, data: volume }, null, 2));
20
+ }
21
+
22
+ /**
23
+ * @param {unknown} body
24
+ * @param {{ appKey: string, from?: string, to?: string, granularity?: string }} requested
25
+ */
26
+ function parseChallengeVolume(body, requested) {
27
+ const ok = body && typeof body === "object" ?
28
+ Reflect.get(body, "ok") : undefined;
29
+
30
+ const data = body && typeof body === "object" ?
31
+ Reflect.get(body, "data") : undefined;
32
+
33
+ if (ok !== true || !data || typeof data !== "object" || Array.isArray(data)) {
34
+ throw invalidResponse("The challenge volume response did not contain ok=true and data");
35
+ }
36
+
37
+ const appKey = objectString(data, "app_key");
38
+ const from = objectString(data, "from");
39
+ const to = objectString(data, "to");
40
+ const granularity = objectString(data, "granularity");
41
+ const challengeCount = Reflect.get(data, "challenge_count");
42
+
43
+ if (appKey !== requested.appKey) {
44
+ throw invalidResponse("The challenge volume response did not match the requested App Key");
45
+ }
46
+ if (!from || !to || !isIsoTime(from) || !isIsoTime(to) || Date.parse(from) > Date.parse(to)) {
47
+ throw invalidResponse("The challenge volume response contained an invalid time range");
48
+ }
49
+ if (requested.from && Date.parse(from) !== Date.parse(requested.from)) {
50
+ throw invalidResponse("The challenge volume response did not match the requested start time");
51
+ }
52
+ if (requested.to && Date.parse(to) !== Date.parse(requested.to)) {
53
+ throw invalidResponse("The challenge volume response did not match the requested end time");
54
+ }
55
+ if (!granularity || (requested.granularity && granularity !== requested.granularity)) {
56
+ throw invalidResponse("The challenge volume response contained an unexpected granularity");
57
+ }
58
+ if (!Number.isSafeInteger(challengeCount) || /** @type {number} */ (challengeCount) < 0) {
59
+ throw invalidResponse("The challenge volume response contained an invalid challenge_count");
60
+ }
61
+
62
+ // Rebuild the response so unexpected API fields cannot reach command output.
63
+ return {
64
+ app_key: appKey,
65
+ from,
66
+ to,
67
+ granularity,
68
+ challenge_count: challengeCount,
69
+ };
70
+ }
71
+
72
+ /** @param {string} value */
73
+ function isIsoTime(value) {
74
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})$/.test(value)
75
+ && Number.isFinite(Date.parse(value));
76
+ }
77
+
78
+ /** @param {string} message */
79
+ function invalidResponse(message) {
80
+ return new CliError(message, { code: "INVALID_RESPONSE" });
81
+ }
@@ -0,0 +1,106 @@
1
+ // @ts-check
2
+
3
+ import { CliError } from "../errors.mjs";
4
+ import { objectString } from "../api-client.mjs";
5
+
6
+ const SENSITIVE_FIELDS = new Set([
7
+ "access_token",
8
+ "accesstoken",
9
+ "api_secret",
10
+ "apisecret",
11
+ "secret_key",
12
+ "secretkey",
13
+ "session_token",
14
+ "sessiontoken",
15
+ ]);
16
+
17
+ /**
18
+ * @param {{ api: { integrationContract(accessToken: string): Promise<{body: unknown}> }, sessions: { loadToken(): Promise<string> }, log: (message: string) => void }} deps
19
+ */
20
+ export async function showIntegrationContract(deps) {
21
+ const accessToken =
22
+ await deps.sessions.loadToken();
23
+
24
+ const result =
25
+ await deps.api.integrationContract(accessToken);
26
+
27
+ const data = parseIntegrationContract(result.body);
28
+
29
+ deps.log(JSON.stringify({ ok: true, data }, null, 2));
30
+ }
31
+
32
+ /** @param {unknown} body @returns {Record<string, unknown>} */
33
+ function parseIntegrationContract(body) {
34
+ const ok = body && typeof body === "object" ?
35
+ Reflect.get(body, "ok") : undefined;
36
+
37
+ const data = objectValue(body, "data");
38
+ if (ok !== true || !data) {
39
+ throw invalidResponse("The integration contract response did not contain ok=true and data");
40
+ }
41
+
42
+ const browser = objectValue(data, "browser");
43
+ const backend = objectValue(data, "backend");
44
+ const minJdk = backend ?
45
+ Reflect.get(backend, "min_jdk") : undefined;
46
+
47
+ if (
48
+ !objectString(data, "contract_version")
49
+ || !browser
50
+ || !objectString(browser, "script_url")
51
+ || !objectString(browser, "token_header")
52
+ || !stringArray(browser, "available_modes")
53
+ || !backend
54
+ || !stringArray(backend, "supported_languages")
55
+ || !Number.isInteger(minJdk)
56
+ || /** @type {number} */ (minJdk) < 1
57
+ || !objectString(data, "release_state")
58
+ ) {
59
+ throw invalidResponse("The integration contract response is missing required fields");
60
+ }
61
+
62
+ // Preserve future contract fields, but never relay credential-shaped fields to output.
63
+ assertNoSensitiveFields(data);
64
+ return data;
65
+ }
66
+
67
+ /** @param {unknown} value @param {string} property */
68
+ function objectValue(value, property) {
69
+ if (!value || typeof value !== "object")
70
+ return undefined;
71
+
72
+ const found = Reflect.get(value, property);
73
+
74
+ return found && typeof found === "object" && !Array.isArray(found) ? found : undefined;
75
+ }
76
+
77
+ /** @param {unknown} value @param {string} property */
78
+ function stringArray(value, property) {
79
+ if (!value || typeof value !== "object")
80
+ return false;
81
+
82
+ const found = Reflect.get(value, property);
83
+
84
+ return Array.isArray(found) && found.every((item) => typeof item === "string" && item !== "");
85
+ }
86
+
87
+ /** @param {unknown} value */
88
+ function assertNoSensitiveFields(value) {
89
+ if (Array.isArray(value)) {
90
+ for (const item of value) assertNoSensitiveFields(item);
91
+ return;
92
+ }
93
+ if (!value || typeof value !== "object")
94
+ return;
95
+ for (const [key, nestedValue] of Object.entries(value)) {
96
+ if (SENSITIVE_FIELDS.has(key.toLowerCase())) {
97
+ throw invalidResponse("The integration contract response contained a sensitive field");
98
+ }
99
+ assertNoSensitiveFields(nestedValue);
100
+ }
101
+ }
102
+
103
+ /** @param {string} message */
104
+ function invalidResponse(message) {
105
+ return new CliError(message, { code: "INVALID_RESPONSE" });
106
+ }
@@ -0,0 +1,139 @@
1
+ // @ts-check
2
+
3
+ import readline from "node:readline/promises";
4
+ import { spawn } from "node:child_process";
5
+ import { stdin, stdout } from "node:process";
6
+ import { CliError } from "../errors.mjs";
7
+
8
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
9
+
10
+ /**
11
+ * @param {{ currentVersion: string, packageName: string, log: (message: string) => void, fetchImpl?: typeof fetch, confirm?: () => Promise<boolean>, install?: (packageName: string, version: string) => Promise<void> }} options
12
+ */
13
+ export async function checkForUpdate(options) {
14
+ const latestVersion = await fetchLatestVersion(
15
+ options.packageName,
16
+ options.fetchImpl ?? fetch,
17
+ );
18
+
19
+ options.log(`Current version: ${options.currentVersion}`);
20
+ options.log(`Latest version: ${latestVersion}`);
21
+
22
+ if (compareVersions(latestVersion, options.currentVersion) <= 0) {
23
+ options.log("RequestShield is already up to date.");
24
+ return;
25
+ }
26
+
27
+ const accepted = await (options.confirm ?? confirmUpdate)(latestVersion);
28
+ if (!accepted) {
29
+ options.log("Update cancelled.");
30
+ return;
31
+ }
32
+
33
+ options.log(`Updating RequestShield to ${latestVersion}...`);
34
+ await (options.install ?? installWithNpm)(options.packageName, latestVersion);
35
+ options.log(`RequestShield ${latestVersion} was installed successfully.`);
36
+ options.log("Open a new terminal before running RequestShield again.");
37
+ }
38
+
39
+ /** @param {string} packageName @param {typeof fetch} fetchImpl */
40
+ async function fetchLatestVersion(packageName, fetchImpl) {
41
+ const url = `${DEFAULT_REGISTRY}/${encodeURIComponent(packageName)}/latest`;
42
+ let response;
43
+ try {
44
+ response = await fetchImpl(url, { signal: AbortSignal.timeout(15_000) });
45
+ } catch (error) {
46
+ throw new CliError(`Could not check the latest RequestShield version: ${messageOf(error)}`, {
47
+ code: "UPDATE_CHECK_FAILED",
48
+ exitCode: 7,
49
+ });
50
+ }
51
+
52
+ if (!response.ok) {
53
+ throw new CliError(`Could not check the latest RequestShield version: HTTP ${response.status}`, {
54
+ code: "UPDATE_CHECK_FAILED",
55
+ exitCode: 7,
56
+ });
57
+ }
58
+
59
+ const body = await response.json();
60
+ const version = body && typeof body === "object" ? Reflect.get(body, "version") : undefined;
61
+ if (typeof version !== "string" || !parseVersion(version)) {
62
+ throw new CliError("The npm registry returned an invalid RequestShield version", {
63
+ code: "INVALID_UPDATE_RESPONSE",
64
+ });
65
+ }
66
+ return version;
67
+ }
68
+
69
+ /** @param {string} latestVersion */
70
+ async function confirmUpdate(latestVersion) {
71
+ if (!stdin.isTTY || !stdout.isTTY) {
72
+ throw new CliError(`RequestShield ${latestVersion} is available, but confirmation requires an interactive terminal`, {
73
+ code: "UPDATE_CONFIRMATION_REQUIRED",
74
+ exitCode: 2,
75
+ });
76
+ }
77
+ const prompt = readline.createInterface({ input: stdin, output: stdout });
78
+ try {
79
+ const answer = await prompt.question(`Update to RequestShield ${latestVersion}? (y/N): `);
80
+ return answer.trim().toLowerCase() === "y";
81
+ } finally {
82
+ prompt.close();
83
+ }
84
+ }
85
+
86
+ /** @param {string} packageName @param {string} version */
87
+ async function installWithNpm(packageName, version) {
88
+ await new Promise((resolve, reject) => {
89
+ const child = spawn("npm", ["install", "--global", `${packageName}@${version}`], {
90
+ shell: process.platform === "win32",
91
+ stdio: "inherit",
92
+ windowsHide: true,
93
+ });
94
+ child.once("error", (error) => {
95
+ reject(new CliError(`Could not start npm: ${error.message}`, {
96
+ code: "UPDATE_FAILED",
97
+ exitCode: 1,
98
+ }));
99
+ });
100
+ child.once("exit", (code) => {
101
+ if (code === 0) resolve(undefined);
102
+ else reject(new CliError(`npm update failed with exit code ${code ?? "unknown"}`, {
103
+ code: "UPDATE_FAILED",
104
+ exitCode: 1,
105
+ }));
106
+ });
107
+ });
108
+ }
109
+
110
+ /** @param {string} left @param {string} right */
111
+ function compareVersions(left, right) {
112
+ const a = parseVersion(left);
113
+ const b = parseVersion(right);
114
+ if (!a || !b) throw new CliError("RequestShield has an invalid semantic version");
115
+ for (let index = 0; index < 3; index++) {
116
+ if (a.numbers[index] !== b.numbers[index]) return a.numbers[index] - b.numbers[index];
117
+ }
118
+ if (a.prerelease === b.prerelease) return 0;
119
+ if (a.prerelease === undefined) return 1;
120
+ if (b.prerelease === undefined) return -1;
121
+ return a.prerelease.localeCompare(b.prerelease, "en", { numeric: true });
122
+ }
123
+
124
+ /** @param {string} value */
125
+ function parseVersion(value) {
126
+ const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
127
+ if (!match) return undefined;
128
+ return {
129
+ numbers: [Number(match[1]), Number(match[2]), Number(match[3])],
130
+ prerelease: match[4],
131
+ };
132
+ }
133
+
134
+ /** @param {unknown} error */
135
+ function messageOf(error) {
136
+ return error instanceof Error ? error.message : String(error);
137
+ }
138
+
139
+ export { compareVersions };
package/src/config.mjs ADDED
@@ -0,0 +1,8 @@
1
+ export const DEFAULT_MANAGEMENT_API_URL =
2
+ "https://api.intellifend.ai";
3
+
4
+ export const DEFAULT_MAVEN_REPOSITORY =
5
+ "https://sdk.intellifend.com/packages/maven";
6
+
7
+ export const DEFAULT_BROWSER_SCRIPT_URL =
8
+ "https://intellifend.ai/intellifend.js";