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 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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "or3-provider-basic-auth",
3
3
  "configKey": "or3-provider-basic-auth",
4
- "version": "0.0.11",
4
+ "version": "0.0.12",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -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 = "Email is already in use.";
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
- if (mode === "invite_only" && !body.inviteToken?.trim()) {
72
+ const inviteToken = body.inviteToken ?? getInviteTokenFromEvent(event);
73
+ if (mode === "invite_only" && !inviteToken) {
72
74
  mapRegistrationErrorToHttp("invite_required");
73
75
  }
74
- if (mode === "invite_only") {
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: body.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 (mode === "invite_only" && body.inviteToken?.trim()) {
133
- setCookie(event, "or3_invite_token", body.inviteToken.trim(), {
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "or3-provider-basic-auth",
3
- "version": "0.0.11",
3
+ "version": "0.0.12",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Saluana/or3-provider-basic-auth.git"