@maroonedsoftware/scim 0.1.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/errors/scim.error.d.ts +42 -0
  4. package/dist/errors/scim.error.d.ts.map +1 -0
  5. package/dist/filter/filter.ast.d.ts +40 -0
  6. package/dist/filter/filter.ast.d.ts.map +1 -0
  7. package/dist/filter/filter.parser.d.ts +13 -0
  8. package/dist/filter/filter.parser.d.ts.map +1 -0
  9. package/dist/filter/filter.tokenizer.d.ts +14 -0
  10. package/dist/filter/filter.tokenizer.d.ts.map +1 -0
  11. package/dist/filter/index.d.ts +4 -0
  12. package/dist/filter/index.d.ts.map +1 -0
  13. package/dist/index.d.ts +14 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +2153 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/middleware/index.d.ts +4 -0
  18. package/dist/middleware/index.d.ts.map +1 -0
  19. package/dist/middleware/require.scim.scope.middleware.d.ts +11 -0
  20. package/dist/middleware/require.scim.scope.middleware.d.ts.map +1 -0
  21. package/dist/middleware/scim.content.type.middleware.d.ts +10 -0
  22. package/dist/middleware/scim.content.type.middleware.d.ts.map +1 -0
  23. package/dist/middleware/scim.error.middleware.d.ts +12 -0
  24. package/dist/middleware/scim.error.middleware.d.ts.map +1 -0
  25. package/dist/patch/index.d.ts +2 -0
  26. package/dist/patch/index.d.ts.map +1 -0
  27. package/dist/patch/patch.applier.d.ts +11 -0
  28. package/dist/patch/patch.applier.d.ts.map +1 -0
  29. package/dist/repositories/index.d.ts +4 -0
  30. package/dist/repositories/index.d.ts.map +1 -0
  31. package/dist/repositories/repository.types.d.ts +35 -0
  32. package/dist/repositories/repository.types.d.ts.map +1 -0
  33. package/dist/repositories/scim.group.repository.d.ts +23 -0
  34. package/dist/repositories/scim.group.repository.d.ts.map +1 -0
  35. package/dist/repositories/scim.user.repository.d.ts +34 -0
  36. package/dist/repositories/scim.user.repository.d.ts.map +1 -0
  37. package/dist/router/index.d.ts +2 -0
  38. package/dist/router/index.d.ts.map +1 -0
  39. package/dist/router/scim.router.d.ts +46 -0
  40. package/dist/router/scim.router.d.ts.map +1 -0
  41. package/dist/schemas/enterprise.user.schema.d.ts +8 -0
  42. package/dist/schemas/enterprise.user.schema.d.ts.map +1 -0
  43. package/dist/schemas/group.schema.d.ts +8 -0
  44. package/dist/schemas/group.schema.d.ts.map +1 -0
  45. package/dist/schemas/index.d.ts +9 -0
  46. package/dist/schemas/index.d.ts.map +1 -0
  47. package/dist/schemas/resource.type.schema.d.ts +21 -0
  48. package/dist/schemas/resource.type.schema.d.ts.map +1 -0
  49. package/dist/schemas/schema.types.d.ts +33 -0
  50. package/dist/schemas/schema.types.d.ts.map +1 -0
  51. package/dist/schemas/service.provider.config.schema.d.ts +53 -0
  52. package/dist/schemas/service.provider.config.schema.d.ts.map +1 -0
  53. package/dist/schemas/user.schema.d.ts +9 -0
  54. package/dist/schemas/user.schema.d.ts.map +1 -0
  55. package/dist/services/index.d.ts +4 -0
  56. package/dist/services/index.d.ts.map +1 -0
  57. package/dist/services/scim.group.service.d.ts +59 -0
  58. package/dist/services/scim.group.service.d.ts.map +1 -0
  59. package/dist/services/scim.service.provider.service.d.ts +33 -0
  60. package/dist/services/scim.service.provider.service.d.ts.map +1 -0
  61. package/dist/services/scim.user.service.d.ts +62 -0
  62. package/dist/services/scim.user.service.d.ts.map +1 -0
  63. package/dist/types/list.response.d.ts +15 -0
  64. package/dist/types/list.response.d.ts.map +1 -0
  65. package/dist/types/patch.op.d.ts +25 -0
  66. package/dist/types/patch.op.d.ts.map +1 -0
  67. package/dist/types/scim.group.d.ts +14 -0
  68. package/dist/types/scim.group.d.ts.map +1 -0
  69. package/dist/types/scim.meta.d.ts +30 -0
  70. package/dist/types/scim.meta.d.ts.map +1 -0
  71. package/dist/types/scim.user.d.ts +75 -0
  72. package/dist/types/scim.user.d.ts.map +1 -0
  73. package/package.json +64 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Marooned Software
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # @maroonedsoftware/scim
2
+
3
+ SCIM 2.0 (RFC 7643/7644) server toolkit for ServerKit.
4
+
5
+ This package provides the protocol layer — schemas, filter parser, PATCH applier, error envelope, and a Koa router — for building a SCIM server. Resource storage is left to the consumer via abstract repositories, matching the pattern used by `@maroonedsoftware/authentication` and `@maroonedsoftware/permissions`.
6
+
7
+ ## What's included
8
+
9
+ - **Resource schemas** — `User`, `Group`, and the `EnterpriseUser` extension (RFC 7643).
10
+ - **Filter parser** — full SCIM filter grammar (RFC 7644 §3.4.2.2) returning a typed AST.
11
+ - **PATCH applier** — `add` / `replace` / `remove` ops with the path mini-language (RFC 7644 §3.5.2).
12
+ - **Error envelope** — `scimError(status, scimType?)` builder producing the SCIM error JSON.
13
+ - **Abstract repositories** — `ScimUserRepository`, `ScimGroupRepository`. The consumer implements these against their datastore.
14
+ - **Services** — `ScimUserService`, `ScimGroupService`, `ScimServiceProviderService`.
15
+ - **Koa middleware** — `scimErrorMiddleware()`, `scimContentTypeMiddleware()`, `requireScimScope(scope)`.
16
+ - **Router factory** — `createScimRouter(options)` mounting the standard SCIM endpoints.
17
+
18
+ ## Quick start
19
+
20
+ ```ts
21
+ import Koa from 'koa';
22
+ import { ServerKitContext, serverKitContextMiddleware, authenticationMiddleware } from '@maroonedsoftware/koa';
23
+ import { createScimRouter, ScimUserRepository, ScimGroupRepository, scimErrorMiddleware } from '@maroonedsoftware/scim';
24
+
25
+ class MyScimUserRepository extends ScimUserRepository {
26
+ // implement against your datastore
27
+ }
28
+
29
+ class MyScimGroupRepository extends ScimGroupRepository {
30
+ // implement against your datastore
31
+ }
32
+
33
+ const app = new Koa();
34
+ app.use(serverKitContextMiddleware(container));
35
+ app.use(authenticationMiddleware());
36
+
37
+ const scimRouter = createScimRouter({
38
+ userRepository: new MyScimUserRepository(),
39
+ groupRepository: new MyScimGroupRepository(),
40
+ basePath: '/scim/v2',
41
+ serviceProviderConfig: {
42
+ documentationUri: 'https://example.com/scim/docs',
43
+ patch: { supported: true },
44
+ bulk: { supported: false, maxOperations: 0, maxPayloadSize: 0 },
45
+ filter: { supported: true, maxResults: 200 },
46
+ changePassword: { supported: false },
47
+ sort: { supported: true },
48
+ etag: { supported: false },
49
+ },
50
+ });
51
+
52
+ app.use(scimErrorMiddleware());
53
+ app.use(scimRouter.routes());
54
+ ```
55
+
56
+ ## Authentication
57
+
58
+ This package does not validate bearer tokens itself. It reads `ctx.authenticationSession` (populated by `authenticationMiddleware()` from `@maroonedsoftware/koa`) and provides a `requireScimScope(scope)` guard that checks for SCIM scopes on `session.claims.scimScopes`. You register an `AuthenticationSchemeHandler` in your app that turns IdP-issued bearer tokens into a session.
59
+
60
+ ## Compliance testing
61
+
62
+ The router has been designed against RFC 7643/7644. To validate against a real IdP, point Okta or [`scim2-compliance-test-utils`](https://github.com/suvera/scim2-compliance-test-utils) at a sample app that mounts `createScimRouter`.
@@ -0,0 +1,42 @@
1
+ import { HttpError, HttpStatusCodes, type HttpStatusMessage } from '@maroonedsoftware/errors';
2
+ /** Schema URI for the SCIM Error message. */
3
+ export declare const ScimErrorSchema = "urn:ietf:params:scim:api:messages:2.0:Error";
4
+ /**
5
+ * `scimType` values defined by RFC 7644 §3.12 plus the few extensions clients
6
+ * commonly emit. Keep this open as `string` to allow vendor-specific values.
7
+ */
8
+ export type ScimErrorType = 'invalidFilter' | 'tooMany' | 'uniqueness' | 'mutability' | 'invalidSyntax' | 'invalidPath' | 'noTarget' | 'invalidValue' | 'invalidVers' | 'sensitive' | 'insufficientScope' | (string & {});
9
+ /**
10
+ * SCIM error envelope (RFC 7644 §3.12). The HTTP status comes from
11
+ * the underlying {@link HttpError}; the JSON body matches:
12
+ *
13
+ * ```json
14
+ * { "schemas": ["urn:ietf:params:scim:api:messages:2.0:Error"], "status": "404", "scimType": "invalidPath", "detail": "..." }
15
+ * ```
16
+ */
17
+ export declare class ScimError extends HttpError {
18
+ /** SCIM-specific error subtype, see RFC 7644 §3.12. */
19
+ readonly scimType?: ScimErrorType;
20
+ constructor(statusCode: HttpStatusCodes, scimType?: ScimErrorType, message?: HttpStatusMessage<HttpStatusCodes>);
21
+ /** Build the SCIM error JSON body for this error. */
22
+ toScimBody(): {
23
+ schemas: [typeof ScimErrorSchema];
24
+ status: string;
25
+ scimType?: ScimErrorType;
26
+ detail?: string;
27
+ };
28
+ }
29
+ /** Type guard for {@link ScimError}. */
30
+ export declare const IsScimError: (error: unknown) => error is ScimError;
31
+ /**
32
+ * Factory for building a SCIM-shaped HTTP error. The returned object behaves like
33
+ * an {@link HttpError}, so chained `.withDetails()` / `.withCause()` /
34
+ * `.withInternalDetails()` work as usual.
35
+ *
36
+ * @example
37
+ * ```ts
38
+ * throw scimError(400, 'invalidFilter', 'Bad Request');
39
+ * ```
40
+ */
41
+ export declare const scimError: <Status extends HttpStatusCodes>(statusCode: Status, scimType?: ScimErrorType, message?: HttpStatusMessage<Status>) => ScimError;
42
+ //# sourceMappingURL=scim.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scim.error.d.ts","sourceRoot":"","sources":["../../src/errors/scim.error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAE9F,6CAA6C;AAC7C,eAAO,MAAM,eAAe,gDAAgD,CAAC;AAE7E;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,SAAS,GACT,YAAY,GACZ,YAAY,GACZ,eAAe,GACf,aAAa,GACb,UAAU,GACV,cAAc,GACd,aAAa,GACb,WAAW,GACX,mBAAmB,GACnB,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB;;;;;;;GAOG;AACH,qBAAa,SAAU,SAAQ,SAAS;IACtC,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;gBAEtB,UAAU,EAAE,eAAe,EAAE,QAAQ,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,EAAE,iBAAiB,CAAC,eAAe,CAAC;IAM/G,qDAAqD;IACrD,UAAU,IAAI;QAAE,OAAO,EAAE,CAAC,OAAO,eAAe,CAAC,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE;CAQ/G;AAED,wCAAwC;AACxC,eAAO,MAAM,WAAW,GAAI,OAAO,OAAO,KAAG,KAAK,IAAI,SAAuC,CAAC;AAE9F;;;;;;;;;GASG;AACH,eAAO,MAAM,SAAS,GAAI,MAAM,SAAS,eAAe,EACtD,YAAY,MAAM,EAClB,WAAW,aAAa,EACxB,UAAU,iBAAiB,CAAC,MAAM,CAAC,KAClC,SAAyD,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * SCIM filter comparison operators (RFC 7644 §3.4.2.2).
3
+ */
4
+ export type ScimComparisonOperator = 'eq' | 'ne' | 'co' | 'sw' | 'ew' | 'gt' | 'ge' | 'lt' | 'le' | 'pr';
5
+ /**
6
+ * SCIM filter logical operators.
7
+ */
8
+ export type ScimLogicalOperator = 'and' | 'or';
9
+ /**
10
+ * Comparison node: `<attrPath> <op> <value>` or `<attrPath> pr` (presence).
11
+ */
12
+ export interface ScimFilterComparison {
13
+ kind: 'comparison';
14
+ /** Attribute path, e.g. `userName`, `name.familyName`, `urn:.../User:userName`. */
15
+ attribute: string;
16
+ operator: ScimComparisonOperator;
17
+ /** Literal value; absent for `pr` (presence). */
18
+ value?: string | number | boolean | null;
19
+ }
20
+ /** Boolean conjunction / disjunction. */
21
+ export interface ScimFilterLogical {
22
+ kind: 'logical';
23
+ operator: ScimLogicalOperator;
24
+ left: ScimFilterNode;
25
+ right: ScimFilterNode;
26
+ }
27
+ /** Negation: `not(<filter>)`. */
28
+ export interface ScimFilterNot {
29
+ kind: 'not';
30
+ filter: ScimFilterNode;
31
+ }
32
+ /** Value-path filter: `emails[type eq "work" and primary eq true]`. */
33
+ export interface ScimFilterValuePath {
34
+ kind: 'valuePath';
35
+ attribute: string;
36
+ filter: ScimFilterNode;
37
+ }
38
+ /** Discriminated union of every parsed SCIM filter node. */
39
+ export type ScimFilterNode = ScimFilterComparison | ScimFilterLogical | ScimFilterNot | ScimFilterValuePath;
40
+ //# sourceMappingURL=filter.ast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.ast.d.ts","sourceRoot":"","sources":["../../src/filter/filter.ast.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEzG;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAAG,KAAK,GAAG,IAAI,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,mFAAmF;IACnF,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,sBAAsB,CAAC;IACjC,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;CAC1C;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,cAAc,CAAC;CACvB;AAED,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,KAAK,CAAC;IACZ,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,uEAAuE;AACvE,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,WAAW,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;CACxB;AAED,4DAA4D;AAC5D,MAAM,MAAM,cAAc,GAAG,oBAAoB,GAAG,iBAAiB,GAAG,aAAa,GAAG,mBAAmB,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { ScimFilterNode } from './filter.ast.js';
2
+ /**
3
+ * Parse a SCIM filter expression (RFC 7644 §3.4.2.2) into a typed AST.
4
+ * Throws a `400 invalidFilter` SCIM error on malformed input.
5
+ *
6
+ * Operator precedence (highest first):
7
+ * 1. parentheses, value-path brackets, `not(...)`
8
+ * 2. comparison
9
+ * 3. `and`
10
+ * 4. `or`
11
+ */
12
+ export declare const parseScimFilter: (input: string) => ScimFilterNode;
13
+ //# sourceMappingURL=filter.parser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.parser.d.ts","sourceRoot":"","sources":["../../src/filter/filter.parser.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAA0B,MAAM,iBAAiB,CAAC;AAG9E;;;;;;;;;GASG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,MAAM,KAAG,cAU/C,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type ScimTokenKind = 'identifier' | 'string' | 'number' | 'true' | 'false' | 'null' | 'and' | 'or' | 'not' | 'lparen' | 'rparen' | 'lbracket' | 'rbracket' | 'op';
2
+ export interface ScimToken {
3
+ kind: ScimTokenKind;
4
+ /** For `identifier`/`string`/`op`: the textual value. For `number`: numeric. */
5
+ value: string | number | boolean | null;
6
+ /** Source position where the token starts. */
7
+ start: number;
8
+ }
9
+ /**
10
+ * Tokenize a SCIM filter expression into a stream of {@link ScimToken}s.
11
+ * Throws a `400 invalidFilter` SCIM error on malformed input.
12
+ */
13
+ export declare const tokenizeScimFilter: (input: string) => ScimToken[];
14
+ //# sourceMappingURL=filter.tokenizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filter.tokenizer.d.ts","sourceRoot":"","sources":["../../src/filter/filter.tokenizer.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GACrB,YAAY,GACZ,QAAQ,GACR,QAAQ,GACR,MAAM,GACN,OAAO,GACP,MAAM,GACN,KAAK,GACL,IAAI,GACJ,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,UAAU,GACV,IAAI,CAAC;AAET,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,aAAa,CAAC;IACpB,gFAAgF;IAChF,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;IACxC,8CAA8C;IAC9C,KAAK,EAAE,MAAM,CAAC;CACf;AAID;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,KAAG,SAAS,EA0I3D,CAAC"}
@@ -0,0 +1,4 @@
1
+ export * from './filter.ast.js';
2
+ export * from './filter.parser.js';
3
+ export type { ScimToken, ScimTokenKind } from './filter.tokenizer.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/filter/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,YAAY,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,14 @@
1
+ export * from './types/scim.meta.js';
2
+ export * from './types/scim.user.js';
3
+ export * from './types/scim.group.js';
4
+ export * from './types/list.response.js';
5
+ export * from './types/patch.op.js';
6
+ export * from './schemas/index.js';
7
+ export * from './filter/index.js';
8
+ export * from './patch/index.js';
9
+ export * from './errors/scim.error.js';
10
+ export * from './repositories/index.js';
11
+ export * from './services/index.js';
12
+ export * from './middleware/index.js';
13
+ export * from './router/index.js';
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,sBAAsB,CAAC;AACrC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,oBAAoB,CAAC;AAGnC,cAAc,mBAAmB,CAAC;AAGlC,cAAc,kBAAkB,CAAC;AAGjC,cAAc,wBAAwB,CAAC;AAGvC,cAAc,yBAAyB,CAAC;AAGxC,cAAc,qBAAqB,CAAC;AAGpC,cAAc,uBAAuB,CAAC;AAGtC,cAAc,mBAAmB,CAAC"}