@robelest/convex-auth 0.0.4-preview.27 → 0.0.4-preview.28

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 (88) hide show
  1. package/README.md +3 -5
  2. package/dist/bin.js +6488 -1571
  3. package/dist/browser/index.js +10 -7
  4. package/dist/browser/locks.js +3 -5
  5. package/dist/browser/navigation.js +7 -10
  6. package/dist/browser/runtime.js +35 -33
  7. package/dist/client/core/types.js +17 -0
  8. package/dist/client/factors/device.js +26 -19
  9. package/dist/client/index.js +151 -163
  10. package/dist/client/runtime/proxy.js +6 -6
  11. package/dist/client/services/adapters.js +3 -7
  12. package/dist/client/services/http.js +2 -5
  13. package/dist/client/services/resolve.js +5 -11
  14. package/dist/client/services/runtime.js +2 -5
  15. package/dist/component/_generated/component.d.ts +46 -0
  16. package/dist/component/index.d.ts +3 -3
  17. package/dist/component/model.d.ts +25 -25
  18. package/dist/component/public/identity/sessions.js +38 -1
  19. package/dist/component/public/identity/tokens.js +81 -3
  20. package/dist/component/public/identity/verifiers.js +9 -3
  21. package/dist/component/public.js +3 -3
  22. package/dist/component/schema.d.ts +320 -320
  23. package/dist/core/index.d.ts +380 -0
  24. package/dist/core/index.js +83 -0
  25. package/dist/otel.d.ts +13 -17
  26. package/dist/otel.js +39 -49
  27. package/dist/providers/email.d.ts +2 -2
  28. package/dist/providers/password.js +8 -16
  29. package/dist/providers/phone.js +2 -9
  30. package/dist/server/auth-context.d.ts +204 -0
  31. package/dist/server/auth-context.js +76 -0
  32. package/dist/server/auth.d.ts +25 -187
  33. package/dist/server/auth.js +5 -96
  34. package/dist/server/componentContext.d.ts +12 -0
  35. package/dist/server/componentContext.js +1 -0
  36. package/dist/server/config.js +1 -12
  37. package/dist/server/constants.js +6 -0
  38. package/dist/server/contract.d.ts +1 -1
  39. package/dist/server/core.js +5 -14
  40. package/dist/server/crypto.js +26 -18
  41. package/dist/server/db.js +6 -1
  42. package/dist/server/device.js +88 -78
  43. package/dist/server/http.d.ts +4 -3
  44. package/dist/server/http.js +74 -86
  45. package/dist/server/index.d.ts +2 -1
  46. package/dist/server/limits.js +22 -15
  47. package/dist/server/mounts.d.ts +103 -103
  48. package/dist/server/mutations/account.js +6 -4
  49. package/dist/server/mutations/invalidate.js +3 -6
  50. package/dist/server/mutations/oauth.js +86 -88
  51. package/dist/server/mutations/refresh.js +45 -87
  52. package/dist/server/mutations/register.js +19 -19
  53. package/dist/server/mutations/retrieve.js +17 -15
  54. package/dist/server/mutations/signature.js +9 -13
  55. package/dist/server/mutations/signin.js +7 -3
  56. package/dist/server/mutations/signout.js +10 -15
  57. package/dist/server/mutations/store.js +22 -12
  58. package/dist/server/mutations/verifier.js +11 -6
  59. package/dist/server/mutations/verify.js +55 -46
  60. package/dist/server/oauth/runtime.js +27 -25
  61. package/dist/server/passkey.js +299 -250
  62. package/dist/server/prefetch.js +283 -281
  63. package/dist/server/refresh.js +7 -60
  64. package/dist/server/runtime.d.ts +82 -206
  65. package/dist/server/runtime.js +63 -56
  66. package/dist/server/services/config.js +5 -3
  67. package/dist/server/services/logger.js +2 -4
  68. package/dist/server/services/providers.js +2 -4
  69. package/dist/server/services/refresh.js +2 -4
  70. package/dist/server/services/resolve.js +15 -14
  71. package/dist/server/services/signin.js +2 -4
  72. package/dist/server/sessions.js +32 -33
  73. package/dist/server/signin.js +177 -142
  74. package/dist/server/sso/domain.d.ts +20 -68
  75. package/dist/server/sso/domain.js +444 -413
  76. package/dist/server/sso/http.js +53 -59
  77. package/dist/server/sso/oidc.js +94 -80
  78. package/dist/server/tokens.js +13 -3
  79. package/dist/server/totp.js +153 -116
  80. package/dist/server/types.d.ts +2 -2
  81. package/dist/server/users.js +18 -23
  82. package/dist/server/utils/cache.js +51 -0
  83. package/dist/server/utils/dispatch.js +36 -0
  84. package/dist/server/utils/retry.js +24 -0
  85. package/dist/server/utils/span.js +32 -0
  86. package/dist/shared/errors.js +9 -3
  87. package/dist/shared/log.js +20 -22
  88. package/package.json +41 -33
@@ -1,5 +1,6 @@
1
1
  import { log } from "../log.js";
2
2
  import { addConnectionDomain, createGroupConnection, deleteConnectionDomain, deleteConnectionDomainVerification, deleteGroupConnection, getConnectionDomainVerification, getGroupConnection, getGroupConnectionByDomain, getScimConfigByConnection, listAuditEvents, listConnectionDomains, listGroupConnections, updateGroupConnection, upsertConnectionDomainVerification, upsertGroupConnectionSecret, verifyConnectionDomain } from "../contract.js";
3
+ import { retryWithBackoff } from "../utils/retry.js";
3
4
  import { getGroupOidcUrls, getGroupSamlUrls, groupOidcProviderId, groupSamlProviderId, normalizeDomain } from "./shared.js";
4
5
  import { getOidcConfig, getPublicOidcConfig, getSamlConfig, upsertProtocolConfig, withOidcSecretState } from "./config.js";
5
6
  import { createGroupPolicyDomain } from "./policies.js";
@@ -7,17 +8,9 @@ import { createGroupScimDomain } from "./provision.js";
7
8
  import { createServiceProviderMetadata, getSamlServiceProviderOptions, parseSamlIdpMetadataChecked } from "./saml.js";
8
9
  import { createGroupWebhookDomain } from "./webhook.js";
9
10
  import { ConvexError } from "convex/values";
10
- import { Effect, Schedule } from "effect";
11
11
 
12
12
  //#region src/server/sso/domain.ts
13
- const NETWORK_RETRY_SCHEDULE = Schedule.both(Schedule.jittered(Schedule.exponential("200 millis")), Schedule.recurs(2));
14
13
  const convexError = (data) => new ConvexError(data);
15
- const toSsoError = (error) => error instanceof ConvexError ? error : typeof error === "object" && error !== null && "code" in error && "message" in error ? convexError(error) : error;
16
- const tryPromise = (options) => Effect.tryPromise({
17
- try: async () => options.try(),
18
- catch: (error) => toSsoError(options.catch(error))
19
- });
20
- const runSsoBoundary = (effect) => Effect.runPromise(effect);
21
14
  /**
22
15
  * Build the connection and SSO management domain.
23
16
  */
@@ -412,239 +405,258 @@ function createGroupConnectionDomain(deps) {
412
405
  }
413
406
  },
414
407
  saml: {
415
- configure: (ctx, data) => {
416
- return runSsoBoundary(Effect.gen(function* () {
417
- const connection = yield* tryPromise({
418
- try: () => getGroupConnection(ctx, config.component.public, data.connectionId),
419
- catch: () => ({
420
- code: "INTERNAL_ERROR",
421
- message: "Failed to load connection."
422
- })
423
- }).pipe(Effect.flatMap((connection$1) => connection$1 === null ? Effect.fail(convexError({
424
- code: "INVALID_PARAMETERS",
425
- message: connectionNotFoundError
426
- })) : Effect.succeed(connection$1)));
427
- if (connection.protocol !== "saml") return yield* Effect.fail(convexError({
408
+ configure: async (ctx, data) => {
409
+ let connection;
410
+ try {
411
+ connection = await getGroupConnection(ctx, config.component.public, data.connectionId);
412
+ } catch {
413
+ throw convexError({
414
+ code: "INTERNAL_ERROR",
415
+ message: "Failed to load connection."
416
+ });
417
+ }
418
+ if (connection === null) throw convexError({
419
+ code: "INVALID_PARAMETERS",
420
+ message: connectionNotFoundError
421
+ });
422
+ if (connection.protocol !== "saml") throw convexError({
423
+ code: "INVALID_PARAMETERS",
424
+ message: "This connection is not a SAML connection."
425
+ });
426
+ const metadataUrl = typeof data.metadata.url === "string" && data.metadata.url.length > 0 ? data.metadata.url : void 0;
427
+ let metadataXml;
428
+ if (metadataUrl) try {
429
+ metadataXml = await retryWithBackoff(async () => {
430
+ const response = await fetch(metadataUrl, { signal: AbortSignal.timeout(1e4) });
431
+ if (!response.ok) throw new Error(`Failed to fetch SAML metadata: ${response.status}`);
432
+ return await response.text();
433
+ });
434
+ } catch (error) {
435
+ throw convexError({
428
436
  code: "INVALID_PARAMETERS",
429
- message: "This connection is not a SAML connection."
430
- }));
431
- const metadataUrl = typeof data.metadata.url === "string" && data.metadata.url.length > 0 ? data.metadata.url : void 0;
432
- const metadataXml = metadataUrl ? yield* tryPromise({
433
- try: async () => {
434
- const response = await fetch(metadataUrl, { signal: AbortSignal.timeout(1e4) });
435
- if (!response.ok) throw new Error(`Failed to fetch SAML metadata: ${response.status}`);
436
- return await response.text();
437
- },
438
- catch: (error) => ({
439
- code: "INVALID_PARAMETERS",
440
- message: error instanceof Error ? error.message : "Failed to fetch SAML metadata"
441
- })
442
- }).pipe(Effect.retry({ schedule: NETWORK_RETRY_SCHEDULE })) : data.metadata.xml ? data.metadata.xml : yield* Effect.fail(convexError({
437
+ message: error instanceof Error ? error.message : "Failed to fetch SAML metadata"
438
+ });
439
+ }
440
+ else if (data.metadata.xml) metadataXml = data.metadata.xml;
441
+ else throw convexError({
442
+ code: "INVALID_PARAMETERS",
443
+ message: "SAML registration requires metadataXml or metadataUrl."
444
+ });
445
+ let parsed;
446
+ try {
447
+ parsed = parseSamlIdpMetadataChecked({
448
+ metadataXml,
449
+ config: { protocols: { saml: { security: data.security } } }
450
+ });
451
+ } catch (error) {
452
+ throw convexError({
443
453
  code: "INVALID_PARAMETERS",
444
- message: "SAML registration requires metadataXml or metadataUrl."
445
- }));
446
- const parsed = yield* tryPromise({
447
- try: () => parseSamlIdpMetadataChecked({
448
- metadataXml,
449
- config: { protocols: { saml: { security: data.security } } }
450
- }),
451
- catch: (error) => ({
452
- code: "INVALID_PARAMETERS",
453
- message: error instanceof Error ? `Failed to parse SAML metadata: ${error.message}` : "Failed to parse SAML metadata."
454
- })
454
+ message: error instanceof Error ? `Failed to parse SAML metadata: ${error.message}` : "Failed to parse SAML metadata."
455
455
  });
456
- log("DEBUG", "[group-sso] saml:configure:parsed", {
457
- connectionId: data.connectionId,
456
+ }
457
+ log("DEBUG", "[group-sso] saml:configure:parsed", {
458
+ connectionId: data.connectionId,
459
+ metadataUrl,
460
+ entityId: parsed.entityId,
461
+ issuer: parsed.issuer
462
+ });
463
+ const baseConfig = upsertProtocolConfig(connection.config, "saml", {
464
+ enabled: true,
465
+ idp: {
458
466
  metadataUrl,
459
- entityId: parsed.entityId,
460
- issuer: parsed.issuer
467
+ metadataXml,
468
+ ...parsed
469
+ },
470
+ serviceProvider: data.serviceProvider,
471
+ request: {
472
+ signAuthnRequests: data.request?.signAuthnRequests ?? parsed.wantsSignedAuthnRequests,
473
+ nameIdFormat: data.request?.nameIdFormat,
474
+ forceAuthn: data.request?.forceAuthn,
475
+ authnContextClassRefs: data.request?.authnContextClassRefs
476
+ },
477
+ profile: {
478
+ mapping: data.profile?.mapping,
479
+ extraFields: data.profile?.extraFields
480
+ },
481
+ security: data.security
482
+ });
483
+ const normalizedDomains = data.domains?.map(normalizeDomain);
484
+ const nextConfig = normalizedDomains ? {
485
+ ...baseConfig,
486
+ domains: normalizedDomains
487
+ } : baseConfig;
488
+ const nextSamlConfig = nextConfig.protocols?.saml ?? void 0;
489
+ log("DEBUG", "[group-sso] saml:configure:nextConfig", {
490
+ connectionId: data.connectionId,
491
+ entityId: nextSamlConfig?.idp?.entityId ?? null,
492
+ issuer: nextSamlConfig?.idp?.issuer ?? null,
493
+ metadataUrl: nextSamlConfig?.idp?.metadataUrl ?? null,
494
+ hasMetadataXml: typeof nextSamlConfig?.idp?.metadataXml === "string"
495
+ });
496
+ try {
497
+ await updateGroupConnection(ctx, config.component.public, {
498
+ connectionId: connection._id,
499
+ data: {
500
+ status: "active",
501
+ config: nextConfig
502
+ }
461
503
  });
462
- const baseConfig = upsertProtocolConfig(connection.config, "saml", {
463
- enabled: true,
464
- idp: {
465
- metadataUrl,
466
- metadataXml,
467
- ...parsed
468
- },
469
- serviceProvider: data.serviceProvider,
470
- request: {
471
- signAuthnRequests: data.request?.signAuthnRequests ?? parsed.wantsSignedAuthnRequests,
472
- nameIdFormat: data.request?.nameIdFormat,
473
- forceAuthn: data.request?.forceAuthn,
474
- authnContextClassRefs: data.request?.authnContextClassRefs
475
- },
476
- profile: {
477
- mapping: data.profile?.mapping,
478
- extraFields: data.profile?.extraFields
479
- },
480
- security: data.security
504
+ } catch {
505
+ throw convexError({
506
+ code: "INTERNAL_ERROR",
507
+ message: "Failed to persist SAML registration."
481
508
  });
482
- const normalizedDomains = data.domains?.map(normalizeDomain);
483
- const nextConfig = normalizedDomains ? {
484
- ...baseConfig,
485
- domains: normalizedDomains
486
- } : baseConfig;
487
- const nextSamlConfig = nextConfig.protocols?.saml ?? void 0;
488
- log("DEBUG", "[group-sso] saml:configure:nextConfig", {
489
- connectionId: data.connectionId,
490
- entityId: nextSamlConfig?.idp?.entityId ?? null,
491
- issuer: nextSamlConfig?.idp?.issuer ?? null,
492
- metadataUrl: nextSamlConfig?.idp?.metadataUrl ?? null,
493
- hasMetadataXml: typeof nextSamlConfig?.idp?.metadataXml === "string"
509
+ }
510
+ if (normalizedDomains) for (const [index, domain] of normalizedDomains.entries()) try {
511
+ await addConnectionDomain(ctx, config.component.public, {
512
+ connectionId: connection._id,
513
+ groupId: connection.groupId,
514
+ domain,
515
+ isPrimary: index === 0
494
516
  });
495
- yield* tryPromise({
496
- try: () => updateGroupConnection(ctx, config.component.public, {
497
- connectionId: connection._id,
498
- data: {
499
- status: "active",
500
- config: nextConfig
501
- }
502
- }),
503
- catch: () => ({
504
- code: "INTERNAL_ERROR",
505
- message: "Failed to persist SAML registration."
506
- })
517
+ } catch {
518
+ throw convexError({
519
+ code: "INTERNAL_ERROR",
520
+ message: "Failed to persist connection domain."
507
521
  });
508
- if (normalizedDomains) for (const [index, domain] of normalizedDomains.entries()) yield* tryPromise({
509
- try: () => addConnectionDomain(ctx, config.component.public, {
510
- connectionId: connection._id,
511
- groupId: connection.groupId,
512
- domain,
513
- isPrimary: index === 0
514
- }),
515
- catch: () => ({
516
- code: "INTERNAL_ERROR",
517
- message: "Failed to persist connection domain."
518
- })
522
+ }
523
+ try {
524
+ await recordGroupAuditEvent(ctx, {
525
+ connectionId: connection._id,
526
+ groupId: connection.groupId,
527
+ eventType: "group.sso.saml.registered",
528
+ actorType: "system",
529
+ subjectType: "group_connection_saml",
530
+ subjectId: connection._id,
531
+ ok: true,
532
+ metadata: {
533
+ metadataUrl,
534
+ domains: normalizedDomains
535
+ }
519
536
  });
520
- yield* tryPromise({
521
- try: () => recordGroupAuditEvent(ctx, {
522
- connectionId: connection._id,
523
- groupId: connection.groupId,
524
- eventType: "group.sso.saml.registered",
525
- actorType: "system",
526
- subjectType: "group_connection_saml",
527
- subjectId: connection._id,
528
- ok: true,
529
- metadata: {
530
- metadataUrl,
531
- domains: normalizedDomains
532
- }
533
- }),
534
- catch: () => ({
535
- code: "INTERNAL_ERROR",
536
- message: "Failed to record SAML registration audit event."
537
- })
537
+ } catch {
538
+ throw convexError({
539
+ code: "INTERNAL_ERROR",
540
+ message: "Failed to record SAML registration audit event."
538
541
  });
539
- return {
540
- connectionId: connection._id,
541
- groupId: connection.groupId
542
- };
543
- }));
542
+ }
543
+ return {
544
+ connectionId: connection._id,
545
+ groupId: connection.groupId
546
+ };
544
547
  },
545
- refresh: (ctx, data) => {
546
- return runSsoBoundary(Effect.gen(function* () {
547
- const connection = yield* tryPromise({
548
- try: () => getGroupConnection(ctx, config.component.public, data.connectionId),
549
- catch: () => ({
550
- code: "INTERNAL_ERROR",
551
- message: "Failed to load connection."
552
- })
553
- }).pipe(Effect.flatMap((connection$1) => connection$1 === null ? Effect.fail(convexError({
554
- code: "INVALID_PARAMETERS",
555
- message: connectionNotFoundError
556
- })) : Effect.succeed(connection$1)));
557
- const samlConfig = connection.config?.protocols?.saml;
558
- if (connection.protocol !== "saml") return yield* Effect.fail(convexError({
559
- code: "INVALID_PARAMETERS",
560
- message: "This connection is not a SAML connection."
561
- }));
562
- if (typeof samlConfig?.idp?.metadataUrl !== "string") return yield* Effect.fail(convexError({
548
+ refresh: async (ctx, data) => {
549
+ let connection;
550
+ try {
551
+ connection = await getGroupConnection(ctx, config.component.public, data.connectionId);
552
+ } catch {
553
+ throw convexError({
554
+ code: "INTERNAL_ERROR",
555
+ message: "Failed to load connection."
556
+ });
557
+ }
558
+ if (connection === null) throw convexError({
559
+ code: "INVALID_PARAMETERS",
560
+ message: connectionNotFoundError
561
+ });
562
+ const samlConfig = connection.config?.protocols?.saml;
563
+ if (connection.protocol !== "saml") throw convexError({
564
+ code: "INVALID_PARAMETERS",
565
+ message: "This connection is not a SAML connection."
566
+ });
567
+ if (typeof samlConfig?.idp?.metadataUrl !== "string") throw convexError({
568
+ code: "INVALID_PARAMETERS",
569
+ message: "SAML metadataUrl is not configured."
570
+ });
571
+ const metadataUrl = samlConfig.idp.metadataUrl;
572
+ let metadataXml;
573
+ try {
574
+ metadataXml = await retryWithBackoff(async () => {
575
+ const response = await fetch(metadataUrl, { signal: AbortSignal.timeout(1e4) });
576
+ if (!response.ok) throw new Error(`Failed to fetch SAML metadata: ${response.status}`);
577
+ return await response.text();
578
+ });
579
+ } catch (error) {
580
+ throw convexError({
563
581
  code: "INVALID_PARAMETERS",
564
- message: "SAML metadataUrl is not configured."
565
- }));
566
- const metadataUrl = samlConfig.idp.metadataUrl;
567
- const response = yield* tryPromise({
568
- try: async () => {
569
- const response$1 = await fetch(metadataUrl, { signal: AbortSignal.timeout(1e4) });
570
- if (!response$1.ok) throw new Error(`Failed to fetch SAML metadata: ${response$1.status}`);
571
- return await response$1.text();
572
- },
573
- catch: (error) => ({
574
- code: "INVALID_PARAMETERS",
575
- message: error instanceof Error ? error.message : "Failed to fetch SAML metadata"
576
- })
577
- }).pipe(Effect.retry({ schedule: NETWORK_RETRY_SCHEDULE }));
578
- const parsed = yield* tryPromise({
579
- try: () => parseSamlIdpMetadataChecked({
580
- metadataXml: response,
581
- config: connection.config
582
- }),
583
- catch: (error) => ({
584
- code: "INVALID_PARAMETERS",
585
- message: error instanceof Error ? `Failed to parse SAML metadata: ${error.message}` : "Failed to parse SAML metadata."
586
- })
582
+ message: error instanceof Error ? error.message : "Failed to fetch SAML metadata"
587
583
  });
588
- const nextConfig = upsertProtocolConfig(connection.config, "saml", {
589
- enabled: true,
590
- idp: {
591
- metadataUrl,
592
- metadataXml: response,
593
- ...parsed
594
- },
595
- serviceProvider: samlConfig.serviceProvider,
596
- request: samlConfig.request,
597
- profile: samlConfig.profile,
598
- security: samlConfig.security
584
+ }
585
+ let parsed;
586
+ try {
587
+ parsed = parseSamlIdpMetadataChecked({
588
+ metadataXml,
589
+ config: connection.config
599
590
  });
600
- yield* tryPromise({
601
- try: () => updateGroupConnection(ctx, config.component.public, {
602
- connectionId: connection._id,
603
- data: {
604
- status: connection.status,
605
- config: nextConfig
606
- }
607
- }),
608
- catch: () => ({
609
- code: "INTERNAL_ERROR",
610
- message: "Failed to persist refreshed SAML metadata."
611
- })
591
+ } catch (error) {
592
+ throw convexError({
593
+ code: "INVALID_PARAMETERS",
594
+ message: error instanceof Error ? `Failed to parse SAML metadata: ${error.message}` : "Failed to parse SAML metadata."
612
595
  });
613
- yield* tryPromise({
614
- try: () => recordGroupAuditEvent(ctx, {
615
- connectionId: connection._id,
616
- groupId: connection.groupId,
617
- eventType: "group.sso.saml.refreshed",
618
- actorType: "system",
619
- subjectType: "group_connection_saml",
620
- subjectId: connection._id,
621
- ok: true,
622
- metadata: { metadataUrl }
623
- }),
624
- catch: () => ({
625
- code: "INTERNAL_ERROR",
626
- message: "Failed to record SAML refresh audit event."
627
- })
596
+ }
597
+ const nextConfig = upsertProtocolConfig(connection.config, "saml", {
598
+ enabled: true,
599
+ idp: {
600
+ metadataUrl,
601
+ metadataXml,
602
+ ...parsed
603
+ },
604
+ serviceProvider: samlConfig.serviceProvider,
605
+ request: samlConfig.request,
606
+ profile: samlConfig.profile,
607
+ security: samlConfig.security
608
+ });
609
+ try {
610
+ await updateGroupConnection(ctx, config.component.public, {
611
+ connectionId: connection._id,
612
+ data: {
613
+ status: connection.status,
614
+ config: nextConfig
615
+ }
628
616
  });
629
- return {
617
+ } catch {
618
+ throw convexError({
619
+ code: "INTERNAL_ERROR",
620
+ message: "Failed to persist refreshed SAML metadata."
621
+ });
622
+ }
623
+ try {
624
+ await recordGroupAuditEvent(ctx, {
630
625
  connectionId: connection._id,
631
- groupId: connection.groupId
632
- };
633
- }));
626
+ groupId: connection.groupId,
627
+ eventType: "group.sso.saml.refreshed",
628
+ actorType: "system",
629
+ subjectType: "group_connection_saml",
630
+ subjectId: connection._id,
631
+ ok: true,
632
+ metadata: { metadataUrl }
633
+ });
634
+ } catch {
635
+ throw convexError({
636
+ code: "INTERNAL_ERROR",
637
+ message: "Failed to record SAML refresh audit event."
638
+ });
639
+ }
640
+ return {
641
+ connectionId: connection._id,
642
+ groupId: connection.groupId
643
+ };
634
644
  },
635
- get: (ctx, connectionId) => {
636
- return runSsoBoundary(Effect.gen(function* () {
637
- return getSamlConfig((yield* tryPromise({
638
- try: () => getGroupConnection(ctx, config.component.public, connectionId),
639
- catch: () => ({
640
- code: "INTERNAL_ERROR",
641
- message: "Failed to load connection."
642
- })
643
- }).pipe(Effect.flatMap((connection) => connection === null ? Effect.fail(convexError({
644
- code: "INVALID_PARAMETERS",
645
- message: connectionNotFoundError
646
- })) : Effect.succeed(connection)))).config);
647
- }));
645
+ get: async (ctx, connectionId) => {
646
+ let connection;
647
+ try {
648
+ connection = await getGroupConnection(ctx, config.component.public, connectionId);
649
+ } catch {
650
+ throw convexError({
651
+ code: "INTERNAL_ERROR",
652
+ message: "Failed to load connection."
653
+ });
654
+ }
655
+ if (connection === null) throw convexError({
656
+ code: "INVALID_PARAMETERS",
657
+ message: connectionNotFoundError
658
+ });
659
+ return getSamlConfig(connection.config);
648
660
  },
649
661
  status: (ctx, connectionId) => {
650
662
  return getGroupConnection(ctx, config.component.public, connectionId).then((connection) => {
@@ -786,141 +798,152 @@ function createGroupConnectionDomain(deps) {
786
798
  },
787
799
  policy,
788
800
  oidc: {
789
- configure: (ctx, data) => {
790
- return runSsoBoundary(Effect.gen(function* () {
791
- if (data.discovery.issuer === void 0 && data.discovery.discoveryUrl === void 0) return yield* Effect.fail(convexError({
792
- code: "INVALID_PARAMETERS",
793
- message: "OIDC registration requires issuer or discoveryUrl."
794
- }));
795
- const connection = yield* tryPromise({
796
- try: () => getGroupConnection(ctx, config.component.public, data.connectionId),
797
- catch: () => ({
798
- code: "INTERNAL_ERROR",
799
- message: "Failed to load connection."
800
- })
801
- }).pipe(Effect.flatMap((connection$1) => connection$1 === null ? Effect.fail(convexError({
802
- code: "INVALID_PARAMETERS",
803
- message: connectionNotFoundError
804
- })) : Effect.succeed(connection$1)));
805
- if (connection.protocol !== "oidc") return yield* Effect.fail(convexError({
806
- code: "INVALID_PARAMETERS",
807
- message: "This connection is not an OIDC connection."
808
- }));
809
- const nextConfig = upsertProtocolConfig(connection.config, "oidc", {
810
- enabled: true,
811
- discovery: {
812
- issuer: data.discovery.issuer,
813
- discoveryUrl: data.discovery.discoveryUrl,
814
- jwksUri: data.discovery.jwksUri,
815
- audience: data.discovery.audience
816
- },
817
- client: {
818
- id: data.client.id,
819
- authMethod: data.client.authMethod
820
- },
821
- request: {
822
- scopes: data.request?.scopes ?? [
823
- "openid",
824
- "profile",
825
- "email"
826
- ],
827
- loginHint: data.request?.loginHint,
828
- authorizationParams: data.request?.authorizationParams
829
- },
830
- security: {
831
- clockToleranceSeconds: data.security?.clockToleranceSeconds,
832
- strictIssuer: data.security?.strictIssuer
833
- },
834
- profile: {
835
- mapping: data.profile?.mapping,
836
- extraFields: data.profile?.extraFields
837
- }
801
+ configure: async (ctx, data) => {
802
+ if (data.discovery.issuer === void 0 && data.discovery.discoveryUrl === void 0) throw convexError({
803
+ code: "INVALID_PARAMETERS",
804
+ message: "OIDC registration requires issuer or discoveryUrl."
805
+ });
806
+ let connection;
807
+ try {
808
+ connection = await getGroupConnection(ctx, config.component.public, data.connectionId);
809
+ } catch {
810
+ throw convexError({
811
+ code: "INTERNAL_ERROR",
812
+ message: "Failed to load connection."
838
813
  });
839
- yield* tryPromise({
840
- try: () => updateGroupConnection(ctx, config.component.public, {
841
- connectionId: data.connectionId,
842
- data: { config: nextConfig }
843
- }),
844
- catch: () => ({
845
- code: "INTERNAL_ERROR",
846
- message: "Failed to persist OIDC registration."
847
- })
814
+ }
815
+ if (connection === null) throw convexError({
816
+ code: "INVALID_PARAMETERS",
817
+ message: connectionNotFoundError
818
+ });
819
+ if (connection.protocol !== "oidc") throw convexError({
820
+ code: "INVALID_PARAMETERS",
821
+ message: "This connection is not an OIDC connection."
822
+ });
823
+ const nextConfig = upsertProtocolConfig(connection.config, "oidc", {
824
+ enabled: true,
825
+ discovery: {
826
+ issuer: data.discovery.issuer,
827
+ discoveryUrl: data.discovery.discoveryUrl,
828
+ jwksUri: data.discovery.jwksUri,
829
+ audience: data.discovery.audience
830
+ },
831
+ client: {
832
+ id: data.client.id,
833
+ authMethod: data.client.authMethod
834
+ },
835
+ request: {
836
+ scopes: data.request?.scopes ?? [
837
+ "openid",
838
+ "profile",
839
+ "email"
840
+ ],
841
+ loginHint: data.request?.loginHint,
842
+ authorizationParams: data.request?.authorizationParams
843
+ },
844
+ security: {
845
+ clockToleranceSeconds: data.security?.clockToleranceSeconds,
846
+ strictIssuer: data.security?.strictIssuer
847
+ },
848
+ profile: {
849
+ mapping: data.profile?.mapping,
850
+ extraFields: data.profile?.extraFields
851
+ }
852
+ });
853
+ try {
854
+ await updateGroupConnection(ctx, config.component.public, {
855
+ connectionId: data.connectionId,
856
+ data: { config: nextConfig }
848
857
  });
849
- if (data.client.secret !== void 0) {
850
- const ciphertext = yield* tryPromise({
851
- try: () => encryptSecret(data.client.secret),
852
- catch: () => ({
853
- code: "INTERNAL_ERROR",
854
- message: "Failed to encrypt OIDC client secret."
855
- })
856
- });
857
- yield* tryPromise({
858
- try: () => upsertGroupConnectionSecret(ctx, config.component.public, {
859
- connectionId: data.connectionId,
860
- groupId: connection.groupId,
861
- kind: GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND,
862
- ciphertext,
863
- updatedAt: Date.now()
864
- }),
865
- catch: () => ({
866
- code: "INTERNAL_ERROR",
867
- message: "Failed to persist OIDC client secret."
868
- })
858
+ } catch {
859
+ throw convexError({
860
+ code: "INTERNAL_ERROR",
861
+ message: "Failed to persist OIDC registration."
862
+ });
863
+ }
864
+ if (data.client.secret !== void 0) {
865
+ let ciphertext;
866
+ try {
867
+ ciphertext = await encryptSecret(data.client.secret);
868
+ } catch {
869
+ throw convexError({
870
+ code: "INTERNAL_ERROR",
871
+ message: "Failed to encrypt OIDC client secret."
869
872
  });
870
873
  }
871
- yield* tryPromise({
872
- try: () => recordGroupAuditEvent(ctx, {
874
+ try {
875
+ await upsertGroupConnectionSecret(ctx, config.component.public, {
873
876
  connectionId: data.connectionId,
874
877
  groupId: connection.groupId,
875
- eventType: "group.sso.oidc.registered",
876
- actorType: "system",
877
- subjectType: "group_connection_oidc",
878
- subjectId: data.connectionId,
879
- ok: true,
880
- metadata: {
881
- issuer: data.discovery.issuer,
882
- discoveryUrl: data.discovery.discoveryUrl,
883
- jwksUri: data.discovery.jwksUri,
884
- audience: data.discovery.audience,
885
- tokenEndpointAuthMethod: data.client.authMethod
886
- }
887
- }),
888
- catch: () => ({
878
+ kind: GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND,
879
+ ciphertext,
880
+ updatedAt: Date.now()
881
+ });
882
+ } catch {
883
+ throw convexError({
889
884
  code: "INTERNAL_ERROR",
890
- message: "Failed to record OIDC registration audit event."
891
- })
885
+ message: "Failed to persist OIDC client secret."
886
+ });
887
+ }
888
+ }
889
+ try {
890
+ await recordGroupAuditEvent(ctx, {
891
+ connectionId: data.connectionId,
892
+ groupId: connection.groupId,
893
+ eventType: "group.sso.oidc.registered",
894
+ actorType: "system",
895
+ subjectType: "group_connection_oidc",
896
+ subjectId: data.connectionId,
897
+ ok: true,
898
+ metadata: {
899
+ issuer: data.discovery.issuer,
900
+ discoveryUrl: data.discovery.discoveryUrl,
901
+ jwksUri: data.discovery.jwksUri,
902
+ audience: data.discovery.audience,
903
+ tokenEndpointAuthMethod: data.client.authMethod
904
+ }
892
905
  });
893
- const secret = yield* tryPromise({
894
- try: () => getGroupConnectionSecret(ctx, data.connectionId, GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND),
895
- catch: () => ({
896
- code: "INTERNAL_ERROR",
897
- message: "Failed to load OIDC secret metadata."
898
- })
906
+ } catch {
907
+ throw convexError({
908
+ code: "INTERNAL_ERROR",
909
+ message: "Failed to record OIDC registration audit event."
899
910
  });
900
- return withOidcSecretState(getPublicOidcConfig(nextConfig), secret !== null);
901
- }));
911
+ }
912
+ let secret;
913
+ try {
914
+ secret = await getGroupConnectionSecret(ctx, data.connectionId, GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND);
915
+ } catch {
916
+ throw convexError({
917
+ code: "INTERNAL_ERROR",
918
+ message: "Failed to load OIDC secret metadata."
919
+ });
920
+ }
921
+ return withOidcSecretState(getPublicOidcConfig(nextConfig), secret !== null);
902
922
  },
903
- get: (ctx, connectionId) => {
904
- return runSsoBoundary(Effect.gen(function* () {
905
- const connection = yield* tryPromise({
906
- try: () => getGroupConnection(ctx, config.component.public, connectionId),
907
- catch: () => ({
908
- code: "INTERNAL_ERROR",
909
- message: "Failed to load connection."
910
- })
911
- }).pipe(Effect.flatMap((connection$1) => connection$1 === null ? Effect.fail(convexError({
912
- code: "INVALID_PARAMETERS",
913
- message: connectionNotFoundError
914
- })) : Effect.succeed(connection$1)));
915
- const secret = yield* tryPromise({
916
- try: () => getGroupConnectionSecret(ctx, connection._id, GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND),
917
- catch: () => ({
918
- code: "INTERNAL_ERROR",
919
- message: "Failed to load OIDC secret metadata."
920
- })
923
+ get: async (ctx, connectionId) => {
924
+ let connection;
925
+ try {
926
+ connection = await getGroupConnection(ctx, config.component.public, connectionId);
927
+ } catch {
928
+ throw convexError({
929
+ code: "INTERNAL_ERROR",
930
+ message: "Failed to load connection."
921
931
  });
922
- return withOidcSecretState(getPublicOidcConfig(connection.config), secret !== null);
923
- }));
932
+ }
933
+ if (connection === null) throw convexError({
934
+ code: "INVALID_PARAMETERS",
935
+ message: connectionNotFoundError
936
+ });
937
+ let secret;
938
+ try {
939
+ secret = await getGroupConnectionSecret(ctx, connection._id, GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND);
940
+ } catch {
941
+ throw convexError({
942
+ code: "INTERNAL_ERROR",
943
+ message: "Failed to load OIDC secret metadata."
944
+ });
945
+ }
946
+ return withOidcSecretState(getPublicOidcConfig(connection.config), secret !== null);
924
947
  },
925
948
  status: (ctx, connectionId) => {
926
949
  return Promise.all([getGroupConnection(ctx, config.component.public, connectionId), getGroupConnectionSecret(ctx, connectionId, GROUP_CONNECTION_OIDC_CLIENT_SECRET_KIND)]).then(([connection, secret]) => {
@@ -948,91 +971,99 @@ function createGroupConnectionDomain(deps) {
948
971
  };
949
972
  });
950
973
  },
951
- signIn: (ctx, data) => {
974
+ signIn: async (ctx, data) => {
952
975
  log("DEBUG", "[group-sso] resolver:start", {
953
976
  connectionId: data.connectionId,
954
977
  email: data.email,
955
978
  domain: data.domain,
956
979
  redirectTo: data.redirectTo
957
980
  });
958
- return runSsoBoundary(Effect.gen(function* () {
959
- const connection = data.connectionId !== void 0 ? yield* tryPromise({
960
- try: () => getGroupConnection(ctx, config.component.public, data.connectionId),
961
- catch: () => ({
981
+ let connection;
982
+ if (data.connectionId !== void 0) {
983
+ try {
984
+ connection = await getGroupConnection(ctx, config.component.public, data.connectionId);
985
+ } catch {
986
+ throw convexError({
962
987
  code: "INTERNAL_ERROR",
963
988
  message: "Failed to load connection."
964
- })
965
- }).pipe(Effect.flatMap((connection$1) => connection$1 === null ? Effect.fail(convexError({
989
+ });
990
+ }
991
+ if (connection === null) throw convexError({
966
992
  code: "INVALID_PARAMETERS",
967
993
  message: connectionNotFoundError
968
- })) : Effect.succeed(connection$1))) : data.domain !== void 0 || data.email !== void 0 ? yield* tryPromise({
969
- try: () => getGroupConnectionByDomain(ctx, config.component.public, normalizeDomain(data.domain ?? String(data.email).split("@").pop() ?? "")),
970
- catch: () => ({
994
+ });
995
+ } else if (data.domain !== void 0 || data.email !== void 0) {
996
+ let result;
997
+ try {
998
+ result = await getGroupConnectionByDomain(ctx, config.component.public, normalizeDomain(data.domain ?? String(data.email).split("@").pop() ?? ""));
999
+ } catch {
1000
+ throw convexError({
971
1001
  code: "INTERNAL_ERROR",
972
1002
  message: "Failed to resolve connection by domain."
973
- })
974
- }).pipe(Effect.tap((result) => Effect.sync(() => {
975
- log("DEBUG", "[group-sso] resolver:domainLookup", result);
976
- })), Effect.flatMap((result) => result?.connection && result.domain?.verifiedAt !== void 0 ? Effect.succeed(result.connection) : Effect.fail(convexError({
977
- code: "INVALID_PARAMETERS",
978
- message: "No group connection matched the provided input."
979
- })))) : yield* Effect.fail(convexError({
1003
+ });
1004
+ }
1005
+ log("DEBUG", "[group-sso] resolver:domainLookup", result);
1006
+ if (result?.connection && result.domain?.verifiedAt !== void 0) connection = result.connection;
1007
+ else throw convexError({
980
1008
  code: "INVALID_PARAMETERS",
981
1009
  message: "No group connection matched the provided input."
982
- }));
983
- if (connection.status !== "active") return yield* Effect.fail(convexError({
984
- code: "INVALID_PARAMETERS",
985
- message: "Group connection is not active."
986
- }));
987
- const protocol = resolveGroupConnectionProtocol(connection);
988
- log("DEBUG", "[group-sso] resolver:connection", {
989
- connectionId: connection._id,
990
- status: connection.status,
991
- protocol
992
1010
  });
993
- const { signInPath, callbackPath, providerId } = protocol === "oidc" ? (() => {
994
- const urls = getGroupOidcUrls({
995
- rootUrl: requireEnv("CONVEX_SITE_URL"),
996
- connectionId: connection._id,
997
- sharedRedirectURI: deps.sharedOidcRedirectURI
998
- });
999
- return {
1000
- signInPath: urls.signInUrl,
1001
- callbackPath: urls.callbackUrl,
1002
- providerId: groupOidcProviderId(connection._id)
1003
- };
1004
- })() : (() => {
1005
- const urls = getGroupSamlUrls({
1006
- rootUrl: requireEnv("CONVEX_SITE_URL"),
1007
- source: {
1008
- kind: "connection",
1009
- id: connection._id
1010
- }
1011
- });
1012
- return {
1013
- signInPath: `${requireEnv("CONVEX_SITE_URL")}/api/auth/connections/${connection._id}/saml/signin`,
1014
- callbackPath: urls.acsUrl,
1015
- providerId: groupSamlProviderId(connection._id)
1016
- };
1017
- })();
1018
- log("DEBUG", "[group-sso] resolver:paths", {
1011
+ } else throw convexError({
1012
+ code: "INVALID_PARAMETERS",
1013
+ message: "No group connection matched the provided input."
1014
+ });
1015
+ if (connection.status !== "active") throw convexError({
1016
+ code: "INVALID_PARAMETERS",
1017
+ message: "Group connection is not active."
1018
+ });
1019
+ const protocol = resolveGroupConnectionProtocol(connection);
1020
+ log("DEBUG", "[group-sso] resolver:connection", {
1021
+ connectionId: connection._id,
1022
+ status: connection.status,
1023
+ protocol
1024
+ });
1025
+ const { signInPath, callbackPath, providerId } = protocol === "oidc" ? (() => {
1026
+ const urls = getGroupOidcUrls({
1027
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
1019
1028
  connectionId: connection._id,
1020
- signInPath,
1021
- callbackPath
1029
+ sharedRedirectURI: deps.sharedOidcRedirectURI
1022
1030
  });
1023
1031
  return {
1024
- connectionId: connection._id,
1025
- protocol,
1026
- providerId,
1027
- signInPath: protocol === "oidc" && typeof data.loginHint === "string" ? (() => {
1028
- const signInUrl = new URL(signInPath);
1029
- signInUrl.searchParams.set("loginHint", data.loginHint);
1030
- return signInUrl.toString();
1031
- })() : signInPath,
1032
- callbackPath,
1033
- redirectTo: data.redirectTo
1032
+ signInPath: urls.signInUrl,
1033
+ callbackPath: urls.callbackUrl,
1034
+ providerId: groupOidcProviderId(connection._id)
1034
1035
  };
1035
- }));
1036
+ })() : (() => {
1037
+ const urls = getGroupSamlUrls({
1038
+ rootUrl: requireEnv("CONVEX_SITE_URL"),
1039
+ source: {
1040
+ kind: "connection",
1041
+ id: connection._id
1042
+ }
1043
+ });
1044
+ return {
1045
+ signInPath: `${requireEnv("CONVEX_SITE_URL")}/api/auth/connections/${connection._id}/saml/signin`,
1046
+ callbackPath: urls.acsUrl,
1047
+ providerId: groupSamlProviderId(connection._id)
1048
+ };
1049
+ })();
1050
+ log("DEBUG", "[group-sso] resolver:paths", {
1051
+ connectionId: connection._id,
1052
+ signInPath,
1053
+ callbackPath
1054
+ });
1055
+ return {
1056
+ connectionId: connection._id,
1057
+ protocol,
1058
+ providerId,
1059
+ signInPath: protocol === "oidc" && typeof data.loginHint === "string" ? (() => {
1060
+ const signInUrl = new URL(signInPath);
1061
+ signInUrl.searchParams.set("loginHint", data.loginHint);
1062
+ return signInUrl.toString();
1063
+ })() : signInPath,
1064
+ callbackPath,
1065
+ redirectTo: data.redirectTo
1066
+ };
1036
1067
  },
1037
1068
  validate: async (ctx, connectionId) => {
1038
1069
  const checks = [];