diff --git a/ark/schema/roots/proto.ts b/ark/schema/roots/proto.ts index b9689c71a..20c13cc8c 100644 --- a/ark/schema/roots/proto.ts +++ b/ark/schema/roots/proto.ts @@ -4,6 +4,7 @@ import { domainOf, getBuiltinNameOfConstructor, hasKey, + isArray, objectKindDescriptions, objectKindOrDomainOf, throwParseError, @@ -144,12 +145,20 @@ export class ProtoNode extends InternalBasis { private readonly requiresInvalidDateCheck = this.proto === Date && !this.dateAllowsInvalid + // Array.isArray is realm-safe; `instanceof Array` rejects arrays from other + // windows/vm contexts (https://github.com/arktypeio/arktype/issues/1597). + private readonly isArrayProto = this.proto === Array + traverseAllows: TraverseAllows = this.requiresInvalidDateCheck ? data => data instanceof Date && data.toString() !== "Invalid Date" - : data => data instanceof this.proto + : this.isArrayProto ? data => isArray(data) + : data => data instanceof this.proto - compiledCondition = `data instanceof ${this.serializedConstructor}${this.requiresInvalidDateCheck ? ` && data.toString() !== "Invalid Date"` : ""}` + compiledCondition = + this.isArrayProto ? `Array.isArray(data)` : ( + `data instanceof ${this.serializedConstructor}${this.requiresInvalidDateCheck ? ` && data.toString() !== "Invalid Date"` : ""}` + ) compiledNegation = `!(${this.compiledCondition})` protected innerToJsonSchema(ctx: ToJsonSchema.Context): JsonSchema { diff --git a/ark/type/__tests__/arrays/array.test.ts b/ark/type/__tests__/arrays/array.test.ts index 3733f8487..460c3a050 100644 --- a/ark/type/__tests__/arrays/array.test.ts +++ b/ark/type/__tests__/arrays/array.test.ts @@ -1,5 +1,6 @@ import { attest, contextualize } from "@ark/attest" import { scope, type } from "arktype" +import { createContext, runInContext } from "node:vm" import { incompleteArrayTokenMessage } from "arktype/internal/parser/shift/operator/operator.ts" import { multipleVariadicMesage, @@ -281,4 +282,19 @@ value at [1] must be a number (was boolean)`) .snap(`value at [0] must be a string (was a number) value at [1] must be a string (was a number)`) }) + + it("accepts arrays from another realm", () => { + const foreign = runInContext("[1, 2, 3]", createContext()) + // Same value identity check would fail: foreign instanceof Array === false + attest(Array.isArray(foreign)).equals(true) + attest(foreign instanceof Array).equals(false) + + const T = type("number[]") + attest(T.allows(foreign)).equals(true) + // Don't use .equals on the array itself — foreign-realm arrays have a + // different constructor, which attest treats as unequal. + const result = T(foreign) + attest(result === foreign).equals(true) + attest([...(result as number[])]).equals([1, 2, 3]) + }) }) diff --git a/ark/util/objectKinds.ts b/ark/util/objectKinds.ts index c86c7f55a..73394465e 100644 --- a/ark/util/objectKinds.ts +++ b/ark/util/objectKinds.ts @@ -155,6 +155,10 @@ type instantiableObjectKind = { export const objectKindOf = ( data: data ): objectKindOf | undefined => { + // Realm-safe: cross-window/vm arrays fail `instanceof Array` but pass + // Array.isArray (https://github.com/arktypeio/arktype/issues/1597). + if (Array.isArray(data)) return "Array" as never + let prototype: Partial | null = Object.getPrototypeOf(data) while ( prototype?.constructor &&