@forgezero/providers 0.1.9 → 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/dist/http.js CHANGED
@@ -38,25 +38,71 @@ function chainCredentials(...sources) {
38
38
  }
39
39
  };
40
40
  }
41
- function staticConfig(services) {
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
+ }
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
- async list(serviceKey) {
46
- return (services[serviceKey] ?? []).map((provider) => ({
47
- ...provider,
48
- health: health.get(`${serviceKey}:${provider.providerId}`) ?? provider.health
71
+ async provider(instanceKey) {
72
+ const provider = providers.get(instanceKey);
73
+ return provider ? { ...provider, config: { ...provider.config } } : undefined;
74
+ },
75
+ async list(serviceKey, methodKey) {
76
+ return (config.services[serviceKey]?.[methodKey] ?? []).map((attachment) => ({
77
+ ...attachment,
78
+ health: health.get(`${serviceKey}:${methodKey}:${attachment.instanceKey}:${attachment.providerMethod}`) ?? attachment.health
49
79
  }));
50
80
  },
51
- async recordHealth(serviceKey, providerId, next) {
52
- health.set(`${serviceKey}:${providerId}`, next);
81
+ async recordHealth(serviceKey, methodKey, instanceKey, providerMethod, next) {
82
+ health.set(`${serviceKey}:${methodKey}:${instanceKey}:${providerMethod}`, next);
53
83
  }
54
84
  };
55
85
  }
86
+ function defineProviderMethod(method) {
87
+ return method;
88
+ }
56
89
  function defineProvider(spec) {
57
90
  return spec;
58
91
  }
92
+ function defineSingleMethodProvider(spec) {
93
+ const { method, invoke, classify, ...identity } = spec;
94
+ return defineProvider({
95
+ ...identity,
96
+ methods: { [method]: defineProviderMethod({ invoke, classify }) }
97
+ });
98
+ }
59
99
  var STRIKES_TO_OFFLINE = 3;
100
+ function serviceMethod() {
101
+ return Object.freeze({});
102
+ }
103
+ function defineService(definition) {
104
+ return definition;
105
+ }
60
106
  function nextHealth(current, kind) {
61
107
  if (kind === "success")
62
108
  return { strikes: 0, status: "ok" };
@@ -71,80 +117,188 @@ function nextHealth(current, kind) {
71
117
  }
72
118
  function createRegistry(options) {
73
119
  const byId = new Map(options.providers.map((provider) => [provider.id, provider]));
74
- async function call(serviceKey, args, callOptions = {}) {
75
- const configured = [...await options.config.list(serviceKey)].filter((provider) => provider.enabled).sort((a, b) => a.priority - b.priority);
120
+ async function call(serviceKey, methodKey, args, callOptions = {}) {
121
+ const configured = [...await options.config.list(serviceKey, methodKey)].filter((attachment) => attachment.enabled).sort((a, b) => a.priority - b.priority);
76
122
  const attempts = [];
77
- for (const entry of configured) {
123
+ for (const attachment of configured) {
78
124
  if (callOptions.signal?.aborted) {
79
125
  const cancelled = {
80
126
  ok: false,
127
+ fallbackUsed: attempts.length > 0,
81
128
  attempts,
82
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
129
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
83
130
  };
84
- options.after?.(cancelled);
131
+ await options.after?.(cancelled);
85
132
  return cancelled;
86
133
  }
87
- const spec = byId.get(entry.providerId);
88
- if (!spec) {
89
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "not registered" });
134
+ const instance = await options.config.provider(attachment.instanceKey);
135
+ if (!instance) {
136
+ attempts.push({ providerId: "unknown", instanceKey: attachment.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance not registered" });
90
137
  continue;
91
138
  }
92
- if (entry.health?.status === "offline") {
93
- attempts.push({ providerId: entry.providerId, outcome: "skipped", error: "offline" });
139
+ const spec = byId.get(instance.providerId);
140
+ const method = spec?.methods[attachment.providerMethod];
141
+ 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" });
94
143
  continue;
95
144
  }
96
- options.before?.({ service: serviceKey, provider: entry.providerId });
145
+ if (!instance.enabled) {
146
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "instance disabled" });
147
+ continue;
148
+ }
149
+ if (attachment.health?.status === "offline") {
150
+ attempts.push({ providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod, outcome: "skipped", error: "offline" });
151
+ continue;
152
+ }
153
+ await options.before?.({ service: serviceKey, method: methodKey, provider: instance.providerId, instance: instance.instanceKey });
154
+ const startedAt = performance.now();
97
155
  try {
98
- const result = await spec.invoke({
99
- config: entry.config,
100
- secret: (field) => options.credentials.get(entry.secretRef, field),
156
+ const result = await method.invoke({
157
+ config: instance.config,
158
+ secret: (field) => options.credentials.get(instance.secretRef, field),
101
159
  signal: callOptions.signal
102
160
  }, args);
103
- attempts.push({ providerId: entry.providerId, outcome: "sent" });
104
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, "success"));
105
- const sent = { ok: true, result, provider: entry.providerId, attempts };
106
- options.after?.(sent);
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"));
163
+ const sent = {
164
+ ok: true,
165
+ result,
166
+ provider: instance.providerId,
167
+ instance: instance.instanceKey,
168
+ method: attachment.providerMethod,
169
+ selected: { providerId: instance.providerId, instanceKey: instance.instanceKey, providerMethod: attachment.providerMethod },
170
+ fallbackUsed: attempts.length > 1,
171
+ attempts
172
+ };
173
+ await options.after?.(sent);
107
174
  return sent;
108
175
  } catch (error) {
109
176
  if (callOptions.signal?.aborted) {
110
177
  const cancelled = {
111
178
  ok: false,
179
+ fallbackUsed: attempts.length > 0,
112
180
  attempts,
113
- error: new ProviderError("CALL_ABORTED", `The "${serviceKey}" call was cancelled.`)
181
+ error: new ProviderError("CALL_ABORTED", `The "${serviceKey}.${methodKey}" call was cancelled.`)
114
182
  };
115
- options.after?.(cancelled);
183
+ await options.after?.(cancelled);
116
184
  return cancelled;
117
185
  }
118
- const kind = spec.classify(error);
186
+ const kind = method.classify(error);
187
+ const message = error instanceof Error ? error.message : String(error);
188
+ const code = error instanceof ProviderError ? error.code : typeof error?.code === "string" ? error.code : undefined;
119
189
  attempts.push({
120
- providerId: entry.providerId,
190
+ providerId: instance.providerId,
191
+ instanceKey: instance.instanceKey,
192
+ providerMethod: attachment.providerMethod,
121
193
  outcome: "failed",
122
194
  kind,
123
- error: error instanceof Error ? error.message : String(error)
195
+ durationMs: performance.now() - startedAt,
196
+ failure: { kind, ...code ? { code } : {}, message },
197
+ error: message
124
198
  });
125
- await options.config.recordHealth(serviceKey, entry.providerId, nextHealth(entry.health, kind));
199
+ await options.config.recordHealth(serviceKey, methodKey, instance.instanceKey, attachment.providerMethod, nextHealth(attachment.health, kind));
126
200
  if (kind === "terminal") {
127
201
  const refused = {
128
202
  ok: false,
203
+ fallbackUsed: attempts.length > 1,
129
204
  attempts,
130
205
  error: new ProviderError("PAYLOAD_REJECTED", "The request was refused as malformed; no provider will accept it.")
131
206
  };
132
- options.after?.(refused);
207
+ await options.after?.(refused);
133
208
  return refused;
134
209
  }
135
210
  }
136
211
  }
137
212
  const failed = {
138
213
  ok: false,
214
+ fallbackUsed: attempts.length > 1,
139
215
  attempts,
140
- error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider is configured for "${serviceKey}".` : `Every provider for "${serviceKey}" failed or was skipped.`)
216
+ error: new ProviderError(attempts.length === 0 ? "NO_PROVIDER" : "ALL_PROVIDERS_FAILED", attempts.length === 0 ? `No provider method is configured for "${serviceKey}.${methodKey}".` : `Every provider method for "${serviceKey}.${methodKey}" failed or was skipped.`)
141
217
  };
142
- options.after?.(failed);
218
+ await options.after?.(failed);
143
219
  return failed;
144
220
  }
145
- return { call };
221
+ function service(definition) {
222
+ return {
223
+ call(method, args, callOptions) {
224
+ return call(definition.key, method, args, callOptions);
225
+ }
226
+ };
227
+ }
228
+ return { call, service };
229
+ }
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
+ };
146
300
  }
147
- var VERSION = "0.1.9";
301
+ var VERSION = "0.1.11";
148
302
 
149
303
  // src/http.ts
150
304
  class BudgetExhausted extends ProviderError {
@@ -227,9 +381,9 @@ function createHttpClient(config) {
227
381
  budget: () => budgetState(config.budget.host)
228
382
  };
229
383
  }
230
- var http = defineProvider({
384
+ var http = defineSingleMethodProvider({
231
385
  id: "http",
232
- service: "http",
386
+ method: "request",
233
387
  label: "HTTP",
234
388
  multiInstance: true,
235
389
  credentials: {
package/dist/index.d.ts CHANGED
@@ -39,6 +39,11 @@ export interface CredentialSource {
39
39
  readonly name: string;
40
40
  get(reference: string, field: string): Promise<string>;
41
41
  }
42
+ /** Credentials already scoped to one provider identity. */
43
+ export interface ScopedCredentialSource {
44
+ readonly name: string;
45
+ get(field: string): Promise<string>;
46
+ }
42
47
  /** Environment variables. `smtp` + `password` → `SMTP_PASSWORD`. */
43
48
  export declare function envCredentials(env: Record<string, string | undefined>): CredentialSource;
44
49
  /**
@@ -50,30 +55,48 @@ export declare function envCredentials(env: Record<string, string | undefined>):
50
55
  * a test that locks the vault and asserts mail still sends.
51
56
  */
52
57
  export declare function chainCredentials(...sources: readonly CredentialSource[]): CredentialSource;
58
+ /** Bind a reference once so a provider receives only its own credential fields. */
59
+ export declare function scopeCredentials(source: CredentialSource, reference: string): ScopedCredentialSource;
60
+ /** Try provider-scoped sources in order, for example Vault then systemd. */
61
+ export declare function chainScopedCredentials(...sources: readonly ScopedCredentialSource[]): ScopedCredentialSource;
53
62
  export interface ProviderHealth {
54
63
  strikes: number;
55
64
  status: 'ok' | 'degraded' | 'offline';
56
65
  lastFailureAtTs?: number;
57
66
  }
58
- export interface ProviderConfig {
67
+ /** One configured provider identity. It is independent of every service. */
68
+ export interface ProviderInstanceConfig {
69
+ /** Stable key used by service-method attachments. */
70
+ instanceKey: string;
59
71
  providerId: string;
60
- /** Named relay, for providers configurable more than once. */
61
- instanceKey?: string;
62
- priority: number;
72
+ /** Disables every method on this instance without deleting its configuration. */
63
73
  enabled: boolean;
64
74
  config: Record<string, unknown>;
65
75
  /** Names a vault entry. NEVER the secret itself — a dump yields metadata. */
66
76
  secretRef: string;
77
+ }
78
+ /** One service method's attachment to one provider instance method. */
79
+ export interface ServiceMethodAttachment {
80
+ instanceKey: string;
81
+ /** Explicit provider capability; it is never inferred from the service name. */
82
+ providerMethod: string;
83
+ priority: number;
84
+ enabled: boolean;
67
85
  health?: ProviderHealth;
68
86
  }
69
- /** Where the ordered list lives: a database, a file, constants. */
87
+ /** Provider identities and service routing policy may live in any store. */
70
88
  export interface ConfigSource {
71
89
  readonly name: string;
72
- list(serviceKey: string): Promise<readonly ProviderConfig[]>;
90
+ provider(instanceKey: string): Promise<ProviderInstanceConfig | undefined>;
91
+ list(serviceKey: string, methodKey: string): Promise<readonly ServiceMethodAttachment[]>;
73
92
  /** Persisted, because strikes that reset on restart retry a dead provider forever. */
74
- recordHealth(serviceKey: string, providerId: string, health: ProviderHealth): Promise<void>;
93
+ recordHealth(serviceKey: string, methodKey: string, instanceKey: string, providerMethod: string, health: ProviderHealth): Promise<void>;
75
94
  }
76
- export declare function staticConfig(services: Record<string, readonly ProviderConfig[]>): ConfigSource;
95
+ export interface StaticRegistryConfig {
96
+ providers: readonly ProviderInstanceConfig[];
97
+ services: Record<string, Record<string, readonly ServiceMethodAttachment[]>>;
98
+ }
99
+ export declare function staticConfig(config: StaticRegistryConfig): ConfigSource;
77
100
  /**
78
101
  * How a failure should be treated.
79
102
  *
@@ -92,29 +115,58 @@ export interface InvokeContext {
92
115
  secret(field: string): Promise<string>;
93
116
  signal?: AbortSignal;
94
117
  }
95
- export interface ProviderSpec<Args = never, Result = never> {
118
+ export interface ProviderMethodSpec<Args = never, Result = never> {
119
+ invoke(context: InvokeContext, args: Args): Promise<Result>;
120
+ classify(error: unknown): FailureKind;
121
+ }
122
+ export declare function defineProviderMethod<Args, Result>(method: ProviderMethodSpec<Args, Result>): ProviderMethodSpec<Args, Result>;
123
+ export type AnyProviderMethod = ProviderMethodSpec<any, any>;
124
+ export type ProviderMethods = Record<string, AnyProviderMethod>;
125
+ /** A vendor/provider identity with named capabilities, independent of services. */
126
+ export interface ProviderDefinition<Methods extends ProviderMethods = ProviderMethods> {
96
127
  id: string;
97
- service: string;
98
128
  label: string;
99
129
  multiInstance?: boolean;
100
130
  /** JSON Schema. `writeOnly` fields route to secret storage. */
101
131
  credentials?: Record<string, unknown>;
102
132
  config?: Record<string, unknown>;
133
+ methods: Methods;
134
+ }
135
+ export declare function defineProvider<Methods extends ProviderMethods>(spec: ProviderDefinition<Methods>): ProviderDefinition<Methods>;
136
+ /** Convenience for a provider that exposes exactly one named method. */
137
+ export declare function defineSingleMethodProvider<Method extends string, Args, Result>(spec: Omit<ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>, 'methods'> & {
138
+ method: Method;
103
139
  invoke(context: InvokeContext, args: Args): Promise<Result>;
104
140
  classify(error: unknown): FailureKind;
105
- }
106
- export declare function defineProvider<Args, Result>(spec: ProviderSpec<Args, Result>): ProviderSpec<Args, Result>;
141
+ }): ProviderDefinition<Record<Method, ProviderMethodSpec<Args, Result>>>;
107
142
  export declare const STRIKES_TO_OFFLINE = 3;
108
143
  export interface Attempt {
109
144
  providerId: string;
145
+ instanceKey: string;
146
+ providerMethod: string;
110
147
  outcome: 'sent' | 'skipped' | 'failed';
111
148
  kind?: FailureKind;
149
+ durationMs?: number;
150
+ failure?: {
151
+ kind: FailureKind;
152
+ code?: string;
153
+ message: string;
154
+ };
155
+ /** @deprecated Read `failure.message`; retained for 0.1 compatibility. */
112
156
  error?: string;
113
157
  }
114
158
  export interface CallResult<Result> {
115
159
  ok: boolean;
116
160
  result?: Result;
117
161
  provider?: string;
162
+ instance?: string;
163
+ method?: string;
164
+ selected?: {
165
+ providerId: string;
166
+ instanceKey: string;
167
+ providerMethod: string;
168
+ };
169
+ fallbackUsed: boolean;
118
170
  attempts: readonly Attempt[];
119
171
  error?: ProviderError;
120
172
  }
@@ -122,6 +174,48 @@ export interface CallOptions {
122
174
  /** Reaches provider fetch/socket code; cancellation never falls through to another vendor. */
123
175
  signal?: AbortSignal;
124
176
  }
177
+ /** Type-only input/output contract for one service method. */
178
+ export interface ServiceMethodContract<Args, Result> {
179
+ readonly __args?: (args: Args) => void;
180
+ readonly __result?: Result;
181
+ }
182
+ export declare function serviceMethod<Args, Result>(): ServiceMethodContract<Args, Result>;
183
+ export type ServiceMethods = Record<string, ServiceMethodContract<any, any>>;
184
+ export interface ServiceDefinition<Key extends string, Methods extends ServiceMethods> {
185
+ key: Key;
186
+ methods: Methods;
187
+ }
188
+ export declare function defineService<Key extends string, Methods extends ServiceMethods>(definition: ServiceDefinition<Key, Methods>): ServiceDefinition<Key, Methods>;
189
+ type ServiceArgs<T> = T extends ServiceMethodContract<infer Args, unknown> ? Args : never;
190
+ type ServiceResult<T> = T extends ServiceMethodContract<unknown, infer Result> ? Result : never;
191
+ /**
192
+ * One direct service attachment. This is the normal application API: pass the
193
+ * provider itself, its capability, scoped credentials, config and priority.
194
+ * Stable instance keys exist only inside the dynamic control-plane adapter.
195
+ */
196
+ export interface DirectServiceAttachment {
197
+ provider: ProviderDefinition;
198
+ method: string;
199
+ credentials: ScopedCredentialSource;
200
+ config?: Record<string, unknown>;
201
+ priority: number;
202
+ enabled?: boolean;
203
+ }
204
+ export type DirectServiceConfig<Methods extends ServiceMethods> = {
205
+ [Method in keyof Methods]: readonly DirectServiceAttachment[];
206
+ };
207
+ export interface DirectServiceOptions {
208
+ before?: RegistryOptions['before'];
209
+ after?: RegistryOptions['after'];
210
+ }
211
+ export type DirectAttempt = Omit<Attempt, 'instanceKey'>;
212
+ export type DirectCallResult<Result> = Omit<CallResult<Result>, 'instance' | 'selected' | 'attempts'> & {
213
+ selected?: {
214
+ providerId: string;
215
+ providerMethod: string;
216
+ };
217
+ attempts: readonly DirectAttempt[];
218
+ };
125
219
  export interface RegistryOptions {
126
220
  credentials: CredentialSource;
127
221
  config: ConfigSource;
@@ -136,16 +230,31 @@ export interface RegistryOptions {
136
230
  * keep at the DEFINITION site, where `defineProvider` still checks that
137
231
  * `invoke` matches what the provider claims to take and return.
138
232
  */
139
- providers: readonly ProviderSpec<any, any>[];
233
+ providers: readonly ProviderDefinition[];
140
234
  before?: (context: {
141
235
  service: string;
236
+ method: string;
142
237
  provider: string;
143
- }) => void;
144
- after?: (result: CallResult<unknown>) => void;
238
+ instance: string;
239
+ }) => void | Promise<void>;
240
+ after?: (result: CallResult<unknown>) => void | Promise<void>;
145
241
  }
146
242
  export declare function nextHealth(current: ProviderHealth | undefined, kind: FailureKind | 'success'): ProviderHealth;
147
243
  export declare function createRegistry(options: RegistryOptions): {
148
- call: <Result>(serviceKey: string, args: unknown, callOptions?: CallOptions) => Promise<CallResult<Result>>;
244
+ call: <Result>(serviceKey: string, methodKey: string, args: unknown, callOptions?: CallOptions) => Promise<CallResult<Result>>;
245
+ service: <Key extends string, Methods extends ServiceMethods>(definition: ServiceDefinition<Key, Methods>) => {
246
+ call<Method extends Extract<keyof Methods, string>>(method: Method, args: ServiceArgs<Methods[Method]>, callOptions?: CallOptions): Promise<CallResult<ServiceResult<Methods[Method]>>>;
247
+ };
248
+ };
249
+ /**
250
+ * Create a typed service directly from ordered provider attachments.
251
+ *
252
+ * This deliberately hides provider-instance keys and the ConfigSource
253
+ * ceremony. Applications normally know their providers in code; ForgeZero's
254
+ * dynamic admin control plane can continue to use createRegistry separately.
255
+ */
256
+ export declare function createService<Key extends string, Methods extends ServiceMethods>(definition: ServiceDefinition<Key, Methods>, methods: DirectServiceConfig<Methods>, options?: DirectServiceOptions): {
257
+ call<Method extends Extract<keyof Methods, string>>(method: Method, args: ServiceArgs<Methods[Method]>, callOptions?: CallOptions): Promise<DirectCallResult<ServiceResult<Methods[Method]>>>;
149
258
  };
150
259
  /** Passed to `registry.call('email', message)`. */
151
260
  export interface EmailMessage {
@@ -176,4 +285,5 @@ export interface EmailBatchResult {
176
285
  };
177
286
  results: readonly EmailBatchItemResult[];
178
287
  }
179
- export declare const VERSION = "0.1.9";
288
+ export declare const VERSION = "0.1.11";
289
+ export {};