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.
@@ -6015,13 +6015,172 @@ var init_databaseUrl = __esm({
6015
6015
  }
6016
6016
  });
6017
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
+
6018
6127
  // ../orm/src/sqlTranslator.ts
6019
6128
  var SQLTranslator, QueryCache;
6020
6129
  var init_sqlTranslator = __esm({
6021
6130
  "../orm/src/sqlTranslator.ts"() {
6022
6131
  "use strict";
6023
6132
  init_databaseUrl();
6133
+ init_point();
6024
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
+ }
6025
6184
  /**
6026
6185
  * Convert LIMIT/OFFSET to Firebird ROWS...TO syntax.
6027
6186
  *
@@ -7675,6 +7834,8 @@ function fieldTypeToPostgres(def) {
7675
7834
  return "TEXT";
7676
7835
  case "json":
7677
7836
  return "JSONB";
7837
+ case "point":
7838
+ return SQLTranslator.pointColumnType("postgres", def.srid ?? 4326);
7678
7839
  case "string":
7679
7840
  return def.maxLength ? `VARCHAR(${def.maxLength})` : "VARCHAR(255)";
7680
7841
  default:
@@ -13298,10 +13459,13 @@ var init_queryBuilder = __esm({
13298
13459
  "use strict";
13299
13460
  init_database();
13300
13461
  init_databaseResult();
13462
+ init_point();
13463
+ init_sqlTranslator();
13301
13464
  QueryBuilder = class _QueryBuilder {
13302
13465
  table;
13303
13466
  db;
13304
13467
  columns = ["*"];
13468
+ selectParams = [];
13305
13469
  wheres = [];
13306
13470
  params = [];
13307
13471
  joinClauses = [];
@@ -13309,14 +13473,17 @@ var init_queryBuilder = __esm({
13309
13473
  havings = [];
13310
13474
  havingParams = [];
13311
13475
  orderByCols = [];
13476
+ orderByParams = [];
13477
+ primaryKey;
13312
13478
  limitVal;
13313
13479
  offsetVal;
13314
13480
  /**
13315
13481
  * Private constructor — use static factory methods.
13316
13482
  */
13317
- constructor(table2, db) {
13483
+ constructor(table2, db, primaryKey) {
13318
13484
  this.table = table2;
13319
13485
  this.db = db;
13486
+ this.primaryKey = primaryKey;
13320
13487
  }
13321
13488
  /**
13322
13489
  * Create a QueryBuilder for a table.
@@ -13325,8 +13492,8 @@ var init_queryBuilder = __esm({
13325
13492
  * @param db - Optional database adapter.
13326
13493
  * @returns A new QueryBuilder instance.
13327
13494
  */
13328
- static fromTable(tableName, db) {
13329
- return new _QueryBuilder(tableName, db);
13495
+ static fromTable(tableName, db, primaryKey) {
13496
+ return new _QueryBuilder(tableName, db, primaryKey);
13330
13497
  }
13331
13498
  /**
13332
13499
  * Set the columns to select.
@@ -13337,6 +13504,7 @@ var init_queryBuilder = __esm({
13337
13504
  select(...cols) {
13338
13505
  if (cols.length > 0) {
13339
13506
  this.columns = cols;
13507
+ this.selectParams = [];
13340
13508
  }
13341
13509
  return this;
13342
13510
  }
@@ -13418,6 +13586,41 @@ var init_queryBuilder = __esm({
13418
13586
  this.orderByCols.push(expression);
13419
13587
  return this;
13420
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
+ }
13421
13624
  /**
13422
13625
  * Set LIMIT and optional OFFSET.
13423
13626
  *
@@ -13483,7 +13686,7 @@ var init_queryBuilder = __esm({
13483
13686
  async get() {
13484
13687
  this.ensureDb();
13485
13688
  const sql = this.toSql();
13486
- const allParams = [...this.params, ...this.havingParams];
13689
+ const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
13487
13690
  const queryParams = allParams.length > 0 ? allParams : void 0;
13488
13691
  const rows = await adapterFetch(
13489
13692
  this.db,
@@ -13511,7 +13714,7 @@ var init_queryBuilder = __esm({
13511
13714
  async first() {
13512
13715
  this.ensureDb();
13513
13716
  const sql = this.toSql();
13514
- const allParams = [...this.params, ...this.havingParams];
13717
+ const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
13515
13718
  return adapterFetchOne(
13516
13719
  this.db,
13517
13720
  sql,
@@ -13526,9 +13729,18 @@ var init_queryBuilder = __esm({
13526
13729
  async count() {
13527
13730
  this.ensureDb();
13528
13731
  const original = this.columns;
13732
+ const originalSelectParams = this.selectParams;
13733
+ const originalOrder = this.orderByCols;
13734
+ const originalOrderParams = this.orderByParams;
13529
13735
  this.columns = ["COUNT(*) as cnt"];
13736
+ this.selectParams = [];
13737
+ this.orderByCols = [];
13738
+ this.orderByParams = [];
13530
13739
  const sql = this.toSql();
13531
13740
  this.columns = original;
13741
+ this.selectParams = originalSelectParams;
13742
+ this.orderByCols = originalOrder;
13743
+ this.orderByParams = originalOrderParams;
13532
13744
  const allParams = [...this.params, ...this.havingParams];
13533
13745
  const row = await adapterFetchOne(
13534
13746
  this.db,
@@ -13713,6 +13925,10 @@ var init_queryBuilder = __esm({
13713
13925
  }
13714
13926
  }
13715
13927
  }
13928
+ engine() {
13929
+ this.ensureDb();
13930
+ return this.db.getDatabaseType();
13931
+ }
13716
13932
  };
13717
13933
  }
13718
13934
  });
@@ -13736,6 +13952,11 @@ function toDbFieldValue(def, value) {
13736
13952
  if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
13737
13953
  return JSON.stringify(value);
13738
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
+ }
13739
13960
  return value;
13740
13961
  }
13741
13962
  function fromDbFieldValue(def, value) {
@@ -13746,6 +13967,11 @@ function fromDbFieldValue(def, value) {
13746
13967
  return value;
13747
13968
  }
13748
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
+ }
13749
13975
  return value;
13750
13976
  }
13751
13977
  function _pluralRelKeys() {
@@ -13781,6 +14007,7 @@ var init_baseModel = __esm({
13781
14007
  init_sqlite();
13782
14008
  init_sqlTranslator();
13783
14009
  init_src3();
14010
+ init_point();
13784
14011
  _fkRegistry = /* @__PURE__ */ new Map();
13785
14012
  EAGER_IN_CHUNK = 500;
13786
14013
  modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
@@ -13836,7 +14063,9 @@ var init_baseModel = __esm({
13836
14063
  for (const [name, def] of Object.entries(fields0)) {
13837
14064
  if (def.default === void 0) continue;
13838
14065
  let dv = typeof def.default === "function" ? def.default() : def.default;
13839
- if (dv !== null && typeof dv === "object") dv = structuredClone(dv);
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);
13840
14069
  this[name] = dv;
13841
14070
  }
13842
14071
  if (data) {
@@ -13938,7 +14167,7 @@ var init_baseModel = __esm({
13938
14167
  * @returns A QueryBuilder instance bound to this model's table and database.
13939
14168
  */
13940
14169
  static query() {
13941
- return QueryBuilder.fromTable(this.tableName, this.getDb());
14170
+ return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
13942
14171
  }
13943
14172
  /**
13944
14173
  * Get the database adapter for this model.
@@ -14391,7 +14620,7 @@ var init_baseModel = __esm({
14391
14620
  for (const key of Object.keys(ModelClass.fields)) {
14392
14621
  if (this[key] !== void 0) {
14393
14622
  const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
14394
- result[outKey] = this[key];
14623
+ result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
14395
14624
  }
14396
14625
  }
14397
14626
  if (ModelClass.softDelete && this.is_deleted !== void 0) {
@@ -14451,6 +14680,19 @@ var init_baseModel = __esm({
14451
14680
  }
14452
14681
  return result;
14453
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
+ }
14454
14696
  /**
14455
14697
  * Convert to an associative object (alias for toDict).
14456
14698
  */
@@ -14501,7 +14743,10 @@ var init_baseModel = __esm({
14501
14743
  */
14502
14744
  static async createTable() {
14503
14745
  const db = this.getDb();
14504
- if (await adapterTableExists(db, this.tableName)) return true;
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);
14505
14750
  if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
14506
14751
  const mappedFields = {};
14507
14752
  for (const [fieldName, def] of Object.entries(this.fields)) {
@@ -14517,7 +14762,7 @@ var init_baseModel = __esm({
14517
14762
  mappedFields["is_deleted"] = { type: "integer", default: 0 };
14518
14763
  }
14519
14764
  await adapterCreateTable(db, this.tableName, mappedFields);
14520
- return true;
14765
+ return this.createSpatialIndexes(db, pointFields);
14521
14766
  }
14522
14767
  const typeMap = {
14523
14768
  integer: "INTEGER",
@@ -14564,6 +14809,14 @@ var init_baseModel = __esm({
14564
14809
  }
14565
14810
  return true;
14566
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
+ }
14567
14820
  /**
14568
14821
  * Find a record by primary key or throw an error if not found.
14569
14822
  */
@@ -17211,6 +17464,7 @@ __export(src_exports, {
17211
17464
  CachedDatabaseAdapter: () => CachedDatabaseAdapter,
17212
17465
  Cursor: () => Cursor,
17213
17466
  DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
17467
+ DEFAULT_SRID: () => DEFAULT_SRID,
17214
17468
  Database: () => Database,
17215
17469
  DatabaseResult: () => DatabaseResult,
17216
17470
  DatabaseUrl: () => DatabaseUrl,
@@ -17226,6 +17480,7 @@ __export(src_exports, {
17226
17480
  NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
17227
17481
  ObjectId: () => ObjectId,
17228
17482
  OdbcAdapter: () => OdbcAdapter,
17483
+ Point: () => Point,
17229
17484
  PostgresAdapter: () => PostgresAdapter,
17230
17485
  QueryBuilder: () => QueryBuilder,
17231
17486
  QueryCache: () => QueryCache,
@@ -17238,6 +17493,7 @@ __export(src_exports, {
17238
17493
  S3Storage: () => S3Storage,
17239
17494
  SQLTranslator: () => SQLTranslator,
17240
17495
  SQLiteAdapter: () => SQLiteAdapter,
17496
+ SpatialNotSupportedError: () => SpatialNotSupportedError,
17241
17497
  SqliteCollection: () => SqliteCollection,
17242
17498
  SqliteDatabase: () => SqliteDatabase,
17243
17499
  adapterColumns: () => adapterColumns,
@@ -17332,6 +17588,7 @@ var init_src = __esm({
17332
17588
  init_baseModel();
17333
17589
  init_queryBuilder();
17334
17590
  init_sqlTranslator();
17591
+ init_point();
17335
17592
  init_connectTimeout();
17336
17593
  init_cachedDatabase();
17337
17594
  init_fakeData2();
@@ -19521,6 +19778,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
19521
19778
  }
19522
19779
  }
19523
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
+ }
19524
19787
  const sessionToken = req2.session?.get?.("token");
19525
19788
  if (sessionToken && validToken(sessionToken)) {
19526
19789
  resolvedToken = sessionToken;
@@ -34092,6 +34355,14 @@ function resolveSecuritySchemes() {
34092
34355
  const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
34093
34356
  schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
34094
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
+ }
34095
34366
  for (const [name, def] of Object.entries(registeredSchemes)) {
34096
34367
  schemes[name] = def;
34097
34368
  }
@@ -34273,7 +34544,9 @@ function generate(routes, models = []) {
34273
34544
  if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
34274
34545
  }
34275
34546
  } else if (routeRequiresAuth(route, method)) {
34276
- operation.security = sanitizeSecurity([{ [defaultScheme]: [] }], schemes);
34547
+ const requirements = [{ [defaultScheme]: [] }];
34548
+ if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
34549
+ operation.security = sanitizeSecurity(requirements, schemes);
34277
34550
  const responses = operation.responses;
34278
34551
  if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
34279
34552
  }
@@ -34587,6 +34860,298 @@ var init_src2 = __esm({
34587
34860
  }
34588
34861
  });
34589
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
+
34590
35155
  // ../core/src/docsAutoDiscovery.ts
34591
35156
  var docsAutoDiscovery_exports = {};
34592
35157
  __export(docsAutoDiscovery_exports, {
@@ -34656,7 +35221,7 @@ var init_docsAutoDiscovery = __esm({
34656
35221
 
34657
35222
  // ../core/src/server.ts
34658
35223
  import { createServer as createServer2 } from "node:http";
34659
- import { randomBytes as randomBytes7 } from "node:crypto";
35224
+ import { randomBytes as randomBytes8 } from "node:crypto";
34660
35225
  import { resolve as resolve19, dirname as dirname14, join as join30, relative as relative8 } from "node:path";
34661
35226
  import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
34662
35227
  import { isatty } from "node:tty";
@@ -35296,7 +35861,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
35296
35861
  }
35297
35862
  }
35298
35863
  }
35299
- const requestId = Log.getRequestId() ?? randomBytes7(4).toString("hex");
35864
+ const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
35300
35865
  if (wantsJson(req2)) {
35301
35866
  const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
35302
35867
  res.raw.writeHead(500, { "Content-Type": "application/json" });
@@ -35367,7 +35932,7 @@ function serveStaticAsset(ctx) {
35367
35932
  return false;
35368
35933
  }
35369
35934
  async function serveNotFound(ctx) {
35370
- const requestId = Log.getRequestId() ?? randomBytes7(4).toString("hex");
35935
+ const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
35371
35936
  if (wantsJson(ctx.req)) {
35372
35937
  const body = negotiatedErrorBody(404, "Not Found", requestId);
35373
35938
  ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
@@ -35487,7 +36052,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
35487
36052
  }
35488
36053
  }
35489
36054
  async function runDispatch(ctx, rawReq, rawRes) {
35490
- const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes7(4).toString("hex");
36055
+ const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
35491
36056
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
35492
36057
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
35493
36058
  }
@@ -35621,6 +36186,8 @@ ${reset2}
35621
36186
  console.log(`
35622
36187
  No routes directory found at ${routesDir}`);
35623
36188
  }
36189
+ const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
36190
+ await Sso2.mountConfigured(router);
35624
36191
  if (attachCsrfFromEnv()) {
35625
36192
  console.log(`
35626
36193
  \x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
@@ -36145,7 +36712,7 @@ var init_mqttMessage = __esm({
36145
36712
  // ../core/src/mqtt.ts
36146
36713
  import net2 from "node:net";
36147
36714
  import tls from "node:tls";
36148
- import { randomBytes as randomBytes8 } from "node:crypto";
36715
+ import { randomBytes as randomBytes9 } from "node:crypto";
36149
36716
  import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
36150
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;
36151
36718
  var init_mqtt = __esm({
@@ -36233,7 +36800,7 @@ var init_mqtt = __esm({
36233
36800
  this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
36234
36801
  this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
36235
36802
  let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
36236
- if (cid === null || cid === "") cid = "tina4-" + randomBytes8(8).toString("hex");
36803
+ if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
36237
36804
  this.clientId = cid;
36238
36805
  this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
36239
36806
  this.cleanSession = options.cleanSession ?? true;
@@ -37163,7 +37730,7 @@ var init_service = __esm({
37163
37730
  import http from "node:http";
37164
37731
  import https from "node:https";
37165
37732
  import { URL as URL2 } from "node:url";
37166
- import { randomBytes as randomBytes9 } from "node:crypto";
37733
+ import { randomBytes as randomBytes10 } from "node:crypto";
37167
37734
  import { promises as fsp, createWriteStream } from "node:fs";
37168
37735
  import { basename as basename6 } from "node:path";
37169
37736
  import { pipeline } from "node:stream/promises";
@@ -37461,7 +38028,7 @@ var init_api = __esm({
37461
38028
  return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
37462
38029
  }
37463
38030
  const partContentType = guessContentType(uploadName);
37464
- const boundary = "----Tina4Boundary" + randomBytes9(16).toString("hex");
38031
+ const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
37465
38032
  const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
37466
38033
  const contentType = `multipart/form-data; boundary=${boundary}`;
37467
38034
  return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
@@ -42311,10 +42878,13 @@ __export(src_exports3, {
42311
42878
  RouteGroup: () => RouteGroup,
42312
42879
  RouteRef: () => RouteRef,
42313
42880
  Router: () => Router,
42881
+ SSO: () => Sso,
42314
42882
  SafeString: () => SafeString2,
42315
42883
  SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
42316
42884
  ServiceRunner: () => ServiceRunner,
42317
42885
  Session: () => Session,
42886
+ Sso: () => Sso,
42887
+ SsoError: () => SsoError,
42318
42888
  TAKEOVER_KILLED: () => TAKEOVER_KILLED,
42319
42889
  TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
42320
42890
  TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
@@ -42551,6 +43121,7 @@ var init_src3 = __esm({
42551
43121
  init_htmlElement();
42552
43122
  init_errorOverlay();
42553
43123
  init_ai();
43124
+ init_sso();
42554
43125
  init_aiClient();
42555
43126
  init_liteBackend();
42556
43127
  init_rabbitmqBackend();