@softure-ai/core 0.1.7 → 0.1.9
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/CHANGELOG.md +17 -0
- package/README.md +63 -1
- package/dist/calendar-day.d.ts +23 -0
- package/dist/calendar-day.d.ts.map +1 -1
- package/dist/calendar-day.js +87 -0
- package/dist/calendar-day.js.map +1 -1
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -1
- package/dist/config.js.map +1 -1
- package/dist/currency-digits.d.ts +3 -0
- package/dist/currency-digits.d.ts.map +1 -0
- package/dist/currency-digits.js +33 -0
- package/dist/currency-digits.js.map +1 -0
- package/dist/format.d.ts +23 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +63 -0
- package/dist/format.js.map +1 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/origins.d.ts +47 -0
- package/dist/origins.d.ts.map +1 -0
- package/dist/origins.js +69 -0
- package/dist/origins.js.map +1 -0
- package/package.json +1 -1
- package/src/calendar-day.ts +104 -0
- package/src/config.ts +32 -1
- package/src/currency-digits.ts +38 -0
- package/src/format.ts +79 -0
- package/src/index.ts +25 -1
- package/src/origins.ts +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
|
|
|
4
4
|
production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
|
|
5
5
|
"Definition of done"). Versions before the first one below are described in their GitHub Releases (`core@x.y.z`).
|
|
6
6
|
|
|
7
|
+
## 0.1.9
|
|
8
|
+
|
|
9
|
+
- Day arithmetic on `YYYY-MM-DD` days with documented semantics: `isCalendarDay`, `addCalendarDays`,
|
|
10
|
+
`addCalendarMonths(day, months, { endOfMonth: "clamp" | "overflow" })` (clamp by default: Jan 31 + 1 month =
|
|
11
|
+
Feb 28/29), `calendarDaysBetween` and `wholeMonthsBetween` (#312).
|
|
12
|
+
- Display: `formatCalendarDay(day, locale, "long" | "medium" | "numeric")`, `formatMoney(minor, currency, locale,
|
|
13
|
+
{ rounded, signed, compact })` (thousands always grouped, so pl writes `1 234,56`) and `formatPercent(basisPoints,
|
|
14
|
+
locale)`. The pinned ISO 4217 table `CURRENCY_MINOR_UNIT_DIGITS` moves here from billing (#312).
|
|
15
|
+
|
|
16
|
+
## 0.1.8
|
|
17
|
+
|
|
18
|
+
- Request origins (#311): `readRequestHost`, `readRequestOrigin`, `readForwardedProto`, `parseOrigin`,
|
|
19
|
+
`getTrustedOrigins` and `resolveAppOrigin(config, request, { trustedOrigins })`, one rule for every module that
|
|
20
|
+
builds an absolute URL for a request (README, "Request origins"). `X-Forwarded-Host` only ever picks a listed origin.
|
|
21
|
+
- The config takes `origins: { trustedOrigins, trustRequestHost }` (both optional; defaults `[]` and `false`), read by
|
|
22
|
+
auth, agent-ready, mcp-access and analytics. `SoftureConfig.origins` is always filled by `defineSoftureConfig`.
|
|
23
|
+
|
|
7
24
|
## 0.1.7
|
|
8
25
|
|
|
9
26
|
- `toCalendarDay(instant, timeZone)` and `getCalendarDay(clock, timeZone)`: the calendar day (`YYYY-MM-DD`) of an
|
package/README.md
CHANGED
|
@@ -28,6 +28,10 @@ interface SoftureConfigInput {
|
|
|
28
28
|
locale: "en" | "pl";
|
|
29
29
|
timezone: string; // IANA zone, e.g. "Europe/Warsaw"
|
|
30
30
|
appOrigin: string; // http(s) origin without a path
|
|
31
|
+
origins?: { // read by resolveAppOrigin, see "Request origins" below
|
|
32
|
+
trustedOrigins?: string[]; // other origins the app is served under (at most 16)
|
|
33
|
+
trustRequestHost?: boolean; // trust Host for any host (default false)
|
|
34
|
+
};
|
|
31
35
|
modules: SoftureModule[]; // a module is enabled by being listed
|
|
32
36
|
}
|
|
33
37
|
```
|
|
@@ -116,6 +120,37 @@ export const notes = defineModule({
|
|
|
116
120
|
- Version ranges in `dependsOn`: `x.y.z`, `^x.y.z`, `~x.y.z`, `*`, each optionally ending in `?`
|
|
117
121
|
(optional dependency). Caret follows npm on `0.x`: `^0.1.0` is `>=0.1.0 <0.2.0`.
|
|
118
122
|
|
|
123
|
+
### Request origins
|
|
124
|
+
|
|
125
|
+
Behind a proxy `request.url` names the server's listening address (`http://0.0.0.0:3000`), so a module that builds an
|
|
126
|
+
absolute URL for a request (auth's login redirect, agent-ready's documents, mcp-access's OAuth URLs, analytics' channel
|
|
127
|
+
redirect) reads the origin from headers, with one rule from core:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
import { getTrustedOrigins, readRequestHost, readRequestOrigin, resolveAppOrigin } from "@softure-ai/core";
|
|
131
|
+
|
|
132
|
+
readRequestHost(request); // first Host value, else the URL's host; lowercased
|
|
133
|
+
readRequestHost(request, { forwardedHost: true }); // first X-Forwarded-Host value first
|
|
134
|
+
readRequestOrigin(request); // X-Forwarded-Proto when http(s), else the URL's scheme; null for a bad host
|
|
135
|
+
getTrustedOrigins(config); // appOrigin, then origins.trustedOrigins
|
|
136
|
+
resolveAppOrigin(config, request); // the app origin to build URLs on for this request
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
`resolveAppOrigin` answers, in order:
|
|
140
|
+
|
|
141
|
+
1. the request's origin read with `X-Forwarded-Host` (a proxy that rewrites `Host` names the public host there), when
|
|
142
|
+
it is one of `getTrustedOrigins(config, extra)`;
|
|
143
|
+
2. with `origins.trustRequestHost`, the request's origin read from `Host`, whatever the host: for one image served
|
|
144
|
+
under origins nobody lists (a test stack on another port), behind a proxy that passes `Host` through and refuses
|
|
145
|
+
hosts it does not serve;
|
|
146
|
+
3. `appOrigin`.
|
|
147
|
+
|
|
148
|
+
`X-Forwarded-Host` only ever picks a listed origin: a client can send it through a proxy that keeps it. An app on two
|
|
149
|
+
hosts (product on `https://app.example.com`, site on `https://example.com`) lists the second once,
|
|
150
|
+
`origins: { trustedOrigins: ["https://example.com"] }`, and every module follows. A response built on the result is
|
|
151
|
+
cached per `Host`, `X-Forwarded-Host` and `X-Forwarded-Proto` (`Vary`). `parseOrigin(value)` is the strict origin check
|
|
152
|
+
the rule uses: exactly `http(s)://host[:port]`, a trailing `/` allowed.
|
|
153
|
+
|
|
119
154
|
## 4. Mounting
|
|
120
155
|
|
|
121
156
|
`@softure-ai/core/next` holds the config registry for server actions and route handlers shipped
|
|
@@ -261,6 +296,32 @@ app's `timezone` from the config. Both throw a `RangeError` for an invalid date
|
|
|
261
296
|
getCalendarDay(context.clock, config.timezone); // "2026-10-08"
|
|
262
297
|
```
|
|
263
298
|
|
|
299
|
+
**Day arithmetic.** Work on the `YYYY-MM-DD` strings themselves, never on `Date`s at local midnight: a day
|
|
300
|
+
is a calendar day, whatever DST makes of its hours. Every function throws a `RangeError` for a malformed day
|
|
301
|
+
or a fractional count.
|
|
302
|
+
|
|
303
|
+
| Function | Semantics |
|
|
304
|
+
| --- | --- |
|
|
305
|
+
| `isCalendarDay(value)` | A real day written `YYYY-MM-DD` (`2026-02-29` and `2026-1-1` are not). |
|
|
306
|
+
| `addCalendarDays(day, days)` | `day` moved by whole days, negative goes back. |
|
|
307
|
+
| `addCalendarMonths(day, months, { endOfMonth })` | `"clamp"` (default): a day the month lacks becomes its last day, Jan 31 + 1 = Feb 28/29. `"overflow"`: it runs on, Jan 31 + 1 = Mar 3 (2026). |
|
|
308
|
+
| `calendarDaysBetween(from, to)` | `to - from` in days; negative when `to` comes first. |
|
|
309
|
+
| `wholeMonthsBetween(from, to)` | The most months `n` with `addCalendarMonths(from, n)` (clamped) not after `to`: Jan 31 to Feb 28 is 1. Swapping the days turns the sign. |
|
|
310
|
+
|
|
311
|
+
**Display.** One way to show days, money and percentages in every module and app, in `config.locale`:
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
formatCalendarDay("2026-10-04", "en"); // "October 4, 2026"; "medium": "Oct 4, 2026"; "numeric": "10/04/2026"
|
|
315
|
+
formatCalendarDay(toCalendarDay(instant, config.timezone), config.locale); // an instant's day in the app's zone
|
|
316
|
+
formatMoney(123456, "PLN", "en"); // "PLN 1,234.56"; pl groups four digits too
|
|
317
|
+
formatMoney(-123456, "PLN", "en", { rounded: true, signed: true }); // "-PLN 1,235"; { compact: true }: "PLN 1.2K"
|
|
318
|
+
formatPercent(1250, "en"); // "12.5%" (basis points, at most two fraction digits)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
A day is formatted at UTC midnight in UTC, so no zone moves it; the Polish long style names the month in
|
|
322
|
+
the genitive. `formatMoney` takes the amount in the currency's minor unit, read from the pinned ISO 4217
|
|
323
|
+
table `CURRENCY_MINOR_UNIT_DIGITS` (a code outside it gets `Intl`'s digits), and always groups thousands.
|
|
324
|
+
|
|
264
325
|
## 10. Hooks
|
|
265
326
|
|
|
266
327
|
`ModuleContext` is what every server function of a module receives: `{ db, clock, config }`
|
|
@@ -285,4 +346,5 @@ manifest's `privacy.exports` / `privacy.deletes` flag is true. The `privacy` mod
|
|
|
285
346
|
are rejected.
|
|
286
347
|
- Route maps mix mounted paths and redirect targets, so two modules may share a path; mount
|
|
287
348
|
collisions are left to `softure doctor`.
|
|
288
|
-
-
|
|
349
|
+
- Formatting covers days, money and percentages; times of day and other numbers use `Intl` with `config.locale` and
|
|
350
|
+
`config.timezone`.
|
package/dist/calendar-day.d.ts
CHANGED
|
@@ -3,4 +3,27 @@ import type { Clock } from "./clock.js";
|
|
|
3
3
|
export declare function toCalendarDay(instant: Date, timeZone: string): string;
|
|
4
4
|
/** Today's calendar day (`YYYY-MM-DD`) in `timeZone`, read from `clock`. */
|
|
5
5
|
export declare function getCalendarDay(clock: Clock, timeZone: string): string;
|
|
6
|
+
/** What `addCalendarMonths` does with a day the target month lacks (31 January + 1 month). */
|
|
7
|
+
export type EndOfMonth = "clamp" | "overflow";
|
|
8
|
+
/** Whether `value` is a real calendar day written `YYYY-MM-DD` (`2026-02-29` and `2026-1-1` are not). */
|
|
9
|
+
export declare function isCalendarDay(value: unknown): value is string;
|
|
10
|
+
/** `day` moved by `days` calendar days (negative goes back). Throws a `RangeError` for a malformed day or count. */
|
|
11
|
+
export declare function addCalendarDays(day: string, days: number): string;
|
|
12
|
+
/**
|
|
13
|
+
* `day` moved by `months` calendar months (negative goes back). A day the target month lacks is the
|
|
14
|
+
* month's last day with `endOfMonth: "clamp"` (the default: 31 January + 1 month = 28 or 29 February,
|
|
15
|
+
* as billing periods count), or runs on into the next month with `"overflow"` (3 March in 2026, as
|
|
16
|
+
* `Date` does). Throws a `RangeError` for a malformed day or count.
|
|
17
|
+
*/
|
|
18
|
+
export declare function addCalendarMonths(day: string, months: number, options?: {
|
|
19
|
+
readonly endOfMonth?: EndOfMonth;
|
|
20
|
+
}): string;
|
|
21
|
+
/** Calendar days from `from` to `to`: 1 from one day to the next, negative when `to` comes first. */
|
|
22
|
+
export declare function calendarDaysBetween(from: string, to: string): number;
|
|
23
|
+
/**
|
|
24
|
+
* Whole months from `from` to `to`: the most months `n` for which `addCalendarMonths(from, n)` (clamped)
|
|
25
|
+
* is not after `to`. So 31 January to 28 February is one month, as a monthly period counts it.
|
|
26
|
+
* Negative when `to` comes first: `wholeMonthsBetween(to, from)` with the sign turned.
|
|
27
|
+
*/
|
|
28
|
+
export declare function wholeMonthsBetween(from: string, to: string): number;
|
|
6
29
|
//# sourceMappingURL=calendar-day.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-day.d.ts","sourceRoot":"","sources":["../src/calendar-day.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAIxC,qHAAqH;AACrH,wBAAgB,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErE"}
|
|
1
|
+
{"version":3,"file":"calendar-day.d.ts","sourceRoot":"","sources":["../src/calendar-day.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAIxC,qHAAqH;AACrH,wBAAgB,aAAa,CAAC,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQrE;AAED,4EAA4E;AAC5E,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAErE;AAiCD,8FAA8F;AAC9F,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,UAAU,CAAC;AAE9C,yGAAyG;AACzG,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAE7D;AAED,oHAAoH;AACpH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIjE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,QAAQ,CAAC,UAAU,CAAC,EAAE,UAAU,CAAA;CAAO,GAAG,MAAM,CAQzH;AAED,qGAAqG;AACrG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAGpE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAOnE"}
|
package/dist/calendar-day.js
CHANGED
|
@@ -29,4 +29,91 @@ function createFormatter(timeZone) {
|
|
|
29
29
|
throw new RangeError(`toCalendarDay: "${timeZone}" is not an IANA time zone`, { cause: error });
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
// Arithmetic on calendar days (`YYYY-MM-DD`): civil days with no time zone, so a day is a day even
|
|
33
|
+
// when DST makes it 23 or 25 hours long. A day number counts days since 1970-01-01.
|
|
34
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
const MONTHS_PER_YEAR = 12;
|
|
36
|
+
const DAY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
37
|
+
/** Whether `value` is a real calendar day written `YYYY-MM-DD` (`2026-02-29` and `2026-1-1` are not). */
|
|
38
|
+
export function isCalendarDay(value) {
|
|
39
|
+
return typeof value === "string" && parseCivilDate(value) !== null;
|
|
40
|
+
}
|
|
41
|
+
/** `day` moved by `days` calendar days (negative goes back). Throws a `RangeError` for a malformed day or count. */
|
|
42
|
+
export function addCalendarDays(day, days) {
|
|
43
|
+
const operation = "addCalendarDays";
|
|
44
|
+
assertWhole(operation, days, "days");
|
|
45
|
+
return fromDayNumber(operation, toDayNumber(operation, day) + days);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* `day` moved by `months` calendar months (negative goes back). A day the target month lacks is the
|
|
49
|
+
* month's last day with `endOfMonth: "clamp"` (the default: 31 January + 1 month = 28 or 29 February,
|
|
50
|
+
* as billing periods count), or runs on into the next month with `"overflow"` (3 March in 2026, as
|
|
51
|
+
* `Date` does). Throws a `RangeError` for a malformed day or count.
|
|
52
|
+
*/
|
|
53
|
+
export function addCalendarMonths(day, months, options = {}) {
|
|
54
|
+
const operation = "addCalendarMonths";
|
|
55
|
+
assertWhole(operation, months, "months");
|
|
56
|
+
const date = readCivilDate(operation, day);
|
|
57
|
+
const month = date.month + months;
|
|
58
|
+
const lastDay = new Date(utcMilliseconds({ year: date.year, month: month + 1, day: 0 })).getUTCDate();
|
|
59
|
+
const target = options.endOfMonth === "overflow" ? date.day : Math.min(date.day, lastDay);
|
|
60
|
+
return fromDayNumber(operation, utcMilliseconds({ year: date.year, month, day: target }) / DAY_MS);
|
|
61
|
+
}
|
|
62
|
+
/** Calendar days from `from` to `to`: 1 from one day to the next, negative when `to` comes first. */
|
|
63
|
+
export function calendarDaysBetween(from, to) {
|
|
64
|
+
const operation = "calendarDaysBetween";
|
|
65
|
+
return toDayNumber(operation, to) - toDayNumber(operation, from);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whole months from `from` to `to`: the most months `n` for which `addCalendarMonths(from, n)` (clamped)
|
|
69
|
+
* is not after `to`. So 31 January to 28 February is one month, as a monthly period counts it.
|
|
70
|
+
* Negative when `to` comes first: `wholeMonthsBetween(to, from)` with the sign turned.
|
|
71
|
+
*/
|
|
72
|
+
export function wholeMonthsBetween(from, to) {
|
|
73
|
+
const operation = "wholeMonthsBetween";
|
|
74
|
+
const start = readCivilDate(operation, from);
|
|
75
|
+
const end = readCivilDate(operation, to);
|
|
76
|
+
if (to < from)
|
|
77
|
+
return -wholeMonthsBetween(to, from);
|
|
78
|
+
const months = (end.year - start.year) * MONTHS_PER_YEAR + (end.month - start.month);
|
|
79
|
+
return addCalendarMonths(from, months) > to ? months - 1 : months;
|
|
80
|
+
}
|
|
81
|
+
function assertWhole(operation, count, unit) {
|
|
82
|
+
if (!Number.isSafeInteger(count))
|
|
83
|
+
throw new RangeError(`${operation}: ${String(count)} is not a whole number of ${unit}`);
|
|
84
|
+
}
|
|
85
|
+
/** Milliseconds of UTC midnight of a civil date; month and day may overflow, as in `Date.UTC`, for any year. */
|
|
86
|
+
function utcMilliseconds({ year, month, day }) {
|
|
87
|
+
// Not `Date.UTC`: it reads years 0-99 as 1900-1999.
|
|
88
|
+
const date = new Date(0);
|
|
89
|
+
date.setUTCFullYear(year, month, day);
|
|
90
|
+
return date.getTime();
|
|
91
|
+
}
|
|
92
|
+
function parseCivilDate(text) {
|
|
93
|
+
const match = DAY_PATTERN.exec(text);
|
|
94
|
+
if (match === null)
|
|
95
|
+
return null;
|
|
96
|
+
const date = { year: Number(match[1]), month: Number(match[2]) - 1, day: Number(match[3]) };
|
|
97
|
+
const parsed = new Date(utcMilliseconds(date));
|
|
98
|
+
if (parsed.getUTCMonth() !== date.month || parsed.getUTCDate() !== date.day)
|
|
99
|
+
return null;
|
|
100
|
+
return date;
|
|
101
|
+
}
|
|
102
|
+
function readCivilDate(operation, day) {
|
|
103
|
+
const date = parseCivilDate(day);
|
|
104
|
+
if (date === null)
|
|
105
|
+
throw new RangeError(`${operation}: "${day}" is not a YYYY-MM-DD calendar day`);
|
|
106
|
+
return date;
|
|
107
|
+
}
|
|
108
|
+
function toDayNumber(operation, day) {
|
|
109
|
+
return utcMilliseconds(readCivilDate(operation, day)) / DAY_MS;
|
|
110
|
+
}
|
|
111
|
+
function fromDayNumber(operation, dayNumber) {
|
|
112
|
+
const date = new Date(dayNumber * DAY_MS);
|
|
113
|
+
const year = date.getUTCFullYear();
|
|
114
|
+
if (year < 0 || year > 9999)
|
|
115
|
+
throw new RangeError(`${operation}: the result falls outside the years 0000-9999`);
|
|
116
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
117
|
+
return `${String(year).padStart(4, "0")}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
118
|
+
}
|
|
32
119
|
//# sourceMappingURL=calendar-day.js.map
|
package/dist/calendar-day.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"calendar-day.js","sourceRoot":"","sources":["../src/calendar-day.ts"],"names":[],"mappings":"AAIA,MAAM,UAAU,GAAG,IAAI,GAAG,EAA+B,CAAC;AAE1D,qHAAqH;AACrH,MAAM,UAAU,aAAa,CAAC,OAAa,EAAE,QAAgB;IAC3D,IAAI,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,UAAU,CAAC,6BAA6B,CAAC,CAAC;IACtD,CAAC;IACD,uGAAuG;IACvG,MAAM,KAAK,GAAG,YAAY,CAAC,QAAQ,CAAC,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;IAC5D,MAAM,QAAQ,GAAG,CAAC,IAAkC,EAAU,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;IACvH,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAC,IAAI,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;AACxF,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,cAAc,CAAC,KAAY,EAAE,QAAgB;IAC3D,OAAO,aAAa,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,YAAY,CAAC,QAAgB;IACpC,IAAI,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACtC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;QACnC,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,eAAe,CAAC,QAAgB;IACvC,IAAI,CAAC;QACH,OAAO,IAAI,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,CAAC;IAC3G,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,UAAU,CAAC,mBAAmB,QAAQ,4BAA4B,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAClG,CAAC;AACH,CAAC"}
|
|
1
|
+
{"version":3,"file":"calendar-day.js","sourceRoot":"","sources":["../src/calendar-day.ts"],"names":[],"mappings":"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"}
|
package/dist/config.d.ts
CHANGED
|
@@ -24,6 +24,23 @@ export interface SoftureDatabaseConfig {
|
|
|
24
24
|
*/
|
|
25
25
|
readonly handle?: () => MaybePromise<SoftureDatabaseHandle>;
|
|
26
26
|
}
|
|
27
|
+
/** The app's origins besides `appOrigin` and how far the request's own host is trusted (core `resolveAppOrigin`). */
|
|
28
|
+
export interface SoftureOrigins {
|
|
29
|
+
/**
|
|
30
|
+
* Other origins the app is served under (an apex beside `app.`, a second domain): absolute URLs for a request sent
|
|
31
|
+
* to one of them stay on it. At most 16.
|
|
32
|
+
*/
|
|
33
|
+
readonly trustedOrigins: readonly string[];
|
|
34
|
+
/**
|
|
35
|
+
* Trust the request's `Host` (and `X-Forwarded-Proto`) for any host, not only listed ones: for an image built once
|
|
36
|
+
* and served under origins nobody lists, behind a proxy that refuses hosts it does not serve. Default false.
|
|
37
|
+
*/
|
|
38
|
+
readonly trustRequestHost: boolean;
|
|
39
|
+
}
|
|
40
|
+
export interface SoftureOriginsInput {
|
|
41
|
+
readonly trustedOrigins?: readonly string[];
|
|
42
|
+
readonly trustRequestHost?: boolean;
|
|
43
|
+
}
|
|
27
44
|
export interface SoftureConfig {
|
|
28
45
|
/**
|
|
29
46
|
* `null` when the app has no database; required as soon as a module has a `dbSchema`. An empty `url`
|
|
@@ -35,6 +52,8 @@ export interface SoftureConfig {
|
|
|
35
52
|
readonly timezone: string;
|
|
36
53
|
/** Scheme, host and port of the app, without a path: `https://app.example.com`. */
|
|
37
54
|
readonly appOrigin: string;
|
|
55
|
+
/** Every origin the app is served under besides `appOrigin`, read by `resolveAppOrigin`. */
|
|
56
|
+
readonly origins: SoftureOrigins;
|
|
38
57
|
/** Enabled modules, in the order the app listed them. */
|
|
39
58
|
readonly modules: readonly AnySoftureModule[];
|
|
40
59
|
}
|
|
@@ -44,6 +63,7 @@ export interface SoftureConfigInput {
|
|
|
44
63
|
readonly locale: Locale;
|
|
45
64
|
readonly timezone: string;
|
|
46
65
|
readonly appOrigin: string;
|
|
66
|
+
readonly origins?: SoftureOriginsInput;
|
|
47
67
|
readonly modules: readonly AnySoftureModule[];
|
|
48
68
|
}
|
|
49
69
|
/**
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAGnD;;;GAGG;AAEH,MAAM,WAAW,0BAA0B;CAAG;AAE9C,iHAAiH;AACjH,MAAM,MAAM,qBAAqB,GAAG,0BAA0B,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5H,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAC7D;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAGnD;;;GAGG;AAEH,MAAM,WAAW,0BAA0B;CAAG;AAE9C,iHAAiH;AACjH,MAAM,MAAM,qBAAqB,GAAG,0BAA0B,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5H,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAC7D;AAED,qHAAqH;AACrH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AA8BD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAmB5E;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEzF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,MAAM,CAAC,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAGrD"}
|
package/dist/config.js
CHANGED
|
@@ -7,6 +7,7 @@ import { LOCALES } from "./i18n.js";
|
|
|
7
7
|
import { err, ok } from "./result.js";
|
|
8
8
|
import { parseVersionRange, satisfiesRange } from "./version-range.js";
|
|
9
9
|
const CONFIG_SUBJECT = "softure.config (defineSoftureConfig)";
|
|
10
|
+
const ORIGIN_MESSAGE = "must be an http(s) origin without a path, e.g. https://app.example.com";
|
|
10
11
|
const configSchema = z.object({
|
|
11
12
|
// The URL is not checked for emptiness here: builds import the config without DATABASE_URL, so an empty URL is
|
|
12
13
|
// refused when something connects (`createDatabase` in @softure-ai/db), not when the config is defined.
|
|
@@ -21,7 +22,13 @@ const configSchema = z.object({
|
|
|
21
22
|
.default(null),
|
|
22
23
|
locale: z.enum(LOCALES),
|
|
23
24
|
timezone: z.string().refine(isTimeZone, "must be an IANA time zone, e.g. Europe/Warsaw"),
|
|
24
|
-
appOrigin: z.string().refine(isOrigin,
|
|
25
|
+
appOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE),
|
|
26
|
+
origins: z
|
|
27
|
+
.strictObject({
|
|
28
|
+
trustedOrigins: z.array(z.string().refine(isOrigin, ORIGIN_MESSAGE)).max(16, "takes at most 16 origins").default([]),
|
|
29
|
+
trustRequestHost: z.boolean().default(false),
|
|
30
|
+
})
|
|
31
|
+
.default({ trustedOrigins: [], trustRequestHost: false }),
|
|
25
32
|
modules: z.array(z.custom(isSoftureModule, "must be a module returned by a module factory")),
|
|
26
33
|
});
|
|
27
34
|
/**
|
|
@@ -42,6 +49,7 @@ export function defineSoftureConfig(input) {
|
|
|
42
49
|
return Object.freeze({
|
|
43
50
|
...config,
|
|
44
51
|
database: config.database === null ? null : Object.freeze({ ...config.database }),
|
|
52
|
+
origins: Object.freeze({ ...config.origins, trustedOrigins: Object.freeze([...config.origins.trustedOrigins]) }),
|
|
45
53
|
modules: Object.freeze([...config.modules]),
|
|
46
54
|
});
|
|
47
55
|
}
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gEAAgE;AAChE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAe,MAAM,WAAW,CAAC;AAEjD,OAAO,EAAE,GAAG,EAAE,EAAE,EAAe,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"currency-digits.d.ts","sourceRoot":"","sources":["../src/currency-digits.ts"],"names":[],"mappings":"AAgCA,8FAA8F;AAC9F,eAAO,MAAM,0BAA0B,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAIvE,CAAC"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The minor unit pinned for each currency SOFTURE accepts: an amount in minor units (billing's
|
|
2
|
+
// `price.amount`, `formatMoney`) counts these units on every runtime, whatever the runtime's `Intl`
|
|
3
|
+
// (CLDR) says. Source: ISO 4217 List One, published 2024-06-25, without fund codes, `N.A.` units
|
|
4
|
+
// (metals, XDR, XTS, XXX ...) and UYW (a wage index unit), plus the two choices below. CLDR differs
|
|
5
|
+
// from ISO on AFN, ALL, IRR, KPW, LAK, LBP, MMK, RSD, SOS, SYP and YER (0 instead of 2) and IQD
|
|
6
|
+
// (0 instead of 3), and its HUF and TWD digits have changed between Node builds; the table follows
|
|
7
|
+
// ISO, as Stripe does for all but MGA.
|
|
8
|
+
/** ISO 4217 codes by the digits of their minor unit. */
|
|
9
|
+
const CODES_BY_DIGITS = {
|
|
10
|
+
0: [
|
|
11
|
+
"BIF", "CLP", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF",
|
|
12
|
+
// Override: ISO writes 2, but one ariary is five iraimbilanja (not a decimal subunit), and CLDR
|
|
13
|
+
// and Stripe both count it without a minor unit.
|
|
14
|
+
"MGA",
|
|
15
|
+
],
|
|
16
|
+
2: [
|
|
17
|
+
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BMD", "BND",
|
|
18
|
+
"BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHF", "CNY", "COP", "CRC", "CUC", "CUP", "CVE",
|
|
19
|
+
"CZK", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GTQ",
|
|
20
|
+
"GYD", "HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IRR", "JMD", "KES", "KGS", "KHR", "KPW", "KYD", "KZT",
|
|
21
|
+
"LAK", "LBP", "LKR", "LRD", "LSL", "MAD", "MDL", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN",
|
|
22
|
+
"MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "QAR", "RON",
|
|
23
|
+
"RSD", "RUB", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SVC", "SYP",
|
|
24
|
+
"SZL", "THB", "TJS", "TMT", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "USD", "UYU", "UZS", "VED", "VES", "WST",
|
|
25
|
+
"XCD", "YER", "ZAR", "ZMW", "ZWG",
|
|
26
|
+
// Addition: the Caribbean guilder, which replaced ANG on 2025-03-31, after the pinned list.
|
|
27
|
+
"XCG",
|
|
28
|
+
],
|
|
29
|
+
3: ["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"],
|
|
30
|
+
};
|
|
31
|
+
/** Digits of the minor unit per accepted currency code, e.g. `PLN: 2`, `JPY: 0`, `KWD: 3`. */
|
|
32
|
+
export const CURRENCY_MINOR_UNIT_DIGITS = Object.freeze(Object.fromEntries(Object.entries(CODES_BY_DIGITS).flatMap(([digits, codes]) => codes.map((code) => [code, Number(digits)]))));
|
|
33
|
+
//# sourceMappingURL=currency-digits.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"currency-digits.js","sourceRoot":"","sources":["../src/currency-digits.ts"],"names":[],"mappings":"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"}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Locale } from "./i18n.js";
|
|
2
|
+
/** `long`: October 4, 2026 (pl names the month in the genitive); `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (pl: 04.10.2026). */
|
|
3
|
+
export type CalendarDayStyle = "long" | "medium" | "numeric";
|
|
4
|
+
export interface FormatMoneyOptions {
|
|
5
|
+
/** Whole units only, rounded half away from zero: PLN 1,235 for 1234.56. */
|
|
6
|
+
readonly rounded?: boolean;
|
|
7
|
+
/** A `+` before a positive amount (a `-` before a negative one is always there); zero has neither. */
|
|
8
|
+
readonly signed?: boolean;
|
|
9
|
+
/** The locale's short notation for large amounts: PLN 1.2M. */
|
|
10
|
+
readonly compact?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/** A `YYYY-MM-DD` day as `locale` writes it. Throws a `RangeError` for a malformed day. */
|
|
13
|
+
export declare function formatCalendarDay(day: string, locale: Locale, style?: CalendarDayStyle): string;
|
|
14
|
+
/**
|
|
15
|
+
* An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
|
|
16
|
+
* grouped (pl writes 1 234,56 where its default would leave four digits together). The minor unit is the pinned ISO
|
|
17
|
+
* 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
|
|
18
|
+
* `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
|
|
19
|
+
*/
|
|
20
|
+
export declare function formatMoney(minor: number, currency: string, locale: Locale, options?: FormatMoneyOptions): string;
|
|
21
|
+
/** Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits. Throws a `RangeError` for a non-finite value. */
|
|
22
|
+
export declare function formatPercent(basisPoints: number, locale: Locale): string;
|
|
23
|
+
//# sourceMappingURL=format.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAExC,mIAAmI;AACnI,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAcD,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,gBAAyB,GAAG,MAAM,CASvG;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAarH;AAED,0IAA0I;AAC1I,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAIzE"}
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Days, money and percentages as people read them, one way for every module and app. A calendar day
|
|
2
|
+
// is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it.
|
|
3
|
+
import { isCalendarDay } from "./calendar-day.js";
|
|
4
|
+
import { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
5
|
+
const BASIS_POINTS_PER_UNIT = 10_000;
|
|
6
|
+
const MAX_PERCENT_FRACTION_DIGITS = 2;
|
|
7
|
+
const dayFormatters = new Map();
|
|
8
|
+
const numberFormatters = new Map();
|
|
9
|
+
const DAY_STYLE_OPTIONS = {
|
|
10
|
+
long: { dateStyle: "long" },
|
|
11
|
+
medium: { dateStyle: "medium" },
|
|
12
|
+
numeric: { day: "2-digit", month: "2-digit", year: "numeric" },
|
|
13
|
+
};
|
|
14
|
+
/** A `YYYY-MM-DD` day as `locale` writes it. Throws a `RangeError` for a malformed day. */
|
|
15
|
+
export function formatCalendarDay(day, locale, style = "long") {
|
|
16
|
+
if (!isCalendarDay(day))
|
|
17
|
+
throw new RangeError(`formatCalendarDay: "${String(day)}" is not a YYYY-MM-DD calendar day`);
|
|
18
|
+
const key = `${locale}:${style}`;
|
|
19
|
+
let format = dayFormatters.get(key);
|
|
20
|
+
if (format === undefined) {
|
|
21
|
+
format = new Intl.DateTimeFormat(locale, { ...DAY_STYLE_OPTIONS[style], timeZone: "UTC" });
|
|
22
|
+
dayFormatters.set(key, format);
|
|
23
|
+
}
|
|
24
|
+
return format.format(new Date(`${day}T00:00:00Z`));
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
|
|
28
|
+
* grouped (pl writes 1 234,56 where its default would leave four digits together). The minor unit is the pinned ISO
|
|
29
|
+
* 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
|
|
30
|
+
* `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
|
|
31
|
+
*/
|
|
32
|
+
export function formatMoney(minor, currency, locale, options = {}) {
|
|
33
|
+
if (!Number.isSafeInteger(minor))
|
|
34
|
+
throw new RangeError(`formatMoney: ${String(minor)} is not a whole number of minor units`);
|
|
35
|
+
const pinned = Object.hasOwn(CURRENCY_MINOR_UNIT_DIGITS, currency) ? CURRENCY_MINOR_UNIT_DIGITS[currency] : undefined;
|
|
36
|
+
const digits = pinned ?? getNumberFormatter(locale, { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
37
|
+
const shownDigits = options.rounded === true ? 0 : digits;
|
|
38
|
+
const format = getNumberFormatter(locale, {
|
|
39
|
+
style: "currency",
|
|
40
|
+
currency,
|
|
41
|
+
useGrouping: "always",
|
|
42
|
+
signDisplay: options.signed === true ? "exceptZero" : "auto",
|
|
43
|
+
...(options.compact === true ? { notation: "compact" } : { minimumFractionDigits: shownDigits, maximumFractionDigits: shownDigits }),
|
|
44
|
+
});
|
|
45
|
+
return format.format(minor / 10 ** digits);
|
|
46
|
+
}
|
|
47
|
+
/** Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits. Throws a `RangeError` for a non-finite value. */
|
|
48
|
+
export function formatPercent(basisPoints, locale) {
|
|
49
|
+
if (!Number.isFinite(basisPoints))
|
|
50
|
+
throw new RangeError(`formatPercent: ${String(basisPoints)} is not a finite number of basis points`);
|
|
51
|
+
const format = getNumberFormatter(locale, { style: "percent", maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS });
|
|
52
|
+
return format.format(basisPoints / BASIS_POINTS_PER_UNIT);
|
|
53
|
+
}
|
|
54
|
+
function getNumberFormatter(locale, options) {
|
|
55
|
+
const key = `${locale}:${JSON.stringify(options)}`;
|
|
56
|
+
let format = numberFormatters.get(key);
|
|
57
|
+
if (format === undefined) {
|
|
58
|
+
format = new Intl.NumberFormat(locale, options);
|
|
59
|
+
numberFormatters.set(key, format);
|
|
60
|
+
}
|
|
61
|
+
return format;
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
export { getCalendarDay, toCalendarDay } from "./calendar-day.js";
|
|
1
|
+
export { addCalendarDays, addCalendarMonths, calendarDaysBetween, getCalendarDay, isCalendarDay, toCalendarDay, wholeMonthsBetween, type EndOfMonth, } from "./calendar-day.js";
|
|
2
|
+
export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
3
|
+
export { formatCalendarDay, formatMoney, formatPercent, type CalendarDayStyle, type FormatMoneyOptions } from "./format.js";
|
|
2
4
|
export { createTestClock, systemClock, type Clock, type TestClock } from "./clock.js";
|
|
3
5
|
export { formatMessage, getMessage, isLocale, LOCALES, mergeMessages, selectPlural, type DeepPartial, type Dictionaries, type Locale, type MessageOverrides, type MessageTree, type PluralForms, } from "./i18n.js";
|
|
4
6
|
export { coreMessages, type CoreMessages } from "./messages/index.js";
|
|
5
7
|
export { err, ok, type Err, type ErrorCode, type Ok, type Result } from "./result.js";
|
|
6
8
|
export { errorLogLabel, getPublicMessage, isPublicError, PublicError, safeError, type CoreErrorCode } from "./safe-error.js";
|
|
7
|
-
export { defineSoftureConfig, getModule, sortModulesByDependencies, type SoftureConfig, type SoftureConfigInput, type SoftureDatabaseConfig, type SoftureDatabaseHandle, type SoftureDatabaseHandleTypes, } from "./config.js";
|
|
9
|
+
export { defineSoftureConfig, getModule, sortModulesByDependencies, type SoftureConfig, type SoftureConfigInput, type SoftureDatabaseConfig, type SoftureDatabaseHandle, type SoftureDatabaseHandleTypes, type SoftureOrigins, type SoftureOriginsInput, } from "./config.js";
|
|
8
10
|
export { SoftureConfigError } from "./config-error.js";
|
|
9
11
|
export { withDatabaseOptional } from "./database-requirement.js";
|
|
10
12
|
export { moduleManifestSchema, type ModuleManifest } from "./manifest.js";
|
|
11
13
|
export { defineModule, resolveMigrationsDir, toModuleJson, type AnySoftureModule, type HealthCheck, type ModuleContext, type ModuleFactory, type ModuleInput, type ModuleMigrations, type ModuleSpec, type PrivacyContributor, type SoftureModule, } from "./module.js";
|
|
14
|
+
export { getTrustedOrigins, parseOrigin, readForwardedProto, readRequestHost, readRequestOrigin, resolveAppOrigin, type OriginRequest, type ReadOriginOptions, type ResolveAppOriginOptions, } from "./origins.js";
|
|
12
15
|
export { findSiteUrlProvider, getSiteUrls, type SiteUrlProvider, type SiteUrls } from "./site-urls.js";
|
|
13
16
|
export { findSwitchReader, readSwitch, type SwitchReader, type SwitchReading } from "./switches.js";
|
|
14
17
|
export { isVersion, parseVersionRange, satisfiesRange, type Version, type VersionRange } from "./version-range.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,KAAK,UAAU,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,KAAK,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAC5H,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,QAAQ,EACR,OAAO,EACP,aAAa,EACb,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,GAAG,EAAE,KAAK,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,EACzB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// Public API of @softure-ai/core: the contract every SOFTURE module stands on
|
|
2
2
|
// (docs/02-module-standard.md). The Next.js registry is a separate entry: `@softure-ai/core/next`.
|
|
3
|
-
export { getCalendarDay, toCalendarDay } from "./calendar-day.js";
|
|
3
|
+
export { addCalendarDays, addCalendarMonths, calendarDaysBetween, getCalendarDay, isCalendarDay, toCalendarDay, wholeMonthsBetween, } from "./calendar-day.js";
|
|
4
|
+
export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
5
|
+
export { formatCalendarDay, formatMoney, formatPercent } from "./format.js";
|
|
4
6
|
export { createTestClock, systemClock } from "./clock.js";
|
|
5
7
|
export { formatMessage, getMessage, isLocale, LOCALES, mergeMessages, selectPlural, } from "./i18n.js";
|
|
6
8
|
export { coreMessages } from "./messages/index.js";
|
|
@@ -11,6 +13,7 @@ export { SoftureConfigError } from "./config-error.js";
|
|
|
11
13
|
export { withDatabaseOptional } from "./database-requirement.js";
|
|
12
14
|
export { moduleManifestSchema } from "./manifest.js";
|
|
13
15
|
export { defineModule, resolveMigrationsDir, toModuleJson, } from "./module.js";
|
|
16
|
+
export { getTrustedOrigins, parseOrigin, readForwardedProto, readRequestHost, readRequestOrigin, resolveAppOrigin, } from "./origins.js";
|
|
14
17
|
export { findSiteUrlProvider, getSiteUrls } from "./site-urls.js";
|
|
15
18
|
export { findSwitchReader, readSwitch } from "./switches.js";
|
|
16
19
|
export { isVersion, parseVersionRange, satisfiesRange } from "./version-range.js";
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAkD,MAAM,aAAa,CAAC;AAC5H,OAAO,EAAE,eAAe,EAAE,WAAW,EAA8B,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,QAAQ,EACR,OAAO,EACP,aAAa,EACb,YAAY,GAOb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAkD,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAsB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,GAQ1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,GAUb,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GAIjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAuC,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAyC,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAmC,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { SoftureConfig } from "./config.js";
|
|
2
|
+
/** What the origin helpers read from a request: a Web `Request`, or Next's `headers()` with a URL. */
|
|
3
|
+
export interface OriginRequest {
|
|
4
|
+
readonly url: string;
|
|
5
|
+
readonly headers: {
|
|
6
|
+
get(name: string): string | null;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface ReadOriginOptions {
|
|
10
|
+
/** Read the first `X-Forwarded-Host` value before `Host`. Only for a result checked against a list of origins. */
|
|
11
|
+
readonly forwardedHost?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface ResolveAppOriginOptions {
|
|
14
|
+
/** Origins trusted besides `appOrigin` and `config.origins.trustedOrigins`, e.g. a module's own option. */
|
|
15
|
+
readonly trustedOrigins?: readonly string[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The normalized origin (`https://example.com`) when `value` is exactly `http(s)://host[:port]`, a trailing `/`
|
|
19
|
+
* allowed; null for anything else: a path, query, fragment, credentials, spaces or another scheme.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseOrigin(value: string): string | null;
|
|
22
|
+
/**
|
|
23
|
+
* The host the request was sent to: the first `X-Forwarded-Host` value when `forwardedHost` is set, else the first
|
|
24
|
+
* `Host` value, else the URL's host; trimmed and lowercased.
|
|
25
|
+
*/
|
|
26
|
+
export declare function readRequestHost(request: OriginRequest, options?: ReadOriginOptions): string;
|
|
27
|
+
/** The first `X-Forwarded-Proto` value when it is `http` or `https` (any case), else null. */
|
|
28
|
+
export declare function readForwardedProto(request: OriginRequest): "http" | "https" | null;
|
|
29
|
+
/**
|
|
30
|
+
* The origin the request was sent to: the first `X-Forwarded-Proto` value when it is http or https (else the URL's
|
|
31
|
+
* scheme) and `readRequestHost`. Null when the host is no host (a path, credentials or spaces in the header).
|
|
32
|
+
*/
|
|
33
|
+
export declare function readRequestOrigin(request: OriginRequest, options?: ReadOriginOptions): string | null;
|
|
34
|
+
/**
|
|
35
|
+
* Every origin the app is served under: `appOrigin`, `config.origins.trustedOrigins`, then `extra`, without
|
|
36
|
+
* repeats. An `extra` entry that is not an http(s) origin is a setup bug and throws by name.
|
|
37
|
+
*/
|
|
38
|
+
export declare function getTrustedOrigins(config: SoftureConfig, extra?: readonly string[]): readonly string[];
|
|
39
|
+
/**
|
|
40
|
+
* The app origin to build absolute URLs on for `request`:
|
|
41
|
+
* 1. the request's origin, read with `X-Forwarded-Host`, when it is one of `getTrustedOrigins`;
|
|
42
|
+
* 2. else, with `config.origins.trustRequestHost`, the request's origin read from `Host` (an image built once and
|
|
43
|
+
* served under other origins, behind a proxy that passes `Host` through and refuses hosts it does not serve);
|
|
44
|
+
* 3. else `config.appOrigin`.
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveAppOrigin(config: SoftureConfig, request: OriginRequest, options?: ResolveAppOriginOptions): string;
|
|
47
|
+
//# sourceMappingURL=origins.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origins.d.ts","sourceRoot":"","sources":["../src/origins.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,sGAAsG;AACtG,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE;QAAE,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CACxD;AAED,MAAM,WAAW,iBAAiB;IAChC,kHAAkH;IAClH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,2GAA2G;IAC3G,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CASxD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,CAG/F;AAED,8FAA8F;AAC9F,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAGlF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,iBAAsB,GAAG,MAAM,GAAG,IAAI,CAGxG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,EAAE,KAAK,GAAE,SAAS,MAAM,EAAO,GAAG,SAAS,MAAM,EAAE,CAOzG;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,GAAE,uBAA4B,GAAG,MAAM,CAK7H"}
|
package/dist/origins.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The normalized origin (`https://example.com`) when `value` is exactly `http(s)://host[:port]`, a trailing `/`
|
|
3
|
+
* allowed; null for anything else: a path, query, fragment, credentials, spaces or another scheme.
|
|
4
|
+
*/
|
|
5
|
+
export function parseOrigin(value) {
|
|
6
|
+
const candidate = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
7
|
+
// Checked on the raw text: `new URL` alone would accept and drop a path, a query or credentials.
|
|
8
|
+
if (!/^https?:\/\/[^/\\?#@\s]+$/i.test(candidate))
|
|
9
|
+
return null;
|
|
10
|
+
try {
|
|
11
|
+
return new URL(candidate).origin;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The host the request was sent to: the first `X-Forwarded-Host` value when `forwardedHost` is set, else the first
|
|
19
|
+
* `Host` value, else the URL's host; trimmed and lowercased.
|
|
20
|
+
*/
|
|
21
|
+
export function readRequestHost(request, options = {}) {
|
|
22
|
+
const forwarded = options.forwardedHost === true ? readFirstValue(request.headers.get("x-forwarded-host")) : null;
|
|
23
|
+
return forwarded ?? readFirstValue(request.headers.get("host")) ?? new URL(request.url).host;
|
|
24
|
+
}
|
|
25
|
+
/** The first `X-Forwarded-Proto` value when it is `http` or `https` (any case), else null. */
|
|
26
|
+
export function readForwardedProto(request) {
|
|
27
|
+
const proto = readFirstValue(request.headers.get("x-forwarded-proto"));
|
|
28
|
+
return proto === "http" || proto === "https" ? proto : null;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The origin the request was sent to: the first `X-Forwarded-Proto` value when it is http or https (else the URL's
|
|
32
|
+
* scheme) and `readRequestHost`. Null when the host is no host (a path, credentials or spaces in the header).
|
|
33
|
+
*/
|
|
34
|
+
export function readRequestOrigin(request, options = {}) {
|
|
35
|
+
const scheme = readForwardedProto(request) ?? new URL(request.url).protocol.slice(0, -1);
|
|
36
|
+
return parseOrigin(`${scheme}://${readRequestHost(request, options)}`);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Every origin the app is served under: `appOrigin`, `config.origins.trustedOrigins`, then `extra`, without
|
|
40
|
+
* repeats. An `extra` entry that is not an http(s) origin is a setup bug and throws by name.
|
|
41
|
+
*/
|
|
42
|
+
export function getTrustedOrigins(config, extra = []) {
|
|
43
|
+
const listed = extra.map((entry) => {
|
|
44
|
+
const origin = parseOrigin(entry);
|
|
45
|
+
if (origin === null)
|
|
46
|
+
throw new Error(`@softure-ai/core: trusted origin "${entry.slice(0, 100)}" must be an http(s) origin such as https://example.com`);
|
|
47
|
+
return origin;
|
|
48
|
+
});
|
|
49
|
+
return [...new Set([config.appOrigin, ...config.origins.trustedOrigins, ...listed])];
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The app origin to build absolute URLs on for `request`:
|
|
53
|
+
* 1. the request's origin, read with `X-Forwarded-Host`, when it is one of `getTrustedOrigins`;
|
|
54
|
+
* 2. else, with `config.origins.trustRequestHost`, the request's origin read from `Host` (an image built once and
|
|
55
|
+
* served under other origins, behind a proxy that passes `Host` through and refuses hosts it does not serve);
|
|
56
|
+
* 3. else `config.appOrigin`.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveAppOrigin(config, request, options = {}) {
|
|
59
|
+
const forwarded = readRequestOrigin(request, { forwardedHost: true });
|
|
60
|
+
if (forwarded !== null && getTrustedOrigins(config, options.trustedOrigins).includes(forwarded))
|
|
61
|
+
return forwarded;
|
|
62
|
+
const requested = config.origins.trustRequestHost ? readRequestOrigin(request) : null;
|
|
63
|
+
return requested ?? config.appOrigin;
|
|
64
|
+
}
|
|
65
|
+
function readFirstValue(header) {
|
|
66
|
+
const value = header?.split(",")[0]?.trim().toLowerCase();
|
|
67
|
+
return value === undefined || value === "" ? null : value;
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=origins.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"origins.js","sourceRoot":"","sources":["../src/origins.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/calendar-day.ts
CHANGED
|
@@ -36,3 +36,107 @@ function createFormatter(timeZone: string): Intl.DateTimeFormat {
|
|
|
36
36
|
throw new RangeError(`toCalendarDay: "${timeZone}" is not an IANA time zone`, { cause: error });
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
+
|
|
40
|
+
// Arithmetic on calendar days (`YYYY-MM-DD`): civil days with no time zone, so a day is a day even
|
|
41
|
+
// when DST makes it 23 or 25 hours long. A day number counts days since 1970-01-01.
|
|
42
|
+
|
|
43
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
44
|
+
const MONTHS_PER_YEAR = 12;
|
|
45
|
+
const DAY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
46
|
+
|
|
47
|
+
interface CivilDate {
|
|
48
|
+
readonly year: number;
|
|
49
|
+
/** 0-based, as `Date` counts months. */
|
|
50
|
+
readonly month: number;
|
|
51
|
+
readonly day: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** What `addCalendarMonths` does with a day the target month lacks (31 January + 1 month). */
|
|
55
|
+
export type EndOfMonth = "clamp" | "overflow";
|
|
56
|
+
|
|
57
|
+
/** Whether `value` is a real calendar day written `YYYY-MM-DD` (`2026-02-29` and `2026-1-1` are not). */
|
|
58
|
+
export function isCalendarDay(value: unknown): value is string {
|
|
59
|
+
return typeof value === "string" && parseCivilDate(value) !== null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** `day` moved by `days` calendar days (negative goes back). Throws a `RangeError` for a malformed day or count. */
|
|
63
|
+
export function addCalendarDays(day: string, days: number): string {
|
|
64
|
+
const operation = "addCalendarDays";
|
|
65
|
+
assertWhole(operation, days, "days");
|
|
66
|
+
return fromDayNumber(operation, toDayNumber(operation, day) + days);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* `day` moved by `months` calendar months (negative goes back). A day the target month lacks is the
|
|
71
|
+
* month's last day with `endOfMonth: "clamp"` (the default: 31 January + 1 month = 28 or 29 February,
|
|
72
|
+
* as billing periods count), or runs on into the next month with `"overflow"` (3 March in 2026, as
|
|
73
|
+
* `Date` does). Throws a `RangeError` for a malformed day or count.
|
|
74
|
+
*/
|
|
75
|
+
export function addCalendarMonths(day: string, months: number, options: { readonly endOfMonth?: EndOfMonth } = {}): string {
|
|
76
|
+
const operation = "addCalendarMonths";
|
|
77
|
+
assertWhole(operation, months, "months");
|
|
78
|
+
const date = readCivilDate(operation, day);
|
|
79
|
+
const month = date.month + months;
|
|
80
|
+
const lastDay = new Date(utcMilliseconds({ year: date.year, month: month + 1, day: 0 })).getUTCDate();
|
|
81
|
+
const target = options.endOfMonth === "overflow" ? date.day : Math.min(date.day, lastDay);
|
|
82
|
+
return fromDayNumber(operation, utcMilliseconds({ year: date.year, month, day: target }) / DAY_MS);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Calendar days from `from` to `to`: 1 from one day to the next, negative when `to` comes first. */
|
|
86
|
+
export function calendarDaysBetween(from: string, to: string): number {
|
|
87
|
+
const operation = "calendarDaysBetween";
|
|
88
|
+
return toDayNumber(operation, to) - toDayNumber(operation, from);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whole months from `from` to `to`: the most months `n` for which `addCalendarMonths(from, n)` (clamped)
|
|
93
|
+
* is not after `to`. So 31 January to 28 February is one month, as a monthly period counts it.
|
|
94
|
+
* Negative when `to` comes first: `wholeMonthsBetween(to, from)` with the sign turned.
|
|
95
|
+
*/
|
|
96
|
+
export function wholeMonthsBetween(from: string, to: string): number {
|
|
97
|
+
const operation = "wholeMonthsBetween";
|
|
98
|
+
const start = readCivilDate(operation, from);
|
|
99
|
+
const end = readCivilDate(operation, to);
|
|
100
|
+
if (to < from) return -wholeMonthsBetween(to, from);
|
|
101
|
+
const months = (end.year - start.year) * MONTHS_PER_YEAR + (end.month - start.month);
|
|
102
|
+
return addCalendarMonths(from, months) > to ? months - 1 : months;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertWhole(operation: string, count: number, unit: string): void {
|
|
106
|
+
if (!Number.isSafeInteger(count)) throw new RangeError(`${operation}: ${String(count)} is not a whole number of ${unit}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Milliseconds of UTC midnight of a civil date; month and day may overflow, as in `Date.UTC`, for any year. */
|
|
110
|
+
function utcMilliseconds({ year, month, day }: CivilDate): number {
|
|
111
|
+
// Not `Date.UTC`: it reads years 0-99 as 1900-1999.
|
|
112
|
+
const date = new Date(0);
|
|
113
|
+
date.setUTCFullYear(year, month, day);
|
|
114
|
+
return date.getTime();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function parseCivilDate(text: string): CivilDate | null {
|
|
118
|
+
const match = DAY_PATTERN.exec(text);
|
|
119
|
+
if (match === null) return null;
|
|
120
|
+
const date = { year: Number(match[1]), month: Number(match[2]) - 1, day: Number(match[3]) };
|
|
121
|
+
const parsed = new Date(utcMilliseconds(date));
|
|
122
|
+
if (parsed.getUTCMonth() !== date.month || parsed.getUTCDate() !== date.day) return null;
|
|
123
|
+
return date;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readCivilDate(operation: string, day: string): CivilDate {
|
|
127
|
+
const date = parseCivilDate(day);
|
|
128
|
+
if (date === null) throw new RangeError(`${operation}: "${day}" is not a YYYY-MM-DD calendar day`);
|
|
129
|
+
return date;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function toDayNumber(operation: string, day: string): number {
|
|
133
|
+
return utcMilliseconds(readCivilDate(operation, day)) / DAY_MS;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function fromDayNumber(operation: string, dayNumber: number): string {
|
|
137
|
+
const date = new Date(dayNumber * DAY_MS);
|
|
138
|
+
const year = date.getUTCFullYear();
|
|
139
|
+
if (year < 0 || year > 9999) throw new RangeError(`${operation}: the result falls outside the years 0000-9999`);
|
|
140
|
+
const pad = (value: number): string => String(value).padStart(2, "0");
|
|
141
|
+
return `${String(year).padStart(4, "0")}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
|
|
142
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -33,6 +33,25 @@ export interface SoftureDatabaseConfig {
|
|
|
33
33
|
readonly handle?: () => MaybePromise<SoftureDatabaseHandle>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/** The app's origins besides `appOrigin` and how far the request's own host is trusted (core `resolveAppOrigin`). */
|
|
37
|
+
export interface SoftureOrigins {
|
|
38
|
+
/**
|
|
39
|
+
* Other origins the app is served under (an apex beside `app.`, a second domain): absolute URLs for a request sent
|
|
40
|
+
* to one of them stay on it. At most 16.
|
|
41
|
+
*/
|
|
42
|
+
readonly trustedOrigins: readonly string[];
|
|
43
|
+
/**
|
|
44
|
+
* Trust the request's `Host` (and `X-Forwarded-Proto`) for any host, not only listed ones: for an image built once
|
|
45
|
+
* and served under origins nobody lists, behind a proxy that refuses hosts it does not serve. Default false.
|
|
46
|
+
*/
|
|
47
|
+
readonly trustRequestHost: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SoftureOriginsInput {
|
|
51
|
+
readonly trustedOrigins?: readonly string[];
|
|
52
|
+
readonly trustRequestHost?: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
36
55
|
export interface SoftureConfig {
|
|
37
56
|
/**
|
|
38
57
|
* `null` when the app has no database; required as soon as a module has a `dbSchema`. An empty `url`
|
|
@@ -44,6 +63,8 @@ export interface SoftureConfig {
|
|
|
44
63
|
readonly timezone: string;
|
|
45
64
|
/** Scheme, host and port of the app, without a path: `https://app.example.com`. */
|
|
46
65
|
readonly appOrigin: string;
|
|
66
|
+
/** Every origin the app is served under besides `appOrigin`, read by `resolveAppOrigin`. */
|
|
67
|
+
readonly origins: SoftureOrigins;
|
|
47
68
|
/** Enabled modules, in the order the app listed them. */
|
|
48
69
|
readonly modules: readonly AnySoftureModule[];
|
|
49
70
|
}
|
|
@@ -54,11 +75,14 @@ export interface SoftureConfigInput {
|
|
|
54
75
|
readonly locale: Locale;
|
|
55
76
|
readonly timezone: string;
|
|
56
77
|
readonly appOrigin: string;
|
|
78
|
+
readonly origins?: SoftureOriginsInput;
|
|
57
79
|
readonly modules: readonly AnySoftureModule[];
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
const CONFIG_SUBJECT = "softure.config (defineSoftureConfig)";
|
|
61
83
|
|
|
84
|
+
const ORIGIN_MESSAGE = "must be an http(s) origin without a path, e.g. https://app.example.com";
|
|
85
|
+
|
|
62
86
|
const configSchema = z.object({
|
|
63
87
|
// The URL is not checked for emptiness here: builds import the config without DATABASE_URL, so an empty URL is
|
|
64
88
|
// refused when something connects (`createDatabase` in @softure-ai/db), not when the config is defined.
|
|
@@ -73,7 +97,13 @@ const configSchema = z.object({
|
|
|
73
97
|
.default(null),
|
|
74
98
|
locale: z.enum(LOCALES),
|
|
75
99
|
timezone: z.string().refine(isTimeZone, "must be an IANA time zone, e.g. Europe/Warsaw"),
|
|
76
|
-
appOrigin: z.string().refine(isOrigin,
|
|
100
|
+
appOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE),
|
|
101
|
+
origins: z
|
|
102
|
+
.strictObject({
|
|
103
|
+
trustedOrigins: z.array(z.string().refine(isOrigin, ORIGIN_MESSAGE)).max(16, "takes at most 16 origins").default([]),
|
|
104
|
+
trustRequestHost: z.boolean().default(false),
|
|
105
|
+
})
|
|
106
|
+
.default({ trustedOrigins: [], trustRequestHost: false }),
|
|
77
107
|
modules: z.array(z.custom<AnySoftureModule>(isSoftureModule, "must be a module returned by a module factory")),
|
|
78
108
|
});
|
|
79
109
|
|
|
@@ -97,6 +127,7 @@ export function defineSoftureConfig(input: SoftureConfigInput): SoftureConfig {
|
|
|
97
127
|
return Object.freeze({
|
|
98
128
|
...config,
|
|
99
129
|
database: config.database === null ? null : Object.freeze({ ...config.database }),
|
|
130
|
+
origins: Object.freeze({ ...config.origins, trustedOrigins: Object.freeze([...config.origins.trustedOrigins]) }),
|
|
100
131
|
modules: Object.freeze([...config.modules]),
|
|
101
132
|
});
|
|
102
133
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// The minor unit pinned for each currency SOFTURE accepts: an amount in minor units (billing's
|
|
2
|
+
// `price.amount`, `formatMoney`) counts these units on every runtime, whatever the runtime's `Intl`
|
|
3
|
+
// (CLDR) says. Source: ISO 4217 List One, published 2024-06-25, without fund codes, `N.A.` units
|
|
4
|
+
// (metals, XDR, XTS, XXX ...) and UYW (a wage index unit), plus the two choices below. CLDR differs
|
|
5
|
+
// from ISO on AFN, ALL, IRR, KPW, LAK, LBP, MMK, RSD, SOS, SYP and YER (0 instead of 2) and IQD
|
|
6
|
+
// (0 instead of 3), and its HUF and TWD digits have changed between Node builds; the table follows
|
|
7
|
+
// ISO, as Stripe does for all but MGA.
|
|
8
|
+
|
|
9
|
+
/** ISO 4217 codes by the digits of their minor unit. */
|
|
10
|
+
const CODES_BY_DIGITS: Readonly<Record<0 | 2 | 3, readonly string[]>> = {
|
|
11
|
+
0: [
|
|
12
|
+
"BIF", "CLP", "DJF", "GNF", "ISK", "JPY", "KMF", "KRW", "PYG", "RWF", "UGX", "VND", "VUV", "XAF", "XOF", "XPF",
|
|
13
|
+
// Override: ISO writes 2, but one ariary is five iraimbilanja (not a decimal subunit), and CLDR
|
|
14
|
+
// and Stripe both count it without a minor unit.
|
|
15
|
+
"MGA",
|
|
16
|
+
],
|
|
17
|
+
2: [
|
|
18
|
+
"AED", "AFN", "ALL", "AMD", "ANG", "AOA", "ARS", "AUD", "AWG", "AZN", "BAM", "BBD", "BDT", "BGN", "BMD", "BND",
|
|
19
|
+
"BOB", "BRL", "BSD", "BTN", "BWP", "BYN", "BZD", "CAD", "CDF", "CHF", "CNY", "COP", "CRC", "CUC", "CUP", "CVE",
|
|
20
|
+
"CZK", "DKK", "DOP", "DZD", "EGP", "ERN", "ETB", "EUR", "FJD", "FKP", "GBP", "GEL", "GHS", "GIP", "GMD", "GTQ",
|
|
21
|
+
"GYD", "HKD", "HNL", "HTG", "HUF", "IDR", "ILS", "INR", "IRR", "JMD", "KES", "KGS", "KHR", "KPW", "KYD", "KZT",
|
|
22
|
+
"LAK", "LBP", "LKR", "LRD", "LSL", "MAD", "MDL", "MKD", "MMK", "MNT", "MOP", "MRU", "MUR", "MVR", "MWK", "MXN",
|
|
23
|
+
"MYR", "MZN", "NAD", "NGN", "NIO", "NOK", "NPR", "NZD", "PAB", "PEN", "PGK", "PHP", "PKR", "PLN", "QAR", "RON",
|
|
24
|
+
"RSD", "RUB", "SAR", "SBD", "SCR", "SDG", "SEK", "SGD", "SHP", "SLE", "SOS", "SRD", "SSP", "STN", "SVC", "SYP",
|
|
25
|
+
"SZL", "THB", "TJS", "TMT", "TOP", "TRY", "TTD", "TWD", "TZS", "UAH", "USD", "UYU", "UZS", "VED", "VES", "WST",
|
|
26
|
+
"XCD", "YER", "ZAR", "ZMW", "ZWG",
|
|
27
|
+
// Addition: the Caribbean guilder, which replaced ANG on 2025-03-31, after the pinned list.
|
|
28
|
+
"XCG",
|
|
29
|
+
],
|
|
30
|
+
3: ["BHD", "IQD", "JOD", "KWD", "LYD", "OMR", "TND"],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Digits of the minor unit per accepted currency code, e.g. `PLN: 2`, `JPY: 0`, `KWD: 3`. */
|
|
34
|
+
export const CURRENCY_MINOR_UNIT_DIGITS: Readonly<Record<string, number>> = Object.freeze(
|
|
35
|
+
Object.fromEntries(
|
|
36
|
+
Object.entries(CODES_BY_DIGITS).flatMap(([digits, codes]) => codes.map((code) => [code, Number(digits)] as const)),
|
|
37
|
+
),
|
|
38
|
+
);
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Days, money and percentages as people read them, one way for every module and app. A calendar day
|
|
2
|
+
// is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it.
|
|
3
|
+
import { isCalendarDay } from "./calendar-day.js";
|
|
4
|
+
import { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
5
|
+
import type { Locale } from "./i18n.js";
|
|
6
|
+
|
|
7
|
+
/** `long`: October 4, 2026 (pl names the month in the genitive); `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (pl: 04.10.2026). */
|
|
8
|
+
export type CalendarDayStyle = "long" | "medium" | "numeric";
|
|
9
|
+
|
|
10
|
+
export interface FormatMoneyOptions {
|
|
11
|
+
/** Whole units only, rounded half away from zero: PLN 1,235 for 1234.56. */
|
|
12
|
+
readonly rounded?: boolean;
|
|
13
|
+
/** A `+` before a positive amount (a `-` before a negative one is always there); zero has neither. */
|
|
14
|
+
readonly signed?: boolean;
|
|
15
|
+
/** The locale's short notation for large amounts: PLN 1.2M. */
|
|
16
|
+
readonly compact?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const BASIS_POINTS_PER_UNIT = 10_000;
|
|
20
|
+
const MAX_PERCENT_FRACTION_DIGITS = 2;
|
|
21
|
+
|
|
22
|
+
const dayFormatters = new Map<string, Intl.DateTimeFormat>();
|
|
23
|
+
const numberFormatters = new Map<string, Intl.NumberFormat>();
|
|
24
|
+
|
|
25
|
+
const DAY_STYLE_OPTIONS: Readonly<Record<CalendarDayStyle, Intl.DateTimeFormatOptions>> = {
|
|
26
|
+
long: { dateStyle: "long" },
|
|
27
|
+
medium: { dateStyle: "medium" },
|
|
28
|
+
numeric: { day: "2-digit", month: "2-digit", year: "numeric" },
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** A `YYYY-MM-DD` day as `locale` writes it. Throws a `RangeError` for a malformed day. */
|
|
32
|
+
export function formatCalendarDay(day: string, locale: Locale, style: CalendarDayStyle = "long"): string {
|
|
33
|
+
if (!isCalendarDay(day)) throw new RangeError(`formatCalendarDay: "${String(day)}" is not a YYYY-MM-DD calendar day`);
|
|
34
|
+
const key = `${locale}:${style}`;
|
|
35
|
+
let format = dayFormatters.get(key);
|
|
36
|
+
if (format === undefined) {
|
|
37
|
+
format = new Intl.DateTimeFormat(locale, { ...DAY_STYLE_OPTIONS[style], timeZone: "UTC" });
|
|
38
|
+
dayFormatters.set(key, format);
|
|
39
|
+
}
|
|
40
|
+
return format.format(new Date(`${day}T00:00:00Z`));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
|
|
45
|
+
* grouped (pl writes 1 234,56 where its default would leave four digits together). The minor unit is the pinned ISO
|
|
46
|
+
* 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
|
|
47
|
+
* `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
|
|
48
|
+
*/
|
|
49
|
+
export function formatMoney(minor: number, currency: string, locale: Locale, options: FormatMoneyOptions = {}): string {
|
|
50
|
+
if (!Number.isSafeInteger(minor)) throw new RangeError(`formatMoney: ${String(minor)} is not a whole number of minor units`);
|
|
51
|
+
const pinned = Object.hasOwn(CURRENCY_MINOR_UNIT_DIGITS, currency) ? CURRENCY_MINOR_UNIT_DIGITS[currency] : undefined;
|
|
52
|
+
const digits = pinned ?? getNumberFormatter(locale, { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
|
|
53
|
+
const shownDigits = options.rounded === true ? 0 : digits;
|
|
54
|
+
const format = getNumberFormatter(locale, {
|
|
55
|
+
style: "currency",
|
|
56
|
+
currency,
|
|
57
|
+
useGrouping: "always",
|
|
58
|
+
signDisplay: options.signed === true ? "exceptZero" : "auto",
|
|
59
|
+
...(options.compact === true ? { notation: "compact" } : { minimumFractionDigits: shownDigits, maximumFractionDigits: shownDigits }),
|
|
60
|
+
});
|
|
61
|
+
return format.format(minor / 10 ** digits);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits. Throws a `RangeError` for a non-finite value. */
|
|
65
|
+
export function formatPercent(basisPoints: number, locale: Locale): string {
|
|
66
|
+
if (!Number.isFinite(basisPoints)) throw new RangeError(`formatPercent: ${String(basisPoints)} is not a finite number of basis points`);
|
|
67
|
+
const format = getNumberFormatter(locale, { style: "percent", maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS });
|
|
68
|
+
return format.format(basisPoints / BASIS_POINTS_PER_UNIT);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getNumberFormatter(locale: Locale, options: Intl.NumberFormatOptions): Intl.NumberFormat {
|
|
72
|
+
const key = `${locale}:${JSON.stringify(options)}`;
|
|
73
|
+
let format = numberFormatters.get(key);
|
|
74
|
+
if (format === undefined) {
|
|
75
|
+
format = new Intl.NumberFormat(locale, options);
|
|
76
|
+
numberFormatters.set(key, format);
|
|
77
|
+
}
|
|
78
|
+
return format;
|
|
79
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// Public API of @softure-ai/core: the contract every SOFTURE module stands on
|
|
2
2
|
// (docs/02-module-standard.md). The Next.js registry is a separate entry: `@softure-ai/core/next`.
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
addCalendarDays,
|
|
5
|
+
addCalendarMonths,
|
|
6
|
+
calendarDaysBetween,
|
|
7
|
+
getCalendarDay,
|
|
8
|
+
isCalendarDay,
|
|
9
|
+
toCalendarDay,
|
|
10
|
+
wholeMonthsBetween,
|
|
11
|
+
type EndOfMonth,
|
|
12
|
+
} from "./calendar-day.js";
|
|
13
|
+
export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
|
|
14
|
+
export { formatCalendarDay, formatMoney, formatPercent, type CalendarDayStyle, type FormatMoneyOptions } from "./format.js";
|
|
4
15
|
export { createTestClock, systemClock, type Clock, type TestClock } from "./clock.js";
|
|
5
16
|
export {
|
|
6
17
|
formatMessage,
|
|
@@ -28,6 +39,8 @@ export {
|
|
|
28
39
|
type SoftureDatabaseConfig,
|
|
29
40
|
type SoftureDatabaseHandle,
|
|
30
41
|
type SoftureDatabaseHandleTypes,
|
|
42
|
+
type SoftureOrigins,
|
|
43
|
+
type SoftureOriginsInput,
|
|
31
44
|
} from "./config.js";
|
|
32
45
|
export { SoftureConfigError } from "./config-error.js";
|
|
33
46
|
export { withDatabaseOptional } from "./database-requirement.js";
|
|
@@ -46,6 +59,17 @@ export {
|
|
|
46
59
|
type PrivacyContributor,
|
|
47
60
|
type SoftureModule,
|
|
48
61
|
} from "./module.js";
|
|
62
|
+
export {
|
|
63
|
+
getTrustedOrigins,
|
|
64
|
+
parseOrigin,
|
|
65
|
+
readForwardedProto,
|
|
66
|
+
readRequestHost,
|
|
67
|
+
readRequestOrigin,
|
|
68
|
+
resolveAppOrigin,
|
|
69
|
+
type OriginRequest,
|
|
70
|
+
type ReadOriginOptions,
|
|
71
|
+
type ResolveAppOriginOptions,
|
|
72
|
+
} from "./origins.js";
|
|
49
73
|
export { findSiteUrlProvider, getSiteUrls, type SiteUrlProvider, type SiteUrls } from "./site-urls.js";
|
|
50
74
|
export { findSwitchReader, readSwitch, type SwitchReader, type SwitchReading } from "./switches.js";
|
|
51
75
|
export { isVersion, parseVersionRange, satisfiesRange, type Version, type VersionRange } from "./version-range.js";
|
package/src/origins.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// The public origin of a request, one rule for every module (issue #311). Behind a proxy `request.url` names the
|
|
2
|
+
// server's own listening address (`http://0.0.0.0:3000`), so the origin is read from headers: the scheme from the
|
|
3
|
+
// first `X-Forwarded-Proto` value when it is http or https, the host from `Host`. `X-Forwarded-Host` is read only
|
|
4
|
+
// where a list of the app's origins gates the result: a client can send it through a proxy that keeps it.
|
|
5
|
+
import type { SoftureConfig } from "./config.js";
|
|
6
|
+
|
|
7
|
+
/** What the origin helpers read from a request: a Web `Request`, or Next's `headers()` with a URL. */
|
|
8
|
+
export interface OriginRequest {
|
|
9
|
+
readonly url: string;
|
|
10
|
+
readonly headers: { get(name: string): string | null };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ReadOriginOptions {
|
|
14
|
+
/** Read the first `X-Forwarded-Host` value before `Host`. Only for a result checked against a list of origins. */
|
|
15
|
+
readonly forwardedHost?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ResolveAppOriginOptions {
|
|
19
|
+
/** Origins trusted besides `appOrigin` and `config.origins.trustedOrigins`, e.g. a module's own option. */
|
|
20
|
+
readonly trustedOrigins?: readonly string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The normalized origin (`https://example.com`) when `value` is exactly `http(s)://host[:port]`, a trailing `/`
|
|
25
|
+
* allowed; null for anything else: a path, query, fragment, credentials, spaces or another scheme.
|
|
26
|
+
*/
|
|
27
|
+
export function parseOrigin(value: string): string | null {
|
|
28
|
+
const candidate = value.endsWith("/") ? value.slice(0, -1) : value;
|
|
29
|
+
// Checked on the raw text: `new URL` alone would accept and drop a path, a query or credentials.
|
|
30
|
+
if (!/^https?:\/\/[^/\\?#@\s]+$/i.test(candidate)) return null;
|
|
31
|
+
try {
|
|
32
|
+
return new URL(candidate).origin;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The host the request was sent to: the first `X-Forwarded-Host` value when `forwardedHost` is set, else the first
|
|
40
|
+
* `Host` value, else the URL's host; trimmed and lowercased.
|
|
41
|
+
*/
|
|
42
|
+
export function readRequestHost(request: OriginRequest, options: ReadOriginOptions = {}): string {
|
|
43
|
+
const forwarded = options.forwardedHost === true ? readFirstValue(request.headers.get("x-forwarded-host")) : null;
|
|
44
|
+
return forwarded ?? readFirstValue(request.headers.get("host")) ?? new URL(request.url).host;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The first `X-Forwarded-Proto` value when it is `http` or `https` (any case), else null. */
|
|
48
|
+
export function readForwardedProto(request: OriginRequest): "http" | "https" | null {
|
|
49
|
+
const proto = readFirstValue(request.headers.get("x-forwarded-proto"));
|
|
50
|
+
return proto === "http" || proto === "https" ? proto : null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The origin the request was sent to: the first `X-Forwarded-Proto` value when it is http or https (else the URL's
|
|
55
|
+
* scheme) and `readRequestHost`. Null when the host is no host (a path, credentials or spaces in the header).
|
|
56
|
+
*/
|
|
57
|
+
export function readRequestOrigin(request: OriginRequest, options: ReadOriginOptions = {}): string | null {
|
|
58
|
+
const scheme = readForwardedProto(request) ?? new URL(request.url).protocol.slice(0, -1);
|
|
59
|
+
return parseOrigin(`${scheme}://${readRequestHost(request, options)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Every origin the app is served under: `appOrigin`, `config.origins.trustedOrigins`, then `extra`, without
|
|
64
|
+
* repeats. An `extra` entry that is not an http(s) origin is a setup bug and throws by name.
|
|
65
|
+
*/
|
|
66
|
+
export function getTrustedOrigins(config: SoftureConfig, extra: readonly string[] = []): readonly string[] {
|
|
67
|
+
const listed = extra.map((entry) => {
|
|
68
|
+
const origin = parseOrigin(entry);
|
|
69
|
+
if (origin === null) throw new Error(`@softure-ai/core: trusted origin "${entry.slice(0, 100)}" must be an http(s) origin such as https://example.com`);
|
|
70
|
+
return origin;
|
|
71
|
+
});
|
|
72
|
+
return [...new Set([config.appOrigin, ...config.origins.trustedOrigins, ...listed])];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The app origin to build absolute URLs on for `request`:
|
|
77
|
+
* 1. the request's origin, read with `X-Forwarded-Host`, when it is one of `getTrustedOrigins`;
|
|
78
|
+
* 2. else, with `config.origins.trustRequestHost`, the request's origin read from `Host` (an image built once and
|
|
79
|
+
* served under other origins, behind a proxy that passes `Host` through and refuses hosts it does not serve);
|
|
80
|
+
* 3. else `config.appOrigin`.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveAppOrigin(config: SoftureConfig, request: OriginRequest, options: ResolveAppOriginOptions = {}): string {
|
|
83
|
+
const forwarded = readRequestOrigin(request, { forwardedHost: true });
|
|
84
|
+
if (forwarded !== null && getTrustedOrigins(config, options.trustedOrigins).includes(forwarded)) return forwarded;
|
|
85
|
+
const requested = config.origins.trustRequestHost ? readRequestOrigin(request) : null;
|
|
86
|
+
return requested ?? config.appOrigin;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readFirstValue(header: string | null): string | null {
|
|
90
|
+
const value = header?.split(",")[0]?.trim().toLowerCase();
|
|
91
|
+
return value === undefined || value === "" ? null : value;
|
|
92
|
+
}
|