or3-provider-basic-auth 0.0.11 → 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 +8 -0
- package/dist/module.json +1 -1
- package/dist/runtime/components/BasicAuthLockPage.client.vue +8 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.d.vue.ts +2 -0
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue +3 -2
- package/dist/runtime/components/BasicAuthRegisterModal.client.vue.d.ts +2 -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 +8 -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
|
@@ -155,3 +155,11 @@ provider's SQLite driver. If the source launcher selects Bun for sync, this
|
|
|
155
155
|
native binding must also work under the project-pinned Bun version. The launcher
|
|
156
156
|
checks it in that runtime before starting Nuxt and reports incompatible bindings
|
|
157
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,6 +17,7 @@
|
|
|
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
|
/>
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
<BasicAuthRegisterModal
|
|
25
26
|
v-model="registerModalOpen"
|
|
26
27
|
:invite-token="inviteToken"
|
|
28
|
+
@open-sign-in="openSignInFromRegister"
|
|
27
29
|
@registered="handleRegistered"
|
|
28
30
|
/>
|
|
29
31
|
</div>
|
|
@@ -59,4 +61,10 @@ async function openRegisterFromSignIn() {
|
|
|
59
61
|
registerModalOpen.value = true;
|
|
60
62
|
}, 0);
|
|
61
63
|
}
|
|
64
|
+
async function openSignInFromRegister(token) {
|
|
65
|
+
inviteToken.value = token || inviteToken.value;
|
|
66
|
+
registerModalOpen.value = false;
|
|
67
|
+
await nextTick();
|
|
68
|
+
signInModalOpen.value = true;
|
|
69
|
+
}
|
|
62
70
|
</script>
|
|
@@ -6,8 +6,10 @@ type __VLS_Props = {
|
|
|
6
6
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
7
7
|
"update:modelValue": (value: boolean) => any;
|
|
8
8
|
registered: () => any;
|
|
9
|
+
"open-sign-in": (inviteToken: string) => any;
|
|
9
10
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
11
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
11
12
|
onRegistered?: (() => any) | undefined;
|
|
13
|
+
"onOpen-sign-in"?: ((inviteToken: string) => any) | undefined;
|
|
12
14
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
15
|
export default _default;
|
|
@@ -93,6 +93,7 @@
|
|
|
93
93
|
{{ errorMessage }}
|
|
94
94
|
</div>
|
|
95
95
|
|
|
96
|
+
<UButton type="button" color="neutral" variant="ghost" :disabled="pending" @click="emit('open-sign-in', state.inviteToken)">Already registered? Sign in</UButton>
|
|
96
97
|
<div class="flex justify-end gap-2 pt-1">
|
|
97
98
|
<UButton
|
|
98
99
|
color="neutral"
|
|
@@ -124,7 +125,7 @@ const props = defineProps({
|
|
|
124
125
|
modelValue: { type: Boolean, required: true },
|
|
125
126
|
inviteToken: { type: [String, null], required: false }
|
|
126
127
|
});
|
|
127
|
-
const emit = defineEmits(["update:modelValue", "registered"]);
|
|
128
|
+
const emit = defineEmits(["update:modelValue", "registered", "open-sign-in"]);
|
|
128
129
|
const state = reactive({
|
|
129
130
|
email: "",
|
|
130
131
|
displayName: "",
|
|
@@ -201,7 +202,7 @@ async function onSubmit() {
|
|
|
201
202
|
} else if (statusCode === 403) {
|
|
202
203
|
errorMessage.value = serverMessage || "Registration is restricted for this instance.";
|
|
203
204
|
} else if (statusCode === 401) {
|
|
204
|
-
errorMessage.value = "
|
|
205
|
+
errorMessage.value = "Unable to register. If you already registered, sign in to continue.";
|
|
205
206
|
} else if (statusCode === 409) {
|
|
206
207
|
errorMessage.value = "Email is already in use.";
|
|
207
208
|
} else {
|
|
@@ -6,8 +6,10 @@ type __VLS_Props = {
|
|
|
6
6
|
declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
7
7
|
"update:modelValue": (value: boolean) => any;
|
|
8
8
|
registered: () => any;
|
|
9
|
+
"open-sign-in": (inviteToken: string) => any;
|
|
9
10
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
10
11
|
"onUpdate:modelValue"?: ((value: boolean) => any) | undefined;
|
|
11
12
|
onRegistered?: (() => any) | undefined;
|
|
13
|
+
"onOpen-sign-in"?: ((inviteToken: string) => any) | undefined;
|
|
12
14
|
}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
|
|
13
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,6 +51,7 @@
|
|
|
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
|
/>
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
<BasicAuthRegisterModal
|
|
59
60
|
v-model="registerModalOpen"
|
|
60
61
|
:invite-token="inviteToken"
|
|
62
|
+
@open-sign-in="openSignInFromRegister"
|
|
61
63
|
@registered="onRegistered"
|
|
62
64
|
/>
|
|
63
65
|
|
|
@@ -181,6 +183,12 @@ onMounted(async () => {
|
|
|
181
183
|
registerModalOpen.value = true;
|
|
182
184
|
}
|
|
183
185
|
});
|
|
186
|
+
async function openSignInFromRegister(token) {
|
|
187
|
+
inviteToken.value = token || inviteToken.value;
|
|
188
|
+
registerModalOpen.value = false;
|
|
189
|
+
await nextTick();
|
|
190
|
+
signInModalOpen.value = true;
|
|
191
|
+
}
|
|
184
192
|
</script>
|
|
185
193
|
|
|
186
194
|
<style>
|
|
@@ -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) => {
|