@webiny/mcp 6.1.0 → 6.2.0

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.
@@ -3,73 +3,128 @@ name: webiny-api-permissions
3
3
  context: webiny-api
4
4
  description: >
5
5
  Schema-based permission system for API features. Use this skill when implementing
6
- authorization in use cases, defining permission schemas with createPermissions,
6
+ authorization in use cases, defining permission schemas with createPermissionSchema,
7
+ creating injectable permissions via createPermissionsAbstraction/createPermissionsFeature,
7
8
  checking read/write/delete/publish permissions, handling own-record scoping,
8
9
  or testing permission scenarios. Covers the full pattern from schema definition
9
10
  to use case integration to test matrices.
10
11
  ---
11
12
 
12
- # Schema-Based Permissions
13
+ # API Permissions
13
14
 
14
15
  ## Overview
15
16
 
16
- Webiny uses a **schema-based permission system** defined via `createPermissions`. Each package declares a permission schema and gets a typed `Permissions` abstraction injectable into use cases via DI. This replaces manual `identityContext.getPermission()` calls with high-level methods like `canRead`, `canEdit`, `canDelete`, `canPublish`, `onlyOwnRecords`, etc.
17
+ Permissions follow two layers: **domain** (schema) and **features** (DI abstractions + feature registration). Each package declares a permission schema and gets a typed `Permissions` abstraction injectable into use cases via DI. Methods like `canRead`, `canEdit`, `canDelete`, `canPublish`, `onlyOwnRecords` replace manual `identityContext.getPermission()` calls.
17
18
 
18
- ## Permission Schema Definition
19
+ ## Layer 1: Domain — Permission Schema
20
+
21
+ Define the schema in `src/domain/permissionsSchema.ts`:
19
22
 
20
23
  ```ts
21
- // domain/permissions.ts (or permissions/schema.ts)
22
- import { createPermissions } from "@webiny/api-core/features/security/permissions/index.js";
23
- import type { Permissions } from "@webiny/api-core/features/security/permissions/index.js";
24
+ import { createPermissionSchema } from "webiny/api/security";
24
25
 
25
- const schema = {
26
- prefix: "wb", // Permission prefix
27
- fullAccess: { name: "wb.*" }, // Wildcard = full access to all entities
26
+ export const SM_PERMISSIONS_SCHEMA = createPermissionSchema({
27
+ prefix: "sm",
28
+ fullAccess: true,
28
29
  entities: [
29
30
  {
30
- id: "page", // Entity identifier (used in method calls)
31
- permission: "wb.page", // Permission name stored on the identity
32
- scopes: ["full", "own"], // Access scopes: "full" = all records, "own" = only own
33
- actions: [
34
- { name: "rwd" }, // Read/Write/Delete (chars: "r", "w", "d")
35
- { name: "pw" } // Publish/Unpublish (chars: "p", "u")
36
- ]
31
+ id: "product",
32
+ permission: "sm.product",
33
+ scopes: ["full", "own"],
34
+ actions: [{ name: "rwd" }, { name: "pw" }]
37
35
  },
38
36
  {
39
37
  id: "settings",
40
- permission: "wb.settings",
41
- scopes: ["full"] // No "own" — no ownership concept for settings
38
+ permission: "sm.settings",
39
+ scopes: ["full"]
42
40
  }
43
41
  ]
44
- } as const; // MUST use `as const` for type narrowing
45
-
46
- type MySchema = typeof schema;
47
-
48
- export const MyPermissions = createPermissions(schema);
49
-
50
- export namespace MyPermissions {
51
- export type Interface = Permissions<MySchema>;
52
- }
42
+ });
53
43
  ```
54
44
 
55
- `createPermissions` returns `{ Abstraction, Implementation }`. Register the Implementation in your feature.
45
+ The schema MUST use `as const` inference (handled by `createPermissionSchema`) for TypeScript to narrow entity IDs in method signatures.
56
46
 
57
47
  ### Schema Fields
58
48
 
59
- | Field | Description |
60
- | ----------------------- | --------------------------------------------------------------------- |
61
- | `prefix` | Namespaces the DI abstraction: `${prefix}:Permissions` |
62
- | `fullAccess.name` | Wildcard permission (e.g. `"wb.*"`) grants all entity access |
63
- | `entities[].id` | Entity identifier used in method calls: `canRead("page")` |
64
- | `entities[].permission` | Permission name matched against identity permissions |
65
- | `entities[].scopes` | `["full"]` or `["full", "own"]` — determines if own-scope supported |
66
- | `entities[].actions` | Action definitions — built-in: `"rwd"`, `"pw"`; custom: boolean flags |
49
+ | Field | Description |
50
+ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
51
+ | `prefix` | Namespaces the DI abstraction: `${prefix}:Permissions` |
52
+ | `fullAccess` | `true` for standard full access. Pass an object with custom boolean flags for full-access extras (e.g., `{ canForceUnlock: true }`). |
53
+ | `entities[].id` | Entity identifier used in method calls: `canRead("product")` |
54
+ | `entities[].permission` | Permission name matched against identity permissions |
55
+ | `entities[].scopes` | `["full"]` or `["full", "own"]` — determines if own-scope supported |
56
+ | `entities[].actions` | Action definitions — built-in: `"rwd"`, `"pw"`; custom: boolean flags |
67
57
 
68
58
  ### Scopes
69
59
 
70
60
  - **`"full"`** — User can access all records (default when no `own` flag on permission object)
71
61
  - **`"own"`** — User can only access records where `createdBy.id === identity.id`
72
62
 
63
+ ### Simple Apps (No Entities)
64
+
65
+ Omit `entities` for binary full/no access:
66
+
67
+ ```ts
68
+ export const MA_PERMISSIONS_SCHEMA = createPermissionSchema({
69
+ prefix: "ma",
70
+ fullAccess: true
71
+ });
72
+ ```
73
+
74
+ ---
75
+
76
+ ## Layer 2: Features — DI Artifacts + Registration
77
+
78
+ ### Abstraction (`src/features/permissions/abstractions.ts`)
79
+
80
+ ```ts
81
+ import { createPermissionsAbstraction } from "webiny/api/security";
82
+ import type { Permissions } from "webiny/api/security";
83
+ import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
84
+
85
+ export const SmPermissions = createPermissionsAbstraction(SM_PERMISSIONS_SCHEMA);
86
+
87
+ export namespace SmPermissions {
88
+ export type Interface = Permissions<typeof SM_PERMISSIONS_SCHEMA>;
89
+ }
90
+ ```
91
+
92
+ ### Feature (`src/features/permissions/feature.ts`)
93
+
94
+ ```ts
95
+ import { createPermissionsFeature } from "webiny/api/security";
96
+ import { SM_PERMISSIONS_SCHEMA } from "~/domain/permissionsSchema.js";
97
+ import { SmPermissions } from "./abstractions.js";
98
+
99
+ export const SmPermissionsFeature = createPermissionsFeature(SM_PERMISSIONS_SCHEMA, SmPermissions);
100
+ ```
101
+
102
+ ### Registration
103
+
104
+ Register the feature in your context plugin:
105
+
106
+ ```ts
107
+ import { SmPermissionsFeature } from "~/features/permissions/feature.js";
108
+
109
+ // In createContext:
110
+ SmPermissionsFeature.register(container);
111
+ ```
112
+
113
+ ---
114
+
115
+ ## File Structure
116
+
117
+ ```
118
+ src/
119
+ ├── domain/
120
+ │ └── permissionsSchema.ts # createPermissionSchema()
121
+ ├── features/
122
+ │ └── permissions/
123
+ │ ├── abstractions.ts # createPermissionsAbstraction() + namespace type
124
+ │ └── feature.ts # createPermissionsFeature()
125
+ └── index.ts # SmPermissionsFeature.register(container)
126
+ ```
127
+
73
128
  ---
74
129
 
75
130
  ## Permission Methods
@@ -77,7 +132,7 @@ export namespace MyPermissions {
77
132
  All methods follow a 3-tier bypass:
78
133
 
79
134
  1. `identityContext.hasFullAccess()` → `name: "*"` permission (super admin)
80
- 2. `hasFullSchemaAccess()` → wildcard permission (e.g. `"wb.*"`)
135
+ 2. `hasFullSchemaAccess()` → wildcard permission (e.g. `"sm.*"`)
81
136
  3. Entity-level permission check
82
137
 
83
138
  ### Method Reference
@@ -94,7 +149,7 @@ All methods follow a 3-tier bypass:
94
149
  | `canUnpublish(entity)` | Unpublish permission | No | Checks `pw` includes `"u"` |
95
150
  | `canAction(action, entity)` | Custom boolean action | No | Checks `permission[action] === true` |
96
151
 
97
- All return `Promise<boolean>`.
152
+ All return `Promise<boolean>`. Entity IDs are fully typed — `canRead("bogus")` produces a type error.
98
153
 
99
154
  ### OwnableItem Interface
100
155
 
@@ -113,20 +168,20 @@ interface OwnableItem {
113
168
  The Get use case is the **central ownership gate** — mutation use cases that delegate to GetById inherit ownership enforcement automatically.
114
169
 
115
170
  ```ts
116
- import { Result } from "@webiny/feature/api";
171
+ import { Result } from "webiny/api";
117
172
  import { GetByIdUseCase as UseCaseAbstraction, GetByIdRepository } from "./abstractions.js";
118
- import { MyPermissions } from "~/domain/permissions.js";
173
+ import { SmPermissions } from "~/features/permissions/abstractions.js";
119
174
  import { NotAuthorizedError } from "~/domain/errors.js";
120
175
 
121
176
  class GetByIdUseCaseImpl implements UseCaseAbstraction.Interface {
122
177
  constructor(
123
- private permissions: MyPermissions.Interface,
178
+ private permissions: SmPermissions.Interface,
124
179
  private repository: GetByIdRepository.Interface
125
180
  ) {}
126
181
 
127
182
  async execute(id: string): UseCaseAbstraction.Return {
128
183
  // 1. Entity-level read check
129
- if (!(await this.permissions.canRead("entity"))) {
184
+ if (!(await this.permissions.canRead("product"))) {
130
185
  return Result.fail(new NotAuthorizedError());
131
186
  }
132
187
 
@@ -137,7 +192,7 @@ class GetByIdUseCaseImpl implements UseCaseAbstraction.Interface {
137
192
  }
138
193
 
139
194
  // 3. Item-level ownership check
140
- if (!(await this.permissions.canAccess("entity", result.value))) {
195
+ if (!(await this.permissions.canAccess("product", result.value))) {
141
196
  return Result.fail(new NotAuthorizedError());
142
197
  }
143
198
 
@@ -147,31 +202,31 @@ class GetByIdUseCaseImpl implements UseCaseAbstraction.Interface {
147
202
 
148
203
  export const GetByIdUseCase = UseCaseAbstraction.createImplementation({
149
204
  implementation: GetByIdUseCaseImpl,
150
- dependencies: [MyPermissions.Abstraction, GetByIdRepository]
205
+ dependencies: [SmPermissions, GetByIdRepository]
151
206
  });
152
207
  ```
153
208
 
154
209
  ### List Use Case (Read + Own Records Filter)
155
210
 
156
211
  ```ts
157
- import { IdentityContext } from "@webiny/api-core/features/security/IdentityContext/index.js";
212
+ import { IdentityContext } from "webiny/api/security";
158
213
 
159
214
  class ListUseCaseImpl implements UseCaseAbstraction.Interface {
160
215
  constructor(
161
- private permissions: MyPermissions.Interface,
216
+ private permissions: SmPermissions.Interface,
162
217
  private identityContext: IdentityContext.Interface,
163
218
  private repository: ListRepository.Interface
164
219
  ) {}
165
220
 
166
221
  async execute(params: UseCaseAbstraction.Params): UseCaseAbstraction.Return {
167
- if (!(await this.permissions.canRead("entity"))) {
222
+ if (!(await this.permissions.canRead("product"))) {
168
223
  return Result.fail(new NotAuthorizedError());
169
224
  }
170
225
 
171
226
  const where = { ...params.where };
172
227
 
173
228
  // Filter to own records if needed
174
- if (await this.permissions.onlyOwnRecords("entity")) {
229
+ if (await this.permissions.onlyOwnRecords("product")) {
175
230
  const identity = this.identityContext.getIdentity();
176
231
  where.createdBy = identity.id;
177
232
  }
@@ -181,7 +236,7 @@ class ListUseCaseImpl implements UseCaseAbstraction.Interface {
181
236
  }
182
237
 
183
238
  // Dependencies must include IdentityContext
184
- dependencies: [MyPermissions.Abstraction, IdentityContext, ListRepository];
239
+ dependencies: [SmPermissions, IdentityContext, ListRepository];
185
240
  ```
186
241
 
187
242
  **Important:** The list `where` type must include `createdBy?: string`. For CMS-based entities, `CmsEntryListWhere` already has this.
@@ -191,14 +246,14 @@ dependencies: [MyPermissions.Abstraction, IdentityContext, ListRepository];
191
246
  ```ts
192
247
  class UpdateUseCaseImpl implements UseCaseAbstraction.Interface {
193
248
  constructor(
194
- private permissions: MyPermissions.Interface,
195
- private getById: GetByIdUseCase.Interface, // Delegates ownership gate
249
+ private permissions: SmPermissions.Interface,
250
+ private getById: GetByIdUseCase.Interface,
196
251
  private repository: UpdateRepository.Interface
197
252
  ) {}
198
253
 
199
254
  async execute(id: string, data: UpdateData): UseCaseAbstraction.Return {
200
255
  // 1. Entity-level edit check (no item yet)
201
- if (!(await this.permissions.canEdit("entity"))) {
256
+ if (!(await this.permissions.canEdit("product"))) {
202
257
  return Result.fail(new NotAuthorizedError());
203
258
  }
204
259
 
@@ -211,7 +266,7 @@ class UpdateUseCaseImpl implements UseCaseAbstraction.Interface {
211
266
  const original = getResult.value;
212
267
 
213
268
  // 3. Item-level edit check (defense in depth)
214
- if (!(await this.permissions.canEdit("entity", original))) {
269
+ if (!(await this.permissions.canEdit("product", original))) {
215
270
  return Result.fail(new NotAuthorizedError());
216
271
  }
217
272
 
@@ -238,7 +293,7 @@ class DeleteUseCaseImpl implements UseCaseAbstraction.Interface {
238
293
  const item = getResult.value;
239
294
 
240
295
  // Item-level delete check — MUST pass the item
241
- if (!(await this.permissions.canDelete("entity", item))) {
296
+ if (!(await this.permissions.canDelete("product", item))) {
242
297
  return Result.fail(new NotAuthorizedError());
243
298
  }
244
299
 
@@ -253,7 +308,7 @@ class DeleteUseCaseImpl implements UseCaseAbstraction.Interface {
253
308
  class PublishUseCaseImpl {
254
309
  async execute(params: Params): UseCaseAbstraction.Return {
255
310
  // 1. Entity-level publish check
256
- if (!(await this.permissions.canPublish("entity"))) {
311
+ if (!(await this.permissions.canPublish("product"))) {
257
312
  return Result.fail(new NotAuthorizedError());
258
313
  }
259
314
 
@@ -264,7 +319,7 @@ class PublishUseCaseImpl {
264
319
  }
265
320
 
266
321
  // 3. Item-level ownership check (defense in depth)
267
- if (!(await this.permissions.canAccess("entity", getResult.value))) {
322
+ if (!(await this.permissions.canAccess("product", getResult.value))) {
268
323
  return Result.fail(new NotAuthorizedError());
269
324
  }
270
325
 
@@ -275,17 +330,44 @@ class PublishUseCaseImpl {
275
330
 
276
331
  ---
277
332
 
333
+ ## DI Injection
334
+
335
+ The permissions abstraction is passed directly as a dependency — it IS the DI key:
336
+
337
+ ```ts
338
+ export const MyUseCase = UseCaseAbstraction.createImplementation({
339
+ implementation: MyUseCaseImpl,
340
+ dependencies: [SmPermissions, OtherDep]
341
+ });
342
+ ```
343
+
344
+ **Note:** Use `SmPermissions` directly (not `SmPermissions.Abstraction`). The abstraction returned by `createPermissionsAbstraction` is the DI key itself.
345
+
346
+ ---
347
+
278
348
  ## Gotchas
279
349
 
280
350
  1. **`canDelete` without item + `own: true` = `false`** — Always pass the item to `canDelete`. Fetch first, then check.
281
351
  2. **`canEdit` without item + `own: true` = `true`** — Intentional: allows editing new/unsaved records.
282
352
  3. **`canAccess` without item = `true`** — Only checks entity-level access, not ownership.
283
353
  4. **List where type** — Ensure the `where` interface includes `createdBy?: string` for own-scope filtering.
284
- 5. **`as const`**The schema MUST use `as const` for TypeScript to narrow entity IDs in method signatures.
285
- 6. **Dependencies order** DI constructor params must match the `dependencies` array order exactly.
354
+ 5. **Dependencies order**DI constructor params must match the `dependencies` array order exactly.
355
+ 6. **Abstraction is the DI key** Use `SmPermissions` directly in dependencies, not `SmPermissions.Abstraction`.
356
+
357
+ ## Matching Admin-Side Permissions
358
+
359
+ The API schema and the admin-side `createPermissionSchema` should use the **same prefix, entity IDs, and action names**. This ensures the permissions emitted by the admin UI are correctly evaluated by the API.
360
+
361
+ ```
362
+ API: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
363
+ Admin: createPermissionSchema({ prefix: "sm", entities: [{ id: "product", permission: "sm.product", ... }] })
364
+ ```
365
+
366
+ See **webiny-admin-permissions** for the admin-side implementation.
286
367
 
287
368
  ## Related Skills
288
369
 
370
+ - **webiny-admin-permissions** — Admin-side permission UI and DI-backed permission checking
289
371
  - **webiny-api-architect** — Architecture overview, Services vs UseCases, feature structure
290
372
  - **webiny-use-case-pattern** — UseCase implementation, Result handling, decorators
291
373
  - **webiny-dependency-injection** — Injectable services catalog
@@ -20,8 +20,7 @@ Every Webiny extension type uses the same DI pattern: define a class implementin
20
20
 
21
21
  ```typescript
22
22
  import { SomeFactory } from "webiny/some/path";
23
- import { Logger } from "webiny/api/logger";
24
- import { BuildParams } from "webiny/api/build-params";
23
+ import { Logger, BuildParams } from "webiny/api";
25
24
 
26
25
  class MyImplementation implements SomeFactory.Interface {
27
26
  constructor(
@@ -151,6 +150,203 @@ export default CorePulumi.createImplementation({
151
150
  });
152
151
  ```
153
152
 
153
+ ## Advanced Dependency Options
154
+
155
+ The `dependencies` array supports three forms per entry:
156
+
157
+ | Form | Meaning |
158
+ |------|---------|
159
+ | `Abstraction` | Single required dependency (shorthand) |
160
+ | `[Abstraction, { optional: true }]` | Single optional dependency — injects `undefined` if not registered |
161
+ | `[Abstraction, { multiple: true }]` | Multi-injection — injects **all** registered implementations as `T[]` |
162
+ | `[Abstraction, { multiple: true, optional: true }]` | Multi-injection, optional — injects `undefined` if none registered (vs empty `[]` with just `multiple`) |
163
+
164
+ ### Multi-injection (`{ multiple: true }`)
165
+
166
+ Use when a class needs all registered implementations of an abstraction. The container calls `resolveAll()` internally and injects the results as an array.
167
+
168
+ **Abstraction:**
169
+
170
+ ```ts
171
+ interface IPageType {
172
+ name: string;
173
+ label: string;
174
+ modify(form: IFormModel): void;
175
+ }
176
+
177
+ export const PageType = createAbstraction<IPageType>("PageType");
178
+
179
+ export namespace PageType {
180
+ export type Interface = IPageType;
181
+ }
182
+ ```
183
+
184
+ **Multiple implementations registered separately:**
185
+
186
+ ```ts
187
+ // StaticPageType.ts
188
+ class StaticPageTypeImpl implements PageType.Interface {
189
+ name = "static";
190
+ label = "Static Page";
191
+ modify(form: IFormModel) { /* no-op — base form is sufficient */ }
192
+ }
193
+
194
+ export const StaticPageType = PageType.createImplementation({
195
+ implementation: StaticPageTypeImpl,
196
+ dependencies: []
197
+ });
198
+
199
+ // ProductPageType.ts (in another package/extension)
200
+ class ProductPageTypeImpl implements PageType.Interface {
201
+ name = "product";
202
+ label = "Product Page";
203
+ modify(form: IFormModel) {
204
+ form.fields(fields => ({
205
+ product: fields.select().label("Product").required("Product is required")
206
+ }));
207
+ form.field("title").disabled(true);
208
+ form.field("path").disabled(true);
209
+ }
210
+ }
211
+
212
+ export const ProductPageType = PageType.createImplementation({
213
+ implementation: ProductPageTypeImpl,
214
+ dependencies: []
215
+ });
216
+ ```
217
+
218
+ **Consumer injects the array:**
219
+
220
+ ```ts
221
+ class CreatePagePresenterImpl implements CreatePagePresenter.Interface {
222
+ constructor(
223
+ private factory: FormModelFactory.Interface,
224
+ private pageTypes: PageType.Interface[],
225
+ private modifiers: CreatePageFormModifier.Interface[]
226
+ ) {}
227
+ }
228
+
229
+ export const CreatePagePresenter = PresenterAbstraction.createImplementation({
230
+ implementation: CreatePagePresenterImpl,
231
+ dependencies: [
232
+ FormModelFactory,
233
+ [PageType, { multiple: true }],
234
+ [CreatePageFormModifier, { multiple: true }]
235
+ ]
236
+ });
237
+ ```
238
+
239
+ **Registration — each implementation is a separate `container.register()` call:**
240
+
241
+ ```ts
242
+ export const CreatePageFeature = createFeature({
243
+ name: "CreatePage",
244
+ register(container) {
245
+ container.register(StaticPageType); // first PageType impl
246
+ container.register(ProductPageType); // second PageType impl
247
+ container.register(CreatePagePresenter);
248
+ }
249
+ });
250
+ ```
251
+
252
+ When `CreatePagePresenter` is resolved, `pageTypes` receives `[StaticPageTypeImpl, ProductPageTypeImpl]` in registration order.
253
+
254
+ ### Optional dependency (`{ optional: true }`)
255
+
256
+ Use when a dependency may not be registered. The container injects `undefined` instead of throwing.
257
+
258
+ ```ts
259
+ class MyPresenterImpl {
260
+ constructor(
261
+ private required: RequiredService.Interface,
262
+ private analytics: AnalyticsService.Interface | undefined
263
+ ) {}
264
+ }
265
+
266
+ export const MyPresenter = Abstraction.createImplementation({
267
+ implementation: MyPresenterImpl,
268
+ dependencies: [
269
+ RequiredService,
270
+ [AnalyticsService, { optional: true }]
271
+ ]
272
+ });
273
+ ```
274
+
275
+ ## Container API Reference
276
+
277
+ ### Registration
278
+
279
+ | Method | Description |
280
+ |--------|-------------|
281
+ | `container.register(Impl)` | Register a class implementation. Returns `RegistrationBuilder` with `.inSingletonScope()`. Multiple registrations of the same abstraction accumulate — `resolve()` returns the last, `resolveAll()` returns all. |
282
+ | `container.registerInstance(Abstraction, instance)` | Register a pre-built instance (no constructor resolution). |
283
+ | `container.registerFactory(Abstraction, factory)` | Register a factory function. Called on every `resolve()`. |
284
+ | `container.registerDecorator(Decorator)` | Register a decorator that wraps resolved instances. Applied in registration order. |
285
+ | `container.registerComposite(Composite)` | Register a composite that aggregates all implementations behind a single `resolve()`. |
286
+
287
+ ### Resolution
288
+
289
+ | Method | Description |
290
+ |--------|-------------|
291
+ | `container.resolve(Abstraction)` | Resolve single instance (last registered wins). Throws if not registered. |
292
+ | `container.resolveAll(Abstraction)` | Resolve all registered implementations as `T[]`. Returns empty array if none. |
293
+ | `container.createChildContainer()` | Create a child container that inherits parent registrations. |
294
+
295
+ ### Lifetime Scopes
296
+
297
+ - **Transient** (default): New instance on every `resolve()`.
298
+ - **Singleton** (`.inSingletonScope()`): Cached after first resolution, one instance per container.
299
+
300
+ **Convention:** Use cases = transient. Repositories, gateways, services, registries = singleton.
301
+
302
+ ### Decorators
303
+
304
+ Decorators wrap resolved instances. The decoratee is always the **last** constructor parameter. The `dependencies` array does NOT include the decoratee.
305
+
306
+ ```ts
307
+ class LoggingServiceDecorator implements MyService.Interface {
308
+ constructor(
309
+ private logger: Logger.Interface,
310
+ private decoratee: MyService.Interface // LAST param — injected automatically
311
+ ) {}
312
+
313
+ execute() {
314
+ this.logger.info("Before");
315
+ this.decoratee.execute();
316
+ }
317
+ }
318
+
319
+ export const MyServiceLoggingDecorator = MyService.createDecorator({
320
+ decorator: LoggingServiceDecorator,
321
+ dependencies: [Logger] // decoratee is NOT listed
322
+ });
323
+
324
+ // Registration:
325
+ container.registerDecorator(MyServiceLoggingDecorator);
326
+ ```
327
+
328
+ ### Composites
329
+
330
+ Composites aggregate multiple implementations behind a single `resolve()` call. Created via `Abstraction.createComposite()`:
331
+
332
+ ```ts
333
+ class AllValidatorsComposite implements Validator.Interface {
334
+ constructor(private validators: Validator.Interface[]) {}
335
+
336
+ validate(input: unknown) {
337
+ for (const v of this.validators) v.validate(input);
338
+ }
339
+ }
340
+
341
+ export const ValidatorComposite = Validator.createComposite({
342
+ implementation: AllValidatorsComposite,
343
+ dependencies: [[Validator, { multiple: true }]]
344
+ });
345
+
346
+ // Registration:
347
+ container.registerComposite(ValidatorComposite);
348
+ ```
349
+
154
350
  ## Key Rules
155
351
 
156
352
  1. Always import from the **feature path**, not the package root.
@@ -160,6 +356,8 @@ export default CorePulumi.createImplementation({
160
356
  5. Extensions with no dependencies use `dependencies: []`.
161
357
  6. `BuildParams.get<T>(name)` returns `T | null` — always type the receiving property/variable as nullable (e.g. `string | null`) and handle the `null` case.
162
358
  7. **BuildParam declarations belong inside the extension's `Extension.tsx`**, not in `webiny.config.tsx`. Expose required params as React props on the extension component so the consumer decides where values come from (see `webiny-full-stack-architect` skill for the full pattern).
359
+ 8. For multi-injection, type the constructor param as `T[]` and use `[Abstraction, { multiple: true }]` in the dependencies array.
360
+ 9. Each implementation of a multi-bound abstraction is a separate `container.register()` call — they accumulate.
163
361
 
164
362
  ## Related Skills
165
363
 
@@ -2,7 +2,7 @@
2
2
  name: webiny-admin-catalog
3
3
  context: webiny-api
4
4
  description: >
5
- admin — 18 abstractions.
5
+ admin — 16 abstractions.
6
6
  ---
7
7
 
8
8
  # admin
@@ -45,16 +45,6 @@ description: >
45
45
  **Import:** `import { createFeature } from "webiny/admin"`
46
46
  **Source:** `@webiny/feature/admin/index.ts`
47
47
 
48
- ---
49
- **Name:** `createHasPermission`
50
- **Import:** `import { createHasPermission } from "webiny/admin"`
51
- **Source:** `@webiny/app-admin/permissions/index.ts`
52
-
53
- ---
54
- **Name:** `createPermissionSchema`
55
- **Import:** `import { createPermissionSchema } from "webiny/admin"`
56
- **Source:** `@webiny/app-admin/permissions/index.ts`
57
-
58
48
  ---
59
49
  **Name:** `createProvider`
60
50
  **Import:** `import { createProvider } from "webiny/admin"`
@@ -70,11 +60,6 @@ This is mostly useful for adding React Context providers.
70
60
  This is particularly useful for wrapping the entire app with custom React Context providers.
71
61
  For more information, visit https://www.webiny.com/docs/admin-area/basics/framework.
72
62
 
73
- ---
74
- **Name:** `createUsePermissions`
75
- **Import:** `import { createUsePermissions } from "webiny/admin"`
76
- **Source:** `@webiny/app-admin/permissions/index.ts`
77
-
78
63
  ---
79
64
  **Name:** `DevToolsSection`
80
65
  **Import:** `import { DevToolsSection } from "webiny/admin"`
@@ -85,6 +70,11 @@ Renders nothing — purely a data registration side-effect.
85
70
  When the component unmounts (e.g., route change), the section
86
71
  is automatically removed from DevTools.
87
72
 
73
+ ---
74
+ **Name:** `MainGraphQLClient`
75
+ **Import:** `import { MainGraphQLClient } from "webiny/admin"`
76
+ **Source:** `@webiny/app/features/mainGraphQLClient/index.ts`
77
+
88
78
  ---
89
79
  **Name:** `NetworkErrorEventHandler`
90
80
  **Import:** `import { NetworkErrorEventHandler } from "webiny/admin"`