@robelest/convex-auth 0.0.4-preview.30 → 0.0.4-preview.31
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/dist/bin.js +125 -36
- package/dist/browser/index.d.ts +3 -13
- package/dist/browser/index.js +47 -12
- package/dist/browser/navigation.js +1 -1
- package/dist/browser/passkey.js +7 -7
- package/dist/browser/runtime.js +13 -15
- package/dist/client/core/types.d.ts +179 -63
- package/dist/client/core/types.js +6 -0
- package/dist/client/factors/totp.js +1 -1
- package/dist/client/index.d.ts +5 -4
- package/dist/client/index.js +115 -56
- package/dist/client/runtime/mutex.js +3 -2
- package/dist/component/_generated/component.d.ts +40 -0
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/http.js +9 -0
- package/dist/component/index.d.ts +1 -1
- package/dist/component/model.d.ts +25 -25
- package/dist/component/model.js +2 -1
- package/dist/component/modules.js +1 -0
- package/dist/component/public/factors/passkeys.js +31 -1
- package/dist/component/public/identity/codes.js +1 -1
- package/dist/component/public/identity/tokens.js +2 -1
- package/dist/component/public/identity/verifiers.js +15 -5
- package/dist/component/public.js +2 -2
- package/dist/component/schema.d.ts +292 -290
- package/dist/component/schema.js +2 -1
- package/dist/core/index.d.ts +8 -3
- package/dist/core/index.js +7 -2
- package/dist/expo/index.d.ts +21 -0
- package/dist/expo/index.js +148 -0
- package/dist/expo/passkey.js +174 -0
- package/dist/providers/apple.d.ts +1 -1
- package/dist/providers/apple.js +6 -8
- package/dist/providers/custom.d.ts +1 -1
- package/dist/providers/custom.js +4 -7
- package/dist/providers/github.d.ts +1 -1
- package/dist/providers/github.js +5 -8
- package/dist/providers/google.d.ts +1 -1
- package/dist/providers/google.js +5 -8
- package/dist/providers/microsoft.d.ts +1 -1
- package/dist/providers/microsoft.js +5 -9
- package/dist/providers/password.d.ts +18 -37
- package/dist/providers/password.js +170 -115
- package/dist/providers/redirect.d.ts +1 -0
- package/dist/providers/redirect.js +20 -0
- package/dist/server/auth.d.ts +6 -7
- package/dist/server/auth.js +3 -2
- package/dist/server/{ctxCache.js → cache/context.js} +2 -2
- package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
- package/dist/server/context.js +3 -10
- package/dist/server/contract.d.ts +2 -87
- package/dist/server/contract.js +1 -1
- package/dist/server/cookies.js +25 -1
- package/dist/server/core.js +1 -1
- package/dist/server/errors.js +24 -1
- package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
- package/dist/server/{auth-context.js → facade.js} +11 -12
- package/dist/server/http.d.ts +7 -7
- package/dist/server/http.js +36 -7
- package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
- package/dist/server/index.d.ts +5 -3
- package/dist/server/index.js +3 -2
- package/dist/server/mounts.d.ts +175 -22
- package/dist/server/mutations/code.js +7 -1
- package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
- package/dist/server/mutations/index.js +1 -1
- package/dist/server/mutations/invalidate.js +11 -1
- package/dist/server/mutations/oauth.js +25 -27
- package/dist/server/mutations/signin.js +6 -0
- package/dist/server/mutations/signout.js +5 -0
- package/dist/server/mutations/store.js +1 -1
- package/dist/server/oauth/factory.js +11 -3
- package/dist/server/passkey.js +126 -110
- package/dist/server/prefetch.js +8 -1
- package/dist/server/redirects.js +11 -3
- package/dist/server/refresh.js +6 -1
- package/dist/server/runtime.d.ts +58 -36
- package/dist/server/runtime.js +333 -36
- package/dist/server/services/group.js +4 -0
- package/dist/server/sessions.js +1 -0
- package/dist/server/signin.js +8 -6
- package/dist/server/sso/domain.d.ts +159 -16
- package/dist/server/sso/domain.js +1 -1
- package/dist/server/sso/http.js +144 -60
- package/dist/server/sso/oidc.js +28 -12
- package/dist/server/sso/policy.js +30 -14
- package/dist/server/sso/provision.js +1 -1
- package/dist/server/sso/saml.js +18 -9
- package/dist/server/sso/scim.js +12 -4
- package/dist/server/sso/shared.js +5 -5
- package/dist/server/telemetry.js +3 -0
- package/dist/server/tokens.js +10 -2
- package/dist/server/totp.js +127 -100
- package/dist/server/types.d.ts +224 -151
- package/dist/server/url.js +1 -1
- package/dist/server/users.js +93 -53
- package/dist/server/wellknown.d.ts +130 -0
- package/dist/server/wellknown.js +195 -0
- package/dist/shared/errors.js +0 -1
- package/package.json +36 -4
- package/dist/server/oauth/index.js +0 -12
- package/dist/server/utils/dispatch.js +0 -36
- package/dist/shared/authResults.d.ts +0 -16
- /package/dist/server/{componentContext.js → component/context.js} +0 -0
- /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ConvexAuthMaterializedConfig,
|
|
2
|
-
import { ComponentCtx, ComponentReadCtx } from "../
|
|
3
|
-
import {
|
|
1
|
+
import { ConvexAuthMaterializedConfig, GroupConnectionPolicy, GroupConnectionPolicyPatch, OIDCClaimMapping } from "../types.js";
|
|
2
|
+
import { ComponentCtx, ComponentReadCtx } from "../component/context.js";
|
|
3
|
+
import { ScimIdentityRecord } from "../contract.js";
|
|
4
4
|
import { GenericActionCtx, GenericDataModel } from "convex/server";
|
|
5
5
|
|
|
6
6
|
//#region src/server/sso/domain.d.ts
|
|
@@ -67,8 +67,39 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
67
67
|
connectionId: string;
|
|
68
68
|
groupId: string;
|
|
69
69
|
}>;
|
|
70
|
-
get: (ctx: ComponentReadCtx, connectionId: string) => Promise<
|
|
71
|
-
|
|
70
|
+
get: (ctx: ComponentReadCtx, connectionId: string) => Promise<{
|
|
71
|
+
_id: string;
|
|
72
|
+
_creationTime: number;
|
|
73
|
+
groupId: string;
|
|
74
|
+
slug?: string;
|
|
75
|
+
name?: string;
|
|
76
|
+
protocol: "oidc" | "saml";
|
|
77
|
+
status: "draft" | "active" | "disabled";
|
|
78
|
+
config?: unknown;
|
|
79
|
+
extend?: unknown;
|
|
80
|
+
} | null>;
|
|
81
|
+
getByDomain: (ctx: ComponentReadCtx, domain: string) => Promise<{
|
|
82
|
+
connection: {
|
|
83
|
+
_id: string;
|
|
84
|
+
_creationTime: number;
|
|
85
|
+
groupId: string;
|
|
86
|
+
slug?: string;
|
|
87
|
+
name?: string;
|
|
88
|
+
protocol: "oidc" | "saml";
|
|
89
|
+
status: "draft" | "active" | "disabled";
|
|
90
|
+
config?: unknown;
|
|
91
|
+
extend?: unknown;
|
|
92
|
+
} | null;
|
|
93
|
+
domain: {
|
|
94
|
+
_id: string;
|
|
95
|
+
_creationTime: number;
|
|
96
|
+
connectionId: string;
|
|
97
|
+
groupId: string;
|
|
98
|
+
domain: string;
|
|
99
|
+
isPrimary: boolean;
|
|
100
|
+
verifiedAt?: number;
|
|
101
|
+
} | null;
|
|
102
|
+
} | null>;
|
|
72
103
|
list: (ctx: ComponentReadCtx, opts?: {
|
|
73
104
|
where?: {
|
|
74
105
|
groupId?: string;
|
|
@@ -79,7 +110,20 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
79
110
|
cursor?: string | null;
|
|
80
111
|
orderBy?: "_creationTime" | "name" | "slug" | "status";
|
|
81
112
|
order?: "asc" | "desc";
|
|
82
|
-
}) => Promise<
|
|
113
|
+
}) => Promise<{
|
|
114
|
+
items: {
|
|
115
|
+
_id: string;
|
|
116
|
+
_creationTime: number;
|
|
117
|
+
groupId: string;
|
|
118
|
+
slug?: string;
|
|
119
|
+
name?: string;
|
|
120
|
+
protocol: "oidc" | "saml";
|
|
121
|
+
status: "draft" | "active" | "disabled";
|
|
122
|
+
config?: unknown;
|
|
123
|
+
extend?: unknown;
|
|
124
|
+
}[];
|
|
125
|
+
nextCursor: string | null;
|
|
126
|
+
}>;
|
|
83
127
|
update: (ctx: ComponentCtx, connectionId: string, data: Record<string, unknown>) => Promise<{
|
|
84
128
|
connectionId: string;
|
|
85
129
|
}>;
|
|
@@ -115,7 +159,7 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
115
159
|
configured: boolean;
|
|
116
160
|
ready: boolean;
|
|
117
161
|
basePath: string | null;
|
|
118
|
-
deprovisionMode:
|
|
162
|
+
deprovisionMode: "soft" | "hard";
|
|
119
163
|
};
|
|
120
164
|
};
|
|
121
165
|
}>;
|
|
@@ -127,7 +171,15 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
127
171
|
domain: string;
|
|
128
172
|
isPrimary?: boolean;
|
|
129
173
|
}) => Promise<string>;
|
|
130
|
-
list: (ctx: ComponentReadCtx, connectionId: string) => Promise<
|
|
174
|
+
list: (ctx: ComponentReadCtx, connectionId: string) => Promise<{
|
|
175
|
+
_id: string;
|
|
176
|
+
_creationTime: number;
|
|
177
|
+
connectionId: string;
|
|
178
|
+
groupId: string;
|
|
179
|
+
domain: string;
|
|
180
|
+
isPrimary: boolean;
|
|
181
|
+
verifiedAt?: number;
|
|
182
|
+
}[]>;
|
|
131
183
|
validate: (ctx: ComponentReadCtx, connectionId: string) => Promise<{
|
|
132
184
|
connectionId: string;
|
|
133
185
|
ready: boolean;
|
|
@@ -480,7 +532,17 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
480
532
|
connectionId: string;
|
|
481
533
|
configured: boolean;
|
|
482
534
|
ready: boolean;
|
|
483
|
-
config:
|
|
535
|
+
config: {
|
|
536
|
+
_id: string;
|
|
537
|
+
_creationTime: number;
|
|
538
|
+
connectionId: string;
|
|
539
|
+
groupId: string;
|
|
540
|
+
status: string;
|
|
541
|
+
basePath: string;
|
|
542
|
+
tokenHash: string;
|
|
543
|
+
lastRotatedAt?: number;
|
|
544
|
+
extend?: unknown;
|
|
545
|
+
} | null;
|
|
484
546
|
checks: {
|
|
485
547
|
name: string;
|
|
486
548
|
ok: boolean;
|
|
@@ -500,7 +562,17 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
500
562
|
etag: boolean;
|
|
501
563
|
} | undefined;
|
|
502
564
|
}>;
|
|
503
|
-
getConfigByToken: (ctx: ComponentReadCtx, token: string) => Promise<
|
|
565
|
+
getConfigByToken: (ctx: ComponentReadCtx, token: string) => Promise<{
|
|
566
|
+
_id: string;
|
|
567
|
+
_creationTime: number;
|
|
568
|
+
connectionId: string;
|
|
569
|
+
groupId: string;
|
|
570
|
+
status: string;
|
|
571
|
+
basePath: string;
|
|
572
|
+
tokenHash: string;
|
|
573
|
+
lastRotatedAt?: number;
|
|
574
|
+
extend?: unknown;
|
|
575
|
+
} | null>;
|
|
504
576
|
validate: (ctx: ComponentReadCtx, connectionId: string) => Promise<{
|
|
505
577
|
ok: boolean;
|
|
506
578
|
connectionId: string;
|
|
@@ -516,7 +588,7 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
516
588
|
ok: boolean;
|
|
517
589
|
connectionId: string;
|
|
518
590
|
basePath: string;
|
|
519
|
-
deprovisionMode:
|
|
591
|
+
deprovisionMode: "soft" | "hard";
|
|
520
592
|
capabilities: {
|
|
521
593
|
users: boolean;
|
|
522
594
|
groups: boolean;
|
|
@@ -568,11 +640,40 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
568
640
|
connectionId?: string;
|
|
569
641
|
groupId?: string;
|
|
570
642
|
limit?: number;
|
|
571
|
-
}) => Promise<
|
|
643
|
+
}) => Promise<{
|
|
644
|
+
_id: string;
|
|
645
|
+
_creationTime: number;
|
|
646
|
+
connectionId?: string;
|
|
647
|
+
groupId: string;
|
|
648
|
+
eventType: string;
|
|
649
|
+
actorType: string;
|
|
650
|
+
actorId?: string;
|
|
651
|
+
subjectType: string;
|
|
652
|
+
subjectId?: string;
|
|
653
|
+
status: string;
|
|
654
|
+
occurredAt: number;
|
|
655
|
+
requestId?: string;
|
|
656
|
+
ip?: string;
|
|
657
|
+
metadata?: Record<string, unknown>;
|
|
658
|
+
}[]>;
|
|
572
659
|
};
|
|
573
660
|
webhook: {
|
|
574
661
|
endpoint: {
|
|
575
|
-
get: (ctx: ComponentReadCtx, endpointId: string) => Promise<
|
|
662
|
+
get: (ctx: ComponentReadCtx, endpointId: string) => Promise<{
|
|
663
|
+
_id: string;
|
|
664
|
+
_creationTime: number;
|
|
665
|
+
connectionId: string;
|
|
666
|
+
groupId: string;
|
|
667
|
+
url: string;
|
|
668
|
+
status: string;
|
|
669
|
+
secretHash: string;
|
|
670
|
+
subscriptions: string[];
|
|
671
|
+
createdByUserId?: string;
|
|
672
|
+
lastSuccessAt?: number;
|
|
673
|
+
lastFailureAt?: number;
|
|
674
|
+
failureCount: number;
|
|
675
|
+
extend?: unknown;
|
|
676
|
+
} | null>;
|
|
576
677
|
create: (ctx: ComponentCtx, data: {
|
|
577
678
|
connectionId: string;
|
|
578
679
|
url: string;
|
|
@@ -582,7 +683,21 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
582
683
|
}) => Promise<{
|
|
583
684
|
endpointId: string;
|
|
584
685
|
}>;
|
|
585
|
-
list: (ctx: ComponentReadCtx, connectionId: string) => Promise<
|
|
686
|
+
list: (ctx: ComponentReadCtx, connectionId: string) => Promise<{
|
|
687
|
+
_id: string;
|
|
688
|
+
_creationTime: number;
|
|
689
|
+
connectionId: string;
|
|
690
|
+
groupId: string;
|
|
691
|
+
url: string;
|
|
692
|
+
status: string;
|
|
693
|
+
secretHash: string;
|
|
694
|
+
subscriptions: string[];
|
|
695
|
+
createdByUserId?: string;
|
|
696
|
+
lastSuccessAt?: number;
|
|
697
|
+
lastFailureAt?: number;
|
|
698
|
+
failureCount: number;
|
|
699
|
+
extend?: unknown;
|
|
700
|
+
}[]>;
|
|
586
701
|
disable: (ctx: ComponentCtx, endpointId: string) => Promise<{
|
|
587
702
|
endpointId: string;
|
|
588
703
|
}>;
|
|
@@ -597,8 +712,36 @@ declare function createGroupConnectionDomain<TDeps extends DomainDeps>(deps: TDe
|
|
|
597
712
|
list: (ctx: ComponentReadCtx, data: {
|
|
598
713
|
connectionId: string;
|
|
599
714
|
limit?: number;
|
|
600
|
-
}) => Promise<
|
|
601
|
-
|
|
715
|
+
}) => Promise<{
|
|
716
|
+
_id: string;
|
|
717
|
+
_creationTime: number;
|
|
718
|
+
connectionId: string;
|
|
719
|
+
endpointId: string;
|
|
720
|
+
auditEventId?: string;
|
|
721
|
+
eventType: string;
|
|
722
|
+
status: string;
|
|
723
|
+
attemptCount: number;
|
|
724
|
+
nextAttemptAt: number;
|
|
725
|
+
lastAttemptAt?: number;
|
|
726
|
+
lastResponseStatus?: number;
|
|
727
|
+
lastError?: string;
|
|
728
|
+
payload: unknown;
|
|
729
|
+
}[]>;
|
|
730
|
+
listReady: (ctx: ComponentReadCtx, limit?: number) => Promise<{
|
|
731
|
+
_id: string;
|
|
732
|
+
_creationTime: number;
|
|
733
|
+
connectionId: string;
|
|
734
|
+
endpointId: string;
|
|
735
|
+
auditEventId?: string;
|
|
736
|
+
eventType: string;
|
|
737
|
+
status: string;
|
|
738
|
+
attemptCount: number;
|
|
739
|
+
nextAttemptAt: number;
|
|
740
|
+
lastAttemptAt?: number;
|
|
741
|
+
lastResponseStatus?: number;
|
|
742
|
+
lastError?: string;
|
|
743
|
+
payload: unknown;
|
|
744
|
+
}[]>;
|
|
602
745
|
markDelivered: (ctx: ComponentCtx, deliveryId: string, responseStatus?: number) => Promise<void>;
|
|
603
746
|
markFailed: (ctx: ComponentCtx, deliveryId: string, data: {
|
|
604
747
|
attemptCount: number;
|
|
@@ -1041,7 +1041,7 @@ function createGroupConnectionDomain(deps) {
|
|
|
1041
1041
|
}
|
|
1042
1042
|
});
|
|
1043
1043
|
return {
|
|
1044
|
-
signInPath: `${requireEnv("CONVEX_SITE_URL")}/
|
|
1044
|
+
signInPath: `${requireEnv("CONVEX_SITE_URL")}/connections/${connection._id}/saml/signin`,
|
|
1045
1045
|
callbackPath: urls.acsUrl,
|
|
1046
1046
|
providerId: groupSamlProviderId(connection._id)
|
|
1047
1047
|
};
|
package/dist/server/sso/http.js
CHANGED
|
@@ -13,12 +13,16 @@ import { ConvexError } from "convex/values";
|
|
|
13
13
|
import { serialize } from "cookie";
|
|
14
14
|
|
|
15
15
|
//#region src/server/sso/http.ts
|
|
16
|
+
function formDataEntries(formData) {
|
|
17
|
+
return formData;
|
|
18
|
+
}
|
|
16
19
|
async function getOidcCallbackParams(request) {
|
|
17
20
|
const url = new URL(request.url);
|
|
18
21
|
const params = new URLSearchParams(url.searchParams);
|
|
19
|
-
if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded"))
|
|
20
|
-
|
|
21
|
-
|
|
22
|
+
if (request.headers.get("Content-Type")?.includes("application/x-www-form-urlencoded")) {
|
|
23
|
+
const formData = await request.formData();
|
|
24
|
+
for (const [key, value] of formDataEntries(formData)) if (typeof value === "string") params.append(key, value);
|
|
25
|
+
}
|
|
22
26
|
return params;
|
|
23
27
|
}
|
|
24
28
|
function addGroupHttpRuntime(deps) {
|
|
@@ -187,7 +191,18 @@ function addGroupHttpRuntime(deps) {
|
|
|
187
191
|
if (!resourceId) return scimError(400, "invalidPath", `${label} resource ID is required.`);
|
|
188
192
|
return null;
|
|
189
193
|
};
|
|
190
|
-
const readScimJson = async (request) =>
|
|
194
|
+
const readScimJson = async (request) => {
|
|
195
|
+
const contentType = request.headers.get("Content-Type") ?? "";
|
|
196
|
+
if (contentType && !contentType.includes("application/scim+json") && !contentType.includes("application/json")) throw new Error("Unsupported SCIM content type.");
|
|
197
|
+
try {
|
|
198
|
+
return await request.json();
|
|
199
|
+
} catch {
|
|
200
|
+
throw new Error("Invalid SCIM JSON.");
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const unsupportedScimPatch = () => {
|
|
204
|
+
throw new Error("Unsupported SCIM PATCH operation.");
|
|
205
|
+
};
|
|
191
206
|
const handleSamlAcs = async (ctx, request, runtimeRoute) => {
|
|
192
207
|
if (runtimeRoute.protocol !== "saml" || runtimeRoute.rest.length !== 1 || runtimeRoute.rest[0] !== "acs") throw convexError("INVALID_PARAMETERS", "Invalid connection runtime path.");
|
|
193
208
|
const connectionId = runtimeRoute.connectionId;
|
|
@@ -256,7 +271,7 @@ function addGroupHttpRuntime(deps) {
|
|
|
256
271
|
}
|
|
257
272
|
}
|
|
258
273
|
});
|
|
259
|
-
const vurl = setURLSearchParam(await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo ?? (typeof parsedResponse.relayState.redirectTo === "string" ? parsedResponse.relayState.redirectTo : void 0) }), "code", verificationCode);
|
|
274
|
+
const vurl = setURLSearchParam(await redirectAbsoluteUrl(ctx, config, { redirectTo: maybeRedirectTo?.redirectTo ?? (typeof parsedResponse.relayState.redirectTo === "string" ? parsedResponse.relayState.redirectTo : void 0) }), "code", verificationCode);
|
|
260
275
|
const vheaders = new Headers({ Location: vurl });
|
|
261
276
|
vheaders.set("Cache-Control", "must-revalidate");
|
|
262
277
|
for (const { name, value, options } of maybeRedirectTo !== null ? [maybeRedirectTo.updatedCookie] : []) vheaders.append("Set-Cookie", serialize(name, value, options));
|
|
@@ -290,8 +305,10 @@ function addGroupHttpRuntime(deps) {
|
|
|
290
305
|
value: responseContext.context,
|
|
291
306
|
relayState: parsedMessage.relayState
|
|
292
307
|
});
|
|
293
|
-
} else if (parsedMessage.hasSamlResponse)
|
|
294
|
-
|
|
308
|
+
} else if (parsedMessage.hasSamlResponse) {
|
|
309
|
+
if (!parsedMessage.parsedResponse) throw convexError("INVALID_PARAMETERS", "Missing SAML logout response payload.");
|
|
310
|
+
return new Response(null, { status: 204 });
|
|
311
|
+
} else throw convexError("INVALID_PARAMETERS", "Missing SAML logout payload.");
|
|
295
312
|
};
|
|
296
313
|
const handleScimRequest = async (ctx, request) => {
|
|
297
314
|
try {
|
|
@@ -493,6 +510,15 @@ function addGroupHttpRuntime(deps) {
|
|
|
493
510
|
const userId = state$1.parsedPath.resourceId;
|
|
494
511
|
const existingUser = await auth.user.get(state$1.ctx, userId);
|
|
495
512
|
if (!existingUser) return scimError(404, "notFound", "User not found.");
|
|
513
|
+
const existingIdentity = await getScimIdentityByConnectionAndUser(state$1.ctx, config.component.public, {
|
|
514
|
+
connectionId: state$1.connection._id,
|
|
515
|
+
userId
|
|
516
|
+
});
|
|
517
|
+
const existingMembership = await auth.member.inspect(state$1.ctx, {
|
|
518
|
+
groupId: state$1.connection.groupId,
|
|
519
|
+
userId
|
|
520
|
+
});
|
|
521
|
+
if (!existingIdentity && !existingMembership.membership) return scimError(404, "notFound", "User not found.");
|
|
496
522
|
const body = await readScimJson(state$1.request);
|
|
497
523
|
const extractedBase = extractScimProfile(state$1.scimConfig, body);
|
|
498
524
|
const extracted = await config.sso?.hooks?.profileResolved?.({
|
|
@@ -517,18 +543,48 @@ function addGroupHttpRuntime(deps) {
|
|
|
517
543
|
if (provisionProfile.extend) patchData.extend = provisionProfile.extend;
|
|
518
544
|
if (typeof patchData.email === "string") patchData.emailVerificationTime = Date.now();
|
|
519
545
|
if (typeof patchData.phone === "string") patchData.phoneVerificationTime = Date.now();
|
|
520
|
-
} else
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
546
|
+
} else {
|
|
547
|
+
const operations = Array.isArray(body.Operations) ? body.Operations : unsupportedScimPatch();
|
|
548
|
+
for (const operation of operations) {
|
|
549
|
+
const op = typeof operation.op === "string" ? operation.op.toLowerCase() : "replace";
|
|
550
|
+
if (op !== "replace" && op !== "add") unsupportedScimPatch();
|
|
551
|
+
if (operation.path === void 0 && typeof operation.value === "object" && operation.value !== null) {
|
|
552
|
+
const value = operation.value;
|
|
553
|
+
if (typeof value.active === "boolean") nextActive = value.active;
|
|
554
|
+
if (typeof value.displayName === "string") patchData.name = value.displayName;
|
|
555
|
+
if (typeof value.userName === "string") {
|
|
556
|
+
patchData.email = value.userName;
|
|
557
|
+
patchData.emailVerificationTime = Date.now();
|
|
558
|
+
}
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
if (operation.path === "active") {
|
|
562
|
+
nextActive = typeof operation.value === "boolean" ? operation.value : void 0;
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
if (operation.path === "displayName" || operation.path === "name.formatted") {
|
|
566
|
+
patchData.name = operation.value;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (operation.path === "name.givenName") {
|
|
570
|
+
patchData.firstName = operation.value;
|
|
571
|
+
continue;
|
|
572
|
+
}
|
|
573
|
+
if (operation.path === "name.familyName") {
|
|
574
|
+
patchData.lastName = operation.value;
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
if (operation.path === "userName" || operation.path === "emails.value") {
|
|
578
|
+
patchData.email = operation.value;
|
|
579
|
+
if (typeof operation.value === "string") patchData.emailVerificationTime = Date.now();
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
if (operation.path === "phoneNumbers.value") {
|
|
583
|
+
patchData.phone = operation.value;
|
|
584
|
+
if (typeof operation.value === "string") patchData.phoneVerificationTime = Date.now();
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
unsupportedScimPatch();
|
|
532
588
|
}
|
|
533
589
|
}
|
|
534
590
|
const nextPatchData = applyUserProvisioningPatch({
|
|
@@ -541,10 +597,7 @@ function addGroupHttpRuntime(deps) {
|
|
|
541
597
|
userId,
|
|
542
598
|
data: nextPatchData
|
|
543
599
|
});
|
|
544
|
-
const resolution =
|
|
545
|
-
groupId: state$1.connection.groupId,
|
|
546
|
-
userId
|
|
547
|
-
});
|
|
600
|
+
const resolution = existingMembership;
|
|
548
601
|
if (resolution.membership) await auth.member.update(state$1.ctx, resolution.membership._id, {
|
|
549
602
|
roleIds: resolveProvisionedRoleIds({
|
|
550
603
|
policy: state$1.policy,
|
|
@@ -557,10 +610,7 @@ function addGroupHttpRuntime(deps) {
|
|
|
557
610
|
connectionId: state$1.connection._id,
|
|
558
611
|
groupId: state$1.connection.groupId,
|
|
559
612
|
resourceType: "user",
|
|
560
|
-
externalId: externalId
|
|
561
|
-
connectionId: state$1.connection._id,
|
|
562
|
-
userId
|
|
563
|
-
}))?.externalId ?? userId,
|
|
613
|
+
externalId: externalId ?? existingIdentity?.externalId ?? userId,
|
|
564
614
|
userId,
|
|
565
615
|
active: provisionProfile.active !== false && nextActive !== false,
|
|
566
616
|
raw: body,
|
|
@@ -587,16 +637,17 @@ function addGroupHttpRuntime(deps) {
|
|
|
587
637
|
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "User");
|
|
588
638
|
if (missing) return missing;
|
|
589
639
|
const userId = state$1.parsedPath.resourceId;
|
|
640
|
+
const identity = await getScimIdentityByConnectionAndUser(state$1.ctx, config.component.public, {
|
|
641
|
+
connectionId: state$1.connection._id,
|
|
642
|
+
userId
|
|
643
|
+
});
|
|
644
|
+
if (!identity) return scimError(404, "notFound", "User not found.");
|
|
590
645
|
const resolution = await auth.member.inspect(state$1.ctx, {
|
|
591
646
|
groupId: state$1.connection.groupId,
|
|
592
647
|
userId
|
|
593
648
|
});
|
|
594
649
|
if (resolution.membership) await auth.member.delete(state$1.ctx, resolution.membership._id);
|
|
595
|
-
|
|
596
|
-
connectionId: state$1.connection._id,
|
|
597
|
-
userId
|
|
598
|
-
});
|
|
599
|
-
if (identity) if (state$1.policy.provisioning.deprovision.mode === "hard") await deleteScimIdentity(state$1.ctx, config.component.public, identity._id);
|
|
650
|
+
if (state$1.policy.provisioning.deprovision.mode === "hard") await deleteScimIdentity(state$1.ctx, config.component.public, identity._id);
|
|
600
651
|
else await upsertScimIdentity(state$1.ctx, config.component.public, {
|
|
601
652
|
connectionId: identity.connectionId,
|
|
602
653
|
groupId: identity.groupId,
|
|
@@ -729,14 +780,15 @@ function addGroupHttpRuntime(deps) {
|
|
|
729
780
|
const currentByUserId = new Map(currentMembers.map((member) => [member.userId, member]));
|
|
730
781
|
const nextUserIds = new Set((Array.isArray(body.members) ? body.members : []).map((member) => String(member.value)));
|
|
731
782
|
for (const member of currentMembers) if (!nextUserIds.has(member.userId)) await auth.member.delete(state$1.ctx, member._id);
|
|
732
|
-
for (const userId of nextUserIds.values()) if (!currentByUserId.has(userId))
|
|
783
|
+
for (const userId of nextUserIds.values()) if (!currentByUserId.has(userId)) {
|
|
784
|
+
if (!await auth.user.get(state$1.ctx, userId)) return scimError(400, "invalidValue", `Group member user not found: ${userId}`);
|
|
733
785
|
await auth.member.create(state$1.ctx, {
|
|
734
786
|
groupId,
|
|
735
787
|
userId,
|
|
736
788
|
roleIds: provisionedRoleIds,
|
|
737
789
|
status: "active"
|
|
738
790
|
});
|
|
739
|
-
}
|
|
791
|
+
}
|
|
740
792
|
await state$1.recordScimEvent(created ? "group.sso.scim.group.created" : "group.sso.scim.group.updated", true, "group", groupId);
|
|
741
793
|
const group = await auth.group.get(state$1.ctx, groupId);
|
|
742
794
|
const location = `${state$1.url.origin}${state$1.url.pathname}/${groupId}`;
|
|
@@ -758,22 +810,43 @@ function addGroupHttpRuntime(deps) {
|
|
|
758
810
|
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "Group");
|
|
759
811
|
if (missing) return missing;
|
|
760
812
|
const groupId = state$1.parsedPath.resourceId;
|
|
813
|
+
const identity = await getScimIdentityByMappedGroup(state$1.ctx, config.component.public, groupId);
|
|
814
|
+
if (!identity || identity.connectionId !== state$1.connection._id) return scimError(404, "notFound", "Group not found.");
|
|
761
815
|
const body = await readScimJson(state$1.request);
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
816
|
+
const operations = Array.isArray(body.Operations) ? body.Operations : unsupportedScimPatch();
|
|
817
|
+
for (const operation of operations) {
|
|
818
|
+
const op = typeof operation.op === "string" ? operation.op.toLowerCase() : "replace";
|
|
819
|
+
if (operation.path === void 0 && op === "replace" && typeof operation.value === "object" && operation.value !== null) {
|
|
820
|
+
const value = operation.value;
|
|
821
|
+
if (typeof value.displayName === "string") await auth.group.update(state$1.ctx, groupId, { name: value.displayName });
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
if (operation.path === "displayName") {
|
|
825
|
+
if (op !== "replace" && op !== "add") unsupportedScimPatch();
|
|
826
|
+
await auth.group.update(state$1.ctx, groupId, { name: operation.value });
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
if (operation.path === "members" && op === "add") {
|
|
830
|
+
for (const member of Array.isArray(operation.value) ? operation.value : []) {
|
|
831
|
+
const userId = String(member.value);
|
|
832
|
+
if (!await auth.user.get(state$1.ctx, userId)) return scimError(400, "invalidValue", `Group member user not found: ${userId}`);
|
|
833
|
+
if (!(await auth.member.inspect(state$1.ctx, {
|
|
834
|
+
groupId,
|
|
835
|
+
userId
|
|
836
|
+
})).membership) await auth.member.create(state$1.ctx, {
|
|
837
|
+
groupId,
|
|
838
|
+
userId,
|
|
839
|
+
roleIds: resolveProvisionedRoleIds({
|
|
840
|
+
policy: state$1.policy,
|
|
841
|
+
groups: typeof body.displayName === "string" ? [body.displayName] : void 0,
|
|
842
|
+
roles: pickStringArray(body.roles)
|
|
843
|
+
}),
|
|
844
|
+
status: "active"
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
continue;
|
|
848
|
+
}
|
|
849
|
+
if (operation.path === "members" && op === "replace") {
|
|
777
850
|
const currentMembers = (await auth.member.list(state$1.ctx, {
|
|
778
851
|
where: {
|
|
779
852
|
groupId,
|
|
@@ -784,7 +857,8 @@ function addGroupHttpRuntime(deps) {
|
|
|
784
857
|
const currentUserIds = new Set(currentMembers.map((member) => member.userId));
|
|
785
858
|
const nextUserIds = new Set((Array.isArray(operation.value) ? operation.value : []).map((member) => String(member.value)));
|
|
786
859
|
for (const member of currentMembers) if (!nextUserIds.has(member.userId)) await auth.member.delete(state$1.ctx, member._id);
|
|
787
|
-
for (const userId of nextUserIds.values()) if (!currentUserIds.has(userId))
|
|
860
|
+
for (const userId of nextUserIds.values()) if (!currentUserIds.has(userId)) {
|
|
861
|
+
if (!await auth.user.get(state$1.ctx, userId)) return scimError(400, "invalidValue", `Group member user not found: ${userId}`);
|
|
788
862
|
await auth.member.create(state$1.ctx, {
|
|
789
863
|
groupId,
|
|
790
864
|
userId,
|
|
@@ -795,9 +869,10 @@ function addGroupHttpRuntime(deps) {
|
|
|
795
869
|
}),
|
|
796
870
|
status: "active"
|
|
797
871
|
});
|
|
798
|
-
}
|
|
872
|
+
}
|
|
873
|
+
continue;
|
|
799
874
|
}
|
|
800
|
-
if (typeof operation.path === "string" &&
|
|
875
|
+
if (typeof operation.path === "string" && op === "remove" && operation.path.startsWith("members[")) {
|
|
801
876
|
const userId = operation.path.match(/^members\[value eq "([^"]+)"\]$/)?.[1];
|
|
802
877
|
if (userId) {
|
|
803
878
|
const resolution = await auth.member.inspect(state$1.ctx, {
|
|
@@ -806,7 +881,9 @@ function addGroupHttpRuntime(deps) {
|
|
|
806
881
|
});
|
|
807
882
|
if (resolution.membership) await auth.member.delete(state$1.ctx, resolution.membership._id);
|
|
808
883
|
}
|
|
884
|
+
continue;
|
|
809
885
|
}
|
|
886
|
+
unsupportedScimPatch();
|
|
810
887
|
}
|
|
811
888
|
await state$1.recordScimEvent("group.sso.scim.group.updated", true, "group", groupId);
|
|
812
889
|
const group = await auth.group.get(state$1.ctx, groupId);
|
|
@@ -829,9 +906,10 @@ function addGroupHttpRuntime(deps) {
|
|
|
829
906
|
const missing = requireScimResourceId(state$1.parsedPath.resourceId, "Group");
|
|
830
907
|
if (missing) return missing;
|
|
831
908
|
const groupId = state$1.parsedPath.resourceId;
|
|
832
|
-
await auth.group.delete(state$1.ctx, groupId);
|
|
833
909
|
const identity = await getScimIdentityByMappedGroup(state$1.ctx, config.component.public, groupId);
|
|
834
|
-
if (identity
|
|
910
|
+
if (!identity || identity.connectionId !== state$1.connection._id) return scimError(404, "notFound", "Group not found.");
|
|
911
|
+
await auth.group.delete(state$1.ctx, groupId);
|
|
912
|
+
await deleteScimIdentity(state$1.ctx, config.component.public, identity._id);
|
|
835
913
|
await state$1.recordScimEvent("group.sso.scim.group.deleted", true, "group", groupId);
|
|
836
914
|
return new Response(null, { status: 204 });
|
|
837
915
|
};
|
|
@@ -882,15 +960,22 @@ function addGroupHttpRuntime(deps) {
|
|
|
882
960
|
return handler ? await handler(state) : scimError(404, "notFound", "SCIM resource not found.");
|
|
883
961
|
} catch (error) {
|
|
884
962
|
if (error instanceof Error && error.message === "Unsupported SCIM filter.") return scimError(400, "invalidFilter", error.message);
|
|
963
|
+
if (error instanceof Error && error.message === "Invalid SCIM pagination.") return scimError(400, "invalidValue", error.message);
|
|
964
|
+
if (error instanceof Error && error.message === "Invalid SCIM JSON.") return scimError(400, "invalidSyntax", error.message);
|
|
965
|
+
if (error instanceof Error && error.message === "Unsupported SCIM content type.") return scimError(415, "invalidSyntax", error.message);
|
|
966
|
+
if (error instanceof Error && error.message === "Unsupported SCIM PATCH operation.") return scimError(400, "invalidSyntax", error.message);
|
|
885
967
|
if (error instanceof ConvexError && typeof error.data === "object" && error.data !== null && "code" in error.data && "message" in error.data) {
|
|
886
968
|
const code = error.data.code;
|
|
887
|
-
|
|
969
|
+
const status = code === "MISSING_BEARER_TOKEN" || code === "INVALID_API_KEY" ? 401 : 400;
|
|
970
|
+
const response = scimError(status, status === 401 ? "invalidValue" : code, error.data.message);
|
|
971
|
+
if (status === 401) response.headers.set("WWW-Authenticate", "Bearer");
|
|
972
|
+
return response;
|
|
888
973
|
}
|
|
889
974
|
throw error;
|
|
890
975
|
}
|
|
891
976
|
};
|
|
892
977
|
const handleOidcCallbackForConnection = async (ctx, request, connectionId) => {
|
|
893
|
-
const
|
|
978
|
+
const params = await getOidcCallbackParams(request);
|
|
894
979
|
const { connection, oidc } = await loadConnectionOidcOrThrow(ctx, connectionId);
|
|
895
980
|
const { providerId, provider, oauthConfig } = await createGroupConnectionOidcRuntime({
|
|
896
981
|
rootUrl: requireEnv("CONVEX_SITE_URL"),
|
|
@@ -900,11 +985,11 @@ function addGroupHttpRuntime(deps) {
|
|
|
900
985
|
});
|
|
901
986
|
const cookies = getCookies(request);
|
|
902
987
|
const maybeRedirectTo = useRedirectToParam(providerId, cookies);
|
|
903
|
-
const destinationUrl = await redirectAbsoluteUrl(config, { redirectTo: maybeRedirectTo?.redirectTo });
|
|
988
|
+
const destinationUrl = await redirectAbsoluteUrl(ctx, config, { redirectTo: maybeRedirectTo?.redirectTo });
|
|
904
989
|
const result = await handleOAuthCallback(providerId, {
|
|
905
990
|
...oauthConfig,
|
|
906
991
|
provider
|
|
907
|
-
}, Object.fromEntries(
|
|
992
|
+
}, Object.fromEntries(params.entries()), cookies);
|
|
908
993
|
const extraFields = typeof oidc.profile === "object" && oidc.profile !== null ? oidc.profile.extraFields : void 0;
|
|
909
994
|
let profile = result.profile;
|
|
910
995
|
if (extraFields && typeof profile === "object" && profile) {
|
|
@@ -1046,8 +1131,7 @@ function addGroupHttpRuntime(deps) {
|
|
|
1046
1131
|
handleOidcSharedCallback: async (ctx, request) => {
|
|
1047
1132
|
const url = new URL(request.url);
|
|
1048
1133
|
const params = await getOidcCallbackParams(request);
|
|
1049
|
-
const { connectionId
|
|
1050
|
-
params.set("state", state);
|
|
1134
|
+
const { connectionId } = decodeGroupOidcState(params.get("state"));
|
|
1051
1135
|
url.search = params.toString();
|
|
1052
1136
|
return await handleOidcCallbackForConnection(ctx, new Request(url, request), connectionId);
|
|
1053
1137
|
},
|