@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/package.json CHANGED
@@ -1,25 +1,31 @@
1
1
  {
2
2
  "name": "@better-auth/scim",
3
- "author": "Jonathan Samines",
4
- "version": "1.5.0-beta.9",
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
- "module": "dist/index.mjs",
22
- "description": "SCIM plugin for Better Auth",
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.0",
47
- "better-call": "1.2.0",
48
- "zod": "^4.3.5"
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.19.0",
52
- "@better-auth/core": "1.5.0-beta.9",
53
- "@better-auth/sso": "1.5.0-beta.9"
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-beta.9",
57
- "better-auth": "1.5.0-beta.9"
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
- "typecheck": "tsc --project tsconfig.json"
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
  }
@@ -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
- ℹ tsdown v0.19.0 powered by rolldown v1.0.0-beta.59
6
- ℹ config file: /home/runner/work/better-auth/better-auth/packages/scim/tsdown.config.ts
7
- ℹ entry: src/index.ts, src/client.ts
8
- ℹ tsconfig: tsconfig.json
9
- ℹ Build start
10
- ℹ dist/index.mjs  39.48 kB │ gzip: 8.06 kB
11
- ℹ dist/client.mjs  0.15 kB │ gzip: 0.14 kB
12
- ℹ dist/index.d.mts 108.72 kB │ gzip: 4.42 kB
13
- ℹ dist/client.d.mts  0.20 kB │ gzip: 0.17 kB
14
- ℹ 4 files, total: 148.54 kB
15
- ✔ Build complete in 7981ms
package/src/client.ts DELETED
@@ -1,9 +0,0 @@
1
- import type { BetterAuthClientPlugin } from "better-auth";
2
- import type { scim } from "./index";
3
-
4
- export const scimClient = () => {
5
- return {
6
- id: "scim-client",
7
- $InferServerPlugin: {} as ReturnType<typeof scim>,
8
- } satisfies BetterAuthClientPlugin;
9
- };
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
- };
@@ -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
- });
@@ -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
- };