@synqauth/nextjs 0.0.0 → 0.2.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/README.md +9 -0
- package/dist/{config-BNh2Aznt.d.ts → config-CpEbAGEs.d.ts} +11 -0
- package/dist/define.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/proxy.d.ts +1 -1
- package/dist/proxy.js +1 -1
- package/dist/{sign-in-error-DSCgrk0b.js → sign-in-error-kd56GHt_.js} +198 -9
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -54,6 +54,15 @@ export const config = { matcher: ['/((?!_next/|.*\\..*).*)'] }
|
|
|
54
54
|
|
|
55
55
|
To add a way to sign in to someone's account (Google, a wallet, anything the brand offers), POST `{ method, returnTo }` as JSON to `/api/auth/link` from your own pages and send the browser to the `redirect` it answers, or call `useAuth().link('google')` from `@synqauth/react`: Synq asks them to sign in again, then adds it.
|
|
56
56
|
|
|
57
|
+
To sign people in on your own page instead of the brand's (AERO-1411), turn on own-UI sign-in for the app in the dashboard and use `<SignInForm>` or `useSignIn()` from `@synqauth/react`. They post each step to `/api/auth/sign-in/flow`, which takes it to Synq as your app. Pass `clientAddress` to `defineSynqConfig` when your host doesn't set `x-real-ip`, so Synq can limit tries per address. Read a header your own proxy sets, never one the browser could send:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
export const synqConfig = defineSynqConfig({
|
|
61
|
+
// Behind Cloudflare
|
|
62
|
+
clientAddress: (request) => request.headers.get('cf-connecting-ip') ?? undefined,
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
57
66
|
In a server component, `await synq.me()` is the signed-in person or null, and `await synq.getAccessToken()` is a token for your API (`resource`). Link `synq.signInPath({ returnTo: '/dashboard' })` to sign in, and sign out with a form that POSTs to `synq.signOutPath`.
|
|
58
67
|
|
|
59
68
|
## When a sign-in doesn't finish
|
|
@@ -173,6 +173,15 @@ interface SynqAuthOptions {
|
|
|
173
173
|
plugins?: readonly SynqPlugin[];
|
|
174
174
|
/** The `fetch` to call Synq with. */
|
|
175
175
|
fetch?: typeof fetch;
|
|
176
|
+
/**
|
|
177
|
+
* Where the person signing in on the app's own screens is
|
|
178
|
+
* (`<basePath>/sign-in/flow`, AERO-1411), as this server saw them: Synq
|
|
179
|
+
* counts its per-address limits for the app with it (`Synq-End-User-IP`).
|
|
180
|
+
* `x-real-ip` by default, which Vercel sets; give one that reads your
|
|
181
|
+
* proxy's header otherwise. Return undefined, and per-address protection
|
|
182
|
+
* is off for the app.
|
|
183
|
+
*/
|
|
184
|
+
clientAddress?: (request: Request) => string | undefined;
|
|
176
185
|
}
|
|
177
186
|
interface SynqConfig {
|
|
178
187
|
readonly issuer: string;
|
|
@@ -194,6 +203,8 @@ interface SynqConfig {
|
|
|
194
203
|
readonly refreshLeewaySeconds: number;
|
|
195
204
|
readonly plugins: readonly SynqPlugin[];
|
|
196
205
|
readonly fetch: typeof fetch | undefined;
|
|
206
|
+
/** Where a person signing in on the app's own screens is: `x-real-ip` by default. */
|
|
207
|
+
readonly clientAddress: (request: Request) => string | undefined;
|
|
197
208
|
/** Over https: `Secure` cookies with the `__Host-` prefix. */
|
|
198
209
|
readonly secure: boolean;
|
|
199
210
|
}
|
package/dist/define.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as SynqAuthOptions } from "./config-
|
|
1
|
+
import { t as SynqAuthOptions } from "./config-CpEbAGEs.js";
|
|
2
2
|
//#region src/define.d.ts
|
|
3
3
|
/** Declares the app's Synq configuration: `SYNQ_*` environment variables fill in whatever it leaves out. */
|
|
4
4
|
export declare function defineSynqConfig<const T extends SynqAuthOptions>(options: T): T;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as KeyValueSessionStoreOptions, c as keyValueSessionStore, d as SynqPluginContext, f as SynqPluginRoute, i as KeyValue, l as memorySessionStore, m as defineSynqPlugin, n as SynqConfig, o as SessionValues, p as SynqUser, r as resolveConfig, s as SynqSessionStore, t as SynqAuthOptions, u as SynqPlugin } from "./config-
|
|
1
|
+
import { a as KeyValueSessionStoreOptions, c as keyValueSessionStore, d as SynqPluginContext, f as SynqPluginRoute, i as KeyValue, l as memorySessionStore, m as defineSynqPlugin, n as SynqConfig, o as SessionValues, p as SynqUser, r as resolveConfig, s as SynqSessionStore, t as SynqAuthOptions, u as SynqPlugin } from "./config-CpEbAGEs.js";
|
|
2
2
|
import { defineSynqConfig } from "./define.js";
|
|
3
3
|
import { cookies } from "next/headers.js";
|
|
4
4
|
//#region src/cookies.d.ts
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as memorySessionStore, d as normalizePath, f as resolveConfig, l as defineSynqPlugin, n as signInErrorMessage, o as createSynqCore, r as signInErrorOf, s as keyValueSessionStore, t as SIGN_IN_ERROR_PARAM } from "./sign-in-error-
|
|
1
|
+
import { c as memorySessionStore, d as normalizePath, f as resolveConfig, l as defineSynqPlugin, n as signInErrorMessage, o as createSynqCore, r as signInErrorOf, s as keyValueSessionStore, t as SIGN_IN_ERROR_PARAM } from "./sign-in-error-kd56GHt_.js";
|
|
2
2
|
import { defineSynqConfig } from "./define.js";
|
|
3
3
|
import { cookies } from "next/headers.js";
|
|
4
4
|
import { cache } from "react";
|
package/dist/proxy.d.ts
CHANGED
package/dist/proxy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as withoutSignInError, f as resolveConfig, i as signInErrorPage, o as createSynqCore, r as signInErrorOf, t as SIGN_IN_ERROR_PARAM, u as requestJar } from "./sign-in-error-
|
|
1
|
+
import { a as withoutSignInError, f as resolveConfig, i as signInErrorPage, o as createSynqCore, r as signInErrorOf, t as SIGN_IN_ERROR_PARAM, u as requestJar } from "./sign-in-error-kd56GHt_.js";
|
|
2
2
|
import { NextResponse } from "next/server.js";
|
|
3
3
|
//#region src/proxy.ts
|
|
4
4
|
/**
|
|
@@ -126,6 +126,7 @@ function resolveConfig(options = {}, env = processEnv()) {
|
|
|
126
126
|
refreshLeewaySeconds: options.refreshLeewaySeconds ?? 30,
|
|
127
127
|
plugins: options.plugins ?? [],
|
|
128
128
|
fetch: options.fetch,
|
|
129
|
+
clientAddress: options.clientAddress ?? ((request) => request.headers.get("x-real-ip")?.trim() || void 0),
|
|
129
130
|
secure: appUrl.protocol === "https:"
|
|
130
131
|
};
|
|
131
132
|
}
|
|
@@ -598,6 +599,10 @@ const TRANSACTION_SECONDS = 3600;
|
|
|
598
599
|
const TRANSACTION_PREFIX = "synq.tx.";
|
|
599
600
|
const ACCOUNT_BODY_LIMIT = 65536;
|
|
600
601
|
const LINK_BODY_LIMIT = 4096;
|
|
602
|
+
/** A step of an own-UI sign-in: a passkey's assertion is the most it carries. */
|
|
603
|
+
const FLOW_BODY_LIMIT = 16384;
|
|
604
|
+
/** Where an own-UI sign-in's flow is kept, sealed: one a browser at a time. */
|
|
605
|
+
const FLOW_KEY = `${TRANSACTION_PREFIX}flow`;
|
|
601
606
|
/** `synq_siws` as @synqauth/solana's `encodeSiws` writes it: base64url, well under the body's limit. */
|
|
602
607
|
const SIWS = /^[A-Za-z0-9_-]{1,3072}$/;
|
|
603
608
|
/** Claims of the ID token that say nothing about the person. */
|
|
@@ -620,6 +625,8 @@ const PROTOCOL_CLAIMS = /* @__PURE__ */ new Set([
|
|
|
620
625
|
* `synqAuth()` instead.
|
|
621
626
|
*/
|
|
622
627
|
function createSynqCore(config) {
|
|
628
|
+
/** Whether the missing end-user address was logged already: once is enough. */
|
|
629
|
+
let warnedAboutAddress = false;
|
|
623
630
|
const keysFor = () => sealingKeys(config.sessionSecret, config.previousSessionSecrets);
|
|
624
631
|
/** Every request to Synq: never cached, and given up after REQUEST_TIMEOUT_MS. */
|
|
625
632
|
const upstream = (input, init = {}) => {
|
|
@@ -769,6 +776,7 @@ function createSynqCore(config) {
|
|
|
769
776
|
for (const { name } of started.slice(4)) jar.delete(name, { secure: config.secure });
|
|
770
777
|
}
|
|
771
778
|
async function signIn(request, jar) {
|
|
779
|
+
if (isPrefetch(request)) return new Response(null, { status: 204 });
|
|
772
780
|
const params = new URL(request.url).searchParams;
|
|
773
781
|
const returnTo = safeReturnTo(params.get("returnTo"), config.appUrl);
|
|
774
782
|
const prompt = params.get("prompt") ?? void 0;
|
|
@@ -789,7 +797,6 @@ function createSynqCore(config) {
|
|
|
789
797
|
}
|
|
790
798
|
async function callback(request, jar) {
|
|
791
799
|
const tx = await transactions(jar);
|
|
792
|
-
const previous = await load(jar);
|
|
793
800
|
const handle = newSession(config.sessionMaxAge, config.store);
|
|
794
801
|
try {
|
|
795
802
|
await client(handle.storage(config.primary, config.primary), config.primary, tx.storage).handleCallback(request.url);
|
|
@@ -797,14 +804,7 @@ function createSynqCore(config) {
|
|
|
797
804
|
const code = isSynqError(error) ? error.code : "server_error";
|
|
798
805
|
return redirect(withError(safeReturnTo(tx.found.returnTo ?? null, config.appUrl), code), 303);
|
|
799
806
|
}
|
|
800
|
-
|
|
801
|
-
const [before, after] = await Promise.all([issuerSessionOf(previous), issuerSessionOf(handle)]);
|
|
802
|
-
if (!(before !== void 0 && after !== void 0 && before.sub === after.sub && (before.sid === void 0 || after.sid === void 0 || before.sid === after.sid))) try {
|
|
803
|
-
await client(previous.storage(config.primary, config.primary), config.primary).signOut();
|
|
804
|
-
} catch {}
|
|
805
|
-
await endSession(jar, previous, config.secure, config.store);
|
|
806
|
-
}
|
|
807
|
-
await commit(jar, handle);
|
|
807
|
+
await startSession(jar, handle);
|
|
808
808
|
return redirect(safeReturnTo(tx.found.returnTo ?? null, config.appUrl), 303);
|
|
809
809
|
}
|
|
810
810
|
async function signOut(request, jar) {
|
|
@@ -904,6 +904,183 @@ function createSynqCore(config) {
|
|
|
904
904
|
params: { synq_siws: input.siws }
|
|
905
905
|
})).href });
|
|
906
906
|
}
|
|
907
|
+
/**
|
|
908
|
+
* One step of a sign-in on the app's own screens (AERO-1411): the page
|
|
909
|
+
* posts what the person typed, `{ step, … }`, and this server takes it to
|
|
910
|
+
* Synq's Authorization Challenge Endpoint as the app, with where the
|
|
911
|
+
* person is (`clientAddress`). The flow (its PKCE verifier and Synq's auth
|
|
912
|
+
* session) stays sealed in a cookie; the page hears only what to show
|
|
913
|
+
* next, never a token, a code or the auth session. A sign-in that ends
|
|
914
|
+
* starts the session as the callback does.
|
|
915
|
+
*/
|
|
916
|
+
async function signInFlow(request, jar) {
|
|
917
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") return problem(415, "unsupported_media_type", "Send the step as JSON");
|
|
918
|
+
const length = Number(request.headers.get("content-length") ?? 0);
|
|
919
|
+
if (request.headers.has("transfer-encoding")) return problem(411, "length_required", "Send the body with a Content-Length");
|
|
920
|
+
if (!Number.isFinite(length) || length > FLOW_BODY_LIMIT) return problem(413, "payload_too_large", "The body is too large");
|
|
921
|
+
let input;
|
|
922
|
+
try {
|
|
923
|
+
const body = await request.text();
|
|
924
|
+
if (new TextEncoder().encode(body).length > FLOW_BODY_LIMIT) return problem(413, "payload_too_large", "The body is too large");
|
|
925
|
+
input = JSON.parse(body);
|
|
926
|
+
if (!input || typeof input !== "object") throw new TypeError("not an object");
|
|
927
|
+
} catch {
|
|
928
|
+
return problem(400, "invalid_request", "Send { step, … } as JSON");
|
|
929
|
+
}
|
|
930
|
+
const text = (name, max = 512) => typeof input[name] === "string" ? input[name].slice(0, max) : "";
|
|
931
|
+
const step = text("step", 32);
|
|
932
|
+
const returnTo = safeReturnTo(text("returnTo", 2048) || null, config.appUrl);
|
|
933
|
+
const tx = await transactions(jar, returnTo);
|
|
934
|
+
const saved = await tx.storage.get(FLOW_KEY);
|
|
935
|
+
if (step === "restart") {
|
|
936
|
+
await tx.storage.remove(FLOW_KEY);
|
|
937
|
+
return json({ status: "restarted" });
|
|
938
|
+
}
|
|
939
|
+
const endUserIp = config.clientAddress(request);
|
|
940
|
+
if (!endUserIp && !warnedAboutAddress) {
|
|
941
|
+
warnedAboutAddress = true;
|
|
942
|
+
console.warn("@synqauth/nextjs: own-UI sign-in can't tell where people sign in from (no x-real-ip): give synqAuth() a clientAddress, or per-address protection is off for this app");
|
|
943
|
+
}
|
|
944
|
+
const handle = newSession(config.sessionMaxAge, config.store);
|
|
945
|
+
const synq = client(handle.storage(config.primary, config.primary), config.primary);
|
|
946
|
+
const flow = saved ? synq.resumeSignIn(saved, { endUserIp }) : synq.startSignIn({
|
|
947
|
+
endUserIp,
|
|
948
|
+
additionalResources: config.accountApi && config.accountApi !== config.primary ? [config.accountApi] : []
|
|
949
|
+
});
|
|
950
|
+
const email = text("email", 254);
|
|
951
|
+
let result;
|
|
952
|
+
try {
|
|
953
|
+
switch (step) {
|
|
954
|
+
case "password":
|
|
955
|
+
result = text("screen") === "sign-up" ? await flow.password.signUp({
|
|
956
|
+
email,
|
|
957
|
+
password: text("password", 1024)
|
|
958
|
+
}) : await flow.password.signIn({
|
|
959
|
+
email,
|
|
960
|
+
password: text("password", 1024)
|
|
961
|
+
});
|
|
962
|
+
break;
|
|
963
|
+
case "email":
|
|
964
|
+
result = await flow.email.send(email);
|
|
965
|
+
break;
|
|
966
|
+
case "code":
|
|
967
|
+
result = flow.step === "verify-email" ? await flow.verifyEmail.verify(text("code", 32)) : await flow.email.verify(text("code", 32));
|
|
968
|
+
break;
|
|
969
|
+
case "resend":
|
|
970
|
+
result = flow.step === "verify-email" ? await flow.verifyEmail.resend() : await flow.email.resend();
|
|
971
|
+
break;
|
|
972
|
+
case "skip":
|
|
973
|
+
result = await flow.verifyEmail.skip();
|
|
974
|
+
break;
|
|
975
|
+
case "passkey-options":
|
|
976
|
+
result = await flow.passkey.options();
|
|
977
|
+
break;
|
|
978
|
+
case "passkey": {
|
|
979
|
+
const credential = input.credential;
|
|
980
|
+
if (!credential || typeof credential !== "object") return problem(400, "invalid_request", "credential is the passkey’s assertion");
|
|
981
|
+
result = await flow.passkey.signIn(credential, { related: input.related === true });
|
|
982
|
+
break;
|
|
983
|
+
}
|
|
984
|
+
default: return problem(400, "invalid_request", "step is password, email, code, resend, skip, passkey-options, passkey or restart");
|
|
985
|
+
}
|
|
986
|
+
} catch (error) {
|
|
987
|
+
if (isSynqError(error, "invalid_session")) {
|
|
988
|
+
await tx.storage.remove(FLOW_KEY);
|
|
989
|
+
return json({
|
|
990
|
+
status: "expired",
|
|
991
|
+
message: error.message
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
if (isSynqError(error, "too_many_requests")) return json({
|
|
995
|
+
status: "retry",
|
|
996
|
+
step: flow.step,
|
|
997
|
+
problem: "limited",
|
|
998
|
+
message: error.message,
|
|
999
|
+
...error.retryAfter === void 0 ? {} : { retryAfter: error.retryAfter }
|
|
1000
|
+
});
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
if (result.status === "signed-in") {
|
|
1004
|
+
await tx.storage.remove(FLOW_KEY);
|
|
1005
|
+
await startSession(jar, handle);
|
|
1006
|
+
return json({
|
|
1007
|
+
status: "signed-in",
|
|
1008
|
+
returnTo: tx.found.returnTo ?? returnTo
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
if (flow.step === "done") await tx.storage.remove(FLOW_KEY);
|
|
1012
|
+
else await tx.storage.set(FLOW_KEY, JSON.stringify(flow));
|
|
1013
|
+
switch (result.status) {
|
|
1014
|
+
case "code-sent": return json({
|
|
1015
|
+
status: result.status,
|
|
1016
|
+
step: flow.step,
|
|
1017
|
+
email: result.email,
|
|
1018
|
+
expiresAt: result.expiresAt.toISOString()
|
|
1019
|
+
});
|
|
1020
|
+
case "verify-email": return json({
|
|
1021
|
+
status: result.status,
|
|
1022
|
+
step: flow.step,
|
|
1023
|
+
email: result.email,
|
|
1024
|
+
canSkip: result.canSkip
|
|
1025
|
+
});
|
|
1026
|
+
case "passkey": return json({
|
|
1027
|
+
status: result.status,
|
|
1028
|
+
step: flow.step,
|
|
1029
|
+
passkey: {
|
|
1030
|
+
options: result.options,
|
|
1031
|
+
related: result.related
|
|
1032
|
+
},
|
|
1033
|
+
...result.problem ? {
|
|
1034
|
+
problem: result.problem,
|
|
1035
|
+
message: result.message
|
|
1036
|
+
} : {}
|
|
1037
|
+
});
|
|
1038
|
+
case "retry": return json({
|
|
1039
|
+
status: result.status,
|
|
1040
|
+
step: flow.step,
|
|
1041
|
+
problem: result.problem,
|
|
1042
|
+
message: result.message,
|
|
1043
|
+
...result.retryAfter === void 0 ? {} : { retryAfter: result.retryAfter }
|
|
1044
|
+
});
|
|
1045
|
+
case "hosted": return json({
|
|
1046
|
+
status: result.status,
|
|
1047
|
+
hosted: {
|
|
1048
|
+
reason: result.reason,
|
|
1049
|
+
...result.method ? { method: result.method } : {},
|
|
1050
|
+
...result.loginHint ? { loginHint: result.loginHint } : {}
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
case "refused": return json({
|
|
1054
|
+
status: result.status,
|
|
1055
|
+
reason: result.reason,
|
|
1056
|
+
message: result.message
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* Starts the session `handle` holds the new sign-in's tokens of. A
|
|
1062
|
+
* sign-in replaces whatever session the browser had, and its refresh
|
|
1063
|
+
* token is revoked: an offline grant outlives the brand's session. But
|
|
1064
|
+
* the same person signing in again within their session at the issuer,
|
|
1065
|
+
* as adding a passkey or a way in asks, gets the grant the replaced
|
|
1066
|
+
* session had, and Synq ends a grant with any refresh token of it
|
|
1067
|
+
* revoked: that one is dropped instead. Same person: the ID tokens'
|
|
1068
|
+
* `sub`. Same issuer session: their `sid`, which Synq includes for every
|
|
1069
|
+
* app (AERO-1624) but one whose tenant turned it off; without it, the
|
|
1070
|
+
* same `sub` is taken for the same session (a sign-in after it lapsed
|
|
1071
|
+
* leaves the old grant to expire).
|
|
1072
|
+
*/
|
|
1073
|
+
async function startSession(jar, handle) {
|
|
1074
|
+
const previous = await load(jar);
|
|
1075
|
+
if (previous) {
|
|
1076
|
+
const [before, after] = await Promise.all([issuerSessionOf(previous), issuerSessionOf(handle)]);
|
|
1077
|
+
if (!(before !== void 0 && after !== void 0 && before.sub === after.sub && (before.sid === void 0 || after.sid === void 0 || before.sid === after.sid))) try {
|
|
1078
|
+
await client(previous.storage(config.primary, config.primary), config.primary).signOut();
|
|
1079
|
+
} catch {}
|
|
1080
|
+
await endSession(jar, previous, config.secure, config.store);
|
|
1081
|
+
}
|
|
1082
|
+
await commit(jar, handle);
|
|
1083
|
+
}
|
|
907
1084
|
async function account(request, jar, path) {
|
|
908
1085
|
if (!config.accountApi) return problem(404, "not_found", "The session holds no account API token: synqAuth()'s account is off");
|
|
909
1086
|
if (!/^v1(\/[A-Za-z0-9._~-]+)+$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) return problem(404, "not_found", "No such account API route");
|
|
@@ -972,6 +1149,7 @@ function createSynqCore(config) {
|
|
|
972
1149
|
try {
|
|
973
1150
|
if (rest === "sign-in" && request.method === "GET") response = await signIn(request, jar);
|
|
974
1151
|
else if (rest === "sign-in" && request.method === "POST") response = await walletSignIn(request, jar);
|
|
1152
|
+
else if (rest === "sign-in/flow" && request.method === "POST") response = await signInFlow(request, jar);
|
|
975
1153
|
else if (rest === "callback" && request.method === "GET") response = await callback(request, jar);
|
|
976
1154
|
else if (rest === "sign-out" && request.method === "POST") response = await signOut(request, jar);
|
|
977
1155
|
else if (rest === "link" && request.method === "POST") response = await link(request, jar);
|
|
@@ -1015,6 +1193,17 @@ function createSynqCore(config) {
|
|
|
1015
1193
|
};
|
|
1016
1194
|
}
|
|
1017
1195
|
/**
|
|
1196
|
+
* Whether a request is a prefetch, not a person going there: Next.js's
|
|
1197
|
+
* router (`Next-Router-Prefetch`) and browsers' speculative loads (`Purpose`,
|
|
1198
|
+
* `Sec-Purpose`: prefetch or prerender).
|
|
1199
|
+
*/
|
|
1200
|
+
function isPrefetch(request) {
|
|
1201
|
+
const h = request.headers;
|
|
1202
|
+
if (h.get("next-router-prefetch") !== null) return true;
|
|
1203
|
+
const purpose = `${h.get("purpose") ?? ""} ${h.get("sec-purpose") ?? ""}`.toLowerCase();
|
|
1204
|
+
return /\b(prefetch|prerender)\b/.test(purpose);
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1018
1207
|
* Where to send the browser back to on the app, as an absolute URL, never
|
|
1019
1208
|
* another site: the app's root for anything else. It checks where the value
|
|
1020
1209
|
* resolves to, not how it's spelled, since dot segments and encodings
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@synqauth/nextjs",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Synq for Next.js: a backend-for-frontend session, so the browser never holds a Synq token.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"access": "public"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@synqauth/js": "0.
|
|
42
|
+
"@synqauth/js": "0.2.0"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"next": ">=16.0.0",
|