@stacksjs/defaults 0.75.227 → 0.75.229
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/ai/skills/stacks-actions/SKILL.md +1 -1
- package/ai/skills/stacks-auth/ACCOUNT-WORKFLOWS.md +24 -7
- package/ai/skills/stacks-auth/SKILL.md +42 -8
- package/ai/skills/stacks-browser/SKILL.md +31 -8
- package/ai/skills/stacks-cli/SKILL.md +8 -0
- package/ai/skills/stacks-database/SKILL.md +4 -0
- package/ai/skills/stacks-datetime/SKILL.md +67 -3
- package/ai/skills/stacks-native/CATALOG.md +32 -32
- package/ai/skills/stacks-notifications/SKILL.md +16 -1
- package/ai/skills/stacks-payments/SKILL.md +120 -5
- package/ai/skills/stacks-router/SKILL.md +23 -1
- package/ai/skills/stacks-security/SKILL.md +44 -1
- package/ai/skills/stacks-socials/SKILL.md +55 -19
- package/ai/skills/stacks-storage/SKILL.md +14 -0
- package/ai/skills/stacks-testing/SKILL.md +95 -4
- package/app/Actions/Auth/SessionHandoffAction.ts +65 -0
- package/app/Actions/Auth/SocialCallbackAction.ts +104 -37
- package/app/Actions/Auth/SocialRedirectAction.ts +41 -6
- package/app/Actions/Auth/social-flow.ts +49 -0
- package/app/Middleware/Throttle.ts +4 -3
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/emails/password-reset.stx +16 -6
- package/routes/socials.ts +14 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-actions
|
|
3
|
-
description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the
|
|
3
|
+
description: Use when working with Stacks server actions - creating actions in app/Actions/, auto-generated API actions from the useApi model trait, the 672 default framework actions (auth, dashboard, commerce, content, deployment, jobs), action request/response handling, or action registration. Covers @stacksjs/actions and storage/framework/defaults/app/Actions/.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -127,18 +127,35 @@ Evidence: `tests/rbac-default-store.test.ts`, `rbac-guard-sync.test.ts`,
|
|
|
127
127
|
|
|
128
128
|
## Social identity linking and referrals
|
|
129
129
|
|
|
130
|
-
`resolveSocialSignIn(provider, identity, store?)` resolves durable
|
|
131
|
-
links first, then applies `config.auth.socials.matching`:
|
|
132
|
-
An email match
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
130
|
+
`resolveSocialSignIn(provider, identity, store?, { tx? })` resolves durable
|
|
131
|
+
provider-id links first, then applies `config.auth.socials.matching`:
|
|
132
|
+
link/create/refuse. An email match links only when the provider vouches for the
|
|
133
|
+
address (`emailVerified === true`, else `'unverified-provider-email'`) AND the
|
|
134
|
+
local account verified it (`users.email_verified_at`, else
|
|
135
|
+
`'unverified-local-email'`). Neither guard is configurable. A user created from
|
|
136
|
+
a provider-verified address starts with `email_verified_at` set (`createUser`
|
|
137
|
+
receives `emailVerified`). Every store method receives the `tx`, so lookup, user
|
|
138
|
+
and link commit together; a store may implement `isUserEmailVerified(userId, tx)`
|
|
139
|
+
and otherwise gets the default that reads `users.email_verified_at`
|
|
140
|
+
(`defaultSocialSignInStore()` is exported). Use SocialSignInRefusedError's
|
|
141
|
+
`reason` to distinguish refusal from infrastructure failure. `stacks-socials`
|
|
142
|
+
owns provider authorization, the flow state and callback exchange.
|
|
143
|
+
|
|
144
|
+
After resolving the user, a redirect flow hands the browser a one-use code, not
|
|
145
|
+
tokens: `createSessionHandoff(userId, { bindTo: 'cookie' | { challenge } })`
|
|
146
|
+
returns `{ code, cookie, expiresAt }` (60 seconds, single use, bound to the
|
|
147
|
+
HttpOnly cookie or to the SHA-256 hex of a native client's verifier), and
|
|
148
|
+
`redeemSessionHandoff(code, secret)` returns `{ userId, data } | null` once.
|
|
149
|
+
`readSessionHandoffCookie(header, code)` / `clearSessionHandoffCookie(code)` read
|
|
150
|
+
and clear the binding cookie. The default `POST /auth/handoff`
|
|
151
|
+
(SessionHandoffAction) redeems and mints the session; mint nothing before it.
|
|
136
152
|
|
|
137
153
|
Referrals expose createReferralCode(ownerId), attributeReferral(newUserId, input),
|
|
138
154
|
qualifyReferral(referredUserId) and referralSummary(ownerId), with optional
|
|
139
155
|
database injection. Normalize/attribute at registration and qualify at the
|
|
140
156
|
application's real conversion milestone. Attribution alone is not a paid reward.
|
|
141
157
|
|
|
142
|
-
Source: `src/socials.ts`, `referrals.ts`. Evidence:
|
|
158
|
+
Source: `src/socials.ts`, `session-handoff.ts`, `referrals.ts`. Evidence:
|
|
159
|
+
`tests/social-linking.test.ts`, `social-callback-flow.test.ts`,
|
|
143
160
|
`referrals.test.ts`. All account features remain subject to model loading,
|
|
144
161
|
selected route bundles, config gates and applied migrations.
|
|
@@ -66,8 +66,8 @@ declaring `useAuth` can hold tokens - not only `User`.
|
|
|
66
66
|
- `tokenCan(scope)` / `tokenCanAll` / `tokenCanAny` / `tokenAbilities` - check
|
|
67
67
|
the current request's token.
|
|
68
68
|
- `revokeToken`, `revokeTokenById`, `revokeAllTokens(id, type?)`,
|
|
69
|
-
`revokeOtherTokens(id, type?)` - revocation also revokes
|
|
70
|
-
token, which a raw row delete does not.
|
|
69
|
+
`revokeOtherTokens(id, type?, { exceptTokenId? })` - revocation also revokes
|
|
70
|
+
the paired refresh token, which a raw row delete does not.
|
|
71
71
|
- `setTrailActor(id)` - attribute writes in a queue job or CLI run that has no
|
|
72
72
|
request to read a user from.
|
|
73
73
|
|
|
@@ -106,7 +106,7 @@ routes: minting and revoking both carry semantics a generic route does not.
|
|
|
106
106
|
- `Auth.revokeToken(token: string): Promise<void>`
|
|
107
107
|
- `Auth.revokeTokenById(tokenId: number): Promise<void>`
|
|
108
108
|
- `Auth.revokeAllTokens(userId?: number): Promise<void>`
|
|
109
|
-
- `Auth.revokeOtherTokens(userId?: number): Promise<
|
|
109
|
+
- `Auth.revokeOtherTokens(userId?: number, { exceptTokenId? }): Promise<{ keptTokenId: number | null }>` - keeps `exceptTokenId`, else the request's own token; when neither resolves it revokes every token (fail closed)
|
|
110
110
|
- `Auth.pruneExpiredTokens(): Promise<number>`
|
|
111
111
|
- `Auth.pruneRevokedTokens(): Promise<number>`
|
|
112
112
|
- `Auth.rotateToken(oldToken: string): Promise<AuthToken | null>`
|
|
@@ -138,7 +138,7 @@ routes: minting and revoking both carry semantics a generic route does not.
|
|
|
138
138
|
- `revokeToken(plainTextToken): Promise<void>`
|
|
139
139
|
- `revokeTokenById(tokenId): Promise<void>`
|
|
140
140
|
- `revokeAllTokens(userId): Promise<void>`
|
|
141
|
-
- `revokeOtherTokens(userId): Promise<
|
|
141
|
+
- `revokeOtherTokens(userId, type?, { exceptTokenId? }): Promise<{ keptTokenId: number | null }>` - same rule as `Auth.revokeOtherTokens`
|
|
142
142
|
- `deleteExpiredTokens(): Promise<number>`
|
|
143
143
|
- `deleteRevokedTokens(daysOld?): Promise<number>`
|
|
144
144
|
|
|
@@ -318,10 +318,37 @@ Internal: database-backed `sessions` rows with expiry, optional IP/User-Agent fi
|
|
|
318
318
|
```typescript
|
|
319
319
|
const actions = passwordResets(email)
|
|
320
320
|
await actions.sendEmail()
|
|
321
|
+
// or: an invitation's first password, a custom expiry, your own delivery
|
|
322
|
+
await actions.sendEmail({
|
|
323
|
+
purpose: 'setup', // 'reset' (default) | 'setup'
|
|
324
|
+
expiresAt: new Date(Date.now() + 3 * 864e5),
|
|
325
|
+
deliver: async ({ purpose, userId, email, token, url, expiresAt }) => { /* localized mail, ledger */ },
|
|
326
|
+
})
|
|
321
327
|
const valid = await actions.verifyToken(token)
|
|
322
328
|
const result = await actions.resetPassword(token, newPassword)
|
|
323
329
|
```
|
|
324
330
|
|
|
331
|
+
`deliver` runs after the token commits, instead of the framework mail; the
|
|
332
|
+
`token`/`url` are the live credential, never log them. `sendEmail` stays a
|
|
333
|
+
silent no-op for unknown or throttled addresses (anti-enumeration).
|
|
334
|
+
|
|
335
|
+
### One credential sweep (password/credentials.ts)
|
|
336
|
+
|
|
337
|
+
- `revokeAccountCredentials(userId, { exceptTokenId?, exceptSessionId?, tx? })`
|
|
338
|
+
stamps `password_changed_at` and revokes two-factor challenges, magic links,
|
|
339
|
+
OAuth grants and codes, access/refresh tokens and sessions, in one transaction
|
|
340
|
+
(joins the caller's when `tx` is given; a `tx` outside its transaction
|
|
341
|
+
throws). The kept token and its refresh token are re-bound to the stamp.
|
|
342
|
+
Returns `{ passwordChangedAt, keptTokenId }`. `resetPassword` runs it.
|
|
343
|
+
- `changePassword(userId, current, next, { keepTokenId?, keepSessionId?, notify? })`
|
|
344
|
+
verifies `current`, commits only if that hash is still stored, runs the sweep,
|
|
345
|
+
then notifies after commit (`notify`: `true` = the `password-changed` mail, a
|
|
346
|
+
function = yours, `false` = nothing). Returns `{ success: true, keptTokenId }`
|
|
347
|
+
or `{ success: false, reason: 'invalid-current-password' | 'user-not-found' | 'invalid-new-password' }`.
|
|
348
|
+
Enforce the new password's policy first.
|
|
349
|
+
- Any other path that changes how an account proves who it is (recovery, an
|
|
350
|
+
admin forcing a sign-out, an email change) should call the sweep too.
|
|
351
|
+
|
|
325
352
|
## Registration (register.ts)
|
|
326
353
|
|
|
327
354
|
- `register(credentials: NewUser): Promise<{ token: AuthToken }>`
|
|
@@ -398,9 +425,12 @@ const locked = await RateLimiter.isRateLimited(email)
|
|
|
398
425
|
await RateLimiter.resetAttempts(email)
|
|
399
426
|
~~~
|
|
400
427
|
|
|
401
|
-
Five failed attempts
|
|
402
|
-
|
|
403
|
-
|
|
428
|
+
Five failed attempts (`maxFailedAttempts()`, times
|
|
429
|
+
`config.security.rateLimit.scale` / `STACKS_RATE_LIMIT_SCALE`) lock an email for
|
|
430
|
+
fifteen minutes. The configured store selects atomic Redis when the cache config
|
|
431
|
+
selects Redis, wrapped in `FallbackRateLimiterStore` so a Redis outage degrades
|
|
432
|
+
to process memory (warned once) instead of failing every sign-in; otherwise
|
|
433
|
+
process-local memory. `useSharedStore(options?)` explicitly chooses Redis;
|
|
404
434
|
`useStore(custom)` installs a store with atomic recordFailedAttempt;
|
|
405
435
|
`useMemoryStore()` and `useConfiguredStore()` reset that choice.
|
|
406
436
|
Shared deployments need a shared atomic store. Partial counters expire, and
|
|
@@ -528,6 +558,10 @@ is the declarative form and the one the ability-name completions come from.
|
|
|
528
558
|
- `DELETE /auth/tokens/{id}` → RevokeTokenAction (auth middleware)
|
|
529
559
|
- `GET /me` → GetMeAction (auth middleware)
|
|
530
560
|
- `POST /logout` → LogoutAction (auth middleware)
|
|
561
|
+
- Social bundle: `GET /auth/{provider}` → SocialRedirectAction (issues the
|
|
562
|
+
browser-bound OAuth state), `GET|POST /auth/{provider}/callback` →
|
|
563
|
+
SocialCallbackAction (redeems it, redirects with `#stx_handoff=<code>`),
|
|
564
|
+
`POST /auth/handoff` → SessionHandoffAction (redeems the code for the session)
|
|
531
565
|
|
|
532
566
|
## User Model Traits
|
|
533
567
|
|
|
@@ -545,7 +579,7 @@ traits: {
|
|
|
545
579
|
|
|
546
580
|
- Auth depends on `@stacksjs/ts-auth` for TOTP and passkey functions
|
|
547
581
|
- Password hashing defaults to bcrypt with 12 rounds (config/hashing.ts)
|
|
548
|
-
- Rate limiting uses a process-local memory store
|
|
582
|
+
- Rate limiting uses a process-local memory store unless `config/cache.ts` selects Redis, which the lockout and route limits then share. Production deployments with multiple workers should configure Redis or provide a custom atomic store. Test suites raise `config.security.rateLimit.scale` rather than disabling limits.
|
|
549
583
|
- Session auth is database-backed through the `sessions` table, so it survives server restarts and is shared across workers.
|
|
550
584
|
- New personal and delegated access tokens are opaque 40-byte hex bearers hashed at rest. Legacy `jwt:encryptedId` bearers remain readable during migration.
|
|
551
585
|
- Token validation hashes the bearer directly. Do not parse or expose token contents, and never log plaintext bearer values.
|
|
@@ -44,16 +44,38 @@ Browser state uses imported storage helpers, not direct document/window mutation
|
|
|
44
44
|
inside STX scripts.
|
|
45
45
|
|
|
46
46
|
refreshSession coalesces concurrent refresh attempts. A server refusal clears the
|
|
47
|
-
session and returns false; a
|
|
48
|
-
credential. authFetch retries the eligible request after
|
|
49
|
-
credential/CSRF flow. Cookie/session behavior and
|
|
50
|
-
depend on server browser-session policy. A
|
|
51
|
-
renewable by keeping an old client refresh
|
|
47
|
+
session and returns false; a network failure, 5xx, 408 or 429 keeps the existing
|
|
48
|
+
credential and returns false. authFetch retries the eligible request after
|
|
49
|
+
refresh using the native credential/CSRF flow. Cookie/session behavior and
|
|
50
|
+
refresh-token availability depend on server browser-session policy. A
|
|
51
|
+
fixed-lifetime login cannot be made renewable by keeping an old client refresh
|
|
52
|
+
token.
|
|
53
|
+
|
|
54
|
+
Only a 401 ends a session. fetchAuthUser shares one in-flight /api/me request
|
|
55
|
+
between concurrent callers. A network failure, 5xx, 403 or other non-401 answer
|
|
56
|
+
keeps token, user and isAuthenticated and resolves the last known user. 403 is
|
|
57
|
+
an authorization decision about a recognised identity, so the app should show
|
|
58
|
+
a forbidden state, not send the user to log in again. An unchanged /me answer
|
|
59
|
+
keeps the same user object, so effects do not re-run. A same-origin authFetch
|
|
60
|
+
401 that refresh cannot cure re-checks /api/me instead of signing out (signed
|
|
61
|
+
URLs and step-up checks also answer 401). Only that check's 401 clears the
|
|
62
|
+
session. A burst of such 401s makes one /me request, one clear and one
|
|
63
|
+
onSessionExpired(listener) call. Answers to requests sent before a new sign-in
|
|
64
|
+
are ignored. logout() does not fire onSessionExpired.
|
|
52
65
|
|
|
53
66
|
completeSocialLogin applies the native handoff through the same storage refs and
|
|
54
|
-
strips its URL fragment.
|
|
55
|
-
|
|
67
|
+
strips its URL fragment. The default callback sends a one-use stx_handoff code.
|
|
68
|
+
Called with no argument, completeSocialLogin redeems it with a credentialed POST
|
|
69
|
+
to /auth/handoff (AUTH_HANDOFF_PATH); a refused or unreachable redemption
|
|
70
|
+
resolves null and leaves the existing session alone. A stx_auth pack from a
|
|
71
|
+
custom callback still works. Use socialHandoffRedirect on the server; a
|
|
72
|
+
hand-built inline script writing token/user keys duplicates the
|
|
73
|
+
encoding/security contract.
|
|
74
|
+
|
|
56
75
|
authGuard controls client navigation; guard server pages/actions independently.
|
|
76
|
+
With a stored but unconfirmed token it checks /api/me instead of redirecting,
|
|
77
|
+
so a network blip on page load keeps the user on the page. A 401 then, or any
|
|
78
|
+
later session expiry on that page, redirects to /login once.
|
|
57
79
|
|
|
58
80
|
## API transports and errors
|
|
59
81
|
|
|
@@ -88,5 +110,6 @@ composable skill instead of copying another fixed inventory of globals.
|
|
|
88
110
|
|
|
89
111
|
`storage/framework/core/browser/src/index.ts`, auto-init.ts, model-loader.ts,
|
|
90
112
|
composables/useAuth.ts/useApi.ts/csrf.ts/request-error.ts and utils/fetch.ts.
|
|
91
|
-
Tests: client-bundle-safety.test.ts, use-auth-refresh.test.ts,
|
|
113
|
+
Tests: client-bundle-safety.test.ts, use-auth-refresh.test.ts (refresh, session
|
|
114
|
+
resilience, burst expiry, authGuard, handoff code), auth.test.ts, csrf.test.ts,
|
|
92
115
|
request-error.test.ts and api-url.test.ts under core/browser/tests.
|
|
@@ -129,6 +129,14 @@ inspection, append a newline, and complete before an immediate `process.exit()`.
|
|
|
129
129
|
The narrow import needs no application configuration. Use `log` for application
|
|
130
130
|
logging with levels, context and transports.
|
|
131
131
|
|
|
132
|
+
For large results or piped output, use `await printAsync(...args)` and
|
|
133
|
+
`await printErrorAsync(...args)` from the same entrypoint. Their signatures are
|
|
134
|
+
`(...args: unknown[]) => Promise<void>`. They append a newline, preserve Node
|
|
135
|
+
formatting, wait for stream writes under backpressure, and reject write errors
|
|
136
|
+
such as `EPIPE`. Await them before `process.exit()`. The synchronous helpers
|
|
137
|
+
remain for small, bounded immediate-exit messages and may throw `EAGAIN` when
|
|
138
|
+
a nonblocking pipe is full. These helpers are also exported by `@stacksjs/cli`.
|
|
139
|
+
|
|
132
140
|
## Output Formatting
|
|
133
141
|
|
|
134
142
|
Via `@stacksjs/utils` color functions (available in CLI context):
|
|
@@ -371,6 +371,10 @@ or dialect-aware raw helpers. sql.as validates an alias; raw text remains a
|
|
|
371
371
|
trusted-code operation. sqlDateTime/parseSqlDateTime provide the canonical
|
|
372
372
|
application timestamp representation. Mixing SQLite datetime text with
|
|
373
373
|
application ISO text in raw comparisons can give wrong expiry boundaries.
|
|
374
|
+
parseSqlDate(value) reads a DATE column back as the YYYY-MM-DD it stores,
|
|
375
|
+
whether the driver built the Date at UTC midnight (Bun's Postgres driver) or at
|
|
376
|
+
the host's local midnight; toISOString().slice(0, 10) shifts the latter a day
|
|
377
|
+
back east of UTC. Null for missing, invalid or impossible input.
|
|
374
378
|
Source: core/database/src/types.ts and sql-helpers.ts; see stacks-query-builder
|
|
375
379
|
for typed tables, raw binding, transaction handles and replica routing.
|
|
376
380
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: stacks-datetime
|
|
3
|
-
description: Use when working with dates and times in Stacks - the DateTime class with Carbon-like API (add/sub, comparison, formatting, start/end of day/month/year), date parsing, format tokens, or
|
|
3
|
+
description: Use when working with dates and times in Stacks - the DateTime class with Carbon-like API (add/sub, comparison, formatting, start/end of day/month/year), date parsing, format tokens, timezone handling, wall-clock times in a named zone across DST, or YYYY-MM-DD calendar dates (today in a zone, day arithmetic, ISO weeks). Covers @stacksjs/datetime.
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: Bun >= 1.3.0, TypeScript
|
|
6
6
|
allowed-tools: Read Edit Write Bash Grep Glob
|
|
@@ -24,10 +24,14 @@ export { parse } from './parse'
|
|
|
24
24
|
export { format as dateFormat } from './format' // convenience alias
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Source files:
|
|
28
28
|
- `now.ts` - `DateTime` class and `now()` helper
|
|
29
29
|
- `format.ts` - standalone `format()` function with token-based formatting and timezone support
|
|
30
30
|
- `parse.ts` - standalone `parse()` function with token-based parsing
|
|
31
|
+
- `zone.ts` - wall clock in a named zone to an instant and back (`zonedTimeToUtc`, `isoInZone`, `zoneOffsetMinutes`)
|
|
32
|
+
- `calendar.ts` - `addCalendarMonths` and the `YYYY-MM-DD` helpers (`todayIn`, `addDays`, `daysBetween`, `startOfIsoWeek`, `isCalendarDate`, `isTimeZone`, `minutesUntil`)
|
|
33
|
+
- `local.ts` - `localDateTime` (site and organization clocks)
|
|
34
|
+
- `schedule.ts` - recurrences, coverage and local reporting periods
|
|
31
35
|
|
|
32
36
|
## DateTime Class (`now.ts`)
|
|
33
37
|
|
|
@@ -252,10 +256,70 @@ The root additionally exports `zoneOffsetMinutes(instant, timeZone)`,
|
|
|
252
256
|
`addCalendarMonths(date, count, { utc? })`. A wall-clock string has no supplied
|
|
253
257
|
offset; the named IANA zone converts it into an instant. `isoInZone` retains
|
|
254
258
|
that zone's offset in serialized output rather than merely displaying a date.
|
|
255
|
-
Review `zone.ts` and its DST behavior when ambiguous/gap local times matter.
|
|
256
259
|
`addCalendarMonths` clones and clamps the day; ordinary `DateTime.addMonths`
|
|
257
260
|
preserves native Date overflow instead. Do not interchange those contracts.
|
|
258
261
|
|
|
262
|
+
`zonedTimeToUtc` resolves DST edges the same way in every zone (RFC 5545 and
|
|
263
|
+
Temporal's `'compatible'` rule):
|
|
264
|
+
|
|
265
|
+
| Wall clock | Zone | Result | Why |
|
|
266
|
+
|---|---|---|---|
|
|
267
|
+
| `2026-10-25T02:30` | `Europe/Berlin` | `2026-10-25T00:30:00Z` (`+02:00`) | Repeated hour: the EARLIER instant |
|
|
268
|
+
| `2026-11-01T01:30` | `America/New_York` | `2026-11-01T05:30:00Z` (`-04:00`) | Repeated hour: the EARLIER instant |
|
|
269
|
+
| `2026-03-29T02:30` | `Europe/Berlin` | `2026-03-29T01:30:00Z`, shown as `03:30+02:00` | Skipped: read with the offset before the gap |
|
|
270
|
+
| `2026-03-08T02:30` | `America/New_York` | `2026-03-08T07:30:00Z`, shown as `03:30-04:00` | Skipped: read with the offset before the gap |
|
|
271
|
+
|
|
272
|
+
Releases up to 0.75.228 returned the LATER instant of a repeated hour in zones
|
|
273
|
+
at or east of UTC (Berlin, London, Sydney) while New York got the earlier one.
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
import { zonedTimeToUtc } from '@stacksjs/datetime'
|
|
277
|
+
|
|
278
|
+
zonedTimeToUtc('2026-10-01T20:00:00', 'America/New_York').toISOString() // '2026-10-02T00:00:00.000Z'
|
|
279
|
+
zonedTimeToUtc('2026-10-25T02:30', 'Europe/Berlin').toISOString() // '2026-10-25T00:30:00.000Z'
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Calendar dates (`YYYY-MM-DD` strings)
|
|
283
|
+
|
|
284
|
+
A date column, a training day or a membership period is a day on a calendar,
|
|
285
|
+
not an instant. Keep it a `YYYY-MM-DD` string: these helpers do their arithmetic
|
|
286
|
+
on the UTC calendar (every day 24 hours, whatever the server zone) and only meet
|
|
287
|
+
a time zone where a real clock is involved (`todayIn`, `minutesUntil`). Source:
|
|
288
|
+
`src/calendar.ts`.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
import { addDays, daysBetween, isCalendarDate, isTimeZone, minutesUntil, startOfIsoWeek, todayIn } from '@stacksjs/datetime'
|
|
292
|
+
|
|
293
|
+
isCalendarDate('2026-02-31') // false (a type guard; also rejects '2026-2-3')
|
|
294
|
+
isTimeZone('Europe/Berlin') // true (a type guard; '' and unknown zones are false)
|
|
295
|
+
todayIn('Pacific/Auckland', now) // the date there, e.g. '2026-10-19' while UTC is on the 18th
|
|
296
|
+
addDays('2026-12-31', 1) // '2027-01-01'
|
|
297
|
+
daysBetween('2026-10-18', '2026-10-25') // 7 (signed: negative when the second is earlier)
|
|
298
|
+
startOfIsoWeek('2026-10-18') // '2026-10-12' (the Monday; a Sunday belongs to the week before)
|
|
299
|
+
|
|
300
|
+
// Real minutes from now until 09:00 on a date in a zone, correct across DST:
|
|
301
|
+
minutesUntil('2026-10-25', '09:00', 'Europe/Berlin', new Date('2026-10-24T22:00:00Z')) // 600, not 540
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
- Every helper throws `RangeError` for an impossible date, a malformed clock
|
|
305
|
+
time (`HH:mm` or `HH:mm:ss`, 00-23 hours), an unknown zone or an invalid
|
|
306
|
+
instant. Validate input with `isCalendarDate` / `isTimeZone` first.
|
|
307
|
+
- `todayIn` takes one zone. To fall back (a site's zone, then its
|
|
308
|
+
organization's, then UTC) use `localDateTime(now, ...zones).date`.
|
|
309
|
+
- `minutesUntil` keeps the seconds of `now` as a fraction; round where you
|
|
310
|
+
display or compare. Negative once the moment has passed.
|
|
311
|
+
- `addDays` refuses a result past 9999-12-31.
|
|
312
|
+
|
|
313
|
+
### Reading `DATE` columns: `parseSqlDate` (`@stacksjs/database`)
|
|
314
|
+
|
|
315
|
+
Drivers return a `DATE` column as a JS `Date`, at UTC midnight (Bun's Postgres
|
|
316
|
+
driver) or at the host's local midnight (others). `date.toISOString().slice(0, 10)`
|
|
317
|
+
is only right for the first: on a host east of UTC a local-midnight `Date` moves
|
|
318
|
+
to the day before. `parseSqlDate(value)` from `@stacksjs/database` returns the
|
|
319
|
+
stored `YYYY-MM-DD` either way (local midnight reads local fields, anything else
|
|
320
|
+
UTC fields), keeps the date part of strings, and returns null for missing,
|
|
321
|
+
invalid or impossible input. It sits beside `parseSqlDateTime`.
|
|
322
|
+
|
|
259
323
|
## Current-time helper example
|
|
260
324
|
|
|
261
325
|
```typescript
|