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
package/packages/cli/dist/bin.js
CHANGED
|
@@ -1463,6 +1463,7 @@ __export(engine_exports, {
|
|
|
1463
1463
|
Frond: () => Frond,
|
|
1464
1464
|
MEMO_CACHE_MAX: () => MEMO_CACHE_MAX,
|
|
1465
1465
|
TEMPLATE_CACHE_MAX: () => TEMPLATE_CACHE_MAX,
|
|
1466
|
+
expressionFormCache: () => expressionFormCache,
|
|
1466
1467
|
filterChainCache: () => filterChainCache,
|
|
1467
1468
|
pathParseCache: () => pathParseCache,
|
|
1468
1469
|
setFormTokenSessionId: () => setFormTokenSessionId
|
|
@@ -1871,62 +1872,58 @@ function splitOutsideQuotes(expr, sep6) {
|
|
|
1871
1872
|
parts.push(expr.slice(currentStart));
|
|
1872
1873
|
return parts;
|
|
1873
1874
|
}
|
|
1874
|
-
function
|
|
1875
|
-
expr
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
}
|
|
1882
|
-
if (expr.length >= 2 && expr[0] === "(" && expr.endsWith(")")) {
|
|
1883
|
-
let depth = 0;
|
|
1884
|
-
let matched = true;
|
|
1885
|
-
for (let pi = 0; pi < expr.length; pi++) {
|
|
1886
|
-
if (expr[pi] === "(") depth++;
|
|
1887
|
-
else if (expr[pi] === ")") depth--;
|
|
1888
|
-
if (depth === 0 && pi < expr.length - 1) {
|
|
1889
|
-
matched = false;
|
|
1890
|
-
break;
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
if (matched) {
|
|
1894
|
-
return evalExpr(expr.slice(1, -1), context);
|
|
1895
|
-
}
|
|
1875
|
+
function parenthesizedInner(expr) {
|
|
1876
|
+
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
1877
|
+
let depth = 0;
|
|
1878
|
+
for (let index = 0; index < expr.length; index++) {
|
|
1879
|
+
if (expr[index] === "(") depth++;
|
|
1880
|
+
else if (expr[index] === ")") depth--;
|
|
1881
|
+
if (depth === 0 && index < expr.length - 1) return null;
|
|
1896
1882
|
}
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
const truePart = rest.slice(0, colonIdx).trim();
|
|
1904
|
-
const falsePart = rest.slice(colonIdx + 1).trim();
|
|
1905
|
-
const cond = evalExpr(condPart, context);
|
|
1906
|
-
return cond ? evalExpr(truePart, context) : evalExpr(falsePart, context);
|
|
1907
|
-
}
|
|
1883
|
+
return expr.slice(1, -1);
|
|
1884
|
+
}
|
|
1885
|
+
function evalPrimary(expr, context) {
|
|
1886
|
+
const quote = expr[0];
|
|
1887
|
+
if (expr.length >= 2 && (quote === '"' || quote === "'") && expr.endsWith(quote) && !expr.slice(1, -1).includes(quote)) {
|
|
1888
|
+
return expr.slice(1, -1);
|
|
1908
1889
|
}
|
|
1890
|
+
const inner = parenthesizedInner(expr);
|
|
1891
|
+
if (inner !== null) return evalExpr(inner, context);
|
|
1892
|
+
return EXPR_NOT_MATCHED;
|
|
1893
|
+
}
|
|
1894
|
+
function evalTernaryExpression(expr, context) {
|
|
1895
|
+
const ternaryIdx = findTernary(expr);
|
|
1896
|
+
if (ternaryIdx === -1) return EXPR_NOT_MATCHED;
|
|
1897
|
+
const rest = expr.slice(ternaryIdx + 1);
|
|
1898
|
+
const colonIdx = findColon(rest);
|
|
1899
|
+
if (colonIdx === -1) return EXPR_NOT_MATCHED;
|
|
1900
|
+
const condition = evalExpr(expr.slice(0, ternaryIdx).trim(), context);
|
|
1901
|
+
const branch = condition ? rest.slice(0, colonIdx) : rest.slice(colonIdx + 1);
|
|
1902
|
+
return evalExpr(branch.trim(), context);
|
|
1903
|
+
}
|
|
1904
|
+
function evalInlineIfExpression(expr, context) {
|
|
1909
1905
|
const ifIdx = findOutsideQuotes(expr, " if ");
|
|
1910
|
-
if (ifIdx
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
}
|
|
1919
|
-
}
|
|
1906
|
+
if (ifIdx < 0) return EXPR_NOT_MATCHED;
|
|
1907
|
+
const elseIdx = findOutsideQuotes(expr, " else ");
|
|
1908
|
+
if (elseIdx < 0 || elseIdx <= ifIdx) return EXPR_NOT_MATCHED;
|
|
1909
|
+
const condition = evalExpr(expr.slice(ifIdx + 4, elseIdx).trim(), context);
|
|
1910
|
+
const branch = condition ? expr.slice(0, ifIdx) : expr.slice(elseIdx + 6);
|
|
1911
|
+
return evalExpr(branch.trim(), context);
|
|
1912
|
+
}
|
|
1913
|
+
function evalCoalesceExpression(expr, context) {
|
|
1920
1914
|
const qqIdx = findOutsideQuotes(expr, "??");
|
|
1921
|
-
if (qqIdx
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
return
|
|
1915
|
+
if (qqIdx === -1) return EXPR_NOT_MATCHED;
|
|
1916
|
+
const value = evalExpr(expr.slice(0, qqIdx).trim(), context);
|
|
1917
|
+
return value === null || value === void 0 ? evalExpr(expr.slice(qqIdx + 2).trim(), context) : value;
|
|
1918
|
+
}
|
|
1919
|
+
function evalConditional(expr, context) {
|
|
1920
|
+
for (const evaluator of [evalTernaryExpression, evalInlineIfExpression, evalCoalesceExpression]) {
|
|
1921
|
+
const result = evaluator(expr, context);
|
|
1922
|
+
if (result !== EXPR_NOT_MATCHED) return result;
|
|
1929
1923
|
}
|
|
1924
|
+
return EXPR_NOT_MATCHED;
|
|
1925
|
+
}
|
|
1926
|
+
function evalConcatOrComparison(expr, context) {
|
|
1930
1927
|
if (findOutsideQuotes(expr, "~") >= 0) {
|
|
1931
1928
|
const parts = splitOutsideQuotes(expr, "~");
|
|
1932
1929
|
if (parts.length > 1) {
|
|
@@ -1944,6 +1941,9 @@ function evalExpr(expr, context) {
|
|
|
1944
1941
|
return evalComparison(expr, context);
|
|
1945
1942
|
}
|
|
1946
1943
|
}
|
|
1944
|
+
return EXPR_NOT_MATCHED;
|
|
1945
|
+
}
|
|
1946
|
+
function evalArithmeticExpression(expr, context) {
|
|
1947
1947
|
for (const op of [" + ", " - ", " * ", " // ", " / ", " % ", " ** "]) {
|
|
1948
1948
|
const pos = findOutsideQuotes(expr, op);
|
|
1949
1949
|
if (pos >= 0) {
|
|
@@ -1956,40 +1956,15 @@ function evalExpr(expr, context) {
|
|
|
1956
1956
|
let rNum = rVal != null ? Number(rVal) : 0;
|
|
1957
1957
|
if (isNaN(lNum)) lNum = 0;
|
|
1958
1958
|
if (isNaN(rNum)) rNum = 0;
|
|
1959
|
-
|
|
1960
|
-
const bothInt = Number.isInteger(lNum) && Number.isInteger(rNum) && opS !== "/";
|
|
1961
|
-
let result;
|
|
1962
|
-
switch (opS) {
|
|
1963
|
-
case "+":
|
|
1964
|
-
result = lNum + rNum;
|
|
1965
|
-
break;
|
|
1966
|
-
case "-":
|
|
1967
|
-
result = lNum - rNum;
|
|
1968
|
-
break;
|
|
1969
|
-
case "*":
|
|
1970
|
-
result = lNum * rNum;
|
|
1971
|
-
break;
|
|
1972
|
-
case "//":
|
|
1973
|
-
result = rNum !== 0 ? Math.floor(lNum / rNum) : 0;
|
|
1974
|
-
break;
|
|
1975
|
-
case "/":
|
|
1976
|
-
result = rNum !== 0 ? lNum / rNum : 0;
|
|
1977
|
-
break;
|
|
1978
|
-
case "%":
|
|
1979
|
-
result = rNum !== 0 ? lNum % rNum : 0;
|
|
1980
|
-
break;
|
|
1981
|
-
case "**":
|
|
1982
|
-
result = lNum ** rNum;
|
|
1983
|
-
break;
|
|
1984
|
-
default:
|
|
1985
|
-
result = 0;
|
|
1986
|
-
}
|
|
1987
|
-
return bothInt && Number.isInteger(result) ? result : result;
|
|
1959
|
+
return ARITHMETIC_OPERATIONS[op.trim()](lNum, rNum);
|
|
1988
1960
|
} catch {
|
|
1989
1961
|
return null;
|
|
1990
1962
|
}
|
|
1991
1963
|
}
|
|
1992
1964
|
}
|
|
1965
|
+
return EXPR_NOT_MATCHED;
|
|
1966
|
+
}
|
|
1967
|
+
function evalFilterExpression(expr, context) {
|
|
1993
1968
|
if (findOutsideQuotes(expr, "|") >= 0) {
|
|
1994
1969
|
const [baseExpr, filters] = parseFilterChain(expr);
|
|
1995
1970
|
if (filters.length > 0) {
|
|
@@ -2008,38 +1983,49 @@ function evalExpr(expr, context) {
|
|
|
2008
1983
|
return value;
|
|
2009
1984
|
}
|
|
2010
1985
|
}
|
|
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
|
-
|
|
2040
|
-
|
|
1986
|
+
return EXPR_NOT_MATCHED;
|
|
1987
|
+
}
|
|
1988
|
+
function evaluateCallArgs(rawArgs, context) {
|
|
1989
|
+
return rawArgs.trim() ? splitArgs(rawArgs).map((arg) => evalExpr(arg.trim(), context)) : [];
|
|
1990
|
+
}
|
|
1991
|
+
function evalDottedFunction(name, rawArgs, context) {
|
|
1992
|
+
const lastDot = name.lastIndexOf(".");
|
|
1993
|
+
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
1994
|
+
const member = name.slice(lastDot + 1);
|
|
1995
|
+
if (!owner || typeof owner !== "object" || !(member in owner)) {
|
|
1996
|
+
return EXPR_NOT_MATCHED;
|
|
1997
|
+
}
|
|
1998
|
+
const method = owner[member];
|
|
1999
|
+
return typeof method === "function" ? method.apply(owner, evaluateCallArgs(rawArgs, context)) : EXPR_NOT_MATCHED;
|
|
2000
|
+
}
|
|
2001
|
+
function evalFunctionExpression(expr, context) {
|
|
2002
|
+
const match = expr.match(FN_CALL_RE);
|
|
2003
|
+
if (!match) return EXPR_NOT_MATCHED;
|
|
2004
|
+
const name = match[1];
|
|
2005
|
+
const rawArgs = match[2] || "";
|
|
2006
|
+
if (name.includes(".")) return evalDottedFunction(name, rawArgs, context);
|
|
2007
|
+
const fn = context[name] ?? resolveVar(name, context);
|
|
2008
|
+
if (typeof fn === "function") return fn(...evaluateCallArgs(rawArgs, context));
|
|
2009
|
+
return EXPR_NOT_MATCHED;
|
|
2010
|
+
}
|
|
2011
|
+
function evalExpr(expr, context) {
|
|
2012
|
+
expr = expr.trim();
|
|
2013
|
+
const cachedForm = expressionFormCache.get(expr);
|
|
2014
|
+
if (cachedForm !== void 0) {
|
|
2015
|
+
if (cachedForm === -1) return resolveVar(expr, context);
|
|
2016
|
+
const result = EXPR_EVALUATORS[cachedForm](expr, context);
|
|
2017
|
+
return result === EXPR_NOT_MATCHED ? resolveVar(expr, context) : result;
|
|
2018
|
+
}
|
|
2019
|
+
for (let index = 0; index < EXPR_EVALUATORS.length; index++) {
|
|
2020
|
+
const result = EXPR_EVALUATORS[index](expr, context);
|
|
2021
|
+
if (result !== EXPR_NOT_MATCHED) {
|
|
2022
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
2023
|
+
expressionFormCache.set(expr, index);
|
|
2024
|
+
return result;
|
|
2041
2025
|
}
|
|
2042
2026
|
}
|
|
2027
|
+
capCache(expressionFormCache, MEMO_CACHE_MAX);
|
|
2028
|
+
expressionFormCache.set(expr, FN_CALL_RE.test(expr) ? EXPR_EVALUATORS.length - 1 : -1);
|
|
2043
2029
|
return resolveVar(expr, context);
|
|
2044
2030
|
}
|
|
2045
2031
|
function findTernary(expr) {
|
|
@@ -2495,7 +2481,7 @@ function _generateFormToken(descriptor = "") {
|
|
|
2495
2481
|
function _generateFormTokenValue(descriptor = "") {
|
|
2496
2482
|
return new SafeString(_buildFormTokenJwt(descriptor));
|
|
2497
2483
|
}
|
|
2498
|
-
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;
|
|
2484
|
+
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;
|
|
2499
2485
|
var init_engine = __esm({
|
|
2500
2486
|
"../frond/src/engine.ts"() {
|
|
2501
2487
|
"use strict";
|
|
@@ -2597,6 +2583,25 @@ var init_engine = __esm({
|
|
|
2597
2583
|
MEMO_CACHE_MAX = 1024;
|
|
2598
2584
|
TOKEN_RE = /(\{%-?\s*[\s\S]*?\s*-?%\})|(\{\{-?\s*[\s\S]*?\s*-?\}\})|(\{#[\s\S]*?#\})/g;
|
|
2599
2585
|
RAW_BLOCK_RE = /\{%-?\s*raw\s*-?%\}([\s\S]*?)\{%-?\s*endraw\s*-?%\}/g;
|
|
2586
|
+
EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
2587
|
+
ARITHMETIC_OPERATIONS = {
|
|
2588
|
+
"+": (left, right) => left + right,
|
|
2589
|
+
"-": (left, right) => left - right,
|
|
2590
|
+
"*": (left, right) => left * right,
|
|
2591
|
+
"//": (left, right) => right !== 0 ? Math.floor(left / right) : 0,
|
|
2592
|
+
"/": (left, right) => right !== 0 ? left / right : 0,
|
|
2593
|
+
"%": (left, right) => right !== 0 ? left % right : 0,
|
|
2594
|
+
"**": (left, right) => left ** right
|
|
2595
|
+
};
|
|
2596
|
+
EXPR_EVALUATORS = [
|
|
2597
|
+
evalPrimary,
|
|
2598
|
+
evalConditional,
|
|
2599
|
+
evalConcatOrComparison,
|
|
2600
|
+
evalArithmeticExpression,
|
|
2601
|
+
evalFilterExpression,
|
|
2602
|
+
evalFunctionExpression
|
|
2603
|
+
];
|
|
2604
|
+
expressionFormCache = /* @__PURE__ */ new Map();
|
|
2600
2605
|
VarRef = class {
|
|
2601
2606
|
constructor(name) {
|
|
2602
2607
|
this.name = name;
|
|
@@ -6010,13 +6015,172 @@ var init_databaseUrl = __esm({
|
|
|
6010
6015
|
}
|
|
6011
6016
|
});
|
|
6012
6017
|
|
|
6018
|
+
// ../orm/src/point.ts
|
|
6019
|
+
function formatCoordinate(value) {
|
|
6020
|
+
return Object.is(value, -0) ? "0" : Number(value.toPrecision(15)).toString();
|
|
6021
|
+
}
|
|
6022
|
+
var DEFAULT_SRID, SpatialNotSupportedError, Point;
|
|
6023
|
+
var init_point = __esm({
|
|
6024
|
+
"../orm/src/point.ts"() {
|
|
6025
|
+
"use strict";
|
|
6026
|
+
DEFAULT_SRID = 4326;
|
|
6027
|
+
SpatialNotSupportedError = class extends Error {
|
|
6028
|
+
constructor(message) {
|
|
6029
|
+
super(message);
|
|
6030
|
+
this.name = "SpatialNotSupportedError";
|
|
6031
|
+
}
|
|
6032
|
+
};
|
|
6033
|
+
Point = class _Point {
|
|
6034
|
+
lon;
|
|
6035
|
+
lat;
|
|
6036
|
+
srid;
|
|
6037
|
+
constructor(lon, lat, srid = DEFAULT_SRID) {
|
|
6038
|
+
if (typeof lon === "boolean" || typeof lat === "boolean" || typeof srid === "boolean") {
|
|
6039
|
+
throw new TypeError("Point longitude, latitude and SRID must be numbers");
|
|
6040
|
+
}
|
|
6041
|
+
this.lon = Number(lon);
|
|
6042
|
+
this.lat = Number(lat);
|
|
6043
|
+
this.srid = Number(srid);
|
|
6044
|
+
if (!Number.isFinite(this.lon) || !Number.isFinite(this.lat) || !Number.isInteger(this.srid)) {
|
|
6045
|
+
throw new TypeError("Point longitude and latitude must be finite numbers and SRID must be an integer");
|
|
6046
|
+
}
|
|
6047
|
+
if (this.srid === DEFAULT_SRID) {
|
|
6048
|
+
if (this.lon < -180 || this.lon > 180) throw new RangeError(`Point longitude ${this.lon} is outside -180..180; Tina4 uses longitude, latitude order`);
|
|
6049
|
+
if (this.lat < -90 || this.lat > 90) throw new RangeError(`Point latitude ${this.lat} is outside -90..90; Tina4 uses longitude, latitude order`);
|
|
6050
|
+
}
|
|
6051
|
+
Object.freeze(this);
|
|
6052
|
+
}
|
|
6053
|
+
get wkt() {
|
|
6054
|
+
return `POINT(${formatCoordinate(this.lon)} ${formatCoordinate(this.lat)})`;
|
|
6055
|
+
}
|
|
6056
|
+
get ewkt() {
|
|
6057
|
+
return `SRID=${this.srid};${this.wkt}`;
|
|
6058
|
+
}
|
|
6059
|
+
get geojson() {
|
|
6060
|
+
return { type: "Point", coordinates: [this.lon, this.lat] };
|
|
6061
|
+
}
|
|
6062
|
+
toJSON() {
|
|
6063
|
+
return this.geojson;
|
|
6064
|
+
}
|
|
6065
|
+
toArray() {
|
|
6066
|
+
return [this.lon, this.lat];
|
|
6067
|
+
}
|
|
6068
|
+
static parse(value, srid = DEFAULT_SRID) {
|
|
6069
|
+
if (value instanceof _Point) return value;
|
|
6070
|
+
if (Array.isArray(value)) {
|
|
6071
|
+
if (value.length < 2) throw new TypeError("Point coordinate pair needs longitude and latitude");
|
|
6072
|
+
return new _Point(value[0], value[1], srid);
|
|
6073
|
+
}
|
|
6074
|
+
if (value && typeof value === "object" && !(value instanceof Uint8Array)) {
|
|
6075
|
+
return _Point.fromGeoJson(value, srid);
|
|
6076
|
+
}
|
|
6077
|
+
if (value instanceof Uint8Array) return _Point.fromWkb(value, srid);
|
|
6078
|
+
if (typeof value === "string") {
|
|
6079
|
+
const text = value.trim();
|
|
6080
|
+
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);
|
|
6081
|
+
if (match) return new _Point(match[2], match[3], match[1] ? Number(match[1]) : srid);
|
|
6082
|
+
if (text.length >= 42 && text.length % 2 === 0 && /^[0-9a-f]+$/i.test(text)) {
|
|
6083
|
+
return _Point.fromWkb(Uint8Array.from(Buffer.from(text, "hex")), srid);
|
|
6084
|
+
}
|
|
6085
|
+
}
|
|
6086
|
+
throw new TypeError("Point must be Point, [longitude, latitude], WKT/EWKT, GeoJSON or WKB/EWKB");
|
|
6087
|
+
}
|
|
6088
|
+
static geometryBinding(value, srid = DEFAULT_SRID) {
|
|
6089
|
+
if (value instanceof _Point || Array.isArray(value)) return [_Point.parse(value, srid).ewkt, "ewkt"];
|
|
6090
|
+
if (value && typeof value === "object") {
|
|
6091
|
+
const candidate = value;
|
|
6092
|
+
const geometry = String(candidate.type).toLowerCase() === "feature" ? candidate.geometry : candidate;
|
|
6093
|
+
const allowed = /* @__PURE__ */ new Set(["point", "linestring", "polygon", "multipoint", "multilinestring", "multipolygon", "geometrycollection"]);
|
|
6094
|
+
if (!geometry || !allowed.has(String(geometry.type).toLowerCase())) throw new TypeError("GeoJSON geometry has an unsupported type");
|
|
6095
|
+
return [JSON.stringify(geometry), "geojson"];
|
|
6096
|
+
}
|
|
6097
|
+
if (typeof value === "string" && /^\s*(?:SRID\s*=\s*\d+\s*;\s*)?(?:POINT|LINESTRING|POLYGON|MULTIPOINT|MULTILINESTRING|MULTIPOLYGON|GEOMETRYCOLLECTION)\b/i.test(value)) {
|
|
6098
|
+
return [/^\s*SRID/i.test(value) ? value.trim() : `SRID=${srid};${value.trim()}`, "ewkt"];
|
|
6099
|
+
}
|
|
6100
|
+
throw new TypeError("Geometry must be Point, coordinate pair, WKT/EWKT or GeoJSON");
|
|
6101
|
+
}
|
|
6102
|
+
static fromGeoJson(data, srid) {
|
|
6103
|
+
const geometry = String(data.type).toLowerCase() === "feature" ? data.geometry : data;
|
|
6104
|
+
if (!geometry || String(geometry.type).toLowerCase() !== "point") throw new TypeError("Point GeoJSON type must be Point");
|
|
6105
|
+
const coordinates = geometry.coordinates;
|
|
6106
|
+
if (!Array.isArray(coordinates) || coordinates.length < 2) throw new TypeError("Point GeoJSON coordinates must be [longitude, latitude]");
|
|
6107
|
+
return new _Point(coordinates[0], coordinates[1], srid);
|
|
6108
|
+
}
|
|
6109
|
+
static fromWkb(raw, srid) {
|
|
6110
|
+
if (raw.byteLength < 21) throw new TypeError("Point WKB is too short");
|
|
6111
|
+
const view = new DataView(raw.buffer, raw.byteOffset, raw.byteLength);
|
|
6112
|
+
const little = raw[0] === 1;
|
|
6113
|
+
const typeWord = view.getUint32(1, little);
|
|
6114
|
+
let offset = 5;
|
|
6115
|
+
if ((typeWord & 536870912) !== 0) {
|
|
6116
|
+
srid = view.getUint32(5, little);
|
|
6117
|
+
offset = 9;
|
|
6118
|
+
}
|
|
6119
|
+
const code = (typeWord & ~(536870912 | 1073741824 | 2147483648)) % 1e3;
|
|
6120
|
+
if (code !== 1 || raw.byteLength < offset + 16) throw new TypeError("WKB geometry is not a Point");
|
|
6121
|
+
return new _Point(view.getFloat64(offset, little), view.getFloat64(offset + 8, little), srid);
|
|
6122
|
+
}
|
|
6123
|
+
};
|
|
6124
|
+
}
|
|
6125
|
+
});
|
|
6126
|
+
|
|
6013
6127
|
// ../orm/src/sqlTranslator.ts
|
|
6014
6128
|
var SQLTranslator, QueryCache;
|
|
6015
6129
|
var init_sqlTranslator = __esm({
|
|
6016
6130
|
"../orm/src/sqlTranslator.ts"() {
|
|
6017
6131
|
"use strict";
|
|
6018
6132
|
init_databaseUrl();
|
|
6133
|
+
init_point();
|
|
6019
6134
|
SQLTranslator = class _SQLTranslator {
|
|
6135
|
+
static SPATIAL_ENGINES = /* @__PURE__ */ new Set(["postgres", "postgresql"]);
|
|
6136
|
+
static SPATIAL_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*$/;
|
|
6137
|
+
static requireSpatial(engine, feature) {
|
|
6138
|
+
const name = String(engine || "unknown").toLowerCase();
|
|
6139
|
+
if (!_SQLTranslator.SPATIAL_ENGINES.has(name)) {
|
|
6140
|
+
throw new SpatialNotSupportedError(
|
|
6141
|
+
`${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.`
|
|
6142
|
+
);
|
|
6143
|
+
}
|
|
6144
|
+
return name;
|
|
6145
|
+
}
|
|
6146
|
+
static spatialIdentifier(name, what = "column") {
|
|
6147
|
+
if (!_SQLTranslator.SPATIAL_IDENTIFIER.test(name)) throw new TypeError(`Spatial ${what} is not a valid SQL identifier: ${name}`);
|
|
6148
|
+
return name;
|
|
6149
|
+
}
|
|
6150
|
+
static pointColumnType(engine, srid = DEFAULT_SRID) {
|
|
6151
|
+
_SQLTranslator.requireSpatial(engine, "PointField");
|
|
6152
|
+
return `geography(Point,${srid})`;
|
|
6153
|
+
}
|
|
6154
|
+
static spatialIndex(engine, table2, column2) {
|
|
6155
|
+
_SQLTranslator.requireSpatial(engine, "spatial index creation");
|
|
6156
|
+
table2 = _SQLTranslator.spatialIdentifier(table2, "table");
|
|
6157
|
+
column2 = _SQLTranslator.spatialIdentifier(column2);
|
|
6158
|
+
return `CREATE INDEX IF NOT EXISTS ${table2.replaceAll(".", "_")}_${column2}_gist ON ${table2} USING GIST (${column2})`;
|
|
6159
|
+
}
|
|
6160
|
+
static pointLiteral(engine, srid = DEFAULT_SRID) {
|
|
6161
|
+
_SQLTranslator.requireSpatial(engine, "spatial predicates");
|
|
6162
|
+
return `ST_SetSRID(ST_MakePoint(?, ?), ${srid})::geography`;
|
|
6163
|
+
}
|
|
6164
|
+
static withinDistance(engine, column2, srid = DEFAULT_SRID) {
|
|
6165
|
+
return `ST_DWithin(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.pointLiteral(engine, srid)}, ?)`;
|
|
6166
|
+
}
|
|
6167
|
+
static distance(engine, column2, srid = DEFAULT_SRID) {
|
|
6168
|
+
return `ST_Distance(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.pointLiteral(engine, srid)})`;
|
|
6169
|
+
}
|
|
6170
|
+
static distanceAs(engine, column2, alias, srid = DEFAULT_SRID) {
|
|
6171
|
+
return `${_SQLTranslator.distance(engine, column2, srid)} AS ${_SQLTranslator.spatialIdentifier(alias, "result alias")}`;
|
|
6172
|
+
}
|
|
6173
|
+
static geometryLiteral(engine, form, srid = DEFAULT_SRID) {
|
|
6174
|
+
_SQLTranslator.requireSpatial(engine, "spatial predicates");
|
|
6175
|
+
return form === "ewkt" ? "ST_GeogFromText(?)" : `ST_SetSRID(ST_GeomFromGeoJSON(?), ${srid})::geography`;
|
|
6176
|
+
}
|
|
6177
|
+
static intersects(engine, column2, form = "ewkt", srid = DEFAULT_SRID) {
|
|
6178
|
+
return `ST_Intersects(${_SQLTranslator.spatialIdentifier(column2)}, ${_SQLTranslator.geometryLiteral(engine, form, srid)})`;
|
|
6179
|
+
}
|
|
6180
|
+
static bbox(engine, column2, srid = DEFAULT_SRID) {
|
|
6181
|
+
_SQLTranslator.requireSpatial(engine, "bbox");
|
|
6182
|
+
return `ST_Intersects(${_SQLTranslator.spatialIdentifier(column2)}, ST_MakeEnvelope(?, ?, ?, ?, ${srid})::geography)`;
|
|
6183
|
+
}
|
|
6020
6184
|
/**
|
|
6021
6185
|
* Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
6022
6186
|
*
|
|
@@ -7670,6 +7834,8 @@ function fieldTypeToPostgres(def) {
|
|
|
7670
7834
|
return "TEXT";
|
|
7671
7835
|
case "json":
|
|
7672
7836
|
return "JSONB";
|
|
7837
|
+
case "point":
|
|
7838
|
+
return SQLTranslator.pointColumnType("postgres", def.srid ?? 4326);
|
|
7673
7839
|
case "string":
|
|
7674
7840
|
return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
|
|
7675
7841
|
default:
|
|
@@ -13293,10 +13459,13 @@ var init_queryBuilder = __esm({
|
|
|
13293
13459
|
"use strict";
|
|
13294
13460
|
init_database();
|
|
13295
13461
|
init_databaseResult();
|
|
13462
|
+
init_point();
|
|
13463
|
+
init_sqlTranslator();
|
|
13296
13464
|
QueryBuilder = class _QueryBuilder {
|
|
13297
13465
|
table;
|
|
13298
13466
|
db;
|
|
13299
13467
|
columns = ["*"];
|
|
13468
|
+
selectParams = [];
|
|
13300
13469
|
wheres = [];
|
|
13301
13470
|
params = [];
|
|
13302
13471
|
joinClauses = [];
|
|
@@ -13304,14 +13473,17 @@ var init_queryBuilder = __esm({
|
|
|
13304
13473
|
havings = [];
|
|
13305
13474
|
havingParams = [];
|
|
13306
13475
|
orderByCols = [];
|
|
13476
|
+
orderByParams = [];
|
|
13477
|
+
primaryKey;
|
|
13307
13478
|
limitVal;
|
|
13308
13479
|
offsetVal;
|
|
13309
13480
|
/**
|
|
13310
13481
|
* Private constructor — use static factory methods.
|
|
13311
13482
|
*/
|
|
13312
|
-
constructor(table2, db) {
|
|
13483
|
+
constructor(table2, db, primaryKey) {
|
|
13313
13484
|
this.table = table2;
|
|
13314
13485
|
this.db = db;
|
|
13486
|
+
this.primaryKey = primaryKey;
|
|
13315
13487
|
}
|
|
13316
13488
|
/**
|
|
13317
13489
|
* Create a QueryBuilder for a table.
|
|
@@ -13320,8 +13492,8 @@ var init_queryBuilder = __esm({
|
|
|
13320
13492
|
* @param db - Optional database adapter.
|
|
13321
13493
|
* @returns A new QueryBuilder instance.
|
|
13322
13494
|
*/
|
|
13323
|
-
static fromTable(tableName, db) {
|
|
13324
|
-
return new _QueryBuilder(tableName, db);
|
|
13495
|
+
static fromTable(tableName, db, primaryKey) {
|
|
13496
|
+
return new _QueryBuilder(tableName, db, primaryKey);
|
|
13325
13497
|
}
|
|
13326
13498
|
/**
|
|
13327
13499
|
* Set the columns to select.
|
|
@@ -13332,6 +13504,7 @@ var init_queryBuilder = __esm({
|
|
|
13332
13504
|
select(...cols) {
|
|
13333
13505
|
if (cols.length > 0) {
|
|
13334
13506
|
this.columns = cols;
|
|
13507
|
+
this.selectParams = [];
|
|
13335
13508
|
}
|
|
13336
13509
|
return this;
|
|
13337
13510
|
}
|
|
@@ -13413,6 +13586,41 @@ var init_queryBuilder = __esm({
|
|
|
13413
13586
|
this.orderByCols.push(expression);
|
|
13414
13587
|
return this;
|
|
13415
13588
|
}
|
|
13589
|
+
withinDistance(column2, pointValue, radiusMetres, srid = DEFAULT_SRID) {
|
|
13590
|
+
const radius = Number(radiusMetres);
|
|
13591
|
+
if (!Number.isFinite(radius) || radius < 0) throw new RangeError("Spatial radius must be finite and greater than or equal to zero");
|
|
13592
|
+
const point = Point.parse(pointValue, srid);
|
|
13593
|
+
return this.where(SQLTranslator.withinDistance(this.engine(), column2, point.srid), [point.lon, point.lat, radius]);
|
|
13594
|
+
}
|
|
13595
|
+
intersects(column2, geometry, srid = DEFAULT_SRID) {
|
|
13596
|
+
const [bound, form] = Point.geometryBinding(geometry, srid);
|
|
13597
|
+
return this.where(SQLTranslator.intersects(this.engine(), column2, form, srid), [bound]);
|
|
13598
|
+
}
|
|
13599
|
+
bbox(column2, minLon, minLat, maxLon, maxLat, srid = DEFAULT_SRID) {
|
|
13600
|
+
const values = [minLon, minLat, maxLon, maxLat].map(Number);
|
|
13601
|
+
if (!values.every(Number.isFinite)) throw new TypeError("Bounding-box coordinates must be finite numbers");
|
|
13602
|
+
const [west, south, east, north] = values;
|
|
13603
|
+
new Point(west, south, srid);
|
|
13604
|
+
new Point(east, north, srid);
|
|
13605
|
+
if (west > east || south > north) throw new RangeError("Bounding box must be ordered west, south, east, north");
|
|
13606
|
+
return this.where(SQLTranslator.bbox(this.engine(), column2, srid), values);
|
|
13607
|
+
}
|
|
13608
|
+
selectDistance(column2, pointValue, alias = "distance", srid = DEFAULT_SRID) {
|
|
13609
|
+
const point = Point.parse(pointValue, srid);
|
|
13610
|
+
this.columns.push(SQLTranslator.distanceAs(this.engine(), column2, alias, point.srid));
|
|
13611
|
+
this.selectParams.push(point.lon, point.lat);
|
|
13612
|
+
return this;
|
|
13613
|
+
}
|
|
13614
|
+
orderByDistance(column2, pointValue, direction = "ASC", srid = DEFAULT_SRID) {
|
|
13615
|
+
const order = direction.toUpperCase();
|
|
13616
|
+
if (order !== "ASC" && order !== "DESC") throw new TypeError("Distance order direction must be ASC or DESC");
|
|
13617
|
+
if (!this.primaryKey) throw new Error("Stable spatial ordering needs a primary key; use BaseModel.query() or pass one to fromTable()");
|
|
13618
|
+
const point = Point.parse(pointValue, srid);
|
|
13619
|
+
this.orderByCols.push(`${SQLTranslator.distance(this.engine(), column2, point.srid)} ${order}`);
|
|
13620
|
+
this.orderByParams.push(point.lon, point.lat);
|
|
13621
|
+
this.orderByCols.push(`${SQLTranslator.spatialIdentifier(this.primaryKey, "primary key")} ASC`);
|
|
13622
|
+
return this;
|
|
13623
|
+
}
|
|
13416
13624
|
/**
|
|
13417
13625
|
* Set LIMIT and optional OFFSET.
|
|
13418
13626
|
*
|
|
@@ -13478,7 +13686,7 @@ var init_queryBuilder = __esm({
|
|
|
13478
13686
|
async get() {
|
|
13479
13687
|
this.ensureDb();
|
|
13480
13688
|
const sql = this.toSql();
|
|
13481
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13689
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13482
13690
|
const queryParams = allParams.length > 0 ? allParams : void 0;
|
|
13483
13691
|
const rows = await adapterFetch(
|
|
13484
13692
|
this.db,
|
|
@@ -13506,7 +13714,7 @@ var init_queryBuilder = __esm({
|
|
|
13506
13714
|
async first() {
|
|
13507
13715
|
this.ensureDb();
|
|
13508
13716
|
const sql = this.toSql();
|
|
13509
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13717
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13510
13718
|
return adapterFetchOne(
|
|
13511
13719
|
this.db,
|
|
13512
13720
|
sql,
|
|
@@ -13521,9 +13729,18 @@ var init_queryBuilder = __esm({
|
|
|
13521
13729
|
async count() {
|
|
13522
13730
|
this.ensureDb();
|
|
13523
13731
|
const original = this.columns;
|
|
13732
|
+
const originalSelectParams = this.selectParams;
|
|
13733
|
+
const originalOrder = this.orderByCols;
|
|
13734
|
+
const originalOrderParams = this.orderByParams;
|
|
13524
13735
|
this.columns = ["COUNT(*) as cnt"];
|
|
13736
|
+
this.selectParams = [];
|
|
13737
|
+
this.orderByCols = [];
|
|
13738
|
+
this.orderByParams = [];
|
|
13525
13739
|
const sql = this.toSql();
|
|
13526
13740
|
this.columns = original;
|
|
13741
|
+
this.selectParams = originalSelectParams;
|
|
13742
|
+
this.orderByCols = originalOrder;
|
|
13743
|
+
this.orderByParams = originalOrderParams;
|
|
13527
13744
|
const allParams = [...this.params, ...this.havingParams];
|
|
13528
13745
|
const row = await adapterFetchOne(
|
|
13529
13746
|
this.db,
|
|
@@ -13708,6 +13925,10 @@ var init_queryBuilder = __esm({
|
|
|
13708
13925
|
}
|
|
13709
13926
|
}
|
|
13710
13927
|
}
|
|
13928
|
+
engine() {
|
|
13929
|
+
this.ensureDb();
|
|
13930
|
+
return this.db.getDatabaseType();
|
|
13931
|
+
}
|
|
13711
13932
|
};
|
|
13712
13933
|
}
|
|
13713
13934
|
});
|
|
@@ -13731,6 +13952,11 @@ function toDbFieldValue(def, value) {
|
|
|
13731
13952
|
if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
|
|
13732
13953
|
return JSON.stringify(value);
|
|
13733
13954
|
}
|
|
13955
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
13956
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
13957
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
13958
|
+
return point.ewkt;
|
|
13959
|
+
}
|
|
13734
13960
|
return value;
|
|
13735
13961
|
}
|
|
13736
13962
|
function fromDbFieldValue(def, value) {
|
|
@@ -13741,6 +13967,11 @@ function fromDbFieldValue(def, value) {
|
|
|
13741
13967
|
return value;
|
|
13742
13968
|
}
|
|
13743
13969
|
}
|
|
13970
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
13971
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
13972
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
13973
|
+
return point;
|
|
13974
|
+
}
|
|
13744
13975
|
return value;
|
|
13745
13976
|
}
|
|
13746
13977
|
function _pluralRelKeys() {
|
|
@@ -13776,6 +14007,7 @@ var init_baseModel = __esm({
|
|
|
13776
14007
|
init_sqlite();
|
|
13777
14008
|
init_sqlTranslator();
|
|
13778
14009
|
init_src3();
|
|
14010
|
+
init_point();
|
|
13779
14011
|
_fkRegistry = /* @__PURE__ */ new Map();
|
|
13780
14012
|
EAGER_IN_CHUNK = 500;
|
|
13781
14013
|
modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
|
|
@@ -13831,7 +14063,9 @@ var init_baseModel = __esm({
|
|
|
13831
14063
|
for (const [name, def] of Object.entries(fields0)) {
|
|
13832
14064
|
if (def.default === void 0) continue;
|
|
13833
14065
|
let dv = typeof def.default === "function" ? def.default() : def.default;
|
|
13834
|
-
if (dv !== null &&
|
|
14066
|
+
if (def.type === "point" && dv !== null && dv !== void 0) {
|
|
14067
|
+
dv = fromDbFieldValue(def, dv);
|
|
14068
|
+
} else if (dv !== null && typeof dv === "object") dv = structuredClone(dv);
|
|
13835
14069
|
this[name] = dv;
|
|
13836
14070
|
}
|
|
13837
14071
|
if (data) {
|
|
@@ -13933,7 +14167,7 @@ var init_baseModel = __esm({
|
|
|
13933
14167
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
13934
14168
|
*/
|
|
13935
14169
|
static query() {
|
|
13936
|
-
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
14170
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
|
|
13937
14171
|
}
|
|
13938
14172
|
/**
|
|
13939
14173
|
* Get the database adapter for this model.
|
|
@@ -14386,7 +14620,7 @@ var init_baseModel = __esm({
|
|
|
14386
14620
|
for (const key of Object.keys(ModelClass.fields)) {
|
|
14387
14621
|
if (this[key] !== void 0) {
|
|
14388
14622
|
const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
|
|
14389
|
-
result[outKey] = this[key];
|
|
14623
|
+
result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
|
|
14390
14624
|
}
|
|
14391
14625
|
}
|
|
14392
14626
|
if (ModelClass.softDelete && this.is_deleted !== void 0) {
|
|
@@ -14446,6 +14680,19 @@ var init_baseModel = __esm({
|
|
|
14446
14680
|
}
|
|
14447
14681
|
return result;
|
|
14448
14682
|
}
|
|
14683
|
+
toFeature(geometryField, include) {
|
|
14684
|
+
const ModelClass = this.constructor;
|
|
14685
|
+
const pointFields = Object.entries(ModelClass.fields).filter(([, def]) => def.type === "point").map(([name]) => name);
|
|
14686
|
+
const field = geometryField ?? pointFields[0];
|
|
14687
|
+
if (!field || !pointFields.includes(field)) throw new Error("toFeature() needs a declared point field");
|
|
14688
|
+
const properties = this.toDict(include, "camel");
|
|
14689
|
+
const geometry = properties[field] ?? null;
|
|
14690
|
+
delete properties[field];
|
|
14691
|
+
return { type: "Feature", geometry, properties };
|
|
14692
|
+
}
|
|
14693
|
+
static featureCollection(models, geometryField, include) {
|
|
14694
|
+
return { type: "FeatureCollection", features: models.map((model) => model.toFeature(geometryField, include)) };
|
|
14695
|
+
}
|
|
14449
14696
|
/**
|
|
14450
14697
|
* Convert to an associative object (alias for toDict).
|
|
14451
14698
|
*/
|
|
@@ -14496,7 +14743,10 @@ var init_baseModel = __esm({
|
|
|
14496
14743
|
*/
|
|
14497
14744
|
static async createTable() {
|
|
14498
14745
|
const db = this.getDb();
|
|
14499
|
-
|
|
14746
|
+
const pointFields = Object.entries(this.fields).filter(([, def]) => def.type === "point");
|
|
14747
|
+
const engine = db.getDatabaseType();
|
|
14748
|
+
if (pointFields.length > 0) SQLTranslator.requireSpatial(engine, "PointField");
|
|
14749
|
+
if (await adapterTableExists(db, this.tableName)) return this.createSpatialIndexes(db, pointFields);
|
|
14500
14750
|
if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
|
|
14501
14751
|
const mappedFields = {};
|
|
14502
14752
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
@@ -14512,7 +14762,7 @@ var init_baseModel = __esm({
|
|
|
14512
14762
|
mappedFields["is_deleted"] = { type: "integer", default: 0 };
|
|
14513
14763
|
}
|
|
14514
14764
|
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
14515
|
-
return
|
|
14765
|
+
return this.createSpatialIndexes(db, pointFields);
|
|
14516
14766
|
}
|
|
14517
14767
|
const typeMap = {
|
|
14518
14768
|
integer: "INTEGER",
|
|
@@ -14559,6 +14809,14 @@ var init_baseModel = __esm({
|
|
|
14559
14809
|
}
|
|
14560
14810
|
return true;
|
|
14561
14811
|
}
|
|
14812
|
+
static async createSpatialIndexes(db, fields) {
|
|
14813
|
+
for (const [fieldName, def] of fields) {
|
|
14814
|
+
SQLTranslator.pointColumnType(db.getDatabaseType(), def.srid ?? DEFAULT_SRID);
|
|
14815
|
+
if (def.spatialIndex === false) continue;
|
|
14816
|
+
await adapterExecute(db, SQLTranslator.spatialIndex(db.getDatabaseType(), this.tableName, this.getDbColumn(fieldName)));
|
|
14817
|
+
}
|
|
14818
|
+
return true;
|
|
14819
|
+
}
|
|
14562
14820
|
/**
|
|
14563
14821
|
* Find a record by primary key or throw an error if not found.
|
|
14564
14822
|
*/
|
|
@@ -17206,6 +17464,7 @@ __export(src_exports, {
|
|
|
17206
17464
|
CachedDatabaseAdapter: () => CachedDatabaseAdapter,
|
|
17207
17465
|
Cursor: () => Cursor,
|
|
17208
17466
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
17467
|
+
DEFAULT_SRID: () => DEFAULT_SRID,
|
|
17209
17468
|
Database: () => Database,
|
|
17210
17469
|
DatabaseResult: () => DatabaseResult,
|
|
17211
17470
|
DatabaseUrl: () => DatabaseUrl,
|
|
@@ -17221,6 +17480,7 @@ __export(src_exports, {
|
|
|
17221
17480
|
NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
|
|
17222
17481
|
ObjectId: () => ObjectId,
|
|
17223
17482
|
OdbcAdapter: () => OdbcAdapter,
|
|
17483
|
+
Point: () => Point,
|
|
17224
17484
|
PostgresAdapter: () => PostgresAdapter,
|
|
17225
17485
|
QueryBuilder: () => QueryBuilder,
|
|
17226
17486
|
QueryCache: () => QueryCache,
|
|
@@ -17233,6 +17493,7 @@ __export(src_exports, {
|
|
|
17233
17493
|
S3Storage: () => S3Storage,
|
|
17234
17494
|
SQLTranslator: () => SQLTranslator,
|
|
17235
17495
|
SQLiteAdapter: () => SQLiteAdapter,
|
|
17496
|
+
SpatialNotSupportedError: () => SpatialNotSupportedError,
|
|
17236
17497
|
SqliteCollection: () => SqliteCollection,
|
|
17237
17498
|
SqliteDatabase: () => SqliteDatabase,
|
|
17238
17499
|
adapterColumns: () => adapterColumns,
|
|
@@ -17327,6 +17588,7 @@ var init_src = __esm({
|
|
|
17327
17588
|
init_baseModel();
|
|
17328
17589
|
init_queryBuilder();
|
|
17329
17590
|
init_sqlTranslator();
|
|
17591
|
+
init_point();
|
|
17330
17592
|
init_connectTimeout();
|
|
17331
17593
|
init_cachedDatabase();
|
|
17332
17594
|
init_fakeData2();
|
|
@@ -19516,6 +19778,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19516
19778
|
}
|
|
19517
19779
|
}
|
|
19518
19780
|
if (!resolvedToken) {
|
|
19781
|
+
const sso = req2.session?.get?.("_tina4_sso");
|
|
19782
|
+
const identity = sso?.identity;
|
|
19783
|
+
if (identity?.issuer && identity?.subject) {
|
|
19784
|
+
req2.user = identity;
|
|
19785
|
+
return false;
|
|
19786
|
+
}
|
|
19519
19787
|
const sessionToken = req2.session?.get?.("token");
|
|
19520
19788
|
if (sessionToken && validToken(sessionToken)) {
|
|
19521
19789
|
resolvedToken = sessionToken;
|
|
@@ -23000,7 +23268,7 @@ var init_metrics = __esm({
|
|
|
23000
23268
|
};
|
|
23001
23269
|
INSTALL_HINT = "update the native tina4 CLI: https://tina4.com/cli";
|
|
23002
23270
|
SUMMARY_KEYS = ["files_analyzed", "total_functions", "avg_complexity", "avg_maintainability"];
|
|
23003
|
-
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "
|
|
23271
|
+
FILE_KEYS = ["path", "loc", "avg_complexity", "maintainability", "has_referencing_test"];
|
|
23004
23272
|
FUNCTION_KEYS = ["name", "file", "line", "complexity", "loc"];
|
|
23005
23273
|
}
|
|
23006
23274
|
});
|
|
@@ -34087,6 +34355,14 @@ function resolveSecuritySchemes() {
|
|
|
34087
34355
|
const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
|
|
34088
34356
|
schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
|
|
34089
34357
|
}
|
|
34358
|
+
const ssoIssuer = (process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
34359
|
+
if (ssoIssuer) {
|
|
34360
|
+
schemes.oidc = {
|
|
34361
|
+
type: "openIdConnect",
|
|
34362
|
+
openIdConnectUrl: `${ssoIssuer}/.well-known/openid-configuration`
|
|
34363
|
+
};
|
|
34364
|
+
schemes.ssoSession = { type: "apiKey", in: "cookie", name: "tina4_session" };
|
|
34365
|
+
}
|
|
34090
34366
|
for (const [name, def] of Object.entries(registeredSchemes)) {
|
|
34091
34367
|
schemes[name] = def;
|
|
34092
34368
|
}
|
|
@@ -34268,7 +34544,9 @@ function generate(routes, models = []) {
|
|
|
34268
34544
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34269
34545
|
}
|
|
34270
34546
|
} else if (routeRequiresAuth(route, method)) {
|
|
34271
|
-
|
|
34547
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
34548
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
34549
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
34272
34550
|
const responses = operation.responses;
|
|
34273
34551
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34274
34552
|
}
|
|
@@ -34582,6 +34860,298 @@ var init_src2 = __esm({
|
|
|
34582
34860
|
}
|
|
34583
34861
|
});
|
|
34584
34862
|
|
|
34863
|
+
// ../core/src/sso.ts
|
|
34864
|
+
var sso_exports = {};
|
|
34865
|
+
__export(sso_exports, {
|
|
34866
|
+
SSO: () => Sso,
|
|
34867
|
+
Sso: () => Sso,
|
|
34868
|
+
SsoError: () => SsoError
|
|
34869
|
+
});
|
|
34870
|
+
import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
34871
|
+
var SsoError, Sso;
|
|
34872
|
+
var init_sso = __esm({
|
|
34873
|
+
"../core/src/sso.ts"() {
|
|
34874
|
+
"use strict";
|
|
34875
|
+
SsoError = class extends Error {
|
|
34876
|
+
};
|
|
34877
|
+
Sso = class _Sso {
|
|
34878
|
+
static PENDING_KEY = "_tina4_sso_pending";
|
|
34879
|
+
static SESSION_KEY = "_tina4_sso";
|
|
34880
|
+
issuer;
|
|
34881
|
+
clientId;
|
|
34882
|
+
clientSecret;
|
|
34883
|
+
redirectUri;
|
|
34884
|
+
scopes;
|
|
34885
|
+
verify;
|
|
34886
|
+
postLogoutRedirectUri;
|
|
34887
|
+
claimMap;
|
|
34888
|
+
timeout;
|
|
34889
|
+
metadata = {};
|
|
34890
|
+
static mountedRouters = /* @__PURE__ */ new WeakSet();
|
|
34891
|
+
constructor(options = {}) {
|
|
34892
|
+
this.issuer = (options.issuer ?? process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
34893
|
+
this.clientId = options.clientId ?? process.env.TINA4_SSO_CLIENT_ID ?? "";
|
|
34894
|
+
this.clientSecret = options.clientSecret ?? process.env.TINA4_SSO_CLIENT_SECRET;
|
|
34895
|
+
this.redirectUri = options.redirectUri ?? process.env.TINA4_SSO_REDIRECT_URI ?? "";
|
|
34896
|
+
this.scopes = options.scopes ?? this.jsonEnv("TINA4_SSO_SCOPES", ["openid", "profile", "email"]);
|
|
34897
|
+
this.verify = options.verify ?? process.env.TINA4_SSO_VERIFY ?? "introspection";
|
|
34898
|
+
this.postLogoutRedirectUri = options.postLogoutRedirectUri ?? process.env.TINA4_SSO_POST_LOGOUT_REDIRECT_URI;
|
|
34899
|
+
this.claimMap = options.claimMap ?? this.jsonEnv("TINA4_SSO_CLAIM_MAP", {});
|
|
34900
|
+
this.timeout = options.timeout ?? 1e4;
|
|
34901
|
+
this.validateConfig();
|
|
34902
|
+
}
|
|
34903
|
+
static async fromIssuer(options = {}) {
|
|
34904
|
+
const value = new _Sso(options);
|
|
34905
|
+
await value.discover();
|
|
34906
|
+
return value;
|
|
34907
|
+
}
|
|
34908
|
+
static configured() {
|
|
34909
|
+
return ["TINA4_SSO_ISSUER", "TINA4_SSO_CLIENT_ID", "TINA4_SSO_REDIRECT_URI"].every((key) => Boolean(process.env[key]));
|
|
34910
|
+
}
|
|
34911
|
+
jsonEnv(name, fallback) {
|
|
34912
|
+
const raw = process.env[name];
|
|
34913
|
+
if (!raw) return fallback;
|
|
34914
|
+
try {
|
|
34915
|
+
return JSON.parse(raw);
|
|
34916
|
+
} catch {
|
|
34917
|
+
throw new SsoError(`${name} must be valid JSON`);
|
|
34918
|
+
}
|
|
34919
|
+
}
|
|
34920
|
+
static secureUrl(value, name) {
|
|
34921
|
+
let url;
|
|
34922
|
+
try {
|
|
34923
|
+
url = new URL(value);
|
|
34924
|
+
} catch {
|
|
34925
|
+
throw new SsoError(`${name} must be an absolute URL`);
|
|
34926
|
+
}
|
|
34927
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
34928
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
34929
|
+
throw new SsoError(`${name} must use HTTPS except on loopback`);
|
|
34930
|
+
}
|
|
34931
|
+
}
|
|
34932
|
+
validateConfig() {
|
|
34933
|
+
if (!this.issuer || !this.clientId || !this.redirectUri) {
|
|
34934
|
+
throw new SsoError("TINA4_SSO_ISSUER, TINA4_SSO_CLIENT_ID and TINA4_SSO_REDIRECT_URI are required");
|
|
34935
|
+
}
|
|
34936
|
+
_Sso.secureUrl(this.issuer, "issuer");
|
|
34937
|
+
_Sso.secureUrl(this.redirectUri, "redirect URI");
|
|
34938
|
+
if (!["introspection", "jwks"].includes(this.verify)) throw new SsoError("TINA4_SSO_VERIFY must be introspection or jwks");
|
|
34939
|
+
if (this.verify === "jwks") throw new SsoError("jwks verification requires an installed cryptography capability");
|
|
34940
|
+
if (this.verify === "introspection" && !this.clientSecret) throw new SsoError("introspection verification requires TINA4_SSO_CLIENT_SECRET");
|
|
34941
|
+
if (!Array.isArray(this.scopes) || !this.scopes.includes("openid")) throw new SsoError("TINA4_SSO_SCOPES must be a list containing openid");
|
|
34942
|
+
}
|
|
34943
|
+
async requestJson(url, form, bearer, basic = false) {
|
|
34944
|
+
const headers = { Accept: "application/json" };
|
|
34945
|
+
let body;
|
|
34946
|
+
if (form) {
|
|
34947
|
+
const parameters = new URLSearchParams();
|
|
34948
|
+
for (const [key, value] of Object.entries(form)) parameters.set(key, String(value));
|
|
34949
|
+
body = parameters.toString();
|
|
34950
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
34951
|
+
}
|
|
34952
|
+
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
34953
|
+
if (basic) headers.Authorization = `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`;
|
|
34954
|
+
const controller = new AbortController();
|
|
34955
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
34956
|
+
try {
|
|
34957
|
+
const response = await fetch(url, { method: form ? "POST" : "GET", headers, body, signal: controller.signal });
|
|
34958
|
+
if (!response.ok) throw new SsoError("OIDC provider request failed");
|
|
34959
|
+
const result = await response.json();
|
|
34960
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new SsoError("OIDC provider returned a non-object response");
|
|
34961
|
+
return result;
|
|
34962
|
+
} catch (error) {
|
|
34963
|
+
if (error instanceof SsoError) throw error;
|
|
34964
|
+
throw new SsoError("OIDC provider request failed");
|
|
34965
|
+
} finally {
|
|
34966
|
+
clearTimeout(timer);
|
|
34967
|
+
}
|
|
34968
|
+
}
|
|
34969
|
+
async discover(force = false) {
|
|
34970
|
+
if (Object.keys(this.metadata).length && !force) return { ...this.metadata };
|
|
34971
|
+
const result = await this.requestJson(`${this.issuer}/.well-known/openid-configuration`);
|
|
34972
|
+
if (result.issuer !== this.issuer) throw new SsoError("OIDC discovery issuer does not exactly match configuration");
|
|
34973
|
+
const required = ["authorization_endpoint", "token_endpoint"];
|
|
34974
|
+
if (this.verify === "introspection") required.push("introspection_endpoint");
|
|
34975
|
+
for (const key of required) {
|
|
34976
|
+
if (!result[key]) throw new SsoError(`OIDC discovery is missing ${key}`);
|
|
34977
|
+
_Sso.secureUrl(result[key], key);
|
|
34978
|
+
}
|
|
34979
|
+
this.metadata = result;
|
|
34980
|
+
return { ...result };
|
|
34981
|
+
}
|
|
34982
|
+
static safeReturn(value) {
|
|
34983
|
+
if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return "/";
|
|
34984
|
+
return [...value].some((char) => char.charCodeAt(0) < 32) ? "/" : value;
|
|
34985
|
+
}
|
|
34986
|
+
session(value) {
|
|
34987
|
+
return value?.session ?? value;
|
|
34988
|
+
}
|
|
34989
|
+
async login(requestOrSession, returnTo = "/") {
|
|
34990
|
+
const session = this.session(requestOrSession);
|
|
34991
|
+
if (!session) throw new SsoError("SSO login requires a Tina4 Session");
|
|
34992
|
+
const state = randomBytes7(32).toString("base64url");
|
|
34993
|
+
const nonce = randomBytes7(32).toString("base64url");
|
|
34994
|
+
const verifier = randomBytes7(64).toString("base64url");
|
|
34995
|
+
const challenge = createHash9("sha256").update(verifier).digest("base64url");
|
|
34996
|
+
session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
|
|
34997
|
+
const metadata = await this.discover();
|
|
34998
|
+
const query = new URLSearchParams({
|
|
34999
|
+
client_id: this.clientId,
|
|
35000
|
+
redirect_uri: this.redirectUri,
|
|
35001
|
+
response_type: "code",
|
|
35002
|
+
scope: this.scopes.join(" "),
|
|
35003
|
+
state,
|
|
35004
|
+
nonce,
|
|
35005
|
+
code_challenge: challenge,
|
|
35006
|
+
code_challenge_method: "S256"
|
|
35007
|
+
});
|
|
35008
|
+
return `${metadata.authorization_endpoint}?${query}`;
|
|
35009
|
+
}
|
|
35010
|
+
static equal(left, right) {
|
|
35011
|
+
const a = Buffer.from(String(left ?? ""));
|
|
35012
|
+
const b = Buffer.from(String(right ?? ""));
|
|
35013
|
+
return a.length === b.length && timingSafeEqual3(a, b);
|
|
35014
|
+
}
|
|
35015
|
+
static jwtPayload(token) {
|
|
35016
|
+
try {
|
|
35017
|
+
return JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
35018
|
+
} catch {
|
|
35019
|
+
throw new SsoError("provider returned an invalid ID token");
|
|
35020
|
+
}
|
|
35021
|
+
}
|
|
35022
|
+
async introspect(accessToken) {
|
|
35023
|
+
const metadata = await this.discover();
|
|
35024
|
+
const result = await this.requestJson(metadata.introspection_endpoint, { token: accessToken, token_type_hint: "access_token" }, void 0, true);
|
|
35025
|
+
if (result.active !== true || result.iss !== this.issuer) throw new SsoError("OIDC access token is inactive or has the wrong issuer");
|
|
35026
|
+
const audience = result.aud ?? result.client_id;
|
|
35027
|
+
const valid = (Array.isArray(audience) ? audience.includes(this.clientId) : audience === this.clientId) || result.client_id === this.clientId;
|
|
35028
|
+
if (!valid) throw new SsoError("OIDC token audience mismatch");
|
|
35029
|
+
return result;
|
|
35030
|
+
}
|
|
35031
|
+
claim(claims, configured, fallback) {
|
|
35032
|
+
let value = claims;
|
|
35033
|
+
for (const part of (configured ?? fallback).split(".")) value = value && typeof value === "object" ? value[part] : void 0;
|
|
35034
|
+
return value;
|
|
35035
|
+
}
|
|
35036
|
+
normalize(claims) {
|
|
35037
|
+
const subject = this.claim(claims, this.claimMap.subject, "sub");
|
|
35038
|
+
const issuer = this.claim(claims, this.claimMap.issuer, "iss") ?? this.issuer;
|
|
35039
|
+
if (!subject || issuer !== this.issuer) throw new SsoError("OIDC identity is missing a valid issuer or subject");
|
|
35040
|
+
const roles = [...this.claim(claims, this.claimMap.roles, "realm_access.roles") ?? [], ...claims.resource_access?.[this.clientId]?.roles ?? []];
|
|
35041
|
+
const groups = this.claim(claims, this.claimMap.groups, "groups") ?? [];
|
|
35042
|
+
return {
|
|
35043
|
+
issuer,
|
|
35044
|
+
subject,
|
|
35045
|
+
username: this.claim(claims, this.claimMap.username, "preferred_username") ?? null,
|
|
35046
|
+
email: this.claim(claims, this.claimMap.email, "email") ?? null,
|
|
35047
|
+
name: this.claim(claims, this.claimMap.name, "name") ?? null,
|
|
35048
|
+
roles: [...new Set(roles.map(String))].sort(),
|
|
35049
|
+
groups: [...new Set(groups.map(String))].sort()
|
|
35050
|
+
};
|
|
35051
|
+
}
|
|
35052
|
+
async callback(requestOrSession, query) {
|
|
35053
|
+
const session = this.session(requestOrSession);
|
|
35054
|
+
const values = query ?? requestOrSession?.query ?? {};
|
|
35055
|
+
const pending = session?.get(_Sso.PENDING_KEY);
|
|
35056
|
+
session?.delete(_Sso.PENDING_KEY);
|
|
35057
|
+
if (!pending || !values.code || !_Sso.equal(values.state, pending.state)) throw new SsoError("OIDC callback state is invalid or already consumed");
|
|
35058
|
+
if (Math.floor(Date.now() / 1e3) - Number(pending.created_at ?? 0) > 600) throw new SsoError("OIDC callback state has expired");
|
|
35059
|
+
const metadata = await this.discover();
|
|
35060
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
35061
|
+
grant_type: "authorization_code",
|
|
35062
|
+
code: values.code,
|
|
35063
|
+
redirect_uri: this.redirectUri,
|
|
35064
|
+
client_id: this.clientId,
|
|
35065
|
+
code_verifier: pending.verifier
|
|
35066
|
+
}, void 0, Boolean(this.clientSecret));
|
|
35067
|
+
if (!tokens.access_token || !tokens.id_token) throw new SsoError("OIDC token response is incomplete");
|
|
35068
|
+
if (this.verify === "jwks") throw new SsoError("JWKS verification requires an installed cryptography capability");
|
|
35069
|
+
const claims = await this.introspect(tokens.access_token);
|
|
35070
|
+
if (!_Sso.equal(_Sso.jwtPayload(tokens.id_token).nonce, pending.nonce)) throw new SsoError("OIDC ID token nonce mismatch");
|
|
35071
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
35072
|
+
const identity = this.normalize(claims);
|
|
35073
|
+
session.regenerate();
|
|
35074
|
+
session.set(_Sso.SESSION_KEY, {
|
|
35075
|
+
version: 1,
|
|
35076
|
+
identity,
|
|
35077
|
+
access_token: tokens.access_token,
|
|
35078
|
+
refresh_token: tokens.refresh_token,
|
|
35079
|
+
id_token: tokens.id_token,
|
|
35080
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
35081
|
+
});
|
|
35082
|
+
return { identity, return_to: _Sso.safeReturn(pending.return_to) };
|
|
35083
|
+
}
|
|
35084
|
+
identity(requestOrSession) {
|
|
35085
|
+
const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
|
|
35086
|
+
const identity = stored?.identity ?? null;
|
|
35087
|
+
if (identity && requestOrSession?.session) requestOrSession.user = identity;
|
|
35088
|
+
return identity;
|
|
35089
|
+
}
|
|
35090
|
+
async refresh(requestOrSession) {
|
|
35091
|
+
const session = this.session(requestOrSession);
|
|
35092
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
35093
|
+
if (!stored?.refresh_token) {
|
|
35094
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
35095
|
+
throw new SsoError("OIDC session cannot be refreshed");
|
|
35096
|
+
}
|
|
35097
|
+
try {
|
|
35098
|
+
const metadata = await this.discover();
|
|
35099
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
35100
|
+
grant_type: "refresh_token",
|
|
35101
|
+
refresh_token: stored.refresh_token,
|
|
35102
|
+
client_id: this.clientId
|
|
35103
|
+
}, void 0, Boolean(this.clientSecret));
|
|
35104
|
+
const claims = await this.introspect(tokens.access_token);
|
|
35105
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
35106
|
+
const identity = this.normalize(claims);
|
|
35107
|
+
session.set(_Sso.SESSION_KEY, {
|
|
35108
|
+
...stored,
|
|
35109
|
+
identity,
|
|
35110
|
+
access_token: tokens.access_token,
|
|
35111
|
+
refresh_token: tokens.refresh_token ?? stored.refresh_token,
|
|
35112
|
+
id_token: tokens.id_token ?? stored.id_token,
|
|
35113
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
35114
|
+
});
|
|
35115
|
+
return identity;
|
|
35116
|
+
} catch (error) {
|
|
35117
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
35118
|
+
throw error;
|
|
35119
|
+
}
|
|
35120
|
+
}
|
|
35121
|
+
async logout(requestOrSession, returnTo = "/") {
|
|
35122
|
+
const session = this.session(requestOrSession);
|
|
35123
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
35124
|
+
session?.destroy();
|
|
35125
|
+
const endpoint = (await this.discover()).end_session_endpoint;
|
|
35126
|
+
const target = this.postLogoutRedirectUri ?? _Sso.safeReturn(returnTo);
|
|
35127
|
+
if (!endpoint) return target;
|
|
35128
|
+
const params = new URLSearchParams({ post_logout_redirect_uri: target, client_id: this.clientId });
|
|
35129
|
+
if (stored?.id_token) params.set("id_token_hint", stored.id_token);
|
|
35130
|
+
return `${endpoint}?${params}`;
|
|
35131
|
+
}
|
|
35132
|
+
static async mountConfigured(router) {
|
|
35133
|
+
if (_Sso.mountedRouters.has(router) || !_Sso.configured()) return false;
|
|
35134
|
+
const owned = /* @__PURE__ */ new Set(["GET /auth/login", "GET /auth/callback", "POST /auth/logout"]);
|
|
35135
|
+
const collisions = router.getRoutes().map((route) => `${route.method} ${route.pattern}`).filter((route) => owned.has(route));
|
|
35136
|
+
if (collisions.length) throw new SsoError(`SSO route collision: ${collisions.join(", ")}`);
|
|
35137
|
+
const sso = await _Sso.fromIssuer();
|
|
35138
|
+
router.get("/auth/login", async (req2, res) => res.redirect(await sso.login(req2, req2.query?.return_to ?? "/")));
|
|
35139
|
+
router.get("/auth/callback", async (req2, res) => {
|
|
35140
|
+
try {
|
|
35141
|
+
return res.redirect((await sso.callback(req2)).return_to);
|
|
35142
|
+
} catch (error) {
|
|
35143
|
+
const message = error instanceof SsoError ? error.message : "OIDC callback failed";
|
|
35144
|
+
return res.error("SSO_CALLBACK_FAILED", message, 400);
|
|
35145
|
+
}
|
|
35146
|
+
});
|
|
35147
|
+
router.post("/auth/logout", async (req2, res) => res.redirect(await sso.logout(req2, req2.query?.return_to ?? "/")));
|
|
35148
|
+
_Sso.mountedRouters.add(router);
|
|
35149
|
+
return true;
|
|
35150
|
+
}
|
|
35151
|
+
};
|
|
35152
|
+
}
|
|
35153
|
+
});
|
|
35154
|
+
|
|
34585
35155
|
// ../core/src/docsAutoDiscovery.ts
|
|
34586
35156
|
var docsAutoDiscovery_exports = {};
|
|
34587
35157
|
__export(docsAutoDiscovery_exports, {
|
|
@@ -34651,7 +35221,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
34651
35221
|
|
|
34652
35222
|
// ../core/src/server.ts
|
|
34653
35223
|
import { createServer as createServer2 } from "node:http";
|
|
34654
|
-
import { randomBytes as
|
|
35224
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34655
35225
|
import { resolve as resolve19, dirname as dirname14, join as join30, relative as relative8 } from "node:path";
|
|
34656
35226
|
import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
34657
35227
|
import { isatty } from "node:tty";
|
|
@@ -35291,7 +35861,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
35291
35861
|
}
|
|
35292
35862
|
}
|
|
35293
35863
|
}
|
|
35294
|
-
const requestId = Log.getRequestId() ??
|
|
35864
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35295
35865
|
if (wantsJson(req2)) {
|
|
35296
35866
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
35297
35867
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -35362,7 +35932,7 @@ function serveStaticAsset(ctx) {
|
|
|
35362
35932
|
return false;
|
|
35363
35933
|
}
|
|
35364
35934
|
async function serveNotFound(ctx) {
|
|
35365
|
-
const requestId = Log.getRequestId() ??
|
|
35935
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35366
35936
|
if (wantsJson(ctx.req)) {
|
|
35367
35937
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
35368
35938
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -35482,7 +36052,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
35482
36052
|
}
|
|
35483
36053
|
}
|
|
35484
36054
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
35485
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
36055
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
|
|
35486
36056
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
35487
36057
|
return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
|
|
35488
36058
|
}
|
|
@@ -35616,6 +36186,8 @@ ${reset2}
|
|
|
35616
36186
|
console.log(`
|
|
35617
36187
|
No routes directory found at ${routesDir}`);
|
|
35618
36188
|
}
|
|
36189
|
+
const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
|
|
36190
|
+
await Sso2.mountConfigured(router);
|
|
35619
36191
|
if (attachCsrfFromEnv()) {
|
|
35620
36192
|
console.log(`
|
|
35621
36193
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
@@ -36140,7 +36712,7 @@ var init_mqttMessage = __esm({
|
|
|
36140
36712
|
// ../core/src/mqtt.ts
|
|
36141
36713
|
import net2 from "node:net";
|
|
36142
36714
|
import tls from "node:tls";
|
|
36143
|
-
import { randomBytes as
|
|
36715
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
36144
36716
|
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
36145
36717
|
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;
|
|
36146
36718
|
var init_mqtt = __esm({
|
|
@@ -36228,7 +36800,7 @@ var init_mqtt = __esm({
|
|
|
36228
36800
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
36229
36801
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
36230
36802
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
36231
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
36803
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
|
|
36232
36804
|
this.clientId = cid;
|
|
36233
36805
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
36234
36806
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -37158,7 +37730,7 @@ var init_service = __esm({
|
|
|
37158
37730
|
import http from "node:http";
|
|
37159
37731
|
import https from "node:https";
|
|
37160
37732
|
import { URL as URL2 } from "node:url";
|
|
37161
|
-
import { randomBytes as
|
|
37733
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
37162
37734
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
37163
37735
|
import { basename as basename6 } from "node:path";
|
|
37164
37736
|
import { pipeline } from "node:stream/promises";
|
|
@@ -37456,7 +38028,7 @@ var init_api = __esm({
|
|
|
37456
38028
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
37457
38029
|
}
|
|
37458
38030
|
const partContentType = guessContentType(uploadName);
|
|
37459
|
-
const boundary = "----Tina4Boundary" +
|
|
38031
|
+
const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
|
|
37460
38032
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
37461
38033
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
37462
38034
|
return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
|
|
@@ -42306,10 +42878,13 @@ __export(src_exports3, {
|
|
|
42306
42878
|
RouteGroup: () => RouteGroup,
|
|
42307
42879
|
RouteRef: () => RouteRef,
|
|
42308
42880
|
Router: () => Router,
|
|
42881
|
+
SSO: () => Sso,
|
|
42309
42882
|
SafeString: () => SafeString2,
|
|
42310
42883
|
SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
|
|
42311
42884
|
ServiceRunner: () => ServiceRunner,
|
|
42312
42885
|
Session: () => Session,
|
|
42886
|
+
Sso: () => Sso,
|
|
42887
|
+
SsoError: () => SsoError,
|
|
42313
42888
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
42314
42889
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
42315
42890
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -42546,6 +43121,7 @@ var init_src3 = __esm({
|
|
|
42546
43121
|
init_htmlElement();
|
|
42547
43122
|
init_errorOverlay();
|
|
42548
43123
|
init_ai();
|
|
43124
|
+
init_sso();
|
|
42549
43125
|
init_aiClient();
|
|
42550
43126
|
init_liteBackend();
|
|
42551
43127
|
init_rabbitmqBackend();
|