@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.
Files changed (105) hide show
  1. package/dist/bin.js +125 -36
  2. package/dist/browser/index.d.ts +3 -13
  3. package/dist/browser/index.js +47 -12
  4. package/dist/browser/navigation.js +1 -1
  5. package/dist/browser/passkey.js +7 -7
  6. package/dist/browser/runtime.js +13 -15
  7. package/dist/client/core/types.d.ts +179 -63
  8. package/dist/client/core/types.js +6 -0
  9. package/dist/client/factors/totp.js +1 -1
  10. package/dist/client/index.d.ts +5 -4
  11. package/dist/client/index.js +115 -56
  12. package/dist/client/runtime/mutex.js +3 -2
  13. package/dist/component/_generated/component.d.ts +40 -0
  14. package/dist/component/convex.config.d.ts +2 -2
  15. package/dist/component/http.js +9 -0
  16. package/dist/component/index.d.ts +1 -1
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/model.js +2 -1
  19. package/dist/component/modules.js +1 -0
  20. package/dist/component/public/factors/passkeys.js +31 -1
  21. package/dist/component/public/identity/codes.js +1 -1
  22. package/dist/component/public/identity/tokens.js +2 -1
  23. package/dist/component/public/identity/verifiers.js +15 -5
  24. package/dist/component/public.js +2 -2
  25. package/dist/component/schema.d.ts +292 -290
  26. package/dist/component/schema.js +2 -1
  27. package/dist/core/index.d.ts +8 -3
  28. package/dist/core/index.js +7 -2
  29. package/dist/expo/index.d.ts +21 -0
  30. package/dist/expo/index.js +148 -0
  31. package/dist/expo/passkey.js +174 -0
  32. package/dist/providers/apple.d.ts +1 -1
  33. package/dist/providers/apple.js +6 -8
  34. package/dist/providers/custom.d.ts +1 -1
  35. package/dist/providers/custom.js +4 -7
  36. package/dist/providers/github.d.ts +1 -1
  37. package/dist/providers/github.js +5 -8
  38. package/dist/providers/google.d.ts +1 -1
  39. package/dist/providers/google.js +5 -8
  40. package/dist/providers/microsoft.d.ts +1 -1
  41. package/dist/providers/microsoft.js +5 -9
  42. package/dist/providers/password.d.ts +18 -37
  43. package/dist/providers/password.js +170 -115
  44. package/dist/providers/redirect.d.ts +1 -0
  45. package/dist/providers/redirect.js +20 -0
  46. package/dist/server/auth.d.ts +6 -7
  47. package/dist/server/auth.js +3 -2
  48. package/dist/server/{ctxCache.js → cache/context.js} +2 -2
  49. package/dist/server/{componentContext.d.ts → component/context.d.ts} +2 -2
  50. package/dist/server/context.js +3 -10
  51. package/dist/server/contract.d.ts +2 -87
  52. package/dist/server/contract.js +1 -1
  53. package/dist/server/cookies.js +25 -1
  54. package/dist/server/core.js +1 -1
  55. package/dist/server/errors.js +24 -1
  56. package/dist/server/{auth-context.d.ts → facade.d.ts} +3 -45
  57. package/dist/server/{auth-context.js → facade.js} +11 -12
  58. package/dist/server/http.d.ts +7 -7
  59. package/dist/server/http.js +36 -7
  60. package/dist/server/{convexIdentity.d.ts → identity/convex.d.ts} +3 -3
  61. package/dist/server/index.d.ts +5 -3
  62. package/dist/server/index.js +3 -2
  63. package/dist/server/mounts.d.ts +175 -22
  64. package/dist/server/mutations/code.js +7 -1
  65. package/dist/server/mutations/{credentialsSignIn.js → credentials/signin.js} +10 -10
  66. package/dist/server/mutations/index.js +1 -1
  67. package/dist/server/mutations/invalidate.js +11 -1
  68. package/dist/server/mutations/oauth.js +25 -27
  69. package/dist/server/mutations/signin.js +6 -0
  70. package/dist/server/mutations/signout.js +5 -0
  71. package/dist/server/mutations/store.js +1 -1
  72. package/dist/server/oauth/factory.js +11 -3
  73. package/dist/server/passkey.js +126 -110
  74. package/dist/server/prefetch.js +8 -1
  75. package/dist/server/redirects.js +11 -3
  76. package/dist/server/refresh.js +6 -1
  77. package/dist/server/runtime.d.ts +58 -36
  78. package/dist/server/runtime.js +333 -36
  79. package/dist/server/services/group.js +4 -0
  80. package/dist/server/sessions.js +1 -0
  81. package/dist/server/signin.js +8 -6
  82. package/dist/server/sso/domain.d.ts +159 -16
  83. package/dist/server/sso/domain.js +1 -1
  84. package/dist/server/sso/http.js +144 -60
  85. package/dist/server/sso/oidc.js +28 -12
  86. package/dist/server/sso/policy.js +30 -14
  87. package/dist/server/sso/provision.js +1 -1
  88. package/dist/server/sso/saml.js +18 -9
  89. package/dist/server/sso/scim.js +12 -4
  90. package/dist/server/sso/shared.js +5 -5
  91. package/dist/server/telemetry.js +3 -0
  92. package/dist/server/tokens.js +10 -2
  93. package/dist/server/totp.js +127 -100
  94. package/dist/server/types.d.ts +224 -151
  95. package/dist/server/url.js +1 -1
  96. package/dist/server/users.js +93 -53
  97. package/dist/server/wellknown.d.ts +130 -0
  98. package/dist/server/wellknown.js +195 -0
  99. package/dist/shared/errors.js +0 -1
  100. package/package.json +36 -4
  101. package/dist/server/oauth/index.js +0 -12
  102. package/dist/server/utils/dispatch.js +0 -36
  103. package/dist/shared/authResults.d.ts +0 -16
  104. /package/dist/server/{componentContext.js → component/context.js} +0 -0
  105. /package/dist/server/{convexIdentity.js → identity/convex.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { ConvexAuthMaterializedConfig, GroupConnectionDeprovisionMode, GroupConnectionPolicy, GroupConnectionPolicyPatch, OIDCClaimMapping } from "../types.js";
2
- import { ComponentCtx, ComponentReadCtx } from "../componentContext.js";
3
- import { AuditEventRecord, ConnectionDomainRecord, GroupConnectionDomainLookupRecord, GroupConnectionListResult, GroupConnectionRecord, ScimConfigRecord, ScimIdentityRecord, WebhookDeliveryRecord, WebhookEndpointRecord } from "../contract.js";
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<GroupConnectionRecord | null>;
71
- getByDomain: (ctx: ComponentReadCtx, domain: string) => Promise<GroupConnectionDomainLookupRecord | null>;
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<GroupConnectionListResult>;
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: GroupConnectionDeprovisionMode;
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<ConnectionDomainRecord[]>;
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: ScimConfigRecord | null;
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<ScimConfigRecord | null>;
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: GroupConnectionDeprovisionMode;
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<AuditEventRecord[]>;
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<WebhookEndpointRecord | null>;
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<WebhookEndpointRecord[]>;
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<WebhookDeliveryRecord[]>;
601
- listReady: (ctx: ComponentReadCtx, limit?: number) => Promise<WebhookDeliveryRecord[]>;
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")}/api/auth/connections/${connection._id}/saml/signin`,
1044
+ signInPath: `${requireEnv("CONVEX_SITE_URL")}/connections/${connection._id}/saml/signin`,
1045
1045
  callbackPath: urls.acsUrl,
1046
1046
  providerId: groupSamlProviderId(connection._id)
1047
1047
  };
@@ -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")) (await request.formData()).forEach((value, key) => {
20
- if (typeof value === "string") params.append(key, value);
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) => await request.json();
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) return new Response(null, { status: 204 });
294
- else throw convexError("INVALID_PARAMETERS", "Missing SAML logout payload.");
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 for (const operation of Array.isArray(body.Operations) ? body.Operations : []) {
521
- if (operation.path === "active") nextActive = typeof operation.value === "boolean" ? operation.value : void 0;
522
- if (operation.path === "displayName" || operation.path === "name.formatted") patchData.name = operation.value;
523
- if (operation.path === "name.givenName") patchData.firstName = operation.value;
524
- if (operation.path === "name.familyName") patchData.lastName = operation.value;
525
- if (operation.path === "userName" || operation.path === "emails.value") {
526
- patchData.email = operation.value;
527
- if (typeof operation.value === "string") patchData.emailVerificationTime = Date.now();
528
- }
529
- if (operation.path === "phoneNumbers.value") {
530
- patchData.phone = operation.value;
531
- if (typeof operation.value === "string") patchData.phoneVerificationTime = Date.now();
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 = await auth.member.inspect(state$1.ctx, {
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 !== void 0 ? externalId : (await getScimIdentityByConnectionAndUser(state$1.ctx, config.component.public, {
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
- const identity = await getScimIdentityByConnectionAndUser(state$1.ctx, config.component.public, {
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)) try {
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
- } catch {}
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
- for (const operation of Array.isArray(body.Operations) ? body.Operations : []) {
763
- if (operation.path === "displayName") await auth.group.update(state$1.ctx, groupId, { name: operation.value });
764
- if (operation.path === "members" && operation.op === "add") for (const member of Array.isArray(operation.value) ? operation.value : []) try {
765
- await auth.member.create(state$1.ctx, {
766
- groupId,
767
- userId: String(member.value),
768
- roleIds: resolveProvisionedRoleIds({
769
- policy: state$1.policy,
770
- groups: typeof body.displayName === "string" ? [body.displayName] : void 0,
771
- roles: pickStringArray(body.roles)
772
- }),
773
- status: "active"
774
- });
775
- } catch {}
776
- if (operation.path === "members" && operation.op === "replace") {
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)) try {
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
- } catch {}
872
+ }
873
+ continue;
799
874
  }
800
- if (typeof operation.path === "string" && operation.op === "remove" && operation.path.startsWith("members[")) {
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) await deleteScimIdentity(state$1.ctx, config.component.public, identity._id);
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
- return scimError(code === "MISSING_BEARER_TOKEN" || code === "INVALID_API_KEY" ? 401 : 400, code, error.data.message);
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 url = new URL(request.url);
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(url.searchParams.entries()), cookies);
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, state } = decodeGroupOidcState(params.get("state"));
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
  },