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
|
@@ -1462,6 +1462,7 @@ __export(engine_exports, {
|
|
|
1462
1462
|
Frond: () => Frond,
|
|
1463
1463
|
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
1464
1464
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
1465
|
+
expressionFormCache: () => expressionFormCache,
|
|
1465
1466
|
filterChainCache: () => filterChainCache,
|
|
1466
1467
|
pathParseCache: () => pathParseCache,
|
|
1467
1468
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
@@ -1870,62 +1871,58 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
1870
1871
|
parts.push(expr.slice(currentStart));
|
|
1871
1872
|
return parts;
|
|
1872
1873
|
}
|
|
1873
|
-
function
|
|
1874
|
-
expr
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
if (
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
}
|
|
1881
|
-
if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
|
|
1882
|
-
let depth = 0;
|
|
1883
|
-
let matched = true;
|
|
1884
|
-
for (let pi = 0; pi < expr.length; pi++) {
|
|
1885
|
-
if (expr[pi] === "(") depth++;
|
|
1886
|
-
else if (expr[pi] === ")") depth--;
|
|
1887
|
-
if (depth === 0 && pi < expr.length - 1) {
|
|
1888
|
-
matched = false;
|
|
1889
|
-
break;
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
if (matched) {
|
|
1893
|
-
return evalExpr(expr.slice(1, -1), context);
|
|
1894
|
-
}
|
|
1874
|
+
function parenthesizedInner(expr) {
|
|
1875
|
+
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
1876
|
+
let depth = 0;
|
|
1877
|
+
for (let index = 0; index < expr.length; index++) {
|
|
1878
|
+
if (expr[index] === "(") depth++;
|
|
1879
|
+
else if (expr[index] === ")") depth--;
|
|
1880
|
+
if (depth === 0 && index < expr.length - 1) return null;
|
|
1895
1881
|
}
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
const truePart = rest.slice(0, colonIdx).trim();
|
|
1903
|
-
const falsePart = rest.slice(colonIdx + 1).trim();
|
|
1904
|
-
const cond = evalExpr(condPart, context);
|
|
1905
|
-
return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
|
|
1906
|
-
}
|
|
1882
|
+
return expr.slice(1, -1);
|
|
1883
|
+
}
|
|
1884
|
+
function evalPrimary(expr, context) {
|
|
1885
|
+
const quote = expr[0];
|
|
1886
|
+
if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
|
|
1887
|
+
return expr.slice(1, -1);
|
|
1907
1888
|
}
|
|
1889
|
+
const inner = parenthesizedInner(expr);
|
|
1890
|
+
if (inner !== null) return evalExpr(inner, context);
|
|
1891
|
+
return EXPR_NOT_MATCHED;
|
|
1892
|
+
}
|
|
1893
|
+
function evalTernaryExpression(expr, context) {
|
|
1894
|
+
const ternaryIdx = findTernary(expr);
|
|
1895
|
+
if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
|
|
1896
|
+
const rest = expr.slice(ternaryIdx + 1);
|
|
1897
|
+
const colonIdx = findColon(rest);
|
|
1898
|
+
if (colonIdx === -1) return EXPR_NOT_MATCHED;
|
|
1899
|
+
const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
|
|
1900
|
+
const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
|
|
1901
|
+
return evalExpr(branch.trim(), context);
|
|
1902
|
+
}
|
|
1903
|
+
function evalInlineIfExpression(expr, context) {
|
|
1908
1904
|
const ifIdx = findOutsideQuotes(expr, " if ");
|
|
1909
|
-
if (ifIdx
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1905
|
+
if (ifIdx < 0) return EXPR_NOT_MATCHED;
|
|
1906
|
+
const elseIdx = findOutsideQuotes(expr, " else ");
|
|
1907
|
+
if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
|
|
1908
|
+
const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
|
|
1909
|
+
const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
|
|
1910
|
+
return evalExpr(branch.trim(), context);
|
|
1911
|
+
}
|
|
1912
|
+
function evalCoalesceExpression(expr, context) {
|
|
1919
1913
|
const qqIdx = findOutsideQuotes(expr, "??");
|
|
1920
|
-
if (qqIdx
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
return
|
|
1914
|
+
if (qqIdx === -1) return EXPR_NOT_MATCHED;
|
|
1915
|
+
const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
|
|
1916
|
+
return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
|
|
1917
|
+
}
|
|
1918
|
+
function evalConditional(expr, context) {
|
|
1919
|
+
for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
|
|
1920
|
+
const result = evaluator(expr, context);
|
|
1921
|
+
if (result !== EXPR_NOT_MATCHED) return result;
|
|
1928
1922
|
}
|
|
1923
|
+
return EXPR_NOT_MATCHED;
|
|
1924
|
+
}
|
|
1925
|
+
function evalConcatOrComparison(expr, context) {
|
|
1929
1926
|
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
1930
1927
|
const parts = splitOutsideQuotes(expr, "~");
|
|
1931
1928
|
if (parts.length > 1) {
|
|
@@ -1943,6 +1940,9 @@ function evalExpr(expr, context) {
|
|
|
1943
1940
|
return evalComparison(expr, context);
|
|
1944
1941
|
}
|
|
1945
1942
|
}
|
|
1943
|
+
return EXPR_NOT_MATCHED;
|
|
1944
|
+
}
|
|
1945
|
+
function evalArithmeticExpression(expr, context) {
|
|
1946
1946
|
for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
|
|
1947
1947
|
const pos = findOutsideQuotes(expr, op);
|
|
1948
1948
|
if (pos >= 0) {
|
|
@@ -1955,40 +1955,15 @@ function evalExpr(expr, context) {
|
|
|
1955
1955
|
let rNum = rVal != null ? Number(rVal) : 0;
|
|
1956
1956
|
if (isNaN(lNum)) lNum = 0;
|
|
1957
1957
|
if (isNaN(rNum)) rNum = 0;
|
|
1958
|
-
|
|
1959
|
-
const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
|
|
1960
|
-
let result;
|
|
1961
|
-
switch (opS) {
|
|
1962
|
-
case "+":
|
|
1963
|
-
result = lNum + rNum;
|
|
1964
|
-
break;
|
|
1965
|
-
case "-":
|
|
1966
|
-
result = lNum - rNum;
|
|
1967
|
-
break;
|
|
1968
|
-
case "*":
|
|
1969
|
-
result = lNum * rNum;
|
|
1970
|
-
break;
|
|
1971
|
-
case "//":
|
|
1972
|
-
result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
|
|
1973
|
-
break;
|
|
1974
|
-
case "/":
|
|
1975
|
-
result = rNum !== 0 ? lNum / rNum : 0;
|
|
1976
|
-
break;
|
|
1977
|
-
case "%":
|
|
1978
|
-
result = rNum !== 0 ? lNum % rNum : 0;
|
|
1979
|
-
break;
|
|
1980
|
-
case "**":
|
|
1981
|
-
result = lNum ** rNum;
|
|
1982
|
-
break;
|
|
1983
|
-
default:
|
|
1984
|
-
result = 0;
|
|
1985
|
-
}
|
|
1986
|
-
return bothInt && Number.isInteger(result) ? result : result;
|
|
1958
|
+
return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
|
|
1987
1959
|
} catch {
|
|
1988
1960
|
return null;
|
|
1989
1961
|
}
|
|
1990
1962
|
}
|
|
1991
1963
|
}
|
|
1964
|
+
return EXPR_NOT_MATCHED;
|
|
1965
|
+
}
|
|
1966
|
+
function evalFilterExpression(expr, context) {
|
|
1992
1967
|
if (findOutsideQuotes(expr, "|") >= 0) {
|
|
1993
1968
|
const [baseExpr, filters] = parseFilterChain(expr);
|
|
1994
1969
|
if (filters.length > 0) {
|
|
@@ -2007,38 +1982,49 @@ function evalExpr(expr, context) {
|
|
|
2007
1982
|
return value;
|
|
2008
1983
|
}
|
|
2009
1984
|
}
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
1985
|
+
return EXPR_NOT_MATCHED;
|
|
1986
|
+
}
|
|
1987
|
+
function evaluateCallArgs(rawArgs, context) {
|
|
1988
|
+
return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
|
|
1989
|
+
}
|
|
1990
|
+
function evalDottedFunction(name, rawArgs, context) {
|
|
1991
|
+
const lastDot = name.lastIndexOf(".");
|
|
1992
|
+
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
1993
|
+
const member = name.slice(lastDot + 1);
|
|
1994
|
+
if (!owner || typeof owner !== "object" || !(member in owner)) {
|
|
1995
|
+
return EXPR_NOT_MATCHED;
|
|
1996
|
+
}
|
|
1997
|
+
const method = owner[member];
|
|
1998
|
+
return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
|
|
1999
|
+
}
|
|
2000
|
+
function evalFunctionExpression(expr, context) {
|
|
2001
|
+
const match = expr.match(FN_CALL_RE);
|
|
2002
|
+
if (!match) return EXPR_NOT_MATCHED;
|
|
2003
|
+
const name = match[1];
|
|
2004
|
+
const rawArgs = match[2] || "";
|
|
2005
|
+
if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
|
|
2006
|
+
const fn = context[name] ?? resolveVar(name, context);
|
|
2007
|
+
if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
|
|
2008
|
+
return EXPR_NOT_MATCHED;
|
|
2009
|
+
}
|
|
2010
|
+
function evalExpr(expr, context) {
|
|
2011
|
+
expr = expr.trim();
|
|
2012
|
+
const cachedForm = expressionFormCache.get(expr);
|
|
2013
|
+
if (cachedForm !== void 0) {
|
|
2014
|
+
if (cachedForm === -1) return resolveVar(expr, context);
|
|
2015
|
+
const result = EXPR_EVALUATORS[cachedForm](expr, context);
|
|
2016
|
+
return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
|
|
2017
|
+
}
|
|
2018
|
+
for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
|
|
2019
|
+
const result = EXPR_EVALUATORS[index](expr, context);
|
|
2020
|
+
if (result !== EXPR_NOT_MATCHED) {
|
|
2021
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
2022
|
+
expressionFormCache.set(expr, index);
|
|
2023
|
+
return result;
|
|
2040
2024
|
}
|
|
2041
2025
|
}
|
|
2026
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
2027
|
+
expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
|
|
2042
2028
|
return resolveVar(expr, context);
|
|
2043
2029
|
}
|
|
2044
2030
|
function findTernary(expr) {
|
|
@@ -2494,7 +2480,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
2494
2480
|
function _generateFormTokenValue(descriptor = "") {
|
|
2495
2481
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
2496
2482
|
}
|
|
2497
|
-
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;
|
|
2483
|
+
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;
|
|
2498
2484
|
var init_engine = __esm({
|
|
2499
2485
|
"../frond/src/engine.ts"() {
|
|
2500
2486
|
"use strict";
|
|
@@ -2596,6 +2582,25 @@ var init_engine = __esm({
|
|
|
2596
2582
|
MEMO_CACHE_MAX = 1024;
|
|
2597
2583
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
2598
2584
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
2585
|
+
EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
2586
|
+
ARITHMETIC_OPERATIONS = {
|
|
2587
|
+
"+": (left, right) => left + right,
|
|
2588
|
+
"-": (left, right) => left - right,
|
|
2589
|
+
"*": (left, right) => left * right,
|
|
2590
|
+
"//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
|
|
2591
|
+
"/": (left, right) => right !== 0 ? left / right : 0,
|
|
2592
|
+
"%": (left, right) => right !== 0 ? left % right : 0,
|
|
2593
|
+
"**": (left, right) => left ** right
|
|
2594
|
+
};
|
|
2595
|
+
EXPR_EVALUATORS = [
|
|
2596
|
+
evalPrimary,
|
|
2597
|
+
evalConditional,
|
|
2598
|
+
evalConcatOrComparison,
|
|
2599
|
+
evalArithmeticExpression,
|
|
2600
|
+
evalFilterExpression,
|
|
2601
|
+
evalFunctionExpression
|
|
2602
|
+
];
|
|
2603
|
+
expressionFormCache = /* @__PURE__ */ new Map();
|
|
2599
2604
|
VarRef = class {
|
|
2600
2605
|
constructor(name) {
|
|
2601
2606
|
this.name = name;
|
|
@@ -6009,13 +6014,172 @@ var init_databaseUrl = __esm({
|
|
|
6009
6014
|
}
|
|
6010
6015
|
});
|
|
6011
6016
|
|
|
6017
|
+
// ../orm/src/point.ts
|
|
6018
|
+
function formatCoordinate(value) {
|
|
6019
|
+
return Object.is(value, -0) ? "0" : Number(value.toPrecision(15)).toString();
|
|
6020
|
+
}
|
|
6021
|
+
var DEFAULT_SRID, SpatialNotSupportedError, Point;
|
|
6022
|
+
var init_point = __esm({
|
|
6023
|
+
"../orm/src/point.ts"() {
|
|
6024
|
+
"use strict";
|
|
6025
|
+
DEFAULT_SRID = 4326;
|
|
6026
|
+
SpatialNotSupportedError = class extends Error {
|
|
6027
|
+
constructor(message) {
|
|
6028
|
+
super(message);
|
|
6029
|
+
this.name = "SpatialNotSupportedError";
|
|
6030
|
+
}
|
|
6031
|
+
};
|
|
6032
|
+
Point = class _Point {
|
|
6033
|
+
lon;
|
|
6034
|
+
lat;
|
|
6035
|
+
srid;
|
|
6036
|
+
constructor(lon, lat, srid = DEFAULT_SRID) {
|
|
6037
|
+
if (typeof lon === "boolean" || typeof lat === "boolean" || typeof srid === "boolean") {
|
|
6038
|
+
throw new TypeError("Point longitude, latitude and SRID must be numbers");
|
|
6039
|
+
}
|
|
6040
|
+
this.lon = Number(lon);
|
|
6041
|
+
this.lat = Number(lat);
|
|
6042
|
+
this.srid = Number(srid);
|
|
6043
|
+
if (!Number.isFinite(this.lon) || !Number.isFinite(this.lat) || !Number.isInteger(this.srid)) {
|
|
6044
|
+
throw new TypeError("Point longitude and latitude must be finite numbers and SRID must be an integer");
|
|
6045
|
+
}
|
|
6046
|
+
if (this.srid === DEFAULT_SRID) {
|
|
6047
|
+
if (this.lon < -180 || this.lon > 180) throw new RangeError(`Point longitude ${this.lon} is outside -180..180; Tina4 uses longitude, latitude order`);
|
|
6048
|
+
if (this.lat < -90 || this.lat > 90) throw new RangeError(`Point latitude ${this.lat} is outside -90..90; Tina4 uses longitude, latitude order`);
|
|
6049
|
+
}
|
|
6050
|
+
Object.freeze(this);
|
|
6051
|
+
}
|
|
6052
|
+
get wkt() {
|
|
6053
|
+
return `POINT(${formatCoordinate(this.lon)} ${formatCoordinate(this.lat)})`;
|
|
6054
|
+
}
|
|
6055
|
+
get ewkt() {
|
|
6056
|
+
return `SRID=${this.srid};${this.wkt}`;
|
|
6057
|
+
}
|
|
6058
|
+
get geojson() {
|
|
6059
|
+
return { type: "Point", coordinates: [this.lon, this.lat] };
|
|
6060
|
+
}
|
|
6061
|
+
toJSON() {
|
|
6062
|
+
return this.geojson;
|
|
6063
|
+
}
|
|
6064
|
+
toArray() {
|
|
6065
|
+
return [this.lon, this.lat];
|
|
6066
|
+
}
|
|
6067
|
+
static parse(value, srid = DEFAULT_SRID) {
|
|
6068
|
+
if (value instanceof _Point) return value;
|
|
6069
|
+
if (Array.isArray(value)) {
|
|
6070
|
+
if (value.length < 2) throw new TypeError("Point coordinate pair needs longitude and latitude");
|
|
6071
|
+
return new _Point(value[0], value[1], srid);
|
|
6072
|
+
}
|
|
6073
|
+
if (value && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
6074
|
+
return _Point.fromGeoJson(value, srid);
|
|
6075
|
+
}
|
|
6076
|
+
if (value instanceof Uint8Array) return _Point.fromWkb(value, srid);
|
|
6077
|
+
if (typeof value === "string") {
|
|
6078
|
+
const text = value.trim();
|
|
6079
|
+
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);
|
|
6080
|
+
if (match) return new _Point(match[2], match[3], match[1] ? Number(match[1]) : srid);
|
|
6081
|
+
if (text.length >= 42 && text.length % 2 === 0 && /^[0-9a-f]+$/i.test(text)) {
|
|
6082
|
+
return _Point.fromWkb(Uint8Array.from(Buffer.from(text, "hex")), srid);
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
throw new TypeError("Point must be Point, [longitude, latitude], WKT/EWKT, GeoJSON or WKB/EWKB");
|
|
6086
|
+
}
|
|
6087
|
+
static geometryBinding(value, srid = DEFAULT_SRID) {
|
|
6088
|
+
if (value instanceof _Point || Array.isArray(value)) return [_Point.parse(value, srid).ewkt, "ewkt"];
|
|
6089
|
+
if (value && typeof value === "object") {
|
|
6090
|
+
const candidate = value;
|
|
6091
|
+
const geometry = String(candidate.type).toLowerCase() === "feature" ? candidate.geometry : candidate;
|
|
6092
|
+
const allowed = /* @__PURE__ */ new Set(["point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection"]);
|
|
6093
|
+
if (!geometry || !allowed.has(String(geometry.type).toLowerCase())) throw new TypeError("GeoJSON geometry has an unsupported type");
|
|
6094
|
+
return [JSON.stringify(geometry), "geojson"];
|
|
6095
|
+
}
|
|
6096
|
+
if (typeof value === "string" && /^\s*(?:SRID\s*=\s*\d+\s*;\s*)?(?:POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)\b/i.test(value)) {
|
|
6097
|
+
return [/^\s*SRID/i.test(value) ? value.trim() : `SRID=${srid};${value.trim()}`, "ewkt"];
|
|
6098
|
+
}
|
|
6099
|
+
throw new TypeError("Geometry must be Point, coordinate pair, WKT/EWKT or GeoJSON");
|
|
6100
|
+
}
|
|
6101
|
+
static fromGeoJson(data, srid) {
|
|
6102
|
+
const geometry = String(data.type).toLowerCase() === "feature" ? data.geometry : data;
|
|
6103
|
+
if (!geometry || String(geometry.type).toLowerCase() !== "point") throw new TypeError("Point GeoJSON type must be Point");
|
|
6104
|
+
const coordinates = geometry.coordinates;
|
|
6105
|
+
if (!Array.isArray(coordinates) || coordinates.length < 2) throw new TypeError("Point GeoJSON coordinates must be [longitude, latitude]");
|
|
6106
|
+
return new _Point(coordinates[0], coordinates[1], srid);
|
|
6107
|
+
}
|
|
6108
|
+
static fromWkb(raw, srid) {
|
|
6109
|
+
if (raw.byteLength < 21) throw new TypeError("Point WKB is too short");
|
|
6110
|
+
const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
6111
|
+
const little = raw[0] === 1;
|
|
6112
|
+
const typeWord = view.getUint32(1, little);
|
|
6113
|
+
let offset = 5;
|
|
6114
|
+
if ((typeWord & 536870912) !== 0) {
|
|
6115
|
+
srid = view.getUint32(5, little);
|
|
6116
|
+
offset = 9;
|
|
6117
|
+
}
|
|
6118
|
+
const code = (typeWord & ~(536870912 | 1073741824 | 2147483648)) % 1e3;
|
|
6119
|
+
if (code !== 1 || raw.byteLength < offset + 16) throw new TypeError("WKB geometry is not a Point");
|
|
6120
|
+
return new _Point(view.getFloat64(offset, little), view.getFloat64(offset + 8, little), srid);
|
|
6121
|
+
}
|
|
6122
|
+
};
|
|
6123
|
+
}
|
|
6124
|
+
});
|
|
6125
|
+
|
|
6012
6126
|
// ../orm/src/sqlTranslator.ts
|
|
6013
6127
|
var SQLTranslator, QueryCache;
|
|
6014
6128
|
var init_sqlTranslator = __esm({
|
|
6015
6129
|
"../orm/src/sqlTranslator.ts"() {
|
|
6016
6130
|
"use strict";
|
|
6017
6131
|
init_databaseUrl();
|
|
6132
|
+
init_point();
|
|
6018
6133
|
SQLTranslator = class _SQLTranslator {
|
|
6134
|
+
static SPATIAL_ENGINES = /* @__PURE__ */ new Set(["postgres", "postgresql"]);
|
|
6135
|
+
static SPATIAL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
6136
|
+
static requireSpatial(engine, feature) {
|
|
6137
|
+
const name = String(engine || "unknown").toLowerCase();
|
|
6138
|
+
if (!_SQLTranslator.SPATIAL_ENGINES.has(name)) {
|
|
6139
|
+
throw new SpatialNotSupportedError(
|
|
6140
|
+
`${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.`
|
|
6141
|
+
);
|
|
6142
|
+
}
|
|
6143
|
+
return name;
|
|
6144
|
+
}
|
|
6145
|
+
static spatialIdentifier(name, what = "column") {
|
|
6146
|
+
if (!_SQLTranslator.SPATIAL_IDENTIFIER.test(name)) throw new TypeError(`Spatial ${what} is not a valid SQL identifier: ${name}`);
|
|
6147
|
+
return name;
|
|
6148
|
+
}
|
|
6149
|
+
static pointColumnType(engine, srid = DEFAULT_SRID) {
|
|
6150
|
+
_SQLTranslator.requireSpatial(engine, "PointField");
|
|
6151
|
+
return `geography(Point,${srid})`;
|
|
6152
|
+
}
|
|
6153
|
+
static spatialIndex(engine, table2, column2) {
|
|
6154
|
+
_SQLTranslator.requireSpatial(engine, "spatial index creation");
|
|
6155
|
+
table2 = _SQLTranslator.spatialIdentifier(table2, "table");
|
|
6156
|
+
column2 = _SQLTranslator.spatialIdentifier(column2);
|
|
6157
|
+
return `CREATE INDEX IF NOT EXISTS ${table2.replaceAll(".", "_")}_${column2}_gist ON ${table2} USING GIST (${column2})`;
|
|
6158
|
+
}
|
|
6159
|
+
static pointLiteral(engine, srid = DEFAULT_SRID) {
|
|
6160
|
+
_SQLTranslator.requireSpatial(engine, "spatial predicates");
|
|
6161
|
+
return `ST_SetSRID(ST_MakePoint(?, ?), ${srid})::geography`;
|
|
6162
|
+
}
|
|
6163
|
+
static withinDistance(engine, column2, srid = DEFAULT_SRID) {
|
|
6164
|
+
return `ST_DWithin(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.pointLiteral(engine, srid)}, ?)`;
|
|
6165
|
+
}
|
|
6166
|
+
static distance(engine, column2, srid = DEFAULT_SRID) {
|
|
6167
|
+
return `ST_Distance(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.pointLiteral(engine, srid)})`;
|
|
6168
|
+
}
|
|
6169
|
+
static distanceAs(engine, column2, alias, srid = DEFAULT_SRID) {
|
|
6170
|
+
return `${_SQLTranslator.distance(engine, column2, srid)} AS ${_SQLTranslator.spatialIdentifier(alias, "result alias")}`;
|
|
6171
|
+
}
|
|
6172
|
+
static geometryLiteral(engine, form, srid = DEFAULT_SRID) {
|
|
6173
|
+
_SQLTranslator.requireSpatial(engine, "spatial predicates");
|
|
6174
|
+
return form === "ewkt" ? "ST_GeogFromText(?)" : `ST_SetSRID(ST_GeomFromGeoJSON(?), ${srid})::geography`;
|
|
6175
|
+
}
|
|
6176
|
+
static intersects(engine, column2, form = "ewkt", srid = DEFAULT_SRID) {
|
|
6177
|
+
return `ST_Intersects(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.geometryLiteral(engine, form, srid)})`;
|
|
6178
|
+
}
|
|
6179
|
+
static bbox(engine, column2, srid = DEFAULT_SRID) {
|
|
6180
|
+
_SQLTranslator.requireSpatial(engine, "bbox");
|
|
6181
|
+
return `ST_Intersects(${_SQLTranslator.spatialIdentifier(column2)}, ST_MakeEnvelope(?, ?, ?, ?, ${srid})::geography)`;
|
|
6182
|
+
}
|
|
6019
6183
|
/**
|
|
6020
6184
|
* Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
6021
6185
|
*
|
|
@@ -7669,6 +7833,8 @@ function fieldTypeToPostgres(def) {
|
|
|
7669
7833
|
return "TEXT";
|
|
7670
7834
|
case "json":
|
|
7671
7835
|
return "JSONB";
|
|
7836
|
+
case "point":
|
|
7837
|
+
return SQLTranslator.pointColumnType("postgres", def.srid ?? 4326);
|
|
7672
7838
|
case "string":
|
|
7673
7839
|
return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
|
|
7674
7840
|
default:
|
|
@@ -13292,10 +13458,13 @@ var init_queryBuilder = __esm({
|
|
|
13292
13458
|
"use strict";
|
|
13293
13459
|
init_database();
|
|
13294
13460
|
init_databaseResult();
|
|
13461
|
+
init_point();
|
|
13462
|
+
init_sqlTranslator();
|
|
13295
13463
|
QueryBuilder = class _QueryBuilder {
|
|
13296
13464
|
table;
|
|
13297
13465
|
db;
|
|
13298
13466
|
columns = ["*"];
|
|
13467
|
+
selectParams = [];
|
|
13299
13468
|
wheres = [];
|
|
13300
13469
|
params = [];
|
|
13301
13470
|
joinClauses = [];
|
|
@@ -13303,14 +13472,17 @@ var init_queryBuilder = __esm({
|
|
|
13303
13472
|
havings = [];
|
|
13304
13473
|
havingParams = [];
|
|
13305
13474
|
orderByCols = [];
|
|
13475
|
+
orderByParams = [];
|
|
13476
|
+
primaryKey;
|
|
13306
13477
|
limitVal;
|
|
13307
13478
|
offsetVal;
|
|
13308
13479
|
/**
|
|
13309
13480
|
* Private constructor — use static factory methods.
|
|
13310
13481
|
*/
|
|
13311
|
-
constructor(table2, db) {
|
|
13482
|
+
constructor(table2, db, primaryKey) {
|
|
13312
13483
|
this.table = table2;
|
|
13313
13484
|
this.db = db;
|
|
13485
|
+
this.primaryKey = primaryKey;
|
|
13314
13486
|
}
|
|
13315
13487
|
/**
|
|
13316
13488
|
* Create a QueryBuilder for a table.
|
|
@@ -13319,8 +13491,8 @@ var init_queryBuilder = __esm({
|
|
|
13319
13491
|
* @param db - Optional database adapter.
|
|
13320
13492
|
* @returns A new QueryBuilder instance.
|
|
13321
13493
|
*/
|
|
13322
|
-
static fromTable(tableName, db) {
|
|
13323
|
-
return new _QueryBuilder(tableName, db);
|
|
13494
|
+
static fromTable(tableName, db, primaryKey) {
|
|
13495
|
+
return new _QueryBuilder(tableName, db, primaryKey);
|
|
13324
13496
|
}
|
|
13325
13497
|
/**
|
|
13326
13498
|
* Set the columns to select.
|
|
@@ -13331,6 +13503,7 @@ var init_queryBuilder = __esm({
|
|
|
13331
13503
|
select(...cols) {
|
|
13332
13504
|
if (cols.length > 0) {
|
|
13333
13505
|
this.columns = cols;
|
|
13506
|
+
this.selectParams = [];
|
|
13334
13507
|
}
|
|
13335
13508
|
return this;
|
|
13336
13509
|
}
|
|
@@ -13412,6 +13585,41 @@ var init_queryBuilder = __esm({
|
|
|
13412
13585
|
this.orderByCols.push(expression);
|
|
13413
13586
|
return this;
|
|
13414
13587
|
}
|
|
13588
|
+
withinDistance(column2, pointValue, radiusMetres, srid = DEFAULT_SRID) {
|
|
13589
|
+
const radius = Number(radiusMetres);
|
|
13590
|
+
if (!Number.isFinite(radius) || radius < 0) throw new RangeError("Spatial radius must be finite and greater than or equal to zero");
|
|
13591
|
+
const point = Point.parse(pointValue, srid);
|
|
13592
|
+
return this.where(SQLTranslator.withinDistance(this.engine(), column2, point.srid), [point.lon, point.lat, radius]);
|
|
13593
|
+
}
|
|
13594
|
+
intersects(column2, geometry, srid = DEFAULT_SRID) {
|
|
13595
|
+
const [bound, form] = Point.geometryBinding(geometry, srid);
|
|
13596
|
+
return this.where(SQLTranslator.intersects(this.engine(), column2, form, srid), [bound]);
|
|
13597
|
+
}
|
|
13598
|
+
bbox(column2, minLon, minLat, maxLon, maxLat, srid = DEFAULT_SRID) {
|
|
13599
|
+
const values = [minLon, minLat, maxLon, maxLat].map(Number);
|
|
13600
|
+
if (!values.every(Number.isFinite)) throw new TypeError("Bounding-box coordinates must be finite numbers");
|
|
13601
|
+
const [west, south, east, north] = values;
|
|
13602
|
+
new Point(west, south, srid);
|
|
13603
|
+
new Point(east, north, srid);
|
|
13604
|
+
if (west > east || south > north) throw new RangeError("Bounding box must be ordered west, south, east, north");
|
|
13605
|
+
return this.where(SQLTranslator.bbox(this.engine(), column2, srid), values);
|
|
13606
|
+
}
|
|
13607
|
+
selectDistance(column2, pointValue, alias = "distance", srid = DEFAULT_SRID) {
|
|
13608
|
+
const point = Point.parse(pointValue, srid);
|
|
13609
|
+
this.columns.push(SQLTranslator.distanceAs(this.engine(), column2, alias, point.srid));
|
|
13610
|
+
this.selectParams.push(point.lon, point.lat);
|
|
13611
|
+
return this;
|
|
13612
|
+
}
|
|
13613
|
+
orderByDistance(column2, pointValue, direction = "ASC", srid = DEFAULT_SRID) {
|
|
13614
|
+
const order = direction.toUpperCase();
|
|
13615
|
+
if (order !== "ASC" && order !== "DESC") throw new TypeError("Distance order direction must be ASC or DESC");
|
|
13616
|
+
if (!this.primaryKey) throw new Error("Stable spatial ordering needs a primary key; use BaseModel.query() or pass one to fromTable()");
|
|
13617
|
+
const point = Point.parse(pointValue, srid);
|
|
13618
|
+
this.orderByCols.push(`${SQLTranslator.distance(this.engine(), column2, point.srid)} ${order}`);
|
|
13619
|
+
this.orderByParams.push(point.lon, point.lat);
|
|
13620
|
+
this.orderByCols.push(`${SQLTranslator.spatialIdentifier(this.primaryKey, "primary key")} ASC`);
|
|
13621
|
+
return this;
|
|
13622
|
+
}
|
|
13415
13623
|
/**
|
|
13416
13624
|
* Set LIMIT and optional OFFSET.
|
|
13417
13625
|
*
|
|
@@ -13477,7 +13685,7 @@ var init_queryBuilder = __esm({
|
|
|
13477
13685
|
async get() {
|
|
13478
13686
|
this.ensureDb();
|
|
13479
13687
|
const sql = this.toSql();
|
|
13480
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13688
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13481
13689
|
const queryParams = allParams.length > 0 ? allParams : void 0;
|
|
13482
13690
|
const rows = await adapterFetch(
|
|
13483
13691
|
this.db,
|
|
@@ -13505,7 +13713,7 @@ var init_queryBuilder = __esm({
|
|
|
13505
13713
|
async first() {
|
|
13506
13714
|
this.ensureDb();
|
|
13507
13715
|
const sql = this.toSql();
|
|
13508
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13716
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13509
13717
|
return adapterFetchOne(
|
|
13510
13718
|
this.db,
|
|
13511
13719
|
sql,
|
|
@@ -13520,9 +13728,18 @@ var init_queryBuilder = __esm({
|
|
|
13520
13728
|
async count() {
|
|
13521
13729
|
this.ensureDb();
|
|
13522
13730
|
const original = this.columns;
|
|
13731
|
+
const originalSelectParams = this.selectParams;
|
|
13732
|
+
const originalOrder = this.orderByCols;
|
|
13733
|
+
const originalOrderParams = this.orderByParams;
|
|
13523
13734
|
this.columns = ["COUNT(*) as cnt"];
|
|
13735
|
+
this.selectParams = [];
|
|
13736
|
+
this.orderByCols = [];
|
|
13737
|
+
this.orderByParams = [];
|
|
13524
13738
|
const sql = this.toSql();
|
|
13525
13739
|
this.columns = original;
|
|
13740
|
+
this.selectParams = originalSelectParams;
|
|
13741
|
+
this.orderByCols = originalOrder;
|
|
13742
|
+
this.orderByParams = originalOrderParams;
|
|
13526
13743
|
const allParams = [...this.params, ...this.havingParams];
|
|
13527
13744
|
const row = await adapterFetchOne(
|
|
13528
13745
|
this.db,
|
|
@@ -13707,6 +13924,10 @@ var init_queryBuilder = __esm({
|
|
|
13707
13924
|
}
|
|
13708
13925
|
}
|
|
13709
13926
|
}
|
|
13927
|
+
engine() {
|
|
13928
|
+
this.ensureDb();
|
|
13929
|
+
return this.db.getDatabaseType();
|
|
13930
|
+
}
|
|
13710
13931
|
};
|
|
13711
13932
|
}
|
|
13712
13933
|
});
|
|
@@ -13730,6 +13951,11 @@ function toDbFieldValue(def, value) {
|
|
|
13730
13951
|
if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
|
|
13731
13952
|
return JSON.stringify(value);
|
|
13732
13953
|
}
|
|
13954
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
13955
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
13956
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
13957
|
+
return point.ewkt;
|
|
13958
|
+
}
|
|
13733
13959
|
return value;
|
|
13734
13960
|
}
|
|
13735
13961
|
function fromDbFieldValue(def, value) {
|
|
@@ -13740,6 +13966,11 @@ function fromDbFieldValue(def, value) {
|
|
|
13740
13966
|
return value;
|
|
13741
13967
|
}
|
|
13742
13968
|
}
|
|
13969
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
13970
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
13971
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
13972
|
+
return point;
|
|
13973
|
+
}
|
|
13743
13974
|
return value;
|
|
13744
13975
|
}
|
|
13745
13976
|
function _pluralRelKeys() {
|
|
@@ -13775,6 +14006,7 @@ var init_baseModel = __esm({
|
|
|
13775
14006
|
init_sqlite();
|
|
13776
14007
|
init_sqlTranslator();
|
|
13777
14008
|
init_index();
|
|
14009
|
+
init_point();
|
|
13778
14010
|
_fkRegistry = /* @__PURE__ */ new Map();
|
|
13779
14011
|
EAGER_IN_CHUNK = 500;
|
|
13780
14012
|
modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
|
|
@@ -13830,7 +14062,9 @@ var init_baseModel = __esm({
|
|
|
13830
14062
|
for (const [name, def] of Object.entries(fields0)) {
|
|
13831
14063
|
if (def.default === void 0) continue;
|
|
13832
14064
|
let dv = typeof def.default === "function" ? def.default() : def.default;
|
|
13833
|
-
if (dv !== null &&
|
|
14065
|
+
if (def.type === "point" && dv !== null && dv !== void 0) {
|
|
14066
|
+
dv = fromDbFieldValue(def, dv);
|
|
14067
|
+
} else if (dv !== null && typeof dv === "object") dv = structuredClone(dv);
|
|
13834
14068
|
this[name] = dv;
|
|
13835
14069
|
}
|
|
13836
14070
|
if (data) {
|
|
@@ -13932,7 +14166,7 @@ var init_baseModel = __esm({
|
|
|
13932
14166
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
13933
14167
|
*/
|
|
13934
14168
|
static query() {
|
|
13935
|
-
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
14169
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
|
|
13936
14170
|
}
|
|
13937
14171
|
/**
|
|
13938
14172
|
* Get the database adapter for this model.
|
|
@@ -14385,7 +14619,7 @@ var init_baseModel = __esm({
|
|
|
14385
14619
|
for (const key of Object.keys(ModelClass.fields)) {
|
|
14386
14620
|
if (this[key] !== void 0) {
|
|
14387
14621
|
const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
|
|
14388
|
-
result[outKey] = this[key];
|
|
14622
|
+
result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
|
|
14389
14623
|
}
|
|
14390
14624
|
}
|
|
14391
14625
|
if (ModelClass.softDelete && this.is_deleted !== void 0) {
|
|
@@ -14445,6 +14679,19 @@ var init_baseModel = __esm({
|
|
|
14445
14679
|
}
|
|
14446
14680
|
return result;
|
|
14447
14681
|
}
|
|
14682
|
+
toFeature(geometryField, include) {
|
|
14683
|
+
const ModelClass = this.constructor;
|
|
14684
|
+
const pointFields = Object.entries(ModelClass.fields).filter(([, def]) => def.type === "point").map(([name]) => name);
|
|
14685
|
+
const field = geometryField ?? pointFields[0];
|
|
14686
|
+
if (!field || !pointFields.includes(field)) throw new Error("toFeature() needs a declared point field");
|
|
14687
|
+
const properties = this.toDict(include, "camel");
|
|
14688
|
+
const geometry = properties[field] ?? null;
|
|
14689
|
+
delete properties[field];
|
|
14690
|
+
return { type: "Feature", geometry, properties };
|
|
14691
|
+
}
|
|
14692
|
+
static featureCollection(models, geometryField, include) {
|
|
14693
|
+
return { type: "FeatureCollection", features: models.map((model) => model.toFeature(geometryField, include)) };
|
|
14694
|
+
}
|
|
14448
14695
|
/**
|
|
14449
14696
|
* Convert to an associative object (alias for toDict).
|
|
14450
14697
|
*/
|
|
@@ -14495,7 +14742,10 @@ var init_baseModel = __esm({
|
|
|
14495
14742
|
*/
|
|
14496
14743
|
static async createTable() {
|
|
14497
14744
|
const db = this.getDb();
|
|
14498
|
-
|
|
14745
|
+
const pointFields = Object.entries(this.fields).filter(([, def]) => def.type === "point");
|
|
14746
|
+
const engine = db.getDatabaseType();
|
|
14747
|
+
if (pointFields.length > 0) SQLTranslator.requireSpatial(engine, "PointField");
|
|
14748
|
+
if (await adapterTableExists(db, this.tableName)) return this.createSpatialIndexes(db, pointFields);
|
|
14499
14749
|
if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
|
|
14500
14750
|
const mappedFields = {};
|
|
14501
14751
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
@@ -14511,7 +14761,7 @@ var init_baseModel = __esm({
|
|
|
14511
14761
|
mappedFields["is_deleted"] = { type: "integer", default: 0 };
|
|
14512
14762
|
}
|
|
14513
14763
|
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
14514
|
-
return
|
|
14764
|
+
return this.createSpatialIndexes(db, pointFields);
|
|
14515
14765
|
}
|
|
14516
14766
|
const typeMap = {
|
|
14517
14767
|
integer: "INTEGER",
|
|
@@ -14558,6 +14808,14 @@ var init_baseModel = __esm({
|
|
|
14558
14808
|
}
|
|
14559
14809
|
return true;
|
|
14560
14810
|
}
|
|
14811
|
+
static async createSpatialIndexes(db, fields) {
|
|
14812
|
+
for (const [fieldName, def] of fields) {
|
|
14813
|
+
SQLTranslator.pointColumnType(db.getDatabaseType(), def.srid ?? DEFAULT_SRID);
|
|
14814
|
+
if (def.spatialIndex === false) continue;
|
|
14815
|
+
await adapterExecute(db, SQLTranslator.spatialIndex(db.getDatabaseType(), this.tableName, this.getDbColumn(fieldName)));
|
|
14816
|
+
}
|
|
14817
|
+
return true;
|
|
14818
|
+
}
|
|
14561
14819
|
/**
|
|
14562
14820
|
* Find a record by primary key or throw an error if not found.
|
|
14563
14821
|
*/
|
|
@@ -17205,6 +17463,7 @@ __export(src_exports, {
|
|
|
17205
17463
|
CachedDatabaseAdapter: () => CachedDatabaseAdapter,
|
|
17206
17464
|
Cursor: () => Cursor,
|
|
17207
17465
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
17466
|
+
DEFAULT_SRID: () => DEFAULT_SRID,
|
|
17208
17467
|
Database: () => Database,
|
|
17209
17468
|
DatabaseResult: () => DatabaseResult,
|
|
17210
17469
|
DatabaseUrl: () => DatabaseUrl,
|
|
@@ -17220,6 +17479,7 @@ __export(src_exports, {
|
|
|
17220
17479
|
NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
|
|
17221
17480
|
ObjectId: () => ObjectId,
|
|
17222
17481
|
OdbcAdapter: () => OdbcAdapter,
|
|
17482
|
+
Point: () => Point,
|
|
17223
17483
|
PostgresAdapter: () => PostgresAdapter,
|
|
17224
17484
|
QueryBuilder: () => QueryBuilder,
|
|
17225
17485
|
QueryCache: () => QueryCache,
|
|
@@ -17232,6 +17492,7 @@ __export(src_exports, {
|
|
|
17232
17492
|
S3Storage: () => S3Storage,
|
|
17233
17493
|
SQLTranslator: () => SQLTranslator,
|
|
17234
17494
|
SQLiteAdapter: () => SQLiteAdapter,
|
|
17495
|
+
SpatialNotSupportedError: () => SpatialNotSupportedError,
|
|
17235
17496
|
SqliteCollection: () => SqliteCollection,
|
|
17236
17497
|
SqliteDatabase: () => SqliteDatabase,
|
|
17237
17498
|
adapterColumns: () => adapterColumns,
|
|
@@ -17326,6 +17587,7 @@ var init_src = __esm({
|
|
|
17326
17587
|
init_baseModel();
|
|
17327
17588
|
init_queryBuilder();
|
|
17328
17589
|
init_sqlTranslator();
|
|
17590
|
+
init_point();
|
|
17329
17591
|
init_connectTimeout();
|
|
17330
17592
|
init_cachedDatabase();
|
|
17331
17593
|
init_fakeData2();
|
|
@@ -19515,6 +19777,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19515
19777
|
}
|
|
19516
19778
|
}
|
|
19517
19779
|
if (!resolvedToken) {
|
|
19780
|
+
const sso = req2.session?.get?.("_tina4_sso");
|
|
19781
|
+
const identity = sso?.identity;
|
|
19782
|
+
if (identity?.issuer && identity?.subject) {
|
|
19783
|
+
req2.user = identity;
|
|
19784
|
+
return false;
|
|
19785
|
+
}
|
|
19518
19786
|
const sessionToken = req2.session?.get?.("token");
|
|
19519
19787
|
if (sessionToken && validToken(sessionToken)) {
|
|
19520
19788
|
resolvedToken = sessionToken;
|
|
@@ -22979,7 +23247,7 @@ var init_metrics = __esm({
|
|
|
22979
23247
|
};
|
|
22980
23248
|
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
22981
23249
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
22982
|
-
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "
|
|
23250
|
+
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
|
|
22983
23251
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
22984
23252
|
}
|
|
22985
23253
|
});
|
|
@@ -34066,6 +34334,14 @@ function resolveSecuritySchemes() {
|
|
|
34066
34334
|
const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
|
|
34067
34335
|
schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
|
|
34068
34336
|
}
|
|
34337
|
+
const ssoIssuer = (process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
34338
|
+
if (ssoIssuer) {
|
|
34339
|
+
schemes.oidc = {
|
|
34340
|
+
type: "openIdConnect",
|
|
34341
|
+
openIdConnectUrl: `${ssoIssuer}/.well-known/openid-configuration`
|
|
34342
|
+
};
|
|
34343
|
+
schemes.ssoSession = { type: "apiKey", in: "cookie", name: "tina4_session" };
|
|
34344
|
+
}
|
|
34069
34345
|
for (const [name, def] of Object.entries(registeredSchemes)) {
|
|
34070
34346
|
schemes[name] = def;
|
|
34071
34347
|
}
|
|
@@ -34247,7 +34523,9 @@ function generate(routes, models = []) {
|
|
|
34247
34523
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34248
34524
|
}
|
|
34249
34525
|
} else if (routeRequiresAuth(route, method)) {
|
|
34250
|
-
|
|
34526
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
34527
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
34528
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
34251
34529
|
const responses = operation.responses;
|
|
34252
34530
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34253
34531
|
}
|
|
@@ -34561,6 +34839,298 @@ var init_src2 = __esm({
|
|
|
34561
34839
|
}
|
|
34562
34840
|
});
|
|
34563
34841
|
|
|
34842
|
+
// src/sso.ts
|
|
34843
|
+
var sso_exports = {};
|
|
34844
|
+
__export(sso_exports, {
|
|
34845
|
+
SSO: () => Sso,
|
|
34846
|
+
Sso: () => Sso,
|
|
34847
|
+
SsoError: () => SsoError
|
|
34848
|
+
});
|
|
34849
|
+
import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
34850
|
+
var SsoError, Sso;
|
|
34851
|
+
var init_sso = __esm({
|
|
34852
|
+
"src/sso.ts"() {
|
|
34853
|
+
"use strict";
|
|
34854
|
+
SsoError = class extends Error {
|
|
34855
|
+
};
|
|
34856
|
+
Sso = class _Sso {
|
|
34857
|
+
static PENDING_KEY = "_tina4_sso_pending";
|
|
34858
|
+
static SESSION_KEY = "_tina4_sso";
|
|
34859
|
+
issuer;
|
|
34860
|
+
clientId;
|
|
34861
|
+
clientSecret;
|
|
34862
|
+
redirectUri;
|
|
34863
|
+
scopes;
|
|
34864
|
+
verify;
|
|
34865
|
+
postLogoutRedirectUri;
|
|
34866
|
+
claimMap;
|
|
34867
|
+
timeout;
|
|
34868
|
+
metadata = {};
|
|
34869
|
+
static mountedRouters = /* @__PURE__ */ new WeakSet();
|
|
34870
|
+
constructor(options = {}) {
|
|
34871
|
+
this.issuer = (options.issuer ?? process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
34872
|
+
this.clientId = options.clientId ?? process.env.TINA4_SSO_CLIENT_ID ?? "";
|
|
34873
|
+
this.clientSecret = options.clientSecret ?? process.env.TINA4_SSO_CLIENT_SECRET;
|
|
34874
|
+
this.redirectUri = options.redirectUri ?? process.env.TINA4_SSO_REDIRECT_URI ?? "";
|
|
34875
|
+
this.scopes = options.scopes ?? this.jsonEnv("TINA4_SSO_SCOPES", ["openid", "profile", "email"]);
|
|
34876
|
+
this.verify = options.verify ?? process.env.TINA4_SSO_VERIFY ?? "introspection";
|
|
34877
|
+
this.postLogoutRedirectUri = options.postLogoutRedirectUri ?? process.env.TINA4_SSO_POST_LOGOUT_REDIRECT_URI;
|
|
34878
|
+
this.claimMap = options.claimMap ?? this.jsonEnv("TINA4_SSO_CLAIM_MAP", {});
|
|
34879
|
+
this.timeout = options.timeout ?? 1e4;
|
|
34880
|
+
this.validateConfig();
|
|
34881
|
+
}
|
|
34882
|
+
static async fromIssuer(options = {}) {
|
|
34883
|
+
const value = new _Sso(options);
|
|
34884
|
+
await value.discover();
|
|
34885
|
+
return value;
|
|
34886
|
+
}
|
|
34887
|
+
static configured() {
|
|
34888
|
+
return ["TINA4_SSO_ISSUER", "TINA4_SSO_CLIENT_ID", "TINA4_SSO_REDIRECT_URI"].every((key) => Boolean(process.env[key]));
|
|
34889
|
+
}
|
|
34890
|
+
jsonEnv(name, fallback) {
|
|
34891
|
+
const raw = process.env[name];
|
|
34892
|
+
if (!raw) return fallback;
|
|
34893
|
+
try {
|
|
34894
|
+
return JSON.parse(raw);
|
|
34895
|
+
} catch {
|
|
34896
|
+
throw new SsoError(`${name} must be valid JSON`);
|
|
34897
|
+
}
|
|
34898
|
+
}
|
|
34899
|
+
static secureUrl(value, name) {
|
|
34900
|
+
let url;
|
|
34901
|
+
try {
|
|
34902
|
+
url = new URL(value);
|
|
34903
|
+
} catch {
|
|
34904
|
+
throw new SsoError(`${name} must be an absolute URL`);
|
|
34905
|
+
}
|
|
34906
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
34907
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
34908
|
+
throw new SsoError(`${name} must use HTTPS except on loopback`);
|
|
34909
|
+
}
|
|
34910
|
+
}
|
|
34911
|
+
validateConfig() {
|
|
34912
|
+
if (!this.issuer || !this.clientId || !this.redirectUri) {
|
|
34913
|
+
throw new SsoError("TINA4_SSO_ISSUER, TINA4_SSO_CLIENT_ID and TINA4_SSO_REDIRECT_URI are required");
|
|
34914
|
+
}
|
|
34915
|
+
_Sso.secureUrl(this.issuer, "issuer");
|
|
34916
|
+
_Sso.secureUrl(this.redirectUri, "redirect URI");
|
|
34917
|
+
if (!["introspection", "jwks"].includes(this.verify)) throw new SsoError("TINA4_SSO_VERIFY must be introspection or jwks");
|
|
34918
|
+
if (this.verify === "jwks") throw new SsoError("jwks verification requires an installed cryptography capability");
|
|
34919
|
+
if (this.verify === "introspection" && !this.clientSecret) throw new SsoError("introspection verification requires TINA4_SSO_CLIENT_SECRET");
|
|
34920
|
+
if (!Array.isArray(this.scopes) || !this.scopes.includes("openid")) throw new SsoError("TINA4_SSO_SCOPES must be a list containing openid");
|
|
34921
|
+
}
|
|
34922
|
+
async requestJson(url, form, bearer, basic = false) {
|
|
34923
|
+
const headers = { Accept: "application/json" };
|
|
34924
|
+
let body;
|
|
34925
|
+
if (form) {
|
|
34926
|
+
const parameters = new URLSearchParams();
|
|
34927
|
+
for (const [key, value] of Object.entries(form)) parameters.set(key, String(value));
|
|
34928
|
+
body = parameters.toString();
|
|
34929
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
34930
|
+
}
|
|
34931
|
+
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
34932
|
+
if (basic) headers.Authorization = `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`;
|
|
34933
|
+
const controller = new AbortController();
|
|
34934
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
34935
|
+
try {
|
|
34936
|
+
const response = await fetch(url, { method: form ? "POST" : "GET", headers, body, signal: controller.signal });
|
|
34937
|
+
if (!response.ok) throw new SsoError("OIDC provider request failed");
|
|
34938
|
+
const result = await response.json();
|
|
34939
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new SsoError("OIDC provider returned a non-object response");
|
|
34940
|
+
return result;
|
|
34941
|
+
} catch (error) {
|
|
34942
|
+
if (error instanceof SsoError) throw error;
|
|
34943
|
+
throw new SsoError("OIDC provider request failed");
|
|
34944
|
+
} finally {
|
|
34945
|
+
clearTimeout(timer);
|
|
34946
|
+
}
|
|
34947
|
+
}
|
|
34948
|
+
async discover(force = false) {
|
|
34949
|
+
if (Object.keys(this.metadata).length && !force) return { ...this.metadata };
|
|
34950
|
+
const result = await this.requestJson(`${this.issuer}/.well-known/openid-configuration`);
|
|
34951
|
+
if (result.issuer !== this.issuer) throw new SsoError("OIDC discovery issuer does not exactly match configuration");
|
|
34952
|
+
const required = ["authorization_endpoint", "token_endpoint"];
|
|
34953
|
+
if (this.verify === "introspection") required.push("introspection_endpoint");
|
|
34954
|
+
for (const key of required) {
|
|
34955
|
+
if (!result[key]) throw new SsoError(`OIDC discovery is missing ${key}`);
|
|
34956
|
+
_Sso.secureUrl(result[key], key);
|
|
34957
|
+
}
|
|
34958
|
+
this.metadata = result;
|
|
34959
|
+
return { ...result };
|
|
34960
|
+
}
|
|
34961
|
+
static safeReturn(value) {
|
|
34962
|
+
if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return "/";
|
|
34963
|
+
return [...value].some((char) => char.charCodeAt(0) < 32) ? "/" : value;
|
|
34964
|
+
}
|
|
34965
|
+
session(value) {
|
|
34966
|
+
return value?.session ?? value;
|
|
34967
|
+
}
|
|
34968
|
+
async login(requestOrSession, returnTo = "/") {
|
|
34969
|
+
const session = this.session(requestOrSession);
|
|
34970
|
+
if (!session) throw new SsoError("SSO login requires a Tina4 Session");
|
|
34971
|
+
const state = randomBytes7(32).toString("base64url");
|
|
34972
|
+
const nonce = randomBytes7(32).toString("base64url");
|
|
34973
|
+
const verifier = randomBytes7(64).toString("base64url");
|
|
34974
|
+
const challenge = createHash9("sha256").update(verifier).digest("base64url");
|
|
34975
|
+
session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
|
|
34976
|
+
const metadata = await this.discover();
|
|
34977
|
+
const query = new URLSearchParams({
|
|
34978
|
+
client_id: this.clientId,
|
|
34979
|
+
redirect_uri: this.redirectUri,
|
|
34980
|
+
response_type: "code",
|
|
34981
|
+
scope: this.scopes.join(" "),
|
|
34982
|
+
state,
|
|
34983
|
+
nonce,
|
|
34984
|
+
code_challenge: challenge,
|
|
34985
|
+
code_challenge_method: "S256"
|
|
34986
|
+
});
|
|
34987
|
+
return `${metadata.authorization_endpoint}?${query}`;
|
|
34988
|
+
}
|
|
34989
|
+
static equal(left, right) {
|
|
34990
|
+
const a = Buffer.from(String(left ?? ""));
|
|
34991
|
+
const b = Buffer.from(String(right ?? ""));
|
|
34992
|
+
return a.length === b.length && timingSafeEqual3(a, b);
|
|
34993
|
+
}
|
|
34994
|
+
static jwtPayload(token) {
|
|
34995
|
+
try {
|
|
34996
|
+
return JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
34997
|
+
} catch {
|
|
34998
|
+
throw new SsoError("provider returned an invalid ID token");
|
|
34999
|
+
}
|
|
35000
|
+
}
|
|
35001
|
+
async introspect(accessToken) {
|
|
35002
|
+
const metadata = await this.discover();
|
|
35003
|
+
const result = await this.requestJson(metadata.introspection_endpoint, { token: accessToken, token_type_hint: "access_token" }, void 0, true);
|
|
35004
|
+
if (result.active !== true || result.iss !== this.issuer) throw new SsoError("OIDC access token is inactive or has the wrong issuer");
|
|
35005
|
+
const audience = result.aud ?? result.client_id;
|
|
35006
|
+
const valid = (Array.isArray(audience) ? audience.includes(this.clientId) : audience === this.clientId) || result.client_id === this.clientId;
|
|
35007
|
+
if (!valid) throw new SsoError("OIDC token audience mismatch");
|
|
35008
|
+
return result;
|
|
35009
|
+
}
|
|
35010
|
+
claim(claims, configured, fallback) {
|
|
35011
|
+
let value = claims;
|
|
35012
|
+
for (const part of (configured ?? fallback).split(".")) value = value && typeof value === "object" ? value[part] : void 0;
|
|
35013
|
+
return value;
|
|
35014
|
+
}
|
|
35015
|
+
normalize(claims) {
|
|
35016
|
+
const subject = this.claim(claims, this.claimMap.subject, "sub");
|
|
35017
|
+
const issuer = this.claim(claims, this.claimMap.issuer, "iss") ?? this.issuer;
|
|
35018
|
+
if (!subject || issuer !== this.issuer) throw new SsoError("OIDC identity is missing a valid issuer or subject");
|
|
35019
|
+
const roles = [...this.claim(claims, this.claimMap.roles, "realm_access.roles") ?? [], ...claims.resource_access?.[this.clientId]?.roles ?? []];
|
|
35020
|
+
const groups = this.claim(claims, this.claimMap.groups, "groups") ?? [];
|
|
35021
|
+
return {
|
|
35022
|
+
issuer,
|
|
35023
|
+
subject,
|
|
35024
|
+
username: this.claim(claims, this.claimMap.username, "preferred_username") ?? null,
|
|
35025
|
+
email: this.claim(claims, this.claimMap.email, "email") ?? null,
|
|
35026
|
+
name: this.claim(claims, this.claimMap.name, "name") ?? null,
|
|
35027
|
+
roles: [...new Set(roles.map(String))].sort(),
|
|
35028
|
+
groups: [...new Set(groups.map(String))].sort()
|
|
35029
|
+
};
|
|
35030
|
+
}
|
|
35031
|
+
async callback(requestOrSession, query) {
|
|
35032
|
+
const session = this.session(requestOrSession);
|
|
35033
|
+
const values = query ?? requestOrSession?.query ?? {};
|
|
35034
|
+
const pending = session?.get(_Sso.PENDING_KEY);
|
|
35035
|
+
session?.delete(_Sso.PENDING_KEY);
|
|
35036
|
+
if (!pending || !values.code || !_Sso.equal(values.state, pending.state)) throw new SsoError("OIDC callback state is invalid or already consumed");
|
|
35037
|
+
if (Math.floor(Date.now() / 1e3) - Number(pending.created_at ?? 0) > 600) throw new SsoError("OIDC callback state has expired");
|
|
35038
|
+
const metadata = await this.discover();
|
|
35039
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
35040
|
+
grant_type: "authorization_code",
|
|
35041
|
+
code: values.code,
|
|
35042
|
+
redirect_uri: this.redirectUri,
|
|
35043
|
+
client_id: this.clientId,
|
|
35044
|
+
code_verifier: pending.verifier
|
|
35045
|
+
}, void 0, Boolean(this.clientSecret));
|
|
35046
|
+
if (!tokens.access_token || !tokens.id_token) throw new SsoError("OIDC token response is incomplete");
|
|
35047
|
+
if (this.verify === "jwks") throw new SsoError("JWKS verification requires an installed cryptography capability");
|
|
35048
|
+
const claims = await this.introspect(tokens.access_token);
|
|
35049
|
+
if (!_Sso.equal(_Sso.jwtPayload(tokens.id_token).nonce, pending.nonce)) throw new SsoError("OIDC ID token nonce mismatch");
|
|
35050
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
35051
|
+
const identity = this.normalize(claims);
|
|
35052
|
+
session.regenerate();
|
|
35053
|
+
session.set(_Sso.SESSION_KEY, {
|
|
35054
|
+
version: 1,
|
|
35055
|
+
identity,
|
|
35056
|
+
access_token: tokens.access_token,
|
|
35057
|
+
refresh_token: tokens.refresh_token,
|
|
35058
|
+
id_token: tokens.id_token,
|
|
35059
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
35060
|
+
});
|
|
35061
|
+
return { identity, return_to: _Sso.safeReturn(pending.return_to) };
|
|
35062
|
+
}
|
|
35063
|
+
identity(requestOrSession) {
|
|
35064
|
+
const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
|
|
35065
|
+
const identity = stored?.identity ?? null;
|
|
35066
|
+
if (identity && requestOrSession?.session) requestOrSession.user = identity;
|
|
35067
|
+
return identity;
|
|
35068
|
+
}
|
|
35069
|
+
async refresh(requestOrSession) {
|
|
35070
|
+
const session = this.session(requestOrSession);
|
|
35071
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
35072
|
+
if (!stored?.refresh_token) {
|
|
35073
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
35074
|
+
throw new SsoError("OIDC session cannot be refreshed");
|
|
35075
|
+
}
|
|
35076
|
+
try {
|
|
35077
|
+
const metadata = await this.discover();
|
|
35078
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
35079
|
+
grant_type: "refresh_token",
|
|
35080
|
+
refresh_token: stored.refresh_token,
|
|
35081
|
+
client_id: this.clientId
|
|
35082
|
+
}, void 0, Boolean(this.clientSecret));
|
|
35083
|
+
const claims = await this.introspect(tokens.access_token);
|
|
35084
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
35085
|
+
const identity = this.normalize(claims);
|
|
35086
|
+
session.set(_Sso.SESSION_KEY, {
|
|
35087
|
+
...stored,
|
|
35088
|
+
identity,
|
|
35089
|
+
access_token: tokens.access_token,
|
|
35090
|
+
refresh_token: tokens.refresh_token ?? stored.refresh_token,
|
|
35091
|
+
id_token: tokens.id_token ?? stored.id_token,
|
|
35092
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
35093
|
+
});
|
|
35094
|
+
return identity;
|
|
35095
|
+
} catch (error) {
|
|
35096
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
35097
|
+
throw error;
|
|
35098
|
+
}
|
|
35099
|
+
}
|
|
35100
|
+
async logout(requestOrSession, returnTo = "/") {
|
|
35101
|
+
const session = this.session(requestOrSession);
|
|
35102
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
35103
|
+
session?.destroy();
|
|
35104
|
+
const endpoint = (await this.discover()).end_session_endpoint;
|
|
35105
|
+
const target = this.postLogoutRedirectUri ?? _Sso.safeReturn(returnTo);
|
|
35106
|
+
if (!endpoint) return target;
|
|
35107
|
+
const params = new URLSearchParams({ post_logout_redirect_uri: target, client_id: this.clientId });
|
|
35108
|
+
if (stored?.id_token) params.set("id_token_hint", stored.id_token);
|
|
35109
|
+
return `${endpoint}?${params}`;
|
|
35110
|
+
}
|
|
35111
|
+
static async mountConfigured(router) {
|
|
35112
|
+
if (_Sso.mountedRouters.has(router) || !_Sso.configured()) return false;
|
|
35113
|
+
const owned = /* @__PURE__ */ new Set(["GET /auth/login", "GET /auth/callback", "POST /auth/logout"]);
|
|
35114
|
+
const collisions = router.getRoutes().map((route) => `${route.method} ${route.pattern}`).filter((route) => owned.has(route));
|
|
35115
|
+
if (collisions.length) throw new SsoError(`SSO route collision: ${collisions.join(", ")}`);
|
|
35116
|
+
const sso = await _Sso.fromIssuer();
|
|
35117
|
+
router.get("/auth/login", async (req2, res) => res.redirect(await sso.login(req2, req2.query?.return_to ?? "/")));
|
|
35118
|
+
router.get("/auth/callback", async (req2, res) => {
|
|
35119
|
+
try {
|
|
35120
|
+
return res.redirect((await sso.callback(req2)).return_to);
|
|
35121
|
+
} catch (error) {
|
|
35122
|
+
const message = error instanceof SsoError ? error.message : "OIDC callback failed";
|
|
35123
|
+
return res.error("SSO_CALLBACK_FAILED", message, 400);
|
|
35124
|
+
}
|
|
35125
|
+
});
|
|
35126
|
+
router.post("/auth/logout", async (req2, res) => res.redirect(await sso.logout(req2, req2.query?.return_to ?? "/")));
|
|
35127
|
+
_Sso.mountedRouters.add(router);
|
|
35128
|
+
return true;
|
|
35129
|
+
}
|
|
35130
|
+
};
|
|
35131
|
+
}
|
|
35132
|
+
});
|
|
35133
|
+
|
|
34564
35134
|
// src/docsAutoDiscovery.ts
|
|
34565
35135
|
var docsAutoDiscovery_exports = {};
|
|
34566
35136
|
__export(docsAutoDiscovery_exports, {
|
|
@@ -34630,7 +35200,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
34630
35200
|
|
|
34631
35201
|
// src/server.ts
|
|
34632
35202
|
import { createServer as createServer2 } from "node:http";
|
|
34633
|
-
import { randomBytes as
|
|
35203
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34634
35204
|
import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative8 } from "node:path";
|
|
34635
35205
|
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
34636
35206
|
import { isatty } from "node:tty";
|
|
@@ -35270,7 +35840,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
35270
35840
|
}
|
|
35271
35841
|
}
|
|
35272
35842
|
}
|
|
35273
|
-
const requestId = Log.getRequestId() ??
|
|
35843
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35274
35844
|
if (wantsJson(req2)) {
|
|
35275
35845
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
35276
35846
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -35341,7 +35911,7 @@ function serveStaticAsset(ctx) {
|
|
|
35341
35911
|
return false;
|
|
35342
35912
|
}
|
|
35343
35913
|
async function serveNotFound(ctx) {
|
|
35344
|
-
const requestId = Log.getRequestId() ??
|
|
35914
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35345
35915
|
if (wantsJson(ctx.req)) {
|
|
35346
35916
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
35347
35917
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -35461,7 +36031,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
35461
36031
|
}
|
|
35462
36032
|
}
|
|
35463
36033
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
35464
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
36034
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
|
|
35465
36035
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
35466
36036
|
return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
|
|
35467
36037
|
}
|
|
@@ -35595,6 +36165,8 @@ ${reset2}
|
|
|
35595
36165
|
console.log(`
|
|
35596
36166
|
No routes directory found at ${routesDir}`);
|
|
35597
36167
|
}
|
|
36168
|
+
const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
|
|
36169
|
+
await Sso2.mountConfigured(router);
|
|
35598
36170
|
if (attachCsrfFromEnv()) {
|
|
35599
36171
|
console.log(`
|
|
35600
36172
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
@@ -36119,7 +36691,7 @@ var init_mqttMessage = __esm({
|
|
|
36119
36691
|
// src/mqtt.ts
|
|
36120
36692
|
import net2 from "node:net";
|
|
36121
36693
|
import tls from "node:tls";
|
|
36122
|
-
import { randomBytes as
|
|
36694
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
36123
36695
|
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
|
|
36124
36696
|
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;
|
|
36125
36697
|
var init_mqtt = __esm({
|
|
@@ -36207,7 +36779,7 @@ var init_mqtt = __esm({
|
|
|
36207
36779
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
36208
36780
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
36209
36781
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
36210
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
36782
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
|
|
36211
36783
|
this.clientId = cid;
|
|
36212
36784
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
36213
36785
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -37137,7 +37709,7 @@ var init_service = __esm({
|
|
|
37137
37709
|
import http from "node:http";
|
|
37138
37710
|
import https from "node:https";
|
|
37139
37711
|
import { URL as URL2 } from "node:url";
|
|
37140
|
-
import { randomBytes as
|
|
37712
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
37141
37713
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
37142
37714
|
import { basename as basename5 } from "node:path";
|
|
37143
37715
|
import { pipeline } from "node:stream/promises";
|
|
@@ -37435,7 +38007,7 @@ var init_api = __esm({
|
|
|
37435
38007
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
37436
38008
|
}
|
|
37437
38009
|
const partContentType = guessContentType(uploadName);
|
|
37438
|
-
const boundary = "----Tina4Boundary" +
|
|
38010
|
+
const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
|
|
37439
38011
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
37440
38012
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
37441
38013
|
return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
|
|
@@ -42267,10 +42839,13 @@ __export(index_exports, {
|
|
|
42267
42839
|
RouteGroup: () => RouteGroup,
|
|
42268
42840
|
RouteRef: () => RouteRef,
|
|
42269
42841
|
Router: () => Router,
|
|
42842
|
+
SSO: () => Sso,
|
|
42270
42843
|
SafeString: () => SafeString2,
|
|
42271
42844
|
SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
|
|
42272
42845
|
ServiceRunner: () => ServiceRunner,
|
|
42273
42846
|
Session: () => Session,
|
|
42847
|
+
Sso: () => Sso,
|
|
42848
|
+
SsoError: () => SsoError,
|
|
42274
42849
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
42275
42850
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
42276
42851
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -42506,6 +43081,7 @@ var init_index = __esm({
|
|
|
42506
43081
|
init_htmlElement();
|
|
42507
43082
|
init_errorOverlay();
|
|
42508
43083
|
init_ai();
|
|
43084
|
+
init_sso();
|
|
42509
43085
|
init_aiClient();
|
|
42510
43086
|
init_liteBackend();
|
|
42511
43087
|
init_rabbitmqBackend();
|
|
@@ -42629,10 +43205,13 @@ export {
|
|
|
42629
43205
|
RouteGroup,
|
|
42630
43206
|
RouteRef,
|
|
42631
43207
|
Router,
|
|
43208
|
+
Sso as SSO,
|
|
42632
43209
|
SafeString2 as SafeString,
|
|
42633
43210
|
SecurityHeadersMiddleware,
|
|
42634
43211
|
ServiceRunner,
|
|
42635
43212
|
Session,
|
|
43213
|
+
Sso,
|
|
43214
|
+
SsoError,
|
|
42636
43215
|
TAKEOVER_KILLED,
|
|
42637
43216
|
TAKEOVER_NOTHING,
|
|
42638
43217
|
TAKEOVER_REFUSALS,
|