tina4-nodejs 3.13.103 → 3.13.105
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 +16 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +747 -57
- package/packages/core/dist/index.js +750 -57
- package/packages/core/src/authGate.ts +6 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/queue.ts +75 -18
- package/packages/core/src/queueBackends/liteBackend.ts +17 -1
- package/packages/core/src/queueBackends/mongoBackend.ts +80 -14
- package/packages/core/src/server.ts +5 -0
- package/packages/core/src/sso.ts +285 -0
- package/packages/orm/dist/index.js +755 -62
- package/packages/orm/src/adapters/postgres.ts +2 -0
- package/packages/orm/src/baseModel.ts +63 -10
- package/packages/orm/src/index.ts +2 -0
- package/packages/orm/src/migration.ts +14 -5
- 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/queue.d.ts +41 -4
- package/types/core/src/queueBackends/liteBackend.d.ts +7 -1
- package/types/core/src/queueBackends/mongoBackend.d.ts +7 -2
- package/types/core/src/sso.d.ts +55 -0
- package/types/orm/src/baseModel.d.ts +13 -5
- package/types/orm/src/index.d.ts +2 -0
- package/types/orm/src/point.d.ts +24 -0
- package/types/orm/src/queryBuilder.d.ts +10 -1
- package/types/orm/src/sqlTranslator.d.ts +13 -0
- package/types/orm/src/types.d.ts +5 -1
package/packages/cli/dist/bin.js
CHANGED
|
@@ -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:
|
|
@@ -12191,7 +12352,9 @@ function buildAddColumnSql(adapter, table2, colName, def) {
|
|
|
12191
12352
|
return sql;
|
|
12192
12353
|
}
|
|
12193
12354
|
function mt(db) {
|
|
12194
|
-
|
|
12355
|
+
const engine = engineOf(db);
|
|
12356
|
+
if (engine === "firebird") return MIGRATION_TABLE;
|
|
12357
|
+
return engine === "mysql" ? `\`${MIGRATION_TABLE}\`` : `"${MIGRATION_TABLE}"`;
|
|
12195
12358
|
}
|
|
12196
12359
|
function deriveDescription(name) {
|
|
12197
12360
|
return name.replace(/^\d+_/, "").replace(/_/g, " ");
|
|
@@ -12214,9 +12377,9 @@ async function ensureMigrationTableOn(db) {
|
|
|
12214
12377
|
id INTEGER NOT NULL PRIMARY KEY,
|
|
12215
12378
|
migration_name VARCHAR(500) NOT NULL UNIQUE,
|
|
12216
12379
|
description VARCHAR(500),
|
|
12217
|
-
batch INTEGER NOT NULL
|
|
12380
|
+
batch INTEGER DEFAULT 1 NOT NULL,
|
|
12218
12381
|
executed_at VARCHAR(50) NOT NULL,
|
|
12219
|
-
passed INTEGER NOT NULL
|
|
12382
|
+
passed INTEGER DEFAULT 1 NOT NULL
|
|
12220
12383
|
)`);
|
|
12221
12384
|
} else {
|
|
12222
12385
|
const idCol = migrationIdColumn(db);
|
|
@@ -12313,7 +12476,7 @@ async function recordApplied(db, name, batch, passed = 1) {
|
|
|
12313
12476
|
"SELECT GEN_ID(GEN_TINA4_MIGRATION_ID, 1) AS NEXT_ID FROM RDB$DATABASE"
|
|
12314
12477
|
);
|
|
12315
12478
|
insertCols.unshift("id");
|
|
12316
|
-
values.unshift(rows[0]?.
|
|
12479
|
+
values.unshift(rows[0]?.next_id ?? 1);
|
|
12317
12480
|
}
|
|
12318
12481
|
const placeholders = insertCols.map(() => "?").join(", ");
|
|
12319
12482
|
await adapterExecute(
|
|
@@ -13298,10 +13461,13 @@ var init_queryBuilder = __esm({
|
|
|
13298
13461
|
"use strict";
|
|
13299
13462
|
init_database();
|
|
13300
13463
|
init_databaseResult();
|
|
13464
|
+
init_point();
|
|
13465
|
+
init_sqlTranslator();
|
|
13301
13466
|
QueryBuilder = class _QueryBuilder {
|
|
13302
13467
|
table;
|
|
13303
13468
|
db;
|
|
13304
13469
|
columns = ["*"];
|
|
13470
|
+
selectParams = [];
|
|
13305
13471
|
wheres = [];
|
|
13306
13472
|
params = [];
|
|
13307
13473
|
joinClauses = [];
|
|
@@ -13309,14 +13475,17 @@ var init_queryBuilder = __esm({
|
|
|
13309
13475
|
havings = [];
|
|
13310
13476
|
havingParams = [];
|
|
13311
13477
|
orderByCols = [];
|
|
13478
|
+
orderByParams = [];
|
|
13479
|
+
primaryKey;
|
|
13312
13480
|
limitVal;
|
|
13313
13481
|
offsetVal;
|
|
13314
13482
|
/**
|
|
13315
13483
|
* Private constructor — use static factory methods.
|
|
13316
13484
|
*/
|
|
13317
|
-
constructor(table2, db) {
|
|
13485
|
+
constructor(table2, db, primaryKey) {
|
|
13318
13486
|
this.table = table2;
|
|
13319
13487
|
this.db = db;
|
|
13488
|
+
this.primaryKey = primaryKey;
|
|
13320
13489
|
}
|
|
13321
13490
|
/**
|
|
13322
13491
|
* Create a QueryBuilder for a table.
|
|
@@ -13325,8 +13494,8 @@ var init_queryBuilder = __esm({
|
|
|
13325
13494
|
* @param db - Optional database adapter.
|
|
13326
13495
|
* @returns A new QueryBuilder instance.
|
|
13327
13496
|
*/
|
|
13328
|
-
static fromTable(tableName, db) {
|
|
13329
|
-
return new _QueryBuilder(tableName, db);
|
|
13497
|
+
static fromTable(tableName, db, primaryKey) {
|
|
13498
|
+
return new _QueryBuilder(tableName, db, primaryKey);
|
|
13330
13499
|
}
|
|
13331
13500
|
/**
|
|
13332
13501
|
* Set the columns to select.
|
|
@@ -13337,6 +13506,7 @@ var init_queryBuilder = __esm({
|
|
|
13337
13506
|
select(...cols) {
|
|
13338
13507
|
if (cols.length > 0) {
|
|
13339
13508
|
this.columns = cols;
|
|
13509
|
+
this.selectParams = [];
|
|
13340
13510
|
}
|
|
13341
13511
|
return this;
|
|
13342
13512
|
}
|
|
@@ -13418,6 +13588,41 @@ var init_queryBuilder = __esm({
|
|
|
13418
13588
|
this.orderByCols.push(expression);
|
|
13419
13589
|
return this;
|
|
13420
13590
|
}
|
|
13591
|
+
withinDistance(column2, pointValue, radiusMetres, srid = DEFAULT_SRID) {
|
|
13592
|
+
const radius = Number(radiusMetres);
|
|
13593
|
+
if (!Number.isFinite(radius) || radius < 0) throw new RangeError("Spatial radius must be finite and greater than or equal to zero");
|
|
13594
|
+
const point = Point.parse(pointValue, srid);
|
|
13595
|
+
return this.where(SQLTranslator.withinDistance(this.engine(), column2, point.srid), [point.lon, point.lat, radius]);
|
|
13596
|
+
}
|
|
13597
|
+
intersects(column2, geometry, srid = DEFAULT_SRID) {
|
|
13598
|
+
const [bound, form] = Point.geometryBinding(geometry, srid);
|
|
13599
|
+
return this.where(SQLTranslator.intersects(this.engine(), column2, form, srid), [bound]);
|
|
13600
|
+
}
|
|
13601
|
+
bbox(column2, minLon, minLat, maxLon, maxLat, srid = DEFAULT_SRID) {
|
|
13602
|
+
const values = [minLon, minLat, maxLon, maxLat].map(Number);
|
|
13603
|
+
if (!values.every(Number.isFinite)) throw new TypeError("Bounding-box coordinates must be finite numbers");
|
|
13604
|
+
const [west, south, east, north] = values;
|
|
13605
|
+
new Point(west, south, srid);
|
|
13606
|
+
new Point(east, north, srid);
|
|
13607
|
+
if (west > east || south > north) throw new RangeError("Bounding box must be ordered west, south, east, north");
|
|
13608
|
+
return this.where(SQLTranslator.bbox(this.engine(), column2, srid), values);
|
|
13609
|
+
}
|
|
13610
|
+
selectDistance(column2, pointValue, alias = "distance", srid = DEFAULT_SRID) {
|
|
13611
|
+
const point = Point.parse(pointValue, srid);
|
|
13612
|
+
this.columns.push(SQLTranslator.distanceAs(this.engine(), column2, alias, point.srid));
|
|
13613
|
+
this.selectParams.push(point.lon, point.lat);
|
|
13614
|
+
return this;
|
|
13615
|
+
}
|
|
13616
|
+
orderByDistance(column2, pointValue, direction = "ASC", srid = DEFAULT_SRID) {
|
|
13617
|
+
const order = direction.toUpperCase();
|
|
13618
|
+
if (order !== "ASC" && order !== "DESC") throw new TypeError("Distance order direction must be ASC or DESC");
|
|
13619
|
+
if (!this.primaryKey) throw new Error("Stable spatial ordering needs a primary key; use BaseModel.query() or pass one to fromTable()");
|
|
13620
|
+
const point = Point.parse(pointValue, srid);
|
|
13621
|
+
this.orderByCols.push(`${SQLTranslator.distance(this.engine(), column2, point.srid)} ${order}`);
|
|
13622
|
+
this.orderByParams.push(point.lon, point.lat);
|
|
13623
|
+
this.orderByCols.push(`${SQLTranslator.spatialIdentifier(this.primaryKey, "primary key")} ASC`);
|
|
13624
|
+
return this;
|
|
13625
|
+
}
|
|
13421
13626
|
/**
|
|
13422
13627
|
* Set LIMIT and optional OFFSET.
|
|
13423
13628
|
*
|
|
@@ -13483,7 +13688,7 @@ var init_queryBuilder = __esm({
|
|
|
13483
13688
|
async get() {
|
|
13484
13689
|
this.ensureDb();
|
|
13485
13690
|
const sql = this.toSql();
|
|
13486
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13691
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13487
13692
|
const queryParams = allParams.length > 0 ? allParams : void 0;
|
|
13488
13693
|
const rows = await adapterFetch(
|
|
13489
13694
|
this.db,
|
|
@@ -13511,7 +13716,7 @@ var init_queryBuilder = __esm({
|
|
|
13511
13716
|
async first() {
|
|
13512
13717
|
this.ensureDb();
|
|
13513
13718
|
const sql = this.toSql();
|
|
13514
|
-
const allParams = [...this.params, ...this.havingParams];
|
|
13719
|
+
const allParams = [...this.selectParams, ...this.params, ...this.havingParams, ...this.orderByParams];
|
|
13515
13720
|
return adapterFetchOne(
|
|
13516
13721
|
this.db,
|
|
13517
13722
|
sql,
|
|
@@ -13526,9 +13731,18 @@ var init_queryBuilder = __esm({
|
|
|
13526
13731
|
async count() {
|
|
13527
13732
|
this.ensureDb();
|
|
13528
13733
|
const original = this.columns;
|
|
13734
|
+
const originalSelectParams = this.selectParams;
|
|
13735
|
+
const originalOrder = this.orderByCols;
|
|
13736
|
+
const originalOrderParams = this.orderByParams;
|
|
13529
13737
|
this.columns = ["COUNT(*) as cnt"];
|
|
13738
|
+
this.selectParams = [];
|
|
13739
|
+
this.orderByCols = [];
|
|
13740
|
+
this.orderByParams = [];
|
|
13530
13741
|
const sql = this.toSql();
|
|
13531
13742
|
this.columns = original;
|
|
13743
|
+
this.selectParams = originalSelectParams;
|
|
13744
|
+
this.orderByCols = originalOrder;
|
|
13745
|
+
this.orderByParams = originalOrderParams;
|
|
13532
13746
|
const allParams = [...this.params, ...this.havingParams];
|
|
13533
13747
|
const row = await adapterFetchOne(
|
|
13534
13748
|
this.db,
|
|
@@ -13713,6 +13927,10 @@ var init_queryBuilder = __esm({
|
|
|
13713
13927
|
}
|
|
13714
13928
|
}
|
|
13715
13929
|
}
|
|
13930
|
+
engine() {
|
|
13931
|
+
this.ensureDb();
|
|
13932
|
+
return this.db.getDatabaseType();
|
|
13933
|
+
}
|
|
13716
13934
|
};
|
|
13717
13935
|
}
|
|
13718
13936
|
});
|
|
@@ -13736,6 +13954,11 @@ function toDbFieldValue(def, value) {
|
|
|
13736
13954
|
if (def?.type === "json" && value !== null && value !== void 0 && typeof value !== "string") {
|
|
13737
13955
|
return JSON.stringify(value);
|
|
13738
13956
|
}
|
|
13957
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
13958
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
13959
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
13960
|
+
return point.ewkt;
|
|
13961
|
+
}
|
|
13739
13962
|
return value;
|
|
13740
13963
|
}
|
|
13741
13964
|
function fromDbFieldValue(def, value) {
|
|
@@ -13746,6 +13969,11 @@ function fromDbFieldValue(def, value) {
|
|
|
13746
13969
|
return value;
|
|
13747
13970
|
}
|
|
13748
13971
|
}
|
|
13972
|
+
if (def?.type === "point" && value !== null && value !== void 0) {
|
|
13973
|
+
const point = Point.parse(value, def.srid ?? DEFAULT_SRID);
|
|
13974
|
+
if (point.srid !== (def.srid ?? DEFAULT_SRID)) throw new TypeError(`Point field expects SRID ${def.srid ?? DEFAULT_SRID}; received ${point.srid}`);
|
|
13975
|
+
return point;
|
|
13976
|
+
}
|
|
13749
13977
|
return value;
|
|
13750
13978
|
}
|
|
13751
13979
|
function _pluralRelKeys() {
|
|
@@ -13781,6 +14009,7 @@ var init_baseModel = __esm({
|
|
|
13781
14009
|
init_sqlite();
|
|
13782
14010
|
init_sqlTranslator();
|
|
13783
14011
|
init_src3();
|
|
14012
|
+
init_point();
|
|
13784
14013
|
_fkRegistry = /* @__PURE__ */ new Map();
|
|
13785
14014
|
EAGER_IN_CHUNK = 500;
|
|
13786
14015
|
modelQueryCache = new QueryCache({ defaultTtl: 0, maxSize: 500 });
|
|
@@ -13836,7 +14065,9 @@ var init_baseModel = __esm({
|
|
|
13836
14065
|
for (const [name, def] of Object.entries(fields0)) {
|
|
13837
14066
|
if (def.default === void 0) continue;
|
|
13838
14067
|
let dv = typeof def.default === "function" ? def.default() : def.default;
|
|
13839
|
-
if (dv !== null &&
|
|
14068
|
+
if (def.type === "point" && dv !== null && dv !== void 0) {
|
|
14069
|
+
dv = fromDbFieldValue(def, dv);
|
|
14070
|
+
} else if (dv !== null && typeof dv === "object") dv = structuredClone(dv);
|
|
13840
14071
|
this[name] = dv;
|
|
13841
14072
|
}
|
|
13842
14073
|
if (data) {
|
|
@@ -13938,7 +14169,7 @@ var init_baseModel = __esm({
|
|
|
13938
14169
|
* @returns A QueryBuilder instance bound to this model's table and database.
|
|
13939
14170
|
*/
|
|
13940
14171
|
static query() {
|
|
13941
|
-
return QueryBuilder.fromTable(this.tableName, this.getDb());
|
|
14172
|
+
return QueryBuilder.fromTable(this.tableName, this.getDb(), this.getPkColumn());
|
|
13942
14173
|
}
|
|
13943
14174
|
/**
|
|
13944
14175
|
* Get the database adapter for this model.
|
|
@@ -14391,7 +14622,7 @@ var init_baseModel = __esm({
|
|
|
14391
14622
|
for (const key of Object.keys(ModelClass.fields)) {
|
|
14392
14623
|
if (this[key] !== void 0) {
|
|
14393
14624
|
const outKey = case_ === "snake" ? ModelClass.fieldMapping[key] ?? key : key;
|
|
14394
|
-
result[outKey] = this[key];
|
|
14625
|
+
result[outKey] = this[key] instanceof Point ? this[key].geojson : this[key];
|
|
14395
14626
|
}
|
|
14396
14627
|
}
|
|
14397
14628
|
if (ModelClass.softDelete && this.is_deleted !== void 0) {
|
|
@@ -14451,6 +14682,19 @@ var init_baseModel = __esm({
|
|
|
14451
14682
|
}
|
|
14452
14683
|
return result;
|
|
14453
14684
|
}
|
|
14685
|
+
toFeature(geometryField, include) {
|
|
14686
|
+
const ModelClass = this.constructor;
|
|
14687
|
+
const pointFields = Object.entries(ModelClass.fields).filter(([, def]) => def.type === "point").map(([name]) => name);
|
|
14688
|
+
const field = geometryField ?? pointFields[0];
|
|
14689
|
+
if (!field || !pointFields.includes(field)) throw new Error("toFeature() needs a declared point field");
|
|
14690
|
+
const properties = this.toDict(include, "camel");
|
|
14691
|
+
const geometry = properties[field] ?? null;
|
|
14692
|
+
delete properties[field];
|
|
14693
|
+
return { type: "Feature", geometry, properties };
|
|
14694
|
+
}
|
|
14695
|
+
static featureCollection(models, geometryField, include) {
|
|
14696
|
+
return { type: "FeatureCollection", features: models.map((model) => model.toFeature(geometryField, include)) };
|
|
14697
|
+
}
|
|
14454
14698
|
/**
|
|
14455
14699
|
* Convert to an associative object (alias for toDict).
|
|
14456
14700
|
*/
|
|
@@ -14501,7 +14745,10 @@ var init_baseModel = __esm({
|
|
|
14501
14745
|
*/
|
|
14502
14746
|
static async createTable() {
|
|
14503
14747
|
const db = this.getDb();
|
|
14504
|
-
|
|
14748
|
+
const pointFields = Object.entries(this.fields).filter(([, def]) => def.type === "point");
|
|
14749
|
+
const engine = db.getDatabaseType();
|
|
14750
|
+
if (pointFields.length > 0) SQLTranslator.requireSpatial(engine, "PointField");
|
|
14751
|
+
if (await adapterTableExists(db, this.tableName)) return this.createSpatialIndexes(db, pointFields);
|
|
14505
14752
|
if (typeof db.createTable === "function" || typeof db.createTableAsync === "function") {
|
|
14506
14753
|
const mappedFields = {};
|
|
14507
14754
|
for (const [fieldName, def] of Object.entries(this.fields)) {
|
|
@@ -14517,7 +14764,7 @@ var init_baseModel = __esm({
|
|
|
14517
14764
|
mappedFields["is_deleted"] = { type: "integer", default: 0 };
|
|
14518
14765
|
}
|
|
14519
14766
|
await adapterCreateTable(db, this.tableName, mappedFields);
|
|
14520
|
-
return
|
|
14767
|
+
return this.createSpatialIndexes(db, pointFields);
|
|
14521
14768
|
}
|
|
14522
14769
|
const typeMap = {
|
|
14523
14770
|
integer: "INTEGER",
|
|
@@ -14564,6 +14811,14 @@ var init_baseModel = __esm({
|
|
|
14564
14811
|
}
|
|
14565
14812
|
return true;
|
|
14566
14813
|
}
|
|
14814
|
+
static async createSpatialIndexes(db, fields) {
|
|
14815
|
+
for (const [fieldName, def] of fields) {
|
|
14816
|
+
SQLTranslator.pointColumnType(db.getDatabaseType(), def.srid ?? DEFAULT_SRID);
|
|
14817
|
+
if (def.spatialIndex === false) continue;
|
|
14818
|
+
await adapterExecute(db, SQLTranslator.spatialIndex(db.getDatabaseType(), this.tableName, this.getDbColumn(fieldName)));
|
|
14819
|
+
}
|
|
14820
|
+
return true;
|
|
14821
|
+
}
|
|
14567
14822
|
/**
|
|
14568
14823
|
* Find a record by primary key or throw an error if not found.
|
|
14569
14824
|
*/
|
|
@@ -14635,15 +14890,25 @@ var init_baseModel = __esm({
|
|
|
14635
14890
|
/**
|
|
14636
14891
|
* Invalidate every cached query that touches this model's table.
|
|
14637
14892
|
*
|
|
14638
|
-
* Tag-scoped
|
|
14639
|
-
* this table is busted too
|
|
14640
|
-
* never touches this table is left intact
|
|
14641
|
-
*
|
|
14642
|
-
*
|
|
14893
|
+
* Tag-scoped in the ORM layer (a cached JOIN on another model that reads
|
|
14894
|
+
* this table is busted too because it carries this table's tag; a query
|
|
14895
|
+
* that never touches this table is left intact), then cascaded to the
|
|
14896
|
+
* DB layer on this model's bound connection so an out-of-band write /
|
|
14897
|
+
* deliberate refresh / race-with-another-process cannot leave stale rows
|
|
14898
|
+
* in db.fetch()'s persistent cache. Called after every ORM write
|
|
14899
|
+
* (save/delete/forceDelete/restore) so a read-after-write never serves
|
|
14900
|
+
* a stale/deleted row (CACHE-DEC-01). PY-06-22 (3.13.105) added the
|
|
14901
|
+
* DB-layer cascade -- previously the two cache layers disagreed under
|
|
14902
|
+
* TINA4_AUTO_CACHING=true + TINA4_DB_CACHE=true.
|
|
14643
14903
|
*/
|
|
14644
14904
|
static clearCache() {
|
|
14645
14905
|
const ModelClass = this;
|
|
14646
14906
|
modelQueryCache.clearTag((ModelClass.tableName ?? "").toLowerCase());
|
|
14907
|
+
try {
|
|
14908
|
+
const db = ModelClass.getDb();
|
|
14909
|
+
if (typeof db?.cacheClear === "function") db.cacheClear();
|
|
14910
|
+
} catch {
|
|
14911
|
+
}
|
|
14647
14912
|
}
|
|
14648
14913
|
/**
|
|
14649
14914
|
* Execute a raw SQL SELECT and return results as model instances.
|
|
@@ -17211,6 +17476,7 @@ __export(src_exports, {
|
|
|
17211
17476
|
CachedDatabaseAdapter: () => CachedDatabaseAdapter,
|
|
17212
17477
|
Cursor: () => Cursor,
|
|
17213
17478
|
DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS: () => DEFAULT_DATABASE_CONNECT_TIMEOUT_SECONDS,
|
|
17479
|
+
DEFAULT_SRID: () => DEFAULT_SRID,
|
|
17214
17480
|
Database: () => Database,
|
|
17215
17481
|
DatabaseResult: () => DatabaseResult,
|
|
17216
17482
|
DatabaseUrl: () => DatabaseUrl,
|
|
@@ -17226,6 +17492,7 @@ __export(src_exports, {
|
|
|
17226
17492
|
NOT_REQUIRED_ON_ADAPTER: () => NOT_REQUIRED_ON_ADAPTER,
|
|
17227
17493
|
ObjectId: () => ObjectId,
|
|
17228
17494
|
OdbcAdapter: () => OdbcAdapter,
|
|
17495
|
+
Point: () => Point,
|
|
17229
17496
|
PostgresAdapter: () => PostgresAdapter,
|
|
17230
17497
|
QueryBuilder: () => QueryBuilder,
|
|
17231
17498
|
QueryCache: () => QueryCache,
|
|
@@ -17238,6 +17505,7 @@ __export(src_exports, {
|
|
|
17238
17505
|
S3Storage: () => S3Storage,
|
|
17239
17506
|
SQLTranslator: () => SQLTranslator,
|
|
17240
17507
|
SQLiteAdapter: () => SQLiteAdapter,
|
|
17508
|
+
SpatialNotSupportedError: () => SpatialNotSupportedError,
|
|
17241
17509
|
SqliteCollection: () => SqliteCollection,
|
|
17242
17510
|
SqliteDatabase: () => SqliteDatabase,
|
|
17243
17511
|
adapterColumns: () => adapterColumns,
|
|
@@ -17332,6 +17600,7 @@ var init_src = __esm({
|
|
|
17332
17600
|
init_baseModel();
|
|
17333
17601
|
init_queryBuilder();
|
|
17334
17602
|
init_sqlTranslator();
|
|
17603
|
+
init_point();
|
|
17335
17604
|
init_connectTimeout();
|
|
17336
17605
|
init_cachedDatabase();
|
|
17337
17606
|
init_fakeData2();
|
|
@@ -19521,6 +19790,12 @@ function enforceRouteAuth(req2, res, match, isDevAdmin) {
|
|
|
19521
19790
|
}
|
|
19522
19791
|
}
|
|
19523
19792
|
if (!resolvedToken) {
|
|
19793
|
+
const sso = req2.session?.get?.("_tina4_sso");
|
|
19794
|
+
const identity = sso?.identity;
|
|
19795
|
+
if (identity?.issuer && identity?.subject) {
|
|
19796
|
+
req2.user = identity;
|
|
19797
|
+
return false;
|
|
19798
|
+
}
|
|
19524
19799
|
const sessionToken = req2.session?.get?.("token");
|
|
19525
19800
|
if (sessionToken && validToken(sessionToken)) {
|
|
19526
19801
|
resolvedToken = sessionToken;
|
|
@@ -27852,17 +28127,67 @@ var init_mongoBackend = __esm({
|
|
|
27852
28127
|
process.stdout.write("__OK__");
|
|
27853
28128
|
}
|
|
27854
28129
|
else if (operation === "retry") {
|
|
27855
|
-
// Explicit manual re-queue (
|
|
27856
|
-
//
|
|
28130
|
+
// Explicit manual re-queue. Serves BOTH Queue.retry(id) (revive
|
|
28131
|
+
// a dead-letter job) AND job.retry() (manual re-queue of a live
|
|
28132
|
+
// reserved/pending job) so the Mongo backend matches
|
|
28133
|
+
// LiteBackend's dual behaviour.
|
|
28134
|
+
//
|
|
28135
|
+
// 1) DL revival (Queue.retry(id) after fail exhausted retries).
|
|
28136
|
+
// Pre-3.13.105 this branch was BROKEN: the search filter was
|
|
28137
|
+
// { queue: queueName, id, status: "failed" } -- three separate
|
|
28138
|
+
// reasons it could never match. dead_letter() inserts under
|
|
28139
|
+
// queueName + ".dead_letter" (not queueName), carries
|
|
28140
|
+
// status "dead" (not "failed"), and the original under
|
|
28141
|
+
// queueName was already acked to "completed" by the time the
|
|
28142
|
+
// DL was written. Now we look up in the DL namespace by id,
|
|
28143
|
+
// delete the DL doc first (so an interrupted retry never
|
|
28144
|
+
// leaves both a DL and a fresh pending doc), and upsert the
|
|
28145
|
+
// original back to pending -- re-hydrating if the original
|
|
28146
|
+
// was purged (housekeeping) so a retry always works.
|
|
28147
|
+
// 2) Live-doc manual re-queue (job.retry() on a job the caller
|
|
28148
|
+
// just popped and wants back in pending). The live-doc path
|
|
28149
|
+
// is preserved from before 3.13.105.
|
|
28150
|
+
//
|
|
28151
|
+
// Returns __OK__ when either path acted; __NOT_FOUND__ when
|
|
28152
|
+
// neither the DL nor the live doc existed, so Queue.retry(id)
|
|
28153
|
+
// can now report the pre-3.13.105 blanket-true as false for
|
|
28154
|
+
// unknown ids. data = JSON { id, delaySeconds }.
|
|
27857
28155
|
const info = JSON.parse(data);
|
|
28156
|
+
const dlTopic = queueName + ".dead_letter";
|
|
28157
|
+
const now = new Date().toISOString();
|
|
27858
28158
|
const avail = info.delaySeconds > 0
|
|
27859
28159
|
? new Date(Date.now() + info.delaySeconds * 1000).toISOString()
|
|
27860
|
-
:
|
|
27861
|
-
await col.
|
|
27862
|
-
|
|
27863
|
-
|
|
27864
|
-
|
|
27865
|
-
|
|
28160
|
+
: now;
|
|
28161
|
+
const dlDoc = await col.findOne({ queue: dlTopic, id: info.id });
|
|
28162
|
+
if (dlDoc !== null) {
|
|
28163
|
+
await col.deleteOne({ _id: dlDoc._id });
|
|
28164
|
+
const payload = dlDoc.payload ?? {};
|
|
28165
|
+
const priority = dlDoc.priority ?? 0;
|
|
28166
|
+
await col.updateOne(
|
|
28167
|
+
{ queue: queueName, id: info.id },
|
|
28168
|
+
{
|
|
28169
|
+
$set: {
|
|
28170
|
+
status: "pending",
|
|
28171
|
+
availableAt: avail,
|
|
28172
|
+
reservedAt: null,
|
|
28173
|
+
error: null,
|
|
28174
|
+
payload,
|
|
28175
|
+
priority,
|
|
28176
|
+
id: info.id,
|
|
28177
|
+
createdAt: dlDoc.createdAt ?? now,
|
|
28178
|
+
},
|
|
28179
|
+
$inc: { attempts: 1 },
|
|
28180
|
+
},
|
|
28181
|
+
{ upsert: true },
|
|
28182
|
+
);
|
|
28183
|
+
process.stdout.write("__OK__");
|
|
28184
|
+
} else {
|
|
28185
|
+
const result = await col.updateOne(
|
|
28186
|
+
{ queue: queueName, id: info.id },
|
|
28187
|
+
{ $set: { status: "pending", availableAt: avail, reservedAt: null }, $inc: { attempts: 1 } },
|
|
28188
|
+
);
|
|
28189
|
+
process.stdout.write(result.matchedCount > 0 ? "__OK__" : "__NOT_FOUND__");
|
|
28190
|
+
}
|
|
27866
28191
|
}
|
|
27867
28192
|
else if (operation === "deadLetters") {
|
|
27868
28193
|
const docs = await col.find({ queue: queueName + ".dead_letter" }).toArray();
|
|
@@ -27907,10 +28232,20 @@ var init_mongoBackend = __esm({
|
|
|
27907
28232
|
process.stdout.write(String(revived));
|
|
27908
28233
|
}
|
|
27909
28234
|
else if (operation === "purge") {
|
|
27910
|
-
// Delete docs by status (default:
|
|
28235
|
+
// Delete docs by status (default: every doc for the topic).
|
|
28236
|
+
// Pre-3.13.105 this filtered by { queue: queueName, status } for
|
|
28237
|
+
// EVERY status -- correct for pending/reserved/completed, wrong
|
|
28238
|
+
// for the dead-letter states (dead/failed/dead_letter) which
|
|
28239
|
+
// live under queueName + ".dead_letter" and carry status "dead".
|
|
28240
|
+
// A purge("dead") therefore deleted nothing and returned 0.
|
|
28241
|
+
// data = JSON { status }.
|
|
27911
28242
|
const info = data ? JSON.parse(data) : {};
|
|
27912
|
-
const
|
|
27913
|
-
|
|
28243
|
+
const isDead = info.status && ["dead", "failed", "dead_letter"].includes(info.status);
|
|
28244
|
+
const filter = isDead
|
|
28245
|
+
? { queue: queueName + ".dead_letter" }
|
|
28246
|
+
: (info.status
|
|
28247
|
+
? { queue: queueName, status: info.status }
|
|
28248
|
+
: { queue: queueName });
|
|
27914
28249
|
const res = await col.deleteMany(filter);
|
|
27915
28250
|
process.stdout.write(String(res.deletedCount || 0));
|
|
27916
28251
|
}
|
|
@@ -28007,9 +28342,15 @@ var init_mongoBackend = __esm({
|
|
|
28007
28342
|
fail(queue, id, error, maxRetries, retryBackoff = 0) {
|
|
28008
28343
|
this.execSync("fail", queue, JSON.stringify({ id, error, maxRetries, retryBackoff }));
|
|
28009
28344
|
}
|
|
28010
|
-
/**
|
|
28345
|
+
/**
|
|
28346
|
+
* Revive a specific dead-letter job by id. Returns true if the DL was found
|
|
28347
|
+
* and revived, false otherwise (parity with LiteBackend.retry(queue, id)
|
|
28348
|
+
* and Python's mongo_backend.retry_job()). Pre-3.13.105 this returned void
|
|
28349
|
+
* and Queue.retry(id) reported success for every call, even for unknown ids.
|
|
28350
|
+
*/
|
|
28011
28351
|
retry(queue, id, delaySeconds = 0) {
|
|
28012
|
-
this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
28352
|
+
const out = this.execSync("retry", queue, JSON.stringify({ id, delaySeconds }));
|
|
28353
|
+
return out.includes("__OK__");
|
|
28013
28354
|
}
|
|
28014
28355
|
/** Jobs that exceeded max retries (the `<queue>.dead_letter` collection topic). */
|
|
28015
28356
|
deadLetters(queue, maxRetries) {
|
|
@@ -28678,10 +29019,20 @@ var init_liteBackend = __esm({
|
|
|
28678
29019
|
* Explicit re-queue requested by the caller (job.retry()).
|
|
28679
29020
|
*
|
|
28680
29021
|
* Always re-enqueues regardless of the retry limit — manual override,
|
|
28681
|
-
* distinct from the automatic failJob() path.
|
|
29022
|
+
* distinct from the automatic failJob() path. Cleans up BOTH the
|
|
29023
|
+
* reservation record AND any dead-letter file for this id, so a caller
|
|
29024
|
+
* that iterates deadLetters() and calls .retry() on each doesn't leave
|
|
29025
|
+
* the failed/ directory carrying duplicates (PY-12-05, 3.13.105).
|
|
29026
|
+
* Aligns with retry(queue, jobId) which had always unlinked the
|
|
29027
|
+
* dead-letter file -- two spellings of the same intent that previously
|
|
29028
|
+
* diverged.
|
|
28682
29029
|
*/
|
|
28683
29030
|
retryJob(queue, job, delaySeconds) {
|
|
28684
29031
|
this.clearReservation(queue, job.id);
|
|
29032
|
+
try {
|
|
29033
|
+
unlinkSync7(join23(this.ensureFailedDir(queue), `${job.id}.queue-data`));
|
|
29034
|
+
} catch {
|
|
29035
|
+
}
|
|
28685
29036
|
job.attempts = (job.attempts || 0) + 1;
|
|
28686
29037
|
job.error = void 0;
|
|
28687
29038
|
this.requeue(queue, job, delaySeconds ?? 0, void 0);
|
|
@@ -28880,7 +29231,19 @@ var init_queue = __esm({
|
|
|
28880
29231
|
}
|
|
28881
29232
|
}
|
|
28882
29233
|
/**
|
|
28883
|
-
* Count jobs
|
|
29234
|
+
* Count jobs by status. Defaults to "pending".
|
|
29235
|
+
*
|
|
29236
|
+
* ``"pending"`` counts jobs waiting to be popped -- INCLUDES retryable-
|
|
29237
|
+
* but-attempted ones, because they live in the pending queue under the
|
|
29238
|
+
* auto-retry lifecycle (see failed()).
|
|
29239
|
+
* ``"reserved"`` counts jobs a consumer has popped but not yet
|
|
29240
|
+
* completed/failed (in-flight against the visibility timeout).
|
|
29241
|
+
* ``"completed"`` counts jobs the consumer has finished successfully.
|
|
29242
|
+
* ``"failed"``, ``"dead"``, ``"dead_letter"`` are ALIASES that all count
|
|
29243
|
+
* the dead-letter store -- jobs whose attempts >= maxRetries and that
|
|
29244
|
+
* have given up. Use deadLetters() to list them. Retryable-but-attempted
|
|
29245
|
+
* jobs are NOT counted by size("failed"); use failed() to list them or
|
|
29246
|
+
* size("pending") to include them in a total.
|
|
28884
29247
|
*/
|
|
28885
29248
|
size(status2 = "pending") {
|
|
28886
29249
|
const q = this.topic;
|
|
@@ -28930,13 +29293,17 @@ var init_queue = __esm({
|
|
|
28930
29293
|
/**
|
|
28931
29294
|
* Get jobs that failed at least once but are still being retried
|
|
28932
29295
|
* (0 < attempts < maxRetries). These live in the pending queue under the
|
|
28933
|
-
* auto-retry lifecycle
|
|
29296
|
+
* auto-retry lifecycle (fail() re-queues them with an incremented attempts
|
|
29297
|
+
* count and a retryBackoff delay) so pop() picks them up again. They are
|
|
29298
|
+
* NOT counted by size("failed") -- that alias counts the dead-letter store,
|
|
29299
|
+
* matching deadLetters(). To include retryable-failed jobs in a total, use
|
|
29300
|
+
* size("pending"). Terminal failures are returned by deadLetters().
|
|
28934
29301
|
*/
|
|
28935
29302
|
failed() {
|
|
28936
|
-
|
|
28937
|
-
|
|
28938
|
-
|
|
28939
|
-
|
|
29303
|
+
const raw = this.externalBackend?.failed ? this.externalBackend.failed(this.topic, this._maxRetries) : this.liteBackend.failed(this.topic, this._maxRetries);
|
|
29304
|
+
return raw.map(
|
|
29305
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
29306
|
+
);
|
|
28940
29307
|
}
|
|
28941
29308
|
/**
|
|
28942
29309
|
* Retry all dead letter jobs for this queue's topic.
|
|
@@ -28948,8 +29315,8 @@ var init_queue = __esm({
|
|
|
28948
29315
|
retry(jobId, delaySeconds) {
|
|
28949
29316
|
if (jobId) {
|
|
28950
29317
|
if (this.externalBackend?.retry) {
|
|
28951
|
-
this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
28952
|
-
return true;
|
|
29318
|
+
const result = this.externalBackend.retry(this.topic, jobId, delaySeconds);
|
|
29319
|
+
return result === void 0 ? true : Boolean(result);
|
|
28953
29320
|
}
|
|
28954
29321
|
return this.liteBackend.retry(this.topic, jobId, delaySeconds);
|
|
28955
29322
|
}
|
|
@@ -28958,8 +29325,8 @@ var init_queue = __esm({
|
|
|
28958
29325
|
let retried = false;
|
|
28959
29326
|
for (const job of deadJobs) {
|
|
28960
29327
|
if (this.externalBackend?.retry) {
|
|
28961
|
-
this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
28962
|
-
retried = true;
|
|
29328
|
+
const result = this.externalBackend.retry(this.topic, job.id, delaySeconds);
|
|
29329
|
+
if (result === void 0 || Boolean(result)) retried = true;
|
|
28963
29330
|
} else if (this.liteBackend.retry(this.topic, job.id, delaySeconds)) {
|
|
28964
29331
|
retried = true;
|
|
28965
29332
|
}
|
|
@@ -28967,13 +29334,28 @@ var init_queue = __esm({
|
|
|
28967
29334
|
return retried;
|
|
28968
29335
|
}
|
|
28969
29336
|
/**
|
|
28970
|
-
* Get
|
|
29337
|
+
* Get jobs that exceeded max_retries -- terminal failures.
|
|
29338
|
+
*
|
|
29339
|
+
* Same set counted by size("failed") / size("dead") / size("dead_letter")
|
|
29340
|
+
* (three aliases for the dead-letter store). To LIST retryable-but-
|
|
29341
|
+
* attempted jobs (attempts > 0 AND attempts < maxRetries) that are still
|
|
29342
|
+
* being auto-retried, use failed() -- those live in the pending queue and
|
|
29343
|
+
* are NOT dead letters.
|
|
29344
|
+
*
|
|
29345
|
+
* Returns Job objects with the failure reason on ``.error`` (not raw dicts)
|
|
29346
|
+
* so callers can iterate uniformly with the rest of the queue API and, in
|
|
29347
|
+
* particular, call ``.retry()`` on each to manually revive it:
|
|
29348
|
+
*
|
|
29349
|
+
* for (const job of queue.deadLetters()) {
|
|
29350
|
+
* Log.warn(`revived ${job.id}: ${job.error}`);
|
|
29351
|
+
* job.retry();
|
|
29352
|
+
* }
|
|
28971
29353
|
*/
|
|
28972
29354
|
deadLetters(maxRetries) {
|
|
28973
|
-
|
|
28974
|
-
|
|
28975
|
-
|
|
28976
|
-
|
|
29355
|
+
const raw = this.externalBackend?.deadLetters ? this.externalBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries) : this.liteBackend.deadLetters(this.topic, maxRetries ?? this._maxRetries);
|
|
29356
|
+
return raw.map(
|
|
29357
|
+
(data) => createJob({ ...data, topic: data.topic ?? this.topic }, this)
|
|
29358
|
+
);
|
|
28977
29359
|
}
|
|
28978
29360
|
/**
|
|
28979
29361
|
* Delete messages by status (e.g. "completed", "failed", "dead").
|
|
@@ -34092,6 +34474,14 @@ function resolveSecuritySchemes() {
|
|
|
34092
34474
|
const apiKeyIn = ["header", "query", "cookie"].includes(rawIn) ? rawIn : "header";
|
|
34093
34475
|
schemes.apiKeyAuth = { type: "apiKey", name: apiKeyName, in: apiKeyIn };
|
|
34094
34476
|
}
|
|
34477
|
+
const ssoIssuer = (process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
34478
|
+
if (ssoIssuer) {
|
|
34479
|
+
schemes.oidc = {
|
|
34480
|
+
type: "openIdConnect",
|
|
34481
|
+
openIdConnectUrl: `${ssoIssuer}/.well-known/openid-configuration`
|
|
34482
|
+
};
|
|
34483
|
+
schemes.ssoSession = { type: "apiKey", in: "cookie", name: "tina4_session" };
|
|
34484
|
+
}
|
|
34095
34485
|
for (const [name, def] of Object.entries(registeredSchemes)) {
|
|
34096
34486
|
schemes[name] = def;
|
|
34097
34487
|
}
|
|
@@ -34273,7 +34663,9 @@ function generate(routes, models = []) {
|
|
|
34273
34663
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34274
34664
|
}
|
|
34275
34665
|
} else if (routeRequiresAuth(route, method)) {
|
|
34276
|
-
|
|
34666
|
+
const requirements = [{ [defaultScheme]: [] }];
|
|
34667
|
+
if (defaultScheme === "bearerAuth" && schemes.ssoSession) requirements.push({ ssoSession: [] });
|
|
34668
|
+
operation.security = sanitizeSecurity(requirements, schemes);
|
|
34277
34669
|
const responses = operation.responses;
|
|
34278
34670
|
if (!responses["401"]) responses["401"] = { description: "Unauthorized" };
|
|
34279
34671
|
}
|
|
@@ -34587,6 +34979,298 @@ var init_src2 = __esm({
|
|
|
34587
34979
|
}
|
|
34588
34980
|
});
|
|
34589
34981
|
|
|
34982
|
+
// ../core/src/sso.ts
|
|
34983
|
+
var sso_exports = {};
|
|
34984
|
+
__export(sso_exports, {
|
|
34985
|
+
SSO: () => Sso,
|
|
34986
|
+
Sso: () => Sso,
|
|
34987
|
+
SsoError: () => SsoError
|
|
34988
|
+
});
|
|
34989
|
+
import { createHash as createHash9, randomBytes as randomBytes7, timingSafeEqual as timingSafeEqual3 } from "node:crypto";
|
|
34990
|
+
var SsoError, Sso;
|
|
34991
|
+
var init_sso = __esm({
|
|
34992
|
+
"../core/src/sso.ts"() {
|
|
34993
|
+
"use strict";
|
|
34994
|
+
SsoError = class extends Error {
|
|
34995
|
+
};
|
|
34996
|
+
Sso = class _Sso {
|
|
34997
|
+
static PENDING_KEY = "_tina4_sso_pending";
|
|
34998
|
+
static SESSION_KEY = "_tina4_sso";
|
|
34999
|
+
issuer;
|
|
35000
|
+
clientId;
|
|
35001
|
+
clientSecret;
|
|
35002
|
+
redirectUri;
|
|
35003
|
+
scopes;
|
|
35004
|
+
verify;
|
|
35005
|
+
postLogoutRedirectUri;
|
|
35006
|
+
claimMap;
|
|
35007
|
+
timeout;
|
|
35008
|
+
metadata = {};
|
|
35009
|
+
static mountedRouters = /* @__PURE__ */ new WeakSet();
|
|
35010
|
+
constructor(options = {}) {
|
|
35011
|
+
this.issuer = (options.issuer ?? process.env.TINA4_SSO_ISSUER ?? "").replace(/\/$/, "");
|
|
35012
|
+
this.clientId = options.clientId ?? process.env.TINA4_SSO_CLIENT_ID ?? "";
|
|
35013
|
+
this.clientSecret = options.clientSecret ?? process.env.TINA4_SSO_CLIENT_SECRET;
|
|
35014
|
+
this.redirectUri = options.redirectUri ?? process.env.TINA4_SSO_REDIRECT_URI ?? "";
|
|
35015
|
+
this.scopes = options.scopes ?? this.jsonEnv("TINA4_SSO_SCOPES", ["openid", "profile", "email"]);
|
|
35016
|
+
this.verify = options.verify ?? process.env.TINA4_SSO_VERIFY ?? "introspection";
|
|
35017
|
+
this.postLogoutRedirectUri = options.postLogoutRedirectUri ?? process.env.TINA4_SSO_POST_LOGOUT_REDIRECT_URI;
|
|
35018
|
+
this.claimMap = options.claimMap ?? this.jsonEnv("TINA4_SSO_CLAIM_MAP", {});
|
|
35019
|
+
this.timeout = options.timeout ?? 1e4;
|
|
35020
|
+
this.validateConfig();
|
|
35021
|
+
}
|
|
35022
|
+
static async fromIssuer(options = {}) {
|
|
35023
|
+
const value = new _Sso(options);
|
|
35024
|
+
await value.discover();
|
|
35025
|
+
return value;
|
|
35026
|
+
}
|
|
35027
|
+
static configured() {
|
|
35028
|
+
return ["TINA4_SSO_ISSUER", "TINA4_SSO_CLIENT_ID", "TINA4_SSO_REDIRECT_URI"].every((key) => Boolean(process.env[key]));
|
|
35029
|
+
}
|
|
35030
|
+
jsonEnv(name, fallback) {
|
|
35031
|
+
const raw = process.env[name];
|
|
35032
|
+
if (!raw) return fallback;
|
|
35033
|
+
try {
|
|
35034
|
+
return JSON.parse(raw);
|
|
35035
|
+
} catch {
|
|
35036
|
+
throw new SsoError(`${name} must be valid JSON`);
|
|
35037
|
+
}
|
|
35038
|
+
}
|
|
35039
|
+
static secureUrl(value, name) {
|
|
35040
|
+
let url;
|
|
35041
|
+
try {
|
|
35042
|
+
url = new URL(value);
|
|
35043
|
+
} catch {
|
|
35044
|
+
throw new SsoError(`${name} must be an absolute URL`);
|
|
35045
|
+
}
|
|
35046
|
+
const loopback = ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
|
|
35047
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
35048
|
+
throw new SsoError(`${name} must use HTTPS except on loopback`);
|
|
35049
|
+
}
|
|
35050
|
+
}
|
|
35051
|
+
validateConfig() {
|
|
35052
|
+
if (!this.issuer || !this.clientId || !this.redirectUri) {
|
|
35053
|
+
throw new SsoError("TINA4_SSO_ISSUER, TINA4_SSO_CLIENT_ID and TINA4_SSO_REDIRECT_URI are required");
|
|
35054
|
+
}
|
|
35055
|
+
_Sso.secureUrl(this.issuer, "issuer");
|
|
35056
|
+
_Sso.secureUrl(this.redirectUri, "redirect URI");
|
|
35057
|
+
if (!["introspection", "jwks"].includes(this.verify)) throw new SsoError("TINA4_SSO_VERIFY must be introspection or jwks");
|
|
35058
|
+
if (this.verify === "jwks") throw new SsoError("jwks verification requires an installed cryptography capability");
|
|
35059
|
+
if (this.verify === "introspection" && !this.clientSecret) throw new SsoError("introspection verification requires TINA4_SSO_CLIENT_SECRET");
|
|
35060
|
+
if (!Array.isArray(this.scopes) || !this.scopes.includes("openid")) throw new SsoError("TINA4_SSO_SCOPES must be a list containing openid");
|
|
35061
|
+
}
|
|
35062
|
+
async requestJson(url, form, bearer, basic = false) {
|
|
35063
|
+
const headers = { Accept: "application/json" };
|
|
35064
|
+
let body;
|
|
35065
|
+
if (form) {
|
|
35066
|
+
const parameters = new URLSearchParams();
|
|
35067
|
+
for (const [key, value] of Object.entries(form)) parameters.set(key, String(value));
|
|
35068
|
+
body = parameters.toString();
|
|
35069
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
35070
|
+
}
|
|
35071
|
+
if (bearer) headers.Authorization = `Bearer ${bearer}`;
|
|
35072
|
+
if (basic) headers.Authorization = `Basic ${Buffer.from(`${this.clientId}:${this.clientSecret}`).toString("base64")}`;
|
|
35073
|
+
const controller = new AbortController();
|
|
35074
|
+
const timer = setTimeout(() => controller.abort(), this.timeout);
|
|
35075
|
+
try {
|
|
35076
|
+
const response = await fetch(url, { method: form ? "POST" : "GET", headers, body, signal: controller.signal });
|
|
35077
|
+
if (!response.ok) throw new SsoError("OIDC provider request failed");
|
|
35078
|
+
const result = await response.json();
|
|
35079
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) throw new SsoError("OIDC provider returned a non-object response");
|
|
35080
|
+
return result;
|
|
35081
|
+
} catch (error) {
|
|
35082
|
+
if (error instanceof SsoError) throw error;
|
|
35083
|
+
throw new SsoError("OIDC provider request failed");
|
|
35084
|
+
} finally {
|
|
35085
|
+
clearTimeout(timer);
|
|
35086
|
+
}
|
|
35087
|
+
}
|
|
35088
|
+
async discover(force = false) {
|
|
35089
|
+
if (Object.keys(this.metadata).length && !force) return { ...this.metadata };
|
|
35090
|
+
const result = await this.requestJson(`${this.issuer}/.well-known/openid-configuration`);
|
|
35091
|
+
if (result.issuer !== this.issuer) throw new SsoError("OIDC discovery issuer does not exactly match configuration");
|
|
35092
|
+
const required = ["authorization_endpoint", "token_endpoint"];
|
|
35093
|
+
if (this.verify === "introspection") required.push("introspection_endpoint");
|
|
35094
|
+
for (const key of required) {
|
|
35095
|
+
if (!result[key]) throw new SsoError(`OIDC discovery is missing ${key}`);
|
|
35096
|
+
_Sso.secureUrl(result[key], key);
|
|
35097
|
+
}
|
|
35098
|
+
this.metadata = result;
|
|
35099
|
+
return { ...result };
|
|
35100
|
+
}
|
|
35101
|
+
static safeReturn(value) {
|
|
35102
|
+
if (!value || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return "/";
|
|
35103
|
+
return [...value].some((char) => char.charCodeAt(0) < 32) ? "/" : value;
|
|
35104
|
+
}
|
|
35105
|
+
session(value) {
|
|
35106
|
+
return value?.session ?? value;
|
|
35107
|
+
}
|
|
35108
|
+
async login(requestOrSession, returnTo = "/") {
|
|
35109
|
+
const session = this.session(requestOrSession);
|
|
35110
|
+
if (!session) throw new SsoError("SSO login requires a Tina4 Session");
|
|
35111
|
+
const state = randomBytes7(32).toString("base64url");
|
|
35112
|
+
const nonce = randomBytes7(32).toString("base64url");
|
|
35113
|
+
const verifier = randomBytes7(64).toString("base64url");
|
|
35114
|
+
const challenge = createHash9("sha256").update(verifier).digest("base64url");
|
|
35115
|
+
session.set(_Sso.PENDING_KEY, { state, nonce, verifier, return_to: _Sso.safeReturn(returnTo), created_at: Math.floor(Date.now() / 1e3) });
|
|
35116
|
+
const metadata = await this.discover();
|
|
35117
|
+
const query = new URLSearchParams({
|
|
35118
|
+
client_id: this.clientId,
|
|
35119
|
+
redirect_uri: this.redirectUri,
|
|
35120
|
+
response_type: "code",
|
|
35121
|
+
scope: this.scopes.join(" "),
|
|
35122
|
+
state,
|
|
35123
|
+
nonce,
|
|
35124
|
+
code_challenge: challenge,
|
|
35125
|
+
code_challenge_method: "S256"
|
|
35126
|
+
});
|
|
35127
|
+
return `${metadata.authorization_endpoint}?${query}`;
|
|
35128
|
+
}
|
|
35129
|
+
static equal(left, right) {
|
|
35130
|
+
const a = Buffer.from(String(left ?? ""));
|
|
35131
|
+
const b = Buffer.from(String(right ?? ""));
|
|
35132
|
+
return a.length === b.length && timingSafeEqual3(a, b);
|
|
35133
|
+
}
|
|
35134
|
+
static jwtPayload(token) {
|
|
35135
|
+
try {
|
|
35136
|
+
return JSON.parse(Buffer.from(token.split(".")[1], "base64url").toString());
|
|
35137
|
+
} catch {
|
|
35138
|
+
throw new SsoError("provider returned an invalid ID token");
|
|
35139
|
+
}
|
|
35140
|
+
}
|
|
35141
|
+
async introspect(accessToken) {
|
|
35142
|
+
const metadata = await this.discover();
|
|
35143
|
+
const result = await this.requestJson(metadata.introspection_endpoint, { token: accessToken, token_type_hint: "access_token" }, void 0, true);
|
|
35144
|
+
if (result.active !== true || result.iss !== this.issuer) throw new SsoError("OIDC access token is inactive or has the wrong issuer");
|
|
35145
|
+
const audience = result.aud ?? result.client_id;
|
|
35146
|
+
const valid = (Array.isArray(audience) ? audience.includes(this.clientId) : audience === this.clientId) || result.client_id === this.clientId;
|
|
35147
|
+
if (!valid) throw new SsoError("OIDC token audience mismatch");
|
|
35148
|
+
return result;
|
|
35149
|
+
}
|
|
35150
|
+
claim(claims, configured, fallback) {
|
|
35151
|
+
let value = claims;
|
|
35152
|
+
for (const part of (configured ?? fallback).split(".")) value = value && typeof value === "object" ? value[part] : void 0;
|
|
35153
|
+
return value;
|
|
35154
|
+
}
|
|
35155
|
+
normalize(claims) {
|
|
35156
|
+
const subject = this.claim(claims, this.claimMap.subject, "sub");
|
|
35157
|
+
const issuer = this.claim(claims, this.claimMap.issuer, "iss") ?? this.issuer;
|
|
35158
|
+
if (!subject || issuer !== this.issuer) throw new SsoError("OIDC identity is missing a valid issuer or subject");
|
|
35159
|
+
const roles = [...this.claim(claims, this.claimMap.roles, "realm_access.roles") ?? [], ...claims.resource_access?.[this.clientId]?.roles ?? []];
|
|
35160
|
+
const groups = this.claim(claims, this.claimMap.groups, "groups") ?? [];
|
|
35161
|
+
return {
|
|
35162
|
+
issuer,
|
|
35163
|
+
subject,
|
|
35164
|
+
username: this.claim(claims, this.claimMap.username, "preferred_username") ?? null,
|
|
35165
|
+
email: this.claim(claims, this.claimMap.email, "email") ?? null,
|
|
35166
|
+
name: this.claim(claims, this.claimMap.name, "name") ?? null,
|
|
35167
|
+
roles: [...new Set(roles.map(String))].sort(),
|
|
35168
|
+
groups: [...new Set(groups.map(String))].sort()
|
|
35169
|
+
};
|
|
35170
|
+
}
|
|
35171
|
+
async callback(requestOrSession, query) {
|
|
35172
|
+
const session = this.session(requestOrSession);
|
|
35173
|
+
const values = query ?? requestOrSession?.query ?? {};
|
|
35174
|
+
const pending = session?.get(_Sso.PENDING_KEY);
|
|
35175
|
+
session?.delete(_Sso.PENDING_KEY);
|
|
35176
|
+
if (!pending || !values.code || !_Sso.equal(values.state, pending.state)) throw new SsoError("OIDC callback state is invalid or already consumed");
|
|
35177
|
+
if (Math.floor(Date.now() / 1e3) - Number(pending.created_at ?? 0) > 600) throw new SsoError("OIDC callback state has expired");
|
|
35178
|
+
const metadata = await this.discover();
|
|
35179
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
35180
|
+
grant_type: "authorization_code",
|
|
35181
|
+
code: values.code,
|
|
35182
|
+
redirect_uri: this.redirectUri,
|
|
35183
|
+
client_id: this.clientId,
|
|
35184
|
+
code_verifier: pending.verifier
|
|
35185
|
+
}, void 0, Boolean(this.clientSecret));
|
|
35186
|
+
if (!tokens.access_token || !tokens.id_token) throw new SsoError("OIDC token response is incomplete");
|
|
35187
|
+
if (this.verify === "jwks") throw new SsoError("JWKS verification requires an installed cryptography capability");
|
|
35188
|
+
const claims = await this.introspect(tokens.access_token);
|
|
35189
|
+
if (!_Sso.equal(_Sso.jwtPayload(tokens.id_token).nonce, pending.nonce)) throw new SsoError("OIDC ID token nonce mismatch");
|
|
35190
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
35191
|
+
const identity = this.normalize(claims);
|
|
35192
|
+
session.regenerate();
|
|
35193
|
+
session.set(_Sso.SESSION_KEY, {
|
|
35194
|
+
version: 1,
|
|
35195
|
+
identity,
|
|
35196
|
+
access_token: tokens.access_token,
|
|
35197
|
+
refresh_token: tokens.refresh_token,
|
|
35198
|
+
id_token: tokens.id_token,
|
|
35199
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
35200
|
+
});
|
|
35201
|
+
return { identity, return_to: _Sso.safeReturn(pending.return_to) };
|
|
35202
|
+
}
|
|
35203
|
+
identity(requestOrSession) {
|
|
35204
|
+
const stored = this.session(requestOrSession)?.get(_Sso.SESSION_KEY);
|
|
35205
|
+
const identity = stored?.identity ?? null;
|
|
35206
|
+
if (identity && requestOrSession?.session) requestOrSession.user = identity;
|
|
35207
|
+
return identity;
|
|
35208
|
+
}
|
|
35209
|
+
async refresh(requestOrSession) {
|
|
35210
|
+
const session = this.session(requestOrSession);
|
|
35211
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
35212
|
+
if (!stored?.refresh_token) {
|
|
35213
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
35214
|
+
throw new SsoError("OIDC session cannot be refreshed");
|
|
35215
|
+
}
|
|
35216
|
+
try {
|
|
35217
|
+
const metadata = await this.discover();
|
|
35218
|
+
const tokens = await this.requestJson(metadata.token_endpoint, {
|
|
35219
|
+
grant_type: "refresh_token",
|
|
35220
|
+
refresh_token: stored.refresh_token,
|
|
35221
|
+
client_id: this.clientId
|
|
35222
|
+
}, void 0, Boolean(this.clientSecret));
|
|
35223
|
+
const claims = await this.introspect(tokens.access_token);
|
|
35224
|
+
if (metadata.userinfo_endpoint) Object.assign(claims, await this.requestJson(metadata.userinfo_endpoint, void 0, tokens.access_token));
|
|
35225
|
+
const identity = this.normalize(claims);
|
|
35226
|
+
session.set(_Sso.SESSION_KEY, {
|
|
35227
|
+
...stored,
|
|
35228
|
+
identity,
|
|
35229
|
+
access_token: tokens.access_token,
|
|
35230
|
+
refresh_token: tokens.refresh_token ?? stored.refresh_token,
|
|
35231
|
+
id_token: tokens.id_token ?? stored.id_token,
|
|
35232
|
+
expires_at: Math.floor(Date.now() / 1e3) + Number(tokens.expires_in ?? 0)
|
|
35233
|
+
});
|
|
35234
|
+
return identity;
|
|
35235
|
+
} catch (error) {
|
|
35236
|
+
session?.delete(_Sso.SESSION_KEY);
|
|
35237
|
+
throw error;
|
|
35238
|
+
}
|
|
35239
|
+
}
|
|
35240
|
+
async logout(requestOrSession, returnTo = "/") {
|
|
35241
|
+
const session = this.session(requestOrSession);
|
|
35242
|
+
const stored = session?.get(_Sso.SESSION_KEY);
|
|
35243
|
+
session?.destroy();
|
|
35244
|
+
const endpoint = (await this.discover()).end_session_endpoint;
|
|
35245
|
+
const target = this.postLogoutRedirectUri ?? _Sso.safeReturn(returnTo);
|
|
35246
|
+
if (!endpoint) return target;
|
|
35247
|
+
const params = new URLSearchParams({ post_logout_redirect_uri: target, client_id: this.clientId });
|
|
35248
|
+
if (stored?.id_token) params.set("id_token_hint", stored.id_token);
|
|
35249
|
+
return `${endpoint}?${params}`;
|
|
35250
|
+
}
|
|
35251
|
+
static async mountConfigured(router) {
|
|
35252
|
+
if (_Sso.mountedRouters.has(router) || !_Sso.configured()) return false;
|
|
35253
|
+
const owned = /* @__PURE__ */ new Set(["GET /auth/login", "GET /auth/callback", "POST /auth/logout"]);
|
|
35254
|
+
const collisions = router.getRoutes().map((route) => `${route.method} ${route.pattern}`).filter((route) => owned.has(route));
|
|
35255
|
+
if (collisions.length) throw new SsoError(`SSO route collision: ${collisions.join(", ")}`);
|
|
35256
|
+
const sso = await _Sso.fromIssuer();
|
|
35257
|
+
router.get("/auth/login", async (req2, res) => res.redirect(await sso.login(req2, req2.query?.return_to ?? "/")));
|
|
35258
|
+
router.get("/auth/callback", async (req2, res) => {
|
|
35259
|
+
try {
|
|
35260
|
+
return res.redirect((await sso.callback(req2)).return_to);
|
|
35261
|
+
} catch (error) {
|
|
35262
|
+
const message = error instanceof SsoError ? error.message : "OIDC callback failed";
|
|
35263
|
+
return res.error("SSO_CALLBACK_FAILED", message, 400);
|
|
35264
|
+
}
|
|
35265
|
+
});
|
|
35266
|
+
router.post("/auth/logout", async (req2, res) => res.redirect(await sso.logout(req2, req2.query?.return_to ?? "/")));
|
|
35267
|
+
_Sso.mountedRouters.add(router);
|
|
35268
|
+
return true;
|
|
35269
|
+
}
|
|
35270
|
+
};
|
|
35271
|
+
}
|
|
35272
|
+
});
|
|
35273
|
+
|
|
34590
35274
|
// ../core/src/docsAutoDiscovery.ts
|
|
34591
35275
|
var docsAutoDiscovery_exports = {};
|
|
34592
35276
|
__export(docsAutoDiscovery_exports, {
|
|
@@ -34656,7 +35340,7 @@ var init_docsAutoDiscovery = __esm({
|
|
|
34656
35340
|
|
|
34657
35341
|
// ../core/src/server.ts
|
|
34658
35342
|
import { createServer as createServer2 } from "node:http";
|
|
34659
|
-
import { randomBytes as
|
|
35343
|
+
import { randomBytes as randomBytes8 } from "node:crypto";
|
|
34660
35344
|
import { resolve as resolve19, dirname as dirname14, join as join30, relative as relative8 } from "node:path";
|
|
34661
35345
|
import { existsSync as existsSync25, readdirSync as readdirSync17, readFileSync as readFileSync23, statSync as statSync17 } from "node:fs";
|
|
34662
35346
|
import { isatty } from "node:tty";
|
|
@@ -35296,7 +35980,7 @@ async function renderDispatchError(err, req2, res, templatesDir) {
|
|
|
35296
35980
|
}
|
|
35297
35981
|
}
|
|
35298
35982
|
}
|
|
35299
|
-
const requestId = Log.getRequestId() ??
|
|
35983
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35300
35984
|
if (wantsJson(req2)) {
|
|
35301
35985
|
const body = negotiatedErrorBody(500, "Internal Server Error", requestId);
|
|
35302
35986
|
res.raw.writeHead(500, { "Content-Type": "application/json" });
|
|
@@ -35367,7 +36051,7 @@ function serveStaticAsset(ctx) {
|
|
|
35367
36051
|
return false;
|
|
35368
36052
|
}
|
|
35369
36053
|
async function serveNotFound(ctx) {
|
|
35370
|
-
const requestId = Log.getRequestId() ??
|
|
36054
|
+
const requestId = Log.getRequestId() ?? randomBytes8(4).toString("hex");
|
|
35371
36055
|
if (wantsJson(ctx.req)) {
|
|
35372
36056
|
const body = negotiatedErrorBody(404, "Not Found", requestId);
|
|
35373
36057
|
ctx.res.raw.writeHead(404, httpReason(404), { "Content-Type": "application/json" });
|
|
@@ -35487,7 +36171,7 @@ async function dispatchInner(ctx, rawReq, rawRes, requestId) {
|
|
|
35487
36171
|
}
|
|
35488
36172
|
}
|
|
35489
36173
|
async function runDispatch(ctx, rawReq, rawRes) {
|
|
35490
|
-
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ??
|
|
36174
|
+
const requestId = Log.sanitizeRequestId(rawReq.headers["x-request-id"]) ?? randomBytes8(4).toString("hex");
|
|
35491
36175
|
if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
|
|
35492
36176
|
return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
|
|
35493
36177
|
}
|
|
@@ -35621,6 +36305,8 @@ ${reset2}
|
|
|
35621
36305
|
console.log(`
|
|
35622
36306
|
No routes directory found at ${routesDir}`);
|
|
35623
36307
|
}
|
|
36308
|
+
const { Sso: Sso2 } = await Promise.resolve().then(() => (init_sso(), sso_exports));
|
|
36309
|
+
await Sso2.mountConfigured(router);
|
|
35624
36310
|
if (attachCsrfFromEnv()) {
|
|
35625
36311
|
console.log(`
|
|
35626
36312
|
\x1B[36mCSRF\x1B[0m protection enabled (TINA4_CSRF)`);
|
|
@@ -36145,7 +36831,7 @@ var init_mqttMessage = __esm({
|
|
|
36145
36831
|
// ../core/src/mqtt.ts
|
|
36146
36832
|
import net2 from "node:net";
|
|
36147
36833
|
import tls from "node:tls";
|
|
36148
|
-
import { randomBytes as
|
|
36834
|
+
import { randomBytes as randomBytes9 } from "node:crypto";
|
|
36149
36835
|
import { existsSync as existsSync26, readFileSync as readFileSync24 } from "node:fs";
|
|
36150
36836
|
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
36837
|
var init_mqtt = __esm({
|
|
@@ -36233,7 +36919,7 @@ var init_mqtt = __esm({
|
|
|
36233
36919
|
this.caFile = options.caFile ?? (Env.str("TINA4_MQTT_CA_FILE") || null);
|
|
36234
36920
|
this.tlsVerify = options.tlsVerify ?? Env.bool("TINA4_MQTT_TLS_VERIFY", true);
|
|
36235
36921
|
let cid = options.clientId ?? (Env.str("TINA4_MQTT_CLIENT_ID") || null);
|
|
36236
|
-
if (cid === null || cid === "") cid = "tina4-" +
|
|
36922
|
+
if (cid === null || cid === "") cid = "tina4-" + randomBytes9(8).toString("hex");
|
|
36237
36923
|
this.clientId = cid;
|
|
36238
36924
|
this.keepalive = options.keepalive ?? Env.int("TINA4_MQTT_KEEPALIVE", DEFAULT_KEEPALIVE);
|
|
36239
36925
|
this.cleanSession = options.cleanSession ?? true;
|
|
@@ -37163,7 +37849,7 @@ var init_service = __esm({
|
|
|
37163
37849
|
import http from "node:http";
|
|
37164
37850
|
import https from "node:https";
|
|
37165
37851
|
import { URL as URL2 } from "node:url";
|
|
37166
|
-
import { randomBytes as
|
|
37852
|
+
import { randomBytes as randomBytes10 } from "node:crypto";
|
|
37167
37853
|
import { promises as fsp, createWriteStream } from "node:fs";
|
|
37168
37854
|
import { basename as basename6 } from "node:path";
|
|
37169
37855
|
import { pipeline } from "node:stream/promises";
|
|
@@ -37461,7 +38147,7 @@ var init_api = __esm({
|
|
|
37461
38147
|
return { http_code: null, body: null, headers: {}, error: "upload requires filePath or fileBytes" };
|
|
37462
38148
|
}
|
|
37463
38149
|
const partContentType = guessContentType(uploadName);
|
|
37464
|
-
const boundary = "----Tina4Boundary" +
|
|
38150
|
+
const boundary = "----Tina4Boundary" + randomBytes10(16).toString("hex");
|
|
37465
38151
|
const bodyBuffer = buildMultipartBody(boundary, fieldName, uploadName, content, partContentType, extraFields);
|
|
37466
38152
|
const contentType = `multipart/form-data; boundary=${boundary}`;
|
|
37467
38153
|
return this.execute("POST", this.buildUrl(path8), bodyBuffer, contentType, headers);
|
|
@@ -42311,10 +42997,13 @@ __export(src_exports3, {
|
|
|
42311
42997
|
RouteGroup: () => RouteGroup,
|
|
42312
42998
|
RouteRef: () => RouteRef,
|
|
42313
42999
|
Router: () => Router,
|
|
43000
|
+
SSO: () => Sso,
|
|
42314
43001
|
SafeString: () => SafeString2,
|
|
42315
43002
|
SecurityHeadersMiddleware: () => SecurityHeadersMiddleware,
|
|
42316
43003
|
ServiceRunner: () => ServiceRunner,
|
|
42317
43004
|
Session: () => Session,
|
|
43005
|
+
Sso: () => Sso,
|
|
43006
|
+
SsoError: () => SsoError,
|
|
42318
43007
|
TAKEOVER_KILLED: () => TAKEOVER_KILLED,
|
|
42319
43008
|
TAKEOVER_NOTHING: () => TAKEOVER_NOTHING,
|
|
42320
43009
|
TAKEOVER_REFUSALS: () => TAKEOVER_REFUSALS,
|
|
@@ -42551,6 +43240,7 @@ var init_src3 = __esm({
|
|
|
42551
43240
|
init_htmlElement();
|
|
42552
43241
|
init_errorOverlay();
|
|
42553
43242
|
init_ai();
|
|
43243
|
+
init_sso();
|
|
42554
43244
|
init_aiClient();
|
|
42555
43245
|
init_liteBackend();
|
|
42556
43246
|
init_rabbitmqBackend();
|