@forgezero/providers 0.1.10 → 0.1.11
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/README.md +238 -161
- package/dist/binance.js +100 -3
- package/dist/chain.js +100 -3
- package/dist/database.js +100 -3
- package/dist/email.js +100 -3
- package/dist/http.js +100 -3
- package/dist/index.d.ts +49 -2
- package/dist/index.js +103 -3
- package/dist/pool.js +100 -3
- package/dist/storage.js +100 -3
- package/dist/translation.js +100 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,185 +1,262 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
GENERATED FILE — do not edit.
|
|
3
|
+
|
|
4
|
+
Change scripts/generate-guides.ts or its typed sources, run `bun run guides`,
|
|
5
|
+
and commit the generator and rendered files together.
|
|
6
|
+
-->
|
|
7
|
+
|
|
1
8
|
# @forgezero/providers
|
|
2
9
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
A provider declares what a vendor can do. A service declares what an
|
|
6
|
-
application needs. Configuration attaches one provider method to one service
|
|
7
|
-
method, so adding a provider never changes the service contract and adding a
|
|
8
|
-
service never forks a provider.
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
bun add @forgezero/providers
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Built-in providers
|
|
15
|
-
|
|
16
|
-
| provider | methods | import |
|
|
17
|
-
|---|---|---|
|
|
18
|
-
| JetEmail | `send`, `sendBatch` | `@forgezero/providers/email` |
|
|
19
|
-
| SMTP | `send` | `@forgezero/providers/email` |
|
|
20
|
-
| ArangoDB | `query` | `@forgezero/providers/database` |
|
|
21
|
-
| EVM JSON-RPC | `request` | `@forgezero/providers/chain` |
|
|
22
|
-
| HTTP | `request` | `@forgezero/providers/http` |
|
|
23
|
-
| S3-compatible storage | `request` | `@forgezero/providers/storage` |
|
|
24
|
-
| Google AI Studio | `translate` | `@forgezero/providers/translation` |
|
|
25
|
-
|
|
26
|
-
`pool`, `binance`, and `realtime` are specialised provider utilities/clients.
|
|
27
|
-
Every public subpath is documented at
|
|
28
|
-
[forgezero.net/docs/providers](https://www.forgezero.net/docs/providers).
|
|
29
|
-
|
|
30
|
-
## Email: exact capability routing
|
|
31
|
-
|
|
32
|
-
JetEmail provides native single and batch delivery. SMTP provides single
|
|
33
|
-
delivery only. ForgeZero does not pretend SMTP has a batch capability by
|
|
34
|
-
silently looping.
|
|
35
|
-
|
|
36
|
-
```ts
|
|
37
|
-
import {
|
|
38
|
-
createRegistry,
|
|
39
|
-
envCredentials,
|
|
40
|
-
staticConfig
|
|
41
|
-
} from '@forgezero/providers';
|
|
42
|
-
import {
|
|
43
|
-
emailProviders,
|
|
44
|
-
emailService
|
|
45
|
-
} from '@forgezero/providers/email';
|
|
46
|
-
|
|
47
|
-
const registry = createRegistry({
|
|
48
|
-
providers: emailProviders,
|
|
49
|
-
credentials: envCredentials(process.env),
|
|
50
|
-
config: staticConfig({
|
|
51
|
-
providers: {
|
|
52
|
-
jetPrimary: {
|
|
53
|
-
providerId: 'jetemail',
|
|
54
|
-
enabled: true,
|
|
55
|
-
config: { from: 'ops@example.com' },
|
|
56
|
-
secretRef: 'jetemail'
|
|
57
|
-
},
|
|
58
|
-
smtpBackup: {
|
|
59
|
-
providerId: 'smtp',
|
|
60
|
-
enabled: true,
|
|
61
|
-
config: {
|
|
62
|
-
host: 'smtp.example.com',
|
|
63
|
-
port: 587,
|
|
64
|
-
from: 'ops@example.com',
|
|
65
|
-
transport
|
|
66
|
-
},
|
|
67
|
-
secretRef: 'smtp'
|
|
68
|
-
}
|
|
69
|
-
},
|
|
70
|
-
services: {
|
|
71
|
-
email: {
|
|
72
|
-
send: [
|
|
73
|
-
{ instanceKey: 'jetPrimary', providerMethod: 'send', priority: 1, enabled: true },
|
|
74
|
-
{ instanceKey: 'smtpBackup', providerMethod: 'send', priority: 2, enabled: true }
|
|
75
|
-
],
|
|
76
|
-
sendBatch: [
|
|
77
|
-
{ instanceKey: 'jetPrimary', providerMethod: 'sendBatch', priority: 1, enabled: true }
|
|
78
|
-
]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
})
|
|
82
|
-
});
|
|
10
|
+
Send through whichever provider is up. Priority and failover, reordered live.
|
|
83
11
|
|
|
84
|
-
|
|
85
|
-
const outcome = await email.call('send', {
|
|
86
|
-
to: 'ada@example.com',
|
|
87
|
-
subject: 'Your invite',
|
|
88
|
-
html: '<p>Welcome</p>'
|
|
89
|
-
});
|
|
12
|
+
## Global package root and supported runtimes
|
|
90
13
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
outcome.attempts; // every failed, skipped and successful attempt
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Hooks and health writes are awaited. A successful fallback therefore still
|
|
99
|
-
returns the complete failure trail for auditing or operator notification.
|
|
100
|
-
|
|
101
|
-
## Define a custom provider
|
|
102
|
-
|
|
103
|
-
A provider does not name a service:
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
import {
|
|
107
|
-
defineProvider,
|
|
108
|
-
defineProviderMethod
|
|
109
|
-
} from '@forgezero/providers';
|
|
110
|
-
|
|
111
|
-
export const postmark = defineProvider({
|
|
112
|
-
id: 'postmark',
|
|
113
|
-
label: 'Postmark',
|
|
114
|
-
credentials: {
|
|
115
|
-
type: 'object',
|
|
116
|
-
additionalProperties: false,
|
|
117
|
-
properties: { token: { type: 'string', writeOnly: true } }
|
|
118
|
-
},
|
|
119
|
-
methods: {
|
|
120
|
-
deliver: defineProviderMethod<Notice, DeliveryReceipt>({
|
|
121
|
-
invoke: async (context, notice) =>
|
|
122
|
-
post(context.config, await context.secret('token'), notice),
|
|
123
|
-
classify: (error) => status(error) === 400 ? 'terminal' : 'retryable'
|
|
124
|
-
})
|
|
125
|
-
}
|
|
126
|
-
});
|
|
14
|
+
Anyone calling somebody else's API who needs the failure classified rather than guessed. Provider capabilities stay independent from application-owned services, which attach named methods by priority with health tracking. Supported runtimes: bun, node, workers, deno. The global base/root import is @forgezero/providers. Every public import or command is listed below; the documentation inventory is checked in both directions against package.json exports.
|
|
15
|
+
|
|
16
|
+
```text
|
|
17
|
+
import * as root from '@forgezero/providers';
|
|
127
18
|
```
|
|
128
19
|
|
|
129
|
-
##
|
|
20
|
+
## Commands
|
|
130
21
|
|
|
131
|
-
|
|
132
|
-
|
|
22
|
+
bun add @forgezero/providers @forgezero/vault — Install provider/service routing and the optional ForgeZero Vault credential adapter.
|
|
23
|
+
bun test — Run the consumer project tests containing provider failure and fallback cases.
|
|
133
24
|
|
|
134
|
-
```
|
|
135
|
-
|
|
25
|
+
```text
|
|
26
|
+
bun add @forgezero/providers @forgezero/vault
|
|
27
|
+
bun test
|
|
28
|
+
```
|
|
136
29
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
30
|
+
## @forgezero/providers
|
|
31
|
+
|
|
32
|
+
The registry: priority, health, and a terminal-versus-retryable verdict per failure.
|
|
33
|
+
|
|
34
|
+
```text
|
|
35
|
+
import * as api from '@forgezero/providers';
|
|
36
|
+
```
|
|
143
37
|
|
|
144
|
-
|
|
145
|
-
|
|
38
|
+
## @forgezero/providers/email
|
|
39
|
+
|
|
40
|
+
JetEmail (`send`, `sendBatch`), SMTP (`send` only), and the typed email service contract; SMTP transport is injected.
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
import * as api from '@forgezero/providers/email';
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## @forgezero/providers/database
|
|
47
|
+
|
|
48
|
+
ArangoDB in the registry for credential rotation and health — with failover off by default.
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
import * as api from '@forgezero/providers/database';
|
|
146
52
|
```
|
|
147
53
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
54
|
+
## @forgezero/providers/http
|
|
55
|
+
|
|
56
|
+
Outbound HTTP with a per-host weight budget reserved before the call and settled from the response.
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
import * as api from '@forgezero/providers/http';
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## @forgezero/providers/pool
|
|
63
|
+
|
|
64
|
+
Which outbound address a request leaves by, sticky per key, over the same budget as everything else.
|
|
151
65
|
|
|
152
|
-
|
|
66
|
+
```text
|
|
67
|
+
import * as api from '@forgezero/providers/pool';
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## @forgezero/providers/chain
|
|
71
|
+
|
|
72
|
+
An EVM node behind the same failover as any other service: read logs, broadcast, and never stop scanning because one endpoint rate-limited.
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
import * as api from '@forgezero/providers/chain';
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## @forgezero/providers/binance
|
|
79
|
+
|
|
80
|
+
Binance behind the venue adapter: three hosts, one weight budget, signed over the exact string that is sent.
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
import * as api from '@forgezero/providers/binance';
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## @forgezero/providers/storage
|
|
87
|
+
|
|
88
|
+
S3-compatible object storage, SigV4 signed with Web Crypto and no vendor SDK.
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
import * as api from '@forgezero/providers/storage';
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## @forgezero/providers/translation
|
|
95
|
+
|
|
96
|
+
Google AI Studio translation with strict batch alignment and classified quota failures.
|
|
97
|
+
|
|
98
|
+
```text
|
|
99
|
+
import * as api from '@forgezero/providers/translation';
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## @forgezero/providers/realtime
|
|
103
|
+
|
|
104
|
+
Cloudflare KV node-directory and Durable Object fan-out clients for project-defined realtime services.
|
|
105
|
+
|
|
106
|
+
```text
|
|
107
|
+
import * as api from '@forgezero/providers/realtime';
|
|
108
|
+
```
|
|
153
109
|
|
|
154
|
-
|
|
155
|
-
structural interface. ForgeZero provides adapters in `@forgezero/vault/providers`;
|
|
156
|
-
other consumers can inject any secret manager.
|
|
110
|
+
## Email with ordered provider fallback
|
|
157
111
|
|
|
158
|
-
|
|
159
|
-
|
|
112
|
+
The service owns send/sendBatch. Attach provider methods directly in priority arrays; SMTP is never presented as batch-capable.
|
|
113
|
+
|
|
114
|
+
```text
|
|
115
|
+
import { createService } from '@forgezero/providers';
|
|
116
|
+
import { emailService, jetemail, smtp } from '@forgezero/providers/email';
|
|
117
|
+
import { createVault } from '@forgezero/vault';
|
|
160
118
|
import { vaultCredentials } from '@forgezero/vault/providers';
|
|
161
119
|
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
120
|
+
const vault = createVault({ project: 'my-project', environment: 'production' });
|
|
121
|
+
const email = createService(emailService, {
|
|
122
|
+
send: [
|
|
123
|
+
{ provider: jetemail, method: 'send', credentials: vaultCredentials(vault, 'jetemail'), config: { from: 'ops@example.com' }, priority: 1 },
|
|
124
|
+
{ provider: smtp, method: 'send', credentials: vaultCredentials(vault, 'smtp'), config: { host: 'smtp.example.com', port: 587, from: 'ops@example.com', transport }, priority: 2 }
|
|
125
|
+
],
|
|
126
|
+
sendBatch: [
|
|
127
|
+
{ provider: jetemail, method: 'sendBatch', credentials: vaultCredentials(vault, 'jetemail'), config: { from: 'ops@example.com' }, priority: 1 }
|
|
128
|
+
]
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const outcome = await email.call('send', message);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Extend with a custom provider and service
|
|
135
|
+
|
|
136
|
+
Public consumers define provider capabilities and application services independently, then attach them with the same ordered-array API.
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
const postmark = defineProvider({
|
|
140
|
+
id: 'postmark', label: 'Postmark',
|
|
141
|
+
methods: { deliver: defineProviderMethod({ invoke, classify }) }
|
|
142
|
+
});
|
|
143
|
+
const notifications = defineService({
|
|
144
|
+
key: 'notifications', methods: { deliver: serviceMethod<Notice, Receipt>() }
|
|
145
|
+
});
|
|
146
|
+
const service = createService(notifications, {
|
|
147
|
+
deliver: [{ provider: postmark, method: 'deliver', credentials: vaultCredentials(vault, 'postmark'), priority: 1 }]
|
|
148
|
+
});
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Vault first, systemd credential fallback
|
|
152
|
+
|
|
153
|
+
Bootstrap tries the scoped Vault entry first and the identically named systemd credential second. If neither exists, the provider call fails.
|
|
154
|
+
|
|
155
|
+
```text
|
|
156
|
+
const credentials = chainScopedCredentials(
|
|
157
|
+
vaultCredentials(vault, 'jetemail'),
|
|
158
|
+
scopeCredentials(systemdCredentials({
|
|
159
|
+
nameFor: (reference, field) => `${reference}-${field}`
|
|
160
|
+
}), 'jetemail')
|
|
169
161
|
);
|
|
170
162
|
```
|
|
171
163
|
|
|
172
|
-
##
|
|
164
|
+
## Built-in provider capabilities
|
|
165
|
+
|
|
166
|
+
A provider owns capabilities; it does not own a service. A service method attaches a provider instance and one of its methods. This keeps custom providers and custom services independent.
|
|
167
|
+
|
|
168
|
+
```text
|
|
169
|
+
evm-rpc request
|
|
170
|
+
jetemail send, sendBatch
|
|
171
|
+
smtp send
|
|
172
|
+
arangodb query
|
|
173
|
+
http request
|
|
174
|
+
s3 request
|
|
175
|
+
google-ai-studio translate
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## EVM JSON-RPC node (evm-rpc)
|
|
179
|
+
|
|
180
|
+
Import: @forgezero/providers/chain. Methods: request. Credential fields: url, bearer. Nonsecret config: chainId, maxLogRange, timeoutMs. Several endpoints per chain is the normal case, not a luxury: public RPC is rate-limited and paid gateways have outages, and a scanner that stops when one is down stops crediting deposits while the money sits on the chain. "Already known" and "nonce too low" are TERMINAL — the transaction is already in flight, so rebroadcasting through another node is how a sweep gets sent twice.
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
importPath: '@forgezero/providers/chain'
|
|
184
|
+
providerId: 'evm-rpc'
|
|
185
|
+
methods: request
|
|
186
|
+
credentials: url, bearer
|
|
187
|
+
config: chainId, maxLogRange, timeoutMs
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## JetEmail (jetemail)
|
|
191
|
+
|
|
192
|
+
Import: @forgezero/providers/email. Methods: send, sendBatch. Credential fields: apiKey. Nonsecret config: eu, from. A 401 is RETRYABLE at the loop level — that key is bad, the next provider's may not be. Treating any 4xx as fatal is what loses the fallback.
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
importPath: '@forgezero/providers/email'
|
|
196
|
+
providerId: 'jetemail'
|
|
197
|
+
methods: send, sendBatch
|
|
198
|
+
credentials: apiKey
|
|
199
|
+
config: eu, from
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## SMTP (smtp)
|
|
203
|
+
|
|
204
|
+
Import: @forgezero/providers/email. Methods: send. Credential fields: user, password. Nonsecret config: host, port, secure, from, transport. Any number of named relays, each with its own priority slot. Also the bootstrap path: the vault opens after a ceremony, and the ceremony needs mail.
|
|
173
205
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
206
|
+
```text
|
|
207
|
+
importPath: '@forgezero/providers/email'
|
|
208
|
+
providerId: 'smtp'
|
|
209
|
+
methods: send
|
|
210
|
+
credentials: user, password
|
|
211
|
+
config: host, port, secure, from, transport
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## ArangoDB (arangodb)
|
|
215
|
+
|
|
216
|
+
Import: @forgezero/providers/database. Methods: query. Credential fields: password. Nonsecret config: url, urls, readPreferredUrls, readPreferredFallback, clusterId, database, username. Failover is OFF by default. Writing to a different database because the first was slow is data loss with extra steps; this is here for credential rotation and health.
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
importPath: '@forgezero/providers/database'
|
|
220
|
+
providerId: 'arangodb'
|
|
221
|
+
methods: query
|
|
222
|
+
credentials: password
|
|
223
|
+
config: url, urls, readPreferredUrls, readPreferredFallback, clusterId, database, username
|
|
224
|
+
```
|
|
179
225
|
|
|
180
|
-
|
|
181
|
-
does not count as a fault.
|
|
226
|
+
## HTTP (http)
|
|
182
227
|
|
|
183
|
-
|
|
228
|
+
Import: @forgezero/providers/http. Methods: request. Credential fields: apiKey, apiSecret. Nonsecret config: baseUrl, limit, windowMs, headroom. 418 and 429 back off rather than falling through. 418 is a venue saying "you ignored a 429 and are now banned", and hammering makes the ban longer.
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
importPath: '@forgezero/providers/http'
|
|
232
|
+
providerId: 'http'
|
|
233
|
+
methods: request
|
|
234
|
+
credentials: apiKey, apiSecret
|
|
235
|
+
config: baseUrl, limit, windowMs, headroom
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## S3-compatible storage (s3)
|
|
239
|
+
|
|
240
|
+
Import: @forgezero/providers/storage. Methods: request. Credential fields: accessKeyId, secretAccessKey, sessionToken. Nonsecret config: endpoint, region, bucket, addressing. A 403 is retryable because on S3 it usually means this key lacks a permission, not that the key is invalid. Works against AWS, R2, MinIO, Garage and Backblaze — path-style addressing by default, because that is what everything except AWS expects.
|
|
241
|
+
|
|
242
|
+
```text
|
|
243
|
+
importPath: '@forgezero/providers/storage'
|
|
244
|
+
providerId: 's3'
|
|
245
|
+
methods: request
|
|
246
|
+
credentials: accessKeyId, secretAccessKey, sessionToken
|
|
247
|
+
config: endpoint, region, bucket, addressing
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Google AI Studio (google-ai-studio)
|
|
251
|
+
|
|
252
|
+
Import: @forgezero/providers/translation. Methods: translate. Credential fields: apiKey. Nonsecret config: model. Machine translation for a tenant that wants it — ForgeZero translates nothing at runtime. Chosen as the first adapter because the free tier needs no billing account, so the feature can be tried without a procurement conversation. A 429 is a BACKOFF rather than a failure: on the free tier it is expected traffic, and treating it as terminal abandons a catalogue most of the way through. A short answer is terminal, because one translation missing from a batch shifts every later string onto the wrong source and nothing about the result looks broken afterwards.
|
|
253
|
+
|
|
254
|
+
```text
|
|
255
|
+
importPath: '@forgezero/providers/translation'
|
|
256
|
+
providerId: 'google-ai-studio'
|
|
257
|
+
methods: translate
|
|
258
|
+
credentials: apiKey
|
|
259
|
+
config: model
|
|
260
|
+
```
|
|
184
261
|
|
|
185
|
-
|
|
262
|
+
Full rendered documentation: https://www.forgezero.net/docs/providers
|
package/dist/binance.js
CHANGED
|
@@ -38,13 +38,39 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
function scopeCredentials(source, reference) {
|
|
42
|
+
return {
|
|
43
|
+
name: `${source.name}:${reference}`,
|
|
44
|
+
get: (field) => source.get(reference, field)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function chainScopedCredentials(...sources) {
|
|
48
|
+
return {
|
|
49
|
+
name: sources.map((source) => source.name).join("+"),
|
|
50
|
+
async get(field) {
|
|
51
|
+
let last;
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
try {
|
|
54
|
+
return await source.get(field);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
41
63
|
function staticConfig(config) {
|
|
64
|
+
const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
|
|
65
|
+
if (providers.size !== config.providers.length) {
|
|
66
|
+
throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
|
|
67
|
+
}
|
|
42
68
|
const health = new Map;
|
|
43
69
|
return {
|
|
44
70
|
name: "static",
|
|
45
71
|
async provider(instanceKey) {
|
|
46
|
-
const provider =
|
|
47
|
-
return provider ? {
|
|
72
|
+
const provider = providers.get(instanceKey);
|
|
73
|
+
return provider ? { ...provider, config: { ...provider.config } } : undefined;
|
|
48
74
|
},
|
|
49
75
|
async list(serviceKey, methodKey) {
|
|
50
76
|
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
@@ -201,7 +227,78 @@ function createRegistry(options) {
|
|
|
201
227
|
}
|
|
202
228
|
return { call, service };
|
|
203
229
|
}
|
|
204
|
-
|
|
230
|
+
function createService(definition, methods, options = {}) {
|
|
231
|
+
const providers = new Map;
|
|
232
|
+
const credentials = new Map;
|
|
233
|
+
const instances = [];
|
|
234
|
+
const serviceMethods = {};
|
|
235
|
+
for (const methodKey of Object.keys(definition.methods)) {
|
|
236
|
+
const attachments = methods[methodKey];
|
|
237
|
+
if (!Array.isArray(attachments)) {
|
|
238
|
+
throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
|
|
239
|
+
}
|
|
240
|
+
const priorities = new Set;
|
|
241
|
+
serviceMethods[methodKey] = attachments.map((attachment, index) => {
|
|
242
|
+
if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
|
|
243
|
+
throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
|
|
244
|
+
}
|
|
245
|
+
if (priorities.has(attachment.priority)) {
|
|
246
|
+
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
247
|
+
}
|
|
248
|
+
priorities.add(attachment.priority);
|
|
249
|
+
if (!attachment.provider.methods[attachment.method]) {
|
|
250
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
|
|
251
|
+
}
|
|
252
|
+
const existing = providers.get(attachment.provider.id);
|
|
253
|
+
if (existing && existing !== attachment.provider) {
|
|
254
|
+
throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
|
|
255
|
+
}
|
|
256
|
+
providers.set(attachment.provider.id, attachment.provider);
|
|
257
|
+
const internalKey = `${definition.key}:${methodKey}:${index}`;
|
|
258
|
+
credentials.set(internalKey, attachment.credentials);
|
|
259
|
+
instances.push({
|
|
260
|
+
instanceKey: internalKey,
|
|
261
|
+
providerId: attachment.provider.id,
|
|
262
|
+
enabled: attachment.enabled ?? true,
|
|
263
|
+
config: { ...attachment.config ?? {} },
|
|
264
|
+
secretRef: internalKey
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
instanceKey: internalKey,
|
|
268
|
+
providerMethod: attachment.method,
|
|
269
|
+
priority: attachment.priority,
|
|
270
|
+
enabled: attachment.enabled ?? true
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const registry = createRegistry({
|
|
275
|
+
providers: [...providers.values()],
|
|
276
|
+
config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
|
|
277
|
+
credentials: {
|
|
278
|
+
name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
|
|
279
|
+
async get(reference, field) {
|
|
280
|
+
const source = credentials.get(reference);
|
|
281
|
+
if (!source)
|
|
282
|
+
throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
|
|
283
|
+
return source.get(field);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
...options
|
|
287
|
+
});
|
|
288
|
+
const dynamic = registry.service(definition);
|
|
289
|
+
return {
|
|
290
|
+
async call(method, args, callOptions) {
|
|
291
|
+
const result = await dynamic.call(method, args, callOptions);
|
|
292
|
+
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
293
|
+
return {
|
|
294
|
+
...rest,
|
|
295
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
|
|
296
|
+
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
var VERSION = "0.1.11";
|
|
205
302
|
|
|
206
303
|
// src/http.ts
|
|
207
304
|
class BudgetExhausted extends ProviderError {
|
package/dist/chain.js
CHANGED
|
@@ -38,13 +38,39 @@ function chainCredentials(...sources) {
|
|
|
38
38
|
}
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
+
function scopeCredentials(source, reference) {
|
|
42
|
+
return {
|
|
43
|
+
name: `${source.name}:${reference}`,
|
|
44
|
+
get: (field) => source.get(reference, field)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function chainScopedCredentials(...sources) {
|
|
48
|
+
return {
|
|
49
|
+
name: sources.map((source) => source.name).join("+"),
|
|
50
|
+
async get(field) {
|
|
51
|
+
let last;
|
|
52
|
+
for (const source of sources) {
|
|
53
|
+
try {
|
|
54
|
+
return await source.get(field);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
last = error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
throw last instanceof Error ? last : new ProviderError("CREDENTIAL_MISSING", `No scoped source held field "${field}".`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
41
63
|
function staticConfig(config) {
|
|
64
|
+
const providers = new Map(config.providers.map((provider) => [provider.instanceKey, provider]));
|
|
65
|
+
if (providers.size !== config.providers.length) {
|
|
66
|
+
throw new ProviderError("CONFIG_DUPLICATE_INSTANCE", "Provider instance keys must be unique.");
|
|
67
|
+
}
|
|
42
68
|
const health = new Map;
|
|
43
69
|
return {
|
|
44
70
|
name: "static",
|
|
45
71
|
async provider(instanceKey) {
|
|
46
|
-
const provider =
|
|
47
|
-
return provider ? {
|
|
72
|
+
const provider = providers.get(instanceKey);
|
|
73
|
+
return provider ? { ...provider, config: { ...provider.config } } : undefined;
|
|
48
74
|
},
|
|
49
75
|
async list(serviceKey, methodKey) {
|
|
50
76
|
return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
|
|
@@ -201,7 +227,78 @@ function createRegistry(options) {
|
|
|
201
227
|
}
|
|
202
228
|
return { call, service };
|
|
203
229
|
}
|
|
204
|
-
|
|
230
|
+
function createService(definition, methods, options = {}) {
|
|
231
|
+
const providers = new Map;
|
|
232
|
+
const credentials = new Map;
|
|
233
|
+
const instances = [];
|
|
234
|
+
const serviceMethods = {};
|
|
235
|
+
for (const methodKey of Object.keys(definition.methods)) {
|
|
236
|
+
const attachments = methods[methodKey];
|
|
237
|
+
if (!Array.isArray(attachments)) {
|
|
238
|
+
throw new ProviderError("CONFIG_METHOD_MISSING", `Service method "${definition.key}.${methodKey}" needs an attachment array.`);
|
|
239
|
+
}
|
|
240
|
+
const priorities = new Set;
|
|
241
|
+
serviceMethods[methodKey] = attachments.map((attachment, index) => {
|
|
242
|
+
if (!Number.isInteger(attachment.priority) || attachment.priority < 1 || attachment.priority > 1000) {
|
|
243
|
+
throw new ProviderError("CONFIG_PRIORITY", `Priority for "${definition.key}.${methodKey}" must be an integer from 1 to 1000.`);
|
|
244
|
+
}
|
|
245
|
+
if (priorities.has(attachment.priority)) {
|
|
246
|
+
throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
|
|
247
|
+
}
|
|
248
|
+
priorities.add(attachment.priority);
|
|
249
|
+
if (!attachment.provider.methods[attachment.method]) {
|
|
250
|
+
throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support "${attachment.method}".`);
|
|
251
|
+
}
|
|
252
|
+
const existing = providers.get(attachment.provider.id);
|
|
253
|
+
if (existing && existing !== attachment.provider) {
|
|
254
|
+
throw new ProviderError("CONFIG_PROVIDER_DUPLICATE", `Provider id "${attachment.provider.id}" has more than one definition.`);
|
|
255
|
+
}
|
|
256
|
+
providers.set(attachment.provider.id, attachment.provider);
|
|
257
|
+
const internalKey = `${definition.key}:${methodKey}:${index}`;
|
|
258
|
+
credentials.set(internalKey, attachment.credentials);
|
|
259
|
+
instances.push({
|
|
260
|
+
instanceKey: internalKey,
|
|
261
|
+
providerId: attachment.provider.id,
|
|
262
|
+
enabled: attachment.enabled ?? true,
|
|
263
|
+
config: { ...attachment.config ?? {} },
|
|
264
|
+
secretRef: internalKey
|
|
265
|
+
});
|
|
266
|
+
return {
|
|
267
|
+
instanceKey: internalKey,
|
|
268
|
+
providerMethod: attachment.method,
|
|
269
|
+
priority: attachment.priority,
|
|
270
|
+
enabled: attachment.enabled ?? true
|
|
271
|
+
};
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const registry = createRegistry({
|
|
275
|
+
providers: [...providers.values()],
|
|
276
|
+
config: staticConfig({ providers: instances, services: { [definition.key]: serviceMethods } }),
|
|
277
|
+
credentials: {
|
|
278
|
+
name: [...new Set([...credentials.values()].map((source) => source.name))].join("+"),
|
|
279
|
+
async get(reference, field) {
|
|
280
|
+
const source = credentials.get(reference);
|
|
281
|
+
if (!source)
|
|
282
|
+
throw new ProviderError("CREDENTIAL_SOURCE_MISSING", "The provider credential source is unavailable.");
|
|
283
|
+
return source.get(field);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
...options
|
|
287
|
+
});
|
|
288
|
+
const dynamic = registry.service(definition);
|
|
289
|
+
return {
|
|
290
|
+
async call(method, args, callOptions) {
|
|
291
|
+
const result = await dynamic.call(method, args, callOptions);
|
|
292
|
+
const { instance: _instance, selected, attempts, ...rest } = result;
|
|
293
|
+
return {
|
|
294
|
+
...rest,
|
|
295
|
+
...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
|
|
296
|
+
attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
var VERSION = "0.1.11";
|
|
205
302
|
|
|
206
303
|
// src/chain.ts
|
|
207
304
|
var hexToNumber = (value) => Number(BigInt(value));
|