@maroonedsoftware/scim 0.2.9 → 0.2.13

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 (2) hide show
  1. package/AGENTS.md +233 -0
  2. package/package.json +7 -6
package/AGENTS.md ADDED
@@ -0,0 +1,233 @@
1
+ # AGENTS.md — @maroonedsoftware/scim
2
+
3
+ Machine-oriented guide for AI agents. Human prose and long-form examples live in [README.md](./README.md).
4
+ Repo-wide conventions live in the [root AGENTS.md](../../AGENTS.md).
5
+
6
+ ## Purpose
7
+
8
+ A SCIM 2.0 (RFC 7643 / 7644) server toolkit: the core schemas (User, Group, EnterpriseUser), the
9
+ discovery endpoints, a full filter grammar with a typed AST, a PATCH-op applier with value-path
10
+ support, the SCIM error envelope, and a Koa router that mounts all of it over abstract repositories
11
+ you implement.
12
+
13
+ Reach for this when an enterprise customer needs directory-driven user provisioning from Okta, Entra
14
+ ID, or similar. Do **not** expect a datastore or an identity model — you implement
15
+ `ScimUserRepository` and `ScimGroupRepository` over your own tables, and the filter arrives as a
16
+ parsed AST for you to translate.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add @maroonedsoftware/scim koa @koa/router
22
+ ```
23
+
24
+ Peers: `koa`, `@koa/router`. Runtime dependencies: `@maroonedsoftware/authentication`,
25
+ `@maroonedsoftware/errors`, `@maroonedsoftware/koa`, `@maroonedsoftware/logger`,
26
+ `@maroonedsoftware/utilities`, `injectkit`, `luxon`.
27
+
28
+ ## Position in the graph
29
+
30
+ - **Depends on:** `authentication`, `errors`, `koa`, `logger`, `utilities`.
31
+ - **Depended on by:** nothing internal.
32
+ - **Subpath exports:** none. The package has no `exports` map at all.
33
+
34
+ Unlike the chat packages, this one **does** depend on `koa` — it ships a router, and SCIM's wire
35
+ contract is inseparable from HTTP.
36
+
37
+ ## API surface
38
+
39
+ ### Resource types
40
+
41
+ | Export | Kind | Notes |
42
+ | ------------------------------------------------------------------------ | -------------------- | --------------------------------------------------------------------------------------- |
43
+ | `ScimCommonAttributes` | interface | `id`, `externalId`, `schemas`, `meta` — the base every resource extends. |
44
+ | `ScimMeta` | interface | `resourceType`, `created`, `lastModified`, `location`, `version`. |
45
+ | `ScimUser` | interface | `extends ScimCommonAttributes`. Plus `ScimUserName`, `ScimAddress`, `ScimUserGroupRef`. |
46
+ | `ScimGroup` | interface | `extends ScimCommonAttributes`. Plus `ScimGroupMember`. |
47
+ | `ScimEnterpriseUser` | interface | The enterprise extension. |
48
+ | `ScimMultiValuedAttribute<TValue>` | interface | Emails, phone numbers, and the rest. |
49
+ | `ScimListResponse<TResource>` / `ListResponseSchema` | interface / constant | The list envelope. |
50
+ | `ScimPatchOp` / `ScimPatchRequest` / `ScimPatchOpKind` / `PatchOpSchema` | — | The PATCH request shape. |
51
+
52
+ ### Schemas and discovery
53
+
54
+ | Export | Kind | Notes |
55
+ | ------------------------------------------------------------------------------------------------------------------ | ---------- | ---------------------------------------------------------- |
56
+ | `userSchema`, `groupSchema`, `enterpriseUserSchema` | constants | `ScimSchema` definitions served by `/Schemas`. |
57
+ | `coreSchemas` | constant | All three together. |
58
+ | `userResourceType`, `groupResourceType` | constants | `ScimResourceType` definitions served by `/ResourceTypes`. |
59
+ | `UserSchemaId`, `GroupSchemaId`, `EnterpriseUserSchemaId`, `ResourceTypeSchemaId`, `ServiceProviderConfigSchemaId` | constants | URN identifiers. |
60
+ | `ScimSchema`, `ScimAttributeDefinition`, `ScimResourceType` | interfaces | Schema metadata shapes. |
61
+ | `buildServiceProviderConfig` | function | Builds the `/ServiceProviderConfig` document. |
62
+ | `ScimServiceProviderConfig` / `…Options` / `ScimAuthenticationScheme` | interfaces | — |
63
+
64
+ ### Filter
65
+
66
+ | Export | Kind | Shape | Notes |
67
+ | ----------------------------- | -------- | ----------------------------------------------------------------------------------- | ----------------------------------- |
68
+ | `tokenizeScimFilter` | function | `(input: string) => ScimToken[]` | — |
69
+ | `parseScimFilter` | function | `(input: string) => ScimFilterNode` | The full RFC 7644 §3.4.2.2 grammar. |
70
+ | `ScimFilterNode` | type | `ScimFilterComparison \| ScimFilterLogical \| ScimFilterNot \| ScimFilterValuePath` | The AST. |
71
+ | `ScimComparisonOperator` | type | `eq`, `ne`, `co`, `sw`, `ew`, `gt`, `ge`, `lt`, `le`, `pr` | — |
72
+ | `ScimLogicalOperator` | type | `and`, `or` | — |
73
+ | `ScimToken` / `ScimTokenKind` | type | Tokenizer output | — |
74
+
75
+ ### PATCH
76
+
77
+ | Export | Kind | Shape | Notes |
78
+ | ---------------- | -------- | ----------------------------- | ----------------------------------------------------------- |
79
+ | `applyScimPatch` | function | `(resource, ops) => resource` | `add` / `replace` / `remove`, including value-path filters. |
80
+
81
+ ### Errors
82
+
83
+ | Export | Kind | Shape | Notes |
84
+ | ----------------- | ---------- | ----------------------------------------------------------- | -------------------------------------------- |
85
+ | `ScimError` | class | `extends HttpError` | So `errorMiddleware` already understands it. |
86
+ | `scimError` | function | `(status, scimType?, detail?) => ScimError` | The factory to use. |
87
+ | `IsScimError` | type guard | — | — |
88
+ | `ScimErrorType` | type | `'invalidFilter'`, `'insufficientScope'`, `'mutability'`, … | RFC 7644 §3.12 `scimType` values. |
89
+ | `ScimErrorSchema` | constant | The error envelope URN | — |
90
+
91
+ ### Repositories and services
92
+
93
+ | Export | Kind | Notes |
94
+ | --------------------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
95
+ | `ScimUserRepository` | abstract class | Implement over your datastore. |
96
+ | `ScimGroupRepository` | abstract class | Implement over your datastore. |
97
+ | `ScimListQuery` | interface | `{ filter?: ScimFilterNode; startIndex; count; sortBy?; sortOrder?; attributes?; excludedAttributes? }` — **the parsed AST**, never the raw string. |
98
+ | `ScimListResult<TResource>` | interface | `{ resources; totalResults }` — `totalResults` is the **filter-matching total**, not the page size. |
99
+ | `ScimSortOrder` | type | `'ascending' \| 'descending'` |
100
+ | `ScimUserService` / `ScimGroupService` / `ScimServiceProviderService` | class | Sit between the router and the repositories. |
101
+
102
+ ### Middleware and router
103
+
104
+ | Export | Kind | Shape | Notes |
105
+ | --------------------------- | --------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
106
+ | `scimErrorMiddleware` | function | `() => ServerKitMiddleware` | **Replaces** `errorMiddleware()` on the SCIM mountpoint. |
107
+ | `scimContentTypeMiddleware` | function | `() => ServerKitMiddleware` | Enforces `application/scim+json`. |
108
+ | `SCIM_MEDIA_TYPE` | constant | `'application/scim+json'` | — |
109
+ | `requireScimScope` | function | `(scope: string) => ServerKitRouterMiddleware` | Reads `ctx.authenticationSession.claims.scimScopes`. `*` grants everything. |
110
+ | `createScimRouter` | function | `(options: CreateScimRouterOptions) => Router<unknown, ServerKitContext>` | Mounts every endpoint below. |
111
+ | `CreateScimRouterOptions` | interface | `{ userService, groupService, serviceProviderService, routeGuards?, maxResults? }` | `maxResults` defaults to the service-provider config's `filter.maxResults`, then 200. |
112
+
113
+ Endpoints mounted: `GET|POST /Users`, `GET|PUT|PATCH|DELETE /Users/:id`, `POST /Users/.search`, the
114
+ same six plus `.search` for `/Groups`, and `GET /Schemas`, `/Schemas/:id`, `/ResourceTypes`,
115
+ `/ResourceTypes/:id`, `/ServiceProviderConfig`.
116
+
117
+ ## Canonical usage
118
+
119
+ ```typescript
120
+ import {
121
+ createScimRouter,
122
+ requireScimScope,
123
+ scimErrorMiddleware,
124
+ ScimUserService,
125
+ ScimGroupService,
126
+ ScimServiceProviderService,
127
+ ScimUserRepository,
128
+ type ScimListQuery,
129
+ type ScimListResult,
130
+ type ScimUser,
131
+ } from '@maroonedsoftware/scim';
132
+
133
+ class PostgresScimUserRepository extends ScimUserRepository {
134
+ async list(query: ScimListQuery): Promise<ScimListResult<ScimUser>> {
135
+ // query.filter is a parsed AST — translate it to SQL
136
+ const where = compileFilter(query.filter);
137
+ const [rows, total] = await Promise.all([this.db.page(where, query.startIndex, query.count), this.db.count(where)]);
138
+ return { resources: rows.map(toScimUser), totalResults: total };
139
+ }
140
+ // …the rest of the abstract methods
141
+ }
142
+
143
+ const router = createScimRouter({
144
+ userService: new ScimUserService(userRepository, logger),
145
+ groupService: new ScimGroupService(groupRepository, logger),
146
+ serviceProviderService: new ScimServiceProviderService(config),
147
+ routeGuards: [requireScimScope('scim:write')],
148
+ });
149
+
150
+ // SCIM mountpoint — note scimErrorMiddleware, not errorMiddleware
151
+ app.use(scimErrorMiddleware());
152
+ app.use(serverKitContextMiddleware(container));
153
+ app.use(authenticationMiddleware());
154
+ app.use(router.routes()).use(router.allowedMethods());
155
+ ```
156
+
157
+ ## Rules for generated code
158
+
159
+ - Use `scimErrorMiddleware()` on the SCIM mountpoint instead of `errorMiddleware()`. SCIM has its
160
+ own error envelope, and the default middleware renders the ServerKit one.
161
+ - The mountpoint still needs `serverKitContextMiddleware` and `authenticationMiddleware`, in that
162
+ order, before the router.
163
+ - Translate `query.filter` — the AST — into your datastore's query language. You never see the raw
164
+ filter string, which is the point.
165
+ - Return the **filter-matching total** in `ScimListResult.totalResults`, not the page length.
166
+ Provisioning clients paginate on it, and a wrong value makes Okta or Entra loop or truncate.
167
+ - Populate `claims.scimScopes` (a string array) when minting the bearer session, or every request
168
+ gets a 403.
169
+ - Honour `startIndex` as **1-based**, per RFC 7644 §3.4.2.4. Off-by-one here silently skips or
170
+ repeats the first record.
171
+ - Apply PATCH with `applyScimPatch` rather than hand-rolling op semantics — value-path filters
172
+ (`emails[type eq "work"].value`) are easy to get subtly wrong.
173
+ - Throw `scimError(status, scimType, detail)` with the right RFC 7644 §3.12 `scimType`. Clients
174
+ branch on it.
175
+ - Serve `/Schemas`, `/ResourceTypes`, and `/ServiceProviderConfig`. Provisioning clients call them
176
+ during setup and fail the connection without them.
177
+
178
+ ## Gotchas
179
+
180
+ - **`requireScimScope` grants everything on a `*` scope.** A session minted with
181
+ `scimScopes: ['*']` bypasses every per-route check. Convenient in development, dangerous if it
182
+ reaches a production token.
183
+ - **Missing or malformed `scimScopes` is a 403, not a 500.** A non-array value (a string, say) fails
184
+ the `Array.isArray` check and denies, so a config mistake looks like a permissions problem.
185
+ - **`routeGuards` applies to every route uniformly**, including the discovery endpoints. There is no
186
+ per-route scope distinction built in — `requireScimScope('users:read')` in `routeGuards` also
187
+ gates `/Groups` and `/ServiceProviderConfig`. For finer control, mount more than one router.
188
+ - **`.search` endpoints exist because filters outgrow a query string.** `POST /Users/.search` takes
189
+ the same parameters in the body. Provisioning clients use it for large filters; omitting it means
190
+ those requests 404.
191
+ - **`maxResults` has a three-level fallback**: the option, then the service-provider config's
192
+ `filter.maxResults`, then 200. A client asking for more is capped silently.
193
+ - **`ScimError extends HttpError`**, so it flows through the standard ServerKit error path as well —
194
+ but only `scimErrorMiddleware` renders the SCIM envelope.
195
+ - **The body parser accepts both `application/scim+json` and `application/json`.** Real clients send
196
+ the former; being lenient avoids a class of integration failures, but it means
197
+ `scimContentTypeMiddleware` is what actually enforces the media type if you want strictness.
198
+ - **`schemas` on a resource is a required array of URNs**, not decoration. Omitting the enterprise
199
+ URN on a user with enterprise attributes makes clients ignore them.
200
+
201
+ ## Working inside this package
202
+
203
+ ```
204
+ src/
205
+ index.ts Root barrel (re-exports every folder's index)
206
+ types/ scim.meta, scim.user, scim.group, list.response, patch.op
207
+ schemas/ user, group, enterprise.user, resource.type,
208
+ service.provider.config, schema.types
209
+ filter/ filter.tokenizer, filter.parser, filter.ast
210
+ patch/ patch.applier
211
+ errors/ scim.error
212
+ repositories/ scim.user.repository, scim.group.repository, repository.types
213
+ services/ scim.user.service, scim.group.service, scim.service.provider.service
214
+ middleware/ require.scim.scope, scim.content.type, scim.error
215
+ router/ scim.router
216
+ ```
217
+
218
+ Every folder has its own `index.ts`; the root barrel re-exports those. Tests are in `tests/`,
219
+ mirroring `src/`.
220
+
221
+ Invariants a change must not break:
222
+
223
+ - Repositories receive the **parsed AST**, never the raw filter string. That is what keeps backend
224
+ translation the only concern a consumer has.
225
+ - `totalResults` is the filter-matching total. Provisioning clients' pagination depends on it.
226
+ - `startIndex` is 1-based throughout.
227
+ - The SCIM error envelope (`ScimErrorSchema`, `scimType`, `detail`) is a wire contract with Okta,
228
+ Entra ID, and every other provisioning client. It is not ServerKit's error shape.
229
+ - The filter grammar and the PATCH value-path semantics follow RFC 7644. Divergence shows up as an
230
+ integration failure at a customer, not as a test failure here.
231
+ - `koa` is a legitimate dependency here (this package ships a router), unlike in the chat packages.
232
+
233
+ User-visible changes need a changeset in `.changeset/`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maroonedsoftware/scim",
3
- "version": "0.2.9",
3
+ "version": "0.2.13",
4
4
  "description": "SCIM 2.0 (RFC 7643/7644) server toolkit for ServerKit — schemas, filter parser, PATCH applier, error envelope, and a Koa router with abstract repositories.",
5
5
  "author": {
6
6
  "name": "Marooned Software",
@@ -30,16 +30,17 @@
30
30
  "types": "./dist/index.d.ts",
31
31
  "license": "MIT",
32
32
  "files": [
33
+ "AGENTS.md",
33
34
  "dist/**"
34
35
  ],
35
36
  "dependencies": {
36
37
  "injectkit": "^1.6.0",
37
38
  "luxon": "^3.7.2",
38
- "@maroonedsoftware/authentication": "4.30.1",
39
- "@maroonedsoftware/koa": "3.0.3",
40
- "@maroonedsoftware/logger": "1.1.3",
41
- "@maroonedsoftware/utilities": "1.11.0",
42
- "@maroonedsoftware/errors": "1.8.0"
39
+ "@maroonedsoftware/errors": "1.8.4",
40
+ "@maroonedsoftware/authentication": "4.30.5",
41
+ "@maroonedsoftware/logger": "1.1.7",
42
+ "@maroonedsoftware/koa": "3.0.7",
43
+ "@maroonedsoftware/utilities": "1.11.4"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "@koa/router": "^15.2.0",