tina4-nodejs 3.13.103 → 3.13.104
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/CLAUDE.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +590 -19
- package/packages/core/dist/index.js +593 -19
- package/packages/core/src/authGate.ts +6 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/server.ts +5 -0
- package/packages/core/src/sso.ts +285 -0
- package/packages/orm/dist/index.js +598 -24
- package/packages/orm/src/adapters/postgres.ts +2 -0
- package/packages/orm/src/baseModel.ts +45 -5
- package/packages/orm/src/index.ts +2 -0
- package/packages/orm/src/point.ts +105 -0
- package/packages/orm/src/queryBuilder.ts +66 -5
- package/packages/orm/src/sqlTranslator.ts +63 -0
- package/packages/orm/src/types.ts +5 -1
- package/packages/swagger/dist/index.js +11 -1
- package/packages/swagger/src/generator.ts +11 -1
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/sso.d.ts +55 -0
- package/types/orm/src/baseModel.d.ts +3 -0
- package/types/orm/src/index.d.ts +2 -0
- package/types/orm/src/point.d.ts +24 -0
- package/types/orm/src/queryBuilder.d.ts +10 -1
- package/types/orm/src/sqlTranslator.d.ts +13 -0
- package/types/orm/src/types.d.ts +5 -1
|
@@ -582,13 +582,172 @@ var init_databaseUrl = __esm({
|
|
|
582
582
|
}
|
|
583
583
|
});
|
|
584
584
|
|
|
585
|
+
// src/point.ts
|
|
586
|
+
function formatCoordinate(value) {
|
|
587
|
+
return Object.is(value, -0) ? "0" : Number(value.toPrecision(15)).toString();
|
|
588
|
+
}
|
|
589
|
+
var DEFAULT_SRID, SpatialNotSupportedError, Point;
|
|
590
|
+
var init_point = __esm({
|
|
591
|
+
"src/point.ts"() {
|
|
592
|
+
"use strict";
|
|
593
|
+
DEFAULT_SRID = 4326;
|
|
594
|
+
SpatialNotSupportedError = class extends Error {
|
|
595
|
+
constructor(message) {
|
|
596
|
+
super(message);
|
|
597
|
+
this.name = "SpatialNotSupportedError";
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
Point = class _Point {
|
|
601
|
+
lon;
|
|
602
|
+
lat;
|
|
603
|
+
srid;
|
|
604
|
+
constructor(lon, lat, srid = DEFAULT_SRID) {
|
|
605
|
+
if (typeof lon === "boolean" || typeof lat === "boolean" || typeof srid === "boolean") {
|
|
606
|
+
throw new TypeError("Point longitude, latitude and SRID must be numbers");
|
|
607
|
+
}
|
|
608
|
+
this.lon = Number(lon);
|
|
609
|
+
this.lat = Number(lat);
|
|
610
|
+
this.srid = Number(srid);
|
|
611
|
+
if (!Number.isFinite(this.lon) || !Number.isFinite(this.lat) || !Number.isInteger(this.srid)) {
|
|
612
|
+
throw new TypeError("Point longitude and latitude must be finite numbers and SRID must be an integer");
|
|
613
|
+
}
|
|
614
|
+
if (this.srid === DEFAULT_SRID) {
|
|
615
|
+
if (this.lon < -180 || this.lon > 180) throw new RangeError(`Point longitude ${this.lon} is outside -180..180; Tina4 uses longitude, latitude order`);
|
|
616
|
+
if (this.lat < -90 || this.lat > 90) throw new RangeError(`Point latitude ${this.lat} is outside -90..90; Tina4 uses longitude, latitude order`);
|
|
617
|
+
}
|
|
618
|
+
Object.freeze(this);
|
|
619
|
+
}
|
|
620
|
+
get wkt() {
|
|
621
|
+
return `POINT(${formatCoordinate(this.lon)} ${formatCoordinate(this.lat)})`;
|
|
622
|
+
}
|
|
623
|
+
get ewkt() {
|
|
624
|
+
return `SRID=${this.srid};${this.wkt}`;
|
|
625
|
+
}
|
|
626
|
+
get geojson() {
|
|
627
|
+
return { type: "Point", coordinates: [this.lon, this.lat] };
|
|
628
|
+
}
|
|
629
|
+
toJSON() {
|
|
630
|
+
return this.geojson;
|
|
631
|
+
}
|
|
632
|
+
toArray() {
|
|
633
|
+
return [this.lon, this.lat];
|
|
634
|
+
}
|
|
635
|
+
static parse(value, srid = DEFAULT_SRID) {
|
|
636
|
+
if (value instanceof _Point) return value;
|
|
637
|
+
if (Array.isArray(value)) {
|
|
638
|
+
if (value.length < 2) throw new TypeError("Point coordinate pair needs longitude and latitude");
|
|
639
|
+
return new _Point(value[0], value[1], srid);
|
|
640
|
+
}
|
|
641
|
+
if (value && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
642
|
+
return _Point.fromGeoJson(value, srid);
|
|
643
|
+
}
|
|
644
|
+
if (value instanceof Uint8Array) return _Point.fromWkb(value, srid);
|
|
645
|
+
if (typeof value === "string") {
|
|
646
|
+
const text = value.trim();
|
|
647
|
+
const match = /^(?:SRID\s*=\s*(\d+)\s*;\s*)?POINT\s*(?:Z|M|ZM)?\s*\(\s*([-+0-9.eE]+)\s+([-+0-9.eE]+)(?:\s+[-+0-9.eE]+)*\s*\)$/i.exec(text);
|
|
648
|
+
if (match) return new _Point(match[2], match[3], match[1] ? Number(match[1]) : srid);
|
|
649
|
+
if (text.length >= 42 && text.length % 2 === 0 && /^[0-9a-f]+$/i.test(text)) {
|
|
650
|
+
return _Point.fromWkb(Uint8Array.from(Buffer.from(text, "hex")), srid);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
throw new TypeError("Point must be Point, [longitude, latitude], WKT/EWKT, GeoJSON or WKB/EWKB");
|
|
654
|
+
}
|
|
655
|
+
static geometryBinding(value, srid = DEFAULT_SRID) {
|
|
656
|
+
if (value instanceof _Point || Array.isArray(value)) return [_Point.parse(value, srid).ewkt, "ewkt"];
|
|
657
|
+
if (value && typeof value === "object") {
|
|
658
|
+
const candidate = value;
|
|
659
|
+
const geometry = String(candidate.type).toLowerCase() === "feature" ? candidate.geometry : candidate;
|
|
660
|
+
const allowed = /* @__PURE__ */ new Set(["point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection"]);
|
|
661
|
+
if (!geometry || !allowed.has(String(geometry.type).toLowerCase())) throw new TypeError("GeoJSON geometry has an unsupported type");
|
|
662
|
+
return [JSON.stringify(geometry), "geojson"];
|
|
663
|
+
}
|
|
664
|
+
if (typeof value === "string" && /^\s*(?:SRID\s*=\s*\d+\s*;\s*)?(?:POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)\b/i.test(value)) {
|
|
665
|
+
return [/^\s*SRID/i.test(value) ? value.trim() : `SRID=${srid};${value.trim()}`, "ewkt"];
|
|
666
|
+
}
|
|
667
|
+
throw new TypeError("Geometry must be Point, coordinate pair, WKT/EWKT or GeoJSON");
|
|
668
|
+
}
|
|
669
|
+
static fromGeoJson(data, srid) {
|
|
670
|
+
const geometry = String(data.type).toLowerCase() === "feature" ? data.geometry : data;
|
|
671
|
+
if (!geometry || String(geometry.type).toLowerCase() !== "point") throw new TypeError("Point GeoJSON type must be Point");
|
|
672
|
+
const coordinates = geometry.coordinates;
|
|
673
|
+
if (!Array.isArray(coordinates) || coordinates.length < 2) throw new TypeError("Point GeoJSON coordinates must be [longitude, latitude]");
|
|
674
|
+
return new _Point(coordinates[0], coordinates[1], srid);
|
|
675
|
+
}
|
|
676
|
+
static fromWkb(raw, srid) {
|
|
677
|
+
if (raw.byteLength < 21) throw new TypeError("Point WKB is too short");
|
|
678
|
+
const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
679
|
+
const little = raw[0] === 1;
|
|
680
|
+
const typeWord = view.getUint32(1, little);
|
|
681
|
+
let offset = 5;
|
|
682
|
+
if ((typeWord & 536870912) !== 0) {
|
|
683
|
+
srid = view.getUint32(5, little);
|
|
684
|
+
offset = 9;
|
|
685
|
+
}
|
|
686
|
+
const code = (typeWord & ~(536870912 | 1073741824 | 2147483648)) % 1e3;
|
|
687
|
+
if (code !== 1 || raw.byteLength < offset + 16) throw new TypeError("WKB geometry is not a Point");
|
|
688
|
+
return new _Point(view.getFloat64(offset, little), view.getFloat64(offset + 8, little), srid);
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
585
694
|
// src/sqlTranslator.ts
|
|
586
695
|
var SQLTranslator, QueryCache;
|
|
587
696
|
var init_sqlTranslator = __esm({
|
|
588
697
|
"src/sqlTranslator.ts"() {
|
|
589
698
|
"use strict";
|
|
590
699
|
init_databaseUrl();
|
|
700
|
+
init_point();
|
|
591
701
|
SQLTranslator = class _SQLTranslator {
|
|
702
|
+
static SPATIAL_ENGINES = /* @__PURE__ */ new Set(["postgres", "postgresql"]);
|
|
703
|
+
static SPATIAL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
704
|
+
static requireSpatial(engine, feature) {
|
|
705
|
+
const name = String(engine || "unknown").toLowerCase();
|
|
706
|
+
if (!_SQLTranslator.SPATIAL_ENGINES.has(name)) {
|
|
707
|
+
throw new SpatialNotSupportedError(
|
|
708
|
+
`${feature} is not supported on the '${name}' database engine. Tina4 GIS support is PostGIS-first: use PostgreSQL with CREATE EXTENSION postgis. Tina4 will not replace a spatial query with an approximate coordinate query.`
|
|
709
|
+
);
|
|
710
|
+
}
|
|
711
|
+
return name;
|
|
712
|
+
}
|
|
713
|
+
static spatialIdentifier(name, what = "column") {
|
|
714
|
+
if (!_SQLTranslator.SPATIAL_IDENTIFIER.test(name)) throw new TypeError(`Spatial ${what} is not a valid SQL identifier: ${name}`);
|
|
715
|
+
return name;
|
|
716
|
+
}
|
|
717
|
+
static pointColumnType(engine, srid = DEFAULT_SRID) {
|
|
718
|
+
_SQLTranslator.requireSpatial(engine, "PointField");
|
|
719
|
+
return `geography(Point,${srid})`;
|
|
720
|
+
}
|
|
721
|
+
static spatialIndex(engine, table2, column2) {
|
|
722
|
+
_SQLTranslator.requireSpatial(engine, "spatial index creation");
|
|
723
|
+
table2 = _SQLTranslator.spatialIdentifier(table2, "table");
|
|
724
|
+
column2 = _SQLTranslator.spatialIdentifier(column2);
|
|
725
|
+
return `CREATE INDEX IF NOT EXISTS ${table2.replaceAll(".", "_")}_${column2}_gist ON ${table2} USING GIST (${column2})`;
|
|
726
|
+
}
|
|
727
|
+
static pointLiteral(engine, srid = DEFAULT_SRID) {
|
|
728
|
+
_SQLTranslator.requireSpatial(engine, "spatial predicates");
|
|
729
|
+
return `ST_SetSRID(ST_MakePoint(?, ?), ${srid})::geography`;
|
|
730
|
+
}
|
|
731
|
+
static withinDistance(engine, column2, srid = DEFAULT_SRID) {
|
|
732
|
+
return `ST_DWithin(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.pointLiteral(engine, srid)}, ?)`;
|
|
733
|
+
}
|
|
734
|
+
static distance(engine, column2, srid = DEFAULT_SRID) {
|
|
735
|
+
return `ST_Distance(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.pointLiteral(engine, srid)})`;
|
|
736
|
+
}
|
|
737
|
+
static distanceAs(engine, column2, alias, srid = DEFAULT_SRID) {
|
|
738
|
+
return `${_SQLTranslator.distance(engine, column2, srid)} AS ${_SQLTranslator.spatialIdentifier(alias, "result alias")}`;
|
|
739
|
+
}
|
|
740
|
+
static geometryLiteral(engine, form, srid = DEFAULT_SRID) {
|
|
741
|
+
_SQLTranslator.requireSpatial(engine, "spatial predicates");
|
|
742
|
+
return form === "ewkt" ? "ST_GeogFromText(?)" : `ST_SetSRID(ST_GeomFromGeoJSON(?), ${srid})::geography`;
|
|
743
|
+
}
|
|
744
|
+
static intersects(engine, column2, form = "ewkt", srid = DEFAULT_SRID) {
|
|
745
|
+
return `ST_Intersects(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.geometryLiteral(engine, form, srid)})`;
|
|
746
|
+
}
|
|
747
|
+
static bbox(engine, column2, srid = DEFAULT_SRID) {
|
|
748
|
+
_SQLTranslator.requireSpatial(engine, "bbox");
|
|
749
|
+
return `ST_Intersects(${_SQLTranslator.spatialIdentifier(column2)}, ST_MakeEnvelope(?, ?, ?, ?, ${srid})::geography)`;
|
|
750
|
+
}
|
|
592
751
|
/**
|
|
593
752
|
* Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
594
753
|
*
|
|
@@ -8773,6 +8932,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
8773
8932
|
}
|
|
8774
8933
|
}
|
|
8775
8934
|
if (!resolvedToken) {
|
|
8935
|
+
const sso = req2.session?.get?.("_tina4_sso");
|
|
8936
|
+
const identity = sso?.identity;
|
|
8937
|
+
if (identity?.issuer && identity?.subject) {
|
|
8938
|
+
req2.user = identity;
|
|
8939
|
+
return false;
|
|
8940
|
+
}
|
|
8776
8941
|
const sessionToken = req2.session?.get?.("token");
|
|
8777
8942
|
if (sessionToken && validToken(sessionToken)) {
|
|
8778
8943
|
resolvedToken = sessionToken;
|
|
@@ -23324,6 +23489,14 @@ function resolveSecuritySchemes() {
|
|
|
23324
23489
|
const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
|
|
23325
23490
|
schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
|
|
23326
23491
|
}
|
|
23492
|
+
const ssoIssuer = (process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
23493
|
+
if (ssoIssuer) {
|
|
23494
|
+
schemes.oidc = {
|
|
23495
|
+
type: "openIdConnect",
|
|
23496
|
+
openIdConnectUrl: `${ssoIssuer}/.well-known/openid-configuration`
|
|
23497
|
+
};
|
|
23498
|
+
schemes.ssoSession = { type: "apiKey", in: "cookie", name: "tina4_session" };
|
|
23499
|
+
}
|
|
23327
23500
|
for (const [name, def] of Object.entries(registeredSchemes)) {
|
|
23328
23501
|
schemes[name] = def;
|
|
23329
23502
|
}
|
|
@@ -23505,7 +23678,9 @@ function generate(routes, models = []) {
|
|
|
23505
23678
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
23506
23679
|
}
|
|
23507
23680
|
} else if (routeRequiresAuth(route, method)) {
|
|
23508
|
-
|
|
23681
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
23682
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
23683
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
23509
23684
|
const responses = operation.responses;
|
|
23510
23685
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
23511
23686
|
}
|
|
@@ -23819,6 +23994,298 @@ var init_src = __esm({
|
|
|
23819
23994
|
}
|
|
23820
23995
|
});
|
|
23821
23996
|
|
|
23997
|
+
// ../core/src/sso.ts
|
|
23998
|
+
var sso_exports = {};
|
|
23999
|
+
__export(sso_exports, {
|
|
24000
|
+
SSO: () => Sso,
|
|
24001
|
+
Sso: () => Sso,
|
|
24002
|
+
SsoError: () => SsoError
|
|
24003
|
+
});
|
|
24004
|
+
import { createHash as createHash9, randomBytes as randomBytes5, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
24005
|
+
var SsoError, Sso;
|
|
24006
|
+
var init_sso = __esm({
|
|
24007
|
+
"../core/src/sso.ts"() {
|
|
24008
|
+
"use strict";
|
|
24009
|
+
SsoError = class extends Error {
|
|
24010
|
+
};
|
|
24011
|
+
Sso = class _Sso {
|
|
24012
|
+
static PENDING_KEY = "_tina4_sso_pending";
|
|
24013
|
+
static SESSION_KEY = "_tina4_sso";
|
|
24014
|
+
issuer;
|
|
24015
|
+
clientId;
|
|
24016
|
+
clientSecret;
|
|
24017
|
+
redirectUri;
|
|
24018
|
+
scopes;
|
|
24019
|
+
verify;
|
|
24020
|
+
postLogoutRedirectUri;
|
|
24021
|
+
claimMap;
|
|
24022
|
+
timeout;
|
|
24023
|
+
metadata = {};
|
|
24024
|
+
static mountedRouters = /* @__PURE__ */ new WeakSet();
|
|
24025
|
+
constructor(options = {}) {
|
|
24026
|
+
this.issuer = (options.issuer ?? process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
24027
|
+
this.clientId = options.clientId ?? process.env.TINA4_SSO_CLIENT_ID ?? "";
|
|
24028
|
+
this.clientSecret = options.clientSecret ?? process.env.TINA4_SSO_CLIENT_SECRET;
|
|
24029
|
+
this.redirectUri = options.redirectUri ?? process.env.TINA4_SSO_REDIRECT_URI ?? "";
|
|
24030
|
+
this.scopes = options.scopes ?? this.jsonEnv("TINA4_SSO_SCOPES", ["openid", "profile", "email"]);
|
|
24031
|
+
this.verify = options.verify ?? process.env.TINA4_SSO_VERIFY ?? "introspection";
|
|
24032
|
+
this.postLogoutRedirectUri = options.postLogoutRedirectUri ?? process.env.TINA4_SSO_POST_LOGOUT_REDIRECT_URI;
|
|
24033
|
+
this.claimMap = options.claimMap ?? this.jsonEnv("TINA4_SSO_CLAIM_MAP", {});
|
|
24034
|
+
this.timeout = options.timeout ?? 1e4;
|
|
24035
|
+
this.validateConfig();
|
|
24036
|
+
}
|
|
24037
|
+
static async fromIssuer(options = {}) {
|
|
24038
|
+
const value = new _Sso(options);
|
|
24039
|
+
await value.discover();
|
|
24040
|
+
return value;
|
|
24041
|
+
}
|
|
24042
|
+
static configured() {
|
|
24043
|
+
return ["TINA4_SSO_ISSUER", "TINA4_SSO_CLIENT_ID", "TINA4_SSO_REDIRECT_URI"].every((key) => Boolean(process.env[key]));
|
|
24044
|
+
}
|
|
24045
|
+
jsonEnv(name, fallback) {
|
|
24046
|
+
const raw = process.env[name];
|
|
24047
|
+
if (!raw) return fallback;
|
|
24048
|
+
try {
|
|
24049
|
+
return JSON.parse(raw);
|
|
24050
|
+
} catch {
|
|
24051
|
+
throw new SsoError(`${name} must be valid JSON`);
|
|
24052
|
+
}
|
|
24053
|
+
}
|
|
24054
|
+
static secureUrl(value, name) {
|
|
24055
|
+
let url;
|
|
24056
|
+
try {
|
|
24057
|
+
url = new URL(value);
|
|
24058
|
+
} catch {
|
|
24059
|
+
throw new SsoError(`${name} must be an absolute URL`);
|
|
24060
|
+
}
|
|
24061
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
24062
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
24063
|
+
throw new SsoError(`${name} must use HTTPS except on loopback`);
|
|
24064
|
+
}
|
|
24065
|
+
}
|
|
24066
|
+
validateConfig() {
|
|
24067
|
+
if (!this.issuer || !this.clientId || !this.redirectUri) {
|
|
24068
|
+
throw new SsoError("TINA4_SSO_ISSUER, TINA4_SSO_CLIENT_ID and TINA4_SSO_REDIRECT_URI are required");
|
|
24069
|
+
}
|
|
24070
|
+
_Sso.secureUrl(this.issuer, "issuer");
|
|
24071
|
+
_Sso.secureUrl(this.redirectUri, "redirect URI");
|
|
24072
|
+
if (!["introspection", "jwks"].includes(this.verify)) throw new SsoError("TINA4_SSO_VERIFY must be introspection or jwks");
|
|
24073
|
+
if (this.verify === "jwks") throw new SsoError("jwks verification requires an installed cryptography capability");
|
|
24074
|
+
if (this.verify === "introspection" && !this.clientSecret) throw new SsoError("introspection verification requires TINA4_SSO_CLIENT_SECRET");
|
|
24075
|
+
if (!Array.isArray(this.scopes) || !this.scopes.includes("openid")) throw new SsoError("TINA4_SSO_SCOPES must be a list containing openid");
|
|
24076
|
+
}
|
|
24077
|
+
async requestJson(url, form, bearer, basic = false) {
|
|
24078
|
+
const headers = { Accept: "application/json" };
|
|
24079
|
+
let body;
|
|
24080
|
+
if (form) {
|
|
24081
|
+
const parameters = new URLSearchParams();
|
|
24082
|
+
for (const [key, value] of Object.entries(form)) parameters.set(key, String(value));
|
|
24083
|
+
body = parameters.toString();
|
|
24084
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
24085
|
+
}
|
|
24086
|
+
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
24087
|
+
if (basic) headers.Authorization = `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`;
|
|
24088
|
+
const controller = new AbortController();
|
|
24089
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
24090
|
+
try {
|
|
24091
|
+
const response = await fetch(url, { method: form ? "POST" : "GET", headers, body, signal: controller.signal });
|
|
24092
|
+
if (!response.ok) throw new SsoError("OIDC provider request failed");
|
|
24093
|
+
const result = await response.json();
|
|
24094
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new SsoError("OIDC provider returned a non-object response");
|
|
24095
|
+
return result;
|
|
24096
|
+
} catch (error) {
|
|
24097
|
+
if (error instanceof SsoError) throw error;
|
|
24098
|
+
throw new SsoError("OIDC provider request failed");
|
|
24099
|
+
} finally {
|
|
24100
|
+
clearTimeout(timer);
|
|
24101
|
+
}
|
|
24102
|
+
}
|
|
24103
|
+
async discover(force = false) {
|
|
24104
|
+
if (Object.keys(this.metadata).length && !force) return { ...this.metadata };
|
|
24105
|
+
const result = await this.requestJson(`${this.issuer}/.well-known/openid-configuration`);
|
|
24106
|
+
if (result.issuer !== this.issuer) throw new SsoError("OIDC discovery issuer does not exactly match configuration");
|
|
24107
|
+
const required = ["authorization_endpoint", "token_endpoint"];
|
|
24108
|
+
if (this.verify === "introspection") required.push("introspection_endpoint");
|
|
24109
|
+
for (const key of required) {
|
|
24110
|
+
if (!result[key]) throw new SsoError(`OIDC discovery is missing ${key}`);
|
|
24111
|
+
_Sso.secureUrl(result[key], key);
|
|
24112
|
+
}
|
|
24113
|
+
this.metadata = result;
|
|
24114
|
+
return { ...result };
|
|
24115
|
+
}
|
|
24116
|
+
static safeReturn(value) {
|
|
24117
|
+
if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return "/";
|
|
24118
|
+
return [...value].some((char) => char.charCodeAt(0) < 32) ? "/" : value;
|
|
24119
|
+
}
|
|
24120
|
+
session(value) {
|
|
24121
|
+
return value?.session ?? value;
|
|
24122
|
+
}
|
|
24123
|
+
async login(requestOrSession, returnTo = "/") {
|
|
24124
|
+
const session = this.session(requestOrSession);
|
|
24125
|
+
if (!session) throw new SsoError("SSO login requires a Tina4 Session");
|
|
24126
|
+
const state = randomBytes5(32).toString("base64url");
|
|
24127
|
+
const nonce = randomBytes5(32).toString("base64url");
|
|
24128
|
+
const verifier = randomBytes5(64).toString("base64url");
|
|
24129
|
+
const challenge = createHash9("sha256").update(verifier).digest("base64url");
|
|
24130
|
+
session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
|
|
24131
|
+
const metadata = await this.discover();
|
|
24132
|
+
const query = new URLSearchParams({
|
|
24133
|
+
client_id: this.clientId,
|
|
24134
|
+
redirect_uri: this.redirectUri,
|
|
24135
|
+
response_type: "code",
|
|
24136
|
+
scope: this.scopes.join(" "),
|
|
24137
|
+
state,
|
|
24138
|
+
nonce,
|
|
24139
|
+
code_challenge: challenge,
|
|
24140
|
+
code_challenge_method: "S256"
|
|
24141
|
+
});
|
|
24142
|
+
return `${metadata.authorization_endpoint}?${query}`;
|
|
24143
|
+
}
|
|
24144
|
+
static equal(left, right) {
|
|
24145
|
+
const a = Buffer.from(String(left ?? ""));
|
|
24146
|
+
const b = Buffer.from(String(right ?? ""));
|
|
24147
|
+
return a.length === b.length && timingSafeEqual3(a, b);
|
|
24148
|
+
}
|
|
24149
|
+
static jwtPayload(token) {
|
|
24150
|
+
try {
|
|
24151
|
+
return JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
24152
|
+
} catch {
|
|
24153
|
+
throw new SsoError("provider returned an invalid ID token");
|
|
24154
|
+
}
|
|
24155
|
+
}
|
|
24156
|
+
async introspect(accessToken) {
|
|
24157
|
+
const metadata = await this.discover();
|
|
24158
|
+
const result = await this.requestJson(metadata.introspection_endpoint, { token: accessToken, token_type_hint: "access_token" }, void 0, true);
|
|
24159
|
+
if (result.active !== true || result.iss !== this.issuer) throw new SsoError("OIDC access token is inactive or has the wrong issuer");
|
|
24160
|
+
const audience = result.aud ?? result.client_id;
|
|
24161
|
+
const valid = (Array.isArray(audience) ? audience.includes(this.clientId) : audience === this.clientId) || result.client_id === this.clientId;
|
|
24162
|
+
if (!valid) throw new SsoError("OIDC token audience mismatch");
|
|
24163
|
+
return result;
|
|
24164
|
+
}
|
|
24165
|
+
claim(claims, configured, fallback) {
|
|
24166
|
+
let value = claims;
|
|
24167
|
+
for (const part of (configured ?? fallback).split(".")) value = value && typeof value === "object" ? value[part] : void 0;
|
|
24168
|
+
return value;
|
|
24169
|
+
}
|
|
24170
|
+
normalize(claims) {
|
|
24171
|
+
const subject = this.claim(claims, this.claimMap.subject, "sub");
|
|
24172
|
+
const issuer = this.claim(claims, this.claimMap.issuer, "iss") ?? this.issuer;
|
|
24173
|
+
if (!subject || issuer !== this.issuer) throw new SsoError("OIDC identity is missing a valid issuer or subject");
|
|
24174
|
+
const roles = [...this.claim(claims, this.claimMap.roles, "realm_access.roles") ?? [], ...claims.resource_access?.[this.clientId]?.roles ?? []];
|
|
24175
|
+
const groups = this.claim(claims, this.claimMap.groups, "groups") ?? [];
|
|
24176
|
+
return {
|
|
24177
|
+
issuer,
|
|
24178
|
+
subject,
|
|
24179
|
+
username: this.claim(claims, this.claimMap.username, "preferred_username") ?? null,
|
|
24180
|
+
email: this.claim(claims, this.claimMap.email, "email") ?? null,
|
|
24181
|
+
name: this.claim(claims, this.claimMap.name, "name") ?? null,
|
|
24182
|
+
roles: [...new Set(roles.map(String))].sort(),
|
|
24183
|
+
groups: [...new Set(groups.map(String))].sort()
|
|
24184
|
+
};
|
|
24185
|
+
}
|
|
24186
|
+
async callback(requestOrSession, query) {
|
|
24187
|
+
const session = this.session(requestOrSession);
|
|
24188
|
+
const values = query ?? requestOrSession?.query ?? {};
|
|
24189
|
+
const pending = session?.get(_Sso.PENDING_KEY);
|
|
24190
|
+
session?.delete(_Sso.PENDING_KEY);
|
|
24191
|
+
if (!pending || !values.code || !_Sso.equal(values.state, pending.state)) throw new SsoError("OIDC callback state is invalid or already consumed");
|
|
24192
|
+
if (Math.floor(Date.now() / 1e3) - Number(pending.created_at ?? 0) > 600) throw new SsoError("OIDC callback state has expired");
|
|
24193
|
+
const metadata = await this.discover();
|
|
24194
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
24195
|
+
grant_type: "authorization_code",
|
|
24196
|
+
code: values.code,
|
|
24197
|
+
redirect_uri: this.redirectUri,
|
|
24198
|
+
client_id: this.clientId,
|
|
24199
|
+
code_verifier: pending.verifier
|
|
24200
|
+
}, void 0, Boolean(this.clientSecret));
|
|
24201
|
+
if (!tokens.access_token || !tokens.id_token) throw new SsoError("OIDC token response is incomplete");
|
|
24202
|
+
if (this.verify === "jwks") throw new SsoError("JWKS verification requires an installed cryptography capability");
|
|
24203
|
+
const claims = await this.introspect(tokens.access_token);
|
|
24204
|
+
if (!_Sso.equal(_Sso.jwtPayload(tokens.id_token).nonce, pending.nonce)) throw new SsoError("OIDC ID token nonce mismatch");
|
|
24205
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
24206
|
+
const identity = this.normalize(claims);
|
|
24207
|
+
session.regenerate();
|
|
24208
|
+
session.set(_Sso.SESSION_KEY, {
|
|
24209
|
+
version: 1,
|
|
24210
|
+
identity,
|
|
24211
|
+
access_token: tokens.access_token,
|
|
24212
|
+
refresh_token: tokens.refresh_token,
|
|
24213
|
+
id_token: tokens.id_token,
|
|
24214
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
24215
|
+
});
|
|
24216
|
+
return { identity, return_to: _Sso.safeReturn(pending.return_to) };
|
|
24217
|
+
}
|
|
24218
|
+
identity(requestOrSession) {
|
|
24219
|
+
const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
|
|
24220
|
+
const identity = stored?.identity ?? null;
|
|
24221
|
+
if (identity && requestOrSession?.session) requestOrSession.user = identity;
|
|
24222
|
+
return identity;
|
|
24223
|
+
}
|
|
24224
|
+
async refresh(requestOrSession) {
|
|
24225
|
+
const session = this.session(requestOrSession);
|
|
24226
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
24227
|
+
if (!stored?.refresh_token) {
|
|
24228
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
24229
|
+
throw new SsoError("OIDC session cannot be refreshed");
|
|
24230
|
+
}
|
|
24231
|
+
try {
|
|
24232
|
+
const metadata = await this.discover();
|
|
24233
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
24234
|
+
grant_type: "refresh_token",
|
|
24235
|
+
refresh_token: stored.refresh_token,
|
|
24236
|
+
client_id: this.clientId
|
|
24237
|
+
}, void 0, Boolean(this.clientSecret));
|
|
24238
|
+
const claims = await this.introspect(tokens.access_token);
|
|
24239
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
24240
|
+
const identity = this.normalize(claims);
|
|
24241
|
+
session.set(_Sso.SESSION_KEY, {
|
|
24242
|
+
...stored,
|
|
24243
|
+
identity,
|
|
24244
|
+
access_token: tokens.access_token,
|
|
24245
|
+
refresh_token: tokens.refresh_token ?? stored.refresh_token,
|
|
24246
|
+
id_token: tokens.id_token ?? stored.id_token,
|
|
24247
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
24248
|
+
});
|
|
24249
|
+
return identity;
|
|
24250
|
+
} catch (error) {
|
|
24251
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
24252
|
+
throw error;
|
|
24253
|
+
}
|
|
24254
|
+
}
|
|
24255
|
+
async logout(requestOrSession, returnTo = "/") {
|
|
24256
|
+
const session = this.session(requestOrSession);
|
|
24257
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
24258
|
+
session?.destroy();
|
|
24259
|
+
const endpoint = (await this.discover()).end_session_endpoint;
|
|
24260
|
+
const target = this.postLogoutRedirectUri ?? _Sso.safeReturn(returnTo);
|
|
24261
|
+
if (!endpoint) return target;
|
|
24262
|
+
const params = new URLSearchParams({ post_logout_redirect_uri: target, client_id: this.clientId });
|
|
24263
|
+
if (stored?.id_token) params.set("id_token_hint", stored.id_token);
|
|
24264
|
+
return `${endpoint}?${params}`;
|
|
24265
|
+
}
|
|
24266
|
+
static async mountConfigured(router) {
|
|
24267
|
+
if (_Sso.mountedRouters.has(router) || !_Sso.configured()) return false;
|
|
24268
|
+
const owned = /* @__PURE__ */ new Set(["GET /auth/login", "GET /auth/callback", "POST /auth/logout"]);
|
|
24269
|
+
const collisions = router.getRoutes().map((route) => `${route.method} ${route.pattern}`).filter((route) => owned.has(route));
|
|
24270
|
+
if (collisions.length) throw new SsoError(`SSO route collision: ${collisions.join(", ")}`);
|
|
24271
|
+
const sso = await _Sso.fromIssuer();
|
|
24272
|
+
router.get("/auth/login", async (req2, res) => res.redirect(await sso.login(req2, req2.query?.return_to ?? "/")));
|
|
24273
|
+
router.get("/auth/callback", async (req2, res) => {
|
|
24274
|
+
try {
|
|
24275
|
+
return res.redirect((await sso.callback(req2)).return_to);
|
|
24276
|
+
} catch (error) {
|
|
24277
|
+
const message = error instanceof SsoError ? error.message : "OIDC callback failed";
|
|
24278
|
+
return res.error("SSO_CALLBACK_FAILED", message, 400);
|
|
24279
|
+
}
|
|
24280
|
+
});
|
|
24281
|
+
router.post("/auth/logout", async (req2, res) => res.redirect(await sso.logout(req2, req2.query?.return_to ?? "/")));
|
|
24282
|
+
_Sso.mountedRouters.add(router);
|
|
24283
|
+
return true;
|
|
24284
|
+
}
|
|
24285
|
+
};
|
|
24286
|
+
}
|
|
24287
|
+
});
|
|
24288
|
+
|
|
23822
24289
|
// ../core/src/docsAutoDiscovery.ts
|
|
23823
24290
|
var docsAutoDiscovery_exports = {};
|
|
23824
24291
|
__export(docsAutoDiscovery_exports, {
|
|
@@ -23888,7 +24355,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
23888
24355
|
|
|
23889
24356
|
// ../core/src/server.ts
|
|
23890
24357
|
import { createServer as createServer2 } from "node:http";
|
|
23891
|
-
import { randomBytes as
|
|
24358
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
23892
24359
|
import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative8 } from "node:path";
|
|
23893
24360
|
import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
|
|
23894
24361
|
import { isatty } from "node:tty";
|
|
@@ -24528,7 +24995,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
24528
24995
|
}
|
|
24529
24996
|
}
|
|
24530
24997
|
}
|
|
24531
|
-
const requestId = Log.getRequestId() ??
|
|
24998
|
+
const requestId = Log.getRequestId() ?? randomBytes6(4).toString("hex");
|
|
24532
24999
|
if (wantsJson(req2)) {
|
|
24533
25000
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
24534
25001
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -24599,7 +25066,7 @@ function serveStaticAsset(ctx) {
|
|
|
24599
25066
|
return false;
|
|
24600
25067
|
}
|
|
24601
25068
|
async function serveNotFound(ctx) {
|
|
24602
|
-
const requestId = Log.getRequestId() ??
|
|
25069
|
+
const requestId = Log.getRequestId() ?? randomBytes6(4).toString("hex");
|
|
24603
25070
|
if (wantsJson(ctx.req)) {
|
|
24604
25071
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
24605
25072
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -24719,7 +25186,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
24719
25186
|
}
|
|
24720
25187
|
}
|
|
24721
25188
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
24722
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
25189
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes6(4).toString("hex");
|
|
24723
25190
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
24724
25191
|
return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
|
|
24725
25192
|
}
|
|
@@ -24853,6 +25320,8 @@ ${reset2}
|
|
|
24853
25320
|
console.log(`
|
|
24854
25321
|
No routes directory found at ${routesDir}`);
|
|
24855
25322
|
}
|
|
25323
|
+
const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
|
|
25324
|
+
await Sso2.mountConfigured(router);
|
|
24856
25325
|
if (attachCsrfFromEnv()) {
|
|
24857
25326
|
console.log(`
|
|
24858
25327
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
@@ -25845,7 +26314,7 @@ var init_mqttMessage = __esm({
|
|
|
25845
26314
|
// ../core/src/mqtt.ts
|
|
25846
26315
|
import net2 from "node:net";
|
|
25847
26316
|
import tls from "node:tls";
|
|
25848
|
-
import { randomBytes as
|
|
26317
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
25849
26318
|
import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
|
|
25850
26319
|
var MqttError, MqttTimeoutError, CONNECT, CONNACK, PUBLISH, PUBACK, SUBSCRIBE, SUBACK, PINGREQ, PINGRESP, DISCONNECT, PROTOCOL_LEVEL, DEFAULT_PORT, DEFAULT_TLS_PORT, DEFAULT_URL, DEFAULT_KEEPALIVE, SUBSCRIPTION_REFUSED, MAX_REMAINING_LENGTH, QOS2_REFUSED_MESSAGE, CONNACK_RETURN_CODES, Mqtt;
|
|
25851
26320
|
var init_mqtt = __esm({
|
|
@@ -25933,7 +26402,7 @@ var init_mqtt = __esm({
|
|
|
25933
26402
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
25934
26403
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
25935
26404
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
25936
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
26405
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes7(8).toString("hex");
|
|
25937
26406
|
this.clientId = cid;
|
|
25938
26407
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
25939
26408
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -26863,7 +27332,7 @@ var init_service = __esm({
|
|
|
26863
27332
|
import http from "node:http";
|
|
26864
27333
|
import https from "node:https";
|
|
26865
27334
|
import { URL as URL2 } from "node:url";
|
|
26866
|
-
import { randomBytes as
|
|
27335
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
26867
27336
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
26868
27337
|
import { basename as basename5 } from "node:path";
|
|
26869
27338
|
import { pipeline } from "node:stream/promises";
|
|
@@ -27161,7 +27630,7 @@ var init_api = __esm({
|
|
|
27161
27630
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
27162
27631
|
}
|
|
27163
27632
|
const partContentType = guessContentType(uploadName);
|
|
27164
|
-
const boundary = "----Tina4Boundary" +
|
|
27633
|
+
const boundary = "----Tina4Boundary" + randomBytes8(16).toString("hex");
|
|
27165
27634
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
27166
27635
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
27167
27636
|
return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
|
|
@@ -31993,10 +32462,13 @@ __export(src_exports2, {
|
|
|
31993
32462
|
RouteGroup: () => RouteGroup,
|
|
31994
32463
|
RouteRef: () => RouteRef,
|
|
31995
32464
|
Router: () => Router,
|
|
32465
|
+
SSO: () => Sso,
|
|
31996
32466
|
SafeString: () => SafeString2,
|
|
31997
32467
|
SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
|
|
31998
32468
|
ServiceRunner: () => ServiceRunner,
|
|
31999
32469
|
Session: () => Session,
|
|
32470
|
+
Sso: () => Sso,
|
|
32471
|
+
SsoError: () => SsoError,
|
|
32000
32472
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
32001
32473
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
32002
32474
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -32233,6 +32705,7 @@ var init_src2 = __esm({
|
|
|
32233
32705
|
init_htmlElement();
|
|
32234
32706
|
init_errorOverlay();
|
|
32235
32707
|
init_ai();
|
|
32708
|
+
init_sso();
|
|
32236
32709
|
init_aiClient();
|
|
32237
32710
|
init_liteBackend();
|
|
32238
32711
|
init_rabbitmqBackend();
|
|
@@ -33331,6 +33804,8 @@ function fieldTypeToPostgres(def) {
|
|
|
33331
33804
|
return "TEXT";
|
|
33332
33805
|
case "json":
|
|
33333
33806
|
return "JSONB";
|
|
33807
|
+
case "point":
|
|
33808
|
+
return SQLTranslator.pointColumnType("postgres", def.srid ?? 4326);
|
|
33334
33809
|
case "string":
|
|
33335
33810
|
return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
|
|
33336
33811
|
default:
|
|
@@ -38954,10 +39429,13 @@ var init_queryBuilder = __esm({
|
|
|
38954
39429
|
"use strict";
|
|
38955
39430
|
init_database();
|
|
38956
39431
|
init_databaseResult();
|
|
39432
|
+
init_point();
|
|
39433
|
+
init_sqlTranslator();
|
|
38957
39434
|
QueryBuilder = class _QueryBuilder {
|
|
38958
39435
|
table;
|
|
38959
39436
|
db;
|
|
38960
39437
|
columns = ["*"];
|
|
39438
|
+
selectParams = [];
|
|
38961
39439
|
wheres = [];
|
|
38962
39440
|
params = [];
|
|
38963
39441
|
joinClauses = [];
|
|
@@ -38965,14 +39443,17 @@ var init_queryBuilder = __esm({
|
|
|
38965
39443
|
havings = [];
|
|
38966
39444
|
havingParams = [];
|
|
38967
39445
|
orderByCols = [];
|
|
39446
|
+
orderByParams = [];
|
|
39447
|
+
primaryKey;
|
|
38968
39448
|
limitVal;
|
|
38969
39449
|
offsetVal;
|
|
38970
39450
|
/**
|
|
38971
39451
|
* Private constructor — use static factory methods.
|
|
38972
39452
|
*/
|
|
38973
|
-
constructor(table2, db) {
|
|
39453
|
+
constructor(table2, db, primaryKey) {
|
|
38974
39454
|
this.table = table2;
|
|
38975
39455
|
this.db = db;
|
|
39456
|
+
this.primaryKey = primaryKey;
|
|
38976
39457
|
}
|
|
38977
39458
|
/**
|
|
38978
39459
|
* Create a QueryBuilder for a table.
|
|
@@ -38981,8 +39462,8 @@ var init_queryBuilder = __esm({
|
|
|
38981
39462
|
* @param db - Optional database adapter.
|
|
38982
39463
|
* @returns A new QueryBuilder instance.
|
|
38983
39464
|
*/
|
|
38984
|
-
static fromTable(tableName, db) {
|
|
38985
|
-
return new _QueryBuilder(tableName, db);
|
|
39465
|
+
static fromTable(tableName, db, primaryKey) {
|
|
39466
|
+
return new _QueryBuilder(tableName, db, primaryKey);
|
|
38986
39467
|
}
|
|
38987
39468
|
/**
|
|
38988
39469
|
* Set the columns to select.
|
|
@@ -38993,6 +39474,7 @@ var init_queryBuilder = __esm({
|
|
|
38993
39474
|
select(...cols) {
|
|
38994
39475
|
if (cols.length > 0) {
|
|
38995
39476
|
this.columns = cols;
|
|
39477
|
+
this.selectParams = [];
|
|
38996
39478
|
}
|
|
38997
39479
|
return this;
|
|
38998
39480
|
}
|
|
@@ -39074,6 +39556,41 @@ var init_queryBuilder = __esm({
|
|
|
39074
39556
|
this.orderByCols.push(expression);
|
|
39075
39557
|
return this;
|
|
39076
39558
|
}
|
|
39559
|
+
withinDistance(column2, pointValue, radiusMetres, srid = DEFAULT_SRID) {
|
|
39560
|
+
const radius = Number(radiusMetres);
|
|
39561
|
+
if (!Number.isFinite(radius) || radius < 0) throw new RangeError("Spatial radius must be finite and greater than or equal to zero");
|
|
39562
|
+
const point = Point.parse(pointValue, srid);
|
|
39563
|
+
return this.where(SQLTranslator.withinDistance(this.engine(), column2, point.srid), [point.lon, point.lat, radius]);
|
|
39564
|
+
}
|
|
39565
|
+
intersects(column2, geometry, srid = DEFAULT_SRID) {
|
|
39566
|
+
const [bound, form] = Point.geometryBinding(geometry, srid);
|
|
39567
|
+
return this.where(SQLTranslator.intersects(this.engine(), column2, form, srid), [bound]);
|
|
39568
|
+
}
|
|
39569
|
+
bbox(column2, minLon, minLat, maxLon, maxLat, srid = DEFAULT_SRID) {
|
|
39570
|
+
const values = [minLon, minLat, maxLon, maxLat].map(Number);
|
|
39571
|
+
if (!values.every(Number.isFinite)) throw new TypeError("Bounding-box coordinates must be finite numbers");
|
|
39572
|
+
const [west, south, east, north] = values;
|
|
39573
|
+
new Point(west, south, srid);
|
|
39574
|
+
new Point(east, north, srid);
|
|
39575
|
+
if (west > east || south > north) throw new RangeError("Bounding box must be ordered west, south, east, north");
|
|
39576
|
+
return this.where(SQLTranslator.bbox(this.engine(), column2, srid), values);
|
|
39577
|
+
}
|
|
39578
|
+
selectDistance(column2, pointValue, alias = "distance", srid = DEFAULT_SRID) {
|
|
39579
|
+
const point = Point.parse(pointValue, srid);
|
|
39580
|
+
this.columns.push(SQLTranslator.distanceAs(this.engine(), column2, alias, point.srid));
|
|
39581
|
+
this.selectParams.push(point.lon, point.lat);
|
|
39582
|
+
return this;
|
|
39583
|
+
}
|
|
39584
|
+
orderByDistance(column2, pointValue, direction = "ASC", srid = DEFAULT_SRID) {
|
|
39585
|
+
const order = direction.toUpperCase();
|
|
39586
|
+
if (order !== "ASC" && order !== "DESC") throw new TypeError("Distance order direction must be ASC or DESC");
|
|
39587
|
+
if (!this.primaryKey) throw new Error("Stable spatial ordering needs a primary key; use BaseModel.query() or pass one to fromTable()");
|
|
39588
|
+
const point = Point.parse(pointValue, srid);
|
|
39589
|
+
this.orderByCols.push(`${SQLTranslator.distance(this.engine(), column2, point.srid)} ${order}`);
|
|
39590
|
+
this.orderByParams.push(point.lon, point.lat);
|
|
39591
|
+
this.orderByCols.push(`${SQLTranslator.spatialIdentifier(this.primaryKey, "primary key")} ASC`);
|
|
39592
|
+
return this;
|
|
39593
|
+
}
|
|
39077
39594
|
/**
|
|
39078
39595
|
* Set LIMIT and optional OFFSET.
|
|
39079
39596
|
*
|
|
@@ -39139,7 +39656,7 @@ var init_queryBuilder = __esm({
|
|
|
39139
39656
|
async get() {
|
|
39140
39657
|
this.ensureDb();
|
|
39141
39658
|
const sql = this.toSql();
|
|
39142
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
39659
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
39143
39660
|
const queryParams = allParams.length > 0 ? allParams : void 0;
|
|
39144
39661
|
const rows = await adapterFetch(
|
|
39145
39662
|
this.db,
|
|
@@ -39167,7 +39684,7 @@ var init_queryBuilder = __esm({
|
|
|
39167
39684
|
async first() {
|
|
39168
39685
|
this.ensureDb();
|
|
39169
39686
|
const sql = this.toSql();
|
|
39170
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
39687
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
39171
39688
|
return adapterFetchOne(
|
|
39172
39689
|
this.db,
|
|
39173
39690
|
sql,
|
|
@@ -39182,9 +39699,18 @@ var init_queryBuilder = __esm({
|
|
|
39182
39699
|
async count() {
|
|
39183
39700
|
this.ensureDb();
|
|
39184
39701
|
const original = this.columns;
|
|
39702
|
+
const originalSelectParams = this.selectParams;
|
|
39703
|
+
const originalOrder = this.orderByCols;
|
|
39704
|
+
const originalOrderParams = this.orderByParams;
|
|
39185
39705
|
this.columns = ["COUNT(*) as cnt"];
|
|
39706
|
+
this.selectParams = [];
|
|
39707
|
+
this.orderByCols = [];
|
|
39708
|
+
this.orderByParams = [];
|
|
39186
39709
|
const sql = this.toSql();
|
|
39187
39710
|
this.columns = original;
|
|
39711
|
+
this.selectParams = originalSelectParams;
|
|
39712
|
+
this.orderByCols = originalOrder;
|
|
39713
|
+
this.orderByParams = originalOrderParams;
|
|
39188
39714
|
const allParams = [...this.params, ...this.havingParams];
|
|
39189
39715
|
const row = await adapterFetchOne(
|
|
39190
39716
|
this.db,
|
|
@@ -39369,6 +39895,10 @@ var init_queryBuilder = __esm({
|
|
|
39369
39895
|
}
|
|
39370
39896
|
}
|
|
39371
39897
|
}
|
|
39898
|
+
engine() {
|
|
39899
|
+
this.ensureDb();
|
|
39900
|
+
return this.db.getDatabaseType();
|
|
39901
|
+
}
|
|
39372
39902
|
};
|
|
39373
39903
|
}
|
|
39374
39904
|
});
|
|
@@ -39392,6 +39922,11 @@ function toDbFieldValue(def, value) {
|
|
|
39392
39922
|
if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
|
|
39393
39923
|
return JSON.stringify(value);
|
|
39394
39924
|
}
|
|
39925
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
39926
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
39927
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
39928
|
+
return point.ewkt;
|
|
39929
|
+
}
|
|
39395
39930
|
return value;
|
|
39396
39931
|
}
|
|
39397
39932
|
function fromDbFieldValue(def, value) {
|
|
@@ -39402,6 +39937,11 @@ function fromDbFieldValue(def, value) {
|
|
|
39402
39937
|
return value;
|
|
39403
39938
|
}
|
|
39404
39939
|
}
|
|
39940
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
39941
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
39942
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
39943
|
+
return point;
|
|
39944
|
+
}
|
|
39405
39945
|
return value;
|
|
39406
39946
|
}
|
|
39407
39947
|
function _pluralRelKeys() {
|
|
@@ -39437,6 +39977,7 @@ var init_baseModel = __esm({
|
|
|
39437
39977
|
init_sqlite();
|
|
39438
39978
|
init_sqlTranslator();
|
|
39439
39979
|
init_src2();
|
|
39980
|
+
init_point();
|
|
39440
39981
|
_fkRegistry = /* @__PURE__ */ new Map();
|
|
39441
39982
|
EAGER_IN_CHUNK = 500;
|
|
39442
39983
|
modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
|
|
@@ -39492,7 +40033,9 @@ var init_baseModel = __esm({
|
|
|
39492
40033
|
for (const [name, def] of Object.entries(fields0)) {
|
|
39493
40034
|
if (def.default === void 0) continue;
|
|
39494
40035
|
let dv = typeof def.default === "function" ? def.default() : def.default;
|
|
39495
|
-
if (dv !== null &&
|
|
40036
|
+
if (def.type === "point" && dv !== null && dv !== void 0) {
|
|
40037
|
+
dv = fromDbFieldValue(def, dv);
|
|
40038
|
+
} else if (dv !== null && typeof dv === "object") dv = structuredClone(dv);
|
|
39496
40039
|
this[name] = dv;
|
|
39497
40040
|
}
|
|
39498
40041
|
if (data) {
|
|
@@ -39594,7 +40137,7 @@ var init_baseModel = __esm({
|
|
|
39594
40137
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
39595
40138
|
*/
|
|
39596
40139
|
static query() {
|
|
39597
|
-
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
40140
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
|
|
39598
40141
|
}
|
|
39599
40142
|
/**
|
|
39600
40143
|
* Get the database adapter for this model.
|
|
@@ -40047,7 +40590,7 @@ var init_baseModel = __esm({
|
|
|
40047
40590
|
for (const key of Object.keys(ModelClass.fields)) {
|
|
40048
40591
|
if (this[key] !== void 0) {
|
|
40049
40592
|
const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
|
|
40050
|
-
result[outKey] = this[key];
|
|
40593
|
+
result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
|
|
40051
40594
|
}
|
|
40052
40595
|
}
|
|
40053
40596
|
if (ModelClass.softDelete && this.is_deleted !== void 0) {
|
|
@@ -40107,6 +40650,19 @@ var init_baseModel = __esm({
|
|
|
40107
40650
|
}
|
|
40108
40651
|
return result;
|
|
40109
40652
|
}
|
|
40653
|
+
toFeature(geometryField, include) {
|
|
40654
|
+
const ModelClass = this.constructor;
|
|
40655
|
+
const pointFields = Object.entries(ModelClass.fields).filter(([, def]) => def.type === "point").map(([name]) => name);
|
|
40656
|
+
const field = geometryField ?? pointFields[0];
|
|
40657
|
+
if (!field || !pointFields.includes(field)) throw new Error("toFeature() needs a declared point field");
|
|
40658
|
+
const properties = this.toDict(include, "camel");
|
|
40659
|
+
const geometry = properties[field] ?? null;
|
|
40660
|
+
delete properties[field];
|
|
40661
|
+
return { type: "Feature", geometry, properties };
|
|
40662
|
+
}
|
|
40663
|
+
static featureCollection(models, geometryField, include) {
|
|
40664
|
+
return { type: "FeatureCollection", features: models.map((model) => model.toFeature(geometryField, include)) };
|
|
40665
|
+
}
|
|
40110
40666
|
/**
|
|
40111
40667
|
* Convert to an associative object (alias for toDict).
|
|
40112
40668
|
*/
|
|
@@ -40157,7 +40713,10 @@ var init_baseModel = __esm({
|
|
|
40157
40713
|
*/
|
|
40158
40714
|
static async createTable() {
|
|
40159
40715
|
const db = this.getDb();
|
|
40160
|
-
|
|
40716
|
+
const pointFields = Object.entries(this.fields).filter(([, def]) => def.type === "point");
|
|
40717
|
+
const engine = db.getDatabaseType();
|
|
40718
|
+
if (pointFields.length > 0) SQLTranslator.requireSpatial(engine, "PointField");
|
|
40719
|
+
if (await adapterTableExists(db, this.tableName)) return this.createSpatialIndexes(db, pointFields);
|
|
40161
40720
|
if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
|
|
40162
40721
|
const mappedFields = {};
|
|
40163
40722
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
@@ -40173,7 +40732,7 @@ var init_baseModel = __esm({
|
|
|
40173
40732
|
mappedFields["is_deleted"] = { type: "integer", default: 0 };
|
|
40174
40733
|
}
|
|
40175
40734
|
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
40176
|
-
return
|
|
40735
|
+
return this.createSpatialIndexes(db, pointFields);
|
|
40177
40736
|
}
|
|
40178
40737
|
const typeMap = {
|
|
40179
40738
|
integer: "INTEGER",
|
|
@@ -40220,6 +40779,14 @@ var init_baseModel = __esm({
|
|
|
40220
40779
|
}
|
|
40221
40780
|
return true;
|
|
40222
40781
|
}
|
|
40782
|
+
static async createSpatialIndexes(db, fields) {
|
|
40783
|
+
for (const [fieldName, def] of fields) {
|
|
40784
|
+
SQLTranslator.pointColumnType(db.getDatabaseType(), def.srid ?? DEFAULT_SRID);
|
|
40785
|
+
if (def.spatialIndex === false) continue;
|
|
40786
|
+
await adapterExecute(db, SQLTranslator.spatialIndex(db.getDatabaseType(), this.tableName, this.getDbColumn(fieldName)));
|
|
40787
|
+
}
|
|
40788
|
+
return true;
|
|
40789
|
+
}
|
|
40223
40790
|
/**
|
|
40224
40791
|
* Find a record by primary key or throw an error if not found.
|
|
40225
40792
|
*/
|
|
@@ -41159,7 +41726,7 @@ var init_seeder = __esm({
|
|
|
41159
41726
|
|
|
41160
41727
|
// src/docstore.ts
|
|
41161
41728
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
41162
|
-
import { randomBytes as
|
|
41729
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
41163
41730
|
import { mkdirSync as mkdirSync19 } from "node:fs";
|
|
41164
41731
|
import { dirname as dirname14, isAbsolute as isAbsolute6, join as join31 } from "node:path";
|
|
41165
41732
|
function iso(d) {
|
|
@@ -41511,8 +42078,8 @@ var init_docstore = __esm({
|
|
|
41511
42078
|
OID_RE = /^[0-9a-fA-F]{24}$/;
|
|
41512
42079
|
ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
41513
42080
|
ObjectId = class _ObjectId {
|
|
41514
|
-
static _counter =
|
|
41515
|
-
static _process =
|
|
42081
|
+
static _counter = randomBytes9(3).readUIntBE(0, 3);
|
|
42082
|
+
static _process = randomBytes9(5);
|
|
41516
42083
|
_bytes;
|
|
41517
42084
|
constructor(oid) {
|
|
41518
42085
|
if (oid === void 0 || oid === null) {
|
|
@@ -41940,7 +42507,7 @@ var init_attachment = __esm({
|
|
|
41940
42507
|
});
|
|
41941
42508
|
|
|
41942
42509
|
// src/realtime/storage.ts
|
|
41943
|
-
import { randomBytes as
|
|
42510
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
41944
42511
|
import { mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
|
|
41945
42512
|
import { resolve as resolve19, sep as sep5 } from "node:path";
|
|
41946
42513
|
import { createRequire as createRequire8 } from "node:module";
|
|
@@ -41951,7 +42518,7 @@ function storageKey(filename = "") {
|
|
|
41951
42518
|
const clean = raw.replace(UNSAFE, "").slice(0, 12);
|
|
41952
42519
|
if (clean) ext = `.${clean}`;
|
|
41953
42520
|
}
|
|
41954
|
-
return `${
|
|
42521
|
+
return `${randomBytes10(16).toString("hex")}${ext}`;
|
|
41955
42522
|
}
|
|
41956
42523
|
function selectStorage(storage) {
|
|
41957
42524
|
if (storage) return storage;
|
|
@@ -42399,6 +42966,7 @@ __export(index_exports, {
|
|
|
42399
42966
|
CachedDatabaseAdapter: () => CachedDatabaseAdapter,
|
|
42400
42967
|
Cursor: () => Cursor,
|
|
42401
42968
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
42969
|
+
DEFAULT_SRID: () => DEFAULT_SRID,
|
|
42402
42970
|
Database: () => Database,
|
|
42403
42971
|
DatabaseResult: () => DatabaseResult,
|
|
42404
42972
|
DatabaseUrl: () => DatabaseUrl,
|
|
@@ -42414,6 +42982,7 @@ __export(index_exports, {
|
|
|
42414
42982
|
NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
|
|
42415
42983
|
ObjectId: () => ObjectId,
|
|
42416
42984
|
OdbcAdapter: () => OdbcAdapter,
|
|
42985
|
+
Point: () => Point,
|
|
42417
42986
|
PostgresAdapter: () => PostgresAdapter,
|
|
42418
42987
|
QueryBuilder: () => QueryBuilder,
|
|
42419
42988
|
QueryCache: () => QueryCache,
|
|
@@ -42426,6 +42995,7 @@ __export(index_exports, {
|
|
|
42426
42995
|
S3Storage: () => S3Storage,
|
|
42427
42996
|
SQLTranslator: () => SQLTranslator,
|
|
42428
42997
|
SQLiteAdapter: () => SQLiteAdapter,
|
|
42998
|
+
SpatialNotSupportedError: () => SpatialNotSupportedError,
|
|
42429
42999
|
SqliteCollection: () => SqliteCollection,
|
|
42430
43000
|
SqliteDatabase: () => SqliteDatabase,
|
|
42431
43001
|
adapterColumns: () => adapterColumns,
|
|
@@ -42519,6 +43089,7 @@ var init_index = __esm({
|
|
|
42519
43089
|
init_baseModel();
|
|
42520
43090
|
init_queryBuilder();
|
|
42521
43091
|
init_sqlTranslator();
|
|
43092
|
+
init_point();
|
|
42522
43093
|
init_connectTimeout();
|
|
42523
43094
|
init_cachedDatabase();
|
|
42524
43095
|
init_fakeData2();
|
|
@@ -42542,6 +43113,7 @@ export {
|
|
|
42542
43113
|
CachedDatabaseAdapter,
|
|
42543
43114
|
Cursor,
|
|
42544
43115
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
43116
|
+
DEFAULT_SRID,
|
|
42545
43117
|
Database,
|
|
42546
43118
|
DatabaseResult,
|
|
42547
43119
|
DatabaseUrl,
|
|
@@ -42557,6 +43129,7 @@ export {
|
|
|
42557
43129
|
NOT_REQUIRED_ON_ADAPTER,
|
|
42558
43130
|
ObjectId,
|
|
42559
43131
|
OdbcAdapter,
|
|
43132
|
+
Point,
|
|
42560
43133
|
PostgresAdapter,
|
|
42561
43134
|
QueryBuilder,
|
|
42562
43135
|
QueryCache,
|
|
@@ -42569,6 +43142,7 @@ export {
|
|
|
42569
43142
|
S3Storage,
|
|
42570
43143
|
SQLTranslator,
|
|
42571
43144
|
SQLiteAdapter,
|
|
43145
|
+
SpatialNotSupportedError,
|
|
42572
43146
|
SqliteCollection,
|
|
42573
43147
|
SqliteDatabase,
|
|
42574
43148
|
adapterColumns,
|