@instroc/auth 1.1.1 → 1.3.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.
@@ -1,5 +1,8 @@
1
1
  // src/forms/use-login-form.ts
2
- import { useState as useState2, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect2 } from "react";
2
+ import { useState as useState3, useCallback as useCallback2, useRef as useRef2, useEffect as useEffect3 } from "react";
3
+
4
+ // src/use-auth.ts
5
+ import { useEffect as useEffect2, useState as useState2 } from "react";
3
6
 
4
7
  // src/provider.tsx
5
8
  import {
@@ -29,7 +32,7 @@ function authErrorFromResponse(response, data, fallbackMessage) {
29
32
 
30
33
  // src/version.ts
31
34
  var SDK_NAME = "instroc-auth";
32
- var SDK_VERSION = true ? "1.1.1" : "0.0.0-dev";
35
+ var SDK_VERSION = true ? "1.3.0" : "0.0.0-dev";
33
36
  function withSdkHeader(init) {
34
37
  const headers = new Headers(init?.headers ?? {});
35
38
  headers.set("X-Instroc-SDK", `${SDK_NAME}/${SDK_VERSION}`);
@@ -42,7 +45,7 @@ var AuthContext = createContext(null);
42
45
  var STORAGE_KEY = "instroc_auth_session";
43
46
  var WARNING_SESSION_KEY = "instroc_auth_sdk_warning_seen";
44
47
  function noteDeprecationWarning(response) {
45
- const warning = response.headers.get("X-Instroc-SDK-Warning");
48
+ const warning = response.headers?.get?.("X-Instroc-SDK-Warning");
46
49
  if (!warning)
47
50
  return;
48
51
  if (typeof window === "undefined")
@@ -596,6 +599,67 @@ function useAuthRequired() {
596
599
  }
597
600
  return { user, session, loading: false };
598
601
  }
602
+ function useIsOwner() {
603
+ const { user, session, loading } = useAuthContext();
604
+ if (loading || !user || !session)
605
+ return false;
606
+ if (user.is_owner === true)
607
+ return true;
608
+ const claim = decodeIsOwnerClaim(session.access_token);
609
+ return claim === true;
610
+ }
611
+ function useIsWorkspaceMember() {
612
+ const [isMember, setIsMember] = useState2(() => isDevHost());
613
+ useEffect2(() => {
614
+ if (typeof window === "undefined")
615
+ return;
616
+ let cancelled = false;
617
+ (async () => {
618
+ try {
619
+ const res = await fetch("/__instroc/workspace-status", {
620
+ credentials: "include",
621
+ cache: "no-store"
622
+ });
623
+ if (cancelled)
624
+ return;
625
+ if (!res.ok) {
626
+ setIsMember(isDevHost());
627
+ return;
628
+ }
629
+ const data = await res.json();
630
+ setIsMember(data.isWorkspaceMember === true);
631
+ } catch {
632
+ if (!cancelled)
633
+ setIsMember(isDevHost());
634
+ }
635
+ })();
636
+ return () => {
637
+ cancelled = true;
638
+ };
639
+ }, []);
640
+ return isMember;
641
+ }
642
+ function isDevHost() {
643
+ if (typeof window === "undefined")
644
+ return false;
645
+ const h = window.location.hostname;
646
+ return h === "localhost" || h === "127.0.0.1" || h.endsWith(".fly.dev") || h.endsWith(".workers.dev");
647
+ }
648
+ function decodeIsOwnerClaim(accessToken) {
649
+ try {
650
+ const parts = accessToken.split(".");
651
+ if (parts.length !== 3)
652
+ return void 0;
653
+ const payload = parts[1];
654
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
655
+ const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
656
+ const json = typeof atob !== "undefined" ? atob(padded) : Buffer.from(padded, "base64").toString("utf-8");
657
+ const decoded = JSON.parse(json);
658
+ return decoded.is_owner;
659
+ } catch {
660
+ return void 0;
661
+ }
662
+ }
599
663
 
600
664
  // src/forms/use-form-action.ts
601
665
  function resolveErrorMessage(err, messages, fallback) {
@@ -623,10 +687,10 @@ function applyErrorResult(err, onError, messages, fallback, setError) {
623
687
  function useLoginForm(options = {}) {
624
688
  const { login, user } = useAuth();
625
689
  const { onSuccess, onNeedsVerification, onError, messages } = options;
626
- const [loading, setLoading] = useState2(false);
627
- const [error, setError] = useState2(null);
690
+ const [loading, setLoading] = useState3(false);
691
+ const [error, setError] = useState3(null);
628
692
  const mountedRef = useRef2(true);
629
- useEffect2(() => {
693
+ useEffect3(() => {
630
694
  mountedRef.current = true;
631
695
  return () => {
632
696
  mountedRef.current = false;
@@ -678,14 +742,14 @@ function useLoginForm(options = {}) {
678
742
  }
679
743
 
680
744
  // src/forms/use-signup-form.ts
681
- import { useState as useState3, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect3 } from "react";
745
+ import { useState as useState4, useCallback as useCallback3, useRef as useRef3, useEffect as useEffect4 } from "react";
682
746
  function useSignupForm(options = {}) {
683
747
  const { signup } = useAuth();
684
748
  const { onSuccess, onNeedsVerification, onNeedsApproval, onError, messages } = options;
685
- const [loading, setLoading] = useState3(false);
686
- const [error, setError] = useState3(null);
749
+ const [loading, setLoading] = useState4(false);
750
+ const [error, setError] = useState4(null);
687
751
  const mountedRef = useRef3(true);
688
- useEffect3(() => {
752
+ useEffect4(() => {
689
753
  mountedRef.current = true;
690
754
  return () => {
691
755
  mountedRef.current = false;
@@ -742,7 +806,7 @@ function useSignupForm(options = {}) {
742
806
  }
743
807
 
744
808
  // src/forms/use-otp-form.ts
745
- import { useState as useState4, useCallback as useCallback4, useRef as useRef4, useEffect as useEffect4 } from "react";
809
+ import { useState as useState5, useCallback as useCallback4, useRef as useRef4, useEffect as useEffect5 } from "react";
746
810
  function useOtpForm(options) {
747
811
  const { verifyOTP, resendOTP } = useAuth();
748
812
  const {
@@ -753,18 +817,18 @@ function useOtpForm(options) {
753
817
  messages,
754
818
  resendCooldownSeconds = 60
755
819
  } = options;
756
- const [loading, setLoading] = useState4(false);
757
- const [resending, setResending] = useState4(false);
758
- const [resendCooldown, setResendCooldown] = useState4(0);
759
- const [error, setError] = useState4(null);
820
+ const [loading, setLoading] = useState5(false);
821
+ const [resending, setResending] = useState5(false);
822
+ const [resendCooldown, setResendCooldown] = useState5(0);
823
+ const [error, setError] = useState5(null);
760
824
  const mountedRef = useRef4(true);
761
- useEffect4(() => {
825
+ useEffect5(() => {
762
826
  mountedRef.current = true;
763
827
  return () => {
764
828
  mountedRef.current = false;
765
829
  };
766
830
  }, []);
767
- useEffect4(() => {
831
+ useEffect5(() => {
768
832
  if (resendCooldown <= 0)
769
833
  return;
770
834
  const id = setTimeout(() => setResendCooldown((s) => s - 1), 1e3);
@@ -846,15 +910,15 @@ function useOtpForm(options) {
846
910
  }
847
911
 
848
912
  // src/forms/use-forgot-password-form.ts
849
- import { useState as useState5, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect5 } from "react";
913
+ import { useState as useState6, useCallback as useCallback5, useRef as useRef5, useEffect as useEffect6 } from "react";
850
914
  function useForgotPasswordForm(options = {}) {
851
915
  const { forgotPassword } = useAuth();
852
916
  const { onSuccess, onError, messages } = options;
853
- const [loading, setLoading] = useState5(false);
854
- const [submitted, setSubmitted] = useState5(false);
855
- const [error, setError] = useState5(null);
917
+ const [loading, setLoading] = useState6(false);
918
+ const [submitted, setSubmitted] = useState6(false);
919
+ const [error, setError] = useState6(null);
856
920
  const mountedRef = useRef5(true);
857
- useEffect5(() => {
921
+ useEffect6(() => {
858
922
  mountedRef.current = true;
859
923
  return () => {
860
924
  mountedRef.current = false;
@@ -909,14 +973,14 @@ function useForgotPasswordForm(options = {}) {
909
973
  }
910
974
 
911
975
  // src/forms/use-reset-password-form.ts
912
- import { useState as useState6, useCallback as useCallback6, useRef as useRef6, useEffect as useEffect6 } from "react";
976
+ import { useState as useState7, useCallback as useCallback6, useRef as useRef6, useEffect as useEffect7 } from "react";
913
977
  function useResetPasswordForm(options) {
914
978
  const { resetPassword } = useAuth();
915
979
  const { token, onSuccess, onError, messages } = options;
916
- const [loading, setLoading] = useState6(false);
917
- const [error, setError] = useState6(null);
980
+ const [loading, setLoading] = useState7(false);
981
+ const [error, setError] = useState7(null);
918
982
  const mountedRef = useRef6(true);
919
- useEffect6(() => {
983
+ useEffect7(() => {
920
984
  mountedRef.current = true;
921
985
  return () => {
922
986
  mountedRef.current = false;
@@ -962,6 +1026,8 @@ export {
962
1026
  useUser,
963
1027
  useSession,
964
1028
  useAuthRequired,
1029
+ useIsOwner,
1030
+ useIsWorkspaceMember,
965
1031
  useLoginForm,
966
1032
  useSignupForm,
967
1033
  useOtpForm,
package/dist/forms.d.ts CHANGED
@@ -1 +1 @@
1
- export { M as MessageOverrides, O as OnErrorResult, m as UseForgotPasswordFormOptions, n as UseForgotPasswordFormReturn, U as UseLoginFormOptions, h as UseLoginFormReturn, k as UseOtpFormOptions, l as UseOtpFormReturn, o as UseResetPasswordFormOptions, p as UseResetPasswordFormReturn, i as UseSignupFormOptions, j as UseSignupFormReturn, f as useForgotPasswordForm, u as useLoginForm, e as useOtpForm, g as useResetPasswordForm, d as useSignupForm } from './index-D4rCSC9H.js';
1
+ export { M as MessageOverrides, O as OnErrorResult, m as UseForgotPasswordFormOptions, n as UseForgotPasswordFormReturn, U as UseLoginFormOptions, h as UseLoginFormReturn, k as UseOtpFormOptions, l as UseOtpFormReturn, o as UseResetPasswordFormOptions, p as UseResetPasswordFormReturn, i as UseSignupFormOptions, j as UseSignupFormReturn, f as useForgotPasswordForm, u as useLoginForm, e as useOtpForm, g as useResetPasswordForm, d as useSignupForm } from './index-iglVgSQI.js';
package/dist/forms.js CHANGED
@@ -4,7 +4,7 @@ import {
4
4
  useOtpForm,
5
5
  useResetPasswordForm,
6
6
  useSignupForm
7
- } from "./chunk-T7NGX4DQ.js";
7
+ } from "./chunk-WPSPVFVM.js";
8
8
  export {
9
9
  useForgotPasswordForm,
10
10
  useLoginForm,
@@ -6,6 +6,13 @@ interface AuthUser {
6
6
  avatar_url: string | null;
7
7
  metadata: Record<string, unknown>;
8
8
  created_at: string;
9
+ /**
10
+ * True for the project owner (first user or explicitly designated). Mirrors
11
+ * the `is_owner` JWT claim used for RLS bypass on the server. Templates
12
+ * gate admin UIs (/admin, /studio) on this field so only the owner sees
13
+ * CRUD controls in production.
14
+ */
15
+ is_owner?: boolean;
9
16
  }
10
17
  interface AuthSession {
11
18
  access_token: string;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { A as AuthProviderProps, a as AuthContextValue, b as AuthUser, c as AuthSession } from './index-D4rCSC9H.js';
2
- export { r as AuthConfig, w as AuthResponse, q as AuthState, L as LoginCredentials, M as MessageOverrides, s as OAuthProvider, O as OnErrorResult, R as RefreshResponse, S as SignupCredentials, t as SignupResult, v as UpdateProfileData, m as UseForgotPasswordFormOptions, n as UseForgotPasswordFormReturn, U as UseLoginFormOptions, h as UseLoginFormReturn, k as UseOtpFormOptions, l as UseOtpFormReturn, o as UseResetPasswordFormOptions, p as UseResetPasswordFormReturn, i as UseSignupFormOptions, j as UseSignupFormReturn, V as VisibilityConfig, f as useForgotPasswordForm, u as useLoginForm, e as useOtpForm, g as useResetPasswordForm, d as useSignupForm } from './index-D4rCSC9H.js';
1
+ import { A as AuthProviderProps, a as AuthContextValue, b as AuthUser, c as AuthSession } from './index-iglVgSQI.js';
2
+ export { r as AuthConfig, w as AuthResponse, q as AuthState, L as LoginCredentials, M as MessageOverrides, s as OAuthProvider, O as OnErrorResult, R as RefreshResponse, S as SignupCredentials, t as SignupResult, v as UpdateProfileData, m as UseForgotPasswordFormOptions, n as UseForgotPasswordFormReturn, U as UseLoginFormOptions, h as UseLoginFormReturn, k as UseOtpFormOptions, l as UseOtpFormReturn, o as UseResetPasswordFormOptions, p as UseResetPasswordFormReturn, i as UseSignupFormOptions, j as UseSignupFormReturn, V as VisibilityConfig, f as useForgotPasswordForm, u as useLoginForm, e as useOtpForm, g as useResetPasswordForm, d as useSignupForm } from './index-iglVgSQI.js';
3
3
 
4
4
  declare function AuthProvider({ children, projectId: initialProjectId, baseUrl, persistSession, onAuthStateChange, }: AuthProviderProps): JSX.Element;
5
5
  declare function useAuthContext(): AuthContextValue;
@@ -22,6 +22,47 @@ declare function useAuthRequired(): {
22
22
  session: AuthSession | null;
23
23
  loading: boolean;
24
24
  };
25
+ /**
26
+ * Returns true if the current user is the project owner. Used by templates
27
+ * to gate admin/studio routes.
28
+ *
29
+ * Reads `user.is_owner` first (populated by the `/me` endpoint). Falls back
30
+ * to decoding the access token's `is_owner` claim so the check is accurate
31
+ * immediately after login (the login response doesn't echo is_owner — only
32
+ * `/me` does, and that only runs on mount).
33
+ *
34
+ * Returns `false` while loading or when unauthenticated.
35
+ */
36
+ declare function useIsOwner(): boolean;
37
+ /**
38
+ * Minimal JWT payload decoder — no signature check (the server already
39
+ * verified it). Returns undefined if the token is malformed. We only read
40
+ * the `is_owner` boolean claim, which the BaaS auth signer sets from
41
+ * `_users.is_owner`.
42
+ */
43
+ /**
44
+ * Returns true if the current visitor is a member of the Instroc workspace
45
+ * that owns this published app. Used by premium templates to gate owner-only
46
+ * UI such as `/admin` or `/studio` routes.
47
+ *
48
+ * This is distinct from {@link useIsOwner} — workspace members are the humans
49
+ * who built the project (via Instroc), whereas app users (BaaS `_users`) are
50
+ * the end users of the published app. The two populations never overlap, so
51
+ * `useIsOwner` always returns `false` for regular app visitors. Admin UI on
52
+ * premium templates should gate on this hook instead.
53
+ *
54
+ * Implementation: calls `GET /__instroc/workspace-status` on the subdomain
55
+ * router, which validates the `instroc_wa` cookie (HttpOnly, set after
56
+ * Instroc workspace login) against the project's `workspaceId`. The same
57
+ * cookie is what the privacy gate uses to allow workspace members into
58
+ * private apps.
59
+ *
60
+ * Dev/preview note: when running on `localhost`, `*.fly.dev`, or
61
+ * `*.workers.dev` (dev-session HMR, editor preview), the hook returns `true`
62
+ * so template authors can see admin UI while building. In production
63
+ * (`*.instroc.app` and custom domains) the server answer is authoritative.
64
+ */
65
+ declare function useIsWorkspaceMember(): boolean;
25
66
 
26
67
  /**
27
68
  * Typed error thrown by every `@instroc/auth` method when the server responds
@@ -54,4 +95,4 @@ declare class AuthError extends Error {
54
95
  constructor(message: string, status: number, code?: string);
55
96
  }
56
97
 
57
- export { AuthContextValue, AuthError, AuthProvider, AuthProviderProps, AuthSession, AuthUser, useAuth, useAuthContext, useAuthRequired, useSession, useUser };
98
+ export { AuthContextValue, AuthError, AuthProvider, AuthProviderProps, AuthSession, AuthUser, useAuth, useAuthContext, useAuthRequired, useIsOwner, useIsWorkspaceMember, useSession, useUser };
package/dist/index.js CHANGED
@@ -5,13 +5,15 @@ import {
5
5
  useAuthContext,
6
6
  useAuthRequired,
7
7
  useForgotPasswordForm,
8
+ useIsOwner,
9
+ useIsWorkspaceMember,
8
10
  useLoginForm,
9
11
  useOtpForm,
10
12
  useResetPasswordForm,
11
13
  useSession,
12
14
  useSignupForm,
13
15
  useUser
14
- } from "./chunk-T7NGX4DQ.js";
16
+ } from "./chunk-WPSPVFVM.js";
15
17
  export {
16
18
  AuthError,
17
19
  AuthProvider,
@@ -19,6 +21,8 @@ export {
19
21
  useAuthContext,
20
22
  useAuthRequired,
21
23
  useForgotPasswordForm,
24
+ useIsOwner,
25
+ useIsWorkspaceMember,
22
26
  useLoginForm,
23
27
  useOtpForm,
24
28
  useResetPasswordForm,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instroc/auth",
3
- "version": "1.1.1",
3
+ "version": "1.3.0",
4
4
  "description": "Authentication hooks for Instroc Cloud — useAuth, useUser, AuthProvider, and headless form hooks",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",