@better-auth/scim 1.5.0-beta.9 → 1.5.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.
- package/README.md +17 -0
- package/dist/client.d.mts +2 -1
- package/dist/client.mjs +2 -1
- package/dist/client.mjs.map +1 -0
- package/dist/index.d.mts +257 -36
- package/dist/index.mjs +222 -50
- package/dist/index.mjs.map +1 -0
- package/package.json +26 -20
- package/.turbo/turbo-build.log +0 -15
- package/src/client.ts +0 -9
- package/src/index.ts +0 -76
- package/src/mappings.ts +0 -38
- package/src/middlewares.ts +0 -89
- package/src/patch-operations.ts +0 -148
- package/src/routes.ts +0 -984
- package/src/scim-error.ts +0 -99
- package/src/scim-filters.ts +0 -69
- package/src/scim-metadata.ts +0 -128
- package/src/scim-resources.ts +0 -35
- package/src/scim-tokens.ts +0 -71
- package/src/scim.test.ts +0 -2525
- package/src/types.ts +0 -70
- package/src/user-schemas.ts +0 -213
- package/src/utils.ts +0 -5
- package/tsconfig.json +0 -11
- package/tsdown.config.ts +0 -7
- package/vitest.config.ts +0 -3
package/package.json
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/scim",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"version": "1.5.0",
|
|
4
|
+
"description": "SCIM plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "dist/index.mjs",
|
|
7
|
-
"types": "dist/index.d.mts",
|
|
8
6
|
"license": "MIT",
|
|
7
|
+
"homepage": "https://www.better-auth.com/docs/plugins/scim",
|
|
9
8
|
"repository": {
|
|
10
9
|
"type": "git",
|
|
11
10
|
"url": "git+https://github.com/better-auth/better-auth.git",
|
|
12
11
|
"directory": "packages/scim"
|
|
13
12
|
},
|
|
13
|
+
"author": "Jonathan Samines",
|
|
14
14
|
"keywords": [
|
|
15
15
|
"auth",
|
|
16
|
-
"scim"
|
|
16
|
+
"scim",
|
|
17
|
+
"typescript",
|
|
18
|
+
"better-auth"
|
|
17
19
|
],
|
|
18
20
|
"publishConfig": {
|
|
19
21
|
"access": "public"
|
|
20
22
|
},
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"main": "./dist/index.mjs",
|
|
27
|
+
"module": "./dist/index.mjs",
|
|
28
|
+
"types": "./dist/index.d.mts",
|
|
23
29
|
"exports": {
|
|
24
30
|
".": {
|
|
25
31
|
"dev-source": "./src/index.ts",
|
|
@@ -43,26 +49,26 @@
|
|
|
43
49
|
}
|
|
44
50
|
},
|
|
45
51
|
"dependencies": {
|
|
46
|
-
"@better-auth/utils": "0.3.
|
|
47
|
-
"better-call": "1.2
|
|
48
|
-
"zod": "^4.3.
|
|
52
|
+
"@better-auth/utils": "0.3.1",
|
|
53
|
+
"better-call": "1.3.2",
|
|
54
|
+
"zod": "^4.3.6"
|
|
49
55
|
},
|
|
50
56
|
"devDependencies": {
|
|
51
|
-
"tsdown": "^0.
|
|
52
|
-
"@better-auth/core": "1.5.0
|
|
53
|
-
"@better-auth/sso": "1.5.0
|
|
57
|
+
"tsdown": "^0.20.3",
|
|
58
|
+
"@better-auth/core": "1.5.0",
|
|
59
|
+
"@better-auth/sso": "1.5.0"
|
|
54
60
|
},
|
|
55
61
|
"peerDependencies": {
|
|
56
|
-
"@better-auth/core": "1.5.0
|
|
57
|
-
"better-auth": "1.5.0
|
|
62
|
+
"@better-auth/core": "1.5.0",
|
|
63
|
+
"better-auth": "1.5.0"
|
|
58
64
|
},
|
|
59
65
|
"scripts": {
|
|
60
|
-
"test": "vitest",
|
|
61
|
-
"coverage": "vitest run --coverage --coverage.provider=istanbul",
|
|
62
|
-
"lint:package": "publint run --strict",
|
|
63
|
-
"lint:types": "attw --profile esm-only --pack .",
|
|
64
66
|
"build": "tsdown",
|
|
65
67
|
"dev": "tsdown --watch",
|
|
66
|
-
"
|
|
68
|
+
"lint:package": "publint run --strict",
|
|
69
|
+
"lint:types": "attw --profile esm-only --pack .",
|
|
70
|
+
"typecheck": "tsc --project tsconfig.json",
|
|
71
|
+
"test": "vitest",
|
|
72
|
+
"coverage": "vitest run --coverage --coverage.provider=istanbul"
|
|
67
73
|
}
|
|
68
74
|
}
|
package/.turbo/turbo-build.log
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @better-auth/scim@1.5.0-beta.9 build /home/runner/work/better-auth/better-auth/packages/scim
|
|
3
|
-
> tsdown
|
|
4
|
-
|
|
5
|
-
[34mℹ[39m tsdown [2mv0.19.0[22m powered by rolldown [2mv1.0.0-beta.59[22m
|
|
6
|
-
[34mℹ[39m config file: [4m/home/runner/work/better-auth/better-auth/packages/scim/tsdown.config.ts[24m
|
|
7
|
-
[34mℹ[39m entry: [34msrc/index.ts, src/client.ts[39m
|
|
8
|
-
[34mℹ[39m tsconfig: [34mtsconfig.json[39m
|
|
9
|
-
[34mℹ[39m Build start
|
|
10
|
-
[34mℹ[39m [2mdist/[22m[1mindex.mjs[22m [2m 39.48 kB[22m [2m│ gzip: 8.06 kB[22m
|
|
11
|
-
[34mℹ[39m [2mdist/[22m[1mclient.mjs[22m [2m 0.15 kB[22m [2m│ gzip: 0.14 kB[22m
|
|
12
|
-
[34mℹ[39m [2mdist/[22m[32m[1mindex.d.mts[22m[39m [2m108.72 kB[22m [2m│ gzip: 4.42 kB[22m
|
|
13
|
-
[34mℹ[39m [2mdist/[22m[32m[1mclient.d.mts[22m[39m [2m 0.20 kB[22m [2m│ gzip: 0.17 kB[22m
|
|
14
|
-
[34mℹ[39m 4 files, total: 148.54 kB
|
|
15
|
-
[32m✔[39m Build complete in [32m7981ms[39m
|
package/src/client.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { BetterAuthPlugin } from "better-auth";
|
|
2
|
-
import { authMiddlewareFactory } from "./middlewares";
|
|
3
|
-
import {
|
|
4
|
-
createSCIMUser,
|
|
5
|
-
deleteSCIMUser,
|
|
6
|
-
generateSCIMToken,
|
|
7
|
-
getSCIMResourceType,
|
|
8
|
-
getSCIMResourceTypes,
|
|
9
|
-
getSCIMSchema,
|
|
10
|
-
getSCIMSchemas,
|
|
11
|
-
getSCIMServiceProviderConfig,
|
|
12
|
-
getSCIMUser,
|
|
13
|
-
listSCIMUsers,
|
|
14
|
-
patchSCIMUser,
|
|
15
|
-
updateSCIMUser,
|
|
16
|
-
} from "./routes";
|
|
17
|
-
import type { SCIMOptions } from "./types";
|
|
18
|
-
|
|
19
|
-
declare module "@better-auth/core" {
|
|
20
|
-
// biome-ignore lint/correctness/noUnusedVariables: Auth and Context need to be same as declared in the module
|
|
21
|
-
interface BetterAuthPluginRegistry<Auth, Context> {
|
|
22
|
-
scim: {
|
|
23
|
-
creator: typeof scim;
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export const scim = (options?: SCIMOptions) => {
|
|
29
|
-
const opts = {
|
|
30
|
-
storeSCIMToken: "plain",
|
|
31
|
-
...options,
|
|
32
|
-
} satisfies SCIMOptions;
|
|
33
|
-
|
|
34
|
-
const authMiddleware = authMiddlewareFactory(opts);
|
|
35
|
-
|
|
36
|
-
return {
|
|
37
|
-
id: "scim",
|
|
38
|
-
endpoints: {
|
|
39
|
-
generateSCIMToken: generateSCIMToken(opts),
|
|
40
|
-
getSCIMUser: getSCIMUser(authMiddleware),
|
|
41
|
-
createSCIMUser: createSCIMUser(authMiddleware),
|
|
42
|
-
patchSCIMUser: patchSCIMUser(authMiddleware),
|
|
43
|
-
deleteSCIMUser: deleteSCIMUser(authMiddleware),
|
|
44
|
-
updateSCIMUser: updateSCIMUser(authMiddleware),
|
|
45
|
-
listSCIMUsers: listSCIMUsers(authMiddleware),
|
|
46
|
-
getSCIMServiceProviderConfig,
|
|
47
|
-
getSCIMSchemas,
|
|
48
|
-
getSCIMSchema,
|
|
49
|
-
getSCIMResourceTypes,
|
|
50
|
-
getSCIMResourceType,
|
|
51
|
-
},
|
|
52
|
-
schema: {
|
|
53
|
-
scimProvider: {
|
|
54
|
-
fields: {
|
|
55
|
-
providerId: {
|
|
56
|
-
type: "string",
|
|
57
|
-
required: true,
|
|
58
|
-
unique: true,
|
|
59
|
-
},
|
|
60
|
-
scimToken: {
|
|
61
|
-
type: "string",
|
|
62
|
-
required: true,
|
|
63
|
-
unique: true,
|
|
64
|
-
},
|
|
65
|
-
organizationId: {
|
|
66
|
-
type: "string",
|
|
67
|
-
required: false,
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
options,
|
|
73
|
-
} satisfies BetterAuthPlugin;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export * from "./types";
|
package/src/mappings.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import type { SCIMEmail, SCIMName } from "./types";
|
|
2
|
-
|
|
3
|
-
export const getAccountId = (userName: string, externalId?: string) => {
|
|
4
|
-
return externalId ?? userName;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
const getFormattedName = (name: SCIMName) => {
|
|
8
|
-
if (name.givenName && name.familyName) {
|
|
9
|
-
return `${name.givenName} ${name.familyName}`;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (name.givenName) {
|
|
13
|
-
return name.givenName;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
return name.familyName ?? "";
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const getUserFullName = (email: string, name?: SCIMName) => {
|
|
20
|
-
if (name) {
|
|
21
|
-
const formatted = name.formatted?.trim() ?? "";
|
|
22
|
-
if (formatted.length > 0) {
|
|
23
|
-
return formatted;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return getFormattedName(name) || email;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
return email;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
export const getUserPrimaryEmail = (userName: string, emails?: SCIMEmail[]) => {
|
|
33
|
-
return (
|
|
34
|
-
emails?.find((email) => email.primary)?.value ??
|
|
35
|
-
emails?.[0]?.value ??
|
|
36
|
-
userName
|
|
37
|
-
);
|
|
38
|
-
};
|
package/src/middlewares.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { base64Url } from "@better-auth/utils/base64";
|
|
2
|
-
import { createAuthMiddleware } from "better-auth/plugins";
|
|
3
|
-
import { SCIMAPIError } from "./scim-error";
|
|
4
|
-
import { verifySCIMToken } from "./scim-tokens";
|
|
5
|
-
import type { SCIMOptions, SCIMProvider } from "./types";
|
|
6
|
-
|
|
7
|
-
export type AuthMiddleware = ReturnType<typeof authMiddlewareFactory>;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* The middleware forces the endpoint to have a valid token
|
|
11
|
-
*/
|
|
12
|
-
export const authMiddlewareFactory = (opts: SCIMOptions) =>
|
|
13
|
-
createAuthMiddleware(async (ctx) => {
|
|
14
|
-
const authHeader = ctx.headers?.get("Authorization");
|
|
15
|
-
const authSCIMToken = authHeader?.replace(/^Bearer\s+/i, "");
|
|
16
|
-
|
|
17
|
-
if (!authSCIMToken) {
|
|
18
|
-
throw new SCIMAPIError("UNAUTHORIZED", {
|
|
19
|
-
detail: "SCIM token is required",
|
|
20
|
-
});
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const baseScimTokenParts = new TextDecoder()
|
|
24
|
-
.decode(base64Url.decode(authSCIMToken))
|
|
25
|
-
.split(":");
|
|
26
|
-
|
|
27
|
-
const [scimToken, providerId] = baseScimTokenParts;
|
|
28
|
-
const organizationId = baseScimTokenParts.slice(2).join(":");
|
|
29
|
-
|
|
30
|
-
if (!scimToken || !providerId) {
|
|
31
|
-
throw new SCIMAPIError("UNAUTHORIZED", {
|
|
32
|
-
detail: "Invalid SCIM token",
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
let scimProvider: Omit<SCIMProvider, "id"> | null =
|
|
37
|
-
opts.defaultSCIM?.find((p) => {
|
|
38
|
-
if (p.providerId === providerId && !organizationId) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
return !!(
|
|
43
|
-
p.providerId === providerId &&
|
|
44
|
-
organizationId &&
|
|
45
|
-
p.organizationId === organizationId
|
|
46
|
-
);
|
|
47
|
-
}) ?? null;
|
|
48
|
-
|
|
49
|
-
if (scimProvider) {
|
|
50
|
-
if (scimProvider.scimToken === scimToken) {
|
|
51
|
-
return { authSCIMToken: scimProvider.scimToken, scimProvider };
|
|
52
|
-
} else {
|
|
53
|
-
throw new SCIMAPIError("UNAUTHORIZED", {
|
|
54
|
-
detail: "Invalid SCIM token",
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
scimProvider = await ctx.context.adapter.findOne<SCIMProvider>({
|
|
60
|
-
model: "scimProvider",
|
|
61
|
-
where: [
|
|
62
|
-
{ field: "providerId", value: providerId },
|
|
63
|
-
...(organizationId
|
|
64
|
-
? [{ field: "organizationId", value: organizationId }]
|
|
65
|
-
: []),
|
|
66
|
-
],
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
if (!scimProvider) {
|
|
70
|
-
throw new SCIMAPIError("UNAUTHORIZED", {
|
|
71
|
-
detail: "Invalid SCIM token",
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const isValidToken = await verifySCIMToken(
|
|
76
|
-
ctx,
|
|
77
|
-
opts,
|
|
78
|
-
scimProvider.scimToken,
|
|
79
|
-
scimToken,
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
if (!isValidToken) {
|
|
83
|
-
throw new SCIMAPIError("UNAUTHORIZED", {
|
|
84
|
-
detail: "Invalid SCIM token",
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return { authSCIMToken: scimToken, scimProvider };
|
|
89
|
-
});
|
package/src/patch-operations.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type { User } from "better-auth";
|
|
2
|
-
import { getUserFullName } from "./mappings";
|
|
3
|
-
|
|
4
|
-
type Operation = {
|
|
5
|
-
op: "add" | "remove" | "replace";
|
|
6
|
-
value: any;
|
|
7
|
-
path?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type Mapping = {
|
|
11
|
-
target: string;
|
|
12
|
-
resource: "user" | "account";
|
|
13
|
-
map: (user: User, op: Operation, resources: Resources) => any;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type Resources = {
|
|
17
|
-
user: Record<string, any>;
|
|
18
|
-
account: Record<string, any>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const identity = (user: User, op: Operation, resources: Resources) => {
|
|
22
|
-
return op.value;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const lowerCase = (user: User, op: Operation, resources: Resources) => {
|
|
26
|
-
return op.value.toLowerCase();
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
const givenName = (user: User, op: Operation, resources: Resources) => {
|
|
30
|
-
const currentName = (resources.user.name as string) ?? user.name;
|
|
31
|
-
const familyName = currentName.split(" ").slice(1).join(" ").trim();
|
|
32
|
-
const givenName = op.value;
|
|
33
|
-
|
|
34
|
-
return getUserFullName(user.email, {
|
|
35
|
-
givenName,
|
|
36
|
-
familyName,
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const familyName = (user: User, op: Operation, resources: Resources) => {
|
|
41
|
-
const currentName = (resources.user.name as string) ?? user.name;
|
|
42
|
-
const givenName = (
|
|
43
|
-
currentName.split(" ").slice(0, -1).join(" ") || currentName
|
|
44
|
-
).trim();
|
|
45
|
-
const familyName = op.value;
|
|
46
|
-
return getUserFullName(user.email, {
|
|
47
|
-
givenName,
|
|
48
|
-
familyName,
|
|
49
|
-
});
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const userPatchMappings: Record<string, Mapping> = {
|
|
53
|
-
"/name/formatted": { resource: "user", target: "name", map: identity },
|
|
54
|
-
"/name/givenName": { resource: "user", target: "name", map: givenName },
|
|
55
|
-
"/name/familyName": {
|
|
56
|
-
resource: "user",
|
|
57
|
-
target: "name",
|
|
58
|
-
map: familyName,
|
|
59
|
-
},
|
|
60
|
-
"/externalId": {
|
|
61
|
-
resource: "account",
|
|
62
|
-
target: "accountId",
|
|
63
|
-
map: identity,
|
|
64
|
-
},
|
|
65
|
-
"/userName": { resource: "user", target: "email", map: lowerCase },
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const normalizePath = (path: string): string => {
|
|
69
|
-
const withoutLeadingSlash = path.startsWith("/") ? path.slice(1) : path;
|
|
70
|
-
return `/${withoutLeadingSlash.replaceAll(".", "/")}`;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const isNestedObject = (value: unknown): value is Record<string, unknown> => {
|
|
74
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const applyMapping = (
|
|
78
|
-
user: User,
|
|
79
|
-
resources: Resources,
|
|
80
|
-
path: string,
|
|
81
|
-
value: unknown,
|
|
82
|
-
op: "add" | "replace",
|
|
83
|
-
) => {
|
|
84
|
-
const normalizedPath = normalizePath(path);
|
|
85
|
-
const mapping = userPatchMappings[normalizedPath];
|
|
86
|
-
|
|
87
|
-
if (!mapping) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const newValue = mapping.map(
|
|
92
|
-
user,
|
|
93
|
-
{
|
|
94
|
-
op,
|
|
95
|
-
value,
|
|
96
|
-
path: normalizedPath,
|
|
97
|
-
},
|
|
98
|
-
resources,
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
if (op === "add" && mapping.resource === "user") {
|
|
102
|
-
const currentValue = (user as Record<string, unknown>)[mapping.target];
|
|
103
|
-
if (currentValue === newValue) {
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
resources[mapping.resource][mapping.target] = newValue;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const applyPatchValue = (
|
|
112
|
-
user: User,
|
|
113
|
-
resources: Resources,
|
|
114
|
-
value: unknown,
|
|
115
|
-
op: "add" | "replace",
|
|
116
|
-
path?: string | undefined,
|
|
117
|
-
) => {
|
|
118
|
-
if (isNestedObject(value)) {
|
|
119
|
-
for (const [key, nestedValue] of Object.entries(value)) {
|
|
120
|
-
const nestedPath = path ? `${path}.${key}` : key;
|
|
121
|
-
applyPatchValue(user, resources, nestedValue, op, nestedPath);
|
|
122
|
-
}
|
|
123
|
-
} else if (path) {
|
|
124
|
-
applyMapping(user, resources, path, value, op);
|
|
125
|
-
}
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
export const buildUserPatch = (user: User, operations: Operation[]) => {
|
|
129
|
-
const userPatch: Record<string, any> = {};
|
|
130
|
-
const accountPatch: Record<string, any> = {};
|
|
131
|
-
const resources: Resources = { user: userPatch, account: accountPatch };
|
|
132
|
-
|
|
133
|
-
for (const operation of operations) {
|
|
134
|
-
if (operation.op !== "add" && operation.op !== "replace") {
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
applyPatchValue(
|
|
139
|
-
user,
|
|
140
|
-
resources,
|
|
141
|
-
operation.value,
|
|
142
|
-
operation.op,
|
|
143
|
-
operation.path,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return resources;
|
|
148
|
-
};
|