tina4-nodejs 3.13.101 → 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 +2 -2
- package/packages/cli/dist/bin.js +707 -131
- package/packages/core/dist/index.js +710 -131
- package/packages/core/public/js/tina4-dev-admin.min.js +2 -2
- package/packages/core/src/authGate.ts +6 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/metrics.ts +1 -1
- package/packages/core/src/server.ts +5 -0
- package/packages/core/src/sso.ts +285 -0
- package/packages/frond/dist/index.js +113 -109
- package/packages/frond/src/engine.ts +128 -130
- package/packages/orm/dist/index.js +715 -136
- 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/frond/src/engine.d.ts +4 -2
- 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
|
*
|
|
@@ -2622,6 +2781,7 @@ __export(engine_exports, {
|
|
|
2622
2781
|
Frond: () => Frond,
|
|
2623
2782
|
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
2624
2783
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
2784
|
+
expressionFormCache: () => expressionFormCache,
|
|
2625
2785
|
filterChainCache: () => filterChainCache,
|
|
2626
2786
|
pathParseCache: () => pathParseCache,
|
|
2627
2787
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
@@ -3030,62 +3190,58 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
3030
3190
|
parts.push(expr.slice(currentStart));
|
|
3031
3191
|
return parts;
|
|
3032
3192
|
}
|
|
3033
|
-
function
|
|
3034
|
-
expr
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
if (
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
}
|
|
3041
|
-
if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
|
|
3042
|
-
let depth = 0;
|
|
3043
|
-
let matched = true;
|
|
3044
|
-
for (let pi = 0; pi < expr.length; pi++) {
|
|
3045
|
-
if (expr[pi] === "(") depth++;
|
|
3046
|
-
else if (expr[pi] === ")") depth--;
|
|
3047
|
-
if (depth === 0 && pi < expr.length - 1) {
|
|
3048
|
-
matched = false;
|
|
3049
|
-
break;
|
|
3050
|
-
}
|
|
3051
|
-
}
|
|
3052
|
-
if (matched) {
|
|
3053
|
-
return evalExpr(expr.slice(1, -1), context);
|
|
3054
|
-
}
|
|
3193
|
+
function parenthesizedInner(expr) {
|
|
3194
|
+
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
3195
|
+
let depth = 0;
|
|
3196
|
+
for (let index = 0; index < expr.length; index++) {
|
|
3197
|
+
if (expr[index] === "(") depth++;
|
|
3198
|
+
else if (expr[index] === ")") depth--;
|
|
3199
|
+
if (depth === 0 && index < expr.length - 1) return null;
|
|
3055
3200
|
}
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
const truePart = rest.slice(0, colonIdx).trim();
|
|
3063
|
-
const falsePart = rest.slice(colonIdx + 1).trim();
|
|
3064
|
-
const cond = evalExpr(condPart, context);
|
|
3065
|
-
return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
|
|
3066
|
-
}
|
|
3201
|
+
return expr.slice(1, -1);
|
|
3202
|
+
}
|
|
3203
|
+
function evalPrimary(expr, context) {
|
|
3204
|
+
const quote = expr[0];
|
|
3205
|
+
if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
|
|
3206
|
+
return expr.slice(1, -1);
|
|
3067
3207
|
}
|
|
3208
|
+
const inner = parenthesizedInner(expr);
|
|
3209
|
+
if (inner !== null) return evalExpr(inner, context);
|
|
3210
|
+
return EXPR_NOT_MATCHED;
|
|
3211
|
+
}
|
|
3212
|
+
function evalTernaryExpression(expr, context) {
|
|
3213
|
+
const ternaryIdx = findTernary(expr);
|
|
3214
|
+
if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
|
|
3215
|
+
const rest = expr.slice(ternaryIdx + 1);
|
|
3216
|
+
const colonIdx = findColon(rest);
|
|
3217
|
+
if (colonIdx === -1) return EXPR_NOT_MATCHED;
|
|
3218
|
+
const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
|
|
3219
|
+
const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
|
|
3220
|
+
return evalExpr(branch.trim(), context);
|
|
3221
|
+
}
|
|
3222
|
+
function evalInlineIfExpression(expr, context) {
|
|
3068
3223
|
const ifIdx = findOutsideQuotes(expr, " if ");
|
|
3069
|
-
if (ifIdx
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3224
|
+
if (ifIdx < 0) return EXPR_NOT_MATCHED;
|
|
3225
|
+
const elseIdx = findOutsideQuotes(expr, " else ");
|
|
3226
|
+
if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
|
|
3227
|
+
const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
|
|
3228
|
+
const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
|
|
3229
|
+
return evalExpr(branch.trim(), context);
|
|
3230
|
+
}
|
|
3231
|
+
function evalCoalesceExpression(expr, context) {
|
|
3079
3232
|
const qqIdx = findOutsideQuotes(expr, "??");
|
|
3080
|
-
if (qqIdx
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
return
|
|
3233
|
+
if (qqIdx === -1) return EXPR_NOT_MATCHED;
|
|
3234
|
+
const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
|
|
3235
|
+
return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
|
|
3236
|
+
}
|
|
3237
|
+
function evalConditional(expr, context) {
|
|
3238
|
+
for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
|
|
3239
|
+
const result = evaluator(expr, context);
|
|
3240
|
+
if (result !== EXPR_NOT_MATCHED) return result;
|
|
3088
3241
|
}
|
|
3242
|
+
return EXPR_NOT_MATCHED;
|
|
3243
|
+
}
|
|
3244
|
+
function evalConcatOrComparison(expr, context) {
|
|
3089
3245
|
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
3090
3246
|
const parts = splitOutsideQuotes(expr, "~");
|
|
3091
3247
|
if (parts.length > 1) {
|
|
@@ -3103,6 +3259,9 @@ function evalExpr(expr, context) {
|
|
|
3103
3259
|
return evalComparison(expr, context);
|
|
3104
3260
|
}
|
|
3105
3261
|
}
|
|
3262
|
+
return EXPR_NOT_MATCHED;
|
|
3263
|
+
}
|
|
3264
|
+
function evalArithmeticExpression(expr, context) {
|
|
3106
3265
|
for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
|
|
3107
3266
|
const pos = findOutsideQuotes(expr, op);
|
|
3108
3267
|
if (pos >= 0) {
|
|
@@ -3115,40 +3274,15 @@ function evalExpr(expr, context) {
|
|
|
3115
3274
|
let rNum = rVal != null ? Number(rVal) : 0;
|
|
3116
3275
|
if (isNaN(lNum)) lNum = 0;
|
|
3117
3276
|
if (isNaN(rNum)) rNum = 0;
|
|
3118
|
-
|
|
3119
|
-
const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
|
|
3120
|
-
let result;
|
|
3121
|
-
switch (opS) {
|
|
3122
|
-
case "+":
|
|
3123
|
-
result = lNum + rNum;
|
|
3124
|
-
break;
|
|
3125
|
-
case "-":
|
|
3126
|
-
result = lNum - rNum;
|
|
3127
|
-
break;
|
|
3128
|
-
case "*":
|
|
3129
|
-
result = lNum * rNum;
|
|
3130
|
-
break;
|
|
3131
|
-
case "//":
|
|
3132
|
-
result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
|
|
3133
|
-
break;
|
|
3134
|
-
case "/":
|
|
3135
|
-
result = rNum !== 0 ? lNum / rNum : 0;
|
|
3136
|
-
break;
|
|
3137
|
-
case "%":
|
|
3138
|
-
result = rNum !== 0 ? lNum % rNum : 0;
|
|
3139
|
-
break;
|
|
3140
|
-
case "**":
|
|
3141
|
-
result = lNum ** rNum;
|
|
3142
|
-
break;
|
|
3143
|
-
default:
|
|
3144
|
-
result = 0;
|
|
3145
|
-
}
|
|
3146
|
-
return bothInt && Number.isInteger(result) ? result : result;
|
|
3277
|
+
return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
|
|
3147
3278
|
} catch {
|
|
3148
3279
|
return null;
|
|
3149
3280
|
}
|
|
3150
3281
|
}
|
|
3151
3282
|
}
|
|
3283
|
+
return EXPR_NOT_MATCHED;
|
|
3284
|
+
}
|
|
3285
|
+
function evalFilterExpression(expr, context) {
|
|
3152
3286
|
if (findOutsideQuotes(expr, "|") >= 0) {
|
|
3153
3287
|
const [baseExpr, filters] = parseFilterChain(expr);
|
|
3154
3288
|
if (filters.length > 0) {
|
|
@@ -3167,38 +3301,49 @@ function evalExpr(expr, context) {
|
|
|
3167
3301
|
return value;
|
|
3168
3302
|
}
|
|
3169
3303
|
}
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3304
|
+
return EXPR_NOT_MATCHED;
|
|
3305
|
+
}
|
|
3306
|
+
function evaluateCallArgs(rawArgs, context) {
|
|
3307
|
+
return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
|
|
3308
|
+
}
|
|
3309
|
+
function evalDottedFunction(name, rawArgs, context) {
|
|
3310
|
+
const lastDot = name.lastIndexOf(".");
|
|
3311
|
+
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
3312
|
+
const member = name.slice(lastDot + 1);
|
|
3313
|
+
if (!owner || typeof owner !== "object" || !(member in owner)) {
|
|
3314
|
+
return EXPR_NOT_MATCHED;
|
|
3315
|
+
}
|
|
3316
|
+
const method = owner[member];
|
|
3317
|
+
return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
|
|
3318
|
+
}
|
|
3319
|
+
function evalFunctionExpression(expr, context) {
|
|
3320
|
+
const match = expr.match(FN_CALL_RE);
|
|
3321
|
+
if (!match) return EXPR_NOT_MATCHED;
|
|
3322
|
+
const name = match[1];
|
|
3323
|
+
const rawArgs = match[2] || "";
|
|
3324
|
+
if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
|
|
3325
|
+
const fn = context[name] ?? resolveVar(name, context);
|
|
3326
|
+
if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
|
|
3327
|
+
return EXPR_NOT_MATCHED;
|
|
3328
|
+
}
|
|
3329
|
+
function evalExpr(expr, context) {
|
|
3330
|
+
expr = expr.trim();
|
|
3331
|
+
const cachedForm = expressionFormCache.get(expr);
|
|
3332
|
+
if (cachedForm !== void 0) {
|
|
3333
|
+
if (cachedForm === -1) return resolveVar(expr, context);
|
|
3334
|
+
const result = EXPR_EVALUATORS[cachedForm](expr, context);
|
|
3335
|
+
return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
|
|
3336
|
+
}
|
|
3337
|
+
for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
|
|
3338
|
+
const result = EXPR_EVALUATORS[index](expr, context);
|
|
3339
|
+
if (result !== EXPR_NOT_MATCHED) {
|
|
3340
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
3341
|
+
expressionFormCache.set(expr, index);
|
|
3342
|
+
return result;
|
|
3200
3343
|
}
|
|
3201
3344
|
}
|
|
3345
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
3346
|
+
expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
|
|
3202
3347
|
return resolveVar(expr, context);
|
|
3203
3348
|
}
|
|
3204
3349
|
function findTernary(expr) {
|
|
@@ -3654,7 +3799,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
3654
3799
|
function _generateFormTokenValue(descriptor = "") {
|
|
3655
3800
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
3656
3801
|
}
|
|
3657
|
-
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3802
|
+
var SafeString, KNOWN_TAGS, TERMINATOR_TAGS, GATEABLE_TAGS, BLOCK_TAG_ENDS, JSON_UNSAFE_RE, JSON_UNSAFE_MAP, NUMERIC_RE, METHOD_CALL_RE, FN_CALL_RE, IS_NOT_RE, IS_RE, NOT_IN_RE, IN_RE, DIVISIBLE_BY_RE, FILTER_WITH_ARGS_RE, FILTER_COMPARISON_RE, TITLE_WORD_RE, STRIP_TAGS_RE, FORMAT_RE, LEADING_WS_RE, TRAILING_WS_RE, THOUSANDS_RE, LIVE_RE, LIVE_WS_RE, LIVE_SRC_RE, EXTENDS_RE, EXTENDS_RE_GLOBAL, filterChainCache, pathParseCache, TEMPLATE_CACHE_MAX, MEMO_CACHE_MAX, TOKEN_RE, RAW_BLOCK_RE, EXPR_NOT_MATCHED, ARITHMETIC_OPERATIONS, EXPR_EVALUATORS, expressionFormCache, VarRef, BUILTIN_FILTERS, _formTokenSessionId, Frond;
|
|
3658
3803
|
var init_engine = __esm({
|
|
3659
3804
|
"../frond/src/engine.ts"() {
|
|
3660
3805
|
"use strict";
|
|
@@ -3756,6 +3901,25 @@ var init_engine = __esm({
|
|
|
3756
3901
|
MEMO_CACHE_MAX = 1024;
|
|
3757
3902
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
3758
3903
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
3904
|
+
EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
3905
|
+
ARITHMETIC_OPERATIONS = {
|
|
3906
|
+
"+": (left, right) => left + right,
|
|
3907
|
+
"-": (left, right) => left - right,
|
|
3908
|
+
"*": (left, right) => left * right,
|
|
3909
|
+
"//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
|
|
3910
|
+
"/": (left, right) => right !== 0 ? left / right : 0,
|
|
3911
|
+
"%": (left, right) => right !== 0 ? left % right : 0,
|
|
3912
|
+
"**": (left, right) => left ** right
|
|
3913
|
+
};
|
|
3914
|
+
EXPR_EVALUATORS = [
|
|
3915
|
+
evalPrimary,
|
|
3916
|
+
evalConditional,
|
|
3917
|
+
evalConcatOrComparison,
|
|
3918
|
+
evalArithmeticExpression,
|
|
3919
|
+
evalFilterExpression,
|
|
3920
|
+
evalFunctionExpression
|
|
3921
|
+
];
|
|
3922
|
+
expressionFormCache = /* @__PURE__ */ new Map();
|
|
3759
3923
|
VarRef = class {
|
|
3760
3924
|
constructor(name) {
|
|
3761
3925
|
this.name = name;
|
|
@@ -8768,6 +8932,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
8768
8932
|
}
|
|
8769
8933
|
}
|
|
8770
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
|
+
}
|
|
8771
8941
|
const sessionToken = req2.session?.get?.("token");
|
|
8772
8942
|
if (sessionToken && validToken(sessionToken)) {
|
|
8773
8943
|
resolvedToken = sessionToken;
|
|
@@ -12232,7 +12402,7 @@ var init_metrics = __esm({
|
|
|
12232
12402
|
};
|
|
12233
12403
|
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
12234
12404
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
12235
|
-
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "
|
|
12405
|
+
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
|
|
12236
12406
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
12237
12407
|
}
|
|
12238
12408
|
});
|
|
@@ -23319,6 +23489,14 @@ function resolveSecuritySchemes() {
|
|
|
23319
23489
|
const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
|
|
23320
23490
|
schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
|
|
23321
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
|
+
}
|
|
23322
23500
|
for (const [name, def] of Object.entries(registeredSchemes)) {
|
|
23323
23501
|
schemes[name] = def;
|
|
23324
23502
|
}
|
|
@@ -23500,7 +23678,9 @@ function generate(routes, models = []) {
|
|
|
23500
23678
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
23501
23679
|
}
|
|
23502
23680
|
} else if (routeRequiresAuth(route, method)) {
|
|
23503
|
-
|
|
23681
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
23682
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
23683
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
23504
23684
|
const responses = operation.responses;
|
|
23505
23685
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
23506
23686
|
}
|
|
@@ -23814,6 +23994,298 @@ var init_src = __esm({
|
|
|
23814
23994
|
}
|
|
23815
23995
|
});
|
|
23816
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
|
+
|
|
23817
24289
|
// ../core/src/docsAutoDiscovery.ts
|
|
23818
24290
|
var docsAutoDiscovery_exports = {};
|
|
23819
24291
|
__export(docsAutoDiscovery_exports, {
|
|
@@ -23883,7 +24355,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
23883
24355
|
|
|
23884
24356
|
// ../core/src/server.ts
|
|
23885
24357
|
import { createServer as createServer2 } from "node:http";
|
|
23886
|
-
import { randomBytes as
|
|
24358
|
+
import { randomBytes as randomBytes6 } from "node:crypto";
|
|
23887
24359
|
import { resolve as resolve14, dirname as dirname11, join as join24, relative as relative8 } from "node:path";
|
|
23888
24360
|
import { existsSync as existsSync22, readdirSync as readdirSync14, readFileSync as readFileSync21, statSync as statSync15 } from "node:fs";
|
|
23889
24361
|
import { isatty } from "node:tty";
|
|
@@ -24523,7 +24995,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
24523
24995
|
}
|
|
24524
24996
|
}
|
|
24525
24997
|
}
|
|
24526
|
-
const requestId = Log.getRequestId() ??
|
|
24998
|
+
const requestId = Log.getRequestId() ?? randomBytes6(4).toString("hex");
|
|
24527
24999
|
if (wantsJson(req2)) {
|
|
24528
25000
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
24529
25001
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -24594,7 +25066,7 @@ function serveStaticAsset(ctx) {
|
|
|
24594
25066
|
return false;
|
|
24595
25067
|
}
|
|
24596
25068
|
async function serveNotFound(ctx) {
|
|
24597
|
-
const requestId = Log.getRequestId() ??
|
|
25069
|
+
const requestId = Log.getRequestId() ?? randomBytes6(4).toString("hex");
|
|
24598
25070
|
if (wantsJson(ctx.req)) {
|
|
24599
25071
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
24600
25072
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -24714,7 +25186,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
24714
25186
|
}
|
|
24715
25187
|
}
|
|
24716
25188
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
24717
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
25189
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes6(4).toString("hex");
|
|
24718
25190
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
24719
25191
|
return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
|
|
24720
25192
|
}
|
|
@@ -24848,6 +25320,8 @@ ${reset2}
|
|
|
24848
25320
|
console.log(`
|
|
24849
25321
|
No routes directory found at ${routesDir}`);
|
|
24850
25322
|
}
|
|
25323
|
+
const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
|
|
25324
|
+
await Sso2.mountConfigured(router);
|
|
24851
25325
|
if (attachCsrfFromEnv()) {
|
|
24852
25326
|
console.log(`
|
|
24853
25327
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
@@ -25840,7 +26314,7 @@ var init_mqttMessage = __esm({
|
|
|
25840
26314
|
// ../core/src/mqtt.ts
|
|
25841
26315
|
import net2 from "node:net";
|
|
25842
26316
|
import tls from "node:tls";
|
|
25843
|
-
import { randomBytes as
|
|
26317
|
+
import { randomBytes as randomBytes7 } from "node:crypto";
|
|
25844
26318
|
import { existsSync as existsSync24, readFileSync as readFileSync22 } from "node:fs";
|
|
25845
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;
|
|
25846
26320
|
var init_mqtt = __esm({
|
|
@@ -25928,7 +26402,7 @@ var init_mqtt = __esm({
|
|
|
25928
26402
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
25929
26403
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
25930
26404
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
25931
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
26405
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes7(8).toString("hex");
|
|
25932
26406
|
this.clientId = cid;
|
|
25933
26407
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
25934
26408
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -26858,7 +27332,7 @@ var init_service = __esm({
|
|
|
26858
27332
|
import http from "node:http";
|
|
26859
27333
|
import https from "node:https";
|
|
26860
27334
|
import { URL as URL2 } from "node:url";
|
|
26861
|
-
import { randomBytes as
|
|
27335
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
26862
27336
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
26863
27337
|
import { basename as basename5 } from "node:path";
|
|
26864
27338
|
import { pipeline } from "node:stream/promises";
|
|
@@ -27156,7 +27630,7 @@ var init_api = __esm({
|
|
|
27156
27630
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
27157
27631
|
}
|
|
27158
27632
|
const partContentType = guessContentType(uploadName);
|
|
27159
|
-
const boundary = "----Tina4Boundary" +
|
|
27633
|
+
const boundary = "----Tina4Boundary" + randomBytes8(16).toString("hex");
|
|
27160
27634
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
27161
27635
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
27162
27636
|
return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
|
|
@@ -31988,10 +32462,13 @@ __export(src_exports2, {
|
|
|
31988
32462
|
RouteGroup: () => RouteGroup,
|
|
31989
32463
|
RouteRef: () => RouteRef,
|
|
31990
32464
|
Router: () => Router,
|
|
32465
|
+
SSO: () => Sso,
|
|
31991
32466
|
SafeString: () => SafeString2,
|
|
31992
32467
|
SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
|
|
31993
32468
|
ServiceRunner: () => ServiceRunner,
|
|
31994
32469
|
Session: () => Session,
|
|
32470
|
+
Sso: () => Sso,
|
|
32471
|
+
SsoError: () => SsoError,
|
|
31995
32472
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
31996
32473
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
31997
32474
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -32228,6 +32705,7 @@ var init_src2 = __esm({
|
|
|
32228
32705
|
init_htmlElement();
|
|
32229
32706
|
init_errorOverlay();
|
|
32230
32707
|
init_ai();
|
|
32708
|
+
init_sso();
|
|
32231
32709
|
init_aiClient();
|
|
32232
32710
|
init_liteBackend();
|
|
32233
32711
|
init_rabbitmqBackend();
|
|
@@ -33326,6 +33804,8 @@ function fieldTypeToPostgres(def) {
|
|
|
33326
33804
|
return "TEXT";
|
|
33327
33805
|
case "json":
|
|
33328
33806
|
return "JSONB";
|
|
33807
|
+
case "point":
|
|
33808
|
+
return SQLTranslator.pointColumnType("postgres", def.srid ?? 4326);
|
|
33329
33809
|
case "string":
|
|
33330
33810
|
return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
|
|
33331
33811
|
default:
|
|
@@ -38949,10 +39429,13 @@ var init_queryBuilder = __esm({
|
|
|
38949
39429
|
"use strict";
|
|
38950
39430
|
init_database();
|
|
38951
39431
|
init_databaseResult();
|
|
39432
|
+
init_point();
|
|
39433
|
+
init_sqlTranslator();
|
|
38952
39434
|
QueryBuilder = class _QueryBuilder {
|
|
38953
39435
|
table;
|
|
38954
39436
|
db;
|
|
38955
39437
|
columns = ["*"];
|
|
39438
|
+
selectParams = [];
|
|
38956
39439
|
wheres = [];
|
|
38957
39440
|
params = [];
|
|
38958
39441
|
joinClauses = [];
|
|
@@ -38960,14 +39443,17 @@ var init_queryBuilder = __esm({
|
|
|
38960
39443
|
havings = [];
|
|
38961
39444
|
havingParams = [];
|
|
38962
39445
|
orderByCols = [];
|
|
39446
|
+
orderByParams = [];
|
|
39447
|
+
primaryKey;
|
|
38963
39448
|
limitVal;
|
|
38964
39449
|
offsetVal;
|
|
38965
39450
|
/**
|
|
38966
39451
|
* Private constructor — use static factory methods.
|
|
38967
39452
|
*/
|
|
38968
|
-
constructor(table2, db) {
|
|
39453
|
+
constructor(table2, db, primaryKey) {
|
|
38969
39454
|
this.table = table2;
|
|
38970
39455
|
this.db = db;
|
|
39456
|
+
this.primaryKey = primaryKey;
|
|
38971
39457
|
}
|
|
38972
39458
|
/**
|
|
38973
39459
|
* Create a QueryBuilder for a table.
|
|
@@ -38976,8 +39462,8 @@ var init_queryBuilder = __esm({
|
|
|
38976
39462
|
* @param db - Optional database adapter.
|
|
38977
39463
|
* @returns A new QueryBuilder instance.
|
|
38978
39464
|
*/
|
|
38979
|
-
static fromTable(tableName, db) {
|
|
38980
|
-
return new _QueryBuilder(tableName, db);
|
|
39465
|
+
static fromTable(tableName, db, primaryKey) {
|
|
39466
|
+
return new _QueryBuilder(tableName, db, primaryKey);
|
|
38981
39467
|
}
|
|
38982
39468
|
/**
|
|
38983
39469
|
* Set the columns to select.
|
|
@@ -38988,6 +39474,7 @@ var init_queryBuilder = __esm({
|
|
|
38988
39474
|
select(...cols) {
|
|
38989
39475
|
if (cols.length > 0) {
|
|
38990
39476
|
this.columns = cols;
|
|
39477
|
+
this.selectParams = [];
|
|
38991
39478
|
}
|
|
38992
39479
|
return this;
|
|
38993
39480
|
}
|
|
@@ -39069,6 +39556,41 @@ var init_queryBuilder = __esm({
|
|
|
39069
39556
|
this.orderByCols.push(expression);
|
|
39070
39557
|
return this;
|
|
39071
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
|
+
}
|
|
39072
39594
|
/**
|
|
39073
39595
|
* Set LIMIT and optional OFFSET.
|
|
39074
39596
|
*
|
|
@@ -39134,7 +39656,7 @@ var init_queryBuilder = __esm({
|
|
|
39134
39656
|
async get() {
|
|
39135
39657
|
this.ensureDb();
|
|
39136
39658
|
const sql = this.toSql();
|
|
39137
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
39659
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
39138
39660
|
const queryParams = allParams.length > 0 ? allParams : void 0;
|
|
39139
39661
|
const rows = await adapterFetch(
|
|
39140
39662
|
this.db,
|
|
@@ -39162,7 +39684,7 @@ var init_queryBuilder = __esm({
|
|
|
39162
39684
|
async first() {
|
|
39163
39685
|
this.ensureDb();
|
|
39164
39686
|
const sql = this.toSql();
|
|
39165
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
39687
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
39166
39688
|
return adapterFetchOne(
|
|
39167
39689
|
this.db,
|
|
39168
39690
|
sql,
|
|
@@ -39177,9 +39699,18 @@ var init_queryBuilder = __esm({
|
|
|
39177
39699
|
async count() {
|
|
39178
39700
|
this.ensureDb();
|
|
39179
39701
|
const original = this.columns;
|
|
39702
|
+
const originalSelectParams = this.selectParams;
|
|
39703
|
+
const originalOrder = this.orderByCols;
|
|
39704
|
+
const originalOrderParams = this.orderByParams;
|
|
39180
39705
|
this.columns = ["COUNT(*) as cnt"];
|
|
39706
|
+
this.selectParams = [];
|
|
39707
|
+
this.orderByCols = [];
|
|
39708
|
+
this.orderByParams = [];
|
|
39181
39709
|
const sql = this.toSql();
|
|
39182
39710
|
this.columns = original;
|
|
39711
|
+
this.selectParams = originalSelectParams;
|
|
39712
|
+
this.orderByCols = originalOrder;
|
|
39713
|
+
this.orderByParams = originalOrderParams;
|
|
39183
39714
|
const allParams = [...this.params, ...this.havingParams];
|
|
39184
39715
|
const row = await adapterFetchOne(
|
|
39185
39716
|
this.db,
|
|
@@ -39364,6 +39895,10 @@ var init_queryBuilder = __esm({
|
|
|
39364
39895
|
}
|
|
39365
39896
|
}
|
|
39366
39897
|
}
|
|
39898
|
+
engine() {
|
|
39899
|
+
this.ensureDb();
|
|
39900
|
+
return this.db.getDatabaseType();
|
|
39901
|
+
}
|
|
39367
39902
|
};
|
|
39368
39903
|
}
|
|
39369
39904
|
});
|
|
@@ -39387,6 +39922,11 @@ function toDbFieldValue(def, value) {
|
|
|
39387
39922
|
if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
|
|
39388
39923
|
return JSON.stringify(value);
|
|
39389
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
|
+
}
|
|
39390
39930
|
return value;
|
|
39391
39931
|
}
|
|
39392
39932
|
function fromDbFieldValue(def, value) {
|
|
@@ -39397,6 +39937,11 @@ function fromDbFieldValue(def, value) {
|
|
|
39397
39937
|
return value;
|
|
39398
39938
|
}
|
|
39399
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
|
+
}
|
|
39400
39945
|
return value;
|
|
39401
39946
|
}
|
|
39402
39947
|
function _pluralRelKeys() {
|
|
@@ -39432,6 +39977,7 @@ var init_baseModel = __esm({
|
|
|
39432
39977
|
init_sqlite();
|
|
39433
39978
|
init_sqlTranslator();
|
|
39434
39979
|
init_src2();
|
|
39980
|
+
init_point();
|
|
39435
39981
|
_fkRegistry = /* @__PURE__ */ new Map();
|
|
39436
39982
|
EAGER_IN_CHUNK = 500;
|
|
39437
39983
|
modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
|
|
@@ -39487,7 +40033,9 @@ var init_baseModel = __esm({
|
|
|
39487
40033
|
for (const [name, def] of Object.entries(fields0)) {
|
|
39488
40034
|
if (def.default === void 0) continue;
|
|
39489
40035
|
let dv = typeof def.default === "function" ? def.default() : def.default;
|
|
39490
|
-
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);
|
|
39491
40039
|
this[name] = dv;
|
|
39492
40040
|
}
|
|
39493
40041
|
if (data) {
|
|
@@ -39589,7 +40137,7 @@ var init_baseModel = __esm({
|
|
|
39589
40137
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
39590
40138
|
*/
|
|
39591
40139
|
static query() {
|
|
39592
|
-
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
40140
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
|
|
39593
40141
|
}
|
|
39594
40142
|
/**
|
|
39595
40143
|
* Get the database adapter for this model.
|
|
@@ -40042,7 +40590,7 @@ var init_baseModel = __esm({
|
|
|
40042
40590
|
for (const key of Object.keys(ModelClass.fields)) {
|
|
40043
40591
|
if (this[key] !== void 0) {
|
|
40044
40592
|
const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
|
|
40045
|
-
result[outKey] = this[key];
|
|
40593
|
+
result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
|
|
40046
40594
|
}
|
|
40047
40595
|
}
|
|
40048
40596
|
if (ModelClass.softDelete && this.is_deleted !== void 0) {
|
|
@@ -40102,6 +40650,19 @@ var init_baseModel = __esm({
|
|
|
40102
40650
|
}
|
|
40103
40651
|
return result;
|
|
40104
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
|
+
}
|
|
40105
40666
|
/**
|
|
40106
40667
|
* Convert to an associative object (alias for toDict).
|
|
40107
40668
|
*/
|
|
@@ -40152,7 +40713,10 @@ var init_baseModel = __esm({
|
|
|
40152
40713
|
*/
|
|
40153
40714
|
static async createTable() {
|
|
40154
40715
|
const db = this.getDb();
|
|
40155
|
-
|
|
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);
|
|
40156
40720
|
if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
|
|
40157
40721
|
const mappedFields = {};
|
|
40158
40722
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
@@ -40168,7 +40732,7 @@ var init_baseModel = __esm({
|
|
|
40168
40732
|
mappedFields["is_deleted"] = { type: "integer", default: 0 };
|
|
40169
40733
|
}
|
|
40170
40734
|
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
40171
|
-
return
|
|
40735
|
+
return this.createSpatialIndexes(db, pointFields);
|
|
40172
40736
|
}
|
|
40173
40737
|
const typeMap = {
|
|
40174
40738
|
integer: "INTEGER",
|
|
@@ -40215,6 +40779,14 @@ var init_baseModel = __esm({
|
|
|
40215
40779
|
}
|
|
40216
40780
|
return true;
|
|
40217
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
|
+
}
|
|
40218
40790
|
/**
|
|
40219
40791
|
* Find a record by primary key or throw an error if not found.
|
|
40220
40792
|
*/
|
|
@@ -41154,7 +41726,7 @@ var init_seeder = __esm({
|
|
|
41154
41726
|
|
|
41155
41727
|
// src/docstore.ts
|
|
41156
41728
|
import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
|
|
41157
|
-
import { randomBytes as
|
|
41729
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
41158
41730
|
import { mkdirSync as mkdirSync19 } from "node:fs";
|
|
41159
41731
|
import { dirname as dirname14, isAbsolute as isAbsolute6, join as join31 } from "node:path";
|
|
41160
41732
|
function iso(d) {
|
|
@@ -41506,8 +42078,8 @@ var init_docstore = __esm({
|
|
|
41506
42078
|
OID_RE = /^[0-9a-fA-F]{24}$/;
|
|
41507
42079
|
ISO_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})?$/;
|
|
41508
42080
|
ObjectId = class _ObjectId {
|
|
41509
|
-
static _counter =
|
|
41510
|
-
static _process =
|
|
42081
|
+
static _counter = randomBytes9(3).readUIntBE(0, 3);
|
|
42082
|
+
static _process = randomBytes9(5);
|
|
41511
42083
|
_bytes;
|
|
41512
42084
|
constructor(oid) {
|
|
41513
42085
|
if (oid === void 0 || oid === null) {
|
|
@@ -41935,7 +42507,7 @@ var init_attachment = __esm({
|
|
|
41935
42507
|
});
|
|
41936
42508
|
|
|
41937
42509
|
// src/realtime/storage.ts
|
|
41938
|
-
import { randomBytes as
|
|
42510
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
41939
42511
|
import { mkdirSync as mkdirSync20, readFileSync as readFileSync26, writeFileSync as writeFileSync17, unlinkSync as unlinkSync8, statSync as statSync18 } from "node:fs";
|
|
41940
42512
|
import { resolve as resolve19, sep as sep5 } from "node:path";
|
|
41941
42513
|
import { createRequire as createRequire8 } from "node:module";
|
|
@@ -41946,7 +42518,7 @@ function storageKey(filename = "") {
|
|
|
41946
42518
|
const clean = raw.replace(UNSAFE, "").slice(0, 12);
|
|
41947
42519
|
if (clean) ext = `.${clean}`;
|
|
41948
42520
|
}
|
|
41949
|
-
return `${
|
|
42521
|
+
return `${randomBytes10(16).toString("hex")}${ext}`;
|
|
41950
42522
|
}
|
|
41951
42523
|
function selectStorage(storage) {
|
|
41952
42524
|
if (storage) return storage;
|
|
@@ -42394,6 +42966,7 @@ __export(index_exports, {
|
|
|
42394
42966
|
CachedDatabaseAdapter: () => CachedDatabaseAdapter,
|
|
42395
42967
|
Cursor: () => Cursor,
|
|
42396
42968
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
42969
|
+
DEFAULT_SRID: () => DEFAULT_SRID,
|
|
42397
42970
|
Database: () => Database,
|
|
42398
42971
|
DatabaseResult: () => DatabaseResult,
|
|
42399
42972
|
DatabaseUrl: () => DatabaseUrl,
|
|
@@ -42409,6 +42982,7 @@ __export(index_exports, {
|
|
|
42409
42982
|
NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
|
|
42410
42983
|
ObjectId: () => ObjectId,
|
|
42411
42984
|
OdbcAdapter: () => OdbcAdapter,
|
|
42985
|
+
Point: () => Point,
|
|
42412
42986
|
PostgresAdapter: () => PostgresAdapter,
|
|
42413
42987
|
QueryBuilder: () => QueryBuilder,
|
|
42414
42988
|
QueryCache: () => QueryCache,
|
|
@@ -42421,6 +42995,7 @@ __export(index_exports, {
|
|
|
42421
42995
|
S3Storage: () => S3Storage,
|
|
42422
42996
|
SQLTranslator: () => SQLTranslator,
|
|
42423
42997
|
SQLiteAdapter: () => SQLiteAdapter,
|
|
42998
|
+
SpatialNotSupportedError: () => SpatialNotSupportedError,
|
|
42424
42999
|
SqliteCollection: () => SqliteCollection,
|
|
42425
43000
|
SqliteDatabase: () => SqliteDatabase,
|
|
42426
43001
|
adapterColumns: () => adapterColumns,
|
|
@@ -42514,6 +43089,7 @@ var init_index = __esm({
|
|
|
42514
43089
|
init_baseModel();
|
|
42515
43090
|
init_queryBuilder();
|
|
42516
43091
|
init_sqlTranslator();
|
|
43092
|
+
init_point();
|
|
42517
43093
|
init_connectTimeout();
|
|
42518
43094
|
init_cachedDatabase();
|
|
42519
43095
|
init_fakeData2();
|
|
@@ -42537,6 +43113,7 @@ export {
|
|
|
42537
43113
|
CachedDatabaseAdapter,
|
|
42538
43114
|
Cursor,
|
|
42539
43115
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
43116
|
+
DEFAULT_SRID,
|
|
42540
43117
|
Database,
|
|
42541
43118
|
DatabaseResult,
|
|
42542
43119
|
DatabaseUrl,
|
|
@@ -42552,6 +43129,7 @@ export {
|
|
|
42552
43129
|
NOT_REQUIRED_ON_ADAPTER,
|
|
42553
43130
|
ObjectId,
|
|
42554
43131
|
OdbcAdapter,
|
|
43132
|
+
Point,
|
|
42555
43133
|
PostgresAdapter,
|
|
42556
43134
|
QueryBuilder,
|
|
42557
43135
|
QueryCache,
|
|
@@ -42564,6 +43142,7 @@ export {
|
|
|
42564
43142
|
S3Storage,
|
|
42565
43143
|
SQLTranslator,
|
|
42566
43144
|
SQLiteAdapter,
|
|
43145
|
+
SpatialNotSupportedError,
|
|
42567
43146
|
SqliteCollection,
|
|
42568
43147
|
SqliteDatabase,
|
|
42569
43148
|
adapterColumns,
|