@forgezero/providers 0.1.19 → 0.1.20

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/dist/database.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.20";
302
340
 
303
341
  // src/database.ts
304
342
  var pools = new Map;
package/dist/email.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.20";
302
340
 
303
341
  // src/email.ts
304
342
  var recipients = (to) => Array.isArray(to) ? [...to] : [to];
@@ -332,6 +370,8 @@ var jetemailConfig = {
332
370
  }
333
371
  };
334
372
  var jetemailSend = defineProviderMethod({
373
+ version: "v1",
374
+ lifecycle: "current",
335
375
  async invoke(context, message) {
336
376
  assertMessage(message);
337
377
  const config = context.config;
@@ -382,6 +422,8 @@ function assertBatch(batch) {
382
422
  assertMessage(message);
383
423
  }
384
424
  var jetemailSendBatch = defineProviderMethod({
425
+ version: "v1",
426
+ lifecycle: "current",
385
427
  async invoke(context, batch) {
386
428
  assertBatch(batch);
387
429
  const config = context.config;
@@ -446,6 +488,8 @@ var jetemail = defineProvider({
446
488
  }
447
489
  });
448
490
  var smtpSend = defineProviderMethod({
491
+ version: "rfc5321",
492
+ lifecycle: "current",
449
493
  async invoke(context, message) {
450
494
  assertMessage(message);
451
495
  const config = context.config;
package/dist/http.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.20";
302
340
 
303
341
  // src/http.ts
304
342
  class BudgetExhausted extends ProviderError {
package/dist/index.d.ts CHANGED
@@ -80,6 +80,8 @@ export interface ServiceMethodAttachment {
80
80
  instanceKey: string;
81
81
  /** Explicit provider capability; it is never inferred from the service name. */
82
82
  providerMethod: string;
83
+ /** Exact provider API/contract branch. Omit only to select the provider's declared current branch. */
84
+ providerMethodVersion?: string;
83
85
  priority: number;
84
86
  enabled: boolean;
85
87
  health?: ProviderHealth;
@@ -90,7 +92,7 @@ export interface ConfigSource {
90
92
  provider(instanceKey: string): Promise<ProviderInstanceConfig | undefined>;
91
93
  list(serviceKey: string, methodKey: string): Promise<readonly ServiceMethodAttachment[]>;
92
94
  /** Persisted, because strikes that reset on restart retry a dead provider forever. */
93
- recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, health: ProviderHealth): Promise<void>;
95
+ recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, providerMethodVersion: string, health: ProviderHealth): Promise<void>;
94
96
  }
95
97
  export interface StaticRegistryConfig {
96
98
  providers: readonly ProviderInstanceConfig[];
@@ -116,12 +118,21 @@ export interface InvokeContext {
116
118
  signal?: AbortSignal;
117
119
  }
118
120
  export interface ProviderMethodSpec<Args = never, Result = never> {
121
+ /** Provider-owned contract/API branch, such as `v1` or `2026-08-01`. */
122
+ version: string;
123
+ lifecycle: 'current' | 'legacy' | 'deprecated' | 'retired';
119
124
  invoke(context: InvokeContext, args: Args): Promise<Result>;
120
125
  classify(error: unknown): FailureKind;
121
126
  }
122
127
  export declare function defineProviderMethod<Args, Result>(method: ProviderMethodSpec<Args, Result>): ProviderMethodSpec<Args, Result>;
123
128
  export type AnyProviderMethod = ProviderMethodSpec<any, any>;
124
- export type ProviderMethods = Record<string, AnyProviderMethod>;
129
+ export interface ProviderMethodBranches<Args = any, Result = any> {
130
+ currentVersion: string;
131
+ versions: Readonly<Record<string, ProviderMethodSpec<Args, Result>>>;
132
+ }
133
+ export declare function defineProviderMethodBranches<Args, Result>(branches: ProviderMethodBranches<Args, Result>): ProviderMethodBranches<Args, Result>;
134
+ export type ProviderMethodEntry = AnyProviderMethod | ProviderMethodBranches;
135
+ export type ProviderMethods = Record<string, ProviderMethodEntry>;
125
136
  /** A vendor/provider identity with named capabilities, independent of services. */
126
137
  export interface ProviderDefinition<Methods extends ProviderMethods = ProviderMethods> {
127
138
  id: string;
@@ -136,6 +147,8 @@ export declare function defineProvider<Methods extends ProviderMethods>(spec: Pr
136
147
  /** Convenience for a provider that exposes exactly one named method. */
137
148
  export declare function defineSingleMethodProvider<Method extends string, Args, Result>(spec: Omit<ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>, 'methods'> & {
138
149
  method: Method;
150
+ version?: string;
151
+ lifecycle?: ProviderMethodSpec['lifecycle'];
139
152
  invoke(context: InvokeContext, args: Args): Promise<Result>;
140
153
  classify(error: unknown): FailureKind;
141
154
  }): ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>;
@@ -144,6 +157,7 @@ export interface Attempt {
144
157
  providerId: string;
145
158
  instanceKey: string;
146
159
  providerMethod: string;
160
+ providerMethodVersion: string;
147
161
  outcome: 'sent' | 'skipped' | 'failed';
148
162
  kind?: FailureKind;
149
163
  durationMs?: number;
@@ -165,6 +179,7 @@ export interface CallResult<Result> {
165
179
  providerId: string;
166
180
  instanceKey: string;
167
181
  providerMethod: string;
182
+ providerMethodVersion: string;
168
183
  };
169
184
  fallbackUsed: boolean;
170
185
  attempts: readonly Attempt[];
@@ -196,6 +211,8 @@ type ServiceResult<T> = T extends ServiceMethodContract<unknown, infer Result> ?
196
211
  export interface DirectServiceAttachment {
197
212
  provider: ProviderDefinition;
198
213
  method: string;
214
+ /** Select an exact legacy/current branch. Omit to use the provider's declared currentVersion. */
215
+ version?: string;
199
216
  credentials: ScopedCredentialSource;
200
217
  config?: Record<string, unknown>;
201
218
  priority: number;
@@ -213,6 +230,7 @@ export type DirectCallResult<Result> = Omit<CallResult<Result>, 'instance' | 'se
213
230
  selected?: {
214
231
  providerId: string;
215
232
  providerMethod: string;
233
+ providerMethodVersion: string;
216
234
  };
217
235
  attempts: readonly DirectAttempt[];
218
236
  };
@@ -285,5 +303,5 @@ export interface EmailBatchResult {
285
303
  };
286
304
  results: readonly EmailBatchItemResult[];
287
305
  }
288
- export declare const VERSION = "0.1.19";
306
+ export declare const VERSION = "0.1.20";
289
307
  export {};