tina4-nodejs 3.13.103 → 3.13.104
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +590 -19
- package/packages/core/dist/index.js +593 -19
- package/packages/core/src/authGate.ts +6 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/server.ts +5 -0
- package/packages/core/src/sso.ts +285 -0
- package/packages/orm/dist/index.js +598 -24
- package/packages/orm/src/adapters/postgres.ts +2 -0
- package/packages/orm/src/baseModel.ts +45 -5
- package/packages/orm/src/index.ts +2 -0
- package/packages/orm/src/point.ts +105 -0
- package/packages/orm/src/queryBuilder.ts +66 -5
- package/packages/orm/src/sqlTranslator.ts +63 -0
- package/packages/orm/src/types.ts +5 -1
- package/packages/swagger/dist/index.js +11 -1
- package/packages/swagger/src/generator.ts +11 -1
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/sso.d.ts +55 -0
- package/types/orm/src/baseModel.d.ts +3 -0
- package/types/orm/src/index.d.ts +2 -0
- package/types/orm/src/point.d.ts +24 -0
- package/types/orm/src/queryBuilder.d.ts +10 -1
- package/types/orm/src/sqlTranslator.d.ts +13 -0
- package/types/orm/src/types.d.ts +5 -1
|
@@ -6014,13 +6014,172 @@ var init_databaseUrl = __esm({
|
|
|
6014
6014
|
}
|
|
6015
6015
|
});
|
|
6016
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
|
+
|
|
6017
6126
|
// ../orm/src/sqlTranslator.ts
|
|
6018
6127
|
var SQLTranslator, QueryCache;
|
|
6019
6128
|
var init_sqlTranslator = __esm({
|
|
6020
6129
|
"../orm/src/sqlTranslator.ts"() {
|
|
6021
6130
|
"use strict";
|
|
6022
6131
|
init_databaseUrl();
|
|
6132
|
+
init_point();
|
|
6023
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
|
+
}
|
|
6024
6183
|
/**
|
|
6025
6184
|
* Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
|
|
6026
6185
|
*
|
|
@@ -7674,6 +7833,8 @@ function fieldTypeToPostgres(def) {
|
|
|
7674
7833
|
return "TEXT";
|
|
7675
7834
|
case "json":
|
|
7676
7835
|
return "JSONB";
|
|
7836
|
+
case "point":
|
|
7837
|
+
return SQLTranslator.pointColumnType("postgres", def.srid ?? 4326);
|
|
7677
7838
|
case "string":
|
|
7678
7839
|
return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
|
|
7679
7840
|
default:
|
|
@@ -13297,10 +13458,13 @@ var init_queryBuilder = __esm({
|
|
|
13297
13458
|
"use strict";
|
|
13298
13459
|
init_database();
|
|
13299
13460
|
init_databaseResult();
|
|
13461
|
+
init_point();
|
|
13462
|
+
init_sqlTranslator();
|
|
13300
13463
|
QueryBuilder = class _QueryBuilder {
|
|
13301
13464
|
table;
|
|
13302
13465
|
db;
|
|
13303
13466
|
columns = ["*"];
|
|
13467
|
+
selectParams = [];
|
|
13304
13468
|
wheres = [];
|
|
13305
13469
|
params = [];
|
|
13306
13470
|
joinClauses = [];
|
|
@@ -13308,14 +13472,17 @@ var init_queryBuilder = __esm({
|
|
|
13308
13472
|
havings = [];
|
|
13309
13473
|
havingParams = [];
|
|
13310
13474
|
orderByCols = [];
|
|
13475
|
+
orderByParams = [];
|
|
13476
|
+
primaryKey;
|
|
13311
13477
|
limitVal;
|
|
13312
13478
|
offsetVal;
|
|
13313
13479
|
/**
|
|
13314
13480
|
* Private constructor — use static factory methods.
|
|
13315
13481
|
*/
|
|
13316
|
-
constructor(table2, db) {
|
|
13482
|
+
constructor(table2, db, primaryKey) {
|
|
13317
13483
|
this.table = table2;
|
|
13318
13484
|
this.db = db;
|
|
13485
|
+
this.primaryKey = primaryKey;
|
|
13319
13486
|
}
|
|
13320
13487
|
/**
|
|
13321
13488
|
* Create a QueryBuilder for a table.
|
|
@@ -13324,8 +13491,8 @@ var init_queryBuilder = __esm({
|
|
|
13324
13491
|
* @param db - Optional database adapter.
|
|
13325
13492
|
* @returns A new QueryBuilder instance.
|
|
13326
13493
|
*/
|
|
13327
|
-
static fromTable(tableName, db) {
|
|
13328
|
-
return new _QueryBuilder(tableName, db);
|
|
13494
|
+
static fromTable(tableName, db, primaryKey) {
|
|
13495
|
+
return new _QueryBuilder(tableName, db, primaryKey);
|
|
13329
13496
|
}
|
|
13330
13497
|
/**
|
|
13331
13498
|
* Set the columns to select.
|
|
@@ -13336,6 +13503,7 @@ var init_queryBuilder = __esm({
|
|
|
13336
13503
|
select(...cols) {
|
|
13337
13504
|
if (cols.length > 0) {
|
|
13338
13505
|
this.columns = cols;
|
|
13506
|
+
this.selectParams = [];
|
|
13339
13507
|
}
|
|
13340
13508
|
return this;
|
|
13341
13509
|
}
|
|
@@ -13417,6 +13585,41 @@ var init_queryBuilder = __esm({
|
|
|
13417
13585
|
this.orderByCols.push(expression);
|
|
13418
13586
|
return this;
|
|
13419
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
|
+
}
|
|
13420
13623
|
/**
|
|
13421
13624
|
* Set LIMIT and optional OFFSET.
|
|
13422
13625
|
*
|
|
@@ -13482,7 +13685,7 @@ var init_queryBuilder = __esm({
|
|
|
13482
13685
|
async get() {
|
|
13483
13686
|
this.ensureDb();
|
|
13484
13687
|
const sql = this.toSql();
|
|
13485
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13688
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13486
13689
|
const queryParams = allParams.length > 0 ? allParams : void 0;
|
|
13487
13690
|
const rows = await adapterFetch(
|
|
13488
13691
|
this.db,
|
|
@@ -13510,7 +13713,7 @@ var init_queryBuilder = __esm({
|
|
|
13510
13713
|
async first() {
|
|
13511
13714
|
this.ensureDb();
|
|
13512
13715
|
const sql = this.toSql();
|
|
13513
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13716
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13514
13717
|
return adapterFetchOne(
|
|
13515
13718
|
this.db,
|
|
13516
13719
|
sql,
|
|
@@ -13525,9 +13728,18 @@ var init_queryBuilder = __esm({
|
|
|
13525
13728
|
async count() {
|
|
13526
13729
|
this.ensureDb();
|
|
13527
13730
|
const original = this.columns;
|
|
13731
|
+
const originalSelectParams = this.selectParams;
|
|
13732
|
+
const originalOrder = this.orderByCols;
|
|
13733
|
+
const originalOrderParams = this.orderByParams;
|
|
13528
13734
|
this.columns = ["COUNT(*) as cnt"];
|
|
13735
|
+
this.selectParams = [];
|
|
13736
|
+
this.orderByCols = [];
|
|
13737
|
+
this.orderByParams = [];
|
|
13529
13738
|
const sql = this.toSql();
|
|
13530
13739
|
this.columns = original;
|
|
13740
|
+
this.selectParams = originalSelectParams;
|
|
13741
|
+
this.orderByCols = originalOrder;
|
|
13742
|
+
this.orderByParams = originalOrderParams;
|
|
13531
13743
|
const allParams = [...this.params, ...this.havingParams];
|
|
13532
13744
|
const row = await adapterFetchOne(
|
|
13533
13745
|
this.db,
|
|
@@ -13712,6 +13924,10 @@ var init_queryBuilder = __esm({
|
|
|
13712
13924
|
}
|
|
13713
13925
|
}
|
|
13714
13926
|
}
|
|
13927
|
+
engine() {
|
|
13928
|
+
this.ensureDb();
|
|
13929
|
+
return this.db.getDatabaseType();
|
|
13930
|
+
}
|
|
13715
13931
|
};
|
|
13716
13932
|
}
|
|
13717
13933
|
});
|
|
@@ -13735,6 +13951,11 @@ function toDbFieldValue(def, value) {
|
|
|
13735
13951
|
if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
|
|
13736
13952
|
return JSON.stringify(value);
|
|
13737
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
|
+
}
|
|
13738
13959
|
return value;
|
|
13739
13960
|
}
|
|
13740
13961
|
function fromDbFieldValue(def, value) {
|
|
@@ -13745,6 +13966,11 @@ function fromDbFieldValue(def, value) {
|
|
|
13745
13966
|
return value;
|
|
13746
13967
|
}
|
|
13747
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
|
+
}
|
|
13748
13974
|
return value;
|
|
13749
13975
|
}
|
|
13750
13976
|
function _pluralRelKeys() {
|
|
@@ -13780,6 +14006,7 @@ var init_baseModel = __esm({
|
|
|
13780
14006
|
init_sqlite();
|
|
13781
14007
|
init_sqlTranslator();
|
|
13782
14008
|
init_index();
|
|
14009
|
+
init_point();
|
|
13783
14010
|
_fkRegistry = /* @__PURE__ */ new Map();
|
|
13784
14011
|
EAGER_IN_CHUNK = 500;
|
|
13785
14012
|
modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
|
|
@@ -13835,7 +14062,9 @@ var init_baseModel = __esm({
|
|
|
13835
14062
|
for (const [name, def] of Object.entries(fields0)) {
|
|
13836
14063
|
if (def.default === void 0) continue;
|
|
13837
14064
|
let dv = typeof def.default === "function" ? def.default() : def.default;
|
|
13838
|
-
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);
|
|
13839
14068
|
this[name] = dv;
|
|
13840
14069
|
}
|
|
13841
14070
|
if (data) {
|
|
@@ -13937,7 +14166,7 @@ var init_baseModel = __esm({
|
|
|
13937
14166
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
13938
14167
|
*/
|
|
13939
14168
|
static query() {
|
|
13940
|
-
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
14169
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
|
|
13941
14170
|
}
|
|
13942
14171
|
/**
|
|
13943
14172
|
* Get the database adapter for this model.
|
|
@@ -14390,7 +14619,7 @@ var init_baseModel = __esm({
|
|
|
14390
14619
|
for (const key of Object.keys(ModelClass.fields)) {
|
|
14391
14620
|
if (this[key] !== void 0) {
|
|
14392
14621
|
const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
|
|
14393
|
-
result[outKey] = this[key];
|
|
14622
|
+
result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
|
|
14394
14623
|
}
|
|
14395
14624
|
}
|
|
14396
14625
|
if (ModelClass.softDelete && this.is_deleted !== void 0) {
|
|
@@ -14450,6 +14679,19 @@ var init_baseModel = __esm({
|
|
|
14450
14679
|
}
|
|
14451
14680
|
return result;
|
|
14452
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
|
+
}
|
|
14453
14695
|
/**
|
|
14454
14696
|
* Convert to an associative object (alias for toDict).
|
|
14455
14697
|
*/
|
|
@@ -14500,7 +14742,10 @@ var init_baseModel = __esm({
|
|
|
14500
14742
|
*/
|
|
14501
14743
|
static async createTable() {
|
|
14502
14744
|
const db = this.getDb();
|
|
14503
|
-
|
|
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);
|
|
14504
14749
|
if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
|
|
14505
14750
|
const mappedFields = {};
|
|
14506
14751
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
@@ -14516,7 +14761,7 @@ var init_baseModel = __esm({
|
|
|
14516
14761
|
mappedFields["is_deleted"] = { type: "integer", default: 0 };
|
|
14517
14762
|
}
|
|
14518
14763
|
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
14519
|
-
return
|
|
14764
|
+
return this.createSpatialIndexes(db, pointFields);
|
|
14520
14765
|
}
|
|
14521
14766
|
const typeMap = {
|
|
14522
14767
|
integer: "INTEGER",
|
|
@@ -14563,6 +14808,14 @@ var init_baseModel = __esm({
|
|
|
14563
14808
|
}
|
|
14564
14809
|
return true;
|
|
14565
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
|
+
}
|
|
14566
14819
|
/**
|
|
14567
14820
|
* Find a record by primary key or throw an error if not found.
|
|
14568
14821
|
*/
|
|
@@ -17210,6 +17463,7 @@ __export(src_exports, {
|
|
|
17210
17463
|
CachedDatabaseAdapter: () => CachedDatabaseAdapter,
|
|
17211
17464
|
Cursor: () => Cursor,
|
|
17212
17465
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
17466
|
+
DEFAULT_SRID: () => DEFAULT_SRID,
|
|
17213
17467
|
Database: () => Database,
|
|
17214
17468
|
DatabaseResult: () => DatabaseResult,
|
|
17215
17469
|
DatabaseUrl: () => DatabaseUrl,
|
|
@@ -17225,6 +17479,7 @@ __export(src_exports, {
|
|
|
17225
17479
|
NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
|
|
17226
17480
|
ObjectId: () => ObjectId,
|
|
17227
17481
|
OdbcAdapter: () => OdbcAdapter,
|
|
17482
|
+
Point: () => Point,
|
|
17228
17483
|
PostgresAdapter: () => PostgresAdapter,
|
|
17229
17484
|
QueryBuilder: () => QueryBuilder,
|
|
17230
17485
|
QueryCache: () => QueryCache,
|
|
@@ -17237,6 +17492,7 @@ __export(src_exports, {
|
|
|
17237
17492
|
S3Storage: () => S3Storage,
|
|
17238
17493
|
SQLTranslator: () => SQLTranslator,
|
|
17239
17494
|
SQLiteAdapter: () => SQLiteAdapter,
|
|
17495
|
+
SpatialNotSupportedError: () => SpatialNotSupportedError,
|
|
17240
17496
|
SqliteCollection: () => SqliteCollection,
|
|
17241
17497
|
SqliteDatabase: () => SqliteDatabase,
|
|
17242
17498
|
adapterColumns: () => adapterColumns,
|
|
@@ -17331,6 +17587,7 @@ var init_src = __esm({
|
|
|
17331
17587
|
init_baseModel();
|
|
17332
17588
|
init_queryBuilder();
|
|
17333
17589
|
init_sqlTranslator();
|
|
17590
|
+
init_point();
|
|
17334
17591
|
init_connectTimeout();
|
|
17335
17592
|
init_cachedDatabase();
|
|
17336
17593
|
init_fakeData2();
|
|
@@ -19520,6 +19777,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19520
19777
|
}
|
|
19521
19778
|
}
|
|
19522
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
|
+
}
|
|
19523
19786
|
const sessionToken = req2.session?.get?.("token");
|
|
19524
19787
|
if (sessionToken && validToken(sessionToken)) {
|
|
19525
19788
|
resolvedToken = sessionToken;
|
|
@@ -34071,6 +34334,14 @@ function resolveSecuritySchemes() {
|
|
|
34071
34334
|
const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
|
|
34072
34335
|
schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
|
|
34073
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
|
+
}
|
|
34074
34345
|
for (const [name, def] of Object.entries(registeredSchemes)) {
|
|
34075
34346
|
schemes[name] = def;
|
|
34076
34347
|
}
|
|
@@ -34252,7 +34523,9 @@ function generate(routes, models = []) {
|
|
|
34252
34523
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34253
34524
|
}
|
|
34254
34525
|
} else if (routeRequiresAuth(route, method)) {
|
|
34255
|
-
|
|
34526
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
34527
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
34528
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
34256
34529
|
const responses = operation.responses;
|
|
34257
34530
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34258
34531
|
}
|
|
@@ -34566,6 +34839,298 @@ var init_src2 = __esm({
|
|
|
34566
34839
|
}
|
|
34567
34840
|
});
|
|
34568
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
|
+
|
|
34569
35134
|
// src/docsAutoDiscovery.ts
|
|
34570
35135
|
var docsAutoDiscovery_exports = {};
|
|
34571
35136
|
__export(docsAutoDiscovery_exports, {
|
|
@@ -34635,7 +35200,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
34635
35200
|
|
|
34636
35201
|
// src/server.ts
|
|
34637
35202
|
import { createServer as createServer2 } from "node:http";
|
|
34638
|
-
import { randomBytes as
|
|
35203
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34639
35204
|
import { resolve as resolve18, dirname as dirname13, join as join29, relative as relative8 } from "node:path";
|
|
34640
35205
|
import { existsSync as existsSync24, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
34641
35206
|
import { isatty } from "node:tty";
|
|
@@ -35275,7 +35840,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
35275
35840
|
}
|
|
35276
35841
|
}
|
|
35277
35842
|
}
|
|
35278
|
-
const requestId = Log.getRequestId() ??
|
|
35843
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35279
35844
|
if (wantsJson(req2)) {
|
|
35280
35845
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
35281
35846
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -35346,7 +35911,7 @@ function serveStaticAsset(ctx) {
|
|
|
35346
35911
|
return false;
|
|
35347
35912
|
}
|
|
35348
35913
|
async function serveNotFound(ctx) {
|
|
35349
|
-
const requestId = Log.getRequestId() ??
|
|
35914
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35350
35915
|
if (wantsJson(ctx.req)) {
|
|
35351
35916
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
35352
35917
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -35466,7 +36031,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
35466
36031
|
}
|
|
35467
36032
|
}
|
|
35468
36033
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
35469
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
36034
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
|
|
35470
36035
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
35471
36036
|
return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
|
|
35472
36037
|
}
|
|
@@ -35600,6 +36165,8 @@ ${reset2}
|
|
|
35600
36165
|
console.log(`
|
|
35601
36166
|
No routes directory found at ${routesDir}`);
|
|
35602
36167
|
}
|
|
36168
|
+
const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
|
|
36169
|
+
await Sso2.mountConfigured(router);
|
|
35603
36170
|
if (attachCsrfFromEnv()) {
|
|
35604
36171
|
console.log(`
|
|
35605
36172
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
@@ -36124,7 +36691,7 @@ var init_mqttMessage = __esm({
|
|
|
36124
36691
|
// src/mqtt.ts
|
|
36125
36692
|
import net2 from "node:net";
|
|
36126
36693
|
import tls from "node:tls";
|
|
36127
|
-
import { randomBytes as
|
|
36694
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
36128
36695
|
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "node:fs";
|
|
36129
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;
|
|
36130
36697
|
var init_mqtt = __esm({
|
|
@@ -36212,7 +36779,7 @@ var init_mqtt = __esm({
|
|
|
36212
36779
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
36213
36780
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
36214
36781
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
36215
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
36782
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
|
|
36216
36783
|
this.clientId = cid;
|
|
36217
36784
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
36218
36785
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -37142,7 +37709,7 @@ var init_service = __esm({
|
|
|
37142
37709
|
import http from "node:http";
|
|
37143
37710
|
import https from "node:https";
|
|
37144
37711
|
import { URL as URL2 } from "node:url";
|
|
37145
|
-
import { randomBytes as
|
|
37712
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
37146
37713
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
37147
37714
|
import { basename as basename5 } from "node:path";
|
|
37148
37715
|
import { pipeline } from "node:stream/promises";
|
|
@@ -37440,7 +38007,7 @@ var init_api = __esm({
|
|
|
37440
38007
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
37441
38008
|
}
|
|
37442
38009
|
const partContentType = guessContentType(uploadName);
|
|
37443
|
-
const boundary = "----Tina4Boundary" +
|
|
38010
|
+
const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
|
|
37444
38011
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
37445
38012
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
37446
38013
|
return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
|
|
@@ -42272,10 +42839,13 @@ __export(index_exports, {
|
|
|
42272
42839
|
RouteGroup: () => RouteGroup,
|
|
42273
42840
|
RouteRef: () => RouteRef,
|
|
42274
42841
|
Router: () => Router,
|
|
42842
|
+
SSO: () => Sso,
|
|
42275
42843
|
SafeString: () => SafeString2,
|
|
42276
42844
|
SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
|
|
42277
42845
|
ServiceRunner: () => ServiceRunner,
|
|
42278
42846
|
Session: () => Session,
|
|
42847
|
+
Sso: () => Sso,
|
|
42848
|
+
SsoError: () => SsoError,
|
|
42279
42849
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
42280
42850
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
42281
42851
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -42511,6 +43081,7 @@ var init_index = __esm({
|
|
|
42511
43081
|
init_htmlElement();
|
|
42512
43082
|
init_errorOverlay();
|
|
42513
43083
|
init_ai();
|
|
43084
|
+
init_sso();
|
|
42514
43085
|
init_aiClient();
|
|
42515
43086
|
init_liteBackend();
|
|
42516
43087
|
init_rabbitmqBackend();
|
|
@@ -42634,10 +43205,13 @@ export {
|
|
|
42634
43205
|
RouteGroup,
|
|
42635
43206
|
RouteRef,
|
|
42636
43207
|
Router,
|
|
43208
|
+
Sso as SSO,
|
|
42637
43209
|
SafeString2 as SafeString,
|
|
42638
43210
|
SecurityHeadersMiddleware,
|
|
42639
43211
|
ServiceRunner,
|
|
42640
43212
|
Session,
|
|
43213
|
+
Sso,
|
|
43214
|
+
SsoError,
|
|
42641
43215
|
TAKEOVER_KILLED,
|
|
42642
43216
|
TAKEOVER_NOTHING,
|
|
42643
43217
|
TAKEOVER_REFUSALS,
|