@adventurelabs/scout-core 1.4.75 → 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.
@@ -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");
@@ -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
+ }
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;
@@ -6,6 +6,10 @@ import { createBrowserClient } from "@supabase/ssr";
6
6
  import { CLIENT_ABILITIES_TOKEN_TTL_SEC, } from "../types/client_abilities_token";
7
7
  import { EnumWebResponse } from "../types/requests";
8
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";
9
13
  const ScoutRefreshContext = createContext(null);
10
14
  const DEFAULT_REFRESH_BEFORE_EXPIRY_SEC = 120;
11
15
  const DEFAULT_PUBLIC_KEYS_CACHE_MS = 10 * 60 * 1000;
@@ -25,102 +29,123 @@ function useScoutRefreshContext() {
25
29
  export function useSupabase() {
26
30
  return useScoutRefreshContext().supabase;
27
31
  }
28
- /** Verified client abilities JWT (minted on a schedule by ScoutRefreshProvider). */
29
32
  export function useClientAbilitiesMint() {
30
33
  return useScoutRefreshContext().abilities;
31
34
  }
35
+ export function usePubsubTokenMint() {
36
+ return useScoutRefreshContext().pubsub;
37
+ }
32
38
  function resolveAbilityParams(abilityParams) {
33
39
  return {
34
40
  mintClientAbilitiesToken: abilityParams?.mintClientAbilitiesToken ?? true,
41
+ mintPubsubToken: abilityParams?.mintPubsubToken ?? false,
35
42
  clientAbilitiesTokenTtlSec: abilityParams?.clientAbilitiesTokenTtlSec ??
36
43
  CLIENT_ABILITIES_TOKEN_TTL_SEC,
44
+ pubsubTokenTtlSec: abilityParams?.pubsubTokenTtlSec ?? PUBSUB_TOKEN_TTL_SEC,
37
45
  refreshBeforeExpirySec: abilityParams?.refreshBeforeExpirySec ??
38
46
  DEFAULT_REFRESH_BEFORE_EXPIRY_SEC,
39
47
  publicKeysCacheTtlMs: abilityParams?.publicKeysCacheTtlMs ?? DEFAULT_PUBLIC_KEYS_CACHE_MS,
40
48
  };
41
49
  }
42
- function useClientAbilitiesMintLifecycle(supabase, config) {
43
- const { mintClientAbilitiesToken: enabled, clientAbilitiesTokenTtlSec: ttlSec, refreshBeforeExpirySec, publicKeysCacheTtlMs, } = config;
44
- const [mint, setMint] = useState(null);
45
- const [isLoading, setIsLoading] = useState(false);
46
- const [error, setError] = useState(null);
47
- const inFlightRef = useRef(false);
48
- const publicKeysRef = useRef(null);
49
- const publicKeysAtRef = useRef(0);
50
- const loadPublicKeys = useCallback(async (force = false) => {
50
+ function useCachedPublicKeys(supabase, publicKeysCacheTtlMs, fetchKeys) {
51
+ const keysRef = useRef(null);
52
+ const keysAtRef = useRef(0);
53
+ return useCallback(async (force = false) => {
51
54
  const now = Date.now();
52
55
  if (!force &&
53
- publicKeysRef.current?.length &&
54
- now - publicKeysAtRef.current < publicKeysCacheTtlMs) {
55
- return publicKeysRef.current;
56
+ keysRef.current?.length &&
57
+ now - keysAtRef.current < publicKeysCacheTtlMs) {
58
+ return keysRef.current;
56
59
  }
57
- const { status, data, msg } = await get_client_abilities_jwt_public_keys(supabase);
60
+ const { status, data, msg } = await fetchKeys(supabase);
58
61
  if (status !== EnumWebResponse.SUCCESS || !data?.length) {
59
- throw new Error(msg ?? "get_client_abilities_jwt_public_keys returned no keys");
62
+ throw new Error(msg ?? "jwt public keys RPC returned no keys");
60
63
  }
61
- publicKeysRef.current = data;
62
- publicKeysAtRef.current = now;
64
+ keysRef.current = data;
65
+ keysAtRef.current = now;
63
66
  return data;
64
- }, [supabase, publicKeysCacheTtlMs]);
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]);
65
75
  const refreshMint = useCallback(async () => {
66
76
  if (!enabled || inFlightRef.current) {
67
77
  return;
68
78
  }
69
79
  inFlightRef.current = true;
70
- setIsLoading(true);
80
+ setInFlight(true);
71
81
  setError(null);
72
82
  try {
73
83
  const { data: { session }, } = await supabase.auth.getSession();
74
84
  if (!session) {
75
85
  setMint(null);
86
+ await scoutCache.clearJwtMint(cacheKey);
76
87
  return;
77
88
  }
78
- const mintResponse = await mint_client_abilities_token(supabase);
89
+ const mintResponse = await mintToken(supabase);
79
90
  if (mintResponse.status !== EnumWebResponse.SUCCESS ||
80
91
  !mintResponse.data) {
81
- setMint(null);
82
92
  setError(mintResponse.msg ?? "mint failed");
83
93
  return;
84
94
  }
85
95
  let keys = await loadPublicKeys();
86
96
  let verified;
87
97
  try {
88
- verified = await verify_client_abilities_token(mintResponse.data, keys);
98
+ verified = await verifyToken(mintResponse.data, keys);
89
99
  }
90
100
  catch {
91
101
  keys = await loadPublicKeys(true);
92
- verified = await verify_client_abilities_token(mintResponse.data, keys);
102
+ verified = await verifyToken(mintResponse.data, keys);
93
103
  }
94
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
+ }
95
111
  }
96
112
  catch (e) {
97
- setMint(null);
98
- setError(e instanceof Error ? e.message : "client abilities mint failed");
113
+ setError(e instanceof Error ? e.message : "mint failed");
99
114
  }
100
115
  finally {
101
116
  inFlightRef.current = false;
102
- setIsLoading(false);
117
+ setInFlight(false);
103
118
  }
104
- }, [enabled, supabase, loadPublicKeys]);
119
+ }, [enabled, supabase, cacheKey, loadPublicKeys, mintToken, verifyToken]);
105
120
  useEffect(() => {
106
121
  if (!enabled) {
107
122
  setMint(null);
108
123
  setError(null);
109
124
  return;
110
125
  }
126
+ let cancelled = false;
127
+ void scoutCache.getJwtMint(cacheKey).then((cached) => {
128
+ if (!cancelled && cached.data) {
129
+ setMint(cached.data);
130
+ }
131
+ });
111
132
  void refreshMint();
112
133
  const { data: { subscription }, } = supabase.auth.onAuthStateChange((event) => {
113
134
  if (event === "SIGNED_OUT") {
114
135
  setMint(null);
115
136
  setError(null);
137
+ void scoutCache.clearJwtMint(cacheKey);
116
138
  return;
117
139
  }
118
140
  if (event === "SIGNED_IN" || event === "TOKEN_REFRESHED") {
119
141
  void refreshMint();
120
142
  }
121
143
  });
122
- return () => subscription.unsubscribe();
123
- }, [enabled, supabase, refreshMint]);
144
+ return () => {
145
+ cancelled = true;
146
+ subscription.unsubscribe();
147
+ };
148
+ }, [enabled, supabase, cacheKey, refreshMint]);
124
149
  useEffect(() => {
125
150
  if (!enabled || !mint) {
126
151
  return;
@@ -134,7 +159,35 @@ function useClientAbilitiesMintLifecycle(supabase, config) {
134
159
  }, delayMs);
135
160
  return () => clearTimeout(timer);
136
161
  }, [enabled, mint, ttlSec, refreshBeforeExpirySec, refreshMint]);
137
- return useMemo(() => ({ mint, isLoading, error, refreshMint }), [mint, isLoading, error, 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
+ });
138
191
  }
139
192
  export function ScoutRefreshProvider({ children, abilityParams, ...refreshOptions }) {
140
193
  const urlRef = useRef(process.env.NEXT_PUBLIC_SUPABASE_URL || "");
@@ -143,6 +196,7 @@ export function ScoutRefreshProvider({ children, abilityParams, ...refreshOption
143
196
  const abilitiesConfig = useMemo(() => resolveAbilityParams(abilityParams), [abilityParams]);
144
197
  useScoutRefresh({ ...refreshOptions, supabase });
145
198
  const abilities = useClientAbilitiesMintLifecycle(supabase, abilitiesConfig);
146
- const value = useMemo(() => ({ supabase, abilities }), [supabase, abilities]);
199
+ const pubsub = usePubsubTokenMintLifecycle(supabase, abilitiesConfig);
200
+ const value = useMemo(() => ({ supabase, abilities, pubsub }), [supabase, abilities, pubsub]);
147
201
  return (_jsx(ScoutRefreshContext.Provider, { value: value, children: children }));
148
202
  }
@@ -1,10 +1,8 @@
1
- /** Herd entry inside a minted client abilities JWT (matches edge / RPC payload). */
2
1
  export interface IClientAbilitiesHerdClaim {
3
2
  herd_id: number;
4
3
  slug: string;
5
4
  abilities: string[];
6
5
  }
7
- /** Response from `mint-client-abilities-token`. */
8
6
  export interface IClientAbilitiesTokenMint {
9
7
  token: string;
10
8
  platform_superadmin: boolean;
@@ -12,13 +10,10 @@ export interface IClientAbilitiesTokenMint {
12
10
  iat: number;
13
11
  exp: number;
14
12
  }
15
- /** Entry from `get_client_abilities_jwt_public_keys()`. */
16
13
  export interface IClientAbilitiesJwtPublicKeyEntry {
17
14
  kid: string;
18
15
  jwk: Record<string, unknown>;
19
16
  }
20
17
  export declare const CLIENT_ABILITIES_JWT_AUDIENCE = "scout-client";
21
- /** Typical edge `CLIENT_ABILITIES_TOKEN_TTL_SEC` (scheduling uses JWT `exp` from mint). */
22
18
  export declare const CLIENT_ABILITIES_TOKEN_TTL_SEC = 3600;
23
- /** UI-only gate; RLS remains authoritative for data access. */
24
- export declare function can_herd_ability(mint: IClientAbilitiesTokenMint | null | undefined, herd_id: number, ability: string): boolean;
19
+ export declare function can_herd_ability(mint: IClientAbilitiesTokenMint | null | undefined, herd_id: number, ability: string, require_valid_expiry?: boolean): boolean;
@@ -1,11 +1,13 @@
1
1
  export const CLIENT_ABILITIES_JWT_AUDIENCE = "scout-client";
2
- /** Typical edge `CLIENT_ABILITIES_TOKEN_TTL_SEC` (scheduling uses JWT `exp` from mint). */
3
2
  export const CLIENT_ABILITIES_TOKEN_TTL_SEC = 3600;
4
- /** UI-only gate; RLS remains authoritative for data access. */
5
- export function can_herd_ability(mint, herd_id, ability) {
3
+ export function can_herd_ability(mint, herd_id, ability, require_valid_expiry = true) {
6
4
  if (!mint?.token) {
7
5
  return false;
8
6
  }
7
+ if (require_valid_expiry &&
8
+ mint.exp <= Math.floor(Date.now() / 1000)) {
9
+ return false;
10
+ }
9
11
  if (mint.platform_superadmin) {
10
12
  return true;
11
13
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adventurelabs/scout-core",
3
- "version": "1.4.75",
3
+ "version": "1.4.76",
4
4
  "description": "Core utilities and helpers for Adventure Labs Scout applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",