@webiny/api-core 6.5.0-beta.5 → 6.5.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/ApiCoreFeature.js +2 -0
  2. package/ApiCoreFeature.js.map +1 -1
  3. package/features/ai/AiModelRegistry.d.ts +3 -1
  4. package/features/ai/AiModelRegistry.js +14 -6
  5. package/features/ai/AiModelRegistry.js.map +1 -1
  6. package/features/ai/DEVELOPERS.md +47 -12
  7. package/features/ai/RemoteAiModelCatalog.d.ts +13 -0
  8. package/features/ai/RemoteAiModelCatalog.js +69 -0
  9. package/features/ai/RemoteAiModelCatalog.js.map +1 -0
  10. package/features/ai/abstractions.d.ts +14 -0
  11. package/features/ai/abstractions.js +2 -1
  12. package/features/ai/abstractions.js.map +1 -1
  13. package/features/ai/feature.js +2 -0
  14. package/features/ai/feature.js.map +1 -1
  15. package/features/ai/index.d.ts +2 -2
  16. package/features/ai/index.js +1 -1
  17. package/features/httpClient/HttpClient.d.ts +7 -0
  18. package/features/httpClient/HttpClient.js +72 -0
  19. package/features/httpClient/HttpClient.js.map +1 -0
  20. package/features/httpClient/HttpRequestError.d.ts +10 -0
  21. package/features/httpClient/HttpRequestError.js +13 -0
  22. package/features/httpClient/HttpRequestError.js.map +1 -0
  23. package/features/httpClient/HttpStatusError.d.ts +12 -0
  24. package/features/httpClient/HttpStatusError.js +12 -0
  25. package/features/httpClient/HttpStatusError.js.map +1 -0
  26. package/features/httpClient/abstractions.d.ts +32 -0
  27. package/features/httpClient/abstractions.js +5 -0
  28. package/features/httpClient/abstractions.js.map +1 -0
  29. package/features/httpClient/feature.d.ts +4 -0
  30. package/features/httpClient/feature.js +11 -0
  31. package/features/httpClient/feature.js.map +1 -0
  32. package/features/httpClient/index.d.ts +3 -0
  33. package/features/httpClient/index.js +3 -0
  34. package/idp/JwksCache.d.ts +7 -2
  35. package/idp/JwksCache.js +43 -16
  36. package/idp/JwksCache.js.map +1 -1
  37. package/idp/feature.js +1 -2
  38. package/idp/feature.js.map +1 -1
  39. package/package.json +14 -14
package/ApiCoreFeature.js CHANGED
@@ -13,12 +13,14 @@ import { EncryptionFeature } from "./features/encryption/feature.js";
13
13
  import { FeatureFlagsFeature } from "./features/featureFlags/feature.js";
14
14
  import { MaskerFeature } from "./features/masker/feature.js";
15
15
  import { AiFeature } from "./features/ai/feature.js";
16
+ import { HttpClientFeature } from "./features/httpClient/feature.js";
16
17
  import { NullWebhookDispatcher } from "./features/webhooks/WebhookDispatcher/NullWebhookDispatcher.js";
17
18
  import { WebhookProviderFeature } from "./features/webhooks/index.js";
18
19
  const ApiCoreFeature = createFeature({
19
20
  name: "ApiCore",
20
21
  register (container, config) {
21
22
  MaskerFeature.register(container);
23
+ HttpClientFeature.register(container);
22
24
  AiFeature.register(container);
23
25
  LoggerFeature.register(container);
24
26
  EventPublisherFeature.register(container);
@@ -1 +1 @@
1
- {"version":3,"file":"ApiCoreFeature.js","sources":["../src/ApiCoreFeature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { Container } from \"@webiny/di\";\nimport { EventPublisherFeature } from \"~/features/eventPublisher/feature.js\";\nimport { LoggerFeature } from \"~/features/logger/feature.js\";\nimport { SecurityFeature } from \"~/features/security/SecurityFeature.js\";\nimport { SystemFeature } from \"~/features/system/SystemFeature.js\";\nimport { TenancyFeature } from \"./features/tenancy/TenancyFeature.js\";\nimport { AdminUsersFeature } from \"~/features/users/AdminUsersFeature.js\";\nimport type { ApiCoreStorageOperations } from \"~/types/core.js\";\nimport { IdpAuthenticatorFeature } from \"~/idp/feature.js\";\nimport { KeyValueStoreFeature } from \"~/features/keyValueStore/feature.js\";\nimport { BuildParamsFeature } from \"~/features/buildParams/feature.js\";\nimport { DebuggerFeature } from \"~/features/debugger/feature.js\";\nimport { EncryptionFeature } from \"~/features/encryption/feature.js\";\nimport { FeatureFlagsFeature } from \"~/features/featureFlags/feature.js\";\nimport { MaskerFeature } from \"~/features/masker/feature.js\";\nimport { AiFeature } from \"~/features/ai/feature.js\";\nimport { NullWebhookDispatcher } from \"./features/webhooks/WebhookDispatcher/NullWebhookDispatcher.js\";\nimport { WebhookProviderFeature } from \"~/features/webhooks/index.js\";\n\nexport const ApiCoreFeature = createFeature({\n name: \"ApiCore\",\n register(container: Container, config: ApiCoreStorageOperations) {\n // Register features\n MaskerFeature.register(container);\n AiFeature.register(container);\n LoggerFeature.register(container);\n EventPublisherFeature.register(container);\n BuildParamsFeature.register(container);\n DebuggerFeature.register(container);\n EncryptionFeature.register(container);\n FeatureFlagsFeature.register(container);\n TenancyFeature.register(container, config.tenancyStorageOperations);\n SecurityFeature.register(container, config.securityStorageOperations);\n AdminUsersFeature.register(container, config.usersStorageOperations);\n KeyValueStoreFeature.register(container, config.keyValueStorageOperations);\n SystemFeature.register(container);\n IdpAuthenticatorFeature.register(container);\n container.register(NullWebhookDispatcher).inSingletonScope();\n WebhookProviderFeature.register(container);\n }\n});\n"],"names":["ApiCoreFeature","createFeature","container","config","MaskerFeature","AiFeature","LoggerFeature","EventPublisherFeature","BuildParamsFeature","DebuggerFeature","EncryptionFeature","FeatureFlagsFeature","TenancyFeature","SecurityFeature","AdminUsersFeature","KeyValueStoreFeature","SystemFeature","IdpAuthenticatorFeature","NullWebhookDispatcher","WebhookProviderFeature"],"mappings":";;;;;;;;;;;;;;;;;AAoBO,MAAMA,iBAAiBC,cAAc;IACxC,MAAM;IACN,UAASC,SAAoB,EAAEC,MAAgC;QAE3DC,cAAc,QAAQ,CAACF;QACvBG,UAAU,QAAQ,CAACH;QACnBI,cAAc,QAAQ,CAACJ;QACvBK,sBAAsB,QAAQ,CAACL;QAC/BM,mBAAmB,QAAQ,CAACN;QAC5BO,gBAAgB,QAAQ,CAACP;QACzBQ,kBAAkB,QAAQ,CAACR;QAC3BS,oBAAoB,QAAQ,CAACT;QAC7BU,eAAe,QAAQ,CAACV,WAAWC,OAAO,wBAAwB;QAClEU,gBAAgB,QAAQ,CAACX,WAAWC,OAAO,yBAAyB;QACpEW,kBAAkB,QAAQ,CAACZ,WAAWC,OAAO,sBAAsB;QACnEY,qBAAqB,QAAQ,CAACb,WAAWC,OAAO,yBAAyB;QACzEa,cAAc,QAAQ,CAACd;QACvBe,wBAAwB,QAAQ,CAACf;QACjCA,UAAU,QAAQ,CAACgB,uBAAuB,gBAAgB;QAC1DC,uBAAuB,QAAQ,CAACjB;IACpC;AACJ"}
1
+ {"version":3,"file":"ApiCoreFeature.js","sources":["../src/ApiCoreFeature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { Container } from \"@webiny/di\";\nimport { EventPublisherFeature } from \"~/features/eventPublisher/feature.js\";\nimport { LoggerFeature } from \"~/features/logger/feature.js\";\nimport { SecurityFeature } from \"~/features/security/SecurityFeature.js\";\nimport { SystemFeature } from \"~/features/system/SystemFeature.js\";\nimport { TenancyFeature } from \"./features/tenancy/TenancyFeature.js\";\nimport { AdminUsersFeature } from \"~/features/users/AdminUsersFeature.js\";\nimport type { ApiCoreStorageOperations } from \"~/types/core.js\";\nimport { IdpAuthenticatorFeature } from \"~/idp/feature.js\";\nimport { KeyValueStoreFeature } from \"~/features/keyValueStore/feature.js\";\nimport { BuildParamsFeature } from \"~/features/buildParams/feature.js\";\nimport { DebuggerFeature } from \"~/features/debugger/feature.js\";\nimport { EncryptionFeature } from \"~/features/encryption/feature.js\";\nimport { FeatureFlagsFeature } from \"~/features/featureFlags/feature.js\";\nimport { MaskerFeature } from \"~/features/masker/feature.js\";\nimport { AiFeature } from \"~/features/ai/feature.js\";\nimport { HttpClientFeature } from \"~/features/httpClient/feature.js\";\nimport { NullWebhookDispatcher } from \"./features/webhooks/WebhookDispatcher/NullWebhookDispatcher.js\";\nimport { WebhookProviderFeature } from \"~/features/webhooks/index.js\";\n\nexport const ApiCoreFeature = createFeature({\n name: \"ApiCore\",\n register(container: Container, config: ApiCoreStorageOperations) {\n // Register features\n MaskerFeature.register(container);\n HttpClientFeature.register(container);\n AiFeature.register(container);\n LoggerFeature.register(container);\n EventPublisherFeature.register(container);\n BuildParamsFeature.register(container);\n DebuggerFeature.register(container);\n EncryptionFeature.register(container);\n FeatureFlagsFeature.register(container);\n TenancyFeature.register(container, config.tenancyStorageOperations);\n SecurityFeature.register(container, config.securityStorageOperations);\n AdminUsersFeature.register(container, config.usersStorageOperations);\n KeyValueStoreFeature.register(container, config.keyValueStorageOperations);\n SystemFeature.register(container);\n IdpAuthenticatorFeature.register(container);\n container.register(NullWebhookDispatcher).inSingletonScope();\n WebhookProviderFeature.register(container);\n }\n});\n"],"names":["ApiCoreFeature","createFeature","container","config","MaskerFeature","HttpClientFeature","AiFeature","LoggerFeature","EventPublisherFeature","BuildParamsFeature","DebuggerFeature","EncryptionFeature","FeatureFlagsFeature","TenancyFeature","SecurityFeature","AdminUsersFeature","KeyValueStoreFeature","SystemFeature","IdpAuthenticatorFeature","NullWebhookDispatcher","WebhookProviderFeature"],"mappings":";;;;;;;;;;;;;;;;;;AAqBO,MAAMA,iBAAiBC,cAAc;IACxC,MAAM;IACN,UAASC,SAAoB,EAAEC,MAAgC;QAE3DC,cAAc,QAAQ,CAACF;QACvBG,kBAAkB,QAAQ,CAACH;QAC3BI,UAAU,QAAQ,CAACJ;QACnBK,cAAc,QAAQ,CAACL;QACvBM,sBAAsB,QAAQ,CAACN;QAC/BO,mBAAmB,QAAQ,CAACP;QAC5BQ,gBAAgB,QAAQ,CAACR;QACzBS,kBAAkB,QAAQ,CAACT;QAC3BU,oBAAoB,QAAQ,CAACV;QAC7BW,eAAe,QAAQ,CAACX,WAAWC,OAAO,wBAAwB;QAClEW,gBAAgB,QAAQ,CAACZ,WAAWC,OAAO,yBAAyB;QACpEY,kBAAkB,QAAQ,CAACb,WAAWC,OAAO,sBAAsB;QACnEa,qBAAqB,QAAQ,CAACd,WAAWC,OAAO,yBAAyB;QACzEc,cAAc,QAAQ,CAACf;QACvBgB,wBAAwB,QAAQ,CAAChB;QACjCA,UAAU,QAAQ,CAACiB,uBAAuB,gBAAgB;QAC1DC,uBAAuB,QAAQ,CAAClB;IACpC;AACJ"}
@@ -1,9 +1,11 @@
1
1
  import { AiModelRegistry as AiModelRegistryAbstraction } from "./abstractions.js";
2
2
  import { AiSdkFactory } from "./abstractions.js";
3
+ import { AiModelCatalog } from "./abstractions.js";
3
4
  import type { AiModel } from "./abstractions.js";
4
5
  declare class AiModelRegistryImpl implements AiModelRegistryAbstraction.Interface {
5
6
  private readonly sdkFactories;
6
- constructor(sdkFactories: AiSdkFactory.Interface[]);
7
+ private readonly catalog;
8
+ constructor(sdkFactories: AiSdkFactory.Interface[], catalog: AiModelCatalog.Interface);
7
9
  listModels(): Promise<AiModel[]>;
8
10
  }
9
11
  export declare const AiModelRegistry: typeof AiModelRegistryImpl & {
@@ -1,17 +1,24 @@
1
- import { AiModelRegistry, AiSdkFactory } from "./abstractions.js";
1
+ import { AiModelCatalog, AiModelRegistry, AiSdkFactory } from "./abstractions.js";
2
2
  class AiModelRegistryImpl {
3
- constructor(sdkFactories){
3
+ constructor(sdkFactories, catalog){
4
4
  this.sdkFactories = sdkFactories;
5
+ this.catalog = catalog;
5
6
  }
6
7
  async listModels() {
7
- return this.sdkFactories.flatMap((factory)=>factory.models.map((m)=>({
8
+ const providers = await this.catalog.listProviders();
9
+ return this.sdkFactories.flatMap((factory)=>{
10
+ const provider = providers?.find((p)=>p.id === factory.id);
11
+ const providerName = provider?.name ?? factory.name;
12
+ const models = provider?.models ?? factory.models;
13
+ return models.map((m)=>({
8
14
  providerId: factory.id,
9
- providerName: factory.name,
15
+ providerName,
10
16
  modelId: m.id,
11
17
  modelName: m.name,
12
18
  deprecated: m.deprecated,
13
19
  endOfLife: m.endOfLife
14
- })));
20
+ }));
21
+ });
15
22
  }
16
23
  }
17
24
  const AiModelRegistry_AiModelRegistry = AiModelRegistry.createImplementation({
@@ -22,7 +29,8 @@ const AiModelRegistry_AiModelRegistry = AiModelRegistry.createImplementation({
22
29
  {
23
30
  multiple: true
24
31
  }
25
- ]
32
+ ],
33
+ AiModelCatalog
26
34
  ]
27
35
  });
28
36
  export { AiModelRegistry_AiModelRegistry as AiModelRegistry };
@@ -1 +1 @@
1
- {"version":3,"file":"features/ai/AiModelRegistry.js","sources":["../../../src/features/ai/AiModelRegistry.ts"],"sourcesContent":["import { AiModelRegistry as AiModelRegistryAbstraction } from \"./abstractions.js\";\nimport { AiSdkFactory } from \"./abstractions.js\";\nimport type { AiModel } from \"./abstractions.js\";\n\nclass AiModelRegistryImpl implements AiModelRegistryAbstraction.Interface {\n constructor(private readonly sdkFactories: AiSdkFactory.Interface[]) {}\n\n async listModels(): Promise<AiModel[]> {\n return this.sdkFactories.flatMap(factory =>\n factory.models.map(m => ({\n providerId: factory.id,\n providerName: factory.name,\n modelId: m.id,\n modelName: m.name,\n deprecated: m.deprecated,\n endOfLife: m.endOfLife\n }))\n );\n }\n}\n\nexport const AiModelRegistry = AiModelRegistryAbstraction.createImplementation({\n implementation: AiModelRegistryImpl,\n dependencies: [[AiSdkFactory, { multiple: true }]]\n});\n"],"names":["AiModelRegistryImpl","sdkFactories","factory","m","AiModelRegistry","AiModelRegistryAbstraction","AiSdkFactory"],"mappings":";AAIA,MAAMA;IACF,YAA6BC,YAAsC,CAAE;aAAxCA,YAAY,GAAZA;IAAyC;IAEtE,MAAM,aAAiC;QACnC,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAACC,CAAAA,UAC7BA,QAAQ,MAAM,CAAC,GAAG,CAACC,CAAAA,IAAM;oBACrB,YAAYD,QAAQ,EAAE;oBACtB,cAAcA,QAAQ,IAAI;oBAC1B,SAASC,EAAE,EAAE;oBACb,WAAWA,EAAE,IAAI;oBACjB,YAAYA,EAAE,UAAU;oBACxB,WAAWA,EAAE,SAAS;gBAC1B;IAER;AACJ;AAEO,MAAMC,kCAAkBC,gBAAAA,oBAA+C,CAAC;IAC3E,gBAAgBL;IAChB,cAAc;QAAC;YAACM;YAAc;gBAAE,UAAU;YAAK;SAAE;KAAC;AACtD"}
1
+ {"version":3,"file":"features/ai/AiModelRegistry.js","sources":["../../../src/features/ai/AiModelRegistry.ts"],"sourcesContent":["import { AiModelRegistry as AiModelRegistryAbstraction } from \"./abstractions.js\";\nimport { AiSdkFactory } from \"./abstractions.js\";\nimport { AiModelCatalog } from \"./abstractions.js\";\nimport type { AiModel } from \"./abstractions.js\";\n\nclass AiModelRegistryImpl implements AiModelRegistryAbstraction.Interface {\n constructor(\n private readonly sdkFactories: AiSdkFactory.Interface[],\n private readonly catalog: AiModelCatalog.Interface\n ) {}\n\n async listModels(): Promise<AiModel[]> {\n const providers = await this.catalog.listProviders();\n\n /*\n * A provider is only offered when an SDK factory can serve it. The catalog decides its\n * models; a factory the catalog doesn't know (a custom SDK), or an unreachable catalog,\n * falls back to the factory's own list.\n */\n return this.sdkFactories.flatMap(factory => {\n const provider = providers?.find(p => p.id === factory.id);\n const providerName = provider?.name ?? factory.name;\n const models = provider?.models ?? factory.models;\n\n return models.map(m => ({\n providerId: factory.id,\n providerName,\n modelId: m.id,\n modelName: m.name,\n deprecated: m.deprecated,\n endOfLife: m.endOfLife\n }));\n });\n }\n}\n\nexport const AiModelRegistry = AiModelRegistryAbstraction.createImplementation({\n implementation: AiModelRegistryImpl,\n dependencies: [[AiSdkFactory, { multiple: true }], AiModelCatalog]\n});\n"],"names":["AiModelRegistryImpl","sdkFactories","catalog","providers","factory","provider","p","providerName","models","m","AiModelRegistry","AiModelRegistryAbstraction","AiSdkFactory","AiModelCatalog"],"mappings":";AAKA,MAAMA;IACF,YACqBC,YAAsC,EACtCC,OAAiC,CACpD;aAFmBD,YAAY,GAAZA;aACAC,OAAO,GAAPA;IAClB;IAEH,MAAM,aAAiC;QACnC,MAAMC,YAAY,MAAM,IAAI,CAAC,OAAO,CAAC,aAAa;QAOlD,OAAO,IAAI,CAAC,YAAY,CAAC,OAAO,CAACC,CAAAA;YAC7B,MAAMC,WAAWF,WAAW,KAAKG,CAAAA,IAAKA,EAAE,EAAE,KAAKF,QAAQ,EAAE;YACzD,MAAMG,eAAeF,UAAU,QAAQD,QAAQ,IAAI;YACnD,MAAMI,SAASH,UAAU,UAAUD,QAAQ,MAAM;YAEjD,OAAOI,OAAO,GAAG,CAACC,CAAAA,IAAM;oBACpB,YAAYL,QAAQ,EAAE;oBACtBG;oBACA,SAASE,EAAE,EAAE;oBACb,WAAWA,EAAE,IAAI;oBACjB,YAAYA,EAAE,UAAU;oBACxB,WAAWA,EAAE,SAAS;gBAC1B;QACJ;IACJ;AACJ;AAEO,MAAMC,kCAAkBC,gBAAAA,oBAA+C,CAAC;IAC3E,gBAAgBX;IAChB,cAAc;QAAC;YAACY;YAAc;gBAAE,UAAU;YAAK;SAAE;QAAEC;KAAe;AACtE"}
@@ -1,10 +1,41 @@
1
1
  # AI Models
2
2
 
3
- This directory contains the AI SDK factory implementations that define which models are available in Webiny's AI Powerups.
3
+ This directory contains the AI SDK factory implementations that serve the models available in Webiny's AI Powerups.
4
+
5
+ ## Where the model list comes from
6
+
7
+ `AiModelRegistry` takes the published catalog at `https://api.webiny.com/ai/models` (`RemoteAiModelCatalog`, overridable through `WCP_API_URL`) and intersects it with the registered SDK factories:
8
+
9
+ - A catalog provider with no SDK factory is not offered. The SDK factory is the last gate.
10
+ - For a provider that has a factory, the catalog's models and names replace the factory's own list.
11
+ - A factory the catalog doesn't know (a custom SDK) keeps its own `models`.
12
+ - If the catalog can't be loaded (timeout, error status, invalid body), every factory falls back to its own `models`.
13
+
14
+ The catalog is cached for an hour per process. A failed load is retried after a minute. The first AI call on a cold Lambda waits for the fetch. If the catalog doesn't answer within 3 seconds, the fetch is aborted and the factories' own lists are used.
15
+
16
+ So adding or deprecating a model for an existing provider is a catalog change and needs no release. The `*_MODELS` arrays below are the offline fallback; keep them roughly in sync.
17
+
18
+ The catalog response looks like this. Dates are ISO strings, and fields the schema doesn't know (`api`, `supports`, ...) are dropped:
19
+
20
+ ```json
21
+ {
22
+ "providers": [
23
+ {
24
+ "id": "openai",
25
+ "name": "OpenAI",
26
+ "models": [
27
+ { "id": "o3", "name": "o3", "deprecated": "2026-06-11", "endOfLife": "2026-12-11" }
28
+ ]
29
+ }
30
+ ]
31
+ }
32
+ ```
33
+
34
+ A response that doesn't match the schema counts as a failed load, so one bad entry drops the whole catalog and every factory falls back to its own list.
4
35
 
5
36
  ## Selection criteria
6
37
 
7
- Every model listed in a factory **must** support:
38
+ Every model in the catalog or in a factory **must** support:
8
39
 
9
40
  - **Tool/function calling** — AI Powerups relies on tools for structured output (content generation, entry comparison, image enrichment, etc.). Models without tool support will break these features at runtime.
10
41
  - **Chat/text generation** — only conversational models are listed; embedding, image-generation, TTS, and other specialized models are excluded.
@@ -21,7 +52,7 @@ Each model entry in `IAiSdkModel` supports two optional `Date` fields:
21
52
 
22
53
  Set these fields when the provider announces deprecation or end-of-life dates. The UI can use them to warn users about upcoming shutdowns or steer them toward replacements.
23
54
 
24
- When a model reaches its `endOfLife` date, remove it from the factory in the next release — keeping it would cause runtime failures.
55
+ When a model reaches its `endOfLife` date, remove it from the catalog, which takes effect within an hour. Remove it from the factory in the next release too, or deployments that can't reach the catalog will keep offering a model that no longer works.
25
56
 
26
57
  ## Providers
27
58
 
@@ -103,24 +134,28 @@ Source: https://ai.google.dev/gemini-api/docs/deprecations
103
134
  ## Adding a new model
104
135
 
105
136
  1. Verify the model supports tool/function calling.
106
- 2. Add an entry to the `*_MODELS` array in the corresponding factory file. The `id` must match the provider's API model ID exactly.
107
- 3. If the model has known deprecation or end-of-life dates, set `deprecated` and/or `endOfLife` as `new Date("YYYY-MM-DD")`.
108
- 4. Update the table in this file.
137
+ 2. Add it to the catalog at `https://api.webiny.com/ai/models`. That is what users see.
138
+ 3. Add an entry to the `*_MODELS` array in the corresponding factory file. The `id` must match the provider's API model ID exactly.
139
+ 4. If the model has known deprecation or end-of-life dates, set `deprecated` and/or `endOfLife`: ISO strings (`"YYYY-MM-DD"`) in the catalog, `new Date("YYYY-MM-DD")` in the factory.
140
+ 5. Update the table in this file.
109
141
 
110
142
  ## Adding a new provider
111
143
 
112
- 1. Create a new `<Provider>SdkFactory.ts` implementing `AiSdkFactory`.
144
+ A new provider needs a release, because the catalog can only offer providers that have an SDK factory.
145
+
146
+ 1. Create a new `<Provider>SdkFactory.ts` implementing `AiSdkFactory`. Its `id` is the key the catalog matches on.
113
147
  2. Add the `@ai-sdk/<provider>` dependency via `yarn add` in `packages/api-core`.
114
148
  3. Register the factory in `feature.ts`.
115
- 4. Update this file.
149
+ 4. Add the provider to the catalog under the same `id`. Until you do, the factory's own `models` are used.
150
+ 5. Update this file.
116
151
 
117
152
  ## Updating models
118
153
 
119
- Check the provider docs periodically and update the lists:
154
+ Check the provider docs periodically and update the catalog first, then the factory lists:
120
155
 
121
156
  - **New models**: add to the top of the active section.
122
157
  - **Deprecated models**: set `deprecated` and `endOfLife` dates, move to the bottom of the array.
123
- - **End-of-life reached**: remove from the factory. Keeping a dead model causes runtime failures.
124
- - **Never remove a model before its end-of-life date** — users may have presets referencing it, and removing it breaks `ProvidersHandler` validation on save.
158
+ - **End-of-life reached**: remove from the catalog and the factory. Keeping a dead model causes runtime failures.
159
+ - **Never remove a model before its end-of-life date** — users may have presets referencing it, and removing it breaks `ProvidersHandler` validation on save. This matters more for the catalog than for the factory: `Ai` rejects any model the registry doesn't list, so dropping a model from the catalog breaks calls that use it in every deployment within the hour.
125
160
 
126
- Last updated: September 2026.
161
+ Last updated: October 2026.
@@ -0,0 +1,13 @@
1
+ import { AiModelCatalog as AiModelCatalogAbstraction } from "./abstractions.js";
2
+ import type { AiCatalogProvider } from "./abstractions.js";
3
+ import { Logger } from "../../features/logger/index.js";
4
+ import { HttpClient } from "../../features/httpClient/index.js";
5
+ declare class RemoteAiModelCatalogImpl implements AiModelCatalogAbstraction.Interface {
6
+ private readonly httpClient;
7
+ private readonly logger;
8
+ constructor(httpClient: HttpClient.Interface, logger: Logger.Interface);
9
+ listProviders(): Promise<AiCatalogProvider[] | undefined>;
10
+ private fetchProviders;
11
+ }
12
+ export declare const RemoteAiModelCatalog: import("@webiny/di").Implementation<typeof RemoteAiModelCatalogImpl>;
13
+ export {};
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import { createImplementation } from "@webiny/feature/api";
3
+ import { getWcpApiUrl } from "@webiny/wcp";
4
+ import { AiModelCatalog } from "./abstractions.js";
5
+ import { Logger } from "../logger/index.js";
6
+ import { HttpClient } from "../httpClient/index.js";
7
+ const FETCH_TIMEOUT_MS = 3000;
8
+ const SUCCESS_TTL_MS = 3600000;
9
+ const FAILURE_TTL_MS = 60000;
10
+ const optionalDate = z.coerce.date().optional();
11
+ const catalogSchema = z.object({
12
+ providers: z.array(z.object({
13
+ id: z.string(),
14
+ name: z.string(),
15
+ models: z.array(z.object({
16
+ id: z.string(),
17
+ name: z.string(),
18
+ deprecated: optionalDate,
19
+ endOfLife: optionalDate
20
+ }))
21
+ }))
22
+ });
23
+ let cached;
24
+ class RemoteAiModelCatalogImpl {
25
+ constructor(httpClient, logger){
26
+ this.httpClient = httpClient;
27
+ this.logger = logger;
28
+ }
29
+ listProviders() {
30
+ if (cached && cached.expiresAt > Date.now()) return cached.providers;
31
+ const entry = {
32
+ expiresAt: Date.now() + SUCCESS_TTL_MS,
33
+ providers: this.fetchProviders().then((providers)=>{
34
+ if (!providers) entry.expiresAt = Date.now() + FAILURE_TTL_MS;
35
+ return providers;
36
+ })
37
+ };
38
+ cached = entry;
39
+ return entry.providers;
40
+ }
41
+ async fetchProviders() {
42
+ const url = getWcpApiUrl("/ai/models");
43
+ const response = await this.httpClient.requestJson({
44
+ url,
45
+ timeoutMs: FETCH_TIMEOUT_MS
46
+ });
47
+ if (response.isFail()) return void this.logger.warn({
48
+ url,
49
+ error: response.error
50
+ }, "Failed to load AI models.");
51
+ const result = catalogSchema.safeParse(response.value);
52
+ if (!result.success) return void this.logger.warn({
53
+ url,
54
+ error: result.error
55
+ }, "Received invalid AI models.");
56
+ return result.data.providers;
57
+ }
58
+ }
59
+ const RemoteAiModelCatalog = createImplementation({
60
+ abstraction: AiModelCatalog,
61
+ implementation: RemoteAiModelCatalogImpl,
62
+ dependencies: [
63
+ HttpClient,
64
+ Logger
65
+ ]
66
+ });
67
+ export { RemoteAiModelCatalog };
68
+
69
+ //# sourceMappingURL=RemoteAiModelCatalog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/ai/RemoteAiModelCatalog.js","sources":["../../../src/features/ai/RemoteAiModelCatalog.ts"],"sourcesContent":["import { z } from \"zod\";\nimport { createImplementation } from \"@webiny/feature/api\";\nimport { getWcpApiUrl } from \"@webiny/wcp\";\nimport { AiModelCatalog as AiModelCatalogAbstraction } from \"./abstractions.js\";\nimport type { AiCatalogProvider } from \"./abstractions.js\";\nimport { Logger } from \"~/features/logger/index.js\";\nimport { HttpClient } from \"~/features/httpClient/index.js\";\n\nconst FETCH_TIMEOUT_MS = 3000;\nconst SUCCESS_TTL_MS = 60 * 60 * 1000;\nconst FAILURE_TTL_MS = 60 * 1000;\n\nconst optionalDate = z.coerce.date().optional();\n\nconst catalogSchema = z.object({\n providers: z.array(\n z.object({\n id: z.string(),\n name: z.string(),\n models: z.array(\n z.object({\n id: z.string(),\n name: z.string(),\n deprecated: optionalDate,\n endOfLife: optionalDate\n })\n )\n })\n )\n});\n\ninterface CachedCatalog {\n expiresAt: number;\n providers: Promise<AiCatalogProvider[] | undefined>;\n}\n\n/*\n * Process-wide on purpose: the catalog is the same for every tenant and request, and\n * `Ai.resolveLanguageModel()` reads it on every call, so a warm Lambda should fetch it once.\n */\nlet cached: CachedCatalog | undefined;\n\nclass RemoteAiModelCatalogImpl implements AiModelCatalogAbstraction.Interface {\n constructor(\n private readonly httpClient: HttpClient.Interface,\n private readonly logger: Logger.Interface\n ) {}\n\n listProviders(): Promise<AiCatalogProvider[] | undefined> {\n if (cached && cached.expiresAt > Date.now()) {\n return cached.providers;\n }\n\n const entry: CachedCatalog = {\n expiresAt: Date.now() + SUCCESS_TTL_MS,\n providers: this.fetchProviders().then(providers => {\n if (!providers) {\n entry.expiresAt = Date.now() + FAILURE_TTL_MS;\n }\n return providers;\n })\n };\n cached = entry;\n\n return entry.providers;\n }\n\n private async fetchProviders(): Promise<AiCatalogProvider[] | undefined> {\n const url = getWcpApiUrl(\"/ai/models\");\n\n const response = await this.httpClient.requestJson({ url, timeoutMs: FETCH_TIMEOUT_MS });\n if (response.isFail()) {\n this.logger.warn({ url, error: response.error }, \"Failed to load AI models.\");\n return undefined;\n }\n\n const result = catalogSchema.safeParse(response.value);\n if (!result.success) {\n this.logger.warn({ url, error: result.error }, \"Received invalid AI models.\");\n return undefined;\n }\n\n return result.data.providers;\n }\n}\n\nexport const RemoteAiModelCatalog = createImplementation({\n abstraction: AiModelCatalogAbstraction,\n implementation: RemoteAiModelCatalogImpl,\n dependencies: [HttpClient, Logger]\n});\n"],"names":["FETCH_TIMEOUT_MS","SUCCESS_TTL_MS","FAILURE_TTL_MS","optionalDate","z","catalogSchema","cached","RemoteAiModelCatalogImpl","httpClient","logger","Date","entry","providers","url","getWcpApiUrl","response","result","RemoteAiModelCatalog","createImplementation","AiModelCatalogAbstraction","HttpClient","Logger"],"mappings":";;;;;;AAQA,MAAMA,mBAAmB;AACzB,MAAMC,iBAAiB;AACvB,MAAMC,iBAAiB;AAEvB,MAAMC,eAAeC,EAAE,MAAM,CAAC,IAAI,GAAG,QAAQ;AAE7C,MAAMC,gBAAgBD,EAAE,MAAM,CAAC;IAC3B,WAAWA,EAAE,KAAK,CACdA,EAAE,MAAM,CAAC;QACL,IAAIA,EAAE,MAAM;QACZ,MAAMA,EAAE,MAAM;QACd,QAAQA,EAAE,KAAK,CACXA,EAAE,MAAM,CAAC;YACL,IAAIA,EAAE,MAAM;YACZ,MAAMA,EAAE,MAAM;YACd,YAAYD;YACZ,WAAWA;QACf;IAER;AAER;AAWA,IAAIG;AAEJ,MAAMC;IACF,YACqBC,UAAgC,EAChCC,MAAwB,CAC3C;aAFmBD,UAAU,GAAVA;aACAC,MAAM,GAANA;IAClB;IAEH,gBAA0D;QACtD,IAAIH,UAAUA,OAAO,SAAS,GAAGI,KAAK,GAAG,IACrC,OAAOJ,OAAO,SAAS;QAG3B,MAAMK,QAAuB;YACzB,WAAWD,KAAK,GAAG,KAAKT;YACxB,WAAW,IAAI,CAAC,cAAc,GAAG,IAAI,CAACW,CAAAA;gBAClC,IAAI,CAACA,WACDD,MAAM,SAAS,GAAGD,KAAK,GAAG,KAAKR;gBAEnC,OAAOU;YACX;QACJ;QACAN,SAASK;QAET,OAAOA,MAAM,SAAS;IAC1B;IAEA,MAAc,iBAA2D;QACrE,MAAME,MAAMC,aAAa;QAEzB,MAAMC,WAAW,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAEF;YAAK,WAAWb;QAAiB;QACtF,IAAIe,SAAS,MAAM,IAAI,YACnB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAAEF;YAAK,OAAOE,SAAS,KAAK;QAAC,GAAG;QAIrD,MAAMC,SAASX,cAAc,SAAS,CAACU,SAAS,KAAK;QACrD,IAAI,CAACC,OAAO,OAAO,EAAE,YACjB,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC;YAAEH;YAAK,OAAOG,OAAO,KAAK;QAAC,GAAG;QAInD,OAAOA,OAAO,IAAI,CAAC,SAAS;IAChC;AACJ;AAEO,MAAMC,uBAAuBC,qBAAqB;IACrD,aAAaC;IACb,gBAAgBZ;IAChB,cAAc;QAACa;QAAYC;KAAO;AACtC"}
@@ -82,6 +82,20 @@ export declare const AiModelRegistry: import("@webiny/di").Abstraction<IAiModelR
82
82
  export declare namespace AiModelRegistry {
83
83
  type Interface = IAiModelRegistry;
84
84
  }
85
+ export interface AiCatalogProvider {
86
+ id: string;
87
+ name: string;
88
+ models: IAiSdkModel[];
89
+ }
90
+ export interface IAiModelCatalog {
91
+ /** Returns `undefined` when the catalog can't be loaded. */
92
+ listProviders(): Promise<AiCatalogProvider[] | undefined>;
93
+ }
94
+ /** The published list of providers and models Webiny supports. SDK factories still gate what is offered. */
95
+ export declare const AiModelCatalog: import("@webiny/di").Abstraction<IAiModelCatalog>;
96
+ export declare namespace AiModelCatalog {
97
+ type Interface = IAiModelCatalog;
98
+ }
85
99
  export interface IAiSdkTool<TInput = any> {
86
100
  readonly name: string;
87
101
  readonly description: string;
@@ -4,8 +4,9 @@ const AiSdkFactory = createAbstraction("AiSdkFactory");
4
4
  const AiConnectionFactory = createAbstraction("AiConnectionFactory");
5
5
  const Ai = createAbstraction("Ai");
6
6
  const AiModelRegistry = createAbstraction("AiModelRegistry");
7
+ const AiModelCatalog = createAbstraction("AiModelCatalog");
7
8
  const AiSdkTool = createAbstraction("AiSdkTool");
8
9
  const AiSdkTools = createAbstraction("AiSdkTools");
9
- export { Ai, AiConnectionFactory, AiModelRegistry, AiSdk, AiSdkFactory, AiSdkTool, AiSdkTools };
10
+ export { Ai, AiConnectionFactory, AiModelCatalog, AiModelRegistry, AiSdk, AiSdkFactory, AiSdkTool, AiSdkTools };
10
11
 
11
12
  //# sourceMappingURL=abstractions.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"features/ai/abstractions.js","sources":["../../../src/features/ai/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\nimport type { generateText } from \"ai\";\nimport type { streamText } from \"ai\";\nimport type { LanguageModel } from \"ai\";\nimport type { FlexibleSchema, ToolSet } from \"ai\";\n\n// AiSdk\n\nexport interface IAiSdkModel {\n id: string; // raw model id, e.g. \"claude-sonnet-4-5\"\n name: string; // human-friendly name, e.g. \"Claude Sonnet 4.5\"\n deprecated?: Date;\n endOfLife?: Date;\n}\n\nexport interface IAiSdk {\n languageModel(modelId: string): LanguageModel;\n}\n\n/** A single AI SDK instance (e.g. OpenAI, Anthropic) that resolves model instances. */\nexport const AiSdk = createAbstraction<IAiSdk>(\"AiSdk\");\n\nexport namespace AiSdk {\n export type Interface = IAiSdk;\n}\n\n// AiSdkFactory\n\nexport interface IAiSdkFactory {\n readonly id: string; // machine id, e.g. \"anthropic\"\n readonly name: string; // human-friendly name, e.g. \"Anthropic\"\n readonly models: readonly IAiSdkModel[]; // static model list, no API call needed\n execute(apiKey?: string): Promise<IAiSdk>;\n}\n\n/** Factory that asynchronously initialises an AI SDK. Register one per provider namespace. */\nexport const AiSdkFactory = createAbstraction<IAiSdkFactory>(\"AiSdkFactory\");\n\nexport namespace AiSdkFactory {\n export type Interface = IAiSdkFactory;\n}\n\n// AiConnection\n\nexport interface IAiConnectionInline {\n readonly sdkName: string;\n readonly apiKey?: string;\n}\n\nexport interface IAiConnection extends IAiConnectionInline {\n readonly id: string;\n}\n\n// AiConnectionFactory\n\nexport interface IAiConnectionFactory {\n execute(): Promise<IAiConnection>;\n}\n\n/** Factory that asynchronously produces an AiConnection. */\nexport const AiConnectionFactory = createAbstraction<IAiConnectionFactory>(\"AiConnectionFactory\");\n\nexport namespace AiConnectionFactory {\n export type Interface = IAiConnectionFactory;\n}\n\n// Ai\n\ntype SDKGenerateTextParams = Parameters<typeof generateText>[0];\ntype SDKStreamTextParams = Parameters<typeof streamText>[0];\n\nexport type AiGenerateTextParams = Omit<SDKGenerateTextParams, \"model\"> & {\n model: string;\n connection?: string | IAiConnectionInline;\n};\nexport type AiStreamTextParams = Omit<SDKStreamTextParams, \"model\"> & {\n model: string;\n connection?: string | IAiConnectionInline;\n};\n\nexport interface AiModel {\n providerId: string; // e.g. \"anthropic\"\n providerName: string; // e.g. \"Anthropic\"\n modelId: string; // e.g. \"claude-sonnet-4-5\"\n modelName: string; // e.g. \"Claude Sonnet 4.5\"\n deprecated: Date | undefined;\n endOfLife: Date | undefined;\n}\n\nexport interface IAi {\n generateText(params: AiGenerateTextParams): ReturnType<typeof generateText>;\n streamText(params: AiStreamTextParams): Promise<ReturnType<typeof streamText>>;\n listModels(): Promise<AiModel[]>;\n listModelsByConnections(): Promise<AiModel[]>;\n listModelsByConnection(connection: string | IAiConnectionInline): Promise<AiModel[]>;\n}\n\n/** Interact with AI language models using registered providers. */\nexport const Ai = createAbstraction<IAi>(\"Ai\");\n\nexport namespace Ai {\n export type Interface = IAi;\n export type GenerateTextParams = AiGenerateTextParams;\n export type StreamTextParams = AiStreamTextParams;\n}\n\n// AiModelRegistry\n\nexport interface IAiModelRegistry {\n listModels(): Promise<AiModel[]>;\n}\n\n/** Single source of truth for available AI models. Decoratable to restrict the model list. */\nexport const AiModelRegistry = createAbstraction<IAiModelRegistry>(\"AiModelRegistry\");\n\nexport namespace AiModelRegistry {\n export type Interface = IAiModelRegistry;\n}\n\n// AiSdkTool\n\nexport interface IAiSdkTool<TInput = any> {\n readonly name: string;\n readonly description: string;\n readonly inputSchema: FlexibleSchema<TInput>;\n execute(input: TInput): Promise<unknown>;\n}\n\n/** A single tool that can be provided to AI generateText/streamText calls. */\nexport const AiSdkTool = createAbstraction<IAiSdkTool>(\"AiSdkTool\");\n\nexport namespace AiSdkTool {\n export type Interface = IAiSdkTool;\n}\n\n// AiSdkTools\n\nexport interface IAiSdkTools {\n getToolSet(): ToolSet;\n}\n\n/** Collection of AI SDK tools. Returns a ready-to-use ToolSet for generateText/streamText. */\nexport const AiSdkTools = createAbstraction<IAiSdkTools>(\"AiSdkTools\");\n\nexport namespace AiSdkTools {\n export type Interface = IAiSdkTools;\n}\n"],"names":["AiSdk","createAbstraction","AiSdkFactory","AiConnectionFactory","Ai","AiModelRegistry","AiSdkTool","AiSdkTools"],"mappings":";AAoBO,MAAMA,QAAQC,kBAA0B;AAgBxC,MAAMC,eAAeD,kBAAiC;AAwBtD,MAAME,sBAAsBF,kBAAwC;AAsCpE,MAAMG,KAAKH,kBAAuB;AAelC,MAAMI,kBAAkBJ,kBAAoC;AAgB5D,MAAMK,YAAYL,kBAA8B;AAahD,MAAMM,aAAaN,kBAA+B"}
1
+ {"version":3,"file":"features/ai/abstractions.js","sources":["../../../src/features/ai/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\nimport type { generateText } from \"ai\";\nimport type { streamText } from \"ai\";\nimport type { LanguageModel } from \"ai\";\nimport type { FlexibleSchema, ToolSet } from \"ai\";\n\n// AiSdk\n\nexport interface IAiSdkModel {\n id: string; // raw model id, e.g. \"claude-sonnet-4-5\"\n name: string; // human-friendly name, e.g. \"Claude Sonnet 4.5\"\n deprecated?: Date;\n endOfLife?: Date;\n}\n\nexport interface IAiSdk {\n languageModel(modelId: string): LanguageModel;\n}\n\n/** A single AI SDK instance (e.g. OpenAI, Anthropic) that resolves model instances. */\nexport const AiSdk = createAbstraction<IAiSdk>(\"AiSdk\");\n\nexport namespace AiSdk {\n export type Interface = IAiSdk;\n}\n\n// AiSdkFactory\n\nexport interface IAiSdkFactory {\n readonly id: string; // machine id, e.g. \"anthropic\"\n readonly name: string; // human-friendly name, e.g. \"Anthropic\"\n readonly models: readonly IAiSdkModel[]; // static model list, no API call needed\n execute(apiKey?: string): Promise<IAiSdk>;\n}\n\n/** Factory that asynchronously initialises an AI SDK. Register one per provider namespace. */\nexport const AiSdkFactory = createAbstraction<IAiSdkFactory>(\"AiSdkFactory\");\n\nexport namespace AiSdkFactory {\n export type Interface = IAiSdkFactory;\n}\n\n// AiConnection\n\nexport interface IAiConnectionInline {\n readonly sdkName: string;\n readonly apiKey?: string;\n}\n\nexport interface IAiConnection extends IAiConnectionInline {\n readonly id: string;\n}\n\n// AiConnectionFactory\n\nexport interface IAiConnectionFactory {\n execute(): Promise<IAiConnection>;\n}\n\n/** Factory that asynchronously produces an AiConnection. */\nexport const AiConnectionFactory = createAbstraction<IAiConnectionFactory>(\"AiConnectionFactory\");\n\nexport namespace AiConnectionFactory {\n export type Interface = IAiConnectionFactory;\n}\n\n// Ai\n\ntype SDKGenerateTextParams = Parameters<typeof generateText>[0];\ntype SDKStreamTextParams = Parameters<typeof streamText>[0];\n\nexport type AiGenerateTextParams = Omit<SDKGenerateTextParams, \"model\"> & {\n model: string;\n connection?: string | IAiConnectionInline;\n};\nexport type AiStreamTextParams = Omit<SDKStreamTextParams, \"model\"> & {\n model: string;\n connection?: string | IAiConnectionInline;\n};\n\nexport interface AiModel {\n providerId: string; // e.g. \"anthropic\"\n providerName: string; // e.g. \"Anthropic\"\n modelId: string; // e.g. \"claude-sonnet-4-5\"\n modelName: string; // e.g. \"Claude Sonnet 4.5\"\n deprecated: Date | undefined;\n endOfLife: Date | undefined;\n}\n\nexport interface IAi {\n generateText(params: AiGenerateTextParams): ReturnType<typeof generateText>;\n streamText(params: AiStreamTextParams): Promise<ReturnType<typeof streamText>>;\n listModels(): Promise<AiModel[]>;\n listModelsByConnections(): Promise<AiModel[]>;\n listModelsByConnection(connection: string | IAiConnectionInline): Promise<AiModel[]>;\n}\n\n/** Interact with AI language models using registered providers. */\nexport const Ai = createAbstraction<IAi>(\"Ai\");\n\nexport namespace Ai {\n export type Interface = IAi;\n export type GenerateTextParams = AiGenerateTextParams;\n export type StreamTextParams = AiStreamTextParams;\n}\n\n// AiModelRegistry\n\nexport interface IAiModelRegistry {\n listModels(): Promise<AiModel[]>;\n}\n\n/** Single source of truth for available AI models. Decoratable to restrict the model list. */\nexport const AiModelRegistry = createAbstraction<IAiModelRegistry>(\"AiModelRegistry\");\n\nexport namespace AiModelRegistry {\n export type Interface = IAiModelRegistry;\n}\n\n// AiModelCatalog\n\nexport interface AiCatalogProvider {\n id: string; // Matches `AiSdkFactory.id`, e.g. \"anthropic\".\n name: string; // Human-friendly name, e.g. \"Anthropic\".\n models: IAiSdkModel[];\n}\n\nexport interface IAiModelCatalog {\n /** Returns `undefined` when the catalog can't be loaded. */\n listProviders(): Promise<AiCatalogProvider[] | undefined>;\n}\n\n/** The published list of providers and models Webiny supports. SDK factories still gate what is offered. */\nexport const AiModelCatalog = createAbstraction<IAiModelCatalog>(\"AiModelCatalog\");\n\nexport namespace AiModelCatalog {\n export type Interface = IAiModelCatalog;\n}\n\n// AiSdkTool\n\nexport interface IAiSdkTool<TInput = any> {\n readonly name: string;\n readonly description: string;\n readonly inputSchema: FlexibleSchema<TInput>;\n execute(input: TInput): Promise<unknown>;\n}\n\n/** A single tool that can be provided to AI generateText/streamText calls. */\nexport const AiSdkTool = createAbstraction<IAiSdkTool>(\"AiSdkTool\");\n\nexport namespace AiSdkTool {\n export type Interface = IAiSdkTool;\n}\n\n// AiSdkTools\n\nexport interface IAiSdkTools {\n getToolSet(): ToolSet;\n}\n\n/** Collection of AI SDK tools. Returns a ready-to-use ToolSet for generateText/streamText. */\nexport const AiSdkTools = createAbstraction<IAiSdkTools>(\"AiSdkTools\");\n\nexport namespace AiSdkTools {\n export type Interface = IAiSdkTools;\n}\n"],"names":["AiSdk","createAbstraction","AiSdkFactory","AiConnectionFactory","Ai","AiModelRegistry","AiModelCatalog","AiSdkTool","AiSdkTools"],"mappings":";AAoBO,MAAMA,QAAQC,kBAA0B;AAgBxC,MAAMC,eAAeD,kBAAiC;AAwBtD,MAAME,sBAAsBF,kBAAwC;AAsCpE,MAAMG,KAAKH,kBAAuB;AAelC,MAAMI,kBAAkBJ,kBAAoC;AAoB5D,MAAMK,iBAAiBL,kBAAmC;AAgB1D,MAAMM,YAAYN,kBAA8B;AAahD,MAAMO,aAAaP,kBAA+B"}
@@ -4,6 +4,7 @@ import { AnthropicSdkFactory } from "./AnthropicSdkFactory.js";
4
4
  import { GoogleSdkFactory } from "./GoogleSdkFactory.js";
5
5
  import { Ai } from "./Ai.js";
6
6
  import { AiModelRegistry } from "./AiModelRegistry.js";
7
+ import { RemoteAiModelCatalog } from "./RemoteAiModelCatalog.js";
7
8
  import { AiSdkTools } from "./AiSdkTools.js";
8
9
  import { AiOutputToolRegistry } from "./toolPipeline/AiOutputToolRegistry.js";
9
10
  import { AiToolPipelineRunner } from "./toolPipeline/AiToolPipelineRunner.js";
@@ -15,6 +16,7 @@ const AiFeature = createFeature({
15
16
  container.register(AnthropicSdkFactory);
16
17
  container.register(GoogleSdkFactory);
17
18
  container.register(Ai);
19
+ container.register(RemoteAiModelCatalog);
18
20
  container.register(AiModelRegistry);
19
21
  container.register(AiSdkTools);
20
22
  container.register(AiOutputToolRegistry);
@@ -1 +1 @@
1
- {"version":3,"file":"features/ai/feature.js","sources":["../../../src/features/ai/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { OpenAiSdkFactory } from \"./OpenAiSdkFactory.js\";\nimport { AnthropicSdkFactory } from \"./AnthropicSdkFactory.js\";\nimport { GoogleSdkFactory } from \"./GoogleSdkFactory.js\";\nimport { Ai } from \"./Ai.js\";\nimport { AiModelRegistry } from \"./AiModelRegistry.js\";\nimport { AiSdkTools } from \"./AiSdkTools.js\";\nimport { AiOutputToolRegistry } from \"./toolPipeline/AiOutputToolRegistry.js\";\nimport { AiToolPipelineRunner } from \"./toolPipeline/AiToolPipelineRunner.js\";\nimport { DefaultTextExtractor } from \"./TextExtractor/DefaultTextExtractor.js\";\n\nexport const AiFeature = createFeature({\n name: \"AiFeature\",\n register(container) {\n container.register(OpenAiSdkFactory);\n container.register(AnthropicSdkFactory);\n container.register(GoogleSdkFactory);\n container.register(Ai);\n container.register(AiModelRegistry);\n container.register(AiSdkTools);\n container.register(AiOutputToolRegistry);\n container.register(AiToolPipelineRunner);\n container.register(DefaultTextExtractor);\n }\n});\n"],"names":["AiFeature","createFeature","container","OpenAiSdkFactory","AnthropicSdkFactory","GoogleSdkFactory","Ai","AiModelRegistry","AiSdkTools","AiOutputToolRegistry","AiToolPipelineRunner","DefaultTextExtractor"],"mappings":";;;;;;;;;;AAWO,MAAMA,YAAYC,cAAc;IACnC,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;QACnBD,UAAU,QAAQ,CAACE;QACnBF,UAAU,QAAQ,CAACG;QACnBH,UAAU,QAAQ,CAACI;QACnBJ,UAAU,QAAQ,CAACK;QACnBL,UAAU,QAAQ,CAACM;QACnBN,UAAU,QAAQ,CAACO;QACnBP,UAAU,QAAQ,CAACQ;QACnBR,UAAU,QAAQ,CAACS;IACvB;AACJ"}
1
+ {"version":3,"file":"features/ai/feature.js","sources":["../../../src/features/ai/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { OpenAiSdkFactory } from \"./OpenAiSdkFactory.js\";\nimport { AnthropicSdkFactory } from \"./AnthropicSdkFactory.js\";\nimport { GoogleSdkFactory } from \"./GoogleSdkFactory.js\";\nimport { Ai } from \"./Ai.js\";\nimport { AiModelRegistry } from \"./AiModelRegistry.js\";\nimport { RemoteAiModelCatalog } from \"./RemoteAiModelCatalog.js\";\nimport { AiSdkTools } from \"./AiSdkTools.js\";\nimport { AiOutputToolRegistry } from \"./toolPipeline/AiOutputToolRegistry.js\";\nimport { AiToolPipelineRunner } from \"./toolPipeline/AiToolPipelineRunner.js\";\nimport { DefaultTextExtractor } from \"./TextExtractor/DefaultTextExtractor.js\";\n\nexport const AiFeature = createFeature({\n name: \"AiFeature\",\n register(container) {\n container.register(OpenAiSdkFactory);\n container.register(AnthropicSdkFactory);\n container.register(GoogleSdkFactory);\n container.register(Ai);\n container.register(RemoteAiModelCatalog);\n container.register(AiModelRegistry);\n container.register(AiSdkTools);\n container.register(AiOutputToolRegistry);\n container.register(AiToolPipelineRunner);\n container.register(DefaultTextExtractor);\n }\n});\n"],"names":["AiFeature","createFeature","container","OpenAiSdkFactory","AnthropicSdkFactory","GoogleSdkFactory","Ai","RemoteAiModelCatalog","AiModelRegistry","AiSdkTools","AiOutputToolRegistry","AiToolPipelineRunner","DefaultTextExtractor"],"mappings":";;;;;;;;;;;AAYO,MAAMA,YAAYC,cAAc;IACnC,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;QACnBD,UAAU,QAAQ,CAACE;QACnBF,UAAU,QAAQ,CAACG;QACnBH,UAAU,QAAQ,CAACI;QACnBJ,UAAU,QAAQ,CAACK;QACnBL,UAAU,QAAQ,CAACM;QACnBN,UAAU,QAAQ,CAACO;QACnBP,UAAU,QAAQ,CAACQ;QACnBR,UAAU,QAAQ,CAACS;QACnBT,UAAU,QAAQ,CAACU;IACvB;AACJ"}
@@ -1,5 +1,5 @@
1
- export { AiSdk, AiSdkFactory, AiConnectionFactory, Ai, AiModelRegistry, AiSdkTool, AiSdkTools } from "./abstractions.js";
2
- export type { IAiConnection, IAiConnectionInline, AiModel, IAiSdkModel, IAiModelRegistry, IAiSdkTool, IAiSdkTools } from "./abstractions.js";
1
+ export { AiSdk, AiSdkFactory, AiConnectionFactory, Ai, AiModelRegistry, AiModelCatalog, AiSdkTool, AiSdkTools } from "./abstractions.js";
2
+ export type { IAiConnection, IAiConnectionInline, AiModel, IAiSdkModel, IAiModelRegistry, AiCatalogProvider, IAiSdkTool, IAiSdkTools } from "./abstractions.js";
3
3
  export { AiOutputTool, AiOutputToolRegistry, AiToolPipelineRunner } from "./toolPipeline/index.js";
4
4
  export type { IAiOutputTool, IAiOutputToolRegistry, IAiToolPipelineRunner } from "./toolPipeline/index.js";
5
5
  export { TextExtractor, DefaultTextExtractor } from "./TextExtractor/index.js";
@@ -1,4 +1,4 @@
1
- export { Ai, AiConnectionFactory, AiModelRegistry, AiSdk, AiSdkFactory, AiSdkTool, AiSdkTools } from "./abstractions.js";
1
+ export { Ai, AiConnectionFactory, AiModelCatalog, AiModelRegistry, AiSdk, AiSdkFactory, AiSdkTool, AiSdkTools } from "./abstractions.js";
2
2
  export { AiOutputTool, AiOutputToolRegistry, AiToolPipelineRunner } from "./toolPipeline/index.js";
3
3
  export { DefaultTextExtractor, TextExtractor } from "./TextExtractor/index.js";
4
4
  export { AiAfterGenerateTextEvent, AiAfterGenerateTextEventHandler, AiBeforeGenerateTextEvent, AiBeforeGenerateTextEventHandler, AiBeforeStreamTextEvent, AiBeforeStreamTextEventHandler, AiGenerateTextErrorEvent, AiGenerateTextErrorEventHandler } from "./events.js";
@@ -0,0 +1,7 @@
1
+ import { Result } from "@webiny/feature/api";
2
+ import { HttpClient as HttpClientAbstraction } from "./abstractions.js";
3
+ declare class HttpClientImpl implements HttpClientAbstraction.Interface {
4
+ requestJson(request: HttpClientAbstraction.Request): Promise<Result<unknown, HttpClientAbstraction.Error>>;
5
+ }
6
+ export declare const HttpClient: import("@webiny/di").Implementation<typeof HttpClientImpl>;
7
+ export {};
@@ -0,0 +1,72 @@
1
+ import { Result, createImplementation } from "@webiny/feature/api";
2
+ import { HttpClient } from "./abstractions.js";
3
+ import { HttpRequestError } from "./HttpRequestError.js";
4
+ import { HttpStatusError } from "./HttpStatusError.js";
5
+ const DEFAULT_TIMEOUT_MS = 10000;
6
+ function createRequestInit(request) {
7
+ const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;
8
+ const headers = new Headers(request.headers);
9
+ const init = {
10
+ method: request.method ?? "GET",
11
+ headers,
12
+ signal: AbortSignal.timeout(timeoutMs)
13
+ };
14
+ if (void 0 !== request.body) {
15
+ if (!headers.has("content-type")) headers.set("content-type", "application/json");
16
+ init.body = JSON.stringify(request.body);
17
+ }
18
+ return init;
19
+ }
20
+ function isTimeout(error) {
21
+ return error instanceof Error && "TimeoutError" === error.name;
22
+ }
23
+ function parseJson(text) {
24
+ if ("" === text) return Result.ok(void 0);
25
+ try {
26
+ const value = JSON.parse(text);
27
+ return Result.ok(value);
28
+ } catch (error) {
29
+ return Result.fail(error);
30
+ }
31
+ }
32
+ class HttpClientImpl {
33
+ async requestJson(request) {
34
+ const { url } = request;
35
+ const init = createRequestInit(request);
36
+ let response;
37
+ let text;
38
+ try {
39
+ response = await fetch(url, init);
40
+ text = await response.text();
41
+ } catch (error) {
42
+ const reason = isTimeout(error) ? "timeout" : "network";
43
+ return Result.fail(new HttpRequestError({
44
+ url,
45
+ reason
46
+ }, error));
47
+ }
48
+ const body = parseJson(text);
49
+ if (!response.ok) {
50
+ const statusError = new HttpStatusError({
51
+ url,
52
+ status: response.status,
53
+ statusText: response.statusText,
54
+ body: body.isOk() ? body.value : void 0
55
+ });
56
+ return Result.fail(statusError);
57
+ }
58
+ if (body.isFail()) return Result.fail(new HttpRequestError({
59
+ url,
60
+ reason: "invalidJson"
61
+ }, body.error));
62
+ return Result.ok(body.value);
63
+ }
64
+ }
65
+ const HttpClient_HttpClient = createImplementation({
66
+ abstraction: HttpClient,
67
+ implementation: HttpClientImpl,
68
+ dependencies: []
69
+ });
70
+ export { HttpClient_HttpClient as HttpClient };
71
+
72
+ //# sourceMappingURL=HttpClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/httpClient/HttpClient.js","sources":["../../../src/features/httpClient/HttpClient.ts"],"sourcesContent":["import { createImplementation } from \"@webiny/feature/api\";\nimport { Result } from \"@webiny/feature/api\";\nimport { HttpClient as HttpClientAbstraction } from \"./abstractions.js\";\nimport { HttpRequestError } from \"./HttpRequestError.js\";\nimport { HttpStatusError } from \"./HttpStatusError.js\";\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\n\nfunction createRequestInit(request: HttpClientAbstraction.Request): RequestInit {\n const timeoutMs = request.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const headers = new Headers(request.headers);\n const init: RequestInit = {\n method: request.method ?? \"GET\",\n headers,\n signal: AbortSignal.timeout(timeoutMs)\n };\n\n if (request.body !== undefined) {\n if (!headers.has(\"content-type\")) {\n headers.set(\"content-type\", \"application/json\");\n }\n init.body = JSON.stringify(request.body);\n }\n\n return init;\n}\n\nfunction isTimeout(error: unknown): boolean {\n return error instanceof Error && error.name === \"TimeoutError\";\n}\n\n// An empty body parses to `undefined`, so a 204 is not an invalid response.\nfunction parseJson(text: string): Result<unknown, unknown> {\n if (text === \"\") {\n return Result.ok(undefined);\n }\n try {\n const value: unknown = JSON.parse(text);\n return Result.ok(value);\n } catch (error) {\n return Result.fail(error);\n }\n}\n\nclass HttpClientImpl implements HttpClientAbstraction.Interface {\n async requestJson(\n request: HttpClientAbstraction.Request\n ): Promise<Result<unknown, HttpClientAbstraction.Error>> {\n const { url } = request;\n const init = createRequestInit(request);\n\n let response: Response;\n let text: string;\n try {\n response = await fetch(url, init);\n text = await response.text();\n } catch (error) {\n const reason = isTimeout(error) ? \"timeout\" : \"network\";\n return Result.fail(new HttpRequestError({ url, reason }, error));\n }\n\n const body = parseJson(text);\n\n if (!response.ok) {\n const statusError = new HttpStatusError({\n url,\n status: response.status,\n statusText: response.statusText,\n body: body.isOk() ? body.value : undefined\n });\n return Result.fail(statusError);\n }\n\n if (body.isFail()) {\n return Result.fail(new HttpRequestError({ url, reason: \"invalidJson\" }, body.error));\n }\n\n return Result.ok(body.value);\n }\n}\n\nexport const HttpClient = createImplementation({\n abstraction: HttpClientAbstraction,\n implementation: HttpClientImpl,\n dependencies: []\n});\n"],"names":["DEFAULT_TIMEOUT_MS","createRequestInit","request","timeoutMs","headers","Headers","init","AbortSignal","undefined","JSON","isTimeout","error","Error","parseJson","text","Result","value","HttpClientImpl","url","response","fetch","reason","HttpRequestError","body","statusError","HttpStatusError","HttpClient","createImplementation","HttpClientAbstraction"],"mappings":";;;;AAMA,MAAMA,qBAAqB;AAE3B,SAASC,kBAAkBC,OAAsC;IAC7D,MAAMC,YAAYD,QAAQ,SAAS,IAAIF;IACvC,MAAMI,UAAU,IAAIC,QAAQH,QAAQ,OAAO;IAC3C,MAAMI,OAAoB;QACtB,QAAQJ,QAAQ,MAAM,IAAI;QAC1BE;QACA,QAAQG,YAAY,OAAO,CAACJ;IAChC;IAEA,IAAID,AAAiBM,WAAjBN,QAAQ,IAAI,EAAgB;QAC5B,IAAI,CAACE,QAAQ,GAAG,CAAC,iBACbA,QAAQ,GAAG,CAAC,gBAAgB;QAEhCE,KAAK,IAAI,GAAGG,KAAK,SAAS,CAACP,QAAQ,IAAI;IAC3C;IAEA,OAAOI;AACX;AAEA,SAASI,UAAUC,KAAc;IAC7B,OAAOA,iBAAiBC,SAASD,AAAe,mBAAfA,MAAM,IAAI;AAC/C;AAGA,SAASE,UAAUC,IAAY;IAC3B,IAAIA,AAAS,OAATA,MACA,OAAOC,OAAO,EAAE,CAACP;IAErB,IAAI;QACA,MAAMQ,QAAiBP,KAAK,KAAK,CAACK;QAClC,OAAOC,OAAO,EAAE,CAACC;IACrB,EAAE,OAAOL,OAAO;QACZ,OAAOI,OAAO,IAAI,CAACJ;IACvB;AACJ;AAEA,MAAMM;IACF,MAAM,YACFf,OAAsC,EACe;QACrD,MAAM,EAAEgB,GAAG,EAAE,GAAGhB;QAChB,MAAMI,OAAOL,kBAAkBC;QAE/B,IAAIiB;QACJ,IAAIL;QACJ,IAAI;YACAK,WAAW,MAAMC,MAAMF,KAAKZ;YAC5BQ,OAAO,MAAMK,SAAS,IAAI;QAC9B,EAAE,OAAOR,OAAO;YACZ,MAAMU,SAASX,UAAUC,SAAS,YAAY;YAC9C,OAAOI,OAAO,IAAI,CAAC,IAAIO,iBAAiB;gBAAEJ;gBAAKG;YAAO,GAAGV;QAC7D;QAEA,MAAMY,OAAOV,UAAUC;QAEvB,IAAI,CAACK,SAAS,EAAE,EAAE;YACd,MAAMK,cAAc,IAAIC,gBAAgB;gBACpCP;gBACA,QAAQC,SAAS,MAAM;gBACvB,YAAYA,SAAS,UAAU;gBAC/B,MAAMI,KAAK,IAAI,KAAKA,KAAK,KAAK,GAAGf;YACrC;YACA,OAAOO,OAAO,IAAI,CAACS;QACvB;QAEA,IAAID,KAAK,MAAM,IACX,OAAOR,OAAO,IAAI,CAAC,IAAIO,iBAAiB;YAAEJ;YAAK,QAAQ;QAAc,GAAGK,KAAK,KAAK;QAGtF,OAAOR,OAAO,EAAE,CAACQ,KAAK,KAAK;IAC/B;AACJ;AAEO,MAAMG,wBAAaC,qBAAqB;IAC3C,aAAaC;IACb,gBAAgBX;IAChB,cAAc,EAAE;AACpB"}
@@ -0,0 +1,10 @@
1
+ import { BaseError } from "@webiny/feature/api";
2
+ export interface HttpRequestErrorData {
3
+ url: string;
4
+ reason: "network" | "timeout" | "invalidJson";
5
+ }
6
+ /** The request didn't produce a usable response: it failed, timed out, or the body wasn't JSON. */
7
+ export declare class HttpRequestError extends BaseError<HttpRequestErrorData> {
8
+ readonly code: "HttpClient/Request";
9
+ constructor(data: HttpRequestErrorData, cause: unknown);
10
+ }
@@ -0,0 +1,13 @@
1
+ import { BaseError } from "@webiny/feature/api";
2
+ class HttpRequestError extends BaseError {
3
+ constructor(data, cause){
4
+ super({
5
+ message: `Request to "${data.url}" failed (${data.reason}).`,
6
+ data
7
+ }), this.code = "HttpClient/Request";
8
+ this.cause = cause;
9
+ }
10
+ }
11
+ export { HttpRequestError };
12
+
13
+ //# sourceMappingURL=HttpRequestError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/httpClient/HttpRequestError.js","sources":["../../../src/features/httpClient/HttpRequestError.ts"],"sourcesContent":["import { BaseError } from \"@webiny/feature/api\";\n\nexport interface HttpRequestErrorData {\n url: string;\n reason: \"network\" | \"timeout\" | \"invalidJson\";\n}\n\n/** The request didn't produce a usable response: it failed, timed out, or the body wasn't JSON. */\nexport class HttpRequestError extends BaseError<HttpRequestErrorData> {\n override readonly code = \"HttpClient/Request\" as const;\n\n constructor(data: HttpRequestErrorData, cause: unknown) {\n super({\n message: `Request to \"${data.url}\" failed (${data.reason}).`,\n data\n });\n this.cause = cause;\n }\n}\n"],"names":["HttpRequestError","BaseError","data","cause"],"mappings":";AAQO,MAAMA,yBAAyBC;IAGlC,YAAYC,IAA0B,EAAEC,KAAc,CAAE;QACpD,KAAK,CAAC;YACF,SAAS,CAAC,YAAY,EAAED,KAAK,GAAG,CAAC,UAAU,EAAEA,KAAK,MAAM,CAAC,EAAE,CAAC;YAC5DA;QACJ,SANc,IAAI,GAAG;QAOrB,IAAI,CAAC,KAAK,GAAGC;IACjB;AACJ"}
@@ -0,0 +1,12 @@
1
+ import { BaseError } from "@webiny/feature/api";
2
+ export interface HttpStatusErrorData {
3
+ url: string;
4
+ status: number;
5
+ statusText: string;
6
+ body: unknown;
7
+ }
8
+ /** The server answered with a non-2xx status. */
9
+ export declare class HttpStatusError extends BaseError<HttpStatusErrorData> {
10
+ readonly code: "HttpClient/Status";
11
+ constructor(data: HttpStatusErrorData);
12
+ }
@@ -0,0 +1,12 @@
1
+ import { BaseError } from "@webiny/feature/api";
2
+ class HttpStatusError extends BaseError {
3
+ constructor(data){
4
+ super({
5
+ message: `Request to "${data.url}" failed with status ${data.status}.`,
6
+ data
7
+ }), this.code = "HttpClient/Status";
8
+ }
9
+ }
10
+ export { HttpStatusError };
11
+
12
+ //# sourceMappingURL=HttpStatusError.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/httpClient/HttpStatusError.js","sources":["../../../src/features/httpClient/HttpStatusError.ts"],"sourcesContent":["import { BaseError } from \"@webiny/feature/api\";\n\nexport interface HttpStatusErrorData {\n url: string;\n status: number;\n statusText: string;\n body: unknown; // the parsed JSON body, or `undefined` when there was none\n}\n\n/** The server answered with a non-2xx status. */\nexport class HttpStatusError extends BaseError<HttpStatusErrorData> {\n override readonly code = \"HttpClient/Status\" as const;\n\n constructor(data: HttpStatusErrorData) {\n super({\n message: `Request to \"${data.url}\" failed with status ${data.status}.`,\n data\n });\n }\n}\n"],"names":["HttpStatusError","BaseError","data"],"mappings":";AAUO,MAAMA,wBAAwBC;IAGjC,YAAYC,IAAyB,CAAE;QACnC,KAAK,CAAC;YACF,SAAS,CAAC,YAAY,EAAEA,KAAK,GAAG,CAAC,qBAAqB,EAAEA,KAAK,MAAM,CAAC,CAAC,CAAC;YACtEA;QACJ,SANc,IAAI,GAAG;IAOzB;AACJ"}
@@ -0,0 +1,32 @@
1
+ import type { Result } from "@webiny/feature/api";
2
+ import type { HttpRequestError } from "./HttpRequestError.js";
3
+ import type { HttpStatusError } from "./HttpStatusError.js";
4
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
5
+ export interface IHttpClientRequest {
6
+ url: string;
7
+ method?: HttpMethod;
8
+ headers?: Record<string, string>;
9
+ body?: unknown;
10
+ timeoutMs?: number;
11
+ }
12
+ export interface IHttpClientErrors {
13
+ request: HttpRequestError;
14
+ status: HttpStatusError;
15
+ }
16
+ type HttpClientError = IHttpClientErrors[keyof IHttpClientErrors];
17
+ export interface IHttpClient {
18
+ /**
19
+ * Sends a request and parses the response body as JSON. The body is `unknown` on purpose:
20
+ * validate it before use.
21
+ */
22
+ requestJson(request: IHttpClientRequest): Promise<Result<unknown, HttpClientError>>;
23
+ }
24
+ /** Outbound HTTP requests with a timeout, a status check and JSON parsing. */
25
+ export declare const HttpClient: import("@webiny/di").Abstraction<IHttpClient>;
26
+ export declare namespace HttpClient {
27
+ type Interface = IHttpClient;
28
+ type Request = IHttpClientRequest;
29
+ type Method = HttpMethod;
30
+ type Error = HttpClientError;
31
+ }
32
+ export {};
@@ -0,0 +1,5 @@
1
+ import { createAbstraction } from "@webiny/feature/api";
2
+ const HttpClient = createAbstraction("HttpClient");
3
+ export { HttpClient };
4
+
5
+ //# sourceMappingURL=abstractions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/httpClient/abstractions.js","sources":["../../../src/features/httpClient/abstractions.ts"],"sourcesContent":["import { createAbstraction } from \"@webiny/feature/api\";\nimport type { Result } from \"@webiny/feature/api\";\nimport type { HttpRequestError } from \"./HttpRequestError.js\";\nimport type { HttpStatusError } from \"./HttpStatusError.js\";\n\nexport type HttpMethod = \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n\nexport interface IHttpClientRequest {\n url: string;\n method?: HttpMethod; // defaults to \"GET\"\n headers?: Record<string, string>;\n body?: unknown; // sent as JSON\n timeoutMs?: number; // defaults to 10 seconds\n}\n\nexport interface IHttpClientErrors {\n request: HttpRequestError;\n status: HttpStatusError;\n}\n\ntype HttpClientError = IHttpClientErrors[keyof IHttpClientErrors];\n\nexport interface IHttpClient {\n /**\n * Sends a request and parses the response body as JSON. The body is `unknown` on purpose:\n * validate it before use.\n */\n requestJson(request: IHttpClientRequest): Promise<Result<unknown, HttpClientError>>;\n}\n\n/** Outbound HTTP requests with a timeout, a status check and JSON parsing. */\nexport const HttpClient = createAbstraction<IHttpClient>(\"HttpClient\");\n\nexport namespace HttpClient {\n export type Interface = IHttpClient;\n export type Request = IHttpClientRequest;\n export type Method = HttpMethod;\n export type Error = HttpClientError;\n}\n"],"names":["HttpClient","createAbstraction"],"mappings":";AA+BO,MAAMA,aAAaC,kBAA+B"}
@@ -0,0 +1,4 @@
1
+ export declare const HttpClientFeature: {
2
+ name: string;
3
+ register(container: import("@webiny/di").Container): void;
4
+ };
@@ -0,0 +1,11 @@
1
+ import { createFeature } from "@webiny/feature/api";
2
+ import { HttpClient } from "./HttpClient.js";
3
+ const HttpClientFeature = createFeature({
4
+ name: "HttpClientFeature",
5
+ register (container) {
6
+ container.register(HttpClient).inSingletonScope();
7
+ }
8
+ });
9
+ export { HttpClientFeature };
10
+
11
+ //# sourceMappingURL=feature.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"features/httpClient/feature.js","sources":["../../../src/features/httpClient/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { HttpClient } from \"./HttpClient.js\";\n\nexport const HttpClientFeature = createFeature({\n name: \"HttpClientFeature\",\n register(container) {\n container.register(HttpClient).inSingletonScope();\n }\n});\n"],"names":["HttpClientFeature","createFeature","container","HttpClient"],"mappings":";;AAGO,MAAMA,oBAAoBC,cAAc;IAC3C,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC,YAAY,gBAAgB;IACnD;AACJ"}
@@ -0,0 +1,3 @@
1
+ export { HttpClient } from "./abstractions.js";
2
+ export { HttpRequestError } from "./HttpRequestError.js";
3
+ export { HttpStatusError } from "./HttpStatusError.js";
@@ -0,0 +1,3 @@
1
+ export { HttpClient } from "./abstractions.js";
2
+ export { HttpRequestError } from "./HttpRequestError.js";
3
+ export { HttpStatusError } from "./HttpStatusError.js";
@@ -1,7 +1,12 @@
1
1
  import type { Jwk } from "../features/security/utils/verifyJwtUsingJwk.js";
2
+ import { HttpClient } from "../features/httpClient/index.js";
2
3
  import { JwkCache } from "./abstractions.js";
3
- export declare class JwksCache implements JwkCache.Interface {
4
+ declare class JwksCacheImpl implements JwkCache.Interface {
5
+ private readonly httpClient;
4
6
  private cache;
7
+ constructor(httpClient: HttpClient.Interface);
5
8
  getKeys(issuer: string): Promise<Jwk[]>;
6
- clearCache(): void;
9
+ private fetchJson;
7
10
  }
11
+ export declare const JwksCache: import("@webiny/di").Implementation<typeof JwksCacheImpl>;
12
+ export {};
package/idp/JwksCache.js CHANGED
@@ -1,26 +1,53 @@
1
- class JwksCache {
1
+ import { createImplementation } from "@webiny/feature/api";
2
+ import { z } from "zod";
3
+ import { HttpClient } from "../features/httpClient/index.js";
4
+ import { JwkCache } from "./abstractions.js";
5
+ const oidcConfigurationSchema = z.object({
6
+ jwks_uri: z.string()
7
+ });
8
+ const jwksSchema = z.object({
9
+ keys: z.array(z.looseObject({
10
+ kid: z.string().optional()
11
+ }))
12
+ });
13
+ function getOpenidConfigurationUrl(issuer) {
14
+ const openidUrl = new URL(issuer);
15
+ const pathname = openidUrl.pathname + "/.well-known/openid-configuration";
16
+ openidUrl.pathname = pathname.replace(/\/+/g, "/");
17
+ return openidUrl.toString();
18
+ }
19
+ class JwksCacheImpl {
20
+ constructor(httpClient){
21
+ this.httpClient = httpClient;
22
+ this.cache = new Map();
23
+ }
2
24
  async getKeys(issuer) {
3
25
  const cached = this.cache.get(issuer);
4
- if (cached) return cached.jwks;
5
- const openidUrl = new URL(issuer);
6
- const pathname = openidUrl.pathname + "/.well-known/openid-configuration";
7
- openidUrl.pathname = pathname.replace(/\/+/g, "/");
8
- const oidcConfig = await fetch(openidUrl.toString()).then((res)=>res.json());
9
- const jwksResponse = await fetch(oidcConfig.jwks_uri).then((res)=>res.json());
26
+ if (cached) return cached;
27
+ const openidConfigurationUrl = getOpenidConfigurationUrl(issuer);
28
+ const oidcConfig = await this.fetchJson(openidConfigurationUrl, oidcConfigurationSchema);
29
+ const jwksResponse = await this.fetchJson(oidcConfig.jwks_uri, jwksSchema);
10
30
  const jwks = jwksResponse.keys;
11
- this.cache.set(issuer, {
12
- oidcConfig,
13
- jwks
14
- });
31
+ this.cache.set(issuer, jwks);
15
32
  return jwks;
16
33
  }
17
- clearCache() {
18
- this.cache.clear();
19
- }
20
- constructor(){
21
- this.cache = new Map();
34
+ async fetchJson(url, schema) {
35
+ const response = await this.httpClient.requestJson({
36
+ url
37
+ });
38
+ if (response.isFail()) throw response.error;
39
+ const result = schema.safeParse(response.value);
40
+ if (!result.success) throw new Error(`Invalid response from "${url}": ${result.error.message}`);
41
+ return result.data;
22
42
  }
23
43
  }
44
+ const JwksCache = createImplementation({
45
+ abstraction: JwkCache,
46
+ implementation: JwksCacheImpl,
47
+ dependencies: [
48
+ HttpClient
49
+ ]
50
+ });
24
51
  export { JwksCache };
25
52
 
26
53
  //# sourceMappingURL=JwksCache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"idp/JwksCache.js","sources":["../../src/idp/JwksCache.ts"],"sourcesContent":["import type { Jwk } from \"~/features/security/utils/verifyJwtUsingJwk.js\";\nimport { JwkCache } from \"./abstractions.js\";\n\ninterface OidcConfiguration {\n jwks_uri: string;\n [key: string]: any;\n}\n\ninterface CacheEntry {\n oidcConfig: OidcConfiguration;\n jwks: Jwk[];\n}\n\nexport class JwksCache implements JwkCache.Interface {\n private cache = new Map<string, CacheEntry>();\n\n async getKeys(issuer: string): Promise<Jwk[]> {\n // Check cache first\n const cached = this.cache.get(issuer);\n if (cached) {\n return cached.jwks;\n }\n\n // Fetch OIDC configuration\n const openidUrl = new URL(issuer);\n const pathname = openidUrl.pathname + \"/.well-known/openid-configuration\";\n openidUrl.pathname = pathname.replace(/\\/+/g, \"/\");\n\n const oidcConfig = await fetch(openidUrl.toString()).then(\n res => res.json() as Promise<OidcConfiguration>\n );\n\n // Fetch JWKs from jwks_uri\n const jwksResponse = await fetch(oidcConfig.jwks_uri).then(res => res.json());\n const jwks = jwksResponse.keys as Jwk[];\n\n // Cache both config and JWKs\n this.cache.set(issuer, {\n oidcConfig,\n jwks\n });\n\n return jwks;\n }\n\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"names":["JwksCache","issuer","cached","openidUrl","URL","pathname","oidcConfig","fetch","res","jwksResponse","jwks","Map"],"mappings":"AAaO,MAAMA;IAGT,MAAM,QAAQC,MAAc,EAAkB;QAE1C,MAAMC,SAAS,IAAI,CAAC,KAAK,CAAC,GAAG,CAACD;QAC9B,IAAIC,QACA,OAAOA,OAAO,IAAI;QAItB,MAAMC,YAAY,IAAIC,IAAIH;QAC1B,MAAMI,WAAWF,UAAU,QAAQ,GAAG;QACtCA,UAAU,QAAQ,GAAGE,SAAS,OAAO,CAAC,QAAQ;QAE9C,MAAMC,aAAa,MAAMC,MAAMJ,UAAU,QAAQ,IAAI,IAAI,CACrDK,CAAAA,MAAOA,IAAI,IAAI;QAInB,MAAMC,eAAe,MAAMF,MAAMD,WAAW,QAAQ,EAAE,IAAI,CAACE,CAAAA,MAAOA,IAAI,IAAI;QAC1E,MAAME,OAAOD,aAAa,IAAI;QAG9B,IAAI,CAAC,KAAK,CAAC,GAAG,CAACR,QAAQ;YACnBK;YACAI;QACJ;QAEA,OAAOA;IACX;IAEA,aAAmB;QACf,IAAI,CAAC,KAAK,CAAC,KAAK;IACpB;;aAjCQ,KAAK,GAAG,IAAIC;;AAkCxB"}
1
+ {"version":3,"file":"idp/JwksCache.js","sources":["../../src/idp/JwksCache.ts"],"sourcesContent":["import { createImplementation } from \"@webiny/feature/api\";\nimport { z } from \"zod\";\nimport type { Jwk } from \"~/features/security/utils/verifyJwtUsingJwk.js\";\nimport { HttpClient } from \"~/features/httpClient/index.js\";\nimport { JwkCache } from \"./abstractions.js\";\n\nconst oidcConfigurationSchema = z.object({\n jwks_uri: z.string()\n});\n\nconst jwksSchema = z.object({\n keys: z.array(z.looseObject({ kid: z.string().optional() }))\n});\n\nfunction getOpenidConfigurationUrl(issuer: string): string {\n const openidUrl = new URL(issuer);\n const pathname = openidUrl.pathname + \"/.well-known/openid-configuration\";\n openidUrl.pathname = pathname.replace(/\\/+/g, \"/\");\n return openidUrl.toString();\n}\n\nclass JwksCacheImpl implements JwkCache.Interface {\n private cache = new Map<string, Jwk[]>();\n\n constructor(private readonly httpClient: HttpClient.Interface) {}\n\n async getKeys(issuer: string): Promise<Jwk[]> {\n const cached = this.cache.get(issuer);\n if (cached) {\n return cached;\n }\n\n const openidConfigurationUrl = getOpenidConfigurationUrl(issuer);\n const oidcConfig = await this.fetchJson(openidConfigurationUrl, oidcConfigurationSchema);\n const jwksResponse = await this.fetchJson(oidcConfig.jwks_uri, jwksSchema);\n const jwks: Jwk[] = jwksResponse.keys;\n\n this.cache.set(issuer, jwks);\n\n return jwks;\n }\n\n // Throws, so a failed fetch fails the token verification instead of caching a bad answer.\n private async fetchJson<T>(url: string, schema: z.ZodType<T>): Promise<T> {\n const response = await this.httpClient.requestJson({ url });\n if (response.isFail()) {\n throw response.error;\n }\n\n const result = schema.safeParse(response.value);\n if (!result.success) {\n throw new Error(`Invalid response from \"${url}\": ${result.error.message}`);\n }\n\n return result.data;\n }\n}\n\nexport const JwksCache = createImplementation({\n abstraction: JwkCache,\n implementation: JwksCacheImpl,\n dependencies: [HttpClient]\n});\n"],"names":["oidcConfigurationSchema","z","jwksSchema","getOpenidConfigurationUrl","issuer","openidUrl","URL","pathname","JwksCacheImpl","httpClient","Map","cached","openidConfigurationUrl","oidcConfig","jwksResponse","jwks","url","schema","response","result","Error","JwksCache","createImplementation","JwkCache","HttpClient"],"mappings":";;;;AAMA,MAAMA,0BAA0BC,EAAE,MAAM,CAAC;IACrC,UAAUA,EAAE,MAAM;AACtB;AAEA,MAAMC,aAAaD,EAAE,MAAM,CAAC;IACxB,MAAMA,EAAE,KAAK,CAACA,EAAE,WAAW,CAAC;QAAE,KAAKA,EAAE,MAAM,GAAG,QAAQ;IAAG;AAC7D;AAEA,SAASE,0BAA0BC,MAAc;IAC7C,MAAMC,YAAY,IAAIC,IAAIF;IAC1B,MAAMG,WAAWF,UAAU,QAAQ,GAAG;IACtCA,UAAU,QAAQ,GAAGE,SAAS,OAAO,CAAC,QAAQ;IAC9C,OAAOF,UAAU,QAAQ;AAC7B;AAEA,MAAMG;IAGF,YAA6BC,UAAgC,CAAE;aAAlCA,UAAU,GAAVA;aAFrB,KAAK,GAAG,IAAIC;IAE4C;IAEhE,MAAM,QAAQN,MAAc,EAAkB;QAC1C,MAAMO,SAAS,IAAI,CAAC,KAAK,CAAC,GAAG,CAACP;QAC9B,IAAIO,QACA,OAAOA;QAGX,MAAMC,yBAAyBT,0BAA0BC;QACzD,MAAMS,aAAa,MAAM,IAAI,CAAC,SAAS,CAACD,wBAAwBZ;QAChE,MAAMc,eAAe,MAAM,IAAI,CAAC,SAAS,CAACD,WAAW,QAAQ,EAAEX;QAC/D,MAAMa,OAAcD,aAAa,IAAI;QAErC,IAAI,CAAC,KAAK,CAAC,GAAG,CAACV,QAAQW;QAEvB,OAAOA;IACX;IAGA,MAAc,UAAaC,GAAW,EAAEC,MAAoB,EAAc;QACtE,MAAMC,WAAW,MAAM,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC;YAAEF;QAAI;QACzD,IAAIE,SAAS,MAAM,IACf,MAAMA,SAAS,KAAK;QAGxB,MAAMC,SAASF,OAAO,SAAS,CAACC,SAAS,KAAK;QAC9C,IAAI,CAACC,OAAO,OAAO,EACf,MAAM,IAAIC,MAAM,CAAC,uBAAuB,EAAEJ,IAAI,GAAG,EAAEG,OAAO,KAAK,CAAC,OAAO,EAAE;QAG7E,OAAOA,OAAO,IAAI;IACtB;AACJ;AAEO,MAAME,YAAYC,qBAAqB;IAC1C,aAAaC;IACb,gBAAgBf;IAChB,cAAc;QAACgB;KAAW;AAC9B"}
package/idp/feature.js CHANGED
@@ -1,6 +1,5 @@
1
1
  import { createFeature } from "@webiny/feature/api";
2
2
  import { JwtAuthenticator } from "./JwtAuthenticator.js";
3
- import { JwkCache } from "./abstractions.js";
4
3
  import { JwksCache } from "./JwksCache.js";
5
4
  import { OidcJwtIdentityProvider } from "./OidcJwtIdentityProvider.js";
6
5
  const IdpAuthenticatorFeature = createFeature({
@@ -8,7 +7,7 @@ const IdpAuthenticatorFeature = createFeature({
8
7
  register (container) {
9
8
  container.register(JwtAuthenticator);
10
9
  container.register(OidcJwtIdentityProvider);
11
- container.registerInstance(JwkCache, new JwksCache());
10
+ container.register(JwksCache).inSingletonScope();
12
11
  }
13
12
  });
14
13
  export { IdpAuthenticatorFeature };
@@ -1 +1 @@
1
- {"version":3,"file":"idp/feature.js","sources":["../../src/idp/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { JwtAuthenticator } from \"./JwtAuthenticator.js\";\nimport { JwkCache } from \"./abstractions.js\";\nimport { JwksCache } from \"./JwksCache.js\";\nimport { OidcJwtIdentityProvider } from \"~/idp/OidcJwtIdentityProvider.js\";\n\nexport const IdpAuthenticatorFeature = createFeature({\n name: \"IdpAuthenticator\",\n register(container) {\n container.register(JwtAuthenticator);\n container.register(OidcJwtIdentityProvider);\n container.registerInstance(JwkCache, new JwksCache());\n }\n});\n"],"names":["IdpAuthenticatorFeature","createFeature","container","JwtAuthenticator","OidcJwtIdentityProvider","JwkCache","JwksCache"],"mappings":";;;;;AAMO,MAAMA,0BAA0BC,cAAc;IACjD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;QACnBD,UAAU,QAAQ,CAACE;QACnBF,UAAU,gBAAgB,CAACG,UAAU,IAAIC;IAC7C;AACJ"}
1
+ {"version":3,"file":"idp/feature.js","sources":["../../src/idp/feature.ts"],"sourcesContent":["import { createFeature } from \"@webiny/feature/api\";\nimport { JwtAuthenticator } from \"./JwtAuthenticator.js\";\nimport { JwksCache } from \"./JwksCache.js\";\nimport { OidcJwtIdentityProvider } from \"~/idp/OidcJwtIdentityProvider.js\";\n\nexport const IdpAuthenticatorFeature = createFeature({\n name: \"IdpAuthenticator\",\n register(container) {\n container.register(JwtAuthenticator);\n container.register(OidcJwtIdentityProvider);\n // Singleton, so the keys stay cached for the life of the process.\n container.register(JwksCache).inSingletonScope();\n }\n});\n"],"names":["IdpAuthenticatorFeature","createFeature","container","JwtAuthenticator","OidcJwtIdentityProvider","JwksCache"],"mappings":";;;;AAKO,MAAMA,0BAA0BC,cAAc;IACjD,MAAM;IACN,UAASC,SAAS;QACdA,UAAU,QAAQ,CAACC;QACnBD,UAAU,QAAQ,CAACE;QAEnBF,UAAU,QAAQ,CAACG,WAAW,gBAAgB;IAClD;AACJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webiny/api-core",
3
- "version": "6.5.0-beta.5",
3
+ "version": "6.5.0-beta.6",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,17 +17,17 @@
17
17
  "@ai-sdk/anthropic": "4.0.65",
18
18
  "@ai-sdk/google": "4.0.82",
19
19
  "@ai-sdk/openai": "4.0.78",
20
- "@webiny/api": "6.5.0-beta.5",
20
+ "@webiny/api": "6.5.0-beta.6",
21
21
  "@webiny/di": "1.1.0",
22
- "@webiny/error": "6.5.0-beta.5",
23
- "@webiny/feature": "6.5.0-beta.5",
24
- "@webiny/feature-flags": "6.5.0-beta.5",
25
- "@webiny/handler": "6.5.0-beta.5",
26
- "@webiny/handler-db": "6.5.0-beta.5",
27
- "@webiny/handler-graphql": "6.5.0-beta.5",
28
- "@webiny/plugins": "6.5.0-beta.5",
29
- "@webiny/utils": "6.5.0-beta.5",
30
- "@webiny/wcp": "6.5.0-beta.5",
22
+ "@webiny/error": "6.5.0-beta.6",
23
+ "@webiny/feature": "6.5.0-beta.6",
24
+ "@webiny/feature-flags": "6.5.0-beta.6",
25
+ "@webiny/handler": "6.5.0-beta.6",
26
+ "@webiny/handler-db": "6.5.0-beta.6",
27
+ "@webiny/handler-graphql": "6.5.0-beta.6",
28
+ "@webiny/plugins": "6.5.0-beta.6",
29
+ "@webiny/utils": "6.5.0-beta.6",
30
+ "@webiny/wcp": "6.5.0-beta.6",
31
31
  "ai": "7.0.118",
32
32
  "dataloader": "2.2.3",
33
33
  "jose": "6.2.12",
@@ -40,9 +40,9 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/jsonwebtoken": "9.0.10",
43
- "@webiny/build-tools": "6.5.0-beta.5",
44
- "@webiny/handler-aws": "6.5.0-beta.5",
45
- "@webiny/project-utils": "6.5.0-beta.5",
43
+ "@webiny/build-tools": "6.5.0-beta.6",
44
+ "@webiny/handler-aws": "6.5.0-beta.6",
45
+ "@webiny/project-utils": "6.5.0-beta.6",
46
46
  "rimraf": "6.1.3",
47
47
  "typescript": "7.0.2",
48
48
  "vitest": "5.0.2"