@fin.cx/opendata 3.5.1 → 4.0.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 (87) hide show
  1. package/{npmextra.json → .smartconfig.json} +24 -18
  2. package/dist_ts/00_commitinfo_data.js +3 -3
  3. package/dist_ts/handelsregister/classes.handelsregisterclient.d.ts +64 -0
  4. package/dist_ts/handelsregister/classes.handelsregisterclient.js +397 -0
  5. package/dist_ts/handelsregister/classes.handelsregistererror.d.ts +56 -0
  6. package/dist_ts/handelsregister/classes.handelsregistererror.js +37 -0
  7. package/dist_ts/handelsregister/classes.portalsession.d.ts +46 -0
  8. package/dist_ts/handelsregister/classes.portalsession.js +160 -0
  9. package/dist_ts/handelsregister/classes.slidingwindowgate.d.ts +32 -0
  10. package/dist_ts/handelsregister/classes.slidingwindowgate.js +55 -0
  11. package/dist_ts/handelsregister/constants.d.ts +13 -0
  12. package/dist_ts/handelsregister/constants.js +19 -0
  13. package/dist_ts/handelsregister/data.codelists.d.ts +29 -0
  14. package/dist_ts/handelsregister/data.codelists.js +616 -0
  15. package/dist_ts/handelsregister/forms.d.ts +27 -0
  16. package/dist_ts/handelsregister/forms.js +77 -0
  17. package/dist_ts/handelsregister/html.d.ts +26 -0
  18. package/dist_ts/handelsregister/html.js +82 -0
  19. package/dist_ts/handelsregister/index.d.ts +9 -0
  20. package/dist_ts/handelsregister/index.js +10 -0
  21. package/dist_ts/handelsregister/interfaces.d.ts +240 -0
  22. package/dist_ts/handelsregister/interfaces.js +2 -0
  23. package/dist_ts/handelsregister/parse.searchresults.d.ts +35 -0
  24. package/dist_ts/handelsregister/parse.searchresults.js +175 -0
  25. package/dist_ts/handelsregister/parse.structuredcontent.d.ts +6 -0
  26. package/dist_ts/handelsregister/parse.structuredcontent.js +306 -0
  27. package/dist_ts/handelsregister/plugins.d.ts +4 -0
  28. package/dist_ts/handelsregister/plugins.js +8 -0
  29. package/dist_ts/handelsregister/references.d.ts +34 -0
  30. package/dist_ts/handelsregister/references.js +76 -0
  31. package/dist_ts/index.d.ts +0 -1
  32. package/dist_ts/index.js +1 -2
  33. package/dist_ts/plugins.d.ts +2 -14
  34. package/dist_ts/plugins.js +8 -16
  35. package/dist_ts/stocks/classes.baseproviderservice.js +8 -8
  36. package/dist_ts/stocks/classes.fundamentalsservice.js +9 -9
  37. package/dist_ts/stocks/classes.stockdataservice.js +23 -23
  38. package/dist_ts/stocks/classes.stockservice.js +9 -9
  39. package/dist_ts/stocks/providers/provider.coingecko.d.ts +1 -1
  40. package/dist_ts/stocks/providers/provider.coingecko.js +45 -39
  41. package/dist_ts/stocks/providers/provider.marketstack.d.ts +3 -3
  42. package/dist_ts/stocks/providers/provider.marketstack.js +20 -16
  43. package/dist_ts/stocks/providers/provider.secedgar.d.ts +1 -1
  44. package/dist_ts/stocks/providers/provider.secedgar.js +22 -22
  45. package/dist_ts/stocks/providers/provider.yahoo.d.ts +2 -2
  46. package/dist_ts/stocks/providers/provider.yahoo.js +14 -13
  47. package/package.json +30 -31
  48. package/readme.md +327 -676
  49. package/ts/00_commitinfo_data.ts +2 -2
  50. package/ts/handelsregister/classes.handelsregisterclient.ts +474 -0
  51. package/ts/handelsregister/classes.handelsregistererror.ts +82 -0
  52. package/ts/handelsregister/classes.portalsession.ts +191 -0
  53. package/ts/handelsregister/classes.slidingwindowgate.ts +74 -0
  54. package/ts/handelsregister/constants.ts +30 -0
  55. package/ts/handelsregister/data.codelists.ts +640 -0
  56. package/ts/handelsregister/forms.ts +99 -0
  57. package/ts/handelsregister/html.ts +113 -0
  58. package/ts/handelsregister/index.ts +27 -0
  59. package/ts/handelsregister/interfaces.ts +272 -0
  60. package/ts/handelsregister/parse.searchresults.ts +241 -0
  61. package/ts/handelsregister/parse.structuredcontent.ts +338 -0
  62. package/ts/handelsregister/plugins.ts +15 -0
  63. package/ts/handelsregister/references.ts +115 -0
  64. package/ts/index.ts +0 -1
  65. package/ts/plugins.ts +9 -29
  66. package/ts/stocks/classes.fundamentalsservice.ts +1 -1
  67. package/ts/stocks/classes.stockdataservice.ts +17 -7
  68. package/ts/stocks/classes.stockservice.ts +2 -2
  69. package/ts/stocks/providers/provider.marketstack.ts +9 -7
  70. package/ts/stocks/providers/provider.secedgar.ts +2 -2
  71. package/ts/stocks/providers/provider.yahoo.ts +3 -3
  72. package/dist_ts/classes.businessrecord.d.ts +0 -38
  73. package/dist_ts/classes.businessrecord.js +0 -47
  74. package/dist_ts/classes.germanbusinessdata.d.ts +0 -9
  75. package/dist_ts/classes.germanbusinessdata.js +0 -78
  76. package/dist_ts/classes.handelsregister.d.ts +0 -132
  77. package/dist_ts/classes.handelsregister.js +0 -288
  78. package/dist_ts/classes.jsonldata.d.ts +0 -43
  79. package/dist_ts/classes.jsonldata.js +0 -65
  80. package/dist_ts/classes.main.opendata.d.ts +0 -24
  81. package/dist_ts/classes.main.opendata.js +0 -58
  82. package/readme.hints.md +0 -89
  83. package/readme.plan.md +0 -193
  84. package/ts/classes.businessrecord.ts +0 -67
  85. package/ts/classes.handelsregister.ts +0 -360
  86. package/ts/classes.jsonldata.ts +0 -113
  87. package/ts/classes.main.opendata.ts +0 -87
@@ -0,0 +1,191 @@
1
+ import { HandelsregisterError, type THandelsregisterStep } from './classes.handelsregistererror.js';
2
+
3
+ export interface IPortalSessionSettings {
4
+ portalUrl: string;
5
+ userAgent: string;
6
+ requestTimeoutMs: number;
7
+ maxResponseBytes: number;
8
+ signal?: AbortSignal;
9
+ }
10
+
11
+ /** what the caller of a session learns about the progress of its call */
12
+ export interface ICallProgress {
13
+ step?: THandelsregisterStep;
14
+ /** counted requests sent so far */
15
+ countedRequests: number;
16
+ }
17
+
18
+ export interface IPortalResponse {
19
+ /** the URL that answered, after redirects */
20
+ url: string;
21
+ contentType: string;
22
+ body: Uint8Array;
23
+ }
24
+
25
+ const redirectStatuses = new Set([301, 302, 303]);
26
+ const maxRedirects = 3;
27
+
28
+ /**
29
+ * One visit to the register portal: a cookie jar, the fixed language and a limit for every
30
+ * request. It lives for one call of the client and is dropped afterwards, so nothing is shared
31
+ * between calls or callers.
32
+ */
33
+ export class PortalSession {
34
+ private cookies = new Map<string, string>();
35
+ private origin: string;
36
+ /** the signal of the request in flight */
37
+ private currentSignal?: AbortSignal;
38
+
39
+ constructor(private settings: IPortalSessionSettings, private progress: ICallProgress) {
40
+ this.origin = new URL(settings.portalUrl).origin;
41
+ }
42
+
43
+ /** resolves a path or a relative action against the portal or a page */
44
+ public resolve(targetArg: string, baseArg: string = this.origin): string {
45
+ return new URL(targetArg, baseArg).toString();
46
+ }
47
+
48
+ public async get(urlArg: string): Promise<IPortalResponse> {
49
+ return this.request('GET', urlArg);
50
+ }
51
+
52
+ /** submits a form as `application/x-www-form-urlencoded` */
53
+ public async post(urlArg: string, fieldsArg: Array<[string, string]>): Promise<IPortalResponse> {
54
+ return this.request('POST', urlArg, new URLSearchParams(fieldsArg).toString());
55
+ }
56
+
57
+ private fail(
58
+ codeArg: 'aborted' | 'timeout' | 'unreachable' | 'refused' | 'too_large' | 'unexpected_response',
59
+ messageArg: string,
60
+ extraArg: { status?: number; cause?: unknown } = {}
61
+ ): HandelsregisterError {
62
+ return new HandelsregisterError(codeArg, messageArg, {
63
+ ...extraArg,
64
+ step: this.progress.step,
65
+ countedRequests: this.progress.countedRequests,
66
+ });
67
+ }
68
+
69
+ private async request(methodArg: 'GET' | 'POST', urlArg: string, bodyArg?: string): Promise<IPortalResponse> {
70
+ let method = methodArg;
71
+ let url = urlArg;
72
+ let body = bodyArg;
73
+ for (let redirects = 0; ; redirects++) {
74
+ if (new URL(url).origin !== this.origin) {
75
+ throw this.fail('unexpected_response', 'The portal pointed to another site.');
76
+ }
77
+ const response = await this.send(method, url, body);
78
+ if (redirectStatuses.has(response.status)) {
79
+ const location = response.headers.get('location');
80
+ await response.body?.cancel();
81
+ if (!location || redirects >= maxRedirects) {
82
+ throw this.fail('unexpected_response', 'The portal redirected in a way this version does not follow.');
83
+ }
84
+ url = new URL(location, url).toString();
85
+ method = 'GET';
86
+ body = undefined;
87
+ continue;
88
+ }
89
+ if (response.status !== 200) {
90
+ await response.body?.cancel();
91
+ throw this.fail('refused', `The register portal answered with HTTP status ${response.status}.`, {
92
+ status: response.status,
93
+ });
94
+ }
95
+ return {
96
+ url,
97
+ contentType: response.headers.get('content-type') ?? '',
98
+ body: await this.readBody(response),
99
+ };
100
+ }
101
+ }
102
+
103
+ private async send(methodArg: 'GET' | 'POST', urlArg: string, bodyArg?: string): Promise<Response> {
104
+ if (this.settings.signal?.aborted) {
105
+ throw this.fail('aborted', 'The call was aborted.', { cause: this.settings.signal.reason });
106
+ }
107
+ const headers: Record<string, string> = {
108
+ 'User-Agent': this.settings.userAgent,
109
+ Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
110
+ 'Accept-Language': 'de-DE,de;q=0.9',
111
+ };
112
+ if (this.cookies.size > 0) {
113
+ headers.Cookie = [...this.cookies].map(([name, value]) => `${name}=${value}`).join('; ');
114
+ }
115
+ if (bodyArg !== undefined) {
116
+ headers['Content-Type'] = 'application/x-www-form-urlencoded';
117
+ }
118
+ let response: Response;
119
+ try {
120
+ response = await fetch(urlArg, {
121
+ method: methodArg,
122
+ headers,
123
+ body: bodyArg,
124
+ redirect: 'manual',
125
+ signal: this.requestSignal(),
126
+ });
127
+ } catch (error) {
128
+ throw this.failureOf(error);
129
+ }
130
+ for (const cookie of response.headers.getSetCookie()) {
131
+ const pair = cookie.split(';', 1)[0];
132
+ const separator = pair.indexOf('=');
133
+ if (separator > 0) {
134
+ this.cookies.set(pair.slice(0, separator).trim(), pair.slice(separator + 1).trim());
135
+ }
136
+ }
137
+ return response;
138
+ }
139
+
140
+ /** a signal for one request: the caller's signal and the request's own time limit */
141
+ private requestSignal(): AbortSignal {
142
+ const timeout = AbortSignal.timeout(this.settings.requestTimeoutMs);
143
+ this.currentSignal = this.settings.signal ? AbortSignal.any([this.settings.signal, timeout]) : timeout;
144
+ return this.currentSignal;
145
+ }
146
+
147
+ private failureOf(errorArg: unknown): HandelsregisterError {
148
+ if (errorArg instanceof HandelsregisterError) {
149
+ return errorArg;
150
+ }
151
+ if (this.settings.signal?.aborted) {
152
+ return this.fail('aborted', 'The call was aborted.', { cause: this.settings.signal.reason });
153
+ }
154
+ if (this.currentSignal?.aborted) {
155
+ return this.fail('timeout', 'The register portal did not answer in time.', { cause: errorArg });
156
+ }
157
+ return this.fail('unreachable', 'The register portal could not be reached.', { cause: errorArg });
158
+ }
159
+
160
+ private async readBody(responseArg: Response): Promise<Uint8Array> {
161
+ if (!responseArg.body) {
162
+ return new Uint8Array(0);
163
+ }
164
+ const reader = responseArg.body.getReader();
165
+ const chunks: Uint8Array[] = [];
166
+ let total = 0;
167
+ try {
168
+ for (;;) {
169
+ const chunk = await reader.read();
170
+ if (chunk.done) {
171
+ break;
172
+ }
173
+ total += chunk.value.byteLength;
174
+ if (total > this.settings.maxResponseBytes) {
175
+ await reader.cancel();
176
+ throw this.fail('too_large', 'An answer of the register portal exceeded the size limit.');
177
+ }
178
+ chunks.push(chunk.value);
179
+ }
180
+ } catch (error) {
181
+ throw this.failureOf(error);
182
+ }
183
+ const body = new Uint8Array(total);
184
+ let offset = 0;
185
+ for (const chunk of chunks) {
186
+ body.set(chunk, offset);
187
+ offset += chunk.byteLength;
188
+ }
189
+ return body;
190
+ }
191
+ }
@@ -0,0 +1,74 @@
1
+ import { HandelsregisterError } from './classes.handelsregistererror.js';
2
+ import { portalRequestsPerHour } from './constants.js';
3
+ import type { IRegisterCountedRequest, IRegisterRequestGate } from './interfaces.js';
4
+
5
+ export interface ISlidingWindowGateOptions {
6
+ /** requests allowed within one window; default 60, the portal's limit */
7
+ limit?: number;
8
+ /** length of the window; default one hour */
9
+ windowMs?: number;
10
+ /** clock; default `Date.now` */
11
+ now?: () => number;
12
+ }
13
+
14
+ /**
15
+ * Holds the portal's limit of 60 searches and retrievals per hour over a sliding window: a request
16
+ * is allowed when fewer than `limit` requests were allowed in the `windowMs` before it. A refusal is
17
+ * a `HandelsregisterError` with the code `rate_limited` and `retryAt`, the moment the oldest request
18
+ * leaves the window.
19
+ *
20
+ * The window lives in this object, so it counts for one process. Share one gate between every
21
+ * client of the installation that reaches the portal from the same address.
22
+ */
23
+ export class SlidingWindowGate implements IRegisterRequestGate {
24
+ public readonly limit: number;
25
+ public readonly windowMs: number;
26
+ private now: () => number;
27
+ /** epoch ms of the allowed requests still inside the window, oldest first */
28
+ private allowed: number[] = [];
29
+
30
+ constructor(optionsArg: ISlidingWindowGateOptions = {}) {
31
+ this.limit = optionsArg.limit ?? portalRequestsPerHour;
32
+ this.windowMs = optionsArg.windowMs ?? 60 * 60 * 1000;
33
+ this.now = optionsArg.now ?? Date.now;
34
+ if (!Number.isInteger(this.limit) || this.limit < 1) {
35
+ throw new RangeError('limit must be a positive integer');
36
+ }
37
+ if (!Number.isFinite(this.windowMs) || this.windowMs <= 0) {
38
+ throw new RangeError('windowMs must be positive');
39
+ }
40
+ }
41
+
42
+ private prune(nowArg: number): void {
43
+ while (this.allowed.length > 0 && this.allowed[0] <= nowArg - this.windowMs) {
44
+ this.allowed.shift();
45
+ }
46
+ }
47
+
48
+ public async acquire(_requestArg: IRegisterCountedRequest): Promise<void> {
49
+ const now = this.now();
50
+ this.prune(now);
51
+ if (this.allowed.length >= this.limit) {
52
+ const retryAt = this.allowed[0] + this.windowMs;
53
+ throw new HandelsregisterError(
54
+ 'rate_limited',
55
+ `The limit of ${this.limit} register requests per window is used up.`,
56
+ { retryAt }
57
+ );
58
+ }
59
+ this.allowed.push(now);
60
+ }
61
+
62
+ /** requests still allowed now */
63
+ public remaining(): number {
64
+ this.prune(this.now());
65
+ return this.limit - this.allowed.length;
66
+ }
67
+
68
+ /** epoch ms from which the next request is allowed; now, when one is allowed already */
69
+ public nextAllowedAt(): number {
70
+ const now = this.now();
71
+ this.prune(now);
72
+ return this.allowed.length < this.limit ? now : this.allowed[this.allowed.length - this.limit] + this.windowMs;
73
+ }
74
+ }
@@ -0,0 +1,30 @@
1
+ import type { IStructuredContentNotice, TRegisterDocument, TRegisterType } from './interfaces.js';
2
+
3
+ export const registerTypes: readonly TRegisterType[] = ['HRA', 'HRB', 'GnR', 'PR', 'VR', 'GsR'];
4
+
5
+ export const registerDocuments: readonly TRegisterDocument[] = ['AD', 'CD', 'HD', 'DK', 'UT', 'VÖ', 'SI'];
6
+
7
+ /** the portal's note on its structured register content, from its start page (2026-09-23) */
8
+ export const structuredContentNotice: IStructuredContentNotice = {
9
+ de:
10
+ 'Soweit in den Ländern strukturierte Informationen zum Abruf zur Verfügung gestellt werden, handelt es sich ' +
11
+ 'um einen unverbindlichen Service. Der Inhalt kann in speziellen Fällen vom Inhalt des aktuellen Abdrucks ' +
12
+ 'abweichen und diesen ggf. nicht vollständig wiedergeben.',
13
+ en:
14
+ 'Where the federal states provide structured information for retrieval, this is a non-binding service. In ' +
15
+ 'special cases its content can differ from the current printout and may not reproduce it completely.',
16
+ };
17
+
18
+ /** the portal's maximum number of hits for one search */
19
+ export const portalMaximumHits = 100;
20
+
21
+ export const defaultPortalUrl = 'https://www.handelsregister.de';
22
+
23
+ export const defaultUserAgent = '@fin.cx/opendata (+https://code.foss.global/fin.cx/opendata)';
24
+
25
+ export const defaultRequestTimeoutMs = 20_000;
26
+
27
+ export const defaultMaxResponseBytes = 5 * 1024 * 1024;
28
+
29
+ /** the portal's terms: at most 60 searches or retrievals of entities per hour */
30
+ export const portalRequestsPerHour = 60;