@spfn/auth 0.3.0-beta.20 → 0.3.0-beta.22
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/README.md +131 -3
- package/dist/client-proof.js +5 -3
- package/dist/client-proof.js.map +1 -1
- package/dist/config.d.ts +40 -0
- package/dist/config.js +19 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +28 -2
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +19 -2
- package/dist/index.js +21 -1
- package/dist/index.js.map +1 -1
- package/dist/{machine-principals-BD4tnASp.d.ts → machine-principals-B7N8gux0.d.ts} +355 -7
- package/dist/nextjs/server.d.ts +107 -1
- package/dist/nextjs/server.js +182 -0
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +506 -268
- package/dist/server.js +1405 -950
- package/dist/server.js.map +1 -1
- package/migrations/20260918142743_smart_cassandra_nova/migration.sql +2 -0
- package/migrations/20260918142743_smart_cassandra_nova/snapshot.json +5297 -0
- package/migrations/20260918143800_fat_princess_powerful/migration.sql +17 -0
- package/migrations/20260918143800_fat_princess_powerful/snapshot.json +5523 -0
- package/package.json +1 -1
package/dist/nextjs/server.js
CHANGED
|
@@ -778,6 +778,187 @@ function createOAuth2AuthorizeHandlers(options) {
|
|
|
778
778
|
}
|
|
779
779
|
return { GET, POST };
|
|
780
780
|
}
|
|
781
|
+
|
|
782
|
+
// src/nextjs/revoke-all-page-handlers.ts
|
|
783
|
+
import { cookies as cookies4 } from "next/headers.js";
|
|
784
|
+
import { NextResponse as NextResponse3 } from "next/server";
|
|
785
|
+
import { authApi as authApi3 } from "@spfn/auth";
|
|
786
|
+
import { logger as logger4 } from "@spfn/core/logger";
|
|
787
|
+
var CSRF_COOKIE = "spfn_revoke_all_csrf";
|
|
788
|
+
var CSRF_TTL_SECONDS = 15 * 60;
|
|
789
|
+
var FORM_CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
790
|
+
function screen2(status, body) {
|
|
791
|
+
return new NextResponse3(body, {
|
|
792
|
+
status,
|
|
793
|
+
headers: {
|
|
794
|
+
"Content-Type": "text/html; charset=utf-8",
|
|
795
|
+
"Content-Security-Policy": "frame-ancestors 'none'",
|
|
796
|
+
"Cache-Control": "no-store"
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
function refusalScreen2(status, heading, message) {
|
|
801
|
+
return screen2(status, [
|
|
802
|
+
"<!DOCTYPE html>",
|
|
803
|
+
'<html lang="en"><head><meta charset="utf-8"><title>Sign out everywhere</title></head>',
|
|
804
|
+
`<body><h1>${heading}</h1><p>${message}</p></body></html>`
|
|
805
|
+
].join("\n"));
|
|
806
|
+
}
|
|
807
|
+
function missingTokenScreen() {
|
|
808
|
+
return refusalScreen2(
|
|
809
|
+
400,
|
|
810
|
+
"Incomplete link",
|
|
811
|
+
"This address is missing the part that identifies the request. Open the link from your email again, in full."
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
function unavailableScreen2() {
|
|
815
|
+
return refusalScreen2(
|
|
816
|
+
500,
|
|
817
|
+
"Sign-out unavailable",
|
|
818
|
+
"This link could not be checked. Nothing was changed. Please try again."
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
function unverifiedScreen() {
|
|
822
|
+
return refusalScreen2(
|
|
823
|
+
403,
|
|
824
|
+
"Request could not be verified",
|
|
825
|
+
"This form did not carry the token the page set for it. Open the link from your email again and press the button on the page it opens."
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
function unsupportedScreen() {
|
|
829
|
+
return refusalScreen2(
|
|
830
|
+
415,
|
|
831
|
+
"Unsupported request",
|
|
832
|
+
"This page is answered for a browser form. Open the link from your email again."
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
function hiddenFields2(fields, csrfToken) {
|
|
836
|
+
return [...Object.entries(fields), ["csrf", csrfToken]].map(([name, value]) => `<input type="hidden" name="${escapeHtml(name)}" value="${escapeHtml(value)}">`).join("\n ");
|
|
837
|
+
}
|
|
838
|
+
function confirmBody(view) {
|
|
839
|
+
return `<h1>Sign out everywhere</h1>
|
|
840
|
+
<p>This will sign out <strong>${view.activeKeyCount}</strong> signed-in device(s), including
|
|
841
|
+
this one. You will need to sign in again afterwards.</p>
|
|
842
|
+
<p>The link stops working at <time datetime="${escapeHtml(view.expiresAt ?? "")}"
|
|
843
|
+
>${escapeHtml(view.expiresAt ?? "")}</time>.</p>
|
|
844
|
+
<form method="post">
|
|
845
|
+
${hiddenFields2(view.fields, view.csrfToken)}
|
|
846
|
+
<button type="submit">Sign out every device</button>
|
|
847
|
+
</form>`;
|
|
848
|
+
}
|
|
849
|
+
function stageBody(view) {
|
|
850
|
+
if (view.stage === "confirm") {
|
|
851
|
+
return confirmBody(view);
|
|
852
|
+
}
|
|
853
|
+
if (view.stage === "done") {
|
|
854
|
+
return `<h1>Signed out</h1>
|
|
855
|
+
<p>${view.revokedCount} device(s) signed out. Sign in again to carry on.</p>`;
|
|
856
|
+
}
|
|
857
|
+
return `<h1>Link no longer valid</h1>
|
|
858
|
+
<p>This link cannot be used. Ask for a new one, and open the most recent email you were sent.</p>`;
|
|
859
|
+
}
|
|
860
|
+
function defaultRender2(view) {
|
|
861
|
+
return `<!DOCTYPE html>
|
|
862
|
+
<html lang="en">
|
|
863
|
+
<head><meta charset="utf-8"><title>Sign out everywhere</title></head>
|
|
864
|
+
<body>
|
|
865
|
+
${stageBody(view)}
|
|
866
|
+
</body>
|
|
867
|
+
</html>`;
|
|
868
|
+
}
|
|
869
|
+
function stageView(stage, revokedCount) {
|
|
870
|
+
return { stage, revokedCount, fields: {}, csrfToken: "" };
|
|
871
|
+
}
|
|
872
|
+
function statusOf2(thrown) {
|
|
873
|
+
const error = thrown;
|
|
874
|
+
return Number(error?.status ?? error?.statusCode ?? 0);
|
|
875
|
+
}
|
|
876
|
+
function answerRefusal2(thrown, render) {
|
|
877
|
+
if (statusOf2(thrown) === 404) {
|
|
878
|
+
return screen2(404, render(stageView("invalid")));
|
|
879
|
+
}
|
|
880
|
+
logger4.error("Revoke-all link could not be answered", { status: statusOf2(thrown) });
|
|
881
|
+
return unavailableScreen2();
|
|
882
|
+
}
|
|
883
|
+
function mintCsrfToken() {
|
|
884
|
+
return Array.from(crypto.getRandomValues(new Uint8Array(32))).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
885
|
+
}
|
|
886
|
+
function setCsrfCookie(response, csrfToken, path) {
|
|
887
|
+
response.cookies.set(CSRF_COOKIE, csrfToken, {
|
|
888
|
+
httpOnly: true,
|
|
889
|
+
secure: process.env.NODE_ENV === "production",
|
|
890
|
+
sameSite: "strict",
|
|
891
|
+
path,
|
|
892
|
+
maxAge: CSRF_TTL_SECONDS
|
|
893
|
+
});
|
|
894
|
+
return response;
|
|
895
|
+
}
|
|
896
|
+
function clearCsrfCookie(response, path) {
|
|
897
|
+
response.cookies.delete({ name: CSRF_COOKIE, path });
|
|
898
|
+
return response;
|
|
899
|
+
}
|
|
900
|
+
async function renderConfirm(request, token, render) {
|
|
901
|
+
const described = await authApi3.confirmRevokeAllLink.call({ body: { token } });
|
|
902
|
+
const csrfToken = mintCsrfToken();
|
|
903
|
+
const response = screen2(200, render({
|
|
904
|
+
stage: "confirm",
|
|
905
|
+
expiresAt: described.expiresAt,
|
|
906
|
+
activeKeyCount: described.activeKeyCount,
|
|
907
|
+
fields: { token },
|
|
908
|
+
csrfToken
|
|
909
|
+
}));
|
|
910
|
+
return setCsrfCookie(response, csrfToken, request.nextUrl.pathname);
|
|
911
|
+
}
|
|
912
|
+
async function refuseUnverifiedPost2(form) {
|
|
913
|
+
const presented = form.get("csrf");
|
|
914
|
+
const expected = (await cookies4()).get(CSRF_COOKIE)?.value;
|
|
915
|
+
if (!expected || !matchesCsrfToken(expected, typeof presented === "string" ? presented : null)) {
|
|
916
|
+
return unverifiedScreen();
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
function isFormPost2(request) {
|
|
921
|
+
return (request.headers.get("content-type") ?? "").startsWith(FORM_CONTENT_TYPE);
|
|
922
|
+
}
|
|
923
|
+
async function consume(token, render) {
|
|
924
|
+
const { revokedCount } = await authApi3.consumeRevokeAllLink.call({ body: { token } });
|
|
925
|
+
return screen2(200, render(stageView("done", revokedCount)));
|
|
926
|
+
}
|
|
927
|
+
function createRevokeAllPageHandlers(options = {}) {
|
|
928
|
+
const render = options.render ?? defaultRender2;
|
|
929
|
+
async function GET(request) {
|
|
930
|
+
const token = request.nextUrl.searchParams.get("token");
|
|
931
|
+
if (!token) {
|
|
932
|
+
return missingTokenScreen();
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
return await renderConfirm(request, token, render);
|
|
936
|
+
} catch (error) {
|
|
937
|
+
return answerRefusal2(error, render);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
async function POST(request) {
|
|
941
|
+
if (!isFormPost2(request)) {
|
|
942
|
+
return unsupportedScreen();
|
|
943
|
+
}
|
|
944
|
+
const form = await request.formData();
|
|
945
|
+
const refusal = await refuseUnverifiedPost2(form);
|
|
946
|
+
if (refusal) {
|
|
947
|
+
return refusal;
|
|
948
|
+
}
|
|
949
|
+
const path = request.nextUrl.pathname;
|
|
950
|
+
const token = form.get("token");
|
|
951
|
+
if (typeof token !== "string" || !token) {
|
|
952
|
+
return clearCsrfCookie(missingTokenScreen(), path);
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
return clearCsrfCookie(await consume(token, render), path);
|
|
956
|
+
} catch (error) {
|
|
957
|
+
return clearCsrfCookie(answerRefusal2(error, render), path);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
return { GET, POST };
|
|
961
|
+
}
|
|
781
962
|
export {
|
|
782
963
|
RequireAuth,
|
|
783
964
|
RequirePermission,
|
|
@@ -787,6 +968,7 @@ export {
|
|
|
787
968
|
clearSessionCookies,
|
|
788
969
|
createOAuth2AuthorizeHandlers,
|
|
789
970
|
createOAuthCallbackHandler,
|
|
971
|
+
createRevokeAllPageHandlers,
|
|
790
972
|
escapeHtml,
|
|
791
973
|
getAuthSessionData,
|
|
792
974
|
getPendingSession,
|