@augmenting-integrations/auth 8.4.1 → 8.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -33,18 +33,16 @@ __export(server_exports, {
33
33
  AuthError: () => AuthError,
34
34
  IMPERSONATE_COOKIE_NAME: () => IMPERSONATE_COOKIE_NAME,
35
35
  IMPERSONATE_TTL_SECONDS: () => IMPERSONATE_TTL_SECONDS,
36
- TENANT_GLOBAL_KEY: () => TENANT_GLOBAL_KEY,
37
- TenantBootScript: () => TenantBootScript,
38
36
  createAuth: () => createAuth,
39
37
  createGetOrCreateAppUser: () => createGetOrCreateAppUser,
40
38
  createImpersonateHandlers: () => createImpersonateHandlers,
41
39
  createInvitationHandlers: () => createInvitationHandlers,
40
+ createInvitationSendHandlers: () => createInvitationSendHandlers,
42
41
  createMeHandler: () => createMeHandler,
42
+ createSettingsHandlers: () => createSettingsHandlers,
43
43
  getUserGroups: () => getUserGroups,
44
44
  hasGroup: () => hasGroup,
45
- loadTenantConfig: () => loadTenantConfig,
46
45
  mintImpersonationToken: () => mintImpersonationToken,
47
- publicSubset: () => publicSubset,
48
46
  requireGroup: () => requireGroup,
49
47
  verifyImpersonationToken: () => verifyImpersonationToken
50
48
  });
@@ -115,6 +113,9 @@ function createAuth(opts) {
115
113
  appDomain: tenant.appDomain,
116
114
  apex: tenant.apex
117
115
  });
116
+ const requiredIdentityGroups = opts.appAccess?.requiredIdentityGroups ?? [];
117
+ const hasAccessPolicy = requiredIdentityGroups.length > 0;
118
+ const forbiddenPage = opts.appAccess?.forbiddenPage ?? (tenant.appDomain === tenant.apex ? `/login?error=app_forbidden&app=${encodeURIComponent(tenant.appSlug ?? "apex")}` : `https://${tenant.apex}/login?error=app_forbidden&app=${encodeURIComponent(tenant.appSlug ?? "")}`);
118
119
  const config = {
119
120
  secret: authSecret,
120
121
  cookies: cookieDomain ? {
@@ -161,7 +162,7 @@ function createAuth(opts) {
161
162
  ],
162
163
  session: { strategy: "jwt" },
163
164
  callbacks: {
164
- jwt: ({ token, user, profile }) => {
165
+ jwt: ({ token, user, profile, account }) => {
165
166
  if (user) {
166
167
  token.sub ??= user.id ?? void 0;
167
168
  token.email ??= user.email ?? void 0;
@@ -179,11 +180,18 @@ function createAuth(opts) {
179
180
  token["cognito:groups"] = groups;
180
181
  }
181
182
  }
183
+ if (account?.access_token) {
184
+ token["accessToken"] = account.access_token;
185
+ }
182
186
  return token;
183
187
  },
184
188
  session: ({ session, token }) => {
185
189
  const groups = token["cognito:groups"] ?? [];
186
190
  session.user.groups = groups;
191
+ const accessToken = token["accessToken"];
192
+ if (typeof accessToken === "string") {
193
+ session.accessToken = accessToken;
194
+ }
187
195
  return session;
188
196
  },
189
197
  authorized: ({ auth: session, request: { nextUrl } }) => {
@@ -199,6 +207,16 @@ function createAuth(opts) {
199
207
  }
200
208
  return false;
201
209
  }
210
+ if (session && isAuthedRoute && hasAccessPolicy) {
211
+ const userGroups = (session.user?.groups ?? []).map((g) => g.toLowerCase());
212
+ const allowed = requiredIdentityGroups.some(
213
+ (g) => userGroups.includes(g.toLowerCase())
214
+ );
215
+ if (!allowed) {
216
+ const target = forbiddenPage.startsWith("http") ? new URL(forbiddenPage) : new URL(forbiddenPage, nextUrl.origin);
217
+ return Response.redirect(target.toString(), 302);
218
+ }
219
+ }
202
220
  return true;
203
221
  },
204
222
  redirect: buildRedirectCallback(tenant.parentDomain)
@@ -345,91 +363,8 @@ function createGetOrCreateAppUser(opts) {
345
363
  };
346
364
  }
347
365
 
348
- // src/server/tenant.ts
349
- var import_server_only3 = require("server-only");
350
- var React = __toESM(require("react"));
351
-
352
- // src/tenant-types.ts
353
- var TENANT_GLOBAL_KEY = "__TENANT__";
354
-
355
- // src/server/tenant.ts
356
- function loadTenantConfig(opts) {
357
- const env = process.env;
358
- const o = opts.overrides ?? {};
359
- const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;
360
- const apexFallback = parentDomainRaw?.replace(/^\./, "");
361
- const draft = {
362
- role: opts.role,
363
- apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,
364
- cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,
365
- parentDomain: parentDomainRaw,
366
- region: o.region ?? env.AWS_REGION ?? "us-east-1",
367
- appSlug: o.appSlug ?? env.APP_SLUG,
368
- appDomain: o.appDomain ?? env.APP_DOMAIN,
369
- authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,
370
- registryTable: o.registryTable ?? env.APP_REGISTRY_TABLE,
371
- authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,
372
- cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,
373
- cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,
374
- adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,
375
- dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,
376
- dbHost: o.dbHost ?? env.DB_HOST,
377
- dbName: o.dbName ?? env.DB_NAME,
378
- stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,
379
- stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN
380
- };
381
- if (opts.role === "apex" && !draft.appDomain) {
382
- draft.appDomain = draft.apex;
383
- }
384
- const required = [
385
- { key: "apex", env: "APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)" },
386
- { key: "cookieDomain", env: "AUTH_COOKIE_DOMAIN" },
387
- { key: "parentDomain", env: "AUTH_ALLOWED_PARENT_DOMAIN" },
388
- { key: "authSecretArn", env: "AUTH_SECRET_ARN" },
389
- { key: "appDomain", env: "APP_DOMAIN" }
390
- ];
391
- if (opts.role === "apex") {
392
- required.push(
393
- { key: "authCognitoSecretArn", env: "AUTH_COGNITO_SECRET_ARN" },
394
- { key: "cognitoIssuer", env: "AUTH_COGNITO_ISSUER" },
395
- { key: "cognitoClientId", env: "AUTH_COGNITO_ID" }
396
- );
397
- } else {
398
- required.push({ key: "appSlug", env: "APP_SLUG" });
399
- }
400
- if (process.env.NEXT_PHASE === "phase-production-build" || !process.env.AWS_LAMBDA_FUNCTION_NAME) {
401
- return draft;
402
- }
403
- const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);
404
- if (missing.length > 0) {
405
- throw new Error(
406
- `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(", ")}`
407
- );
408
- }
409
- return draft;
410
- }
411
- function publicSubset(config) {
412
- return {
413
- apex: config.apex,
414
- cookieDomain: config.cookieDomain,
415
- parentDomain: config.parentDomain,
416
- region: config.region,
417
- appSlug: config.appSlug,
418
- appDomain: config.appDomain,
419
- role: config.role
420
- };
421
- }
422
- var INNER_HTML_PROP = "dangerouslySetInnerHTML";
423
- function TenantBootScript({ config }) {
424
- const payload = JSON.stringify(config).replace(/</g, "\\u003c");
425
- const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;
426
- const props = {};
427
- props[INNER_HTML_PROP] = { __html: body };
428
- return React.createElement("script", props);
429
- }
430
-
431
366
  // src/server/handlers.ts
432
- var import_server_only4 = require("server-only");
367
+ var import_server_only3 = require("server-only");
433
368
  var import_server2 = require("next/server");
434
369
  function createMeHandler(opts) {
435
370
  return {
@@ -655,23 +590,280 @@ function createInvitationHandlers(opts) {
655
590
  }
656
591
  };
657
592
  }
593
+
594
+ // src/server/settings.ts
595
+ var import_server_only4 = require("server-only");
596
+ var import_server3 = require("next/server");
597
+ function getAccessToken(session) {
598
+ if (!session) return null;
599
+ const fromTop = session.accessToken;
600
+ if (typeof fromTop === "string" && fromTop.length > 0) return fromTop;
601
+ return null;
602
+ }
603
+ function accessTokenMissingResponse() {
604
+ return import_server3.NextResponse.json(
605
+ {
606
+ error: "Cognito access token not present on the session. The user must sign in fresh on the apex.",
607
+ code: "access_token_missing"
608
+ },
609
+ { status: 401 }
610
+ );
611
+ }
612
+ function jsonValidation(message) {
613
+ return import_server3.NextResponse.json({ error: message, code: "validation" }, { status: 400 });
614
+ }
615
+ function createSettingsHandlers(opts) {
616
+ const passwordChange = {
617
+ POST: async (request) => {
618
+ const session = await opts.auth();
619
+ if (!session?.user) {
620
+ return import_server3.NextResponse.json({ error: "unauthorized" }, { status: 401 });
621
+ }
622
+ const accessToken = getAccessToken(session);
623
+ if (!accessToken) return accessTokenMissingResponse();
624
+ let body;
625
+ try {
626
+ body = await request.json();
627
+ } catch {
628
+ return jsonValidation("invalid JSON body");
629
+ }
630
+ const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
631
+ const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
632
+ if (!currentPassword) return jsonValidation("currentPassword required");
633
+ if (newPassword.length < 8)
634
+ return jsonValidation("newPassword must be at least 8 chars");
635
+ try {
636
+ await opts.cognito.changePassword({
637
+ accessToken,
638
+ previousPassword: currentPassword,
639
+ proposedPassword: newPassword
640
+ });
641
+ const appUser = await opts.getOrCreateAppUser(session);
642
+ const db = await opts.getDb();
643
+ await db.user.update({
644
+ where: { id: appUser.id },
645
+ data: { must_change_password: false }
646
+ });
647
+ return import_server3.NextResponse.json({ ok: true });
648
+ } catch (err) {
649
+ const message = err instanceof Error ? err.message : String(err);
650
+ return import_server3.NextResponse.json({ error: message, code: "cognito" }, { status: 400 });
651
+ }
652
+ }
653
+ };
654
+ const twoFactorSetup = {
655
+ POST: async () => {
656
+ const session = await opts.auth();
657
+ if (!session?.user) {
658
+ return import_server3.NextResponse.json({ error: "unauthorized" }, { status: 401 });
659
+ }
660
+ const accessToken = getAccessToken(session);
661
+ if (!accessToken) return accessTokenMissingResponse();
662
+ try {
663
+ const { secretCode } = await opts.cognito.associateSoftwareToken({
664
+ accessToken
665
+ });
666
+ const accountName = session.user.email ?? "user";
667
+ const otpAuthUri = opts.cognito.buildOtpAuthUri({
668
+ secret: secretCode,
669
+ accountName,
670
+ issuer: opts.appDisplayName
671
+ });
672
+ const qrDataUrl = opts.generateQrDataUrl ? await opts.generateQrDataUrl(otpAuthUri).catch(() => "") : "";
673
+ return import_server3.NextResponse.json({ qrDataUrl, otpAuthUri, secret: secretCode });
674
+ } catch (err) {
675
+ const message = err instanceof Error ? err.message : String(err);
676
+ return import_server3.NextResponse.json({ error: message, code: "cognito" }, { status: 500 });
677
+ }
678
+ }
679
+ };
680
+ const twoFactorVerify = {
681
+ POST: async (request) => {
682
+ const session = await opts.auth();
683
+ if (!session?.user) {
684
+ return import_server3.NextResponse.json({ error: "unauthorized" }, { status: 401 });
685
+ }
686
+ const accessToken = getAccessToken(session);
687
+ if (!accessToken) return accessTokenMissingResponse();
688
+ let body;
689
+ try {
690
+ body = await request.json();
691
+ } catch {
692
+ return jsonValidation("invalid JSON body");
693
+ }
694
+ const code = typeof body.code === "string" ? body.code : "";
695
+ if (!/^[0-9]{6}$/.test(code)) return jsonValidation("code must be 6 digits");
696
+ try {
697
+ const result = await opts.cognito.verifySoftwareToken({ accessToken, code });
698
+ if (result.status !== "SUCCESS") {
699
+ return import_server3.NextResponse.json(
700
+ { error: `verify failed: ${result.status}`, code: "verify_failed" },
701
+ { status: 400 }
702
+ );
703
+ }
704
+ await opts.cognito.setUserMfaPreference({ accessToken, enabled: true });
705
+ const appUser = await opts.getOrCreateAppUser(session);
706
+ const db = await opts.getDb();
707
+ await db.user.update({
708
+ where: { id: appUser.id },
709
+ data: { two_factor_confirmed_at: /* @__PURE__ */ new Date() }
710
+ });
711
+ return import_server3.NextResponse.json({ ok: true });
712
+ } catch (err) {
713
+ const message = err instanceof Error ? err.message : String(err);
714
+ return import_server3.NextResponse.json({ error: message, code: "cognito" }, { status: 500 });
715
+ }
716
+ }
717
+ };
718
+ const twoFactorDisable = {
719
+ POST: async () => {
720
+ const session = await opts.auth();
721
+ if (!session?.user) {
722
+ return import_server3.NextResponse.json({ error: "unauthorized" }, { status: 401 });
723
+ }
724
+ const accessToken = getAccessToken(session);
725
+ if (!accessToken) return accessTokenMissingResponse();
726
+ try {
727
+ await opts.cognito.setUserMfaPreference({ accessToken, enabled: false });
728
+ const appUser = await opts.getOrCreateAppUser(session);
729
+ const db = await opts.getDb();
730
+ await db.user.update({
731
+ where: { id: appUser.id },
732
+ data: { two_factor_confirmed_at: null }
733
+ });
734
+ return import_server3.NextResponse.json({ ok: true });
735
+ } catch (err) {
736
+ const message = err instanceof Error ? err.message : String(err);
737
+ return import_server3.NextResponse.json({ error: message, code: "cognito" }, { status: 500 });
738
+ }
739
+ }
740
+ };
741
+ return { passwordChange, twoFactorSetup, twoFactorVerify, twoFactorDisable };
742
+ }
743
+
744
+ // src/server/invitations.ts
745
+ var import_server_only5 = require("server-only");
746
+ var import_server4 = require("next/server");
747
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
748
+ var ROLE_RE = /^[a-z_]+$/;
749
+ var PARENT_RE = /^\d+$/;
750
+ function jsonError(status, error, code, extra) {
751
+ return import_server4.NextResponse.json({ error, code, ...extra ?? {} }, { status });
752
+ }
753
+ function createInvitationSendHandlers(opts) {
754
+ const send = {
755
+ POST: async (request) => {
756
+ const session = await opts.auth();
757
+ if (!session?.user) return jsonError(401, "unauthorized", "unauthorized");
758
+ const caller = await opts.getOrCreateAppUser(session);
759
+ if (!opts.canInvite(caller.role)) {
760
+ return jsonError(403, "forbidden", "forbidden");
761
+ }
762
+ let body;
763
+ try {
764
+ body = await request.json();
765
+ } catch {
766
+ return jsonError(400, "invalid JSON body", "validation");
767
+ }
768
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
769
+ const role = typeof body.role === "string" ? body.role.trim() : "";
770
+ const parentIdRaw = body.parent_id;
771
+ if (!EMAIL_RE.test(email) || email.length > 255) {
772
+ return jsonError(400, "invalid email", "validation");
773
+ }
774
+ if (!role || role.length > 30 || !ROLE_RE.test(role)) {
775
+ return jsonError(400, "role must be snake_case lowercase", "validation");
776
+ }
777
+ if (opts.allowedRoles && !opts.allowedRoles.includes(role)) {
778
+ return jsonError(
779
+ 400,
780
+ `role must be one of: ${opts.allowedRoles.join(", ")}`,
781
+ "validation"
782
+ );
783
+ }
784
+ let parentOverride = null;
785
+ if (parentIdRaw !== void 0 && parentIdRaw !== null) {
786
+ if (typeof parentIdRaw !== "string" || !PARENT_RE.test(parentIdRaw)) {
787
+ return jsonError(
788
+ 400,
789
+ "parent_id must be a stringified BigInt or null",
790
+ "validation"
791
+ );
792
+ }
793
+ parentOverride = BigInt(parentIdRaw);
794
+ }
795
+ const parent_id = opts.resolveInviteScope(caller, parentOverride);
796
+ const db = await opts.getDb();
797
+ const existing = await db.user.findUnique({ where: { email } });
798
+ if (existing) {
799
+ return jsonError(409, "user with that email already exists", "conflict");
800
+ }
801
+ const token = opts.generateInvitationToken();
802
+ const expiresAt = opts.invitationExpiresAt();
803
+ const invitation = await db.invitation.create({
804
+ data: {
805
+ token,
806
+ email,
807
+ inviter_id: caller.id,
808
+ intended_role: role,
809
+ parent_id,
810
+ expires_at: expiresAt
811
+ }
812
+ });
813
+ const origin = opts.appDomainEnv ? `https://${opts.appDomainEnv}` : new URL(request.url).origin;
814
+ const invitationUrl = opts.buildInvitationUrl(token, origin);
815
+ const rendered = opts.renderInvitationEmail({
816
+ inviterName: caller.name,
817
+ inviteeEmail: email,
818
+ intendedRole: role,
819
+ invitationUrl,
820
+ expiresAt,
821
+ appDisplayName: opts.appDisplayName
822
+ });
823
+ try {
824
+ await opts.sendEmail({
825
+ to: email,
826
+ subject: rendered.subject,
827
+ html: rendered.html,
828
+ text: rendered.text
829
+ });
830
+ } catch (err) {
831
+ const message = err instanceof Error ? err.message : String(err);
832
+ return import_server4.NextResponse.json(
833
+ {
834
+ invitationId: invitation.id.toString(),
835
+ expiresAt: invitation.expires_at.toISOString(),
836
+ warning: `invitation saved but email send failed: ${message}`
837
+ },
838
+ { status: 202 }
839
+ );
840
+ }
841
+ return import_server4.NextResponse.json(
842
+ {
843
+ invitationId: invitation.id.toString(),
844
+ expiresAt: invitation.expires_at.toISOString()
845
+ },
846
+ { status: 201 }
847
+ );
848
+ }
849
+ };
850
+ return { send };
851
+ }
658
852
  // Annotate the CommonJS export names for ESM import in node:
659
853
  0 && (module.exports = {
660
854
  AuthError,
661
855
  IMPERSONATE_COOKIE_NAME,
662
856
  IMPERSONATE_TTL_SECONDS,
663
- TENANT_GLOBAL_KEY,
664
- TenantBootScript,
665
857
  createAuth,
666
858
  createGetOrCreateAppUser,
667
859
  createImpersonateHandlers,
668
860
  createInvitationHandlers,
861
+ createInvitationSendHandlers,
669
862
  createMeHandler,
863
+ createSettingsHandlers,
670
864
  getUserGroups,
671
865
  hasGroup,
672
- loadTenantConfig,
673
866
  mintImpersonationToken,
674
- publicSubset,
675
867
  requireGroup,
676
868
  verifyImpersonationToken
677
869
  });