From ffb3423848628c08e91a9a81b2f948e7ddf3bbcb Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Tue, 30 Jun 2026 16:35:16 -0500 Subject: [PATCH 1/5] Add `isNumber`/`number` validator --- src/json/index.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/json/index.ts b/src/json/index.ts index 8a1b60a178..9d40d41b0c 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -30,6 +30,11 @@ export function isString(value: unknown): value is string { return typeof value === "string"; } +/** Asserts that `value` is a number. */ +export function isNumber(value: unknown): value is number { + return typeof value === "number"; +} + /** Asserts that `value` is either a string or undefined. */ export function isStringOrUndefined( value: unknown, @@ -55,6 +60,12 @@ export const string = { required: true, } as const satisfies Validator; +/** A validator for number fields in schemas. */ +export const number = { + validate: isNumber, + required: true, +} as const satisfies Validator; + /** Transforms a validator to be optional. */ export function optional(validator: Validator) { return { From 4e30aa9c67a048e983bdd758b9ca8ae79cacef7a Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Tue, 30 Jun 2026 16:37:35 -0500 Subject: [PATCH 2/5] Add `object` validator --- src/json/index.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/json/index.ts b/src/json/index.ts index 9d40d41b0c..b121db7195 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -66,6 +66,12 @@ export const number = { required: true, } as const satisfies Validator; +/** A validator for object fields in schemas. */ +export const object = { + validate: isObject, + required: true, +} as const satisfies Validator>; + /** Transforms a validator to be optional. */ export function optional(validator: Validator) { return { From 9939a54a819182b6d5d0027986025a71fc3a89ca Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Tue, 30 Jun 2026 16:39:00 -0500 Subject: [PATCH 3/5] Change `optional` to include `undefined` The previous implementation of `optional` is now `optionalOrNull`. --- lib/entry-points.js | 32 ++++++++++++++++---------------- src/json/index.test.ts | 31 +++++++++++++++++++++++++++---- src/json/index.ts | 20 ++++++++++++++++++-- src/start-proxy/types.ts | 16 ++++++++-------- 4 files changed, 69 insertions(+), 30 deletions(-) diff --git a/lib/entry-points.js b/lib/entry-points.js index 93a4d0ed58..4e068da6b3 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -77636,14 +77636,14 @@ var require_reflection_json_writer = __commonJS({ /** * Returns `null` as the default for google.protobuf.NullValue. */ - enum(type, value, fieldName, optional2, emitDefaultValues, enumAsInteger) { + enum(type, value, fieldName, optional, emitDefaultValues, enumAsInteger) { if (type[0] == "google.protobuf.NullValue") - return !emitDefaultValues && !optional2 ? void 0 : null; + return !emitDefaultValues && !optional ? void 0 : null; if (value === void 0) { - assert_1.assert(optional2); + assert_1.assert(optional); return void 0; } - if (value === 0 && !emitDefaultValues && !optional2) + if (value === 0 && !emitDefaultValues && !optional) return void 0; assert_1.assert(typeof value == "number"); assert_1.assert(Number.isInteger(value)); @@ -77658,12 +77658,12 @@ var require_reflection_json_writer = __commonJS({ return options.emitDefaultValues ? null : void 0; return type.internalJsonWrite(value, options); } - scalar(type, value, fieldName, optional2, emitDefaultValues) { + scalar(type, value, fieldName, optional, emitDefaultValues) { if (value === void 0) { - assert_1.assert(optional2); + assert_1.assert(optional); return void 0; } - const ed = emitDefaultValues || optional2; + const ed = emitDefaultValues || optional; switch (type) { // int32, fixed32, uint32: JSON value will be a decimal number. Either numbers or strings are accepted. case reflection_info_1.ScalarType.INT32: @@ -144262,7 +144262,7 @@ var string = { validate: isString, required: true }; -function optional(validator) { +function optionalOrNull(validator) { return { validate: (val) => { return val === void 0 || val === null || validator.validate(val); @@ -161165,14 +161165,14 @@ var toolcache4 = __toESM(require_tool_cache()); // src/start-proxy/types.ts var usernameSchema = { /** The username needed to authenticate to the package registry, if any. */ - username: optional(string) + username: optionalOrNull(string) }; function hasUsername(config) { return "username" in config; } var usernamePasswordSchema = { /** The password needed to authenticate to the package registry, if any. */ - password: optional(string), + password: optionalOrNull(string), ...usernameSchema }; function hasUsernameAndPassword(config) { @@ -161180,7 +161180,7 @@ function hasUsernameAndPassword(config) { } var tokenSchema = { /** The token needed to authenticate to the package registry, if any. */ - token: optional(string), + token: optionalOrNull(string), ...usernameSchema }; function hasToken(config) { @@ -161202,15 +161202,15 @@ var awsConfigSchema = { "role-name": string, domain: string, "domain-owner": string, - audience: optional(string) + audience: optionalOrNull(string) }; function isAWSConfig(config) { return validateSchema(awsConfigSchema, config); } var jfrogConfigSchema = { "jfrog-oidc-provider-name": string, - audience: optional(string), - "identity-mapping-name": optional(string) + audience: optionalOrNull(string), + "identity-mapping-name": optionalOrNull(string) }; function isJFrogConfig(config) { return validateSchema(jfrogConfigSchema, config); @@ -161225,8 +161225,8 @@ function isCloudsmithConfig(config) { } var gcpConfigSchema = { "workload-identity-provider": string, - "service-account": optional(string), - audience: optional(string) + "service-account": optionalOrNull(string), + audience: optionalOrNull(string) }; function isGCPConfig(config) { return validateSchema(gcpConfigSchema, config); diff --git a/src/json/index.test.ts b/src/json/index.test.ts index 825bbc0e70..3e96d7639c 100644 --- a/src/json/index.test.ts +++ b/src/json/index.test.ts @@ -10,8 +10,8 @@ const testSchema = { requiredKey: json.string, }; -const optionalSchema = { - optionalKey: json.optional(json.string), +const optionalOrNullSchema = { + optionalKey: json.optionalOrNull(json.string), }; test("validateSchema - required properties are required", async (t) => { @@ -30,11 +30,34 @@ test("validateSchema - required properties are required", async (t) => { test("validateSchema - optional properties are optional", async (t) => { // Optional fields may be absent + t.true(json.validateSchema(optionalOrNullSchema, {})); + t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: undefined })); + t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: null })); + + // But, if present, should have the expected type + t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 0 })); + t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: 123 })); + t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: false })); + t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: true })); + t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: [] })); + t.false(json.validateSchema(optionalOrNullSchema, { optionalKey: {} })); + t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "" })); + t.true(json.validateSchema(optionalOrNullSchema, { optionalKey: "foo" })); +}); + +const optionalSchema = { + optionalKey: json.optional(json.string), +}; + +test("validateSchema - optional properties may be absent or undefined, but reject null", async (t) => { + // Optional fields may be absent or explicitly undefined t.true(json.validateSchema(optionalSchema, {})); t.true(json.validateSchema(optionalSchema, { optionalKey: undefined })); - t.true(json.validateSchema(optionalSchema, { optionalKey: null })); - // But, if present, should have the expected type + // But should reject null + t.false(json.validateSchema(optionalSchema, { optionalKey: null })); + + // And, if present, should have the expected type t.false(json.validateSchema(optionalSchema, { optionalKey: 0 })); t.false(json.validateSchema(optionalSchema, { optionalKey: 123 })); t.false(json.validateSchema(optionalSchema, { optionalKey: false })); diff --git a/src/json/index.ts b/src/json/index.ts index b121db7195..66adc606bd 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -72,8 +72,11 @@ export const object = { required: true, } as const satisfies Validator>; -/** Transforms a validator to be optional. */ -export function optional(validator: Validator) { +/** + * Transforms a validator to be optional, accepting `undefined` or `null` for an + * absent value. + */ +export function optionalOrNull(validator: Validator) { return { validate: (val: unknown) => { return val === undefined || val === null || validator.validate(val); @@ -82,6 +85,19 @@ export function optional(validator: Validator) { } as const satisfies Validator; } +/** + * Transforms a validator to be optional, accepting `undefined` for an absent + * value but, unlike `optionalOrNull`, rejecting `null`. + */ +export function optional(validator: Validator) { + return { + validate: (val: unknown): val is T | undefined => { + return val === undefined || validator.validate(val); + }, + required: false, + } as const satisfies Validator; +} + /** Represents an arbitrary object schema. */ export type Schema = Record>; diff --git a/src/start-proxy/types.ts b/src/start-proxy/types.ts index 13a4ce0e8f..13369edbfa 100644 --- a/src/start-proxy/types.ts +++ b/src/start-proxy/types.ts @@ -12,7 +12,7 @@ export type RawCredential = UnvalidatedObject; /** A schema for credential objects with a username. */ export const usernameSchema = { /** The username needed to authenticate to the package registry, if any. */ - username: json.optional(json.string), + username: json.optionalOrNull(json.string), } as const satisfies json.Schema; /** Usernames may be present for both authentication with tokens or passwords. */ @@ -29,7 +29,7 @@ export function hasUsername(config: AuthConfig): config is Username { /** A schema for credential objects with a username and password. */ export const usernamePasswordSchema = { /** The password needed to authenticate to the package registry, if any. */ - password: json.optional(json.string), + password: json.optionalOrNull(json.string), ...usernameSchema, } as const satisfies json.Schema; @@ -52,7 +52,7 @@ export function hasUsernameAndPassword( /** A schema for credential objects for token-based authentication. */ export const tokenSchema = { /** The token needed to authenticate to the package registry, if any. */ - token: json.optional(json.string), + token: json.optionalOrNull(json.string), ...usernameSchema, } as const satisfies json.Schema; @@ -100,7 +100,7 @@ export const awsConfigSchema = { "role-name": json.string, domain: json.string, "domain-owner": json.string, - audience: json.optional(json.string), + audience: json.optionalOrNull(json.string), } as const satisfies json.Schema; /** Configuration for AWS OIDC. */ @@ -116,8 +116,8 @@ export function isAWSConfig( /** A schema for JFrog OIDC configurations. */ export const jfrogConfigSchema = { "jfrog-oidc-provider-name": json.string, - audience: json.optional(json.string), - "identity-mapping-name": json.optional(json.string), + audience: json.optionalOrNull(json.string), + "identity-mapping-name": json.optionalOrNull(json.string), } as const satisfies json.Schema; /** Configuration for JFrog OIDC. */ @@ -150,8 +150,8 @@ export function isCloudsmithConfig( /** A schema for GCP OIDC configurations. */ export const gcpConfigSchema = { "workload-identity-provider": json.string, - "service-account": json.optional(json.string), - audience: json.optional(json.string), + "service-account": json.optionalOrNull(json.string), + audience: json.optionalOrNull(json.string), } as const satisfies json.Schema; /** Configuration for GCP OIDC. */ From 2a939e7934ea79d338764196cdcff01776dafe4c Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Tue, 30 Jun 2026 17:27:31 -0500 Subject: [PATCH 4/5] Update object validator to accept any type Using `unknown` meant that the validated object would not be indexable because its type would devolve to `{}` --- src/json/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/json/index.ts b/src/json/index.ts index 66adc606bd..2e6b346277 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -68,9 +68,9 @@ export const number = { /** A validator for object fields in schemas. */ export const object = { - validate: isObject, + validate: isObject, required: true, -} as const satisfies Validator>; +} as const satisfies Validator>; /** * Transforms a validator to be optional, accepting `undefined` or `null` for an From 46563f5d82ac29ffb775a497fa283312e0bebf89 Mon Sep 17 00:00:00 2001 From: Mario Campos Date: Tue, 30 Jun 2026 17:29:25 -0500 Subject: [PATCH 5/5] Restrict `isNumber` to finite numbers Because `NaN` and Infinity are not representable in JSON. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/json/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/json/index.ts b/src/json/index.ts index 2e6b346277..dc726acd04 100644 --- a/src/json/index.ts +++ b/src/json/index.ts @@ -30,9 +30,8 @@ export function isString(value: unknown): value is string { return typeof value === "string"; } -/** Asserts that `value` is a number. */ export function isNumber(value: unknown): value is number { - return typeof value === "number"; + return typeof value === "number" && Number.isFinite(value); } /** Asserts that `value` is either a string or undefined. */