{"_id":"@bjnewman/joi-temporal","name":"@bjnewman/joi-temporal","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.1":{"name":"@bjnewman/joi-temporal","version":"1.0.1","description":"Joi extension for Temporal API types","author":{"name":"Ben Newman"},"type":"module","sideEffects":false,"main":"dist/index.js","types":"dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"}}},"scripts":{"build":"tsc","test":"c8 node --import temporal-polyfill/global --import tsx --test 'test/**/*.test.ts'","lint":"oxlint","type:check":"tsc --noEmit","prepublishOnly":"npm run build","prepare":"husky"},"keywords":["joi","temporal","validation","schema","date","time","duration","typescript"],"license":"MIT","publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/bjnewman/joi-temporal.git"},"peerDependencies":{"joi":">=17.0.0"},"lint-staged":{"*.{ts,js}":"oxlint"},"devDependencies":{"@semantic-release/changelog":"^6.0.3","@semantic-release/git":"^10.0.1","@tsconfig/node22":"^22.0.0","c8":"^10.1.0","husky":"^9.1.7","joi":"^17.13.0","lint-staged":"^16.2.7","oxlint":"^0.16.0","semantic-release":"^25.0.3","temporal-polyfill":"^0.2.5","tsx":"^4.19.0","typescript":"^5.9.0"},"gitHead":"fad242d2f9acd3b7883e1580a3fa09e5e99a0f47","_id":"@bjnewman/joi-temporal@1.0.1","bugs":{"url":"https://github.com/bjnewman/joi-temporal/issues"},"homepage":"https://github.com/bjnewman/joi-temporal#readme","_nodeVersion":"22.22.0","_npmVersion":"11.10.0","dist":{"integrity":"sha512-y3fpJoaMz8UXtHtO7yA207eEYyj5qEkDZjY1ngSDej6f4Ft6Bcz2dw0gsHOBADNUQhnbGXxN9vYho5Yp/DSQ0A==","shasum":"9d2def042be94e1c8baee0c9e12dedc31e503931","tarball":"https://registry.npmjs.org/@bjnewman/joi-temporal/-/joi-temporal-1.0.1.tgz","fileCount":4,"unpackedSize":18879,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCID98tCyxjwbPXtgT8MXFF3bzqRMu2v2b9LRsLA33VDFkAiEAx4VVHDAOPVsFnBXBboUwgM8LC5XlzbipmY/1H3FJ6R4="}]},"_npmUser":{"name":"bjnewman85","email":"benjaminjnewman@gmail.com"},"directories":{},"maintainers":[{"name":"bjnewman85","email":"benjaminjnewman@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/joi-temporal_1.0.1_1770967299061_0.2065242129920788"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-13T07:21:38.915Z","1.0.1":"2026-02-13T07:21:39.213Z","modified":"2026-02-13T07:21:39.514Z"},"maintainers":[{"name":"bjnewman85","email":"benjaminjnewman@gmail.com"}],"description":"Joi extension for Temporal API types","homepage":"https://github.com/bjnewman/joi-temporal#readme","keywords":["joi","temporal","validation","schema","date","time","duration","typescript"],"repository":{"type":"git","url":"git+https://github.com/bjnewman/joi-temporal.git"},"author":{"name":"Ben Newman"},"bugs":{"url":"https://github.com/bjnewman/joi-temporal/issues"},"license":"MIT","readme":"# joi-temporal\n\nJoi extension for validating and coercing [Temporal API](https://tc39.es/proposal-temporal/docs/) types.\n\n[![npm version](https://img.shields.io/npm/v/@bjnewman/joi-temporal)](https://www.npmjs.com/package/@bjnewman/joi-temporal)\n[![CI](https://img.shields.io/github/actions/workflow/status/bjnewman/joi-temporal/ci.yml?branch=main&label=CI)](https://github.com/bjnewman/joi-temporal/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)\n\n```ts\nimport Joi from \"joi\";\nimport joiTemporal from \"@bjnewman/joi-temporal\";\n\nconst custom = Joi.extend(...joiTemporal);\n\nconst schema = custom.object({\n    date: custom.plainDate().min(\"2020-01-01\").max(\"2025-12-31\"),\n    time: custom.plainTime().min(\"09:00\").max(\"17:00\"),\n    duration: custom.duration().positive().max(\"PT8H\"),\n});\n\nconst { value } = schema.validate({\n    date: \"2024-03-15\",\n    time: \"14:30\",\n    duration: \"PT2H30M\",\n});\n\nvalue.date instanceof Temporal.PlainDate; // true\nvalue.time instanceof Temporal.PlainTime; // true\nvalue.duration instanceof Temporal.Duration; // true\n```\n\n## Why not @joi/date or joi-luxon?\n\nJoi's existing date extensions (`@joi/date`, `@reis/joi-luxon`, `joi-date-dayjs`) all coerce to either `Date`, Luxon `DateTime`, or Day.js objects — and each comes with trade-offs:\n\n| | @joi/date | @reis/joi-luxon | joi-temporal |\n|---|---|---|---|\n| **Coerces to** | `Date` | Luxon `DateTime` | Temporal types |\n| **Timezones** | `.utc()` only | Yes (via Luxon) | Native `ZonedDateTime` |\n| **Durations** | No | No | `Temporal.Duration` with `.positive()`, `.min()`, `.max()` |\n| **Distinct date vs time** | No — everything is `Date` | No — everything is `DateTime` | `PlainDate`, `PlainTime`, `PlainDateTime`, `Instant`, etc. |\n| **Runtime dep** | moment (format parsing) | Luxon | None — uses the platform |\n| **Future** | moment is deprecated | Luxon is maintained | Temporal is a [TC39 standard](https://tc39.es/proposal-temporal/docs/), shipping in Chrome 137+, Firefox 139+, Node.js 22+ |\n\nThe [Temporal API](https://tc39.es/proposal-temporal/docs/) is the JavaScript standard that replaces `Date`. It's already native in major browsers and Node.js, and production-grade polyfills like [`temporal-polyfill`](https://www.npmjs.com/package/temporal-polyfill) make it usable everywhere else today. Unlike library-specific types, Temporal objects are what the rest of the ecosystem is converging on.\n\n- **Strings in, Temporal objects out** — ISO 8601 strings from JSON payloads are coerced to real Temporal instances. No manual parsing.\n- **Feels native to Joi** — same `.min()`, `.max()`, `.required()`, `.messages()` chaining you already know.\n- **All 8 Temporal types** — the right type for each use case instead of stuffing everything into `Date`.\n- **Zero dependencies** — just your Joi peer dependency and a Temporal runtime.\n- **`\"now\"` comparators** — `.min(\"now\")` resolves at validation time, not schema construction time.\n- **Polyfill now, native later** — swap the polyfill for native Temporal support with zero code changes.\n\n## Install\n\n```bash\nnpm install @bjnewman/joi-temporal\n```\n\nYou'll also need the Temporal API available at runtime. Pick one:\n\n| Environment | How to enable |\n|---|---|\n| **Node.js 22+** | `node --harmony-temporal app.js` |\n| **Chrome 137+** / **Firefox 139+** | Ships natively |\n| **Everywhere else** | `npm install temporal-polyfill` and `import \"temporal-polyfill/global\"` at your entry point |\n\n**Peer dependency:** `joi >= 17.0.0`\n\n## Supported Types\n\n| Method | Temporal Type | Example Input |\n|--------|--------------|---------------|\n| `plainDate()` | `Temporal.PlainDate` | `\"2024-03-15\"` |\n| `plainTime()` | `Temporal.PlainTime` | `\"14:30:00\"` |\n| `plainDateTime()` | `Temporal.PlainDateTime` | `\"2024-03-15T14:30:00\"` |\n| `zonedDateTime()` | `Temporal.ZonedDateTime` | `\"2024-03-15T14:30:00-04:00[America/New_York]\"` |\n| `instant()` | `Temporal.Instant` | `\"2024-03-15T14:30:00Z\"` |\n| `duration()` | `Temporal.Duration` | `\"PT2H30M\"` |\n| `plainYearMonth()` | `Temporal.PlainYearMonth` | `\"2024-03\"` |\n| `plainMonthDay()` | `Temporal.PlainMonthDay` | `\"03-15\"` |\n\nAll types coerce from ISO 8601 strings and pass through existing Temporal instances.\n\n## API\n\n### Comparison Rules\n\nAvailable on all types except `plainMonthDay`:\n\n```ts\ncustom.plainDate().min(\"2020-01-01\")   // >= (inclusive)\ncustom.plainDate().max(\"2025-12-31\")   // <= (inclusive)\ncustom.plainDate().gt(\"2020-01-01\")    // >  (exclusive)\ncustom.plainDate().lt(\"2025-12-31\")    // <  (exclusive)\ncustom.plainDate().gte(\"2020-01-01\")   // alias for .min()\ncustom.plainDate().lte(\"2025-12-31\")   // alias for .max()\n```\n\nComparators accept ISO strings or Temporal instances. `plainDate`, `plainDateTime`, and `plainTime` also accept `\"now\"`.\n\n### Duration Rules\n\n```ts\ncustom.duration().positive()   // sign must be > 0\ncustom.duration().negative()   // sign must be < 0\ncustom.duration().nonzero()    // sign must not be 0\ncustom.duration().min(\"PT1H\")  // at least 1 hour\ncustom.duration().max(\"P1D\")   // at most 1 day\n```\n\n### ZonedDateTime Timezone\n\n```ts\ncustom.zonedDateTime().timezone(\"America/New_York\")\n```\n\n## Usage with Hapi\n\nISO strings in JSON payloads are coerced to Temporal objects before your handler runs:\n\n```ts\nimport Hapi from \"@hapi/hapi\";\nimport Joi from \"joi\";\nimport joiTemporal from \"@bjnewman/joi-temporal\";\n\nconst custom = Joi.extend(...joiTemporal);\n\nserver.route({\n    method: \"POST\",\n    path: \"/bookings\",\n    options: {\n        validate: {\n            payload: custom.object({\n                date: custom.plainDate().min(\"now\").max(\"2026-12-31\").required(),\n                startTime: custom.plainTime().min(\"09:00\").max(\"17:00\").required(),\n                duration: custom.duration().positive().min(\"PT30M\").max(\"PT4H\").required(),\n            }),\n        },\n    },\n    handler(request) {\n        const { date, startTime, duration } = request.payload;\n        const end = startTime.add(duration); // already Temporal objects\n        return { date: date.toString(), start: startTime.toString(), end: end.toString() };\n    },\n});\n```\n\n## Usage with React Hook Form\n\nWorks with [@hookform/resolvers](https://github.com/react-hook-form/resolvers) and HTML date/time inputs, which produce ISO strings that joi-temporal coerces automatically:\n\n```ts\nimport { useForm } from \"react-hook-form\";\nimport { joiResolver } from \"@hookform/resolvers/joi\";\nimport Joi from \"joi\";\nimport joiTemporal from \"@bjnewman/joi-temporal\";\n\nconst custom = Joi.extend(...joiTemporal);\n\nconst schema = custom.object({\n    startDate: custom.plainDate().min(\"now\").required()\n        .messages({ \"temporal.plainDate.min\": \"Date must be today or later\" }),\n    meetingTime: custom.plainTime().min(\"08:00\").max(\"18:00\").required()\n        .messages({ \"temporal.plainTime.min\": \"Must be during business hours\" }),\n});\n\nconst { register, handleSubmit } = useForm({ resolver: joiResolver(schema) });\n// <input type=\"date\" {...register(\"startDate\")} />\n// <input type=\"time\" {...register(\"meetingTime\")} />\n```\n\n## Error Messages\n\nEvery error code can be overridden with `.messages()`:\n\n| Error Code | Default Message |\n|------------|----------------|\n| `temporal.plainDate.base` | `\"must be a valid ISO 8601 date string or Temporal.PlainDate\"` |\n| `temporal.plainDate.min` | `\"must be on or after {#limit}\"` |\n| `temporal.plainDate.max` | `\"must be on or before {#limit}\"` |\n| `temporal.plainDate.gt` | `\"must be after {#limit}\"` |\n| `temporal.plainDate.lt` | `\"must be before {#limit}\"` |\n| `temporal.plainTime.base` | `\"must be a valid ISO 8601 time string or Temporal.PlainTime\"` |\n| `temporal.plainDateTime.base` | `\"must be a valid ISO 8601 date-time string or Temporal.PlainDateTime\"` |\n| `temporal.zonedDateTime.base` | `\"must be a valid ISO 8601 date-time string with timezone or Temporal.ZonedDateTime\"` |\n| `temporal.zonedDateTime.timezone` | `\"must be in timezone {#timezone}\"` |\n| `temporal.instant.base` | `\"must be a valid ISO 8601 string with offset or Temporal.Instant\"` |\n| `temporal.duration.base` | `\"must be a valid ISO 8601 duration string or Temporal.Duration\"` |\n| `temporal.duration.min` | `\"must be at least {#limit}\"` |\n| `temporal.duration.max` | `\"must be at most {#limit}\"` |\n| `temporal.duration.positive` | `\"must be a positive duration\"` |\n| `temporal.duration.negative` | `\"must be a negative duration\"` |\n| `temporal.duration.nonzero` | `\"must not be zero\"` |\n| `temporal.plainYearMonth.base` | `\"must be a valid ISO 8601 year-month string or Temporal.PlainYearMonth\"` |\n| `temporal.plainMonthDay.base` | `\"must be a valid ISO 8601 month-day string or Temporal.PlainMonthDay\"` |\n\nThe `{#limit}` token is replaced with the ISO string representation of the comparator.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-f9bc5ac1bf7934dc4793f77e2601ebc3"}