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.
Files changed (95) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +23 -0
  3. package/dist/cli.d.ts +6 -0
  4. package/dist/cli.js +198 -0
  5. package/dist/client.d.ts +84 -0
  6. package/dist/client.js +492 -0
  7. package/dist/commands/batch.d.ts +5 -0
  8. package/dist/commands/batch.js +121 -0
  9. package/dist/commands/cache.d.ts +5 -0
  10. package/dist/commands/cache.js +43 -0
  11. package/dist/commands/cite.d.ts +5 -0
  12. package/dist/commands/cite.js +68 -0
  13. package/dist/commands/config.d.ts +5 -0
  14. package/dist/commands/config.js +56 -0
  15. package/dist/commands/export.d.ts +8 -0
  16. package/dist/commands/export.js +169 -0
  17. package/dist/commands/generate.d.ts +10 -0
  18. package/dist/commands/generate.js +320 -0
  19. package/dist/commands/get.d.ts +5 -0
  20. package/dist/commands/get.js +99 -0
  21. package/dist/commands/help.d.ts +13 -0
  22. package/dist/commands/help.js +298 -0
  23. package/dist/commands/search.d.ts +5 -0
  24. package/dist/commands/search.js +96 -0
  25. package/dist/commands/stream.d.ts +5 -0
  26. package/dist/commands/stream.js +100 -0
  27. package/dist/config.d.ts +81 -0
  28. package/dist/config.js +209 -0
  29. package/dist/errors.d.ts +108 -0
  30. package/dist/errors.js +173 -0
  31. package/dist/mcp/server.d.ts +13 -0
  32. package/dist/mcp/server.js +428 -0
  33. package/dist/mcp-cli.d.ts +6 -0
  34. package/dist/mcp-cli.js +37 -0
  35. package/dist/models/canonical.d.ts +423 -0
  36. package/dist/models/canonical.js +92 -0
  37. package/dist/models/index.d.ts +892 -0
  38. package/dist/models/index.js +223 -0
  39. package/dist/output/index.d.ts +34 -0
  40. package/dist/output/index.js +195 -0
  41. package/dist/output/legal-metadata-publication.d.ts +18 -0
  42. package/dist/output/legal-metadata-publication.js +23 -0
  43. package/dist/providers/canonical-metadata.d.ts +3 -0
  44. package/dist/providers/canonical-metadata.js +202 -0
  45. package/dist/providers/commonwealth-provider.d.ts +27 -0
  46. package/dist/providers/commonwealth-provider.js +81 -0
  47. package/dist/providers/index.d.ts +20 -0
  48. package/dist/providers/index.js +27 -0
  49. package/dist/providers/legislation-provider.d.ts +227 -0
  50. package/dist/providers/legislation-provider.js +308 -0
  51. package/dist/providers/nz-provider.d.ts +36 -0
  52. package/dist/providers/nz-provider.js +130 -0
  53. package/dist/providers/output-adapters.d.ts +14 -0
  54. package/dist/providers/output-adapters.js +116 -0
  55. package/dist/providers/plugin-discovery.d.ts +39 -0
  56. package/dist/providers/plugin-discovery.js +91 -0
  57. package/dist/providers/plugin-loader.d.ts +86 -0
  58. package/dist/providers/plugin-loader.js +219 -0
  59. package/dist/providers/queensland-provider.d.ts +42 -0
  60. package/dist/providers/queensland-provider.js +105 -0
  61. package/dist/utils/api-optimization.d.ts +92 -0
  62. package/dist/utils/api-optimization.js +276 -0
  63. package/dist/utils/batch.d.ts +110 -0
  64. package/dist/utils/batch.js +269 -0
  65. package/dist/utils/branded-types.d.ts +0 -0
  66. package/dist/utils/branded-types.js +1 -0
  67. package/dist/utils/compatibility-matrix.d.ts +89 -0
  68. package/dist/utils/compatibility-matrix.js +214 -0
  69. package/dist/utils/config-validator.d.ts +39 -0
  70. package/dist/utils/config-validator.js +197 -0
  71. package/dist/utils/env-loader.d.ts +55 -0
  72. package/dist/utils/env-loader.js +77 -0
  73. package/dist/utils/health-monitor.d.ts +93 -0
  74. package/dist/utils/health-monitor.js +209 -0
  75. package/dist/utils/invocation.d.ts +4 -0
  76. package/dist/utils/invocation.js +33 -0
  77. package/dist/utils/logger.d.ts +94 -0
  78. package/dist/utils/logger.js +220 -0
  79. package/dist/utils/plugin-marketplace.d.ts +77 -0
  80. package/dist/utils/plugin-marketplace.js +191 -0
  81. package/dist/utils/presentation.d.ts +2 -0
  82. package/dist/utils/presentation.js +32 -0
  83. package/dist/utils/rate-limiter.d.ts +100 -0
  84. package/dist/utils/rate-limiter.js +256 -0
  85. package/dist/utils/scraper-cache.d.ts +115 -0
  86. package/dist/utils/scraper-cache.js +229 -0
  87. package/dist/utils/secure-config.d.ts +40 -0
  88. package/dist/utils/secure-config.js +195 -0
  89. package/dist/utils/streaming.d.ts +121 -0
  90. package/dist/utils/streaming.js +333 -0
  91. package/dist/utils/validation.d.ts +190 -0
  92. package/dist/utils/validation.js +209 -0
  93. package/dist/utils/version.d.ts +13 -0
  94. package/dist/utils/version.js +46 -0
  95. 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,2 @@
1
+ export declare function buildCliHelpFooter(invokedCliName: string, alternateCliName: string): string;
2
+ export declare function buildMcpStartupMessages(invokedBinaryName: string, alternateBinaryName: string): string[];
@@ -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
+ }