ris-mcp-ts 1.1.3 → 1.3.0

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.
Files changed (75) hide show
  1. package/README.md +39 -13
  2. package/dist/client.d.ts +33 -9
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +181 -76
  5. package/dist/client.js.map +1 -1
  6. package/dist/constants.d.ts +12 -1
  7. package/dist/constants.d.ts.map +1 -1
  8. package/dist/constants.js +57 -0
  9. package/dist/constants.js.map +1 -1
  10. package/dist/formatting.d.ts.map +1 -1
  11. package/dist/formatting.js +33 -2
  12. package/dist/formatting.js.map +1 -1
  13. package/dist/helpers.d.ts +45 -10
  14. package/dist/helpers.d.ts.map +1 -1
  15. package/dist/helpers.js +27 -5
  16. package/dist/helpers.js.map +1 -1
  17. package/dist/http.d.ts +33 -2
  18. package/dist/http.d.ts.map +1 -1
  19. package/dist/http.js +163 -16
  20. package/dist/http.js.map +1 -1
  21. package/dist/index.js +0 -0
  22. package/dist/parser.d.ts.map +1 -1
  23. package/dist/parser.js +41 -4
  24. package/dist/parser.js.map +1 -1
  25. package/dist/server.d.ts.map +1 -1
  26. package/dist/server.js +2 -1
  27. package/dist/server.js.map +1 -1
  28. package/dist/tools/bezirke.d.ts.map +1 -1
  29. package/dist/tools/bezirke.js +29 -23
  30. package/dist/tools/bezirke.js.map +1 -1
  31. package/dist/tools/bundesgesetzblatt.d.ts.map +1 -1
  32. package/dist/tools/bundesgesetzblatt.js +29 -22
  33. package/dist/tools/bundesgesetzblatt.js.map +1 -1
  34. package/dist/tools/bundesrecht.d.ts +20 -0
  35. package/dist/tools/bundesrecht.d.ts.map +1 -1
  36. package/dist/tools/bundesrecht.js +80 -41
  37. package/dist/tools/bundesrecht.js.map +1 -1
  38. package/dist/tools/dokument.d.ts.map +1 -1
  39. package/dist/tools/dokument.js +105 -135
  40. package/dist/tools/dokument.js.map +1 -1
  41. package/dist/tools/gemeinden.d.ts.map +1 -1
  42. package/dist/tools/gemeinden.js +71 -53
  43. package/dist/tools/gemeinden.js.map +1 -1
  44. package/dist/tools/history.d.ts.map +1 -1
  45. package/dist/tools/history.js +29 -23
  46. package/dist/tools/history.js.map +1 -1
  47. package/dist/tools/judikatur.d.ts +27 -0
  48. package/dist/tools/judikatur.d.ts.map +1 -1
  49. package/dist/tools/judikatur.js +111 -27
  50. package/dist/tools/judikatur.js.map +1 -1
  51. package/dist/tools/landesgesetzblatt.d.ts.map +1 -1
  52. package/dist/tools/landesgesetzblatt.js +26 -20
  53. package/dist/tools/landesgesetzblatt.js.map +1 -1
  54. package/dist/tools/landesrecht.d.ts +20 -0
  55. package/dist/tools/landesrecht.d.ts.map +1 -1
  56. package/dist/tools/landesrecht.js +83 -31
  57. package/dist/tools/landesrecht.js.map +1 -1
  58. package/dist/tools/regierungsvorlagen.d.ts.map +1 -1
  59. package/dist/tools/regierungsvorlagen.js +59 -53
  60. package/dist/tools/regierungsvorlagen.js.map +1 -1
  61. package/dist/tools/sonstige.d.ts.map +1 -1
  62. package/dist/tools/sonstige.js +97 -91
  63. package/dist/tools/sonstige.js.map +1 -1
  64. package/dist/tools/verordnungen.d.ts.map +1 -1
  65. package/dist/tools/verordnungen.js +26 -20
  66. package/dist/tools/verordnungen.js.map +1 -1
  67. package/dist/types.d.ts +142 -19
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +61 -1
  70. package/dist/types.js.map +1 -1
  71. package/dist/version.d.ts +13 -0
  72. package/dist/version.d.ts.map +1 -0
  73. package/dist/version.js +15 -0
  74. package/dist/version.js.map +1 -0
  75. package/package.json +43 -27
package/README.md CHANGED
@@ -33,11 +33,11 @@ The server translates these natural language requests into structured API calls
33
33
 
34
34
  - **12 specialized tools** covering all major RIS collections
35
35
  - **Federal law** (ABGB, StGB, UGB, ...) and **state law** for all 9 provinces
36
- - **Court decisions** from 11 court types (Supreme Court, Constitutional Court, Administrative Court, ...)
36
+ - **Court decisions** from 16 court types (Supreme Court, Constitutional Court, Administrative Court, ...)
37
37
  - **Law gazettes** — Federal (BGBl) and state (LGBl)
38
38
  - **Government bills**, ministerial decrees, cabinet protocols
39
39
  - **Full document retrieval** with smart prefix-based routing
40
- - **Change history tracking** across 30 application types
40
+ - **Change history tracking** across 36 application types
41
41
  - **Markdown and JSON** output formats
42
42
  - **Free and open** — uses Austria's Open Government Data API, no API key needed
43
43
 
@@ -148,7 +148,7 @@ npx -y ris-mcp-ts
148
148
  |------|-------------|
149
149
  | `ris_bundesrecht` | Search federal laws (ABGB, StGB, UGB, etc.) |
150
150
  | `ris_landesrecht` | Search state/provincial laws (all 9 provinces) |
151
- | `ris_judikatur` | Search court decisions (11 court types) |
151
+ | `ris_judikatur` | Search court decisions (16 court types) |
152
152
  | `ris_bundesgesetzblatt` | Search Federal Law Gazettes (BGBl I/II/III) |
153
153
  | `ris_landesgesetzblatt` | Search State Law Gazettes (LGBl) |
154
154
  | `ris_regierungsvorlagen` | Search government bills |
@@ -156,7 +156,7 @@ npx -y ris-mcp-ts
156
156
  | `ris_bezirke` | Search district authority announcements |
157
157
  | `ris_gemeinden` | Search municipal law and regulations |
158
158
  | `ris_sonstige` | Search miscellaneous collections (8 sub-apps) |
159
- | `ris_history` | Track document change history (30 app types) |
159
+ | `ris_history` | Track document change history (36 app types) |
160
160
  | `ris_verordnungen` | Search state ordinance gazettes |
161
161
 
162
162
  ## Tool Reference
@@ -172,8 +172,9 @@ Search Austrian federal laws such as ABGB, StGB, UGB, and more.
172
172
  |-----------|------|-------------|
173
173
  | `suchworte` | string | Full-text search (e.g., "Mietrecht", "Schadenersatz") |
174
174
  | `titel` | string | Search in law titles (e.g., "ABGB", "Strafgesetzbuch") |
175
- | `paragraph` | string | Section number (e.g., "1295" for §1295) |
176
- | `applikation` | string | "BrKons" (consolidated, default), "Begut" (draft reviews), "Erv" (English version) |
175
+ | `paragraph` | string | Section number (e.g., "1295" for §1295, "7" for Art 7) |
176
+ | `abschnitt_typ` | string | Type of section `paragraph` refers to: "Paragraph" (default), "Artikel" (article-based laws like B-VG), "Anlage" (annex) |
177
+ | `applikation` | string | "BrKons" (consolidated, default), "Begut" (drafts), "BgblAuth" (gazette), "Erv" (English translations) |
177
178
  | `fassung_vom` | string | Date for historical version (YYYY-MM-DD) |
178
179
  | `seite` | number | Page number (default: 1) |
179
180
  | `limit` | number | Results per page: 10, 20, 50, 100 (default: 20) |
@@ -181,6 +182,8 @@ Search Austrian federal laws such as ABGB, StGB, UGB, and more.
181
182
 
182
183
  All parameters are optional. At least one search parameter (`suchworte`, `titel`, or `paragraph`) should be provided.
183
184
 
185
+ > **Note:** `applikation="Erv"` (English translations) uses a different parameter vocabulary internally (`SearchTerms`/`Title`) and does not support `paragraph`/`abschnitt_typ` or `fassung_vom`.
186
+
184
187
  </details>
185
188
 
186
189
  <details>
@@ -195,6 +198,10 @@ Search state laws of the nine Austrian provinces.
195
198
  | `suchworte` | string | Full-text search |
196
199
  | `titel` | string | Search in law titles |
197
200
  | `bundesland` | string | Province: Wien, Niederoesterreich, Oberoesterreich, Salzburg, Tirol, Vorarlberg, Kaernten, Steiermark, Burgenland |
201
+ | `paragraph` | string | Section number (e.g., "1" for §1, "7" for Art 7) |
202
+ | `abschnitt_typ` | string | Type of section `paragraph` refers to: "Paragraph" (default), "Artikel", "Anlage" |
203
+ | `fassung_vom` | string | Date for historical version (YYYY-MM-DD) |
204
+ | `gesetzesnummer` | string | Exact law number (Gesetzesnummer) of a specific state law |
198
205
  | `applikation` | string | "LrKons" (consolidated, default) |
199
206
  | `seite` | number | Page number |
200
207
  | `limit` | number | Results per page |
@@ -212,18 +219,25 @@ Search court decisions from Austrian courts.
212
219
  | Parameter | Type | Description |
213
220
  |-----------|------|-------------|
214
221
  | `suchworte` | string | Full-text search in decisions |
215
- | `gericht` | string | Court type (see below) |
222
+ | `gerichtsbarkeit` | string | Court system / data collection to search — the RIS "Applikation" (see below; default "Justiz") |
223
+ | `dokumenttyp` | string | "rechtssatz" (headnotes only), "entscheidungstext" (full texts only), or "beide" (both, default) |
224
+ | `gericht` | string | Actual court within the jurisdiction (e.g., "OGH", "OLG Wien"); applies to gerichtsbarkeit="Justiz" |
225
+ | `rechtsgebiet` | string | Broad legal area: "Zivilrecht" or "Strafrecht" (Justiz) |
226
+ | `fachgebiet` | string | OGH subject area (e.g., "Arbeitsrecht"); Justiz, needs dokumenttyp "entscheidungstext"/"beide" |
227
+ | `entscheidungsart` | string | Decision type (e.g., "Erkenntnis", "Beschluss"); allowed values differ by jurisdiction |
228
+ | `sammlungsnummer` | string | Collection number: VfSlg (Vfgh) or VwSlg (Vwgh) |
216
229
  | `norm` | string | Legal norm (e.g., "1319a ABGB") |
217
230
  | `geschaeftszahl` | string | Case number (e.g., "5Ob234/20b") |
218
231
  | `entscheidungsdatum_von` | string | Decision date from (YYYY-MM-DD) |
219
232
  | `entscheidungsdatum_bis` | string | Decision date to (YYYY-MM-DD) |
233
+ | `sortierung` | string | Sort by decision date: "datum_auf" (oldest first) or "datum_ab" (newest first) |
220
234
  | `seite` | number | Page number |
221
235
  | `limit` | number | Results per page |
222
236
  | `response_format` | string | "markdown" or "json" |
223
237
 
224
- **Available courts:**
238
+ **Available `gerichtsbarkeit` values (16):**
225
239
 
226
- | Value | Court |
240
+ | Value | Court system |
227
241
  |-------|-------|
228
242
  | `Justiz` | Supreme Court (OGH), Higher Regional Courts (OLG), Regional Courts (LG) — default |
229
243
  | `Vfgh` | Constitutional Court |
@@ -231,11 +245,16 @@ Search court decisions from Austrian courts.
231
245
  | `Bvwg` | Federal Administrative Court |
232
246
  | `Lvwg` | Provincial Administrative Courts |
233
247
  | `Dsk` | Data Protection Authority |
234
- | `AsylGH` | Asylum Court (historical) |
248
+ | `AsylGH` | Asylum Court (historical, until 2013) |
235
249
  | `Normenliste` | Index of legal norms |
236
250
  | `Pvak` | Personnel Representation Supervisory Commission |
237
251
  | `Gbk` | Equal Treatment Commission |
238
252
  | `Dok` | Disciplinary Commission |
253
+ | `Verg` | Federal Procurement Office (historical, dissolved 2014) |
254
+ | `Uvs` | Independent Administrative Senates (historical, dissolved 2014) |
255
+ | `Ubas` | Independent Federal Asylum Senate (historical, dissolved 2014) |
256
+ | `Umse` | Environmental Senate (historical, dissolved 2014) |
257
+ | `Bks` | Federal Communications Board (historical, dissolved 2014) |
239
258
 
240
259
  </details>
241
260
 
@@ -325,12 +344,19 @@ At least one of `dokumentnummer` or `url` is required. Long documents are trunca
325
344
  |--------|------|
326
345
  | NOR | Federal norms |
327
346
  | LBG, LKT, LNO, LOO, LSB, LST, LTI, LVB, LWI | State laws (9 provinces) |
328
- | JWR, JFR, JWT, BVWG, LVWG, DSB, GBK, PVAK, ASYLGH | Court decisions |
329
- | BGBLA, BGBL | Federal Law Gazette |
347
+ | JWR, JWT | Supreme Administrative Court (VwGH) |
348
+ | JFR, JFT | Constitutional Court (VfGH) |
349
+ | JJR, JJT | Ordinary courts (Justiz) |
350
+ | BVWG, LVWG | Federal / State Administrative Courts |
351
+ | DSB, PDK, GBK, PVAB, DKT, NL, ASYLGH | Data protection, equal treatment, personnel, disciplinary, norm lists, asylum |
352
+ | VERG, JUR, JUT, UBAS, UMSE, BKS | Historical jurisdictions (dissolved 2014) |
353
+ | BGBLA, BGBL, BGBLPDF | Federal Law Gazette (authentic / 1945–2003 / PDF) |
330
354
  | REGV | Government bills |
331
355
  | BVB | District authorities |
332
356
  | VBL | Ordinance gazettes |
333
- | MRP, ERL | Cabinet protocols, ministerial decrees |
357
+ | MRP, ERL, PRUEF, AVSV, SPG, KMGER | Cabinet protocols, decrees, trade exams, social insurance, health plans, court announcements |
358
+
359
+ > The full routing registry lives in `DOCUMENT_ROUTES` (`src/client.ts`); unknown prefixes fall back to a Justiz search.
334
360
 
335
361
  </details>
336
362
 
package/dist/client.d.ts CHANGED
@@ -35,35 +35,35 @@ export declare function isAllowedUrl(url: string): boolean;
35
35
  /**
36
36
  * Search federal law (Bundesrecht).
37
37
  */
38
- export declare function searchBundesrecht(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
38
+ export declare function searchBundesrecht(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
39
39
  /**
40
40
  * Search state/provincial law (Landesrecht).
41
41
  */
42
- export declare function searchLandesrecht(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
42
+ export declare function searchLandesrecht(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
43
43
  /**
44
44
  * Search case law/jurisprudence (Judikatur).
45
45
  */
46
- export declare function searchJudikatur(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
46
+ export declare function searchJudikatur(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
47
47
  /**
48
48
  * Search district administrative authorities (Bezirke).
49
49
  */
50
- export declare function searchBezirke(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
50
+ export declare function searchBezirke(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
51
51
  /**
52
52
  * Search municipal law (Gemeinden).
53
53
  */
54
- export declare function searchGemeinden(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
54
+ export declare function searchGemeinden(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
55
55
  /**
56
56
  * Search miscellaneous legal collections (Sonstige).
57
57
  */
58
- export declare function searchSonstige(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
58
+ export declare function searchSonstige(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
59
59
  /**
60
60
  * Search document change history (History).
61
61
  */
62
- export declare function searchHistory(params: Record<string, unknown>, timeout?: number): Promise<NormalizedSearchResults>;
62
+ export declare function searchHistory(params: Record<string, unknown>, timeout?: number, signal?: AbortSignal): Promise<NormalizedSearchResults>;
63
63
  /**
64
64
  * Fetch HTML content from a document URL.
65
65
  */
66
- export declare function getDocumentContent(url: string, timeout?: number): Promise<string>;
66
+ export declare function getDocumentContent(url: string, timeout?: number, signal?: AbortSignal): Promise<string>;
67
67
  /**
68
68
  * Validate dokumentnummer contains only safe characters.
69
69
  * Defense-in-depth check in addition to Zod schema.
@@ -76,6 +76,28 @@ export declare function getDocumentContent(url: string, timeout?: number): Promi
76
76
  * Examples: NOR40052761, BVWG_W123_2000000_1_00
77
77
  */
78
78
  export declare function isValidDokumentnummer(dokumentnummer: string): boolean;
79
+ /**
80
+ * RIS search endpoint used for the fallback search when direct URL fetch fails.
81
+ */
82
+ export type DocumentEndpoint = 'Bundesrecht' | 'Landesrecht' | 'Judikatur' | 'Sonstige' | 'Bezirke';
83
+ /**
84
+ * Routing information for a Dokumentnummer prefix.
85
+ */
86
+ export interface DocumentRoute {
87
+ /** Folder segment in the ris.bka.gv.at/Dokumente/<folder>/ direct URL. */
88
+ urlFolder: string;
89
+ /** Search endpoint used for the fallback search. */
90
+ endpoint: DocumentEndpoint;
91
+ /** `Applikation` API parameter for the fallback search. */
92
+ applikation: string;
93
+ }
94
+ /**
95
+ * Look up the routing information for a Dokumentnummer by its prefix.
96
+ *
97
+ * @param dokumentnummer - The RIS document number (e.g., "NOR12019037")
98
+ * @returns The matching route, or null if the prefix is unknown
99
+ */
100
+ export declare function getDocumentRoute(dokumentnummer: string): DocumentRoute | null;
79
101
  /**
80
102
  * Construct a direct document URL based on the Dokumentnummer prefix.
81
103
  *
@@ -103,7 +125,9 @@ export type DirectDocumentResult = {
103
125
  *
104
126
  * @param dokumentnummer - The RIS document number (e.g., "NOR12019037")
105
127
  * @param timeout - Request timeout in milliseconds
128
+ * @param signal - Caller's cancellation signal; a cancellation is thrown rather
129
+ * than reported as an unsuccessful fetch, so callers do not fall back to a search
106
130
  * @returns Result object with HTML content on success, or error details on failure
107
131
  */
108
- export declare function getDocumentByNumber(dokumentnummer: string, timeout?: number): Promise<DirectDocumentResult>;
132
+ export declare function getDocumentByNumber(dokumentnummer: string, timeout?: number, signal?: AbortSignal): Promise<DirectDocumentResult>;
109
133
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,uBAAuB,EAIxB,MAAM,YAAY,CAAC;AAMpB;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,SAAgB,UAAU,CAAC,EAAE,MAAM,CAAC;gBAExB,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAKjD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW;gBAClC,OAAO,SAAiC;CAIrD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,SAAgB,aAAa,CAAC,EAAE,KAAK,CAAC;gBAE1B,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK;CAKnD;AAcD;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOjD;AA6HD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,SAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CAqChG;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAOrE;AAyDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiB1E;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;;;;;;GASG;AACH,wBAAsB,mBAAmB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,SAAkB,GACxB,OAAO,CAAC,oBAAoB,CAAC,CAkC/B"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,uBAAuB,EAIxB,MAAM,YAAY,CAAC;AAMpB;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,SAAgB,UAAU,CAAC,EAAE,MAAM,CAAC;gBAExB,OAAO,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM;CAKjD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW;gBAClC,OAAO,SAAiC;CAIrD;AAED;;GAEG;AACH,qBAAa,eAAgB,SAAQ,WAAW;IAC9C,SAAgB,aAAa,CAAC,EAAE,KAAK,CAAC;gBAE1B,OAAO,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,KAAK;CAKnD;AAgED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAOjD;AA8JD;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,uBAAuB,CAAC,CAElC;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,MAAM,CAAC,CA0CjB;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,cAAc,EAAE,MAAM,GAAG,OAAO,CAOrE;AAED;;GAEG;AACH,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,CAAC;AAEpG;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,2DAA2D;IAC3D,WAAW,EAAE,MAAM,CAAC;CACrB;AAwED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,cAAc,EAAE,MAAM,GAAG,aAAa,GAAG,IAAI,CAO7E;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,cAAc,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY1E;AAED;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAC5B;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,GAC5C;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAE3D;;;;;;;;;;;GAWG;AACH,wBAAsB,mBAAmB,CACvC,cAAc,EAAE,MAAM,EACtB,OAAO,SAAkB,EACzB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAwC/B"}
package/dist/client.js CHANGED
@@ -46,6 +46,47 @@ export class RISParsingError extends RISAPIError {
46
46
  // =============================================================================
47
47
  const BASE_URL = 'https://data.bka.gv.at/ris/api/v2.6/';
48
48
  const DEFAULT_TIMEOUT = 30000; // 30 seconds in milliseconds
49
+ /** HTTP status codes treated as transient and worth a single retry. */
50
+ const TRANSIENT_STATUS_CODES = new Set([502, 503, 504]);
51
+ /** Delay before the single retry of a transient failure. */
52
+ const RETRY_DELAY_MS = 500;
53
+ /**
54
+ * Resolve after the given number of milliseconds, or reject as soon as the
55
+ * caller cancels — a cancelled request should not sit out the retry backoff.
56
+ */
57
+ function delay(ms, signal) {
58
+ return new Promise((resolve, reject) => {
59
+ if (signal?.aborted) {
60
+ reject(signal.reason);
61
+ return;
62
+ }
63
+ const timeoutId = setTimeout(resolve, ms);
64
+ signal?.addEventListener('abort', () => {
65
+ clearTimeout(timeoutId);
66
+ reject(signal.reason);
67
+ }, { once: true });
68
+ });
69
+ }
70
+ /**
71
+ * Combine the per-request timeout with the caller's cancellation signal so the
72
+ * fetch aborts on whichever fires first.
73
+ */
74
+ function withTimeoutSignal(timeoutSignal, callerSignal) {
75
+ return callerSignal ? AbortSignal.any([timeoutSignal, callerSignal]) : timeoutSignal;
76
+ }
77
+ /**
78
+ * Determine whether an error is transient (a gateway error or timeout) and
79
+ * therefore worth retrying once.
80
+ */
81
+ function isTransientError(error) {
82
+ if (error instanceof RISTimeoutError) {
83
+ return true;
84
+ }
85
+ if (error instanceof RISAPIError && error.statusCode !== undefined) {
86
+ return TRANSIENT_STATUS_CODES.has(error.statusCode);
87
+ }
88
+ return false;
89
+ }
49
90
  /**
50
91
  * Allowed hostnames for document content fetching (SSRF protection).
51
92
  */
@@ -123,20 +164,18 @@ function extractSearchResults(parsedResponse) {
123
164
  };
124
165
  }
125
166
  /**
126
- * Make a request to the RIS API.
167
+ * Perform a single request attempt against the RIS API.
127
168
  */
128
- async function request(endpoint, params, timeout = DEFAULT_TIMEOUT) {
129
- const url = new URL(endpoint, BASE_URL);
130
- url.search = buildParams(params).toString();
169
+ async function requestOnce(url, endpoint, timeout, signal) {
131
170
  const controller = new AbortController();
132
171
  const timeoutId = setTimeout(() => controller.abort(), timeout);
133
172
  try {
134
- const response = await fetch(url.toString(), {
173
+ const response = await fetch(url, {
135
174
  method: 'GET',
136
175
  headers: {
137
176
  Accept: 'application/json',
138
177
  },
139
- signal: controller.signal,
178
+ signal: withTimeoutSignal(controller.signal, signal),
140
179
  });
141
180
  clearTimeout(timeoutId);
142
181
  if (!response.ok) {
@@ -149,6 +188,12 @@ async function request(endpoint, params, timeout = DEFAULT_TIMEOUT) {
149
188
  }
150
189
  catch (e) {
151
190
  clearTimeout(timeoutId);
191
+ // The combined signal makes a timeout and a caller cancellation look alike
192
+ // to fetch, so the signals decide: a cancelled request is not an upstream
193
+ // failure and must not be dressed up as one.
194
+ if (signal?.aborted) {
195
+ throw e;
196
+ }
152
197
  if (e instanceof RISAPIError || e instanceof RISParsingError) {
153
198
  throw e;
154
199
  }
@@ -161,58 +206,80 @@ async function request(endpoint, params, timeout = DEFAULT_TIMEOUT) {
161
206
  throw new RISAPIError(`Request failed for ${endpoint}: ${String(e)}`);
162
207
  }
163
208
  }
209
+ /**
210
+ * Make a request to the RIS API, retrying once on transient failures
211
+ * (HTTP 502/503/504 and timeouts) after a short delay.
212
+ */
213
+ async function request(endpoint, params, timeout = DEFAULT_TIMEOUT, signal) {
214
+ const url = new URL(endpoint, BASE_URL);
215
+ url.search = buildParams(params).toString();
216
+ const urlString = url.toString();
217
+ try {
218
+ return await requestOnce(urlString, endpoint, timeout, signal);
219
+ }
220
+ catch (e) {
221
+ if (!isTransientError(e)) {
222
+ throw e;
223
+ }
224
+ // A cancelled request is not a transient upstream error: `delay` rejects
225
+ // straight away if the caller has cancelled, or as soon as it does, so the
226
+ // second attempt never starts.
227
+ await delay(RETRY_DELAY_MS, signal);
228
+ return requestOnce(urlString, endpoint, timeout, signal);
229
+ }
230
+ }
164
231
  /**
165
232
  * Search federal law (Bundesrecht).
166
233
  */
167
- export async function searchBundesrecht(params, timeout = DEFAULT_TIMEOUT) {
168
- return request('Bundesrecht', params, timeout);
234
+ export async function searchBundesrecht(params, timeout = DEFAULT_TIMEOUT, signal) {
235
+ return request('Bundesrecht', params, timeout, signal);
169
236
  }
170
237
  /**
171
238
  * Search state/provincial law (Landesrecht).
172
239
  */
173
- export async function searchLandesrecht(params, timeout = DEFAULT_TIMEOUT) {
174
- return request('Landesrecht', params, timeout);
240
+ export async function searchLandesrecht(params, timeout = DEFAULT_TIMEOUT, signal) {
241
+ return request('Landesrecht', params, timeout, signal);
175
242
  }
176
243
  /**
177
244
  * Search case law/jurisprudence (Judikatur).
178
245
  */
179
- export async function searchJudikatur(params, timeout = DEFAULT_TIMEOUT) {
180
- return request('Judikatur', params, timeout);
246
+ export async function searchJudikatur(params, timeout = DEFAULT_TIMEOUT, signal) {
247
+ return request('Judikatur', params, timeout, signal);
181
248
  }
182
249
  /**
183
250
  * Search district administrative authorities (Bezirke).
184
251
  */
185
- export async function searchBezirke(params, timeout = DEFAULT_TIMEOUT) {
186
- return request('Bezirke', params, timeout);
252
+ export async function searchBezirke(params, timeout = DEFAULT_TIMEOUT, signal) {
253
+ return request('Bezirke', params, timeout, signal);
187
254
  }
188
255
  /**
189
256
  * Search municipal law (Gemeinden).
190
257
  */
191
- export async function searchGemeinden(params, timeout = DEFAULT_TIMEOUT) {
192
- return request('Gemeinden', params, timeout);
258
+ export async function searchGemeinden(params, timeout = DEFAULT_TIMEOUT, signal) {
259
+ return request('Gemeinden', params, timeout, signal);
193
260
  }
194
261
  /**
195
262
  * Search miscellaneous legal collections (Sonstige).
196
263
  */
197
- export async function searchSonstige(params, timeout = DEFAULT_TIMEOUT) {
198
- return request('Sonstige', params, timeout);
264
+ export async function searchSonstige(params, timeout = DEFAULT_TIMEOUT, signal) {
265
+ return request('Sonstige', params, timeout, signal);
199
266
  }
200
267
  /**
201
268
  * Search document change history (History).
202
269
  */
203
- export async function searchHistory(params, timeout = DEFAULT_TIMEOUT) {
204
- return request('History', params, timeout);
270
+ export async function searchHistory(params, timeout = DEFAULT_TIMEOUT, signal) {
271
+ return request('History', params, timeout, signal);
205
272
  }
206
273
  /**
207
274
  * Fetch HTML content from a document URL.
208
275
  */
209
- export async function getDocumentContent(url, timeout = DEFAULT_TIMEOUT) {
276
+ export async function getDocumentContent(url, timeout = DEFAULT_TIMEOUT, signal) {
210
277
  const controller = new AbortController();
211
278
  const timeoutId = setTimeout(() => controller.abort(), timeout);
212
279
  try {
213
280
  const response = await fetch(url, {
214
281
  method: 'GET',
215
- signal: controller.signal,
282
+ signal: withTimeoutSignal(controller.signal, signal),
216
283
  });
217
284
  clearTimeout(timeoutId);
218
285
  if (!response.ok) {
@@ -223,6 +290,10 @@ export async function getDocumentContent(url, timeout = DEFAULT_TIMEOUT) {
223
290
  }
224
291
  catch (e) {
225
292
  clearTimeout(timeoutId);
293
+ // See requestOnce: only the signals distinguish a cancellation from a timeout.
294
+ if (signal?.aborted) {
295
+ throw e;
296
+ }
226
297
  if (e instanceof RISAPIError) {
227
298
  throw e;
228
299
  }
@@ -258,52 +329,83 @@ export function isValidDokumentnummer(dokumentnummer) {
258
329
  return /^[A-Z][A-Z0-9_]+$/.test(dokumentnummer);
259
330
  }
260
331
  /**
261
- * URL patterns for different document types based on Dokumentnummer prefix.
262
- * The {dokumentnummer} placeholder will be replaced with the actual document number.
332
+ * Single source of truth mapping Dokumentnummer prefixes to their direct-URL
333
+ * folder and fallback-search routing. Used by both {@link constructDocumentUrl}
334
+ * (direct URL) and the ris_dokument fallback search (via {@link getDocumentRoute}).
335
+ * Prefixes are matched longest-first.
336
+ *
337
+ * The Judikatur document-ID scheme is `J<court><R|T>` where R = Rechtssatz and
338
+ * T = Entscheidungstext: JJ = Justiz, JW = VwGH, JF = VfGH, JU = UVS.
339
+ *
340
+ * Folder segments and endpoints verified against RIS API v2.6
341
+ * (data.bka.gv.at / ris.bka.gv.at) on 2026-07-03.
263
342
  */
264
- const DOCUMENT_URL_PATTERNS = {
265
- // Bundesrecht (Federal Law)
266
- NOR: 'https://ris.bka.gv.at/Dokumente/Bundesnormen/{dokumentnummer}/{dokumentnummer}.html',
267
- // Landesrecht (State Law) - one prefix per state
268
- LBG: 'https://ris.bka.gv.at/Dokumente/LrBgld/{dokumentnummer}/{dokumentnummer}.html',
269
- LKT: 'https://ris.bka.gv.at/Dokumente/LrK/{dokumentnummer}/{dokumentnummer}.html',
270
- LNO: 'https://ris.bka.gv.at/Dokumente/LrNO/{dokumentnummer}/{dokumentnummer}.html',
271
- LOO: 'https://ris.bka.gv.at/Dokumente/LrOO/{dokumentnummer}/{dokumentnummer}.html',
272
- LSB: 'https://ris.bka.gv.at/Dokumente/LrSbg/{dokumentnummer}/{dokumentnummer}.html',
273
- LST: 'https://ris.bka.gv.at/Dokumente/LrStmk/{dokumentnummer}/{dokumentnummer}.html',
274
- LTI: 'https://ris.bka.gv.at/Dokumente/LrT/{dokumentnummer}/{dokumentnummer}.html',
275
- LVB: 'https://ris.bka.gv.at/Dokumente/LrVbg/{dokumentnummer}/{dokumentnummer}.html',
276
- LWI: 'https://ris.bka.gv.at/Dokumente/LrW/{dokumentnummer}/{dokumentnummer}.html',
277
- // Judikatur (Case Law)
278
- JWR: 'https://ris.bka.gv.at/Dokumente/Vwgh/{dokumentnummer}/{dokumentnummer}.html',
279
- JFR: 'https://ris.bka.gv.at/Dokumente/Vfgh/{dokumentnummer}/{dokumentnummer}.html',
280
- JFT: 'https://ris.bka.gv.at/Dokumente/Vfgh/{dokumentnummer}/{dokumentnummer}.html',
281
- JWT: 'https://ris.bka.gv.at/Dokumente/Justiz/{dokumentnummer}/{dokumentnummer}.html',
282
- JJR: 'https://ris.bka.gv.at/Dokumente/Justiz/{dokumentnummer}/{dokumentnummer}.html',
283
- BVWG: 'https://ris.bka.gv.at/Dokumente/Bvwg/{dokumentnummer}/{dokumentnummer}.html',
284
- LVWG: 'https://ris.bka.gv.at/Dokumente/Lvwg/{dokumentnummer}/{dokumentnummer}.html',
285
- DSB: 'https://ris.bka.gv.at/Dokumente/Dsk/{dokumentnummer}/{dokumentnummer}.html',
286
- GBK: 'https://ris.bka.gv.at/Dokumente/Gbk/{dokumentnummer}/{dokumentnummer}.html',
287
- PVAK: 'https://ris.bka.gv.at/Dokumente/Pvak/{dokumentnummer}/{dokumentnummer}.html',
288
- ASYLGH: 'https://ris.bka.gv.at/Dokumente/AsylGH/{dokumentnummer}/{dokumentnummer}.html',
289
- // Bundesgesetzblätter (Federal Law Gazettes)
290
- BGBLA: 'https://ris.bka.gv.at/Dokumente/BgblAuth/{dokumentnummer}/{dokumentnummer}.html',
291
- BGBL: 'https://ris.bka.gv.at/Dokumente/BgblAlt/{dokumentnummer}/{dokumentnummer}.html',
292
- BGBLPDF: 'https://ris.bka.gv.at/Dokumente/BgblPdf/{dokumentnummer}/{dokumentnummer}.html',
293
- // Regierungsvorlagen (Government Bills)
294
- REGV: 'https://ris.bka.gv.at/Dokumente/RegV/{dokumentnummer}/{dokumentnummer}.html',
295
- // Bezirke (District Administrative Authorities)
296
- BVB: 'https://ris.bka.gv.at/Dokumente/Bvb/{dokumentnummer}/{dokumentnummer}.html',
297
- // Verordnungsblätter (State Ordinance Gazettes)
298
- VBL: 'https://ris.bka.gv.at/Dokumente/Vbl/{dokumentnummer}/{dokumentnummer}.html',
299
- // Sonstige (Miscellaneous)
300
- MRP: 'https://ris.bka.gv.at/Dokumente/Mrp/{dokumentnummer}/{dokumentnummer}.html',
301
- ERL: 'https://ris.bka.gv.at/Dokumente/Erlaesse/{dokumentnummer}/{dokumentnummer}.html',
302
- PRUEF: 'https://ris.bka.gv.at/Dokumente/PruefGewO/{dokumentnummer}/{dokumentnummer}.html',
303
- AVSV: 'https://ris.bka.gv.at/Dokumente/Avsv/{dokumentnummer}/{dokumentnummer}.html',
304
- SPG: 'https://ris.bka.gv.at/Dokumente/Spg/{dokumentnummer}/{dokumentnummer}.html',
305
- KMGER: 'https://ris.bka.gv.at/Dokumente/KmGer/{dokumentnummer}/{dokumentnummer}.html',
343
+ const DOCUMENT_ROUTES = {
344
+ // --- Bundesrecht (Federal Law) ---
345
+ NOR: { urlFolder: 'Bundesnormen', endpoint: 'Bundesrecht', applikation: 'BrKons' },
346
+ BGBLPDF: { urlFolder: 'BgblPdf', endpoint: 'Bundesrecht', applikation: 'BgblPdf' },
347
+ BGBLA: { urlFolder: 'BgblAuth', endpoint: 'Bundesrecht', applikation: 'BgblAuth' },
348
+ BGBL: { urlFolder: 'BgblAlt', endpoint: 'Bundesrecht', applikation: 'BgblAlt' },
349
+ REGV: { urlFolder: 'RegV', endpoint: 'Bundesrecht', applikation: 'RegV' },
350
+ // --- Landesrecht (State Law) — one prefix per state ---
351
+ LBG: { urlFolder: 'LrBgld', endpoint: 'Landesrecht', applikation: 'LrKons' },
352
+ LKT: { urlFolder: 'LrK', endpoint: 'Landesrecht', applikation: 'LrKons' },
353
+ LNO: { urlFolder: 'LrNO', endpoint: 'Landesrecht', applikation: 'LrKons' },
354
+ LOO: { urlFolder: 'LrOO', endpoint: 'Landesrecht', applikation: 'LrKons' },
355
+ LSB: { urlFolder: 'LrSbg', endpoint: 'Landesrecht', applikation: 'LrKons' },
356
+ LST: { urlFolder: 'LrStmk', endpoint: 'Landesrecht', applikation: 'LrKons' },
357
+ LTI: { urlFolder: 'LrT', endpoint: 'Landesrecht', applikation: 'LrKons' },
358
+ LVB: { urlFolder: 'LrVbg', endpoint: 'Landesrecht', applikation: 'LrKons' },
359
+ LWI: { urlFolder: 'LrW', endpoint: 'Landesrecht', applikation: 'LrKons' },
360
+ VBL: { urlFolder: 'Vbl', endpoint: 'Landesrecht', applikation: 'Vbl' },
361
+ // --- Judikatur (Case Law) ---
362
+ JWR: { urlFolder: 'Vwgh', endpoint: 'Judikatur', applikation: 'Vwgh' },
363
+ JWT: { urlFolder: 'Vwgh', endpoint: 'Judikatur', applikation: 'Vwgh' },
364
+ JFR: { urlFolder: 'Vfgh', endpoint: 'Judikatur', applikation: 'Vfgh' },
365
+ JFT: { urlFolder: 'Vfgh', endpoint: 'Judikatur', applikation: 'Vfgh' },
366
+ JJR: { urlFolder: 'Justiz', endpoint: 'Judikatur', applikation: 'Justiz' },
367
+ JJT: { urlFolder: 'Justiz', endpoint: 'Judikatur', applikation: 'Justiz' },
368
+ BVWG: { urlFolder: 'Bvwg', endpoint: 'Judikatur', applikation: 'Bvwg' },
369
+ LVWG: { urlFolder: 'Lvwg', endpoint: 'Judikatur', applikation: 'Lvwg' },
370
+ DSB: { urlFolder: 'Dsk', endpoint: 'Judikatur', applikation: 'Dsk' },
371
+ PDK: { urlFolder: 'Dsk', endpoint: 'Judikatur', applikation: 'Dsk' },
372
+ GBK: { urlFolder: 'Gbk', endpoint: 'Judikatur', applikation: 'Gbk' },
373
+ PVAB: { urlFolder: 'Pvak', endpoint: 'Judikatur', applikation: 'Pvak' },
374
+ DKT: { urlFolder: 'Dok', endpoint: 'Judikatur', applikation: 'Dok' },
375
+ VERG: { urlFolder: 'Verg', endpoint: 'Judikatur', applikation: 'Verg' },
376
+ JUR: { urlFolder: 'Uvs', endpoint: 'Judikatur', applikation: 'Uvs' },
377
+ JUT: { urlFolder: 'Uvs', endpoint: 'Judikatur', applikation: 'Uvs' },
378
+ UBAS: { urlFolder: 'Ubas', endpoint: 'Judikatur', applikation: 'Ubas' },
379
+ UMSE: { urlFolder: 'Umse', endpoint: 'Judikatur', applikation: 'Umse' },
380
+ BKS: { urlFolder: 'Bks', endpoint: 'Judikatur', applikation: 'Bks' },
381
+ ASYLGH: { urlFolder: 'AsylGH', endpoint: 'Judikatur', applikation: 'AsylGH' },
382
+ NL: { urlFolder: 'Normenliste', endpoint: 'Judikatur', applikation: 'Normenliste' },
383
+ // --- Sonstige (Miscellaneous) ---
384
+ MRP: { urlFolder: 'Mrp', endpoint: 'Sonstige', applikation: 'Mrp' },
385
+ ERL: { urlFolder: 'Erlaesse', endpoint: 'Sonstige', applikation: 'Erlaesse' },
386
+ PRUEF: { urlFolder: 'PruefGewO', endpoint: 'Sonstige', applikation: 'PruefGewO' },
387
+ AVSV: { urlFolder: 'Avsv', endpoint: 'Sonstige', applikation: 'Avsv' },
388
+ SPG: { urlFolder: 'Spg', endpoint: 'Sonstige', applikation: 'Spg' },
389
+ KMGER: { urlFolder: 'KmGer', endpoint: 'Sonstige', applikation: 'KmGer' },
390
+ // --- Bezirke (District Administrative Authorities) ---
391
+ BVB: { urlFolder: 'Bvb', endpoint: 'Bezirke', applikation: 'Bvb' },
306
392
  };
393
+ /** Prefixes sorted longest-first so specific prefixes win over shorter ones. */
394
+ const DOCUMENT_ROUTE_PREFIXES = Object.keys(DOCUMENT_ROUTES).sort((a, b) => b.length - a.length);
395
+ /**
396
+ * Look up the routing information for a Dokumentnummer by its prefix.
397
+ *
398
+ * @param dokumentnummer - The RIS document number (e.g., "NOR12019037")
399
+ * @returns The matching route, or null if the prefix is unknown
400
+ */
401
+ export function getDocumentRoute(dokumentnummer) {
402
+ for (const prefix of DOCUMENT_ROUTE_PREFIXES) {
403
+ if (dokumentnummer.startsWith(prefix)) {
404
+ return DOCUMENT_ROUTES[prefix];
405
+ }
406
+ }
407
+ return null;
408
+ }
307
409
  /**
308
410
  * Construct a direct document URL based on the Dokumentnummer prefix.
309
411
  *
@@ -315,15 +417,11 @@ export function constructDocumentUrl(dokumentnummer) {
315
417
  if (!isValidDokumentnummer(dokumentnummer)) {
316
418
  return null;
317
419
  }
318
- // Find matching prefix (check longer prefixes first to avoid false matches)
319
- const prefixes = Object.keys(DOCUMENT_URL_PATTERNS).sort((a, b) => b.length - a.length);
320
- for (const prefix of prefixes) {
321
- if (dokumentnummer.startsWith(prefix)) {
322
- const pattern = DOCUMENT_URL_PATTERNS[prefix];
323
- return pattern.replace(/{dokumentnummer}/g, dokumentnummer);
324
- }
420
+ const route = getDocumentRoute(dokumentnummer);
421
+ if (!route) {
422
+ return null;
325
423
  }
326
- return null;
424
+ return `https://ris.bka.gv.at/Dokumente/${route.urlFolder}/${dokumentnummer}/${dokumentnummer}.html`;
327
425
  }
328
426
  /**
329
427
  * Attempt to fetch a document directly by its Dokumentnummer using URL construction.
@@ -333,9 +431,11 @@ export function constructDocumentUrl(dokumentnummer) {
333
431
  *
334
432
  * @param dokumentnummer - The RIS document number (e.g., "NOR12019037")
335
433
  * @param timeout - Request timeout in milliseconds
434
+ * @param signal - Caller's cancellation signal; a cancellation is thrown rather
435
+ * than reported as an unsuccessful fetch, so callers do not fall back to a search
336
436
  * @returns Result object with HTML content on success, or error details on failure
337
437
  */
338
- export async function getDocumentByNumber(dokumentnummer, timeout = DEFAULT_TIMEOUT) {
438
+ export async function getDocumentByNumber(dokumentnummer, timeout = DEFAULT_TIMEOUT, signal) {
339
439
  // Validate dokumentnummer before any URL operations
340
440
  if (!isValidDokumentnummer(dokumentnummer)) {
341
441
  return {
@@ -351,10 +451,15 @@ export async function getDocumentByNumber(dokumentnummer, timeout = DEFAULT_TIME
351
451
  };
352
452
  }
353
453
  try {
354
- const html = await getDocumentContent(url, timeout);
454
+ const html = await getDocumentContent(url, timeout, signal);
355
455
  return { success: true, html, url };
356
456
  }
357
457
  catch (e) {
458
+ // Reporting a cancellation as a failed direct fetch would send the caller
459
+ // into its fallback search — a second round-trip nobody is waiting for.
460
+ if (signal?.aborted) {
461
+ throw e;
462
+ }
358
463
  if (e instanceof RISAPIError) {
359
464
  return {
360
465
  success: false,