anz-legislation 1.2.1
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/LICENSE +201 -0
- package/README.md +23 -0
- package/dist/cli.d.ts +6 -0
- package/dist/cli.js +198 -0
- package/dist/client.d.ts +84 -0
- package/dist/client.js +492 -0
- package/dist/commands/batch.d.ts +5 -0
- package/dist/commands/batch.js +121 -0
- package/dist/commands/cache.d.ts +5 -0
- package/dist/commands/cache.js +43 -0
- package/dist/commands/cite.d.ts +5 -0
- package/dist/commands/cite.js +68 -0
- package/dist/commands/config.d.ts +5 -0
- package/dist/commands/config.js +56 -0
- package/dist/commands/export.d.ts +8 -0
- package/dist/commands/export.js +169 -0
- package/dist/commands/generate.d.ts +10 -0
- package/dist/commands/generate.js +320 -0
- package/dist/commands/get.d.ts +5 -0
- package/dist/commands/get.js +99 -0
- package/dist/commands/help.d.ts +13 -0
- package/dist/commands/help.js +298 -0
- package/dist/commands/search.d.ts +5 -0
- package/dist/commands/search.js +96 -0
- package/dist/commands/stream.d.ts +5 -0
- package/dist/commands/stream.js +100 -0
- package/dist/config.d.ts +81 -0
- package/dist/config.js +209 -0
- package/dist/errors.d.ts +108 -0
- package/dist/errors.js +173 -0
- package/dist/mcp/server.d.ts +13 -0
- package/dist/mcp/server.js +428 -0
- package/dist/mcp-cli.d.ts +6 -0
- package/dist/mcp-cli.js +37 -0
- package/dist/models/canonical.d.ts +423 -0
- package/dist/models/canonical.js +92 -0
- package/dist/models/index.d.ts +892 -0
- package/dist/models/index.js +223 -0
- package/dist/output/index.d.ts +34 -0
- package/dist/output/index.js +195 -0
- package/dist/output/legal-metadata-publication.d.ts +18 -0
- package/dist/output/legal-metadata-publication.js +23 -0
- package/dist/providers/canonical-metadata.d.ts +3 -0
- package/dist/providers/canonical-metadata.js +202 -0
- package/dist/providers/commonwealth-provider.d.ts +27 -0
- package/dist/providers/commonwealth-provider.js +81 -0
- package/dist/providers/index.d.ts +20 -0
- package/dist/providers/index.js +27 -0
- package/dist/providers/legislation-provider.d.ts +227 -0
- package/dist/providers/legislation-provider.js +308 -0
- package/dist/providers/nz-provider.d.ts +36 -0
- package/dist/providers/nz-provider.js +130 -0
- package/dist/providers/output-adapters.d.ts +14 -0
- package/dist/providers/output-adapters.js +116 -0
- package/dist/providers/plugin-discovery.d.ts +39 -0
- package/dist/providers/plugin-discovery.js +91 -0
- package/dist/providers/plugin-loader.d.ts +86 -0
- package/dist/providers/plugin-loader.js +219 -0
- package/dist/providers/queensland-provider.d.ts +42 -0
- package/dist/providers/queensland-provider.js +105 -0
- package/dist/utils/api-optimization.d.ts +92 -0
- package/dist/utils/api-optimization.js +276 -0
- package/dist/utils/batch.d.ts +110 -0
- package/dist/utils/batch.js +269 -0
- package/dist/utils/branded-types.d.ts +0 -0
- package/dist/utils/branded-types.js +1 -0
- package/dist/utils/compatibility-matrix.d.ts +89 -0
- package/dist/utils/compatibility-matrix.js +214 -0
- package/dist/utils/config-validator.d.ts +39 -0
- package/dist/utils/config-validator.js +197 -0
- package/dist/utils/env-loader.d.ts +55 -0
- package/dist/utils/env-loader.js +77 -0
- package/dist/utils/health-monitor.d.ts +93 -0
- package/dist/utils/health-monitor.js +209 -0
- package/dist/utils/invocation.d.ts +4 -0
- package/dist/utils/invocation.js +33 -0
- package/dist/utils/logger.d.ts +94 -0
- package/dist/utils/logger.js +220 -0
- package/dist/utils/plugin-marketplace.d.ts +77 -0
- package/dist/utils/plugin-marketplace.js +191 -0
- package/dist/utils/presentation.d.ts +2 -0
- package/dist/utils/presentation.js +32 -0
- package/dist/utils/rate-limiter.d.ts +100 -0
- package/dist/utils/rate-limiter.js +256 -0
- package/dist/utils/scraper-cache.d.ts +115 -0
- package/dist/utils/scraper-cache.js +229 -0
- package/dist/utils/secure-config.d.ts +40 -0
- package/dist/utils/secure-config.js +195 -0
- package/dist/utils/streaming.d.ts +121 -0
- package/dist/utils/streaming.js +333 -0
- package/dist/utils/validation.d.ts +190 -0
- package/dist/utils/validation.js +209 -0
- package/dist/utils/version.d.ts +13 -0
- package/dist/utils/version.js +46 -0
- package/package.json +56 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Marketplace
|
|
3
|
+
*
|
|
4
|
+
* Framework for discovering, installing, and managing plugins.
|
|
5
|
+
* Supports both official and community plugins.
|
|
6
|
+
*/
|
|
7
|
+
export interface PluginInfo {
|
|
8
|
+
name: string;
|
|
9
|
+
version: string;
|
|
10
|
+
description: string;
|
|
11
|
+
author: string;
|
|
12
|
+
type: 'official' | 'community';
|
|
13
|
+
status: 'stable' | 'beta' | 'alpha';
|
|
14
|
+
downloads: number;
|
|
15
|
+
rating?: number;
|
|
16
|
+
repository?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PluginManifest {
|
|
19
|
+
name: string;
|
|
20
|
+
version: string;
|
|
21
|
+
description: string;
|
|
22
|
+
main: string;
|
|
23
|
+
author: string;
|
|
24
|
+
license: string;
|
|
25
|
+
peerDependencies: Record<string, string>;
|
|
26
|
+
pluginType: 'official' | 'community';
|
|
27
|
+
pluginStatus: 'stable' | 'beta' | 'alpha';
|
|
28
|
+
}
|
|
29
|
+
export declare class PluginMarketplace {
|
|
30
|
+
private registryUrl;
|
|
31
|
+
private installedPlugins;
|
|
32
|
+
constructor(registryUrl?: string);
|
|
33
|
+
/**
|
|
34
|
+
* List available plugins
|
|
35
|
+
*/
|
|
36
|
+
list(filter?: {
|
|
37
|
+
type?: 'official' | 'community';
|
|
38
|
+
status?: string;
|
|
39
|
+
}): Promise<PluginInfo[]>;
|
|
40
|
+
/**
|
|
41
|
+
* Install plugin
|
|
42
|
+
*/
|
|
43
|
+
install(name: string, version?: string): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Uninstall plugin
|
|
46
|
+
*/
|
|
47
|
+
uninstall(name: string): Promise<void>;
|
|
48
|
+
/**
|
|
49
|
+
* Update plugin
|
|
50
|
+
*/
|
|
51
|
+
update(name: string): Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Get installed plugins
|
|
54
|
+
*/
|
|
55
|
+
getInstalled(): PluginManifest[];
|
|
56
|
+
/**
|
|
57
|
+
* Register installed plugin
|
|
58
|
+
*/
|
|
59
|
+
registerPlugin(manifest: PluginManifest): void;
|
|
60
|
+
/**
|
|
61
|
+
* Search plugins
|
|
62
|
+
*/
|
|
63
|
+
search(query: string): Promise<PluginInfo[]>;
|
|
64
|
+
/**
|
|
65
|
+
* Get plugin details
|
|
66
|
+
*/
|
|
67
|
+
getDetails(name: string): Promise<PluginInfo | null>;
|
|
68
|
+
/**
|
|
69
|
+
* Submit community plugin
|
|
70
|
+
*/
|
|
71
|
+
submit(manifest: PluginManifest): Promise<void>;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* CLI command helpers
|
|
75
|
+
*/
|
|
76
|
+
export declare function formatPluginList(plugins: PluginInfo[]): string;
|
|
77
|
+
export declare function getGlobalMarketplace(): PluginMarketplace;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Marketplace
|
|
3
|
+
*
|
|
4
|
+
* Framework for discovering, installing, and managing plugins.
|
|
5
|
+
* Supports both official and community plugins.
|
|
6
|
+
*/
|
|
7
|
+
export class PluginMarketplace {
|
|
8
|
+
registryUrl;
|
|
9
|
+
installedPlugins = new Map();
|
|
10
|
+
constructor(registryUrl = 'https://registry.npmjs.org') {
|
|
11
|
+
this.registryUrl = registryUrl;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* List available plugins
|
|
15
|
+
*/
|
|
16
|
+
async list(filter) {
|
|
17
|
+
// In real implementation, fetch from registry
|
|
18
|
+
// For now, return mock data
|
|
19
|
+
const plugins = [
|
|
20
|
+
// Official
|
|
21
|
+
{
|
|
22
|
+
name: '@nz-legislation/queensland',
|
|
23
|
+
version: '1.0.0',
|
|
24
|
+
description: 'Queensland legislation',
|
|
25
|
+
author: 'ANZ Legislation Team',
|
|
26
|
+
type: 'official',
|
|
27
|
+
status: 'stable',
|
|
28
|
+
downloads: 1000,
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: '@nz-legislation/commonwealth',
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
description: 'Commonwealth legislation',
|
|
34
|
+
author: 'ANZ Legislation Team',
|
|
35
|
+
type: 'official',
|
|
36
|
+
status: 'stable',
|
|
37
|
+
downloads: 800,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: '@nz-legislation/nsw',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
description: 'NSW legislation',
|
|
43
|
+
author: 'ANZ Legislation Team',
|
|
44
|
+
type: 'official',
|
|
45
|
+
status: 'stable',
|
|
46
|
+
downloads: 600,
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: '@nz-legislation/victoria',
|
|
50
|
+
version: '1.0.0',
|
|
51
|
+
description: 'Victoria legislation',
|
|
52
|
+
author: 'ANZ Legislation Team',
|
|
53
|
+
type: 'official',
|
|
54
|
+
status: 'stable',
|
|
55
|
+
downloads: 500,
|
|
56
|
+
},
|
|
57
|
+
// Community
|
|
58
|
+
{
|
|
59
|
+
name: '@community/fiji',
|
|
60
|
+
version: '0.1.0',
|
|
61
|
+
description: 'Fiji legislation (beta)',
|
|
62
|
+
author: 'Community',
|
|
63
|
+
type: 'community',
|
|
64
|
+
status: 'beta',
|
|
65
|
+
downloads: 50,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: '@community/samoa',
|
|
69
|
+
version: '0.1.0',
|
|
70
|
+
description: 'Samoa legislation (alpha)',
|
|
71
|
+
author: 'Community',
|
|
72
|
+
type: 'community',
|
|
73
|
+
status: 'alpha',
|
|
74
|
+
downloads: 20,
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
if (filter?.type) {
|
|
78
|
+
return plugins.filter(p => p.type === filter.type);
|
|
79
|
+
}
|
|
80
|
+
if (filter?.status) {
|
|
81
|
+
return plugins.filter(p => p.status === filter.status);
|
|
82
|
+
}
|
|
83
|
+
return plugins;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Install plugin
|
|
87
|
+
*/
|
|
88
|
+
async install(name, version) {
|
|
89
|
+
console.log(`Installing ${name}${version ? `@${version}` : ''}...`);
|
|
90
|
+
// In real implementation:
|
|
91
|
+
// 1. Check compatibility
|
|
92
|
+
// 2. Download from registry
|
|
93
|
+
// 3. Install to plugins directory
|
|
94
|
+
// 4. Register with plugin loader
|
|
95
|
+
console.log(`✅ ${name} installed successfully`);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Uninstall plugin
|
|
99
|
+
*/
|
|
100
|
+
async uninstall(name) {
|
|
101
|
+
console.log(`Uninstalling ${name}...`);
|
|
102
|
+
// In real implementation:
|
|
103
|
+
// 1. Remove from plugins directory
|
|
104
|
+
// 2. Unregister from plugin loader
|
|
105
|
+
console.log(`✅ ${name} uninstalled successfully`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Update plugin
|
|
109
|
+
*/
|
|
110
|
+
async update(name) {
|
|
111
|
+
console.log(`Updating ${name}...`);
|
|
112
|
+
// In real implementation:
|
|
113
|
+
// 1. Check for newer version
|
|
114
|
+
// 2. Download and install
|
|
115
|
+
console.log(`✅ ${name} updated successfully`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get installed plugins
|
|
119
|
+
*/
|
|
120
|
+
getInstalled() {
|
|
121
|
+
return Array.from(this.installedPlugins.values());
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Register installed plugin
|
|
125
|
+
*/
|
|
126
|
+
registerPlugin(manifest) {
|
|
127
|
+
this.installedPlugins.set(manifest.name, manifest);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Search plugins
|
|
131
|
+
*/
|
|
132
|
+
async search(query) {
|
|
133
|
+
const all = await this.list();
|
|
134
|
+
const queryLower = query.toLowerCase();
|
|
135
|
+
return all.filter(p => p.name.toLowerCase().includes(queryLower) ||
|
|
136
|
+
p.description.toLowerCase().includes(queryLower));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get plugin details
|
|
140
|
+
*/
|
|
141
|
+
async getDetails(name) {
|
|
142
|
+
const all = await this.list();
|
|
143
|
+
return all.find(p => p.name === name) ?? null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Submit community plugin
|
|
147
|
+
*/
|
|
148
|
+
async submit(manifest) {
|
|
149
|
+
console.log(`Submitting community plugin: ${manifest.name}...`);
|
|
150
|
+
// In real implementation:
|
|
151
|
+
// 1. Validate manifest
|
|
152
|
+
// 2. Run tests
|
|
153
|
+
// 3. Publish to community registry
|
|
154
|
+
console.log(`✅ ${manifest.name} submitted successfully`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* CLI command helpers
|
|
159
|
+
*/
|
|
160
|
+
export function formatPluginList(plugins) {
|
|
161
|
+
const official = plugins.filter(p => p.type === 'official');
|
|
162
|
+
const community = plugins.filter(p => p.type === 'community');
|
|
163
|
+
const lines = ['Available Plugins:', ''];
|
|
164
|
+
if (official.length > 0) {
|
|
165
|
+
lines.push('Official Plugins:');
|
|
166
|
+
for (const plugin of official) {
|
|
167
|
+
const icon = plugin.status === 'stable' ? '✅' : plugin.status === 'beta' ? '🧪' : '🔬';
|
|
168
|
+
lines.push(` ${icon} ${plugin.name}@${plugin.version} - ${plugin.description}`);
|
|
169
|
+
}
|
|
170
|
+
lines.push('');
|
|
171
|
+
}
|
|
172
|
+
if (community.length > 0) {
|
|
173
|
+
lines.push('Community Plugins:');
|
|
174
|
+
for (const plugin of community) {
|
|
175
|
+
const icon = plugin.status === 'stable' ? '✅' : plugin.status === 'beta' ? '🧪' : '🔬';
|
|
176
|
+
lines.push(` ${icon} ${plugin.name}@${plugin.version} - ${plugin.description} (by ${plugin.author})`);
|
|
177
|
+
}
|
|
178
|
+
lines.push('');
|
|
179
|
+
}
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Create global marketplace instance
|
|
184
|
+
*/
|
|
185
|
+
let globalMarketplace = null;
|
|
186
|
+
export function getGlobalMarketplace() {
|
|
187
|
+
if (!globalMarketplace) {
|
|
188
|
+
globalMarketplace = new PluginMarketplace();
|
|
189
|
+
}
|
|
190
|
+
return globalMarketplace;
|
|
191
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function buildCliHelpFooter(invokedCliName, alternateCliName) {
|
|
2
|
+
return `
|
|
3
|
+
Jurisdictions:
|
|
4
|
+
- nz (New Zealand, default)
|
|
5
|
+
- au-comm (Australian Commonwealth)
|
|
6
|
+
- au-qld (Queensland)
|
|
7
|
+
|
|
8
|
+
Examples:
|
|
9
|
+
$ ${invokedCliName} search --query "health" --type act
|
|
10
|
+
$ ${invokedCliName} get "act_public_1989_18"
|
|
11
|
+
$ ${invokedCliName} get "act/1988/123" --jurisdiction au-comm
|
|
12
|
+
$ ${invokedCliName} get "act_public_1989_18" --versions
|
|
13
|
+
$ ${invokedCliName} export --query "health" --output health.csv
|
|
14
|
+
$ ${invokedCliName} stream --query "health" --output health.csv # Stream large exports
|
|
15
|
+
$ ${invokedCliName} batch --ids "act_public_1989_18,act_public_1986_132" --type getWork --output results.json
|
|
16
|
+
$ ${invokedCliName} batch --file works.csv --type getWork --output results.json
|
|
17
|
+
$ ${invokedCliName} cite "act_public_1989_18" --style bibtex
|
|
18
|
+
$ ${invokedCliName} config --show
|
|
19
|
+
$ ${invokedCliName} cache --stats
|
|
20
|
+
|
|
21
|
+
Documentation: https://github.com/edithatogo/anz-legislation
|
|
22
|
+
Also available as: ${alternateCliName}
|
|
23
|
+
NZ API Documentation: https://api.legislation.govt.nz/docs/`;
|
|
24
|
+
}
|
|
25
|
+
export function buildMcpStartupMessages(invokedBinaryName, alternateBinaryName) {
|
|
26
|
+
return [
|
|
27
|
+
`ANZ Legislation MCP Server running on stdio via ${invokedBinaryName} (server id: nz-legislation)`,
|
|
28
|
+
`Also available as: ${alternateBinaryName}`,
|
|
29
|
+
'Tools available: search_legislation, get_legislation, get_legislation_versions, generate_citation, export_legislation, get_config',
|
|
30
|
+
'Resources available: legislation://{workId}',
|
|
31
|
+
];
|
|
32
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Per-jurisdiction rate limiting to avoid IP blocks and
|
|
5
|
+
* be a good internet citizen.
|
|
6
|
+
*/
|
|
7
|
+
export interface RateLimitOptions {
|
|
8
|
+
requests: number;
|
|
9
|
+
per: number;
|
|
10
|
+
}
|
|
11
|
+
export interface RateLimitStatus {
|
|
12
|
+
remaining: number;
|
|
13
|
+
limit: number;
|
|
14
|
+
resetTime: Date;
|
|
15
|
+
retryAfter?: number;
|
|
16
|
+
}
|
|
17
|
+
export declare class RateLimiter {
|
|
18
|
+
private limit;
|
|
19
|
+
private windowMs;
|
|
20
|
+
private tokens;
|
|
21
|
+
private lastRefill;
|
|
22
|
+
private queue;
|
|
23
|
+
private wakeUpTimer;
|
|
24
|
+
constructor(options: RateLimitOptions);
|
|
25
|
+
/**
|
|
26
|
+
* Wait until rate limit allows
|
|
27
|
+
*/
|
|
28
|
+
throttle(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Get current rate limit status
|
|
31
|
+
*/
|
|
32
|
+
getStatus(): RateLimitStatus;
|
|
33
|
+
/**
|
|
34
|
+
* Get remaining requests
|
|
35
|
+
*/
|
|
36
|
+
getRemainingRequests(): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get reset time
|
|
39
|
+
*/
|
|
40
|
+
getResetTime(): Date;
|
|
41
|
+
/**
|
|
42
|
+
* Refill tokens based on elapsed time
|
|
43
|
+
*/
|
|
44
|
+
private refillTokens;
|
|
45
|
+
/**
|
|
46
|
+
* Process waiting requests
|
|
47
|
+
*/
|
|
48
|
+
private processQueue;
|
|
49
|
+
/**
|
|
50
|
+
* Get wait time in ms
|
|
51
|
+
*/
|
|
52
|
+
private getWaitTime;
|
|
53
|
+
/**
|
|
54
|
+
* Reset rate limiter
|
|
55
|
+
*/
|
|
56
|
+
reset(): void;
|
|
57
|
+
private scheduleQueueProcessing;
|
|
58
|
+
private clearScheduledWakeUp;
|
|
59
|
+
/**
|
|
60
|
+
* Create rate limiter from string (e.g., "30 per minute")
|
|
61
|
+
*/
|
|
62
|
+
static fromString(spec: string): RateLimiter;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Rate limiter registry for multiple jurisdictions
|
|
66
|
+
*/
|
|
67
|
+
export declare class RateLimiterRegistry {
|
|
68
|
+
private limiters;
|
|
69
|
+
/**
|
|
70
|
+
* Get or create rate limiter for jurisdiction
|
|
71
|
+
*/
|
|
72
|
+
get(jurisdiction: string): RateLimiter;
|
|
73
|
+
/**
|
|
74
|
+
* Set custom rate limiter for jurisdiction
|
|
75
|
+
*/
|
|
76
|
+
set(jurisdiction: string, limiter: RateLimiter): void;
|
|
77
|
+
/**
|
|
78
|
+
* Get status for all jurisdictions
|
|
79
|
+
*/
|
|
80
|
+
getAllStatus(): Array<{
|
|
81
|
+
jurisdiction: string;
|
|
82
|
+
status: RateLimitStatus;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Reset all limiters
|
|
86
|
+
*/
|
|
87
|
+
resetAll(): void;
|
|
88
|
+
/**
|
|
89
|
+
* Create default limiter based on jurisdiction
|
|
90
|
+
*/
|
|
91
|
+
private createDefaultLimiter;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Rate limit decorator for methods
|
|
95
|
+
*/
|
|
96
|
+
export declare function rateLimited(jurisdiction: string): <T extends (...args: unknown[]) => Promise<unknown>>(target: object, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) => TypedPropertyDescriptor<T>;
|
|
97
|
+
/**
|
|
98
|
+
* CLI helper for rate limit status
|
|
99
|
+
*/
|
|
100
|
+
export declare function formatRateLimitStatus(status: RateLimitStatus): string;
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rate Limiter
|
|
3
|
+
*
|
|
4
|
+
* Per-jurisdiction rate limiting to avoid IP blocks and
|
|
5
|
+
* be a good internet citizen.
|
|
6
|
+
*/
|
|
7
|
+
export class RateLimiter {
|
|
8
|
+
limit;
|
|
9
|
+
windowMs;
|
|
10
|
+
tokens;
|
|
11
|
+
lastRefill;
|
|
12
|
+
queue = [];
|
|
13
|
+
wakeUpTimer = null;
|
|
14
|
+
constructor(options) {
|
|
15
|
+
this.limit = options.requests;
|
|
16
|
+
this.windowMs = options.per * 1000; // Convert to ms
|
|
17
|
+
this.tokens = this.limit;
|
|
18
|
+
this.lastRefill = Date.now();
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Wait until rate limit allows
|
|
22
|
+
*/
|
|
23
|
+
async throttle() {
|
|
24
|
+
this.refillTokens();
|
|
25
|
+
if (this.tokens >= 1) {
|
|
26
|
+
this.tokens--;
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Need to wait
|
|
30
|
+
const waitTime = this.getWaitTime();
|
|
31
|
+
if (waitTime > 0) {
|
|
32
|
+
await new Promise(resolve => {
|
|
33
|
+
this.queue.push({ resolve, timestamp: Date.now() + waitTime });
|
|
34
|
+
this.scheduleQueueProcessing();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Get current rate limit status
|
|
40
|
+
*/
|
|
41
|
+
getStatus() {
|
|
42
|
+
this.refillTokens();
|
|
43
|
+
const resetTime = new Date(Date.now() + this.windowMs);
|
|
44
|
+
const remaining = Math.floor(this.tokens);
|
|
45
|
+
return {
|
|
46
|
+
remaining,
|
|
47
|
+
limit: this.limit,
|
|
48
|
+
resetTime,
|
|
49
|
+
retryAfter: remaining <= 0 ? Math.ceil(this.getWaitTime() / 1000) : undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Get remaining requests
|
|
54
|
+
*/
|
|
55
|
+
getRemainingRequests() {
|
|
56
|
+
this.refillTokens();
|
|
57
|
+
return Math.floor(this.tokens);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Get reset time
|
|
61
|
+
*/
|
|
62
|
+
getResetTime() {
|
|
63
|
+
return new Date(Date.now() + this.windowMs);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Refill tokens based on elapsed time
|
|
67
|
+
*/
|
|
68
|
+
refillTokens() {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const elapsed = now - this.lastRefill;
|
|
71
|
+
const tokensToAdd = (elapsed / this.windowMs) * this.limit;
|
|
72
|
+
this.tokens = Math.min(this.limit, this.tokens + tokensToAdd);
|
|
73
|
+
this.lastRefill = now;
|
|
74
|
+
// Process queue
|
|
75
|
+
this.processQueue();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Process waiting requests
|
|
79
|
+
*/
|
|
80
|
+
processQueue() {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
// Remove stale entries (older than 1 minute) to prevent memory leaks
|
|
83
|
+
const maxQueueAge = 60 * 1000; // 1 minute
|
|
84
|
+
this.queue = this.queue.filter(item => now - item.timestamp < maxQueueAge);
|
|
85
|
+
// Limit queue size to prevent memory issues
|
|
86
|
+
const maxQueueSize = 1000;
|
|
87
|
+
if (this.queue.length > maxQueueSize) {
|
|
88
|
+
console.warn(`Rate limiter queue exceeded ${maxQueueSize} items, trimming...`);
|
|
89
|
+
this.queue = this.queue.slice(0, maxQueueSize);
|
|
90
|
+
}
|
|
91
|
+
while (this.queue.length > 0 && this.tokens >= 1) {
|
|
92
|
+
const next = this.queue[0];
|
|
93
|
+
if (now >= next.timestamp) {
|
|
94
|
+
this.queue.shift();
|
|
95
|
+
this.tokens--;
|
|
96
|
+
next.resolve();
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.scheduleQueueProcessing(next.timestamp - now);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (this.queue.length === 0) {
|
|
104
|
+
this.clearScheduledWakeUp();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get wait time in ms
|
|
109
|
+
*/
|
|
110
|
+
getWaitTime() {
|
|
111
|
+
if (this.tokens >= 1) {
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
const tokensNeeded = 1 - this.tokens;
|
|
115
|
+
const timePerToken = this.windowMs / this.limit;
|
|
116
|
+
return tokensNeeded * timePerToken;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Reset rate limiter
|
|
120
|
+
*/
|
|
121
|
+
reset() {
|
|
122
|
+
this.tokens = this.limit;
|
|
123
|
+
this.lastRefill = Date.now();
|
|
124
|
+
this.clearScheduledWakeUp();
|
|
125
|
+
// Resolve all waiting
|
|
126
|
+
for (const item of this.queue) {
|
|
127
|
+
item.resolve();
|
|
128
|
+
}
|
|
129
|
+
this.queue = [];
|
|
130
|
+
}
|
|
131
|
+
scheduleQueueProcessing(delayMs) {
|
|
132
|
+
if (this.queue.length === 0) {
|
|
133
|
+
this.clearScheduledWakeUp();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const nextDelay = Math.max(0, delayMs ?? this.queue[0].timestamp - Date.now());
|
|
137
|
+
if (this.wakeUpTimer) {
|
|
138
|
+
clearTimeout(this.wakeUpTimer);
|
|
139
|
+
}
|
|
140
|
+
this.wakeUpTimer = setTimeout(() => {
|
|
141
|
+
this.wakeUpTimer = null;
|
|
142
|
+
this.refillTokens();
|
|
143
|
+
}, nextDelay);
|
|
144
|
+
}
|
|
145
|
+
clearScheduledWakeUp() {
|
|
146
|
+
if (this.wakeUpTimer) {
|
|
147
|
+
clearTimeout(this.wakeUpTimer);
|
|
148
|
+
this.wakeUpTimer = null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Create rate limiter from string (e.g., "30 per minute")
|
|
153
|
+
*/
|
|
154
|
+
static fromString(spec) {
|
|
155
|
+
const match = spec.match(/(\d+)\s*per\s*(second|minute|hour|day)/i);
|
|
156
|
+
if (!match) {
|
|
157
|
+
throw new Error(`Invalid rate limit spec: ${spec}`);
|
|
158
|
+
}
|
|
159
|
+
const requests = parseInt(match[1], 10);
|
|
160
|
+
const unit = match[2].toLowerCase();
|
|
161
|
+
const perSeconds = {
|
|
162
|
+
second: 1,
|
|
163
|
+
minute: 60,
|
|
164
|
+
hour: 3600,
|
|
165
|
+
day: 86400,
|
|
166
|
+
};
|
|
167
|
+
return new RateLimiter({
|
|
168
|
+
requests,
|
|
169
|
+
per: perSeconds[unit],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Rate limiter registry for multiple jurisdictions
|
|
175
|
+
*/
|
|
176
|
+
export class RateLimiterRegistry {
|
|
177
|
+
limiters = new Map();
|
|
178
|
+
/**
|
|
179
|
+
* Get or create rate limiter for jurisdiction
|
|
180
|
+
*/
|
|
181
|
+
get(jurisdiction) {
|
|
182
|
+
if (!this.limiters.has(jurisdiction)) {
|
|
183
|
+
const limiter = this.createDefaultLimiter(jurisdiction);
|
|
184
|
+
this.limiters.set(jurisdiction, limiter);
|
|
185
|
+
}
|
|
186
|
+
return this.limiters.get(jurisdiction);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Set custom rate limiter for jurisdiction
|
|
190
|
+
*/
|
|
191
|
+
set(jurisdiction, limiter) {
|
|
192
|
+
this.limiters.set(jurisdiction, limiter);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Get status for all jurisdictions
|
|
196
|
+
*/
|
|
197
|
+
getAllStatus() {
|
|
198
|
+
return Array.from(this.limiters.entries()).map(([jurisdiction, limiter]) => ({
|
|
199
|
+
jurisdiction,
|
|
200
|
+
status: limiter.getStatus(),
|
|
201
|
+
}));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Reset all limiters
|
|
205
|
+
*/
|
|
206
|
+
resetAll() {
|
|
207
|
+
for (const limiter of this.limiters.values()) {
|
|
208
|
+
limiter.reset();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Create default limiter based on jurisdiction
|
|
213
|
+
*/
|
|
214
|
+
createDefaultLimiter(jurisdiction) {
|
|
215
|
+
// Default limits based on jurisdiction
|
|
216
|
+
const limits = {
|
|
217
|
+
nz: { requests: 100, per: 60 }, // 100 per minute
|
|
218
|
+
'au-qld': { requests: 30, per: 60 }, // 30 per minute (more conservative)
|
|
219
|
+
'au-comm': { requests: 50, per: 60 }, // 50 per minute
|
|
220
|
+
'au-nsw': { requests: 40, per: 60 },
|
|
221
|
+
'au-vic': { requests: 40, per: 60 },
|
|
222
|
+
'au-wa': { requests: 30, per: 60 },
|
|
223
|
+
'au-sa': { requests: 30, per: 60 },
|
|
224
|
+
'au-tas': { requests: 20, per: 60 },
|
|
225
|
+
'au-nt': { requests: 20, per: 60 },
|
|
226
|
+
'au-act': { requests: 30, per: 60 },
|
|
227
|
+
};
|
|
228
|
+
const options = limits[jurisdiction] ?? { requests: 30, per: 60 };
|
|
229
|
+
return new RateLimiter(options);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Rate limit decorator for methods
|
|
234
|
+
*/
|
|
235
|
+
export function rateLimited(jurisdiction) {
|
|
236
|
+
return function (_target, _propertyKey, descriptor) {
|
|
237
|
+
const originalMethod = descriptor.value;
|
|
238
|
+
const registry = new RateLimiterRegistry();
|
|
239
|
+
if (!originalMethod) {
|
|
240
|
+
return descriptor;
|
|
241
|
+
}
|
|
242
|
+
descriptor.value = async function (...args) {
|
|
243
|
+
const limiter = registry.get(jurisdiction);
|
|
244
|
+
await limiter.throttle();
|
|
245
|
+
return (await originalMethod.apply(this, args));
|
|
246
|
+
};
|
|
247
|
+
return descriptor;
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* CLI helper for rate limit status
|
|
252
|
+
*/
|
|
253
|
+
export function formatRateLimitStatus(status) {
|
|
254
|
+
const icon = status.remaining > status.limit * 0.5 ? '✅' : status.remaining > 0 ? '⚠️' : '❌';
|
|
255
|
+
return `${icon} ${status.remaining}/${status.limit} remaining${status.retryAfter ? ` (retry after ${status.retryAfter}s)` : ''}`;
|
|
256
|
+
}
|