@adventurelabs/scout-core 1.4.75 → 1.4.78

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,4 +1,10 @@
1
1
  import { IHerdModule } from "../types/herd_module";
2
+ export type JwtMintCacheKey = "client_abilities" | "pubsub";
3
+ type CachedJwtMint = {
4
+ token: string;
5
+ iat: number;
6
+ exp: number;
7
+ };
2
8
  export interface CacheMetadata {
3
9
  key: string;
4
10
  timestamp: number;
@@ -43,6 +49,9 @@ export declare class ScoutCache {
43
49
  private validateDatabaseSchema;
44
50
  setHerdModules(herdModules: IHerdModule[], ttlMs?: number, etag?: string): Promise<void>;
45
51
  getHerdModules(): Promise<CacheResult<IHerdModule[]>>;
52
+ setJwtMint<T extends CachedJwtMint>(key: JwtMintCacheKey, mint: T): Promise<void>;
53
+ getJwtMint<T extends CachedJwtMint>(key: JwtMintCacheKey): Promise<CacheResult<T>>;
54
+ clearJwtMint(key: JwtMintCacheKey): Promise<void>;
46
55
  clearHerdModules(): Promise<void>;
47
56
  invalidateHerdModules(): Promise<void>;
48
57
  getCacheStats(): Promise<CacheStats>;
@@ -60,3 +69,4 @@ export declare class ScoutCache {
60
69
  checkDatabaseHealth(): Promise<DatabaseHealth>;
61
70
  }
62
71
  export declare const scoutCache: ScoutCache;
72
+ export {};
@@ -1,6 +1,7 @@
1
1
  const DB_NAME = "ScoutCache";
2
- const DB_VERSION = 6;
2
+ const DB_VERSION = 8;
3
3
  const HERD_MODULES_STORE = "herd_modules";
4
+ const JWT_MINTS_STORE = "jwt_mints";
4
5
  const CACHE_METADATA_STORE = "cache_metadata";
5
6
  // Default TTL: 24 hours (1 day)
6
7
  const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000;
@@ -56,6 +57,13 @@ export class ScoutCache {
56
57
  unique: false,
57
58
  });
58
59
  console.log("[ScoutCache] Created herd_modules object store");
60
+ const jwtMintsStore = db.createObjectStore(JWT_MINTS_STORE, {
61
+ keyPath: "key",
62
+ });
63
+ jwtMintsStore.createIndex("timestamp", "timestamp", {
64
+ unique: false,
65
+ });
66
+ console.log("[ScoutCache] Created jwt_mints object store");
59
67
  // Create cache metadata store
60
68
  const metadataStore = db.createObjectStore(CACHE_METADATA_STORE, {
61
69
  keyPath: "key",
@@ -78,14 +86,18 @@ export class ScoutCache {
78
86
  if (!this.db)
79
87
  return false;
80
88
  const hasHerdModulesStore = this.db.objectStoreNames.contains(HERD_MODULES_STORE);
89
+ const hasJwtMintsStore = this.db.objectStoreNames.contains(JWT_MINTS_STORE);
81
90
  const hasMetadataStore = this.db.objectStoreNames.contains(CACHE_METADATA_STORE);
82
91
  if (!hasHerdModulesStore) {
83
92
  console.error("[ScoutCache] Missing herd_modules object store");
84
93
  }
94
+ if (!hasJwtMintsStore) {
95
+ console.error("[ScoutCache] Missing jwt_mints object store");
96
+ }
85
97
  if (!hasMetadataStore) {
86
98
  console.error("[ScoutCache] Missing cache_metadata object store");
87
99
  }
88
- return hasHerdModulesStore && hasMetadataStore;
100
+ return hasHerdModulesStore && hasJwtMintsStore && hasMetadataStore;
89
101
  }
90
102
  async setHerdModules(herdModules, ttlMs = DEFAULT_TTL_MS, etag) {
91
103
  await this.init();
@@ -190,6 +202,78 @@ export class ScoutCache {
190
202
  };
191
203
  });
192
204
  }
205
+ async setJwtMint(key, mint) {
206
+ await this.init();
207
+ if (!this.db)
208
+ throw new Error("Database not initialized");
209
+ if (!this.validateDatabaseSchema()) {
210
+ throw new Error("Database schema validation failed - required object stores not found");
211
+ }
212
+ const transaction = this.db.transaction([JWT_MINTS_STORE], "readwrite");
213
+ return new Promise((resolve, reject) => {
214
+ transaction.onerror = () => reject(transaction.error);
215
+ transaction.oncomplete = () => resolve();
216
+ transaction.objectStore(JWT_MINTS_STORE).put({
217
+ key,
218
+ data: mint,
219
+ timestamp: Date.now(),
220
+ dbVersion: DB_VERSION,
221
+ });
222
+ });
223
+ }
224
+ async getJwtMint(key) {
225
+ await this.init();
226
+ if (!this.db)
227
+ throw new Error("Database not initialized");
228
+ if (!this.validateDatabaseSchema()) {
229
+ throw new Error("Database schema validation failed - required object stores not found");
230
+ }
231
+ const transaction = this.db.transaction([JWT_MINTS_STORE], "readonly");
232
+ return new Promise((resolve, reject) => {
233
+ transaction.onerror = () => reject(transaction.error);
234
+ const request = transaction.objectStore(JWT_MINTS_STORE).get(key);
235
+ request.onsuccess = () => {
236
+ const entry = request.result;
237
+ if (!entry?.data?.token ||
238
+ entry.dbVersion !== DB_VERSION ||
239
+ !entry.timestamp) {
240
+ this.stats.misses++;
241
+ resolve({ data: null, isStale: true, age: 0, metadata: null });
242
+ return;
243
+ }
244
+ const now = Date.now();
245
+ const age = now - entry.timestamp;
246
+ const isStale = entry.data.exp <= Math.floor(now / 1000);
247
+ this.stats.hits++;
248
+ resolve({
249
+ data: entry.data,
250
+ isStale,
251
+ age,
252
+ metadata: {
253
+ key,
254
+ timestamp: entry.timestamp,
255
+ ttl: Math.max((entry.data.exp - entry.data.iat) * 1000, 0),
256
+ version: "1.0.0",
257
+ dbVersion: DB_VERSION,
258
+ },
259
+ });
260
+ };
261
+ });
262
+ }
263
+ async clearJwtMint(key) {
264
+ await this.init();
265
+ if (!this.db)
266
+ throw new Error("Database not initialized");
267
+ if (!this.validateDatabaseSchema()) {
268
+ throw new Error("Database schema validation failed - required object stores not found");
269
+ }
270
+ const transaction = this.db.transaction([JWT_MINTS_STORE], "readwrite");
271
+ return new Promise((resolve, reject) => {
272
+ transaction.onerror = () => reject(transaction.error);
273
+ transaction.oncomplete = () => resolve();
274
+ transaction.objectStore(JWT_MINTS_STORE).delete(key);
275
+ });
276
+ }
193
277
  async clearHerdModules() {
194
278
  await this.init();
195
279
  if (!this.db)
@@ -4,5 +4,4 @@ import { IClientAbilitiesJwtPublicKeyEntry, IClientAbilitiesTokenMint } from "..
4
4
  import { IWebResponseCompatible } from "../types/requests";
5
5
  export declare function mint_client_abilities_token(client: SupabaseClient<Database>): Promise<IWebResponseCompatible<IClientAbilitiesTokenMint>>;
6
6
  export declare function get_client_abilities_jwt_public_keys(client: SupabaseClient<Database>): Promise<IWebResponseCompatible<IClientAbilitiesJwtPublicKeyEntry[]>>;
7
- /** Cryptographic check only; returns the same mint envelope from the edge. */
8
7
  export declare function verify_client_abilities_token(mint: IClientAbilitiesTokenMint, publicKeys: IClientAbilitiesJwtPublicKeyEntry[]): Promise<IClientAbilitiesTokenMint>;
@@ -32,7 +32,6 @@ export async function get_client_abilities_jwt_public_keys(client) {
32
32
  }
33
33
  return IWebResponse.success(keys).to_compatible();
34
34
  }
35
- /** Cryptographic check only; returns the same mint envelope from the edge. */
36
35
  export async function verify_client_abilities_token(mint, publicKeys) {
37
36
  if (publicKeys.length === 0) {
38
37
  throw new Error("no client abilities JWT public keys configured");
@@ -0,0 +1,2 @@
1
+ import { IHerdModule } from "../types/herd_module";
2
+ export declare function herdModulesSemanticallyEqual(current: IHerdModule[], incoming: IHerdModule[]): boolean;
@@ -0,0 +1,28 @@
1
+ import { dequal } from "dequal";
2
+ function withoutFetchTimestamp({ timestamp_last_refreshed: _timestamp, ...module }) {
3
+ return module;
4
+ }
5
+ export function herdModulesSemanticallyEqual(current, incoming) {
6
+ if (current === incoming) {
7
+ return true;
8
+ }
9
+ if (current.length !== incoming.length) {
10
+ return false;
11
+ }
12
+ if (current.length === 0) {
13
+ return true;
14
+ }
15
+ const currentByHerdId = new Map(current.map((module) => [
16
+ String(module.herd.id),
17
+ withoutFetchTimestamp(module),
18
+ ]));
19
+ for (const incomingModule of incoming) {
20
+ const herdId = String(incomingModule.herd.id);
21
+ const existing = currentByHerdId.get(herdId);
22
+ if (!existing ||
23
+ !dequal(existing, withoutFetchTimestamp(incomingModule))) {
24
+ return false;
25
+ }
26
+ }
27
+ return true;
28
+ }
@@ -1,5 +1,7 @@
1
1
  import { SupabaseClient } from "@supabase/supabase-js";
2
2
  import { Database } from "../types/supabase";
3
- import { IPubsubTokenMint } from "../types/pubsub_token";
3
+ import { IPubsubJwtPublicKeyEntry, IPubsubTokenMint } from "../types/pubsub_token";
4
4
  import { IWebResponseCompatible } from "../types/requests";
5
5
  export declare function mint_pubsub_token(client: SupabaseClient<Database>): Promise<IWebResponseCompatible<IPubsubTokenMint>>;
6
+ export declare function get_pubsub_jwt_public_keys(client: SupabaseClient<Database>): Promise<IWebResponseCompatible<IPubsubJwtPublicKeyEntry[]>>;
7
+ export declare function verify_pubsub_token(mint: IPubsubTokenMint, publicKeys: IPubsubJwtPublicKeyEntry[]): Promise<IPubsubTokenMint>;
@@ -1,3 +1,4 @@
1
+ import * as jose from "jose";
1
2
  import { IWebResponse } from "../types/requests";
2
3
  export async function mint_pubsub_token(client) {
3
4
  const { data, error } = await client.functions.invoke("mint-pubsub-token", {
@@ -12,3 +13,43 @@ export async function mint_pubsub_token(client) {
12
13
  }
13
14
  return IWebResponse.success(mint).to_compatible();
14
15
  }
16
+ export async function get_pubsub_jwt_public_keys(client) {
17
+ const { data, error } = await client.rpc("get_pubsub_jwt_public_keys");
18
+ if (error) {
19
+ return IWebResponse.error(error.message).to_compatible();
20
+ }
21
+ const entries = Array.isArray(data) ? data : [];
22
+ const keys = [];
23
+ for (const row of entries) {
24
+ if (row &&
25
+ typeof row === "object" &&
26
+ "kid" in row &&
27
+ "jwk" in row &&
28
+ typeof row.kid === "string" &&
29
+ row.jwk &&
30
+ typeof row.jwk === "object") {
31
+ keys.push({ kid: row.kid, jwk: row.jwk });
32
+ }
33
+ }
34
+ return IWebResponse.success(keys).to_compatible();
35
+ }
36
+ export async function verify_pubsub_token(mint, publicKeys) {
37
+ if (publicKeys.length === 0) {
38
+ throw new Error("no pubsub JWT public keys configured");
39
+ }
40
+ const header = jose.decodeProtectedHeader(mint.token);
41
+ const kid = header.kid;
42
+ if (!kid || typeof kid !== "string") {
43
+ throw new Error("pubsub JWT missing kid");
44
+ }
45
+ const entry = publicKeys.find((k) => k.kid === kid);
46
+ if (!entry) {
47
+ throw new Error(`unknown pubsub JWT kid: ${kid}`);
48
+ }
49
+ const alg = typeof entry.jwk.alg === "string" && entry.jwk.alg
50
+ ? entry.jwk.alg
51
+ : header.alg ?? "ES256";
52
+ const verifyKey = await jose.importJWK(entry.jwk, alg);
53
+ await jose.jwtVerify(mint.token, verifyKey, { algorithms: [alg] });
54
+ return mint;
55
+ }
@@ -7,4 +7,3 @@ export declare function get_versions_software_by_system(client: SupabaseClient<D
7
7
  export declare function create_version_software(client: SupabaseClient<Database>, newVersionSoftware: VersionsSoftwareInsert): Promise<IWebResponseCompatible<IVersionsSoftware | null>>;
8
8
  export declare function update_version_software(client: SupabaseClient<Database>, version_id: number, updatedVersionSoftware: Partial<VersionsSoftwareInsert>): Promise<IWebResponseCompatible<IVersionsSoftware | null>>;
9
9
  export declare function delete_version_software(client: SupabaseClient<Database>, version_id: number): Promise<IWebResponseCompatible<IVersionsSoftware | null>>;
10
- export declare function get_versions_software_by_created_by(client: SupabaseClient<Database>, user_id: string): Promise<IWebResponseCompatible<IVersionsSoftware[]>>;
@@ -75,17 +75,3 @@ export async function delete_version_software(client, version_id) {
75
75
  }
76
76
  return IWebResponse.success(data).to_compatible();
77
77
  }
78
- export async function get_versions_software_by_created_by(client, user_id) {
79
- const { data, error } = await client
80
- .from("versions_software")
81
- .select("*")
82
- .eq("created_by", user_id)
83
- .order("created_at", { ascending: false });
84
- if (error) {
85
- return IWebResponse.error(error.message).to_compatible();
86
- }
87
- if (!data) {
88
- return IWebResponse.error("No software versions found for user").to_compatible();
89
- }
90
- return IWebResponse.success(data).to_compatible();
91
- }
@@ -3,4 +3,3 @@ import { IWebResponseCompatible } from "../types/requests";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
4
  export declare function server_get_versions_software(client?: SupabaseClient): Promise<IWebResponseCompatible<IVersionsSoftwareWithBuildUrl[]>>;
5
5
  export declare function server_get_versions_software_by_system(system: string, client?: SupabaseClient): Promise<IWebResponseCompatible<IVersionsSoftwareWithBuildUrl[]>>;
6
- export declare function server_get_versions_software_by_created_by(user_id: string, client?: SupabaseClient): Promise<IWebResponseCompatible<IVersionsSoftwareWithBuildUrl[]>>;
@@ -53,19 +53,3 @@ export async function server_get_versions_software_by_system(system, client) {
53
53
  const withUrls = await attachBuildArtifactUrls(data, client);
54
54
  return IWebResponse.success(withUrls).to_compatible();
55
55
  }
56
- export async function server_get_versions_software_by_created_by(user_id, client) {
57
- const supabase = client || (await newServerClient());
58
- const { data, error } = await supabase
59
- .from("versions_software")
60
- .select("*")
61
- .eq("created_by", user_id)
62
- .order("created_at", { ascending: false });
63
- if (error) {
64
- return IWebResponse.error(error.message).to_compatible();
65
- }
66
- if (!data) {
67
- return IWebResponse.error("No software versions found for user").to_compatible();
68
- }
69
- const withUrls = await attachBuildArtifactUrls(data, client);
70
- return IWebResponse.success(withUrls).to_compatible();
71
- }
@@ -1,6 +1,5 @@
1
- import { useEffect, useCallback, useRef, useMemo } from "react";
1
+ import { useEffect, useCallback, useRef, useMemo, startTransition } from "react";
2
2
  import { useAppDispatch } from "../store/hooks";
3
- import { useStore } from "react-redux";
4
3
  import { EnumScoutStateStatus, setHerdModules, setStatus, setHerdModulesLoadingState, setHerdModulesLoadedInMs, setHerdModulesApiServerProcessingDuration, setHerdModulesApiTotalRequestDuration, setUserApiDuration, setDataProcessingDuration, setCacheLoadDuration, setUser, setDataSource, setDataSourceInfo, } from "../store/scout";
5
4
  import { EnumHerdModulesLoadingState } from "../types/herd_module";
6
5
  import { server_load_herd_modules } from "../helpers/herds";
@@ -8,6 +7,11 @@ import { scoutCache } from "../helpers/cache";
8
7
  import { EnumDataSource } from "../types/data_source";
9
8
  import { EnumWebResponse } from "../types/requests";
10
9
  import { createBrowserClient } from "@supabase/ssr";
10
+ function dispatchInTransition(dispatch, action) {
11
+ startTransition(() => {
12
+ dispatch(action);
13
+ });
14
+ }
11
15
  /**
12
16
  * Hook for refreshing scout data with detailed timing measurements and cache-first loading
13
17
  *
@@ -38,7 +42,6 @@ export function useScoutRefresh(options = {}) {
38
42
  onlineRefetchMinIntervalMs = 15 * 1000, // 15 seconds
39
43
  } = options;
40
44
  const dispatch = useAppDispatch();
41
- const store = useStore();
42
45
  const refreshInProgressRef = useRef(false);
43
46
  const lastQueryAtRef = useRef(0);
44
47
  const supabase = useMemo(() => {
@@ -98,7 +101,7 @@ export function useScoutRefresh(options = {}) {
98
101
  const totalMs = Date.now() - startTotal;
99
102
  timingRefs.current.userApiDuration = totalMs;
100
103
  dispatch(setUserApiDuration(totalMs));
101
- dispatch(setUser(data.user));
104
+ dispatchInTransition(dispatch, setUser(data.user));
102
105
  return data.user;
103
106
  }
104
107
  catch (e) {
@@ -116,7 +119,7 @@ export function useScoutRefresh(options = {}) {
116
119
  const totalMs = Date.now() - startTotal;
117
120
  timingRefs.current.userApiDuration = totalMs;
118
121
  dispatch(setUserApiDuration(totalMs));
119
- dispatch(setUser(data.user));
122
+ dispatchInTransition(dispatch, setUser(data.user));
120
123
  return data.user;
121
124
  }
122
125
  }
@@ -155,7 +158,7 @@ export function useScoutRefresh(options = {}) {
155
158
  }));
156
159
  // Update the store with cached data
157
160
  console.log(`[useScoutRefresh] Updating store with cached herd modules`);
158
- dispatch(setHerdModules(cachedHerdModules));
161
+ dispatchInTransition(dispatch, setHerdModules(cachedHerdModules));
159
162
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
160
163
  // If cache is fresh, we still background fetch but don't wait (only when online)
161
164
  if (!cacheResult.isStale) {
@@ -204,7 +207,7 @@ export function useScoutRefresh(options = {}) {
204
207
  }
205
208
  // Update store with fresh data from background request
206
209
  console.log(`[useScoutRefresh] Updating store with background herd modules`);
207
- dispatch(setHerdModules(backgroundHerdModulesResult.data));
210
+ dispatchInTransition(dispatch, setHerdModules(backgroundHerdModulesResult.data));
208
211
  // Update data source to DATABASE
209
212
  dispatch(setDataSource(EnumDataSource.DATABASE));
210
213
  dispatch(setDataSourceInfo({
@@ -245,7 +248,7 @@ export function useScoutRefresh(options = {}) {
245
248
  source: EnumDataSource.CACHE,
246
249
  timestamp: Date.now(),
247
250
  }));
248
- dispatch(setHerdModules(cachedHerdModules));
251
+ dispatchInTransition(dispatch, setHerdModules(cachedHerdModules));
249
252
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
250
253
  }
251
254
  else {
@@ -327,11 +330,11 @@ export function useScoutRefresh(options = {}) {
327
330
  await scoutCache.setHerdModules(compatible_new_herd_modules, cacheTtlMs);
328
331
  });
329
332
  }
330
- // Step 4: Conditionally update store with fresh data, skip timestamp-only changes
333
+ // Step 4: Update store with fresh data (reducer skips no-op updates)
331
334
  const dataProcessingStartTime = Date.now();
332
335
  // Update store with new data
333
336
  console.log(`[useScoutRefresh] Updating store with fresh herd modules`);
334
- dispatch(setHerdModules(compatible_new_herd_modules));
337
+ dispatchInTransition(dispatch, setHerdModules(compatible_new_herd_modules));
335
338
  dispatch(setHerdModulesLoadingState(EnumHerdModulesLoadingState.SUCCESSFULLY_LOADED));
336
339
  const dataProcessingDuration = Date.now() - dataProcessingStartTime;
337
340
  timingRefs.current.dataProcessingDuration = dataProcessingDuration;
@@ -367,7 +370,6 @@ export function useScoutRefresh(options = {}) {
367
370
  }
368
371
  }, [
369
372
  dispatch,
370
- store,
371
373
  supabase,
372
374
  onRefreshComplete,
373
375
  cacheFirst,
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export * from "./types/events";
12
12
  export * from "./types/connectivity";
13
13
  export * from "./types/pubsub_token";
14
14
  export * from "./types/client_abilities_token";
15
+ export * from "./types/jwt_mint";
15
16
  export * from "./helpers/analysis_usage";
16
17
  export * from "./helpers/artifacts";
17
18
  export * from "./helpers/auth";
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ export * from "./types/events";
14
14
  export * from "./types/connectivity";
15
15
  export * from "./types/pubsub_token";
16
16
  export * from "./types/client_abilities_token";
17
+ export * from "./types/jwt_mint";
17
18
  // Helpers
18
19
  export * from "./helpers/analysis_usage";
19
20
  export * from "./helpers/artifacts";
@@ -3,30 +3,31 @@ import { type ReactNode } from "react";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
4
  import { Database } from "../types/supabase";
5
5
  import { IClientAbilitiesTokenMint } from "../types/client_abilities_token";
6
+ import { IPubsubTokenMint } from "../types/pubsub_token";
7
+ import { EnumJwtMintStatus } from "../types/jwt_mint";
6
8
  export interface ClientAbilitiesMintState {
7
- /** Set only after jose verification against DB pubkeys. */
8
9
  mint: IClientAbilitiesTokenMint | null;
9
- isLoading: boolean;
10
+ status: EnumJwtMintStatus;
10
11
  error: string | null;
11
12
  refreshMint: () => Promise<void>;
12
13
  }
13
14
  export interface ScoutRefreshAbilityParams {
14
- /** Mint and verify a client abilities JWT. Default: `true`. */
15
15
  mintClientAbilitiesToken?: boolean;
16
- /**
17
- * Expected JWT lifetime in seconds (default 1 hour).
18
- * Align with edge `CLIENT_ABILITIES_TOKEN_TTL_SEC`; used to schedule re-mint
19
- * (falls back when `exp` is missing; otherwise uses `exp` from the mint).
20
- */
16
+ mintPubsubToken?: boolean;
21
17
  clientAbilitiesTokenTtlSec?: number;
22
- /** Re-mint this many seconds before expiry (default 120). */
18
+ pubsubTokenTtlSec?: number;
23
19
  refreshBeforeExpirySec?: number;
24
- /** Ms to cache pubkey RPC results (default 10 min). */
25
20
  publicKeysCacheTtlMs?: number;
26
21
  }
22
+ export interface PubsubTokenMintState {
23
+ mint: IPubsubTokenMint | null;
24
+ status: EnumJwtMintStatus;
25
+ error: string | null;
26
+ refreshMint: () => Promise<void>;
27
+ }
27
28
  export declare function useSupabase(): SupabaseClient<Database>;
28
- /** Verified client abilities JWT (minted on a schedule by ScoutRefreshProvider). */
29
29
  export declare function useClientAbilitiesMint(): ClientAbilitiesMintState;
30
+ export declare function usePubsubTokenMint(): PubsubTokenMintState;
30
31
  export interface ScoutRefreshProviderProps extends UseScoutRefreshOptions {
31
32
  children: ReactNode;
32
33
  abilityParams?: ScoutRefreshAbilityParams;