@azure-net/node-kit 0.1.1 → 0.2.2

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/VALIDATION.md CHANGED
@@ -726,6 +726,7 @@ existence, ownership, authenticity, allocation, or reachability checks.
726
726
  | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
727
727
  | `email(o?)` | ASCII dot-atom local part up to 64 characters, dotted DNS domain, total up to 254; not all RFC 5322 mailboxes, no quoted local parts or deliverability check |
728
728
  | `uuid(o?)` | RFC 9562 versions 1-8; `UuidOptions.version` restricts version; unrestricted also accepts Nil/Max UUIDs; case-insensitive |
729
+ | `nonEmptyUuid(o?)` | Like `uuid`, but rejects `00000000-0000-0000-0000-000000000000`; accepts `UuidOptions`, including version and custom message. |
729
730
  | `guid(o?)` | Hyphenated `8-4-4-4-12` hex shape without UUID version/variant constraints |
730
731
  | `url(o?)` | Absolute WHATWG URL, including non-HTTP schemes; `UrlOptions.protocol`/`hostname` RegExp filters |
731
732
  | `httpUrl(o?)` | HTTP/HTTPS with explicit `://` and hostname; same URL filters; localhost/IPs can pass |
@@ -864,3 +865,62 @@ similar names do not guarantee interchangeable acceptance sets or error shapes.
864
865
  [type tests](tests/validation.types.ts), [result tests](tests/validation-result.test.mjs),
865
866
  [value tests](tests/validation-values.test.mjs), and
866
867
  [string-format tests](tests/validation-strings.test.mjs).
868
+
869
+ ## Parsing before validation
870
+
871
+ `parse(input, options?)` converts values using the built-in type descriptors, then
872
+ runs the ordinary validator and returns `ValidationResult<T>`. `parseAndThrow`
873
+ returns the validated `T` or throws `ValidationError`. Their asynchronous equivalents
874
+ are `parseAsync` and `parseAndThrowAsync`. Locale, context and cancellation options
875
+ are the same as for validation. Ordinary `validate` methods remain strict.
876
+
877
+ ```ts
878
+ class CollectionQueryValidator extends Validator<{ page: number; ids: number[] }> {
879
+ protected rules() {
880
+ return {
881
+ page: [rules.number(), rules.integer(), rules.positive()],
882
+ ids: [rules.array([rules.number(), rules.integer()])]
883
+ };
884
+ }
885
+ }
886
+
887
+ const query = CollectionQueryValidator.parseAndThrow({
888
+ page: '2',
889
+ ids: ['10', '20']
890
+ });
891
+ // { page: 2, ids: [10, 20] }
892
+ ```
893
+
894
+ The caller assembles the structure (for example using Hono `req.query('page')`
895
+ and `req.queries('ids')`). Parsing never interprets URLSearchParams, bracket keys,
896
+ JSON strings or comma-separated values and never wraps scalars in arrays.
897
+
898
+ - Finite decimal numeric strings (including exponent notation) become numbers.
899
+ Empty/whitespace strings, hexadecimal, Infinity and unsafe integer results are
900
+ left unchanged. Floating-point values retain JavaScript number precision.
901
+ - Boolean conversion accepts exactly `"true"` and `"false"`, not truthiness or 0/1.
902
+ - Bigint accepts integer decimal strings and safe integer numbers.
903
+ - String conversion accepts finite numbers, booleans and bigints, never objects.
904
+ - Date conversion accepts valid ISO date/date-time strings; omitted timezone means
905
+ UTC. Invalid calendar dates are not normalized into a different date.
906
+ - Homogeneous primitive enums/literals select their primitive type. Membership is
907
+ still checked during validation. Mixed enums are not coerced.
908
+ - A structural type rule is required, e.g. `number()` plus `integer()`. Constraints
909
+ like `positive()` or `integer()` alone do not identify an unambiguous input type.
910
+ - Object, array, tuple and record values are traversed by their schemas. Record keys
911
+ stay strings. Nested validators and lazy schema descriptions are supported.
912
+ - Map/Set inputs must already be Map/Set instances. Converted keys/elements that
913
+ would collapse distinct entries cause that collection to remain unchanged.
914
+ - Ordinary unions and overlapping intersection branches are not guessed. A
915
+ discriminated union can parse the branch selected by the existing discriminator.
916
+ - Null/undefined remain unchanged; missing fields are not filled with defaults.
917
+ - Failed conversions leave the original value for ordinary validation to reject.
918
+ Custom callbacks run only in validation, against the converted data.
919
+
920
+ The parser uses copy-on-write: unchanged branches may share references with input,
921
+ changed branches are copied without invoking getters. It is not a deep-cloning or
922
+ sanitizing API. Unknown keys remain subject to the ordinary unknown-key policy.
923
+ Depth/node limits bound parsing; if exceeded, the conversion is discarded and the
924
+ original input is validated. Cycles are not expanded indefinitely. Primitive
925
+ conversion strings are bounded to 4096 characters. Cached plans contain schemas
926
+ only, never request values or contexts.
@@ -16,6 +16,10 @@ export declare abstract class Validator<T extends object, TContext = unknown> {
16
16
  static validateAndThrow<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): T;
17
17
  static validateAsync<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): Promise<ValidationResult<T>>;
18
18
  static validateAndThrowAsync<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): Promise<T>;
19
+ static parse<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): ValidationResult<T>;
20
+ static parseAndThrow<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): T;
21
+ static parseAsync<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): Promise<ValidationResult<T>>;
22
+ static parseAndThrowAsync<T extends object, C = unknown>(this: ValidatorConstructor<T, C>, input: unknown, ...[options]: ValidationArguments<C>): Promise<T>;
19
23
  /** Both Validator.create(options) and new Validator.create(options) return a base class. */
20
24
  static readonly create: {
21
25
  (options?: ValidatorCreateOptions): typeof Validator;
@@ -1,3 +1,4 @@
1
+ import { parseInput } from "./parsing/Parse.js";
1
2
  import { validateAsync, validateSync } from "./Engine.js";
2
3
  import { ValidationError } from "./Result.js";
3
4
  import { validatorDefinition } from "./Symbols.js";
@@ -83,6 +84,24 @@ export class Validator {
83
84
  throw new ValidationError(result);
84
85
  return result.data;
85
86
  }
87
+ static parse(input, ...[options]) {
88
+ return validateSync(parseInput(input, Validator.schema(this), Validator.config(this)), Validator.schema(this), Validator.config(this), options ?? {});
89
+ }
90
+ static parseAndThrow(input, ...[options]) {
91
+ const result = validateSync(parseInput(input, Validator.schema(this), Validator.config(this)), Validator.schema(this), Validator.config(this), options ?? {});
92
+ if (!result.isValid)
93
+ throw new ValidationError(result);
94
+ return result.data;
95
+ }
96
+ static async parseAsync(input, ...[options]) {
97
+ return validateAsync(parseInput(input, Validator.schema(this), Validator.config(this)), Validator.schema(this), Validator.config(this), options ?? {});
98
+ }
99
+ static async parseAndThrowAsync(input, ...[options]) {
100
+ const result = await validateAsync(parseInput(input, Validator.schema(this), Validator.config(this)), Validator.schema(this), Validator.config(this), options ?? {});
101
+ if (!result.isValid)
102
+ throw new ValidationError(result);
103
+ return result.data;
104
+ }
86
105
  /** Both Validator.create(options) and new Validator.create(options) return a base class. */
87
106
  static create = function (options = {}) {
88
107
  const configuration = settings(options);
@@ -84,6 +84,7 @@ export const validationMessagesEn = Object.freeze({
84
84
  uppercase: "Must be uppercase.",
85
85
  email: "Must be a valid email address.",
86
86
  uuid: "Must be a valid UUID.",
87
+ nonEmptyUuid: "Must be a valid non-empty UUID.",
87
88
  guid: "Must be a valid GUID.",
88
89
  url: "Must be a valid absolute URL.",
89
90
  httpUrl: "Must be a valid HTTP or HTTPS URL.",
@@ -84,6 +84,7 @@ export const validationMessagesRu = Object.freeze({
84
84
  uppercase: "Строка должна быть в верхнем регистре.",
85
85
  email: "Некорректный адрес электронной почты.",
86
86
  uuid: "Некорректный UUID.",
87
+ nonEmptyUuid: "Требуется корректный ненулевой UUID.",
87
88
  guid: "Некорректный GUID.",
88
89
  url: "Некорректный абсолютный URL.",
89
90
  httpUrl: "Некорректный HTTP или HTTPS URL.",
@@ -0,0 +1,5 @@
1
+ import type { BuiltinRule } from "../Types.js";
2
+ export type Scalar = "number" | "string" | "boolean" | "bigint" | "date";
3
+ /** Uses immutable built-in identifiers, never options.code or message keys. */
4
+ export declare function scalarType(rule: BuiltinRule): Scalar | undefined;
5
+ export declare function convert(value: unknown, type: Scalar): unknown;
@@ -0,0 +1,64 @@
1
+ import { DateHelper } from "../../tools/DateHelper.js";
2
+ const numberCodes = new Set(["number", "finite", "int32", "uint32", "float32"]);
3
+ const bigintCodes = new Set(["bigint", "int64", "uint64"]);
4
+ /** Uses immutable built-in identifiers, never options.code or message keys. */
5
+ export function scalarType(rule) {
6
+ if (rule.kind !== "test" || !rule.structural)
7
+ return undefined;
8
+ if (numberCodes.has(rule.code))
9
+ return "number";
10
+ if (bigintCodes.has(rule.code))
11
+ return "bigint";
12
+ if (["string", "boolean", "date"].includes(rule.code))
13
+ return rule.code;
14
+ const values = rule.code === "enum"
15
+ ? rule.params.values
16
+ : rule.code === "literal"
17
+ ? [rule.params.expected]
18
+ : undefined;
19
+ if (!values?.length)
20
+ return undefined;
21
+ const type = typeof values[0];
22
+ return ["number", "string", "boolean", "bigint"].includes(type) &&
23
+ values.every((value) => typeof value === type)
24
+ ? type
25
+ : undefined;
26
+ }
27
+ const numeric = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
28
+ export function convert(value, type) {
29
+ if (value === null || value === undefined)
30
+ return value;
31
+ if (type === "string") {
32
+ return typeof value === "boolean" ||
33
+ typeof value === "bigint" ||
34
+ (typeof value === "number" && Number.isFinite(value))
35
+ ? String(value)
36
+ : value;
37
+ }
38
+ if (type === "bigint" && typeof value === "number")
39
+ return Number.isSafeInteger(value) ? BigInt(value) : value;
40
+ if (typeof value !== "string" || value.length > 4096)
41
+ return value;
42
+ if (type === "boolean")
43
+ return value === "true" ? true : value === "false" ? false : value;
44
+ if (type === "number") {
45
+ if (!numeric.test(value))
46
+ return value;
47
+ const number = Number(value);
48
+ return Number.isFinite(number) &&
49
+ (!Number.isInteger(number) || Number.isSafeInteger(number))
50
+ ? number
51
+ : value;
52
+ }
53
+ if (type === "bigint")
54
+ return /^[+-]?\d+$/.test(value) ? BigInt(value) : value;
55
+ if (type === "date") {
56
+ if (value !== value.trim() || DateHelper.compare(value, value) !== 0)
57
+ return value;
58
+ const iso = value.replace(" ", "T");
59
+ return new Date(iso.includes("T") && !/(?:Z|[+-]\d{2}:\d{2})$/.test(iso)
60
+ ? `${iso}Z`
61
+ : iso);
62
+ }
63
+ return value;
64
+ }
@@ -0,0 +1,5 @@
1
+ import type { AnyRule, ValidatorOptions } from "../Types.js";
2
+ type Shape = Readonly<Record<string, readonly AnyRule[]>>;
3
+ /** Per-call state only; on exhaustion validation sees the original input and enforces its own limits. */
4
+ export declare function parseInput(input: unknown, shape: Shape, config: ValidatorOptions): unknown;
5
+ export {};
@@ -0,0 +1,181 @@
1
+ import { types } from "node:util";
2
+ import { convert, scalarType } from "./Conversions.js";
3
+ const plans = new WeakMap();
4
+ function planFor(rules) {
5
+ let plan = plans.get(rules);
6
+ if (plan)
7
+ return plan;
8
+ const descriptors = rules.filter((rule) => typeof rule !== "function");
9
+ const scalars = new Set(descriptors.map(scalarType).filter((type) => type !== undefined));
10
+ const children = descriptors.filter((rule) => !["test", "optional", "nullable"].includes(rule.kind));
11
+ plan = {
12
+ scalar: scalars.size === 1 ? [...scalars][0] : undefined,
13
+ children,
14
+ ambiguous: scalars.size > 1 || (scalars.size > 0 && children.length > 0),
15
+ };
16
+ plans.set(rules, plan);
17
+ return plan;
18
+ }
19
+ function record(value) {
20
+ if (!value || typeof value !== "object" || types.isProxy(value))
21
+ return false;
22
+ const prototype = Object.getPrototypeOf(value);
23
+ return prototype === null || prototype === Object.prototype;
24
+ }
25
+ /** Per-call state only; on exhaustion validation sees the original input and enforces its own limits. */
26
+ export function parseInput(input, shape, config) {
27
+ const maxDepth = config.maxDepth ?? 64;
28
+ const maxNodes = config.maxNodes ?? 10000;
29
+ let nodes = 0;
30
+ const exhausted = Symbol();
31
+ const active = new WeakSet();
32
+ function budget(depth) {
33
+ if (++nodes > maxNodes || depth > maxDepth)
34
+ throw exhausted;
35
+ }
36
+ function properties(value, select, depth) {
37
+ let copy;
38
+ for (const key of Object.keys(value)) {
39
+ const property = Object.getOwnPropertyDescriptor(value, key);
40
+ const rules = select(key);
41
+ if (!("value" in property) || !rules) {
42
+ budget(depth + 1);
43
+ continue;
44
+ }
45
+ const next = visit(property.value, rules, depth + 1);
46
+ if (Object.is(next, property.value))
47
+ continue;
48
+ if (!copy) {
49
+ // Preserve unknown fields, accessors and symbols without executing getters.
50
+ copy = Array.isArray(value)
51
+ ? []
52
+ : Object.create(Object.getPrototypeOf(value));
53
+ for (const ownKey of Reflect.ownKeys(value)) {
54
+ const descriptor = Object.getOwnPropertyDescriptor(value, ownKey);
55
+ Object.defineProperty(copy, ownKey, {
56
+ ...descriptor,
57
+ ...(ownKey === "length" && Array.isArray(value)
58
+ ? { writable: true }
59
+ : { configurable: true }),
60
+ });
61
+ }
62
+ }
63
+ Object.defineProperty(copy, key, {
64
+ ...property,
65
+ value: next,
66
+ configurable: true,
67
+ });
68
+ }
69
+ return copy ?? value;
70
+ }
71
+ function object(value, fields, depth) {
72
+ return record(value)
73
+ ? properties(value, (key) => (Object.hasOwn(fields, key) ? fields[key] : undefined), depth)
74
+ : value;
75
+ }
76
+ function child(value, rule, depth) {
77
+ switch (rule.kind) {
78
+ case "object":
79
+ return object(value, rule.shape, depth);
80
+ case "array":
81
+ case "tuple":
82
+ return Array.isArray(value)
83
+ ? properties(value, (key) => /^(0|[1-9]\d*)$/.test(key)
84
+ ? rule.kind === "array"
85
+ ? rule.items
86
+ : rule.tuple[Number(key)]
87
+ : undefined, depth)
88
+ : value;
89
+ case "record":
90
+ return record(value)
91
+ ? properties(value, () => rule.items, depth)
92
+ : value;
93
+ case "lazy":
94
+ case "validator":
95
+ return visit(value, rule.resolve(), depth + 1, true);
96
+ case "union": {
97
+ // No branch guessing or user predicates during parsing.
98
+ if (!rule.discriminator || !record(value))
99
+ return value;
100
+ const prop = Object.getOwnPropertyDescriptor(value, rule.discriminator);
101
+ const key = prop && "value" in prop ? prop.value : undefined;
102
+ if (!["string", "number", "boolean"].includes(typeof key))
103
+ return value;
104
+ const rules = Object.hasOwn(rule.variants, String(key))
105
+ ? rule.variants[String(key)]
106
+ : undefined;
107
+ return rules ? visit(value, rules, depth + 1, true) : value;
108
+ }
109
+ // Intersections may specify incompatible conversions. Let validation decide.
110
+ case "intersection":
111
+ return value;
112
+ case "map": {
113
+ if (!(value instanceof Map))
114
+ return value;
115
+ const entries = [];
116
+ const keys = new Set();
117
+ let changed = false;
118
+ for (const [key, item] of Map.prototype.entries.call(value)) {
119
+ const nextKey = visit(key, rule.keys, depth + 1);
120
+ const nextItem = visit(item, rule.items, depth + 1);
121
+ if (keys.has(nextKey))
122
+ return value;
123
+ keys.add(nextKey);
124
+ changed ||= !Object.is(key, nextKey) || !Object.is(item, nextItem);
125
+ entries.push([nextKey, nextItem]);
126
+ }
127
+ return changed ? new Map(entries) : value;
128
+ }
129
+ case "set": {
130
+ if (!(value instanceof Set))
131
+ return value;
132
+ const items = new Set();
133
+ let changed = false;
134
+ for (const item of Set.prototype.values.call(value)) {
135
+ const next = visit(item, rule.items, depth + 1);
136
+ if (items.has(next))
137
+ return value;
138
+ items.add(next);
139
+ changed ||= !Object.is(item, next);
140
+ }
141
+ return changed ? items : value;
142
+ }
143
+ default:
144
+ return value;
145
+ }
146
+ }
147
+ function visit(value, rules, depth, sameNode = false) {
148
+ budget(depth);
149
+ if (value === null || value === undefined || types.isProxy(value))
150
+ return value;
151
+ const plan = planFor(rules);
152
+ if (plan.ambiguous)
153
+ return value;
154
+ if (plan.scalar)
155
+ return convert(value, plan.scalar);
156
+ const tracked = typeof value === "object";
157
+ if (tracked && !sameNode && active.has(value))
158
+ return value;
159
+ if (tracked && !sameNode)
160
+ active.add(value);
161
+ try {
162
+ let result = value;
163
+ for (const rule of plan.children)
164
+ result = child(result, rule, depth);
165
+ return result;
166
+ }
167
+ finally {
168
+ if (tracked && !sameNode)
169
+ active.delete(value);
170
+ }
171
+ }
172
+ try {
173
+ budget(0);
174
+ return object(input, shape, 0);
175
+ }
176
+ catch (error) {
177
+ if (error === exhausted)
178
+ return input;
179
+ throw error;
180
+ }
181
+ }
@@ -30,11 +30,6 @@ export interface IsoDateTimeOptions extends IsoTimeOptions {
30
30
  readonly local?: boolean;
31
31
  }
32
32
  export type HashAlgorithm = "md5" | "sha1" | "sha256" | "sha384" | "sha512";
33
- /**
34
- * Noncoercive format checks. Format scope was reviewed against Zod 4.6.7's
35
- * src/v4/core/{regexes,schemas}.ts (MIT, Colin McDonnell and contributors).
36
- * Implementations below are independent; no Zod runtime dependency is required.
37
- */
38
33
  export declare const stringRules: {
39
34
  string(options?: RuleOptions): BuiltinRule<string>;
40
35
  /** Strings count Unicode code points by default, arrays count elements, maps/sets count entries. */
@@ -52,6 +47,8 @@ export declare const stringRules: {
52
47
  email(options?: RuleOptions): BuiltinRule<string>;
53
48
  /** RFC 9562 versions 1-8 and, without a version filter, Nil/Max UUIDs. */
54
49
  uuid(options?: UuidOptions): BuiltinRule<string>;
50
+ /** A valid UUID excluding the all-zero Nil UUID. */
51
+ nonEmptyUuid(options?: UuidOptions): BuiltinRule<string>;
55
52
  guid(options?: RuleOptions): BuiltinRule<string>;
56
53
  /** Absolute WHATWG URLs, including non-HTTP schemes; not a safe-URL/security policy. */
57
54
  url(options?: UrlOptions): BuiltinRule<string>;
@@ -174,6 +174,18 @@ let currencies;
174
174
  * src/v4/core/{regexes,schemas}.ts (MIT, Colin McDonnell and contributors).
175
175
  * Implementations below are independent; no Zod runtime dependency is required.
176
176
  */
177
+ function uuidRule(options, nonNil) {
178
+ checkOptions(options);
179
+ const version = options.version;
180
+ if (version !== undefined &&
181
+ (!Number.isInteger(version) || version < 1 || version > 8))
182
+ throw new RangeError("UUID version must be 1 through 8.");
183
+ const normal = full(`[0-9a-f]{8}-[0-9a-f]{4}-${version ?? "[1-8]"}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`, "i");
184
+ return format(nonNil ? "nonEmptyUuid" : "uuid", (value) => normal.test(value) ||
185
+ (version === undefined &&
186
+ ((!nonNil && value === "00000000-0000-0000-0000-000000000000") ||
187
+ value.toLowerCase() === "ffffffff-ffff-ffff-ffff-ffffffffffff")), options, { version: version ?? "any" });
188
+ }
177
189
  export const stringRules = {
178
190
  string(options = {}) {
179
191
  checkOptions(options);
@@ -241,16 +253,11 @@ export const stringRules = {
241
253
  },
242
254
  /** RFC 9562 versions 1-8 and, without a version filter, Nil/Max UUIDs. */
243
255
  uuid(options = {}) {
244
- checkOptions(options);
245
- const version = options.version;
246
- if (version !== undefined &&
247
- (!Number.isInteger(version) || version < 1 || version > 8))
248
- throw new RangeError("UUID version must be 1 through 8.");
249
- const normal = full(`[0-9a-f]{8}-[0-9a-f]{4}-${version ?? "[1-8]"}[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}`, "i");
250
- return format("uuid", (value) => normal.test(value) ||
251
- (version === undefined &&
252
- (value === "00000000-0000-0000-0000-000000000000" ||
253
- value.toLowerCase() === "ffffffff-ffff-ffff-ffff-ffffffffffff")), options, { version: version ?? "any" });
256
+ return uuidRule(options, false);
257
+ },
258
+ /** A valid UUID excluding the all-zero Nil UUID. */
259
+ nonEmptyUuid(options = {}) {
260
+ return uuidRule(options, true);
254
261
  },
255
262
  guid(options = {}) {
256
263
  return pattern("guid", "[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}", options, "i");
@@ -44,6 +44,7 @@ export declare const rules: Readonly<{
44
44
  uppercase(options?: RuleOptions): BuiltinRule<string>;
45
45
  email(options?: RuleOptions): BuiltinRule<string>;
46
46
  uuid(options?: import("./StringRules.js").UuidOptions): BuiltinRule<string>;
47
+ nonEmptyUuid(options?: import("./StringRules.js").UuidOptions): BuiltinRule<string>;
47
48
  guid(options?: RuleOptions): BuiltinRule<string>;
48
49
  url(options?: import("./StringRules.js").UrlOptions): BuiltinRule<string>;
49
50
  httpUrl(options?: import("./StringRules.js").UrlOptions): BuiltinRule<string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-net/node-kit",
3
- "version": "0.1.1",
3
+ "version": "0.2.2",
4
4
  "description": "Framework-independent Node.js toolkit for the Azure-net ecosystem",
5
5
  "type": "module",
6
6
  "license": "MIT",