Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion .github/workflows/package-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,20 @@ jobs:
run: |
mkdir -p "$RUNNER_TEMP/npm-candidate"
npm pack --ignore-scripts --pack-destination "$RUNNER_TEMP/npm-candidate"
npm pack --ignore-scripts --workspace @rdlabo/workers-timezone --pack-destination "$RUNNER_TEMP/npm-candidate"

- name: Upload immutable package artifact
uses: actions/upload-artifact@v7
with:
name: ${{ steps.candidate.outputs.artifact }}
path: ${{ runner.temp }}/npm-candidate/*.tgz
path: ${{ runner.temp }}/npm-candidate/rdlabo-workers-hono-kit-*.tgz
if-no-files-found: error
retention-days: 7

- name: Upload timezone workspace artifact
uses: actions/upload-artifact@v7
with:
name: ${{ steps.candidate.outputs.artifact }}-timezone
path: ${{ runner.temp }}/npm-candidate/rdlabo-workers-timezone-*.tgz
if-no-files-found: error
retention-days: 7
25 changes: 24 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ jobs:

- name: Publish packages
run: |
if node -e "process.exit(require('./packages/timezone/package.json').private ? 0 : 1)"; then
echo "::error::Release is disabled while @rdlabo/workers-timezone is private and unpublished"
exit 1
fi
VERSION="${{ steps.tag_version.outputs.version }}"
IS_STABLE=$(echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' && echo true || echo false)

Expand Down Expand Up @@ -381,7 +385,11 @@ jobs:

publish-candidate:
needs: authorize-candidate
if: needs.authorize-candidate.outputs.allowed == 'true'
# Publishing remains explicitly disabled while the timezone workspace is private/unpublished.
# Candidate tarballs are still built and retained for review.
if: >-
needs.authorize-candidate.outputs.allowed == 'true' &&
vars.WORKSPACE_NPM_PUBLISH_ENABLED == 'true'
runs-on: ubuntu-latest
concurrency:
group: npm-${{ needs.authorize-candidate.outputs.dist-tag }}-${{ needs.authorize-candidate.outputs.head-sha }}
Expand Down Expand Up @@ -492,6 +500,21 @@ jobs:
expectedVersion !== process.env.VERSION
) {
core.setFailed('Trusted package identity changed after authorization; rerun the release request.');
return;
}
const { data: timezoneData } = await github.rest.repos.getContent({
owner,
repo,
path: 'packages/timezone/package.json',
ref: defaultBranch,
});
if (Array.isArray(timezoneData) || timezoneData.type !== 'file' || !timezoneData.content) {
core.setFailed('Trusted timezone package.json could not be read.');
return;
}
const timezonePackage = JSON.parse(Buffer.from(timezoneData.content, 'base64').toString('utf8'));
if (timezonePackage.private !== false) {
core.setFailed('Candidate publishing is disabled while @rdlabo/workers-timezone is private.');
}

- name: Setup Node.js
Expand Down
25 changes: 23 additions & 2 deletions .github/workflows/validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ concurrency:
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
timezone:
runs-on: ubuntu-latest
strategy:
matrix:
node: [20, 22]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: npm
- run: npm ci
- run: npm run format:check --workspace @rdlabo/workers-timezone
- run: npm run typecheck --workspace @rdlabo/workers-timezone
- run: npm run lint --workspace @rdlabo/workers-timezone
- run: npm test --workspace @rdlabo/workers-timezone
- run: npm run build --workspace @rdlabo/workers-timezone
- run: npm run test:package --workspace @rdlabo/workers-timezone

lint:
runs-on: ubuntu-latest
steps:
Expand All @@ -24,7 +43,7 @@ jobs:
node-version: 22
cache: npm
cache-dependency-path: '**/package-lock.json'
- run: npm install
- run: npm ci
- name: Typecheck
run: npm run typecheck
- name: Lint
Expand All @@ -41,10 +60,12 @@ jobs:
node-version: 22
cache: npm
cache-dependency-path: '**/package-lock.json'
- run: npm install
- run: npm ci
- name: Test
run: npm run test
env:
CI: true
- name: Build
run: npm run build
- name: Package consumer smoke
run: npm run test:package
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ Shared infrastructure toolkit for Hono + Cloudflare Workers APIs. Published to n
|---------|-------------|-------|
| `.` | `@rdlabo/workers-hono-kit` | Web-standard only (middleware, HTTP helpers, Firebase, AWS, Stripe, AI Gateway, KV cache) |
| `./db` | `@rdlabo/workers-hono-kit/db` | MySQL data layer (requires `mysql2` + `drizzle-orm` peers) |
| `./business-time` | `@rdlabo/workers-hono-kit/business-time` | JST 業務時刻(`toBusinessDateTime`, `normalizeBusinessDate`, `businessDateTimeInstant` 等) |
| `./business-time` | `@rdlabo/workers-hono-kit/business-time` | Deprecated compatibility re-export of `@rdlabo/workers-timezone` |

The repository is an npm workspace. `packages/timezone` is the canonical implementation published
as `@rdlabo/workers-timezone`; the legacy `./business-time` subpath must remain a thin re-export.
| `./offline` | `@rdlabo/workers-hono-kit/offline` | テーブル非依存のREST/DB method converter・replica wire・clock helpers |
| `./testing` | `@rdlabo/workers-hono-kit/testing` | Test helpers (requires `mysql2` + `drizzle-orm` peers) |

Expand Down
34 changes: 21 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,23 @@ Stripe is a direct dependency of the kit. The package is compiled ESM with decla
| ---------------------------------------- | ------------------------------------------------------------------------------ |
| `@rdlabo/workers-hono-kit` | HTTP, auth, errors, Firebase, AWS, AI, Stripe, KV, queues, realtime primitives |
| `@rdlabo/workers-hono-kit/db` | Hyperdrive, MySQL, Drizzle, migrations, JST columns |
| `@rdlabo/workers-hono-kit/business-time` | JST business dates and date-times |
| `@rdlabo/workers-timezone` | IANA-timezone-aware dates and date-times (Asia/Tokyo by default) |
| `@rdlabo/workers-hono-kit/business-time` | Deprecated compatibility re-export of `@rdlabo/workers-timezone` |
| `@rdlabo/workers-hono-kit/offline` | Offline replica wire, cursor, journal, and compatibility contracts |
| `@rdlabo/workers-hono-kit/realtime` | Durable Object WebSocket and retry helpers |
| `@rdlabo/workers-hono-kit/testing` | Test databases, auth helpers, fakes, and Stripe fixtures |

Subpath imports keep optional database and testing dependencies out of the root runtime surface.

Set a deployment-wide timezone once when the Worker module starts. Helpers then use it whenever
their optional timezone argument is omitted:

```ts
import { TIME_ZONES, initializeTimezone } from '@rdlabo/workers-timezone';

initializeTimezone({ timeZone: TIME_ZONES.NEW_YORK });
```

## Documentation

- [HTTP and Authentication](https://docs.rdlabo.dev/projects/workers-hono-kit/docs/http-auth)
Expand All @@ -43,23 +53,20 @@ Subpath imports keep optional database and testing dependencies out of the root
- [API Reference](https://docs.rdlabo.dev/projects/workers-hono-kit/docs/api)

<!-- rdlabo-docs-omit -->

**Full documentation:** [https://docs.rdlabo.dev/projects/workers-hono-kit](https://docs.rdlabo.dev/projects/workers-hono-kit)

## Prerelease channels

An open, non-draft pull request can be published to the npm `beta` dist-tag after its `Validation` and `Package Candidate` workflows pass. A repository owner or maintainer must add a comment whose entire body is:
While `@rdlabo/workers-timezone` is private and unpublished, npm publication of both workspace
packages is disabled. Pull requests and merges still produce separate immutable candidate artifacts
for `@rdlabo/workers-hono-kit` and `@rdlabo/workers-timezone`, but `/beta`, automatic beta, `next`,
and stable publication are blocked.

```text
/beta
```

The request authorizes only the pull request head SHA that existed when the comment was added. The workflow revalidates the owner or maintainer permission and head SHA immediately before publishing. Any new commit requires CI to pass again and a fresh owner or maintainer `/beta` comment. Fork pull requests are supported. Pull requests that change a release-gating workflow cannot be beta-published until those workflow changes land on `main`.

Beta versions use `<base>-beta.pr<PR number>.sha<12-character SHA>`. The candidate is built in a read-only workflow without npm publishing credentials. The privileged release workflow publishes only the validated immutable package artifact with lifecycle scripts disabled. A notification failure cannot invalidate a successful npm publish.

When a pull request is merged into `main`, it is automatically published to `beta` only after the required CI and `Package Candidate` succeed for that exact merge commit. Direct pushes to `main` do not publish a candidate.

Only `npm run release` creates a release tag. Stable `vX.Y.Z` tags publish to npm `latest`; revision/prerelease tags publish to `next`. Neither `beta` nor `next` publishing changes the npm `latest` dist-tag.
Publication may be enabled only after the timezone package becomes public, versions and dependency
ranges are synchronized, and release automation publishes timezone before hono-kit. Candidate
publication additionally requires the repository variable `WORKSPACE_NPM_PUBLISH_ENABLED=true` and
revalidates `packages/timezone/package.json` as non-private immediately before publishing.

## Maintainers

Expand All @@ -68,4 +75,5 @@ Only `npm run release` creates a release tag. Stable `vX.Y.Z` tags publish to np
## License

[MIT](./LICENSE) © rdlabo-dev

<!-- /rdlabo-docs-omit -->
68 changes: 38 additions & 30 deletions docs/api-business-time.md
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
# API: `@rdlabo/workers-hono-kit/business-time`

String-level JST business-time conversions (Workers UTC instant ↔ business calendar date / date-time), with **no `mysql2` / `drizzle-orm` dependency**. This is a different layer from the `./db` column helpers (which handle the MySQL wire format): the DB stays on JST, and the app handles JST explicitly through this module instead of relying implicitly on the connection `timezone`.

| Export | Description |
| --- | --- |
| `today(ref?)` | The JST business calendar date (`YYYY-MM-DD`) of `ref` (defaults to now). |
| `toBusinessDate(instant)` | UTC instant → JST business calendar date (`YYYY-MM-DD`). |
| `normalizeBusinessDate(value)` | Normalize a `string` / `Date` / nullish to `YYYY-MM-DD`; a `YYYY-MM-DD` string passes through unchanged, nullish/empty/invalid → `null`. |
| `toBusinessDateTime(instant)` | UTC instant → JST business date-time (`YYYY-MM-DD HH:mm:ss`). |
| `parseBusinessDateTime(value)` | JST business date-time string → UTC instant (accepts a space or `T` separator). |
| `formatBusinessDateTime(instant, pattern?)` | Format an instant in the business TZ (Nest `helper.formatDate`-compatible tokens). |
| `startOfBusinessDay(date)` / `endOfBusinessDay(date)` | UTC instant of `00:00:00` / `23:59:59` on a JST business date. |
| `businessDateTimeInstant(date, time)` | JST business date + wall-clock time → UTC instant. |
| `addBusinessDays(date, days)` | Add calendar days to a JST business date. |
| `ageOnBusinessDate(birthDate, asOfDate?)` | Full years of age on a business date (`asOfDate` defaults to `today()`). |
| `DEFAULT_BUSINESS_DATETIME_PATTERN` | Default `formatBusinessDateTime` pattern (`YYYY-MM-DDThh:mm:ss`). |
| `BUSINESS_TIMEZONE` / `BusinessDate` / `BusinessDateTime` | JST timezone constant and the business-date / date-time string types. |
# API: `@rdlabo/workers-timezone`

Timezone-aware calendar and wall-clock conversion for Cloudflare Workers, with no database or Node
runtime dependency. The uninitialized default remains `Asia/Tokyo` for compatibility.

```ts
import { TIME_ZONES, initializeTimezone, toLocalDateTime } from '@rdlabo/workers-timezone';

initializeTimezone({ timeZone: TIME_ZONES.NEW_YORK });
toLocalDateTime(new Date('2026-07-01T13:00:00Z')); // '2026-07-01 09:00:00'
```

Initialize once during module evaluation using deployment-wide static configuration. Do not mutate
the default per request, user, or tenant. Every conversion function accepts an explicit IANA
timezone override without changing the module-instance default.

| Export | Description |
| ----------------------------------------------- | -------------------------------------------------------------------------------- |
| `initializeTimezone(config)` | Set the module-instance default once; repeated identical initialization is safe. |
| `getTimezoneConfig()` | Return the active configuration. |
| `toLocalDate(instant, timeZone?)` | Instant to local `YYYY-MM-DD`. |
| `toLocalDateTime(instant, timeZone?)` | Instant to local `YYYY-MM-DD HH:mm:ss`. |
| `localDateTimeToInstant(date, time, timeZone?)` | Local calendar date and wall clock to an instant. |
| `startOfDay` / `endOfDay` | First or final whole second of a local calendar day. |
| `addDays(date, days)` | Add calendar days without assuming a 24-hour day. |
| `TIME_ZONES` / `TimeZone` | Common typed constants and the open IANA timezone type. |

IANA rules determine daylight-saving and historical offsets. A skipped local clock throws
`RangeError`; when a clock occurs twice during a DST overlap, the earlier instant is selected.

## Legacy compatibility

`@rdlabo/workers-hono-kit/business-time` is deprecated and re-exports the same module instance from
`@rdlabo/workers-timezone`. Existing names such as `today`, `normalizeBusinessDate`,
`toBusinessDateTime`, `businessDateTimeInstant`, `formatBusinessDateTime`, and
`ageOnBusinessDate` remain available during migration.

```ts
import {
toBusinessDate,
toBusinessDateTime,
formatBusinessDateTime,
addBusinessDays,
} from '@rdlabo/workers-hono-kit/business-time';

const now = new Date('2026-07-05T21:00:00Z');
toBusinessDate(now); // '2026-07-06' (JST)
toBusinessDateTime(now); // '2026-07-06 06:00:00'
formatBusinessDateTime(now); // '2026-07-06T06:00:00'
addBusinessDays('2026-07-06', 3); // '2026-07-09'
// Deprecated; migrate the import path when practical.
import { toBusinessDateTime } from '@rdlabo/workers-hono-kit/business-time';
```
24 changes: 24 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 12 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
"access": "public"
},
"type": "module",
"workspaces": [
"packages/*"
],
"description": "Infrastructure toolkit for building APIs on Hono + Cloudflare Workers: Firebase ID-token verification (jose), AWS Secrets Manager (aws4fetch), ETag/validation/zod-coerce middleware, deadlock retry, and shared HTTP helpers.",
"author": "Masahiko Sakakibara<sakakibara@rdlabo.jp>",
"license": "MIT",
Expand Down Expand Up @@ -86,18 +89,20 @@
}
},
"scripts": {
"build": "node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
"build": "npm run build --workspace @rdlabo/workers-timezone && node scripts/clean-dist.mjs && tsc -p tsconfig.build.json",
"prepare": "npm run build",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"typecheck": "npm run typecheck --workspace @rdlabo/workers-timezone && tsc --noEmit",
"test": "npm test --workspace @rdlabo/workers-timezone && vitest run",
"test:watch": "vitest",
"lint": "eslint \"src/**/*.ts\"",
"lint:fix": "eslint \"src/**/*.ts\" --fix",
"format": "prettier --write \"src/**/*.ts\"",
"format:check": "prettier --check \"src/**/*.ts\"",
"lint": "npm run lint --workspace @rdlabo/workers-timezone && eslint \"src/**/*.ts\"",
"lint:fix": "npm run lint:fix --workspace @rdlabo/workers-timezone && eslint \"src/**/*.ts\" --fix",
"test:package": "node scripts/workspace-package-smoke.mjs",
"format": "prettier --write \"src/**/*.ts\" \"packages/**/*.ts\" \"packages/**/*.mjs\" \"packages/**/*.md\"",
"format:check": "prettier --check \"src/**/*.ts\" \"packages/**/*.ts\" \"packages/**/*.mjs\" \"packages/**/*.md\"",
"release": "np --no-tests --no-publish"
},
"dependencies": {
"@rdlabo/workers-timezone": "0.1.0",
"stripe": "^20.4.1"
},
"peerDependencies": {
Expand Down
3 changes: 3 additions & 0 deletions packages/timezone/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/
dist/
*.tgz
21 changes: 21 additions & 0 deletions packages/timezone/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 rdlabo-dev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading