or3-provider-basic-auth 0.0.10 → 0.0.12
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/module.json +1 -1
- package/dist/runtime/components/BasicAuthLockPage.client.vue +16 -1
- package/dist/runtime/components/BasicAuthRegisterModal.client.d.vue.ts +4 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue +13 -5
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue.d.ts +4 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.d.vue.ts +1 -0
- package/dist/runtime/components/BasicAuthSignInModal.client.vue +4 -2
- package/dist/runtime/components/BasicAuthSignInModal.client.vue.d.ts +1 -0
- package/dist/runtime/components/SidebarAuthButtonBasic.client.vue +17 -2
- package/dist/runtime/lib/invite-link.client.d.ts +4 -0
- package/dist/runtime/lib/invite-link.client.js +39 -0
- package/dist/runtime/server/api/basic-auth/register.post.js +7 -5
- package/dist/runtime/server/api/basic-auth/sign-in.post.js +18 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ Strict-mode behavior:
|
|
|
69
69
|
- Canonical OR3 users/workspaces are still resolved by the selected `AuthWorkspaceStore`.
|
|
70
70
|
- In `invite_only` mode, signed-token, expiry, persisted invite state/token hash, and normalized-email validation runs before Basic Auth creates an account or session.
|
|
71
71
|
- The selected `AuthWorkspaceStore` must support atomic invite provisioning; internal user/auth mapping, membership, and invite consumption are committed together by that provider.
|
|
72
|
+
- Invite links (`/?invite=TOKEN`, copied from Admin → Workspaces, or carried in the lock page's `next`) open registration for signed-out visitors with the token filled in. The token is removed from the address bar on first read and kept in memory for that page; only one mounted auth control (sidebar, mobile More sheet or lock page) opens registration per link. On mobile the account control lives in More, so registration opens when the visitor opens More. This invite-link handoff requires published `or3-provider-basic-auth@0.0.11` or later.
|
|
72
73
|
- Basic Auth account and initial refresh-session rows are created in one local SQLite transaction.
|
|
73
74
|
- Access JWTs are short-lived and validated by `basicAuthProvider.getSession(event)`.
|
|
74
75
|
- Refresh tokens are rotated and hashed at rest; replay attempts revoke active sessions.
|
|
@@ -154,3 +155,11 @@ provider's SQLite driver. If the source launcher selects Bun for sync, this
|
|
|
154
155
|
native binding must also work under the project-pinned Bun version. The launcher
|
|
155
156
|
checks it in that runtime before starting Nuxt and reports incompatible bindings
|
|
156
157
|
or runtime crashes. Managed Cloud uses Node with `better-sqlite3`.
|
|
158
|
+
|
|
159
|
+
## Instance signup invitations
|
|
160
|
+
|
|
161
|
+
The updated host distinguishes signup invites from existing team invites. Signup invites create a private workspace with only the recipient as owner; team invites retain their selected workspace and role. This requires the updated host and rebuilt provider artifacts, rather than the existing published release alone.
|
|
162
|
+
|
|
163
|
+
Registration preflight rejects invalid invites before credential creation, including in open mode. It resolves the explicit body token first, followed by the host's header, HttpOnly cookie, query, and Referer precedence. A valid token is retained in the invite cookie for canonical session resolution; an explicit replacement overrides a stale cookie. Without a token, open registration remains available. If credentials commit before canonical acceptance, use Already registered? Sign in with the same account and a valid or replacement invite. The sign-in request preserves manually pasted tokens, verifies the signed-in email, and uses the existing HttpOnly invite cookie. The selected sync store still owns atomic acceptance; provider-only credentials grant no protected OR3 access. No credential lifecycle or recovery service is added.
|
|
164
|
+
|
|
165
|
+
Provider-only tests use the exact host registration and token sources pinned in `tests/fixtures/host`, with SHA-256 provenance verified by Vitest. No sibling checkout is required. To exercise the current host integration, run `OR3_CANONICAL_HOST_ROOT=/absolute/path/to/or3-chat bun run test`. Refresh the pinned files and provenance together when adopting a reviewed host contract change; do not replace the host validation logic with a provider-local implementation.
|
package/dist/module.json
CHANGED
|
@@ -17,24 +17,33 @@
|
|
|
17
17
|
|
|
18
18
|
<BasicAuthSignInModal
|
|
19
19
|
v-model="signInModalOpen"
|
|
20
|
+
:invite-token="inviteToken"
|
|
20
21
|
@signed-in="handleSignedIn"
|
|
21
22
|
@open-register="openRegisterFromSignIn"
|
|
22
23
|
/>
|
|
23
24
|
|
|
24
25
|
<BasicAuthRegisterModal
|
|
25
26
|
v-model="registerModalOpen"
|
|
27
|
+
:invite-token="inviteToken"
|
|
28
|
+
@open-sign-in="openSignInFromRegister"
|
|
26
29
|
@registered="handleRegistered"
|
|
27
30
|
/>
|
|
28
31
|
</div>
|
|
29
32
|
</template>
|
|
30
33
|
|
|
31
34
|
<script setup>
|
|
32
|
-
import { nextTick, ref } from "vue";
|
|
35
|
+
import { nextTick, onMounted, ref } from "vue";
|
|
33
36
|
import BasicAuthRegisterModal from "./BasicAuthRegisterModal.client.vue";
|
|
34
37
|
import BasicAuthSignInModal from "./BasicAuthSignInModal.client.vue";
|
|
38
|
+
import { claimInviteLinkAutoOpen, readInviteLinkToken } from "../lib/invite-link.client";
|
|
35
39
|
const loginIcon = "i-lucide-log-in";
|
|
36
40
|
const signInModalOpen = ref(false);
|
|
37
41
|
const registerModalOpen = ref(false);
|
|
42
|
+
const inviteToken = ref(null);
|
|
43
|
+
onMounted(() => {
|
|
44
|
+
inviteToken.value = readInviteLinkToken();
|
|
45
|
+
if (claimInviteLinkAutoOpen()) registerModalOpen.value = true;
|
|
46
|
+
});
|
|
38
47
|
function notifyAuthSessionChanged() {
|
|
39
48
|
if (typeof window === "undefined") return;
|
|
40
49
|
window.dispatchEvent(new CustomEvent("or3:auth-session-changed"));
|
|
@@ -52,4 +61,10 @@ async function openRegisterFromSignIn() {
|
|
|
52
61
|
registerModalOpen.value = true;
|
|
53
62
|
}, 0);
|
|
54
63
|
}
|
|
64
|
+
async function openSignInFromRegister(token) {
|
|
65
|
+
inviteToken.value = token || inviteToken.value;
|
|
66
|
+
registerModalOpen.value = false;
|
|
67
|
+
await nextTick();
|
|
68
|
+
signInModalOpen.value = true;
|
|
69
|
+
}
|
|
55
70
|
</script>
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
|
+
/** Token from an invite link; filled in when the form opens. */
|
|
4
|
+
inviteToken?: string | null;
|
|
3
5
|
};
|
|
4
6
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
7
|
"update:modelValue": (value: boolean) => any;
|
|
6
8
|
registered: () => any;
|
|
9
|
+
"open-sign-in": (inviteToken: string) => any;
|
|
7
10
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
11
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
9
12
|
onRegistered?: (() => any) | undefined;
|
|
13
|
+
"onOpen-sign-in"?: ((inviteToken: string) => any) | undefined;
|
|
10
14
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
15
|
export default _default;
|
|
@@ -73,7 +73,10 @@
|
|
|
73
73
|
/>
|
|
74
74
|
</UFormField>
|
|
75
75
|
|
|
76
|
-
<UFormField
|
|
76
|
+
<UFormField
|
|
77
|
+
:label="props.inviteToken ? 'Invite token (from your invite link)' : 'Invite token (optional)'"
|
|
78
|
+
name="inviteToken"
|
|
79
|
+
>
|
|
77
80
|
<UInput
|
|
78
81
|
v-model="state.inviteToken"
|
|
79
82
|
type="text"
|
|
@@ -90,6 +93,7 @@
|
|
|
90
93
|
{{ errorMessage }}
|
|
91
94
|
</div>
|
|
92
95
|
|
|
96
|
+
<UButton type="button" color="neutral" variant="ghost" :disabled="pending" @click="emit('open-sign-in', state.inviteToken)">Already registered? Sign in</UButton>
|
|
93
97
|
<div class="flex justify-end gap-2 pt-1">
|
|
94
98
|
<UButton
|
|
95
99
|
color="neutral"
|
|
@@ -111,16 +115,17 @@
|
|
|
111
115
|
</template>
|
|
112
116
|
|
|
113
117
|
<script setup>
|
|
114
|
-
import { computed, reactive, ref } from "vue";
|
|
118
|
+
import { computed, reactive, ref, watch } from "vue";
|
|
115
119
|
import { newPasswordFitsBcrypt } from "../password-policy";
|
|
116
120
|
const modalUi = {
|
|
117
121
|
overlay: "basic-auth-modal-overlay",
|
|
118
122
|
content: "basic-auth-modal-content sm:max-w-[420px]"
|
|
119
123
|
};
|
|
120
124
|
const props = defineProps({
|
|
121
|
-
modelValue: { type: Boolean, required: true }
|
|
125
|
+
modelValue: { type: Boolean, required: true },
|
|
126
|
+
inviteToken: { type: [String, null], required: false }
|
|
122
127
|
});
|
|
123
|
-
const emit = defineEmits(["update:modelValue", "registered"]);
|
|
128
|
+
const emit = defineEmits(["update:modelValue", "registered", "open-sign-in"]);
|
|
124
129
|
const state = reactive({
|
|
125
130
|
email: "",
|
|
126
131
|
displayName: "",
|
|
@@ -136,6 +141,9 @@ const isOpen = computed({
|
|
|
136
141
|
emit("update:modelValue", value);
|
|
137
142
|
}
|
|
138
143
|
});
|
|
144
|
+
watch(() => [props.modelValue, props.inviteToken], ([open, token]) => {
|
|
145
|
+
if (open && token && !state.inviteToken.trim()) state.inviteToken = token;
|
|
146
|
+
}, { immediate: true });
|
|
139
147
|
function close() {
|
|
140
148
|
errorMessage.value = "";
|
|
141
149
|
emit("update:modelValue", false);
|
|
@@ -194,7 +202,7 @@ async function onSubmit() {
|
|
|
194
202
|
} else if (statusCode === 403) {
|
|
195
203
|
errorMessage.value = serverMessage || "Registration is restricted for this instance.";
|
|
196
204
|
} else if (statusCode === 401) {
|
|
197
|
-
errorMessage.value = "
|
|
205
|
+
errorMessage.value = "Unable to register. If you already registered, sign in to continue.";
|
|
198
206
|
} else if (statusCode === 409) {
|
|
199
207
|
errorMessage.value = "Email is already in use.";
|
|
200
208
|
} else {
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
|
+
/** Token from an invite link; filled in when the form opens. */
|
|
4
|
+
inviteToken?: string | null;
|
|
3
5
|
};
|
|
4
6
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
7
|
"update:modelValue": (value: boolean) => any;
|
|
6
8
|
registered: () => any;
|
|
9
|
+
"open-sign-in": (inviteToken: string) => any;
|
|
7
10
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
8
11
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
9
12
|
onRegistered?: (() => any) | undefined;
|
|
13
|
+
"onOpen-sign-in"?: ((inviteToken: string) => any) | undefined;
|
|
10
14
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
11
15
|
export default _default;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
|
+
inviteToken?: string;
|
|
3
4
|
};
|
|
4
5
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
6
|
"update:modelValue": (value: boolean) => any;
|
|
@@ -89,7 +89,8 @@ const modalUi = {
|
|
|
89
89
|
content: "basic-auth-modal-content sm:max-w-[380px]"
|
|
90
90
|
};
|
|
91
91
|
const props = defineProps({
|
|
92
|
-
modelValue: { type: Boolean, required: true }
|
|
92
|
+
modelValue: { type: Boolean, required: true },
|
|
93
|
+
inviteToken: { type: String, required: false }
|
|
93
94
|
});
|
|
94
95
|
const emit = defineEmits(["update:modelValue", "signed-in", "open-register"]);
|
|
95
96
|
const state = reactive({
|
|
@@ -116,7 +117,8 @@ async function onSubmit() {
|
|
|
116
117
|
method: "POST",
|
|
117
118
|
body: {
|
|
118
119
|
email: state.email,
|
|
119
|
-
password: state.password
|
|
120
|
+
password: state.password,
|
|
121
|
+
inviteToken: props.inviteToken?.trim() || void 0
|
|
120
122
|
}
|
|
121
123
|
});
|
|
122
124
|
emit("signed-in");
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
type __VLS_Props = {
|
|
2
2
|
modelValue: boolean;
|
|
3
|
+
inviteToken?: string;
|
|
3
4
|
};
|
|
4
5
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
6
|
"update:modelValue": (value: boolean) => any;
|
|
@@ -51,12 +51,15 @@
|
|
|
51
51
|
|
|
52
52
|
<BasicAuthSignInModal
|
|
53
53
|
v-model="signInModalOpen"
|
|
54
|
+
:invite-token="inviteToken"
|
|
54
55
|
@signed-in="onSignedIn"
|
|
55
56
|
@open-register="openRegisterFromSignIn"
|
|
56
57
|
/>
|
|
57
58
|
|
|
58
59
|
<BasicAuthRegisterModal
|
|
59
60
|
v-model="registerModalOpen"
|
|
61
|
+
:invite-token="inviteToken"
|
|
62
|
+
@open-sign-in="openSignInFromRegister"
|
|
60
63
|
@registered="onRegistered"
|
|
61
64
|
/>
|
|
62
65
|
|
|
@@ -76,6 +79,7 @@ import {
|
|
|
76
79
|
AUTH_UI_LAYOUT_KEY
|
|
77
80
|
} from "../lib/auth-ui-layout";
|
|
78
81
|
import { silentRefreshOnce } from "../lib/silent-refresh.client";
|
|
82
|
+
import { claimInviteLinkAutoOpen, readInviteLinkToken } from "../lib/invite-link.client";
|
|
79
83
|
import BasicAuthSignInModal from "./BasicAuthSignInModal.client.vue";
|
|
80
84
|
import BasicAuthRegisterModal from "./BasicAuthRegisterModal.client.vue";
|
|
81
85
|
import BasicAuthUserMenu from "./BasicAuthUserMenu.client.vue";
|
|
@@ -171,9 +175,20 @@ async function onRegistered() {
|
|
|
171
175
|
await refreshSession({ allowSilentRefresh: false });
|
|
172
176
|
notifyAuthSessionChanged();
|
|
173
177
|
}
|
|
174
|
-
|
|
175
|
-
|
|
178
|
+
const inviteToken = ref(null);
|
|
179
|
+
onMounted(async () => {
|
|
180
|
+
inviteToken.value = readInviteLinkToken();
|
|
181
|
+
await refreshSession();
|
|
182
|
+
if (!isSignedIn.value && isBasicAuthProvider.value && claimInviteLinkAutoOpen()) {
|
|
183
|
+
registerModalOpen.value = true;
|
|
184
|
+
}
|
|
176
185
|
});
|
|
186
|
+
async function openSignInFromRegister(token) {
|
|
187
|
+
inviteToken.value = token || inviteToken.value;
|
|
188
|
+
registerModalOpen.value = false;
|
|
189
|
+
await nextTick();
|
|
190
|
+
signInModalOpen.value = true;
|
|
191
|
+
}
|
|
177
192
|
</script>
|
|
178
193
|
|
|
179
194
|
<style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
let token;
|
|
2
|
+
let autoOpenClaimed = false;
|
|
3
|
+
function takeInvite(url) {
|
|
4
|
+
const direct = url.searchParams.get("invite")?.trim();
|
|
5
|
+
if (direct) {
|
|
6
|
+
url.searchParams.delete("invite");
|
|
7
|
+
return direct;
|
|
8
|
+
}
|
|
9
|
+
const next = url.searchParams.get("next");
|
|
10
|
+
if (!next) return null;
|
|
11
|
+
const target = new URL(next, url.origin);
|
|
12
|
+
const carried = target.searchParams.get("invite")?.trim();
|
|
13
|
+
if (!carried) return null;
|
|
14
|
+
target.searchParams.delete("invite");
|
|
15
|
+
url.searchParams.set("next", target.pathname + target.search + target.hash);
|
|
16
|
+
return carried;
|
|
17
|
+
}
|
|
18
|
+
export function readInviteLinkToken() {
|
|
19
|
+
if (token !== void 0) return token;
|
|
20
|
+
token = null;
|
|
21
|
+
if (typeof window === "undefined") return token;
|
|
22
|
+
try {
|
|
23
|
+
const url = new URL(window.location.href);
|
|
24
|
+
token = takeInvite(url);
|
|
25
|
+
if (token) window.history.replaceState(window.history.state, "", url.pathname + url.search + url.hash);
|
|
26
|
+
} catch {
|
|
27
|
+
token = null;
|
|
28
|
+
}
|
|
29
|
+
return token;
|
|
30
|
+
}
|
|
31
|
+
export function claimInviteLinkAutoOpen() {
|
|
32
|
+
if (!readInviteLinkToken() || autoOpenClaimed) return false;
|
|
33
|
+
autoOpenClaimed = true;
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
export function resetInviteLinkForTests() {
|
|
37
|
+
token = void 0;
|
|
38
|
+
autoOpenClaimed = false;
|
|
39
|
+
}
|
|
@@ -4,6 +4,7 @@ import { createError, defineEventHandler, setCookie } from "h3";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { getAuthWorkspaceStore } from "~~/server/auth/store/registry";
|
|
6
6
|
import {
|
|
7
|
+
getInviteTokenFromEvent,
|
|
7
8
|
resolveRegistrationMode,
|
|
8
9
|
validateInviteRegistration
|
|
9
10
|
} from "~~/server/auth/registration";
|
|
@@ -68,10 +69,11 @@ export async function handleRegister(event) {
|
|
|
68
69
|
if (mode === "disabled") {
|
|
69
70
|
mapRegistrationErrorToHttp("disabled");
|
|
70
71
|
}
|
|
71
|
-
|
|
72
|
+
const inviteToken = body.inviteToken ?? getInviteTokenFromEvent(event);
|
|
73
|
+
if (mode === "invite_only" && !inviteToken) {
|
|
72
74
|
mapRegistrationErrorToHttp("invite_required");
|
|
73
75
|
}
|
|
74
|
-
if (
|
|
76
|
+
if (inviteToken) {
|
|
75
77
|
const syncConfig = runtimeConfig;
|
|
76
78
|
const storeId = syncConfig.sync?.provider ?? syncConfig.public?.sync?.provider ?? "convex";
|
|
77
79
|
const store = getAuthWorkspaceStore(storeId);
|
|
@@ -83,7 +85,7 @@ export async function handleRegister(event) {
|
|
|
83
85
|
store,
|
|
84
86
|
mode,
|
|
85
87
|
email,
|
|
86
|
-
inviteToken
|
|
88
|
+
inviteToken
|
|
87
89
|
});
|
|
88
90
|
if (!decision.allowed) {
|
|
89
91
|
mapRegistrationErrorToHttp(decision.reason);
|
|
@@ -129,8 +131,8 @@ export async function handleRegister(event) {
|
|
|
129
131
|
});
|
|
130
132
|
setAccessCookie(event, accessToken, config.accessTtlSeconds);
|
|
131
133
|
setRefreshCookie(event, refreshToken, config.refreshTtlSeconds);
|
|
132
|
-
if (
|
|
133
|
-
setCookie(event, "or3_invite_token",
|
|
134
|
+
if (inviteToken) {
|
|
135
|
+
setCookie(event, "or3_invite_token", inviteToken, {
|
|
134
136
|
httpOnly: true,
|
|
135
137
|
sameSite: "lax",
|
|
136
138
|
secure: process.env.NODE_ENV === "production",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { defineEventHandler } from "h3";
|
|
2
|
+
import { defineEventHandler, setCookie } from "h3";
|
|
3
|
+
import { useRuntimeConfig } from "#imports";
|
|
4
|
+
import { verifyInviteToken } from "~~/server/auth/invite-token";
|
|
3
5
|
import { z } from "zod";
|
|
4
6
|
import { clearAuthCookies, setAccessCookie, setRefreshCookie } from "../../lib/cookies.js";
|
|
5
7
|
import { getBasicAuthConfig } from "../../lib/config.js";
|
|
@@ -21,7 +23,8 @@ import { assertBasicAuthReady, noStore, parseBodyWithSchema } from "./_helpers.j
|
|
|
21
23
|
const signInSchema = z.object({
|
|
22
24
|
email: z.string().email().max(320),
|
|
23
25
|
// Sign-in must allow legacy short passwords; strength is enforced at set/change time.
|
|
24
|
-
password: z.string().min(1).max(512)
|
|
26
|
+
password: z.string().min(1).max(512),
|
|
27
|
+
inviteToken: z.string().trim().min(1).max(4096).optional()
|
|
25
28
|
});
|
|
26
29
|
const DUMMY_PASSWORD_HASH = "$2a$12$1XCXpgmbzURDdc0AGJiAsemtT39PAw8WwV7fBOq6A5VQv6.qN6Va.";
|
|
27
30
|
export async function handleSignIn(event) {
|
|
@@ -68,6 +71,19 @@ export async function handleSignIn(event) {
|
|
|
68
71
|
});
|
|
69
72
|
setAccessCookie(event, accessToken, config.accessTtlSeconds);
|
|
70
73
|
setRefreshCookie(event, refreshToken, config.refreshTtlSeconds);
|
|
74
|
+
const secret = useRuntimeConfig().auth?.invite?.tokenSecret;
|
|
75
|
+
if (input.inviteToken && secret) {
|
|
76
|
+
const verified = verifyInviteToken(input.inviteToken, secret);
|
|
77
|
+
if (verified.ok && normalizeEmail(verified.payload.email) === email) {
|
|
78
|
+
setCookie(event, "or3_invite_token", input.inviteToken, {
|
|
79
|
+
httpOnly: true,
|
|
80
|
+
sameSite: "lax",
|
|
81
|
+
secure: process.env.NODE_ENV === "production",
|
|
82
|
+
path: "/",
|
|
83
|
+
maxAge: Math.min(600, verified.payload.exp - Math.floor(Date.now() / 1e3))
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
71
87
|
return { ok: true };
|
|
72
88
|
}
|
|
73
89
|
export default defineEventHandler(async (event) => {
|