@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.
- package/dist/helpers/cache.d.ts +10 -0
- package/dist/helpers/cache.js +86 -2
- package/dist/helpers/client_abilities_token.d.ts +0 -1
- package/dist/helpers/client_abilities_token.js +0 -1
- package/dist/helpers/pubsub_token.d.ts +3 -1
- package/dist/helpers/pubsub_token.js +41 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/providers/ScoutRefreshProvider.d.ts +12 -11
- package/dist/providers/ScoutRefreshProvider.js +85 -31
- package/dist/types/client_abilities_token.d.ts +1 -6
- package/dist/types/client_abilities_token.js +5 -3
- 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/package.json +1 -1
package/dist/helpers/cache.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/helpers/cache.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const DB_NAME = "ScoutCache";
|
|
2
|
-
const DB_VERSION =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
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
|
-
|
|
54
|
-
now -
|
|
55
|
-
return
|
|
56
|
+
keysRef.current?.length &&
|
|
57
|
+
now - keysAtRef.current < publicKeysCacheTtlMs) {
|
|
58
|
+
return keysRef.current;
|
|
56
59
|
}
|
|
57
|
-
const { status, data, msg } = await
|
|
60
|
+
const { status, data, msg } = await fetchKeys(supabase);
|
|
58
61
|
if (status !== EnumWebResponse.SUCCESS || !data?.length) {
|
|
59
|
-
throw new Error(msg ?? "
|
|
62
|
+
throw new Error(msg ?? "jwt public keys RPC returned no keys");
|
|
60
63
|
}
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
98
|
+
verified = await verifyToken(mintResponse.data, keys);
|
|
89
99
|
}
|
|
90
100
|
catch {
|
|
91
101
|
keys = await loadPublicKeys(true);
|
|
92
|
-
verified = await
|
|
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
|
-
|
|
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
|
-
|
|
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 () =>
|
|
123
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export
|
|
1
|
+
export const PUBSUB_TOKEN_TTL_SEC = 3600;
|