@forgezero/providers 0.1.19 → 0.1.21

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 CHANGED
@@ -59,7 +59,7 @@ bun test
59
59
  <a id="forgezero-providers"></a>
60
60
  ## @forgezero/providers
61
61
 
62
- The registry: priority, health, and a terminal-versus-retryable verdict per failure. This entry exposes 16 named value exports and 29 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
62
+ The registry: priority, health, and a terminal-versus-retryable verdict per failure. This entry exposes 17 named value exports and 31 named type exports. The generated import block lists one name per line for scanning and copying; keep only the names used by your file.
63
63
 
64
64
  ```text
65
65
  import {
@@ -72,6 +72,7 @@ import {
72
72
  createService,
73
73
  defineProvider,
74
74
  defineProviderMethod,
75
+ defineProviderMethodBranches,
75
76
  defineService,
76
77
  defineSingleMethodProvider,
77
78
  envCredentials,
@@ -102,6 +103,8 @@ import type {
102
103
  ProviderDefinition,
103
104
  ProviderHealth,
104
105
  ProviderInstanceConfig,
106
+ ProviderMethodBranches,
107
+ ProviderMethodEntry,
105
108
  ProviderMethodSpec,
106
109
  ProviderMethods,
107
110
  RegistryOptions,
@@ -204,7 +207,7 @@ import type {
204
207
 
205
208
  ## @forgezero/providers/email — Email with ordered provider fallback
206
209
 
207
- The service owns send/sendBatch. Attach provider methods directly in priority arrays; SMTP is never presented as batch-capable.
210
+ The service owns send/sendBatch. Ordered entries select exact provider method versions; SMTP is never presented as batch-capable.
208
211
 
209
212
  ```text
210
213
  import {
@@ -518,100 +521,154 @@ export const selectedCapability = RealtimeProviderError;
518
521
 
519
522
  ## Built-in provider capabilities
520
523
 
521
- 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.
524
+ A provider owns versioned capabilities; it does not own a service. A service method attaches an exact provider, method and version in an ordered array. Current, legacy and deprecated branches can coexist; retired branches never run.
522
525
 
523
526
  ```text
524
- evm-rpc request
525
- jetemail send, sendBatch
526
- smtp send
527
- arangodb query
528
- http request
529
- s3 request
530
- google-ai-studio translate
527
+ evm-rpc
528
+ - request
529
+ jetemail
530
+ - send
531
+ - sendBatch
532
+ smtp
533
+ - send
534
+ arangodb
535
+ - query
536
+ http
537
+ - request
538
+ s3
539
+ - request
540
+ google-ai-studio
541
+ - translate
531
542
  ```
532
543
 
533
544
  ## EVM JSON-RPC node (evm-rpc)
534
545
 
535
- 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.
546
+ Import: @forgezero/providers/chain. Each method below is a provider capability that a service attaches by exact version. 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.
536
547
 
537
548
  ```text
538
549
  importPath: '@forgezero/providers/chain'
539
550
  providerId: 'evm-rpc'
540
- methods: request
541
- credentials: url, bearer
542
- config: chainId, maxLogRange, timeoutMs
551
+ methods:
552
+ - request
553
+ credentials:
554
+ - url
555
+ - bearer
556
+ config:
557
+ - chainId
558
+ - maxLogRange
559
+ - timeoutMs
543
560
  ```
544
561
 
545
562
  ## JetEmail (jetemail)
546
563
 
547
- 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.
564
+ Import: @forgezero/providers/email. Each method below is a provider capability that a service attaches by exact version. 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.
548
565
 
549
566
  ```text
550
567
  importPath: '@forgezero/providers/email'
551
568
  providerId: 'jetemail'
552
- methods: send, sendBatch
553
- credentials: apiKey
554
- config: eu, from
569
+ methods:
570
+ - send
571
+ - sendBatch
572
+ credentials:
573
+ - apiKey
574
+ config:
575
+ - eu
576
+ - from
555
577
  ```
556
578
 
557
579
  ## SMTP (smtp)
558
580
 
559
- 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.
581
+ Import: @forgezero/providers/email. Each method below is a provider capability that a service attaches by exact version. 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.
560
582
 
561
583
  ```text
562
584
  importPath: '@forgezero/providers/email'
563
585
  providerId: 'smtp'
564
- methods: send
565
- credentials: user, password
566
- config: host, port, secure, from, transport
586
+ methods:
587
+ - send
588
+ credentials:
589
+ - user
590
+ - password
591
+ config:
592
+ - host
593
+ - port
594
+ - secure
595
+ - from
596
+ - transport
567
597
  ```
568
598
 
569
599
  ## ArangoDB (arangodb)
570
600
 
571
- 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.
601
+ Import: @forgezero/providers/database. Each method below is a provider capability that a service attaches by exact version. 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.
572
602
 
573
603
  ```text
574
604
  importPath: '@forgezero/providers/database'
575
605
  providerId: 'arangodb'
576
- methods: query
577
- credentials: password
578
- config: url, urls, readPreferredUrls, readPreferredFallback, clusterId, database, username
606
+ methods:
607
+ - query
608
+ credentials:
609
+ - password
610
+ config:
611
+ - url
612
+ - urls
613
+ - readPreferredUrls
614
+ - readPreferredFallback
615
+ - clusterId
616
+ - database
617
+ - username
579
618
  ```
580
619
 
581
620
  ## HTTP (http)
582
621
 
583
- 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.
622
+ Import: @forgezero/providers/http. Each method below is a provider capability that a service attaches by exact version. 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.
584
623
 
585
624
  ```text
586
625
  importPath: '@forgezero/providers/http'
587
626
  providerId: 'http'
588
- methods: request
589
- credentials: apiKey, apiSecret
590
- config: baseUrl, limit, windowMs, headroom
627
+ methods:
628
+ - request
629
+ credentials:
630
+ - apiKey
631
+ - apiSecret
632
+ config:
633
+ - baseUrl
634
+ - limit
635
+ - windowMs
636
+ - headroom
591
637
  ```
592
638
 
593
639
  ## S3-compatible storage (s3)
594
640
 
595
- 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.
641
+ Import: @forgezero/providers/storage. Each method below is a provider capability that a service attaches by exact version. 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.
596
642
 
597
643
  ```text
598
644
  importPath: '@forgezero/providers/storage'
599
645
  providerId: 's3'
600
- methods: request
601
- credentials: accessKeyId, secretAccessKey, sessionToken
602
- config: endpoint, region, bucket, addressing
646
+ methods:
647
+ - request
648
+ credentials:
649
+ - accessKeyId
650
+ - secretAccessKey
651
+ - sessionToken
652
+ config:
653
+ - endpoint
654
+ - region
655
+ - bucket
656
+ - addressing
603
657
  ```
604
658
 
605
659
  ## Google AI Studio (google-ai-studio)
606
660
 
607
- 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.
661
+ Import: @forgezero/providers/translation. Each method below is a provider capability that a service attaches by exact version. 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.
608
662
 
609
663
  ```text
610
664
  importPath: '@forgezero/providers/translation'
611
665
  providerId: 'google-ai-studio'
612
- methods: translate
613
- credentials: apiKey
614
- config: model
666
+ methods:
667
+ - translate
668
+ credentials:
669
+ - apiKey
670
+ config:
671
+ - model
615
672
  ```
616
673
 
617
674
  Full rendered documentation: https://www.forgezero.net/docs/providers
package/dist/binance.js CHANGED
@@ -75,25 +75,55 @@ function staticConfig(config) {
75
75
  async list(serviceKey, methodKey) {
76
76
  return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
77
  ...attachment,
78
- health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
78
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:${attachment.providerMethodVersion ?? "current"}`) ?? (attachment.providerMethodVersion === undefined ? [...health.entries()].find(([key]) => key.startsWith(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:`))?.[1] : undefined) ?? attachment.health
79
79
  }));
80
80
  },
81
- async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
82
- health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
81
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, providerMethodVersion, next) {
82
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}:${providerMethodVersion}`, next);
83
83
  }
84
84
  };
85
85
  }
86
86
  function defineProviderMethod(method) {
87
87
  return method;
88
88
  }
89
+ function defineProviderMethodBranches(branches) {
90
+ const entries = Object.entries(branches.versions);
91
+ if (entries.length === 0 || !branches.versions[branches.currentVersion]) {
92
+ throw new ProviderError("PROVIDER_METHOD_VERSION", "A versioned provider method needs a current branch.");
93
+ }
94
+ for (const [version, method] of entries) {
95
+ if (method.version !== version) {
96
+ throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method branch "${version}" declares version "${method.version}".`);
97
+ }
98
+ if (version === branches.currentVersion && method.lifecycle !== "current") {
99
+ throw new ProviderError("PROVIDER_METHOD_VERSION", `Current provider method branch "${version}" must have lifecycle current.`);
100
+ }
101
+ }
102
+ return branches;
103
+ }
104
+ var methodBranch = (provider, methodName, version) => {
105
+ const entry = provider?.methods[methodName];
106
+ if (!entry)
107
+ return;
108
+ if ("versions" in entry)
109
+ return entry.versions[version ?? entry.currentVersion];
110
+ return version === undefined || version === entry.version ? entry : undefined;
111
+ };
89
112
  function defineProvider(spec) {
113
+ for (const [name, entry] of Object.entries(spec.methods)) {
114
+ if ("versions" in entry)
115
+ defineProviderMethodBranches(entry);
116
+ else if (!entry.version || !["current", "legacy", "deprecated", "retired"].includes(entry.lifecycle)) {
117
+ throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method "${spec.id}.${name}" needs a version and lifecycle.`);
118
+ }
119
+ }
90
120
  return spec;
91
121
  }
92
122
  function defineSingleMethodProvider(spec) {
93
- const { method, invoke, classify, ...identity } = spec;
123
+ const { method, version = "v1", lifecycle = "current", invoke, classify, ...identity } = spec;
94
124
  return defineProvider({
95
125
  ...identity,
96
- methods: { [method]: defineProviderMethod({ invoke, classify }) }
126
+ methods: { [method]: defineProviderMethod({ version, lifecycle, invoke, classify }) }
97
127
  });
98
128
  }
99
129
  var STRIKES_TO_OFFLINE = 3;
@@ -121,6 +151,7 @@ function createRegistry(options) {
121
151
  const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
122
152
  const attempts = [];
123
153
  for (const attachment of configured) {
154
+ const requestedVersion = attachment.providerMethodVersion;
124
155
  if (callOptions.signal?.aborted) {
125
156
  const cancelled = {
126
157
  ok: false,
@@ -133,21 +164,25 @@ function createRegistry(options) {
133
164
  }
134
165
  const instance = await options.config.provider(attachment.instanceKey);
135
166
  if (!instance) {
136
- attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
167
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: "instance not registered" });
137
168
  continue;
138
169
  }
139
170
  const spec = byId.get(instance.providerId);
140
- const method = spec?.methods[attachment.providerMethod];
171
+ const method = methodBranch(spec, attachment.providerMethod, requestedVersion);
141
172
  if (!spec || !method) {
142
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
173
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: !spec ? "provider not registered" : "method version not supported" });
174
+ continue;
175
+ }
176
+ if (method.lifecycle === "retired") {
177
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "method version retired" });
143
178
  continue;
144
179
  }
145
180
  if (!instance.enabled) {
146
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
181
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "instance disabled" });
147
182
  continue;
148
183
  }
149
184
  if (attachment.health?.status === "offline") {
150
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
185
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "offline" });
151
186
  continue;
152
187
  }
153
188
  await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
@@ -158,15 +193,15 @@ function createRegistry(options) {
158
193
  secret: (field) => options.credentials.get(instance.secretRef, field),
159
194
  signal: callOptions.signal
160
195
  }, args);
161
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
162
- await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
196
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "sent", durationMs: performance.now() - startedAt });
197
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, "success"));
163
198
  const sent = {
164
199
  ok: true,
165
200
  result,
166
201
  provider: instance.providerId,
167
202
  instance: instance.instanceKey,
168
203
  method: attachment.providerMethod,
169
- selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
204
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version },
170
205
  fallbackUsed: attempts.length > 1,
171
206
  attempts
172
207
  };
@@ -190,13 +225,14 @@ function createRegistry(options) {
190
225
  providerId: instance.providerId,
191
226
  instanceKey: instance.instanceKey,
192
227
  providerMethod: attachment.providerMethod,
228
+ providerMethodVersion: method.version,
193
229
  outcome: "failed",
194
230
  kind,
195
231
  durationMs: performance.now() - startedAt,
196
232
  failure: { kind, ...code ? { code } : {}, message },
197
233
  error: message
198
234
  });
199
- await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
235
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, kind));
200
236
  if (kind === "terminal") {
201
237
  const refused = {
202
238
  ok: false,
@@ -246,8 +282,9 @@ function createService(definition, methods, options = {}) {
246
282
  throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
247
283
  }
248
284
  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}".`);
285
+ const branch = methodBranch(attachment.provider, attachment.method, attachment.version);
286
+ if (!branch || branch.lifecycle === "retired") {
287
+ throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support active method "${attachment.method}@${attachment.version ?? "current"}".`);
251
288
  }
252
289
  const existing = providers.get(attachment.provider.id);
253
290
  if (existing && existing !== attachment.provider) {
@@ -266,6 +303,7 @@ function createService(definition, methods, options = {}) {
266
303
  return {
267
304
  instanceKey: internalKey,
268
305
  providerMethod: attachment.method,
306
+ providerMethodVersion: branch.version,
269
307
  priority: attachment.priority,
270
308
  enabled: attachment.enabled ?? true
271
309
  };
@@ -292,13 +330,13 @@ function createService(definition, methods, options = {}) {
292
330
  const { instance: _instance, selected, attempts, ...rest } = result;
293
331
  return {
294
332
  ...rest,
295
- ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
333
+ ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod, providerMethodVersion: selected.providerMethodVersion } } : {},
296
334
  attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
297
335
  };
298
336
  }
299
337
  };
300
338
  }
301
- var VERSION = "0.1.19";
339
+ var VERSION = "0.1.21";
302
340
 
303
341
  // src/http.ts
304
342
  class BudgetExhausted extends ProviderError {
package/dist/chain.js CHANGED
@@ -75,25 +75,55 @@ function staticConfig(config) {
75
75
  async list(serviceKey, methodKey) {
76
76
  return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
77
  ...attachment,
78
- health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
78
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:${attachment.providerMethodVersion ?? "current"}`) ?? (attachment.providerMethodVersion === undefined ? [...health.entries()].find(([key]) => key.startsWith(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}:`))?.[1] : undefined) ?? attachment.health
79
79
  }));
80
80
  },
81
- async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
82
- health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
81
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, providerMethodVersion, next) {
82
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}:${providerMethodVersion}`, next);
83
83
  }
84
84
  };
85
85
  }
86
86
  function defineProviderMethod(method) {
87
87
  return method;
88
88
  }
89
+ function defineProviderMethodBranches(branches) {
90
+ const entries = Object.entries(branches.versions);
91
+ if (entries.length === 0 || !branches.versions[branches.currentVersion]) {
92
+ throw new ProviderError("PROVIDER_METHOD_VERSION", "A versioned provider method needs a current branch.");
93
+ }
94
+ for (const [version, method] of entries) {
95
+ if (method.version !== version) {
96
+ throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method branch "${version}" declares version "${method.version}".`);
97
+ }
98
+ if (version === branches.currentVersion && method.lifecycle !== "current") {
99
+ throw new ProviderError("PROVIDER_METHOD_VERSION", `Current provider method branch "${version}" must have lifecycle current.`);
100
+ }
101
+ }
102
+ return branches;
103
+ }
104
+ var methodBranch = (provider, methodName, version) => {
105
+ const entry = provider?.methods[methodName];
106
+ if (!entry)
107
+ return;
108
+ if ("versions" in entry)
109
+ return entry.versions[version ?? entry.currentVersion];
110
+ return version === undefined || version === entry.version ? entry : undefined;
111
+ };
89
112
  function defineProvider(spec) {
113
+ for (const [name, entry] of Object.entries(spec.methods)) {
114
+ if ("versions" in entry)
115
+ defineProviderMethodBranches(entry);
116
+ else if (!entry.version || !["current", "legacy", "deprecated", "retired"].includes(entry.lifecycle)) {
117
+ throw new ProviderError("PROVIDER_METHOD_VERSION", `Provider method "${spec.id}.${name}" needs a version and lifecycle.`);
118
+ }
119
+ }
90
120
  return spec;
91
121
  }
92
122
  function defineSingleMethodProvider(spec) {
93
- const { method, invoke, classify, ...identity } = spec;
123
+ const { method, version = "v1", lifecycle = "current", invoke, classify, ...identity } = spec;
94
124
  return defineProvider({
95
125
  ...identity,
96
- methods: { [method]: defineProviderMethod({ invoke, classify }) }
126
+ methods: { [method]: defineProviderMethod({ version, lifecycle, invoke, classify }) }
97
127
  });
98
128
  }
99
129
  var STRIKES_TO_OFFLINE = 3;
@@ -121,6 +151,7 @@ function createRegistry(options) {
121
151
  const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
122
152
  const attempts = [];
123
153
  for (const attachment of configured) {
154
+ const requestedVersion = attachment.providerMethodVersion;
124
155
  if (callOptions.signal?.aborted) {
125
156
  const cancelled = {
126
157
  ok: false,
@@ -133,21 +164,25 @@ function createRegistry(options) {
133
164
  }
134
165
  const instance = await options.config.provider(attachment.instanceKey);
135
166
  if (!instance) {
136
- attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
167
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: "instance not registered" });
137
168
  continue;
138
169
  }
139
170
  const spec = byId.get(instance.providerId);
140
- const method = spec?.methods[attachment.providerMethod];
171
+ const method = methodBranch(spec, attachment.providerMethod, requestedVersion);
141
172
  if (!spec || !method) {
142
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: !spec ? "provider not registered" : "method not supported" });
173
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: requestedVersion ?? "current", outcome: "skipped", error: !spec ? "provider not registered" : "method version not supported" });
174
+ continue;
175
+ }
176
+ if (method.lifecycle === "retired") {
177
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "method version retired" });
143
178
  continue;
144
179
  }
145
180
  if (!instance.enabled) {
146
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
181
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "instance disabled" });
147
182
  continue;
148
183
  }
149
184
  if (attachment.health?.status === "offline") {
150
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
185
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "skipped", error: "offline" });
151
186
  continue;
152
187
  }
153
188
  await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
@@ -158,15 +193,15 @@ function createRegistry(options) {
158
193
  secret: (field) => options.credentials.get(instance.secretRef, field),
159
194
  signal: callOptions.signal
160
195
  }, args);
161
- attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "sent", durationMs: performance.now() - startedAt });
162
- await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, "success"));
196
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version, outcome: "sent", durationMs: performance.now() - startedAt });
197
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, "success"));
163
198
  const sent = {
164
199
  ok: true,
165
200
  result,
166
201
  provider: instance.providerId,
167
202
  instance: instance.instanceKey,
168
203
  method: attachment.providerMethod,
169
- selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
204
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, providerMethodVersion: method.version },
170
205
  fallbackUsed: attempts.length > 1,
171
206
  attempts
172
207
  };
@@ -190,13 +225,14 @@ function createRegistry(options) {
190
225
  providerId: instance.providerId,
191
226
  instanceKey: instance.instanceKey,
192
227
  providerMethod: attachment.providerMethod,
228
+ providerMethodVersion: method.version,
193
229
  outcome: "failed",
194
230
  kind,
195
231
  durationMs: performance.now() - startedAt,
196
232
  failure: { kind, ...code ? { code } : {}, message },
197
233
  error: message
198
234
  });
199
- await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
235
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, method.version, nextHealth(attachment.health, kind));
200
236
  if (kind === "terminal") {
201
237
  const refused = {
202
238
  ok: false,
@@ -246,8 +282,9 @@ function createService(definition, methods, options = {}) {
246
282
  throw new ProviderError("CONFIG_PRIORITY_DUPLICATE", `Priorities for "${definition.key}.${methodKey}" must be unique.`);
247
283
  }
248
284
  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}".`);
285
+ const branch = methodBranch(attachment.provider, attachment.method, attachment.version);
286
+ if (!branch || branch.lifecycle === "retired") {
287
+ throw new ProviderError("CONFIG_METHOD_UNSUPPORTED", `Provider "${attachment.provider.id}" does not support active method "${attachment.method}@${attachment.version ?? "current"}".`);
251
288
  }
252
289
  const existing = providers.get(attachment.provider.id);
253
290
  if (existing && existing !== attachment.provider) {
@@ -266,6 +303,7 @@ function createService(definition, methods, options = {}) {
266
303
  return {
267
304
  instanceKey: internalKey,
268
305
  providerMethod: attachment.method,
306
+ providerMethodVersion: branch.version,
269
307
  priority: attachment.priority,
270
308
  enabled: attachment.enabled ?? true
271
309
  };
@@ -292,13 +330,13 @@ function createService(definition, methods, options = {}) {
292
330
  const { instance: _instance, selected, attempts, ...rest } = result;
293
331
  return {
294
332
  ...rest,
295
- ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod } } : {},
333
+ ...selected ? { selected: { providerId: selected.providerId, providerMethod: selected.providerMethod, providerMethodVersion: selected.providerMethodVersion } } : {},
296
334
  attempts: attempts.map(({ instanceKey: _instanceKey, ...attempt }) => attempt)
297
335
  };
298
336
  }
299
337
  };
300
338
  }
301
- var VERSION = "0.1.19";
339
+ var VERSION = "0.1.21";
302
340
 
303
341
  // src/chain.ts
304
342
  var hexToNumber = (value) => Number(BigInt(value));