@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.
- package/AGENTS.md +233 -0
- 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.
|
|
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/
|
|
39
|
-
"@maroonedsoftware/
|
|
40
|
-
"@maroonedsoftware/logger": "1.1.
|
|
41
|
-
"@maroonedsoftware/
|
|
42
|
-
"@maroonedsoftware/
|
|
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",
|