@adventurelabs/scout-core 1.4.74 → 1.4.76

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.
Files changed (34) hide show
  1. package/dist/helpers/cache.d.ts +10 -0
  2. package/dist/helpers/cache.js +86 -2
  3. package/dist/helpers/client_abilities_jwt_keys.d.ts +10 -0
  4. package/dist/helpers/client_abilities_jwt_keys.js +25 -0
  5. package/dist/helpers/client_abilities_token.d.ts +7 -0
  6. package/dist/helpers/client_abilities_token.js +53 -0
  7. package/dist/helpers/client_abilities_token_server.d.ts +7 -0
  8. package/dist/helpers/client_abilities_token_server.js +7 -0
  9. package/dist/helpers/herd_roles.d.ts +14 -0
  10. package/dist/helpers/herd_roles.js +134 -0
  11. package/dist/helpers/index.d.ts +4 -0
  12. package/dist/helpers/index.js +4 -0
  13. package/dist/helpers/invitations.d.ts +2 -2
  14. package/dist/helpers/invitations.js +2 -2
  15. package/dist/helpers/pubsub_token.d.ts +3 -1
  16. package/dist/helpers/pubsub_token.js +41 -0
  17. package/dist/helpers/ui.d.ts +3 -2
  18. package/dist/helpers/ui.js +6 -1
  19. package/dist/helpers/users.d.ts +2 -2
  20. package/dist/helpers/users.js +27 -16
  21. package/dist/index.d.ts +7 -1
  22. package/dist/index.js +6 -0
  23. package/dist/providers/ScoutRefreshProvider.d.ts +28 -3532
  24. package/dist/providers/ScoutRefreshProvider.js +191 -36
  25. package/dist/types/client_abilities_token.d.ts +19 -0
  26. package/dist/types/client_abilities_token.js +16 -0
  27. package/dist/types/db.d.ts +11 -2
  28. package/dist/types/jwt_mint.d.ts +10 -0
  29. package/dist/types/jwt_mint.js +26 -0
  30. package/dist/types/pubsub_token.d.ts +5 -0
  31. package/dist/types/pubsub_token.js +1 -1
  32. package/dist/types/supabase.d.ts +191 -38
  33. package/dist/types/supabase.js +0 -1
  34. package/package.json +12 -9
@@ -1,47 +1,202 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  import { useScoutRefresh, } from "../hooks/useScoutRefresh";
4
- import { createContext, useContext, useMemo, useRef } from "react";
4
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
5
5
  import { createBrowserClient } from "@supabase/ssr";
6
- // Create context for the Supabase client
7
- const SupabaseContext = createContext(null);
8
- const ConnectionStatusContext = createContext(null);
9
- // Hook to use the Supabase client
10
- export function useSupabase() {
11
- const supabase = useContext(SupabaseContext);
12
- if (!supabase) {
13
- throw new Error("useSupabase must be used within a SupabaseProvider");
6
+ import { CLIENT_ABILITIES_TOKEN_TTL_SEC, } from "../types/client_abilities_token";
7
+ import { EnumWebResponse } from "../types/requests";
8
+ import { get_client_abilities_jwt_public_keys, mint_client_abilities_token, verify_client_abilities_token, } from "../helpers/client_abilities_token";
9
+ import { scoutCache } from "../helpers/cache";
10
+ import { get_pubsub_jwt_public_keys, mint_pubsub_token, verify_pubsub_token, } from "../helpers/pubsub_token";
11
+ import { PUBSUB_TOKEN_TTL_SEC } from "../types/pubsub_token";
12
+ import { derive_jwt_mint_status, } from "../types/jwt_mint";
13
+ const ScoutRefreshContext = createContext(null);
14
+ const DEFAULT_REFRESH_BEFORE_EXPIRY_SEC = 120;
15
+ const DEFAULT_PUBLIC_KEYS_CACHE_MS = 10 * 60 * 1000;
16
+ function mintExpiresAt(mint, ttlSec) {
17
+ if (mint.exp > mint.iat) {
18
+ return mint.exp;
14
19
  }
15
- return supabase;
20
+ return mint.iat + ttlSec;
16
21
  }
17
- // Hook to use connection status
18
- export function useConnectionStatus() {
19
- const connectionStatus = useContext(ConnectionStatusContext);
20
- if (!connectionStatus) {
21
- throw new Error("useConnectionStatus must be used within a ScoutRefreshProvider");
22
+ function useScoutRefreshContext() {
23
+ const ctx = useContext(ScoutRefreshContext);
24
+ if (!ctx) {
25
+ throw new Error("Scout refresh hooks must be used within ScoutRefreshProvider");
22
26
  }
23
- return connectionStatus;
27
+ return ctx;
28
+ }
29
+ export function useSupabase() {
30
+ return useScoutRefreshContext().supabase;
31
+ }
32
+ export function useClientAbilitiesMint() {
33
+ return useScoutRefreshContext().abilities;
34
+ }
35
+ export function usePubsubTokenMint() {
36
+ return useScoutRefreshContext().pubsub;
37
+ }
38
+ function resolveAbilityParams(abilityParams) {
39
+ return {
40
+ mintClientAbilitiesToken: abilityParams?.mintClientAbilitiesToken ?? true,
41
+ mintPubsubToken: abilityParams?.mintPubsubToken ?? false,
42
+ clientAbilitiesTokenTtlSec: abilityParams?.clientAbilitiesTokenTtlSec ??
43
+ CLIENT_ABILITIES_TOKEN_TTL_SEC,
44
+ pubsubTokenTtlSec: abilityParams?.pubsubTokenTtlSec ?? PUBSUB_TOKEN_TTL_SEC,
45
+ refreshBeforeExpirySec: abilityParams?.refreshBeforeExpirySec ??
46
+ DEFAULT_REFRESH_BEFORE_EXPIRY_SEC,
47
+ publicKeysCacheTtlMs: abilityParams?.publicKeysCacheTtlMs ?? DEFAULT_PUBLIC_KEYS_CACHE_MS,
48
+ };
49
+ }
50
+ function useCachedPublicKeys(supabase, publicKeysCacheTtlMs, fetchKeys) {
51
+ const keysRef = useRef(null);
52
+ const keysAtRef = useRef(0);
53
+ return useCallback(async (force = false) => {
54
+ const now = Date.now();
55
+ if (!force &&
56
+ keysRef.current?.length &&
57
+ now - keysAtRef.current < publicKeysCacheTtlMs) {
58
+ return keysRef.current;
59
+ }
60
+ const { status, data, msg } = await fetchKeys(supabase);
61
+ if (status !== EnumWebResponse.SUCCESS || !data?.length) {
62
+ throw new Error(msg ?? "jwt public keys RPC returned no keys");
63
+ }
64
+ keysRef.current = data;
65
+ keysAtRef.current = now;
66
+ return data;
67
+ }, [supabase, publicKeysCacheTtlMs, fetchKeys]);
68
+ }
69
+ function useJwtMintLifecycle({ enabled, supabase, cacheKey, ttlSec, refreshBeforeExpirySec, loadPublicKeys, mintToken, verifyToken, }) {
70
+ const [mint, setMint] = useState(null);
71
+ const [inFlight, setInFlight] = useState(false);
72
+ const [error, setError] = useState(null);
73
+ const inFlightRef = useRef(false);
74
+ const status = useMemo(() => derive_jwt_mint_status(enabled, mint, inFlight, error), [enabled, mint, inFlight, error]);
75
+ const refreshMint = useCallback(async () => {
76
+ if (!enabled || inFlightRef.current) {
77
+ return;
78
+ }
79
+ inFlightRef.current = true;
80
+ setInFlight(true);
81
+ setError(null);
82
+ try {
83
+ const { data: { session }, } = await supabase.auth.getSession();
84
+ if (!session) {
85
+ setMint(null);
86
+ await scoutCache.clearJwtMint(cacheKey);
87
+ return;
88
+ }
89
+ const mintResponse = await mintToken(supabase);
90
+ if (mintResponse.status !== EnumWebResponse.SUCCESS ||
91
+ !mintResponse.data) {
92
+ setError(mintResponse.msg ?? "mint failed");
93
+ return;
94
+ }
95
+ let keys = await loadPublicKeys();
96
+ let verified;
97
+ try {
98
+ verified = await verifyToken(mintResponse.data, keys);
99
+ }
100
+ catch {
101
+ keys = await loadPublicKeys(true);
102
+ verified = await verifyToken(mintResponse.data, keys);
103
+ }
104
+ setMint(verified);
105
+ try {
106
+ await scoutCache.setJwtMint(cacheKey, verified);
107
+ }
108
+ catch (cacheError) {
109
+ console.warn(`[ScoutRefreshProvider] ${cacheKey} cache save failed:`, cacheError);
110
+ }
111
+ }
112
+ catch (e) {
113
+ setError(e instanceof Error ? e.message : "mint failed");
114
+ }
115
+ finally {
116
+ inFlightRef.current = false;
117
+ setInFlight(false);
118
+ }
119
+ }, [enabled, supabase, cacheKey, loadPublicKeys, mintToken, verifyToken]);
120
+ useEffect(() => {
121
+ if (!enabled) {
122
+ setMint(null);
123
+ setError(null);
124
+ return;
125
+ }
126
+ let cancelled = false;
127
+ void scoutCache.getJwtMint(cacheKey).then((cached) => {
128
+ if (!cancelled && cached.data) {
129
+ setMint(cached.data);
130
+ }
131
+ });
132
+ void refreshMint();
133
+ const { data: { subscription }, } = supabase.auth.onAuthStateChange((event) => {
134
+ if (event === "SIGNED_OUT") {
135
+ setMint(null);
136
+ setError(null);
137
+ void scoutCache.clearJwtMint(cacheKey);
138
+ return;
139
+ }
140
+ if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
141
+ void refreshMint();
142
+ }
143
+ });
144
+ return () => {
145
+ cancelled = true;
146
+ subscription.unsubscribe();
147
+ };
148
+ }, [enabled, supabase, cacheKey, refreshMint]);
149
+ useEffect(() => {
150
+ if (!enabled || !mint) {
151
+ return;
152
+ }
153
+ const nowSec = Math.floor(Date.now() / 1000);
154
+ const expSec = mintExpiresAt(mint, ttlSec);
155
+ const refreshAtSec = Math.max(expSec - refreshBeforeExpirySec, mint.iat + 1);
156
+ const delayMs = Math.max((refreshAtSec - nowSec) * 1000, 1000);
157
+ const timer = setTimeout(() => {
158
+ void refreshMint();
159
+ }, delayMs);
160
+ return () => clearTimeout(timer);
161
+ }, [enabled, mint, ttlSec, refreshBeforeExpirySec, refreshMint]);
162
+ return useMemo(() => ({ mint, status, error, refreshMint }), [mint, status, error, refreshMint]);
163
+ }
164
+ function useClientAbilitiesMintLifecycle(supabase, config) {
165
+ const { mintClientAbilitiesToken: enabled, clientAbilitiesTokenTtlSec: ttlSec, refreshBeforeExpirySec, publicKeysCacheTtlMs, } = config;
166
+ const loadPublicKeys = useCachedPublicKeys(supabase, publicKeysCacheTtlMs, get_client_abilities_jwt_public_keys);
167
+ return useJwtMintLifecycle({
168
+ enabled,
169
+ supabase,
170
+ cacheKey: "client_abilities",
171
+ ttlSec,
172
+ refreshBeforeExpirySec,
173
+ loadPublicKeys,
174
+ mintToken: mint_client_abilities_token,
175
+ verifyToken: verify_client_abilities_token,
176
+ });
177
+ }
178
+ function usePubsubTokenMintLifecycle(supabase, config) {
179
+ const { mintPubsubToken: enabled, pubsubTokenTtlSec: ttlSec, refreshBeforeExpirySec, publicKeysCacheTtlMs, } = config;
180
+ const loadPublicKeys = useCachedPublicKeys(supabase, publicKeysCacheTtlMs, get_pubsub_jwt_public_keys);
181
+ return useJwtMintLifecycle({
182
+ enabled,
183
+ supabase,
184
+ cacheKey: "pubsub",
185
+ ttlSec,
186
+ refreshBeforeExpirySec,
187
+ loadPublicKeys,
188
+ mintToken: mint_pubsub_token,
189
+ verifyToken: verify_pubsub_token,
190
+ });
24
191
  }
25
- export function ScoutRefreshProvider({ children, ...refreshOptions }) {
26
- // Use refs to store the URL and key to prevent unnecessary recreations
27
- // Assumes Next.js environment variables (NEXT_PUBLIC_*)
192
+ export function ScoutRefreshProvider({ children, abilityParams, ...refreshOptions }) {
28
193
  const urlRef = useRef(process.env.NEXT_PUBLIC_SUPABASE_URL || "");
29
194
  const anonKeyRef = useRef(process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "");
30
- // Create a single Supabase client instance that only runs once
31
- const supabaseClient = useMemo(() => {
32
- console.log("[ScoutRefreshProvider] Creating Supabase client");
33
- return createBrowserClient(urlRef.current, anonKeyRef.current);
34
- }, []); // Empty dependency array ensures this only runs once
35
- useScoutRefresh({ ...refreshOptions, supabase: supabaseClient });
36
- // // Log connection status changes for debugging
37
- // if (connectionStatus.lastError) {
38
- // console.warn(
39
- // "[ScoutRefreshProvider] DB Listener error:",
40
- // connectionStatus.lastError
41
- // );
42
- // }
43
- // if (connectionStatus.isConnected) {
44
- // console.log("[ScoutRefreshProvider] ✅ DB Listener connected");
45
- // }
46
- return (_jsx(SupabaseContext.Provider, { value: supabaseClient, children: children }));
195
+ const supabase = useMemo(() => createBrowserClient(urlRef.current, anonKeyRef.current), []);
196
+ const abilitiesConfig = useMemo(() => resolveAbilityParams(abilityParams), [abilityParams]);
197
+ useScoutRefresh({ ...refreshOptions, supabase });
198
+ const abilities = useClientAbilitiesMintLifecycle(supabase, abilitiesConfig);
199
+ const pubsub = usePubsubTokenMintLifecycle(supabase, abilitiesConfig);
200
+ const value = useMemo(() => ({ supabase, abilities, pubsub }), [supabase, abilities, pubsub]);
201
+ return (_jsx(ScoutRefreshContext.Provider, { value: value, children: children }));
47
202
  }
@@ -0,0 +1,19 @@
1
+ export interface IClientAbilitiesHerdClaim {
2
+ herd_id: number;
3
+ slug: string;
4
+ abilities: string[];
5
+ }
6
+ export interface IClientAbilitiesTokenMint {
7
+ token: string;
8
+ platform_superadmin: boolean;
9
+ herds: IClientAbilitiesHerdClaim[];
10
+ iat: number;
11
+ exp: number;
12
+ }
13
+ export interface IClientAbilitiesJwtPublicKeyEntry {
14
+ kid: string;
15
+ jwk: Record<string, unknown>;
16
+ }
17
+ export declare const CLIENT_ABILITIES_JWT_AUDIENCE = "scout-client";
18
+ export declare const CLIENT_ABILITIES_TOKEN_TTL_SEC = 3600;
19
+ export declare function can_herd_ability(mint: IClientAbilitiesTokenMint | null | undefined, herd_id: number, ability: string, require_valid_expiry?: boolean): boolean;
@@ -0,0 +1,16 @@
1
+ export const CLIENT_ABILITIES_JWT_AUDIENCE = "scout-client";
2
+ export const CLIENT_ABILITIES_TOKEN_TTL_SEC = 3600;
3
+ export function can_herd_ability(mint, herd_id, ability, require_valid_expiry = true) {
4
+ if (!mint?.token) {
5
+ return false;
6
+ }
7
+ if (require_valid_expiry &&
8
+ mint.exp <= Math.floor(Date.now() / 1000)) {
9
+ return false;
10
+ }
11
+ if (mint.platform_superadmin) {
12
+ return true;
13
+ }
14
+ const herd = mint.herds.find((h) => h.herd_id === herd_id);
15
+ return herd?.abilities.includes(ability) ?? false;
16
+ }
@@ -1,7 +1,6 @@
1
1
  import { SupabaseClient, User } from "@supabase/supabase-js";
2
2
  import { Database } from "./supabase";
3
3
  export type ScoutDatabaseClient = SupabaseClient<Database, "public">;
4
- export type Role = Database["public"]["Enums"]["role"];
5
4
  export type DeviceType = Database["public"]["Enums"]["device_type"];
6
5
  export type MediaType = Database["public"]["Enums"]["media_type"];
7
6
  export type TagObservationType = Database["public"]["Enums"]["tag_observation_type"];
@@ -27,6 +26,15 @@ export type ILayer = Database["public"]["Tables"]["layers"]["Row"];
27
26
  export type IAction = Database["public"]["Tables"]["actions"]["Row"];
28
27
  export type IZone = Database["public"]["Tables"]["zones"]["Row"];
29
28
  export type IUserRolePerHerd = Database["public"]["Tables"]["users_roles_per_herd"]["Row"];
29
+ export type IAbility = Database["public"]["Tables"]["abilities"]["Row"];
30
+ export type IHerdRole = Database["public"]["Tables"]["herd_roles"]["Row"];
31
+ export type IHerdRoleAbilityRow = Database["public"]["Tables"]["herd_role_abilities"]["Row"];
32
+ export type IHerdRoleWithAbilities = IHerdRole & {
33
+ abilities: IAbility[];
34
+ ability_ids: number[];
35
+ };
36
+ /** Stable machine id on `herd_roles` (e.g. admin, editor, viewer). Not `herds.slug`. */
37
+ export type HerdRoleSystemName = string;
30
38
  export type IHerdInvitation = Database["public"]["Tables"]["herd_invitations"]["Row"];
31
39
  export type IHerdInvitationWithHerdSlug = Database["public"]["CompositeTypes"]["herd_invitation_with_herd_slug"];
32
40
  export type IHerdAllowedDomain = Database["public"]["Tables"]["herd_allowed_domains"]["Row"];
@@ -141,7 +149,8 @@ export interface IEventWithSession extends IEvent {
141
149
  }
142
150
  export type IUserAndRole = {
143
151
  user: IUserProfileRow | null;
144
- role: Role;
152
+ herd_role_id: number;
153
+ herd_role: IHerdRole | null;
145
154
  };
146
155
  export interface IApiKeyScout {
147
156
  id: string;
@@ -0,0 +1,10 @@
1
+ export declare enum EnumJwtMintStatus {
2
+ IDLE = "idle",
3
+ LOADING = "loading",
4
+ REFRESHING = "refreshing",
5
+ READY = "ready",
6
+ ERROR = "error"
7
+ }
8
+ export declare function derive_jwt_mint_status(enabled: boolean, mint: {
9
+ token: string;
10
+ } | null | undefined, inFlight: boolean, error: string | null): EnumJwtMintStatus;
@@ -0,0 +1,26 @@
1
+ export var EnumJwtMintStatus;
2
+ (function (EnumJwtMintStatus) {
3
+ EnumJwtMintStatus["IDLE"] = "idle";
4
+ EnumJwtMintStatus["LOADING"] = "loading";
5
+ EnumJwtMintStatus["REFRESHING"] = "refreshing";
6
+ EnumJwtMintStatus["READY"] = "ready";
7
+ EnumJwtMintStatus["ERROR"] = "error";
8
+ })(EnumJwtMintStatus || (EnumJwtMintStatus = {}));
9
+ export function derive_jwt_mint_status(enabled, mint, inFlight, error) {
10
+ if (!enabled) {
11
+ return EnumJwtMintStatus.IDLE;
12
+ }
13
+ if (inFlight && !mint?.token) {
14
+ return EnumJwtMintStatus.LOADING;
15
+ }
16
+ if (inFlight && mint?.token) {
17
+ return EnumJwtMintStatus.REFRESHING;
18
+ }
19
+ if (error) {
20
+ return EnumJwtMintStatus.ERROR;
21
+ }
22
+ if (mint?.token) {
23
+ return EnumJwtMintStatus.READY;
24
+ }
25
+ return EnumJwtMintStatus.LOADING;
26
+ }
@@ -5,3 +5,8 @@ export interface IPubsubTokenMint {
5
5
  iat: number;
6
6
  exp: number;
7
7
  }
8
+ export interface IPubsubJwtPublicKeyEntry {
9
+ kid: string;
10
+ jwk: Record<string, unknown>;
11
+ }
12
+ export declare const PUBSUB_TOKEN_TTL_SEC = 3600;
@@ -1 +1 @@
1
- export {};
1
+ export const PUBSUB_TOKEN_TTL_SEC = 3600;