@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.js CHANGED
@@ -63,6 +63,9 @@ function createAuth(opts) {
63
63
  appDomain: tenant.appDomain,
64
64
  apex: tenant.apex
65
65
  });
66
+ const requiredIdentityGroups = opts.appAccess?.requiredIdentityGroups ?? [];
67
+ const hasAccessPolicy = requiredIdentityGroups.length > 0;
68
+ 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 ?? "")}`);
66
69
  const config = {
67
70
  secret: authSecret,
68
71
  cookies: cookieDomain ? {
@@ -109,7 +112,7 @@ function createAuth(opts) {
109
112
  ],
110
113
  session: { strategy: "jwt" },
111
114
  callbacks: {
112
- jwt: ({ token, user, profile }) => {
115
+ jwt: ({ token, user, profile, account }) => {
113
116
  if (user) {
114
117
  token.sub ??= user.id ?? void 0;
115
118
  token.email ??= user.email ?? void 0;
@@ -127,11 +130,18 @@ function createAuth(opts) {
127
130
  token["cognito:groups"] = groups;
128
131
  }
129
132
  }
133
+ if (account?.access_token) {
134
+ token["accessToken"] = account.access_token;
135
+ }
130
136
  return token;
131
137
  },
132
138
  session: ({ session, token }) => {
133
139
  const groups = token["cognito:groups"] ?? [];
134
140
  session.user.groups = groups;
141
+ const accessToken = token["accessToken"];
142
+ if (typeof accessToken === "string") {
143
+ session.accessToken = accessToken;
144
+ }
135
145
  return session;
136
146
  },
137
147
  authorized: ({ auth: session, request: { nextUrl } }) => {
@@ -147,6 +157,16 @@ function createAuth(opts) {
147
157
  }
148
158
  return false;
149
159
  }
160
+ if (session && isAuthedRoute && hasAccessPolicy) {
161
+ const userGroups = (session.user?.groups ?? []).map((g) => g.toLowerCase());
162
+ const allowed = requiredIdentityGroups.some(
163
+ (g) => userGroups.includes(g.toLowerCase())
164
+ );
165
+ if (!allowed) {
166
+ const target = forbiddenPage.startsWith("http") ? new URL(forbiddenPage) : new URL(forbiddenPage, nextUrl.origin);
167
+ return Response.redirect(target.toString(), 302);
168
+ }
169
+ }
150
170
  return true;
151
171
  },
152
172
  redirect: buildRedirectCallback(tenant.parentDomain)
@@ -293,89 +313,6 @@ function createGetOrCreateAppUser(opts) {
293
313
  };
294
314
  }
295
315
 
296
- // src/server/tenant.ts
297
- import "server-only";
298
- import * as React from "react";
299
-
300
- // src/tenant-types.ts
301
- var TENANT_GLOBAL_KEY = "__TENANT__";
302
-
303
- // src/server/tenant.ts
304
- function loadTenantConfig(opts) {
305
- const env = process.env;
306
- const o = opts.overrides ?? {};
307
- const parentDomainRaw = o.parentDomain ?? env.AUTH_ALLOWED_PARENT_DOMAIN;
308
- const apexFallback = parentDomainRaw?.replace(/^\./, "");
309
- const draft = {
310
- role: opts.role,
311
- apex: o.apex ?? env.APEX_DOMAIN ?? apexFallback,
312
- cookieDomain: o.cookieDomain ?? env.AUTH_COOKIE_DOMAIN,
313
- parentDomain: parentDomainRaw,
314
- region: o.region ?? env.AWS_REGION ?? "us-east-1",
315
- appSlug: o.appSlug ?? env.APP_SLUG,
316
- appDomain: o.appDomain ?? env.APP_DOMAIN,
317
- authSecretArn: o.authSecretArn ?? env.AUTH_SECRET_ARN,
318
- registryTable: o.registryTable ?? env.APP_REGISTRY_TABLE,
319
- authCognitoSecretArn: o.authCognitoSecretArn ?? env.AUTH_COGNITO_SECRET_ARN,
320
- cognitoIssuer: o.cognitoIssuer ?? env.AUTH_COGNITO_ISSUER,
321
- cognitoClientId: o.cognitoClientId ?? env.AUTH_COGNITO_ID,
322
- adminEmails: o.adminEmails ?? env.ADMIN_EMAILS,
323
- dbSecretArn: o.dbSecretArn ?? env.DB_SECRET_ARN,
324
- dbHost: o.dbHost ?? env.DB_HOST,
325
- dbName: o.dbName ?? env.DB_NAME,
326
- stripeSecretArn: o.stripeSecretArn ?? env.STRIPE_SECRET_ARN,
327
- stripeWebhookSecretArn: o.stripeWebhookSecretArn ?? env.STRIPE_WEBHOOK_SECRET_ARN
328
- };
329
- if (opts.role === "apex" && !draft.appDomain) {
330
- draft.appDomain = draft.apex;
331
- }
332
- const required = [
333
- { key: "apex", env: "APEX_DOMAIN (or derived from AUTH_ALLOWED_PARENT_DOMAIN)" },
334
- { key: "cookieDomain", env: "AUTH_COOKIE_DOMAIN" },
335
- { key: "parentDomain", env: "AUTH_ALLOWED_PARENT_DOMAIN" },
336
- { key: "authSecretArn", env: "AUTH_SECRET_ARN" },
337
- { key: "appDomain", env: "APP_DOMAIN" }
338
- ];
339
- if (opts.role === "apex") {
340
- required.push(
341
- { key: "authCognitoSecretArn", env: "AUTH_COGNITO_SECRET_ARN" },
342
- { key: "cognitoIssuer", env: "AUTH_COGNITO_ISSUER" },
343
- { key: "cognitoClientId", env: "AUTH_COGNITO_ID" }
344
- );
345
- } else {
346
- required.push({ key: "appSlug", env: "APP_SLUG" });
347
- }
348
- if (process.env.NEXT_PHASE === "phase-production-build" || !process.env.AWS_LAMBDA_FUNCTION_NAME) {
349
- return draft;
350
- }
351
- const missing = required.filter((r) => !draft[r.key]).map((r) => r.env);
352
- if (missing.length > 0) {
353
- throw new Error(
354
- `loadTenantConfig(${opts.role}): missing required env vars: ${missing.join(", ")}`
355
- );
356
- }
357
- return draft;
358
- }
359
- function publicSubset(config) {
360
- return {
361
- apex: config.apex,
362
- cookieDomain: config.cookieDomain,
363
- parentDomain: config.parentDomain,
364
- region: config.region,
365
- appSlug: config.appSlug,
366
- appDomain: config.appDomain,
367
- role: config.role
368
- };
369
- }
370
- var INNER_HTML_PROP = "dangerouslySetInnerHTML";
371
- function TenantBootScript({ config }) {
372
- const payload = JSON.stringify(config).replace(/</g, "\\u003c");
373
- const body = `window.${TENANT_GLOBAL_KEY}=${payload};`;
374
- const props = {};
375
- props[INNER_HTML_PROP] = { __html: body };
376
- return React.createElement("script", props);
377
- }
378
-
379
316
  // src/server/handlers.ts
380
317
  import "server-only";
381
318
  import { NextResponse } from "next/server";
@@ -603,22 +540,279 @@ function createInvitationHandlers(opts) {
603
540
  }
604
541
  };
605
542
  }
543
+
544
+ // src/server/settings.ts
545
+ import "server-only";
546
+ import { NextResponse as NextResponse2 } from "next/server";
547
+ function getAccessToken(session) {
548
+ if (!session) return null;
549
+ const fromTop = session.accessToken;
550
+ if (typeof fromTop === "string" && fromTop.length > 0) return fromTop;
551
+ return null;
552
+ }
553
+ function accessTokenMissingResponse() {
554
+ return NextResponse2.json(
555
+ {
556
+ error: "Cognito access token not present on the session. The user must sign in fresh on the apex.",
557
+ code: "access_token_missing"
558
+ },
559
+ { status: 401 }
560
+ );
561
+ }
562
+ function jsonValidation(message) {
563
+ return NextResponse2.json({ error: message, code: "validation" }, { status: 400 });
564
+ }
565
+ function createSettingsHandlers(opts) {
566
+ const passwordChange = {
567
+ POST: async (request) => {
568
+ const session = await opts.auth();
569
+ if (!session?.user) {
570
+ return NextResponse2.json({ error: "unauthorized" }, { status: 401 });
571
+ }
572
+ const accessToken = getAccessToken(session);
573
+ if (!accessToken) return accessTokenMissingResponse();
574
+ let body;
575
+ try {
576
+ body = await request.json();
577
+ } catch {
578
+ return jsonValidation("invalid JSON body");
579
+ }
580
+ const currentPassword = typeof body.currentPassword === "string" ? body.currentPassword : "";
581
+ const newPassword = typeof body.newPassword === "string" ? body.newPassword : "";
582
+ if (!currentPassword) return jsonValidation("currentPassword required");
583
+ if (newPassword.length < 8)
584
+ return jsonValidation("newPassword must be at least 8 chars");
585
+ try {
586
+ await opts.cognito.changePassword({
587
+ accessToken,
588
+ previousPassword: currentPassword,
589
+ proposedPassword: newPassword
590
+ });
591
+ const appUser = await opts.getOrCreateAppUser(session);
592
+ const db = await opts.getDb();
593
+ await db.user.update({
594
+ where: { id: appUser.id },
595
+ data: { must_change_password: false }
596
+ });
597
+ return NextResponse2.json({ ok: true });
598
+ } catch (err) {
599
+ const message = err instanceof Error ? err.message : String(err);
600
+ return NextResponse2.json({ error: message, code: "cognito" }, { status: 400 });
601
+ }
602
+ }
603
+ };
604
+ const twoFactorSetup = {
605
+ POST: async () => {
606
+ const session = await opts.auth();
607
+ if (!session?.user) {
608
+ return NextResponse2.json({ error: "unauthorized" }, { status: 401 });
609
+ }
610
+ const accessToken = getAccessToken(session);
611
+ if (!accessToken) return accessTokenMissingResponse();
612
+ try {
613
+ const { secretCode } = await opts.cognito.associateSoftwareToken({
614
+ accessToken
615
+ });
616
+ const accountName = session.user.email ?? "user";
617
+ const otpAuthUri = opts.cognito.buildOtpAuthUri({
618
+ secret: secretCode,
619
+ accountName,
620
+ issuer: opts.appDisplayName
621
+ });
622
+ const qrDataUrl = opts.generateQrDataUrl ? await opts.generateQrDataUrl(otpAuthUri).catch(() => "") : "";
623
+ return NextResponse2.json({ qrDataUrl, otpAuthUri, secret: secretCode });
624
+ } catch (err) {
625
+ const message = err instanceof Error ? err.message : String(err);
626
+ return NextResponse2.json({ error: message, code: "cognito" }, { status: 500 });
627
+ }
628
+ }
629
+ };
630
+ const twoFactorVerify = {
631
+ POST: async (request) => {
632
+ const session = await opts.auth();
633
+ if (!session?.user) {
634
+ return NextResponse2.json({ error: "unauthorized" }, { status: 401 });
635
+ }
636
+ const accessToken = getAccessToken(session);
637
+ if (!accessToken) return accessTokenMissingResponse();
638
+ let body;
639
+ try {
640
+ body = await request.json();
641
+ } catch {
642
+ return jsonValidation("invalid JSON body");
643
+ }
644
+ const code = typeof body.code === "string" ? body.code : "";
645
+ if (!/^[0-9]{6}$/.test(code)) return jsonValidation("code must be 6 digits");
646
+ try {
647
+ const result = await opts.cognito.verifySoftwareToken({ accessToken, code });
648
+ if (result.status !== "SUCCESS") {
649
+ return NextResponse2.json(
650
+ { error: `verify failed: ${result.status}`, code: "verify_failed" },
651
+ { status: 400 }
652
+ );
653
+ }
654
+ await opts.cognito.setUserMfaPreference({ accessToken, enabled: true });
655
+ const appUser = await opts.getOrCreateAppUser(session);
656
+ const db = await opts.getDb();
657
+ await db.user.update({
658
+ where: { id: appUser.id },
659
+ data: { two_factor_confirmed_at: /* @__PURE__ */ new Date() }
660
+ });
661
+ return NextResponse2.json({ ok: true });
662
+ } catch (err) {
663
+ const message = err instanceof Error ? err.message : String(err);
664
+ return NextResponse2.json({ error: message, code: "cognito" }, { status: 500 });
665
+ }
666
+ }
667
+ };
668
+ const twoFactorDisable = {
669
+ POST: async () => {
670
+ const session = await opts.auth();
671
+ if (!session?.user) {
672
+ return NextResponse2.json({ error: "unauthorized" }, { status: 401 });
673
+ }
674
+ const accessToken = getAccessToken(session);
675
+ if (!accessToken) return accessTokenMissingResponse();
676
+ try {
677
+ await opts.cognito.setUserMfaPreference({ accessToken, enabled: false });
678
+ const appUser = await opts.getOrCreateAppUser(session);
679
+ const db = await opts.getDb();
680
+ await db.user.update({
681
+ where: { id: appUser.id },
682
+ data: { two_factor_confirmed_at: null }
683
+ });
684
+ return NextResponse2.json({ ok: true });
685
+ } catch (err) {
686
+ const message = err instanceof Error ? err.message : String(err);
687
+ return NextResponse2.json({ error: message, code: "cognito" }, { status: 500 });
688
+ }
689
+ }
690
+ };
691
+ return { passwordChange, twoFactorSetup, twoFactorVerify, twoFactorDisable };
692
+ }
693
+
694
+ // src/server/invitations.ts
695
+ import "server-only";
696
+ import { NextResponse as NextResponse3 } from "next/server";
697
+ var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
698
+ var ROLE_RE = /^[a-z_]+$/;
699
+ var PARENT_RE = /^\d+$/;
700
+ function jsonError(status, error, code, extra) {
701
+ return NextResponse3.json({ error, code, ...extra ?? {} }, { status });
702
+ }
703
+ function createInvitationSendHandlers(opts) {
704
+ const send = {
705
+ POST: async (request) => {
706
+ const session = await opts.auth();
707
+ if (!session?.user) return jsonError(401, "unauthorized", "unauthorized");
708
+ const caller = await opts.getOrCreateAppUser(session);
709
+ if (!opts.canInvite(caller.role)) {
710
+ return jsonError(403, "forbidden", "forbidden");
711
+ }
712
+ let body;
713
+ try {
714
+ body = await request.json();
715
+ } catch {
716
+ return jsonError(400, "invalid JSON body", "validation");
717
+ }
718
+ const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "";
719
+ const role = typeof body.role === "string" ? body.role.trim() : "";
720
+ const parentIdRaw = body.parent_id;
721
+ if (!EMAIL_RE.test(email) || email.length > 255) {
722
+ return jsonError(400, "invalid email", "validation");
723
+ }
724
+ if (!role || role.length > 30 || !ROLE_RE.test(role)) {
725
+ return jsonError(400, "role must be snake_case lowercase", "validation");
726
+ }
727
+ if (opts.allowedRoles && !opts.allowedRoles.includes(role)) {
728
+ return jsonError(
729
+ 400,
730
+ `role must be one of: ${opts.allowedRoles.join(", ")}`,
731
+ "validation"
732
+ );
733
+ }
734
+ let parentOverride = null;
735
+ if (parentIdRaw !== void 0 && parentIdRaw !== null) {
736
+ if (typeof parentIdRaw !== "string" || !PARENT_RE.test(parentIdRaw)) {
737
+ return jsonError(
738
+ 400,
739
+ "parent_id must be a stringified BigInt or null",
740
+ "validation"
741
+ );
742
+ }
743
+ parentOverride = BigInt(parentIdRaw);
744
+ }
745
+ const parent_id = opts.resolveInviteScope(caller, parentOverride);
746
+ const db = await opts.getDb();
747
+ const existing = await db.user.findUnique({ where: { email } });
748
+ if (existing) {
749
+ return jsonError(409, "user with that email already exists", "conflict");
750
+ }
751
+ const token = opts.generateInvitationToken();
752
+ const expiresAt = opts.invitationExpiresAt();
753
+ const invitation = await db.invitation.create({
754
+ data: {
755
+ token,
756
+ email,
757
+ inviter_id: caller.id,
758
+ intended_role: role,
759
+ parent_id,
760
+ expires_at: expiresAt
761
+ }
762
+ });
763
+ const origin = opts.appDomainEnv ? `https://${opts.appDomainEnv}` : new URL(request.url).origin;
764
+ const invitationUrl = opts.buildInvitationUrl(token, origin);
765
+ const rendered = opts.renderInvitationEmail({
766
+ inviterName: caller.name,
767
+ inviteeEmail: email,
768
+ intendedRole: role,
769
+ invitationUrl,
770
+ expiresAt,
771
+ appDisplayName: opts.appDisplayName
772
+ });
773
+ try {
774
+ await opts.sendEmail({
775
+ to: email,
776
+ subject: rendered.subject,
777
+ html: rendered.html,
778
+ text: rendered.text
779
+ });
780
+ } catch (err) {
781
+ const message = err instanceof Error ? err.message : String(err);
782
+ return NextResponse3.json(
783
+ {
784
+ invitationId: invitation.id.toString(),
785
+ expiresAt: invitation.expires_at.toISOString(),
786
+ warning: `invitation saved but email send failed: ${message}`
787
+ },
788
+ { status: 202 }
789
+ );
790
+ }
791
+ return NextResponse3.json(
792
+ {
793
+ invitationId: invitation.id.toString(),
794
+ expiresAt: invitation.expires_at.toISOString()
795
+ },
796
+ { status: 201 }
797
+ );
798
+ }
799
+ };
800
+ return { send };
801
+ }
606
802
  export {
607
803
  AuthError,
608
804
  IMPERSONATE_COOKIE_NAME,
609
805
  IMPERSONATE_TTL_SECONDS,
610
- TENANT_GLOBAL_KEY,
611
- TenantBootScript,
612
806
  createAuth,
613
807
  createGetOrCreateAppUser,
614
808
  createImpersonateHandlers,
615
809
  createInvitationHandlers,
810
+ createInvitationSendHandlers,
616
811
  createMeHandler,
812
+ createSettingsHandlers,
617
813
  getUserGroups,
618
814
  hasGroup,
619
- loadTenantConfig,
620
815
  mintImpersonationToken,
621
- publicSubset,
622
816
  requireGroup,
623
817
  verifyImpersonationToken
624
818
  };