@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.
- package/dist/helpers/cache.d.ts +10 -0
- package/dist/helpers/cache.js +86 -2
- package/dist/helpers/client_abilities_jwt_keys.d.ts +10 -0
- package/dist/helpers/client_abilities_jwt_keys.js +25 -0
- package/dist/helpers/client_abilities_token.d.ts +7 -0
- package/dist/helpers/client_abilities_token.js +53 -0
- package/dist/helpers/client_abilities_token_server.d.ts +7 -0
- package/dist/helpers/client_abilities_token_server.js +7 -0
- package/dist/helpers/herd_roles.d.ts +14 -0
- package/dist/helpers/herd_roles.js +134 -0
- package/dist/helpers/index.d.ts +4 -0
- package/dist/helpers/index.js +4 -0
- package/dist/helpers/invitations.d.ts +2 -2
- package/dist/helpers/invitations.js +2 -2
- package/dist/helpers/pubsub_token.d.ts +3 -1
- package/dist/helpers/pubsub_token.js +41 -0
- package/dist/helpers/ui.d.ts +3 -2
- package/dist/helpers/ui.js +6 -1
- package/dist/helpers/users.d.ts +2 -2
- package/dist/helpers/users.js +27 -16
- package/dist/index.d.ts +7 -1
- package/dist/index.js +6 -0
- package/dist/providers/ScoutRefreshProvider.d.ts +28 -3532
- package/dist/providers/ScoutRefreshProvider.js +191 -36
- package/dist/types/client_abilities_token.d.ts +19 -0
- package/dist/types/client_abilities_token.js +16 -0
- package/dist/types/db.d.ts +11 -2
- package/dist/types/jwt_mint.d.ts +10 -0
- package/dist/types/jwt_mint.js +26 -0
- package/dist/types/pubsub_token.d.ts +5 -0
- package/dist/types/pubsub_token.js +1 -1
- package/dist/types/supabase.d.ts +191 -38
- package/dist/types/supabase.js +0 -1
- 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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
20
|
+
return mint.iat + ttlSec;
|
|
16
21
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
+
}
|
package/dist/types/db.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const PUBSUB_TOKEN_TTL_SEC = 3600;
|