@softure-ai/core 0.1.7 → 0.1.8
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 +8 -0
- package/README.md +35 -0
- 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/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- 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/config.ts +32 -1
- package/src/index.ts +13 -0
- package/src/origins.ts +92 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ 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.8
|
|
8
|
+
|
|
9
|
+
- Request origins (#311): `readRequestHost`, `readRequestOrigin`, `readForwardedProto`, `parseOrigin`,
|
|
10
|
+
`getTrustedOrigins` and `resolveAppOrigin(config, request, { trustedOrigins })`, one rule for every module that
|
|
11
|
+
builds an absolute URL for a request (README, "Request origins"). `X-Forwarded-Host` only ever picks a listed origin.
|
|
12
|
+
- The config takes `origins: { trustedOrigins, trustRequestHost }` (both optional; defaults `[]` and `false`), read by
|
|
13
|
+
auth, agent-ready, mcp-access and analytics. `SoftureConfig.origins` is always filled by `defineSoftureConfig`.
|
|
14
|
+
|
|
7
15
|
## 0.1.7
|
|
8
16
|
|
|
9
17
|
- `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
|
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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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,11 +4,12 @@ export { formatMessage, getMessage, isLocale, LOCALES, mergeMessages, selectPlur
|
|
|
4
4
|
export { coreMessages, type CoreMessages } from "./messages/index.js";
|
|
5
5
|
export { err, ok, type Err, type ErrorCode, type Ok, type Result } from "./result.js";
|
|
6
6
|
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";
|
|
7
|
+
export { defineSoftureConfig, getModule, sortModulesByDependencies, type SoftureConfig, type SoftureConfigInput, type SoftureDatabaseConfig, type SoftureDatabaseHandle, type SoftureDatabaseHandleTypes, type SoftureOrigins, type SoftureOriginsInput, } from "./config.js";
|
|
8
8
|
export { SoftureConfigError } from "./config-error.js";
|
|
9
9
|
export { withDatabaseOptional } from "./database-requirement.js";
|
|
10
10
|
export { moduleManifestSchema, type ModuleManifest } from "./manifest.js";
|
|
11
11
|
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";
|
|
12
|
+
export { getTrustedOrigins, parseOrigin, readForwardedProto, readRequestHost, readRequestOrigin, resolveAppOrigin, type OriginRequest, type ReadOriginOptions, type ResolveAppOriginOptions, } from "./origins.js";
|
|
12
13
|
export { findSiteUrlProvider, getSiteUrls, type SiteUrlProvider, type SiteUrls } from "./site-urls.js";
|
|
13
14
|
export { findSwitchReader, readSwitch, type SwitchReader, type SwitchReading } from "./switches.js";
|
|
14
15
|
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,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,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,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,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
|
@@ -11,6 +11,7 @@ export { SoftureConfigError } from "./config-error.js";
|
|
|
11
11
|
export { withDatabaseOptional } from "./database-requirement.js";
|
|
12
12
|
export { moduleManifestSchema } from "./manifest.js";
|
|
13
13
|
export { defineModule, resolveMigrationsDir, toModuleJson, } from "./module.js";
|
|
14
|
+
export { getTrustedOrigins, parseOrigin, readForwardedProto, readRequestHost, readRequestOrigin, resolveAppOrigin, } from "./origins.js";
|
|
14
15
|
export { findSiteUrlProvider, getSiteUrls } from "./site-urls.js";
|
|
15
16
|
export { findSwitchReader, readSwitch } from "./switches.js";
|
|
16
17
|
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,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,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,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClE,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/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
|
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,8 @@ export {
|
|
|
28
28
|
type SoftureDatabaseConfig,
|
|
29
29
|
type SoftureDatabaseHandle,
|
|
30
30
|
type SoftureDatabaseHandleTypes,
|
|
31
|
+
type SoftureOrigins,
|
|
32
|
+
type SoftureOriginsInput,
|
|
31
33
|
} from "./config.js";
|
|
32
34
|
export { SoftureConfigError } from "./config-error.js";
|
|
33
35
|
export { withDatabaseOptional } from "./database-requirement.js";
|
|
@@ -46,6 +48,17 @@ export {
|
|
|
46
48
|
type PrivacyContributor,
|
|
47
49
|
type SoftureModule,
|
|
48
50
|
} from "./module.js";
|
|
51
|
+
export {
|
|
52
|
+
getTrustedOrigins,
|
|
53
|
+
parseOrigin,
|
|
54
|
+
readForwardedProto,
|
|
55
|
+
readRequestHost,
|
|
56
|
+
readRequestOrigin,
|
|
57
|
+
resolveAppOrigin,
|
|
58
|
+
type OriginRequest,
|
|
59
|
+
type ReadOriginOptions,
|
|
60
|
+
type ResolveAppOriginOptions,
|
|
61
|
+
} from "./origins.js";
|
|
49
62
|
export { findSiteUrlProvider, getSiteUrls, type SiteUrlProvider, type SiteUrls } from "./site-urls.js";
|
|
50
63
|
export { findSwitchReader, readSwitch, type SwitchReader, type SwitchReading } from "./switches.js";
|
|
51
64
|
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
|
+
}
|