@proveanything/smartlinks 1.3.45 → 1.4.0
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/api/auth.d.ts +6 -0
- package/dist/api/auth.js +11 -1
- package/dist/docs/API_SUMMARY.md +26 -4
- package/dist/http.d.ts +95 -2
- package/dist/http.js +482 -87
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/persistentCache.d.ts +36 -0
- package/dist/persistentCache.js +178 -0
- package/dist/types/error.d.ts +36 -0
- package/dist/types/error.js +42 -0
- package/docs/API_SUMMARY.md +26 -4
- package/package.json +1 -1
package/dist/api/auth.d.ts
CHANGED
|
@@ -92,6 +92,12 @@ export declare namespace auth {
|
|
|
92
92
|
/**
|
|
93
93
|
* Gets current account information for the logged in user.
|
|
94
94
|
* Returns user, owner, account, and location objects.
|
|
95
|
+
*
|
|
96
|
+
* Short-circuits immediately (no network request) when the SDK has no
|
|
97
|
+
* bearer token or API key set — the server would return 401 anyway.
|
|
98
|
+
* Throws a `SmartlinksApiError` with `statusCode 401` and
|
|
99
|
+
* `details.local = true` so callers can distinguish "never authenticated"
|
|
100
|
+
* from an actual server-side token rejection.
|
|
95
101
|
*/
|
|
96
102
|
function getAccount(): Promise<AccountInfoResponse>;
|
|
97
103
|
}
|
package/dist/api/auth.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { post, request, setBearerToken, getApiHeaders } from "../http";
|
|
1
|
+
import { post, request, setBearerToken, getApiHeaders, hasAuthCredentials } from "../http";
|
|
2
|
+
import { SmartlinksApiError } from "../types/error";
|
|
2
3
|
/*
|
|
3
4
|
user: Record<string, any>
|
|
4
5
|
owner: Record<string, any>
|
|
@@ -84,8 +85,17 @@ export var auth;
|
|
|
84
85
|
/**
|
|
85
86
|
* Gets current account information for the logged in user.
|
|
86
87
|
* Returns user, owner, account, and location objects.
|
|
88
|
+
*
|
|
89
|
+
* Short-circuits immediately (no network request) when the SDK has no
|
|
90
|
+
* bearer token or API key set — the server would return 401 anyway.
|
|
91
|
+
* Throws a `SmartlinksApiError` with `statusCode 401` and
|
|
92
|
+
* `details.local = true` so callers can distinguish "never authenticated"
|
|
93
|
+
* from an actual server-side token rejection.
|
|
87
94
|
*/
|
|
88
95
|
async function getAccount() {
|
|
96
|
+
if (!hasAuthCredentials()) {
|
|
97
|
+
throw new SmartlinksApiError('Not authenticated: no bearer token or API key is set.', 401, { code: 401, errorCode: 'NOT_AUTHENTICATED', message: 'Not authenticated: no bearer token or API key is set.', details: { local: true } });
|
|
98
|
+
}
|
|
89
99
|
return request("/public/auth/account");
|
|
90
100
|
}
|
|
91
101
|
auth.getAccount = getAccount;
|
package/dist/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0 | Generated: 2026-02-20T14:52:16.722Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -87,7 +87,10 @@ Return whether proxy mode is currently enabled.
|
|
|
87
87
|
extraHeaders?: Record<string, string>
|
|
88
88
|
iframeAutoResize?: boolean // default true when in iframe
|
|
89
89
|
logger?: Logger // optional console-like or function to enable verbose logging
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* When true, bypasses the idempotency guard and forces a full re-initialization.
|
|
92
|
+
* Use only when you intentionally need to reset all SDK state (e.g. in tests or
|
|
93
|
+
* when switching accounts) → `void`
|
|
91
94
|
Call this once (e.g. at app startup) to configure baseURL/auth.
|
|
92
95
|
|
|
93
96
|
**setNgrokSkipBrowserWarning**(flag: boolean) → `void`
|
|
@@ -102,13 +105,32 @@ Allows setting the bearerToken at runtime (e.g. after login/logout).
|
|
|
102
105
|
**getBaseURL**() → `string | null`
|
|
103
106
|
Get the currently configured API base URL. Returns null if initializeApi() has not been called yet.
|
|
104
107
|
|
|
108
|
+
**isInitialized**() → `boolean`
|
|
109
|
+
Returns true if initializeApi() has been called at least once. Useful for guards in widgets or shared modules that want to skip initialization when another module has already done it. ```ts if (!isInitialized()) { initializeApi({ baseURL: 'https://smartlinks.app/api/v1' }) } ```
|
|
110
|
+
|
|
111
|
+
**hasAuthCredentials**() → `boolean`
|
|
112
|
+
Returns true if the SDK currently has any auth credential set (bearer token or API key). Use this as a cheap pre-flight check before calling endpoints that require authentication, to avoid issuing a network request that you already know will return a 401. ```ts if (hasAuthCredentials()) { const account = await auth.getAccount() } ```
|
|
113
|
+
|
|
114
|
+
**configureSdkCache**(options: {
|
|
115
|
+
enabled?: boolean
|
|
116
|
+
ttlMs?: number
|
|
117
|
+
maxEntries?: number
|
|
118
|
+
persistence?: 'none' | 'indexeddb'
|
|
119
|
+
persistenceTtlMs?: number
|
|
120
|
+
serveStaleOnOffline?: boolean
|
|
121
|
+
}) → `void`
|
|
122
|
+
Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) ```
|
|
123
|
+
|
|
124
|
+
**invalidateCache**(urlPattern?: string) → `void`
|
|
125
|
+
Manually invalidate entries in the SDK's GET cache. *contains* this string is removed. Omit (or pass `undefined`) to wipe the entire cache. ```ts invalidateCache() // clear everything invalidateCache('/collection/abc123') // one specific collection invalidateCache('/product/') // all product responses ```
|
|
126
|
+
|
|
105
127
|
**proxyUploadFormData**(path: string,
|
|
106
128
|
formData: FormData,
|
|
107
129
|
onProgress?: (percent: number) → `void`
|
|
108
130
|
Upload a FormData payload via proxy with progress events using chunked postMessage. Parent is expected to implement the counterpart protocol.
|
|
109
131
|
|
|
110
132
|
**request**(path: string) → `Promise<T>`
|
|
111
|
-
Internal helper that performs a GET request to
|
|
133
|
+
Internal helper that performs a GET request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. Cache pipeline (when caching is not skipped): L1 hit → return from memory (no I/O) L2 hit → return from IndexedDB, promote to L1 (no network) Miss → fetch from network, store in L1 + L2 Offline → serve stale L2 entry via SmartlinksOfflineError (if persistence enabled) Concurrent identical GETs share one in-flight promise (deduplication). Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
|
|
112
134
|
|
|
113
135
|
**post**(path: string,
|
|
114
136
|
body: any,
|
|
@@ -4122,7 +4144,7 @@ Tries to register a new user account. Can return a bearer token, or a Firebase t
|
|
|
4122
4144
|
Admin: Get a user bearer token (impersonation/automation). POST /admin/auth/userToken All fields are optional; at least one identifier should be provided.
|
|
4123
4145
|
|
|
4124
4146
|
**getAccount**() → `Promise<AccountInfoResponse>`
|
|
4125
|
-
Gets current account information for the logged in user. Returns user, owner, account, and location objects.
|
|
4147
|
+
Gets current account information for the logged in user. Returns user, owner, account, and location objects. Short-circuits immediately (no network request) when the SDK has no bearer token or API key set — the server would return 401 anyway. Throws a `SmartlinksApiError` with `statusCode 401` and `details.local = true` so callers can distinguish "never authenticated" from an actual server-side token rejection.
|
|
4126
4148
|
|
|
4127
4149
|
### authKit
|
|
4128
4150
|
|
package/dist/http.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export declare function initializeApi(options: {
|
|
|
16
16
|
extraHeaders?: Record<string, string>;
|
|
17
17
|
iframeAutoResize?: boolean;
|
|
18
18
|
logger?: Logger;
|
|
19
|
+
/**
|
|
20
|
+
* When true, bypasses the idempotency guard and forces a full re-initialization.
|
|
21
|
+
* Use only when you intentionally need to reset all SDK state (e.g. in tests or
|
|
22
|
+
* when switching accounts). In normal application code, prefer letting the guard
|
|
23
|
+
* protect runtime state such as login tokens.
|
|
24
|
+
*/
|
|
25
|
+
force?: boolean;
|
|
19
26
|
}): void;
|
|
20
27
|
/** Enable/disable automatic "ngrok-skip-browser-warning" header. */
|
|
21
28
|
export declare function setNgrokSkipBrowserWarning(flag: boolean): void;
|
|
@@ -30,15 +37,101 @@ export declare function setBearerToken(token: string | undefined): void;
|
|
|
30
37
|
* Returns null if initializeApi() has not been called yet.
|
|
31
38
|
*/
|
|
32
39
|
export declare function getBaseURL(): string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Returns true if initializeApi() has been called at least once.
|
|
42
|
+
* Useful for guards in widgets or shared modules that want to skip
|
|
43
|
+
* initialization when another module has already done it.
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* ```ts
|
|
47
|
+
* if (!isInitialized()) {
|
|
48
|
+
* initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export declare function isInitialized(): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Returns true if the SDK currently has any auth credential set (bearer token
|
|
55
|
+
* or API key). Use this as a cheap pre-flight check before calling endpoints
|
|
56
|
+
* that require authentication, to avoid issuing a network request that you
|
|
57
|
+
* already know will return a 401.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* if (hasAuthCredentials()) {
|
|
62
|
+
* const account = await auth.getAccount()
|
|
63
|
+
* }
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export declare function hasAuthCredentials(): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Configure the SDK's built-in in-memory GET cache.
|
|
69
|
+
*
|
|
70
|
+
* The cache is transparent — it sits inside the HTTP layer and requires no
|
|
71
|
+
* changes to your existing API calls. All GET requests benefit automatically.
|
|
72
|
+
*
|
|
73
|
+
* @param options.enabled - Turn caching on/off entirely (default: `true`)
|
|
74
|
+
* @param options.ttlMs - Default time-to-live in milliseconds (default: `60_000`).
|
|
75
|
+
* Per-resource rules (collections/products → 1 h,
|
|
76
|
+
* proofs → 30 s, etc.) override this value.
|
|
77
|
+
* @param options.maxEntries - L1 LRU eviction threshold (default: `200`)
|
|
78
|
+
* @param options.persistence - Enable IndexedDB L2 cache (`'indexeddb'`) or keep
|
|
79
|
+
* in-memory only (`'none'`, default). Ignored in Node.js.
|
|
80
|
+
* @param options.persistenceTtlMs - How long L2 entries are eligible as an offline stale
|
|
81
|
+
* fallback, from the original fetch time (default: 7 days).
|
|
82
|
+
* @param options.serveStaleOnOffline - When `true` (default) and persistence is on, throw
|
|
83
|
+
* `SmartlinksOfflineError` with stale data instead of
|
|
84
|
+
* propagating the network error.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* // Enable IndexedDB persistence for offline support
|
|
89
|
+
* configureSdkCache({ persistence: 'indexeddb' })
|
|
90
|
+
*
|
|
91
|
+
* // Disable cache entirely in test environments
|
|
92
|
+
* configureSdkCache({ enabled: false })
|
|
93
|
+
* ```
|
|
94
|
+
*/
|
|
95
|
+
export declare function configureSdkCache(options: {
|
|
96
|
+
enabled?: boolean;
|
|
97
|
+
ttlMs?: number;
|
|
98
|
+
maxEntries?: number;
|
|
99
|
+
persistence?: 'none' | 'indexeddb';
|
|
100
|
+
persistenceTtlMs?: number;
|
|
101
|
+
serveStaleOnOffline?: boolean;
|
|
102
|
+
}): void;
|
|
103
|
+
/**
|
|
104
|
+
* Manually invalidate entries in the SDK's GET cache.
|
|
105
|
+
*
|
|
106
|
+
* @param urlPattern - Optional substring match. Every cache entry whose key
|
|
107
|
+
* *contains* this string is removed. Omit (or pass `undefined`) to wipe the
|
|
108
|
+
* entire cache.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* invalidateCache() // clear everything
|
|
113
|
+
* invalidateCache('/collection/abc123') // one specific collection
|
|
114
|
+
* invalidateCache('/product/') // all product responses
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export declare function invalidateCache(urlPattern?: string): void;
|
|
33
118
|
/**
|
|
34
119
|
* Upload a FormData payload via proxy with progress events using chunked postMessage.
|
|
35
120
|
* Parent is expected to implement the counterpart protocol.
|
|
36
121
|
*/
|
|
37
122
|
export declare function proxyUploadFormData<T>(path: string, formData: FormData, onProgress?: (percent: number) => void): Promise<T>;
|
|
38
123
|
/**
|
|
39
|
-
* Internal helper that performs a GET request to
|
|
124
|
+
* Internal helper that performs a GET request to `${baseURL}${path}`,
|
|
40
125
|
* injecting headers for apiKey or bearerToken if present.
|
|
41
|
-
*
|
|
126
|
+
*
|
|
127
|
+
* Cache pipeline (when caching is not skipped):
|
|
128
|
+
* L1 hit → return from memory (no I/O)
|
|
129
|
+
* L2 hit → return from IndexedDB, promote to L1 (no network)
|
|
130
|
+
* Miss → fetch from network, store in L1 + L2
|
|
131
|
+
* Offline → serve stale L2 entry via SmartlinksOfflineError (if persistence enabled)
|
|
132
|
+
*
|
|
133
|
+
* Concurrent identical GETs share one in-flight promise (deduplication).
|
|
134
|
+
* Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
|
|
42
135
|
*/
|
|
43
136
|
export declare function request<T>(path: string): Promise<T>;
|
|
44
137
|
/**
|
package/dist/http.js
CHANGED
|
@@ -13,13 +13,135 @@ var __rest = (this && this.__rest) || function (s, e) {
|
|
|
13
13
|
}
|
|
14
14
|
return t;
|
|
15
15
|
};
|
|
16
|
-
import { SmartlinksApiError } from "./types/error";
|
|
16
|
+
import { SmartlinksApiError, SmartlinksOfflineError } from "./types/error";
|
|
17
|
+
import { idbGet, idbSet, idbClear } from './persistentCache';
|
|
17
18
|
let baseURL = null;
|
|
18
19
|
let apiKey = undefined;
|
|
19
20
|
let bearerToken = undefined;
|
|
20
21
|
let proxyMode = false;
|
|
21
22
|
let ngrokSkipBrowserWarning = false;
|
|
22
23
|
let extraHeadersGlobal = {};
|
|
24
|
+
/** Whether initializeApi has been successfully called at least once. */
|
|
25
|
+
let initialized = false;
|
|
26
|
+
const httpCache = new Map();
|
|
27
|
+
let cacheEnabled = true;
|
|
28
|
+
/** Default TTL used when no per-resource rule matches (milliseconds). */
|
|
29
|
+
let cacheDefaultTtlMs = 60000; // 60 seconds
|
|
30
|
+
/** Maximum number of entries before the oldest (LRU) entry is evicted. */
|
|
31
|
+
let cacheMaxEntries = 200;
|
|
32
|
+
/** Persistence backend for the L2 cache. 'none' (default) disables IndexedDB persistence. */
|
|
33
|
+
let cachePersistence = 'none';
|
|
34
|
+
/**
|
|
35
|
+
* How long L2 (IndexedDB) entries are considered valid as an offline stale fallback,
|
|
36
|
+
* measured from the original network fetch time (default: 7 days).
|
|
37
|
+
* This is independent of the in-memory TTL — it only governs whether a stale
|
|
38
|
+
* L2 entry is served when the network is unavailable.
|
|
39
|
+
*/
|
|
40
|
+
let cachePersistenceTtlMs = 7 * 24 * 60 * 60000;
|
|
41
|
+
/** When true (default), serve stale L2 data via SmartlinksOfflineError on network failure. */
|
|
42
|
+
let cacheServeStaleOnOffline = true;
|
|
43
|
+
/**
|
|
44
|
+
* Per-resource TTL overrides — checked in order, first match wins.
|
|
45
|
+
*
|
|
46
|
+
* Rules are intentionally specific: they match the collection/product/variant
|
|
47
|
+
* *resource itself* (list or detail) but NOT sub-resources nested beneath them
|
|
48
|
+
* (assets, forms, jobs, app-data, etc.). The regex requires that nothing except
|
|
49
|
+
* an optional query-string follows the matched segment, e.g.:
|
|
50
|
+
* ✅ /public/collection/abc123
|
|
51
|
+
* ✅ /public/collection (list)
|
|
52
|
+
* ❌ /public/collection/abc123/asset/xyz → falls to default TTL
|
|
53
|
+
* ❌ /public/collection/abc123/form/xyz → falls to default TTL
|
|
54
|
+
*
|
|
55
|
+
* More-specific / shorter TTLs are listed first so they cannot be shadowed.
|
|
56
|
+
*/
|
|
57
|
+
const CACHE_TTL_RULES = [
|
|
58
|
+
// Sub-resources that change frequently — short TTLs, listed first
|
|
59
|
+
{ pattern: /\/proof\/[^/]*(\?.*)?$/i, ttlMs: 30000 },
|
|
60
|
+
{ pattern: /\/attestation\/[^/]*(\?.*)?$/i, ttlMs: 2 * 60000 },
|
|
61
|
+
// Slow-changing top-level resources — long TTLs, matched only when path ends at the ID
|
|
62
|
+
{ pattern: /\/product\/[^/]*(\?.*)?$/i, ttlMs: 60 * 60000 },
|
|
63
|
+
{ pattern: /\/variant\/[^/]*(\?.*)?$/i, ttlMs: 60 * 60000 },
|
|
64
|
+
{ pattern: /\/collection\/[^/]*(\?.*)?$/i, ttlMs: 60 * 60000 }, // 1 hour
|
|
65
|
+
];
|
|
66
|
+
function getTtlForPath(path) {
|
|
67
|
+
for (const rule of CACHE_TTL_RULES) {
|
|
68
|
+
if (rule.pattern.test(path))
|
|
69
|
+
return rule.ttlMs;
|
|
70
|
+
}
|
|
71
|
+
return cacheDefaultTtlMs;
|
|
72
|
+
}
|
|
73
|
+
/** Returns true when this path must always bypass the cache. */
|
|
74
|
+
function shouldSkipCache(path) {
|
|
75
|
+
if (!cacheEnabled)
|
|
76
|
+
return true;
|
|
77
|
+
// Never cache auth endpoints — they deal with tokens and session state.
|
|
78
|
+
if (/\/auth\//i.test(path))
|
|
79
|
+
return true;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
/** Evict the oldest (LRU) entry when the cache is at capacity. */
|
|
83
|
+
function evictLruIfNeeded() {
|
|
84
|
+
while (httpCache.size >= cacheMaxEntries) {
|
|
85
|
+
const firstKey = httpCache.keys().next().value;
|
|
86
|
+
if (firstKey !== undefined)
|
|
87
|
+
httpCache.delete(firstKey);
|
|
88
|
+
else
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Return cached data for a key if it exists and is within TTL.
|
|
94
|
+
* Promotes the hit to MRU position. Returns null when missing, expired, or in-flight.
|
|
95
|
+
*/
|
|
96
|
+
function getHttpCacheHit(cacheKey, ttlMs) {
|
|
97
|
+
const entry = httpCache.get(cacheKey);
|
|
98
|
+
if (!entry || entry.promise)
|
|
99
|
+
return null;
|
|
100
|
+
if (Date.now() - entry.timestamp > ttlMs) {
|
|
101
|
+
httpCache.delete(cacheKey);
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// Promote to MRU (delete + re-insert at tail of Map)
|
|
105
|
+
httpCache.delete(cacheKey);
|
|
106
|
+
httpCache.set(cacheKey, entry);
|
|
107
|
+
return entry.data;
|
|
108
|
+
}
|
|
109
|
+
/** Store a resolved response in the cache at MRU position. */
|
|
110
|
+
function setHttpCacheEntry(cacheKey, data) {
|
|
111
|
+
httpCache.delete(cacheKey); // ensure insertion at MRU tail
|
|
112
|
+
evictLruIfNeeded();
|
|
113
|
+
httpCache.set(cacheKey, { data, timestamp: Date.now() });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Auto-invalidate all cached GET entries whose key contains `path`.
|
|
117
|
+
* Called automatically after any mutating request (POST / PUT / PATCH / DELETE).
|
|
118
|
+
* Also sweeps the L2 (IndexedDB) cache when persistence is enabled.
|
|
119
|
+
*/
|
|
120
|
+
function invalidateCacheForPath(path) {
|
|
121
|
+
for (const key of httpCache.keys()) {
|
|
122
|
+
if (key.includes(path))
|
|
123
|
+
httpCache.delete(key);
|
|
124
|
+
}
|
|
125
|
+
if (cachePersistence !== 'none')
|
|
126
|
+
idbClear(path).catch(() => { });
|
|
127
|
+
}
|
|
128
|
+
/** Build the lookup key for a given request path. */
|
|
129
|
+
function buildCacheKey(path) {
|
|
130
|
+
return proxyMode ? `proxy:${path}` : `${baseURL}${path}`;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns true when an error indicates a network-level failure (no connectivity,
|
|
134
|
+
* DNS failure, etc.) rather than an HTTP-level error response.
|
|
135
|
+
* Also returns true when navigator.onLine is explicitly false.
|
|
136
|
+
* Node-safe: navigator is guarded before access.
|
|
137
|
+
*/
|
|
138
|
+
function isNetworkError(err) {
|
|
139
|
+
// navigator is not available in Node.js — guard before access
|
|
140
|
+
if (typeof navigator !== 'undefined' && navigator.onLine === false)
|
|
141
|
+
return true;
|
|
142
|
+
// fetch() throws TypeError on network failure; SmartlinksApiError is not a TypeError
|
|
143
|
+
return err instanceof TypeError;
|
|
144
|
+
}
|
|
23
145
|
let logger;
|
|
24
146
|
function logDebug(...args) {
|
|
25
147
|
if (!logger)
|
|
@@ -169,9 +291,32 @@ function normalizeErrorResponse(responseBody, statusCode) {
|
|
|
169
291
|
import { iframe } from './iframe';
|
|
170
292
|
export function initializeApi(options) {
|
|
171
293
|
// Normalize baseURL by removing trailing slashes.
|
|
172
|
-
|
|
294
|
+
const normalizedBaseURL = options.baseURL.replace(/\/+$/g, "");
|
|
295
|
+
// ------------------------------------------------------------------
|
|
296
|
+
// Firebase-style idempotency guard
|
|
297
|
+
// If we have already been initialized with the same baseURL and the
|
|
298
|
+
// caller is not forcing a reset, return immediately. This prevents
|
|
299
|
+
// any module – widget, component, or re-rendered page – from
|
|
300
|
+
// accidentally wiping runtime state such as a bearerToken that was
|
|
301
|
+
// set by auth.login() after the first initialization.
|
|
302
|
+
// ------------------------------------------------------------------
|
|
303
|
+
if (initialized && !options.force && baseURL === normalizedBaseURL) {
|
|
304
|
+
logDebug('[smartlinks] initializeApi: already initialized with this baseURL – skipping.', { baseURL });
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
baseURL = normalizedBaseURL;
|
|
173
308
|
apiKey = options.apiKey;
|
|
174
|
-
bearerToken
|
|
309
|
+
// Only overwrite bearerToken when the caller explicitly supplies one,
|
|
310
|
+
// OR when this is the very first initialization (start with a clean slate).
|
|
311
|
+
// Re-initialization calls that omit bearerToken must NOT clear a token that
|
|
312
|
+
// was acquired at runtime (e.g. from a successful auth.login()).
|
|
313
|
+
if (options.bearerToken !== undefined) {
|
|
314
|
+
bearerToken = options.bearerToken;
|
|
315
|
+
}
|
|
316
|
+
else if (!initialized) {
|
|
317
|
+
bearerToken = undefined;
|
|
318
|
+
}
|
|
319
|
+
// else: preserve the existing runtime bearerToken.
|
|
175
320
|
proxyMode = !!options.proxyMode;
|
|
176
321
|
// Auto-enable ngrok skip header if domain contains .ngrok.io and user did not explicitly set the flag.
|
|
177
322
|
// Infer ngrok usage from common domains (.ngrok.io or .ngrok-free.dev)
|
|
@@ -184,7 +329,14 @@ export function initializeApi(options) {
|
|
|
184
329
|
if (iframe.isIframe() && options.iframeAutoResize !== false) {
|
|
185
330
|
iframe.enableAutoIframeResize();
|
|
186
331
|
}
|
|
332
|
+
// Clear both cache tiers on forced re-initialization so stale data
|
|
333
|
+
// from the previous configuration cannot bleed through.
|
|
334
|
+
if (options.force) {
|
|
335
|
+
httpCache.clear();
|
|
336
|
+
idbClear().catch(() => { });
|
|
337
|
+
}
|
|
187
338
|
logger = options.logger;
|
|
339
|
+
initialized = true;
|
|
188
340
|
logDebug('[smartlinks] initializeApi', {
|
|
189
341
|
baseURL,
|
|
190
342
|
proxyMode,
|
|
@@ -215,6 +367,107 @@ export function setBearerToken(token) {
|
|
|
215
367
|
export function getBaseURL() {
|
|
216
368
|
return baseURL;
|
|
217
369
|
}
|
|
370
|
+
/**
|
|
371
|
+
* Returns true if initializeApi() has been called at least once.
|
|
372
|
+
* Useful for guards in widgets or shared modules that want to skip
|
|
373
|
+
* initialization when another module has already done it.
|
|
374
|
+
*
|
|
375
|
+
* @example
|
|
376
|
+
* ```ts
|
|
377
|
+
* if (!isInitialized()) {
|
|
378
|
+
* initializeApi({ baseURL: 'https://smartlinks.app/api/v1' })
|
|
379
|
+
* }
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
export function isInitialized() {
|
|
383
|
+
return initialized;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Returns true if the SDK currently has any auth credential set (bearer token
|
|
387
|
+
* or API key). Use this as a cheap pre-flight check before calling endpoints
|
|
388
|
+
* that require authentication, to avoid issuing a network request that you
|
|
389
|
+
* already know will return a 401.
|
|
390
|
+
*
|
|
391
|
+
* @example
|
|
392
|
+
* ```ts
|
|
393
|
+
* if (hasAuthCredentials()) {
|
|
394
|
+
* const account = await auth.getAccount()
|
|
395
|
+
* }
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
export function hasAuthCredentials() {
|
|
399
|
+
return !!(bearerToken || apiKey);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Configure the SDK's built-in in-memory GET cache.
|
|
403
|
+
*
|
|
404
|
+
* The cache is transparent — it sits inside the HTTP layer and requires no
|
|
405
|
+
* changes to your existing API calls. All GET requests benefit automatically.
|
|
406
|
+
*
|
|
407
|
+
* @param options.enabled - Turn caching on/off entirely (default: `true`)
|
|
408
|
+
* @param options.ttlMs - Default time-to-live in milliseconds (default: `60_000`).
|
|
409
|
+
* Per-resource rules (collections/products → 1 h,
|
|
410
|
+
* proofs → 30 s, etc.) override this value.
|
|
411
|
+
* @param options.maxEntries - L1 LRU eviction threshold (default: `200`)
|
|
412
|
+
* @param options.persistence - Enable IndexedDB L2 cache (`'indexeddb'`) or keep
|
|
413
|
+
* in-memory only (`'none'`, default). Ignored in Node.js.
|
|
414
|
+
* @param options.persistenceTtlMs - How long L2 entries are eligible as an offline stale
|
|
415
|
+
* fallback, from the original fetch time (default: 7 days).
|
|
416
|
+
* @param options.serveStaleOnOffline - When `true` (default) and persistence is on, throw
|
|
417
|
+
* `SmartlinksOfflineError` with stale data instead of
|
|
418
|
+
* propagating the network error.
|
|
419
|
+
*
|
|
420
|
+
* @example
|
|
421
|
+
* ```ts
|
|
422
|
+
* // Enable IndexedDB persistence for offline support
|
|
423
|
+
* configureSdkCache({ persistence: 'indexeddb' })
|
|
424
|
+
*
|
|
425
|
+
* // Disable cache entirely in test environments
|
|
426
|
+
* configureSdkCache({ enabled: false })
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
export function configureSdkCache(options) {
|
|
430
|
+
if (options.enabled !== undefined)
|
|
431
|
+
cacheEnabled = options.enabled;
|
|
432
|
+
if (options.ttlMs !== undefined)
|
|
433
|
+
cacheDefaultTtlMs = options.ttlMs;
|
|
434
|
+
if (options.maxEntries !== undefined)
|
|
435
|
+
cacheMaxEntries = options.maxEntries;
|
|
436
|
+
if (options.persistence !== undefined)
|
|
437
|
+
cachePersistence = options.persistence;
|
|
438
|
+
if (options.persistenceTtlMs !== undefined)
|
|
439
|
+
cachePersistenceTtlMs = options.persistenceTtlMs;
|
|
440
|
+
if (options.serveStaleOnOffline !== undefined)
|
|
441
|
+
cacheServeStaleOnOffline = options.serveStaleOnOffline;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Manually invalidate entries in the SDK's GET cache.
|
|
445
|
+
*
|
|
446
|
+
* @param urlPattern - Optional substring match. Every cache entry whose key
|
|
447
|
+
* *contains* this string is removed. Omit (or pass `undefined`) to wipe the
|
|
448
|
+
* entire cache.
|
|
449
|
+
*
|
|
450
|
+
* @example
|
|
451
|
+
* ```ts
|
|
452
|
+
* invalidateCache() // clear everything
|
|
453
|
+
* invalidateCache('/collection/abc123') // one specific collection
|
|
454
|
+
* invalidateCache('/product/') // all product responses
|
|
455
|
+
* ```
|
|
456
|
+
*/
|
|
457
|
+
export function invalidateCache(urlPattern) {
|
|
458
|
+
if (!urlPattern) {
|
|
459
|
+
httpCache.clear();
|
|
460
|
+
if (cachePersistence !== 'none')
|
|
461
|
+
idbClear().catch(() => { });
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
for (const key of httpCache.keys()) {
|
|
465
|
+
if (key.includes(urlPattern))
|
|
466
|
+
httpCache.delete(key);
|
|
467
|
+
}
|
|
468
|
+
if (cachePersistence !== 'none')
|
|
469
|
+
idbClear(urlPattern).catch(() => { });
|
|
470
|
+
}
|
|
218
471
|
// Map of pending proxy requests: id -> {resolve, reject}
|
|
219
472
|
const proxyPending = {};
|
|
220
473
|
function generateProxyId() {
|
|
@@ -408,49 +661,114 @@ export async function proxyUploadFormData(path, formData, onProgress) {
|
|
|
408
661
|
return done;
|
|
409
662
|
}
|
|
410
663
|
/**
|
|
411
|
-
* Internal helper that performs a GET request to
|
|
664
|
+
* Internal helper that performs a GET request to `${baseURL}${path}`,
|
|
412
665
|
* injecting headers for apiKey or bearerToken if present.
|
|
413
|
-
*
|
|
666
|
+
*
|
|
667
|
+
* Cache pipeline (when caching is not skipped):
|
|
668
|
+
* L1 hit → return from memory (no I/O)
|
|
669
|
+
* L2 hit → return from IndexedDB, promote to L1 (no network)
|
|
670
|
+
* Miss → fetch from network, store in L1 + L2
|
|
671
|
+
* Offline → serve stale L2 entry via SmartlinksOfflineError (if persistence enabled)
|
|
672
|
+
*
|
|
673
|
+
* Concurrent identical GETs share one in-flight promise (deduplication).
|
|
674
|
+
* Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
|
|
414
675
|
*/
|
|
415
676
|
export async function request(path) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
677
|
+
const skipCache = shouldSkipCache(path);
|
|
678
|
+
const cacheKey = buildCacheKey(path);
|
|
679
|
+
const ttl = skipCache ? 0 : getTtlForPath(path);
|
|
680
|
+
if (!skipCache) {
|
|
681
|
+
// 1. L1 hit — return from memory immediately
|
|
682
|
+
const l1 = getHttpCacheHit(cacheKey, ttl);
|
|
683
|
+
if (l1 !== null) {
|
|
684
|
+
logDebug('[smartlinks] GET cache hit (L1)', { path });
|
|
685
|
+
return l1;
|
|
686
|
+
}
|
|
687
|
+
// 2. In-flight deduplication — share an already-pending promise
|
|
688
|
+
const inflight = httpCache.get(cacheKey);
|
|
689
|
+
if (inflight === null || inflight === void 0 ? void 0 : inflight.promise) {
|
|
690
|
+
logDebug('[smartlinks] GET in-flight dedup', { path });
|
|
691
|
+
return inflight.promise;
|
|
692
|
+
}
|
|
422
693
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
logDebug('[smartlinks] GET response', { url, status: response.status, ok: response.ok });
|
|
439
|
-
if (!response.ok) {
|
|
440
|
-
// Try to parse error response body and normalize it
|
|
441
|
-
let responseBody;
|
|
694
|
+
// 3. Build the fetch promise.
|
|
695
|
+
// The IIFE starts synchronously until its first `await`, then the outer
|
|
696
|
+
// code registers it as the in-flight entry before the await resolves.
|
|
697
|
+
const fetchPromise = (async () => {
|
|
698
|
+
// 3a. L2 (IndexedDB) check — warms L1 from persistent storage without a
|
|
699
|
+
// network round-trip. First await → in-flight registration runs before this resolves.
|
|
700
|
+
if (!skipCache && cachePersistence !== 'none') {
|
|
701
|
+
const l2 = await idbGet(cacheKey);
|
|
702
|
+
if (l2 && Date.now() - l2.timestamp <= ttl) {
|
|
703
|
+
logDebug('[smartlinks] GET cache hit (L2)', { path });
|
|
704
|
+
setHttpCacheEntry(cacheKey, l2.data);
|
|
705
|
+
return l2.data;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// 3b. Network fetch
|
|
442
709
|
try {
|
|
443
|
-
|
|
710
|
+
let data;
|
|
711
|
+
if (proxyMode) {
|
|
712
|
+
logDebug('[smartlinks] GET via proxy', { path });
|
|
713
|
+
data = await proxyRequest("GET", path);
|
|
714
|
+
}
|
|
715
|
+
else {
|
|
716
|
+
if (!baseURL) {
|
|
717
|
+
throw new Error("HTTP client is not initialized. Call initializeApi(...) first.");
|
|
718
|
+
}
|
|
719
|
+
const url = `${baseURL}${path}`;
|
|
720
|
+
const headers = { "Content-Type": "application/json" };
|
|
721
|
+
if (apiKey)
|
|
722
|
+
headers["X-API-Key"] = apiKey;
|
|
723
|
+
if (bearerToken)
|
|
724
|
+
headers["AUTHORIZATION"] = `Bearer ${bearerToken}`;
|
|
725
|
+
if (ngrokSkipBrowserWarning)
|
|
726
|
+
headers["ngrok-skip-browser-warning"] = "true";
|
|
727
|
+
for (const [k, v] of Object.entries(extraHeadersGlobal))
|
|
728
|
+
headers[k] = v;
|
|
729
|
+
logDebug('[smartlinks] GET fetch', { url, headers: redactHeaders(headers) });
|
|
730
|
+
const response = await fetch(url, { method: "GET", headers });
|
|
731
|
+
logDebug('[smartlinks] GET response', { url, status: response.status, ok: response.ok });
|
|
732
|
+
if (!response.ok) {
|
|
733
|
+
let responseBody;
|
|
734
|
+
try {
|
|
735
|
+
responseBody = await response.json();
|
|
736
|
+
}
|
|
737
|
+
catch (_a) {
|
|
738
|
+
responseBody = null;
|
|
739
|
+
}
|
|
740
|
+
const errBody = normalizeErrorResponse(responseBody, response.status);
|
|
741
|
+
throw new SmartlinksApiError(`Error ${errBody.code}: ${errBody.message}`, response.status, errBody, url);
|
|
742
|
+
}
|
|
743
|
+
data = (await response.json());
|
|
744
|
+
}
|
|
745
|
+
// Persist to L2 on success (fire-and-forget — never blocks the caller)
|
|
746
|
+
if (!skipCache && cachePersistence !== 'none') {
|
|
747
|
+
idbSet(cacheKey, { data, timestamp: Date.now(), persistedAt: Date.now() }).catch(() => { });
|
|
748
|
+
}
|
|
749
|
+
return data;
|
|
444
750
|
}
|
|
445
|
-
catch (
|
|
446
|
-
//
|
|
447
|
-
|
|
751
|
+
catch (fetchErr) {
|
|
752
|
+
// 3c. Offline fallback: when the network fails, serve stale L2 data if
|
|
753
|
+
// it's still within the persistence TTL window.
|
|
754
|
+
if (!skipCache && cachePersistence !== 'none' && cacheServeStaleOnOffline && isNetworkError(fetchErr)) {
|
|
755
|
+
const l2 = await idbGet(cacheKey);
|
|
756
|
+
if (l2 && Date.now() - l2.timestamp <= cachePersistenceTtlMs) {
|
|
757
|
+
logDebug('[smartlinks] GET offline fallback (L2)', { path, cachedAt: l2.timestamp });
|
|
758
|
+
throw new SmartlinksOfflineError('Network unavailable — serving cached data from persistent storage.', l2.data, l2.timestamp);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
throw fetchErr;
|
|
448
762
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
763
|
+
})();
|
|
764
|
+
// Register the in-flight promise so concurrent identical GETs share it.
|
|
765
|
+
// On resolve → promote to L1; on reject → remove so the next call retries.
|
|
766
|
+
if (!skipCache) {
|
|
767
|
+
evictLruIfNeeded();
|
|
768
|
+
httpCache.set(cacheKey, { data: null, timestamp: Date.now(), promise: fetchPromise });
|
|
769
|
+
fetchPromise.then((data) => setHttpCacheEntry(cacheKey, data), () => httpCache.delete(cacheKey));
|
|
452
770
|
}
|
|
453
|
-
return
|
|
771
|
+
return fetchPromise;
|
|
454
772
|
}
|
|
455
773
|
/**
|
|
456
774
|
* Internal helper that performs a POST request to `${baseURL}${path}`,
|
|
@@ -461,7 +779,9 @@ export async function request(path) {
|
|
|
461
779
|
export async function post(path, body, extraHeaders) {
|
|
462
780
|
if (proxyMode) {
|
|
463
781
|
logDebug('[smartlinks] POST via proxy', { path, body: safeBodyPreview(body) });
|
|
464
|
-
|
|
782
|
+
const result = await proxyRequest("POST", path, body, extraHeaders);
|
|
783
|
+
invalidateCacheForPath(path);
|
|
784
|
+
return result;
|
|
465
785
|
}
|
|
466
786
|
if (!baseURL) {
|
|
467
787
|
throw new Error("HTTP client is not initialized. Call initializeApi(...) first.");
|
|
@@ -500,7 +820,9 @@ export async function post(path, body, extraHeaders) {
|
|
|
500
820
|
const message = `Error ${errBody.code}: ${errBody.message}`;
|
|
501
821
|
throw new SmartlinksApiError(message, response.status, errBody, url);
|
|
502
822
|
}
|
|
503
|
-
|
|
823
|
+
const postResult = (await response.json());
|
|
824
|
+
invalidateCacheForPath(path);
|
|
825
|
+
return postResult;
|
|
504
826
|
}
|
|
505
827
|
/**
|
|
506
828
|
* Internal helper that performs a PUT request to `${baseURL}${path}`,
|
|
@@ -511,7 +833,9 @@ export async function post(path, body, extraHeaders) {
|
|
|
511
833
|
export async function put(path, body, extraHeaders) {
|
|
512
834
|
if (proxyMode) {
|
|
513
835
|
logDebug('[smartlinks] PUT via proxy', { path, body: safeBodyPreview(body) });
|
|
514
|
-
|
|
836
|
+
const result = await proxyRequest("PUT", path, body, extraHeaders);
|
|
837
|
+
invalidateCacheForPath(path);
|
|
838
|
+
return result;
|
|
515
839
|
}
|
|
516
840
|
if (!baseURL) {
|
|
517
841
|
throw new Error("HTTP client is not initialized. Call initializeApi(...) first.");
|
|
@@ -550,7 +874,9 @@ export async function put(path, body, extraHeaders) {
|
|
|
550
874
|
const message = `Error ${errBody.code}: ${errBody.message}`;
|
|
551
875
|
throw new SmartlinksApiError(message, response.status, errBody, url);
|
|
552
876
|
}
|
|
553
|
-
|
|
877
|
+
const putResult = (await response.json());
|
|
878
|
+
invalidateCacheForPath(path);
|
|
879
|
+
return putResult;
|
|
554
880
|
}
|
|
555
881
|
/**
|
|
556
882
|
* Internal helper that performs a PATCH request to `${baseURL}${path}`,
|
|
@@ -561,7 +887,9 @@ export async function put(path, body, extraHeaders) {
|
|
|
561
887
|
export async function patch(path, body, extraHeaders) {
|
|
562
888
|
if (proxyMode) {
|
|
563
889
|
logDebug('[smartlinks] PATCH via proxy', { path, body: safeBodyPreview(body) });
|
|
564
|
-
|
|
890
|
+
const result = await proxyRequest("PATCH", path, body, extraHeaders);
|
|
891
|
+
invalidateCacheForPath(path);
|
|
892
|
+
return result;
|
|
565
893
|
}
|
|
566
894
|
if (!baseURL) {
|
|
567
895
|
throw new Error("HTTP client is not initialized. Call initializeApi(...) first.");
|
|
@@ -600,7 +928,9 @@ export async function patch(path, body, extraHeaders) {
|
|
|
600
928
|
const message = `Error ${errBody.code}: ${errBody.message}`;
|
|
601
929
|
throw new SmartlinksApiError(message, response.status, errBody, url);
|
|
602
930
|
}
|
|
603
|
-
|
|
931
|
+
const patchResult = (await response.json());
|
|
932
|
+
invalidateCacheForPath(path);
|
|
933
|
+
return patchResult;
|
|
604
934
|
}
|
|
605
935
|
/**
|
|
606
936
|
* Internal helper that performs a request to `${baseURL}${path}` with custom options,
|
|
@@ -608,52 +938,115 @@ export async function patch(path, body, extraHeaders) {
|
|
|
608
938
|
* Returns the parsed JSON as T, or throws an Error.
|
|
609
939
|
*/
|
|
610
940
|
export async function requestWithOptions(path, options) {
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (options.headers instanceof Headers) {
|
|
623
|
-
options.headers.forEach((value, key) => {
|
|
624
|
-
extraHeaders[key] = value;
|
|
625
|
-
});
|
|
941
|
+
const method = (options.method || 'GET').toUpperCase();
|
|
942
|
+
const isGet = method === 'GET';
|
|
943
|
+
const skipCache = isGet ? shouldSkipCache(path) : true;
|
|
944
|
+
const cacheKey = buildCacheKey(path);
|
|
945
|
+
const ttl = !skipCache ? getTtlForPath(path) : 0;
|
|
946
|
+
if (!skipCache) {
|
|
947
|
+
// L1 hit
|
|
948
|
+
const cached = getHttpCacheHit(cacheKey, ttl);
|
|
949
|
+
if (cached !== null) {
|
|
950
|
+
logDebug('[smartlinks] GET cache hit (requestWithOptions/L1)', { path });
|
|
951
|
+
return cached;
|
|
626
952
|
}
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
else {
|
|
633
|
-
extraHeaders = Object.assign({}, options.headers);
|
|
953
|
+
// In-flight dedup
|
|
954
|
+
const inflight = httpCache.get(cacheKey);
|
|
955
|
+
if (inflight === null || inflight === void 0 ? void 0 : inflight.promise) {
|
|
956
|
+
logDebug('[smartlinks] GET in-flight dedup (requestWithOptions)', { path });
|
|
957
|
+
return inflight.promise;
|
|
634
958
|
}
|
|
635
959
|
}
|
|
636
|
-
const
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
960
|
+
const fetchPromise = (async () => {
|
|
961
|
+
// L2 (IndexedDB) check for GETs — first await, so in-flight registration runs before it resolves
|
|
962
|
+
if (!skipCache && cachePersistence !== 'none') {
|
|
963
|
+
const l2 = await idbGet(cacheKey);
|
|
964
|
+
if (l2 && Date.now() - l2.timestamp <= ttl) {
|
|
965
|
+
logDebug('[smartlinks] GET cache hit (requestWithOptions/L2)', { path });
|
|
966
|
+
setHttpCacheEntry(cacheKey, l2.data);
|
|
967
|
+
return l2.data;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
646
970
|
try {
|
|
647
|
-
|
|
971
|
+
if (proxyMode) {
|
|
972
|
+
logDebug('[smartlinks] requestWithOptions via proxy', { path, method: options.method || 'GET' });
|
|
973
|
+
const result = await proxyRequest(options.method || "GET", path, options.body, options.headers, options);
|
|
974
|
+
if (!isGet) {
|
|
975
|
+
invalidateCacheForPath(path);
|
|
976
|
+
}
|
|
977
|
+
else if (!skipCache && cachePersistence !== 'none') {
|
|
978
|
+
idbSet(cacheKey, { data: result, timestamp: Date.now(), persistedAt: Date.now() }).catch(() => { });
|
|
979
|
+
}
|
|
980
|
+
return result;
|
|
981
|
+
}
|
|
982
|
+
if (!baseURL) {
|
|
983
|
+
throw new Error("HTTP client is not initialized. Call initializeApi(...) first.");
|
|
984
|
+
}
|
|
985
|
+
const url = `${baseURL}${path}`;
|
|
986
|
+
// Safely merge headers, converting Headers/init to Record<string, string>
|
|
987
|
+
let extraHeaders = {};
|
|
988
|
+
if (options.headers) {
|
|
989
|
+
if (options.headers instanceof Headers) {
|
|
990
|
+
options.headers.forEach((value, key) => {
|
|
991
|
+
extraHeaders[key] = value;
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
else if (Array.isArray(options.headers)) {
|
|
995
|
+
for (const [key, value] of options.headers) {
|
|
996
|
+
extraHeaders[key] = value;
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
1000
|
+
extraHeaders = Object.assign({}, options.headers);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const headers = Object.assign(Object.assign(Object.assign(Object.assign({ "Content-Type": "application/json" }, (apiKey ? { "X-API-Key": apiKey } : {})), (bearerToken ? { "AUTHORIZATION": `Bearer ${bearerToken}` } : {})), (ngrokSkipBrowserWarning ? { "ngrok-skip-browser-warning": "true" } : {})), extraHeaders);
|
|
1004
|
+
// Merge global custom headers (do not override existing keys from options.headers)
|
|
1005
|
+
for (const [k, v] of Object.entries(extraHeadersGlobal))
|
|
1006
|
+
if (!(k in headers))
|
|
1007
|
+
headers[k] = v;
|
|
1008
|
+
logDebug('[smartlinks] requestWithOptions fetch', { url, method: options.method || 'GET', headers: redactHeaders(headers), body: safeBodyPreview(options.body) });
|
|
1009
|
+
const response = await fetch(url, Object.assign(Object.assign({}, options), { headers }));
|
|
1010
|
+
logDebug('[smartlinks] requestWithOptions response', { url, status: response.status, ok: response.ok });
|
|
1011
|
+
if (!response.ok) {
|
|
1012
|
+
let responseBody;
|
|
1013
|
+
try {
|
|
1014
|
+
responseBody = await response.json();
|
|
1015
|
+
}
|
|
1016
|
+
catch (_a) {
|
|
1017
|
+
responseBody = null;
|
|
1018
|
+
}
|
|
1019
|
+
const errBody = normalizeErrorResponse(responseBody, response.status);
|
|
1020
|
+
throw new SmartlinksApiError(`Error ${errBody.code}: ${errBody.message}`, response.status, errBody, url);
|
|
1021
|
+
}
|
|
1022
|
+
const rwoResult = (await response.json());
|
|
1023
|
+
if (!isGet) {
|
|
1024
|
+
invalidateCacheForPath(path);
|
|
1025
|
+
}
|
|
1026
|
+
else if (!skipCache && cachePersistence !== 'none') {
|
|
1027
|
+
idbSet(cacheKey, { data: rwoResult, timestamp: Date.now(), persistedAt: Date.now() }).catch(() => { });
|
|
1028
|
+
}
|
|
1029
|
+
return rwoResult;
|
|
648
1030
|
}
|
|
649
|
-
catch (
|
|
650
|
-
|
|
1031
|
+
catch (fetchErr) {
|
|
1032
|
+
// Offline fallback for GETs
|
|
1033
|
+
if (isGet && !skipCache && cachePersistence !== 'none' && cacheServeStaleOnOffline && isNetworkError(fetchErr)) {
|
|
1034
|
+
const l2 = await idbGet(cacheKey);
|
|
1035
|
+
if (l2 && Date.now() - l2.timestamp <= cachePersistenceTtlMs) {
|
|
1036
|
+
logDebug('[smartlinks] GET offline fallback (requestWithOptions/L2)', { path, cachedAt: l2.timestamp });
|
|
1037
|
+
throw new SmartlinksOfflineError('Network unavailable — serving cached data from persistent storage.', l2.data, l2.timestamp);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
throw fetchErr;
|
|
651
1041
|
}
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1042
|
+
})();
|
|
1043
|
+
// Register in-flight for GET requests
|
|
1044
|
+
if (!skipCache) {
|
|
1045
|
+
evictLruIfNeeded();
|
|
1046
|
+
httpCache.set(cacheKey, { data: null, timestamp: Date.now(), promise: fetchPromise });
|
|
1047
|
+
fetchPromise.then((data) => setHttpCacheEntry(cacheKey, data), () => httpCache.delete(cacheKey));
|
|
655
1048
|
}
|
|
656
|
-
return
|
|
1049
|
+
return fetchPromise;
|
|
657
1050
|
}
|
|
658
1051
|
/**
|
|
659
1052
|
* Internal helper that performs a DELETE request to `${baseURL}${path}`,
|
|
@@ -663,7 +1056,9 @@ export async function requestWithOptions(path, options) {
|
|
|
663
1056
|
export async function del(path, extraHeaders) {
|
|
664
1057
|
if (proxyMode) {
|
|
665
1058
|
logDebug('[smartlinks] DELETE via proxy', { path });
|
|
666
|
-
|
|
1059
|
+
const result = await proxyRequest("DELETE", path, undefined, extraHeaders);
|
|
1060
|
+
invalidateCacheForPath(path);
|
|
1061
|
+
return result;
|
|
667
1062
|
}
|
|
668
1063
|
if (!baseURL) {
|
|
669
1064
|
throw new Error("HTTP client is not initialized. Call initializeApi(...) first.");
|
|
@@ -698,9 +1093,9 @@ export async function del(path, extraHeaders) {
|
|
|
698
1093
|
throw new SmartlinksApiError(message, response.status, errBody, url);
|
|
699
1094
|
}
|
|
700
1095
|
// If the response is empty, just return undefined
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
return
|
|
1096
|
+
const delResult = response.status === 204 ? undefined : (await response.json());
|
|
1097
|
+
invalidateCacheForPath(path);
|
|
1098
|
+
return delResult;
|
|
704
1099
|
}
|
|
705
1100
|
/**
|
|
706
1101
|
* Returns the common headers used for API requests, including apiKey and bearerToken if set.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { initializeApi, request, sendCustomProxyMessage } from "./http";
|
|
1
|
+
export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, request, sendCustomProxyMessage } from "./http";
|
|
2
2
|
export * from "./api";
|
|
3
3
|
export * from "./types";
|
|
4
4
|
export { iframe } from "./iframe";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
// Top-level entrypoint of the npm package. Re-export initializeApi + all namespaces.
|
|
3
|
-
export { initializeApi, request, sendCustomProxyMessage } from "./http";
|
|
3
|
+
export { initializeApi, isInitialized, hasAuthCredentials, configureSdkCache, invalidateCache, request, sendCustomProxyMessage } from "./http";
|
|
4
4
|
export * from "./api";
|
|
5
5
|
export * from "./types";
|
|
6
6
|
// Iframe namespace
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PersistentCacheEntry {
|
|
2
|
+
/** The cached response data. */
|
|
3
|
+
data: any;
|
|
4
|
+
/** Unix ms timestamp of when the data was originally fetched from the network. */
|
|
5
|
+
timestamp: number;
|
|
6
|
+
/** Unix ms timestamp of when this entry was written to IndexedDB. */
|
|
7
|
+
persistedAt: number;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Returns true only in environments where IndexedDB is genuinely available.
|
|
11
|
+
* False in Node.js, and in some private-browsing contexts that stub indexedDB
|
|
12
|
+
* but throw on open.
|
|
13
|
+
*/
|
|
14
|
+
export declare function isIdbAvailable(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Read an entry from IndexedDB.
|
|
17
|
+
* Returns `null` when IDB is unavailable, the key doesn't exist, or on any error.
|
|
18
|
+
* Safe to call in Node.js — returns null immediately.
|
|
19
|
+
*/
|
|
20
|
+
export declare function idbGet(key: string): Promise<PersistentCacheEntry | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Write an entry to IndexedDB.
|
|
23
|
+
* Fails silently on quota exceeded, private browsing, or any other error.
|
|
24
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
25
|
+
*/
|
|
26
|
+
export declare function idbSet(key: string, entry: PersistentCacheEntry): Promise<void>;
|
|
27
|
+
/**
|
|
28
|
+
* Delete a single entry from IndexedDB.
|
|
29
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
30
|
+
*/
|
|
31
|
+
export declare function idbDelete(key: string): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Clear all IDB entries, or only those whose key contains `pattern`.
|
|
34
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
35
|
+
*/
|
|
36
|
+
export declare function idbClear(pattern?: string): Promise<void>;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// src/persistentCache.ts
|
|
2
|
+
// IndexedDB-backed L2 cache for the SDK's HTTP layer.
|
|
3
|
+
//
|
|
4
|
+
// Every exported function is Node-safe: all IDB access is guarded by
|
|
5
|
+
// isIdbAvailable() and wrapped in try/catch so that failures in private-
|
|
6
|
+
// browsing, quota-exceeded situations, or server-side environments are always
|
|
7
|
+
// silent — they never propagate errors to the caller.
|
|
8
|
+
const DB_NAME = 'smartlinks-sdk-cache';
|
|
9
|
+
const STORE_NAME = 'responses';
|
|
10
|
+
const DB_VERSION = 1;
|
|
11
|
+
let dbPromise = null;
|
|
12
|
+
/**
|
|
13
|
+
* Returns true only in environments where IndexedDB is genuinely available.
|
|
14
|
+
* False in Node.js, and in some private-browsing contexts that stub indexedDB
|
|
15
|
+
* but throw on open.
|
|
16
|
+
*/
|
|
17
|
+
export function isIdbAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
return typeof indexedDB !== 'undefined' && indexedDB !== null;
|
|
20
|
+
}
|
|
21
|
+
catch (_a) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function openDb() {
|
|
26
|
+
if (dbPromise)
|
|
27
|
+
return dbPromise;
|
|
28
|
+
dbPromise = new Promise((resolve, reject) => {
|
|
29
|
+
try {
|
|
30
|
+
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
|
31
|
+
req.onupgradeneeded = (event) => {
|
|
32
|
+
const db = event.target.result;
|
|
33
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
34
|
+
db.createObjectStore(STORE_NAME);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
req.onsuccess = (event) => {
|
|
38
|
+
resolve(event.target.result);
|
|
39
|
+
};
|
|
40
|
+
req.onerror = () => {
|
|
41
|
+
dbPromise = null;
|
|
42
|
+
reject(req.error);
|
|
43
|
+
};
|
|
44
|
+
req.onblocked = () => {
|
|
45
|
+
dbPromise = null;
|
|
46
|
+
reject(new Error('[smartlinks] IndexedDB open blocked'));
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (err) {
|
|
50
|
+
dbPromise = null;
|
|
51
|
+
reject(err);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return dbPromise;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read an entry from IndexedDB.
|
|
58
|
+
* Returns `null` when IDB is unavailable, the key doesn't exist, or on any error.
|
|
59
|
+
* Safe to call in Node.js — returns null immediately.
|
|
60
|
+
*/
|
|
61
|
+
export async function idbGet(key) {
|
|
62
|
+
if (!isIdbAvailable())
|
|
63
|
+
return null;
|
|
64
|
+
try {
|
|
65
|
+
const db = await openDb();
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
try {
|
|
68
|
+
const tx = db.transaction(STORE_NAME, 'readonly');
|
|
69
|
+
const req = tx.objectStore(STORE_NAME).get(key);
|
|
70
|
+
req.onsuccess = () => { var _a; return resolve((_a = req.result) !== null && _a !== void 0 ? _a : null); };
|
|
71
|
+
req.onerror = () => resolve(null);
|
|
72
|
+
}
|
|
73
|
+
catch (_a) {
|
|
74
|
+
resolve(null);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (_a) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Write an entry to IndexedDB.
|
|
84
|
+
* Fails silently on quota exceeded, private browsing, or any other error.
|
|
85
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
86
|
+
*/
|
|
87
|
+
export async function idbSet(key, entry) {
|
|
88
|
+
if (!isIdbAvailable())
|
|
89
|
+
return;
|
|
90
|
+
try {
|
|
91
|
+
const db = await openDb();
|
|
92
|
+
await new Promise((resolve) => {
|
|
93
|
+
try {
|
|
94
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
95
|
+
tx.objectStore(STORE_NAME).put(entry, key);
|
|
96
|
+
tx.oncomplete = () => resolve();
|
|
97
|
+
tx.onerror = () => resolve(); // quota exceeded etc — fail silently
|
|
98
|
+
tx.onabort = () => resolve();
|
|
99
|
+
}
|
|
100
|
+
catch (_a) {
|
|
101
|
+
resolve();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (_a) {
|
|
106
|
+
// Fail silently — IDB persistence is best-effort
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Delete a single entry from IndexedDB.
|
|
111
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
112
|
+
*/
|
|
113
|
+
export async function idbDelete(key) {
|
|
114
|
+
if (!isIdbAvailable())
|
|
115
|
+
return;
|
|
116
|
+
try {
|
|
117
|
+
const db = await openDb();
|
|
118
|
+
await new Promise((resolve) => {
|
|
119
|
+
try {
|
|
120
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
121
|
+
tx.objectStore(STORE_NAME).delete(key);
|
|
122
|
+
tx.oncomplete = () => resolve();
|
|
123
|
+
tx.onerror = () => resolve();
|
|
124
|
+
tx.onabort = () => resolve();
|
|
125
|
+
}
|
|
126
|
+
catch (_a) {
|
|
127
|
+
resolve();
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
catch (_a) { }
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clear all IDB entries, or only those whose key contains `pattern`.
|
|
135
|
+
* Safe to call in Node.js — no-ops immediately.
|
|
136
|
+
*/
|
|
137
|
+
export async function idbClear(pattern) {
|
|
138
|
+
if (!isIdbAvailable())
|
|
139
|
+
return;
|
|
140
|
+
try {
|
|
141
|
+
const db = await openDb();
|
|
142
|
+
if (!pattern) {
|
|
143
|
+
await new Promise((resolve) => {
|
|
144
|
+
try {
|
|
145
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
146
|
+
tx.objectStore(STORE_NAME).clear();
|
|
147
|
+
tx.oncomplete = () => resolve();
|
|
148
|
+
tx.onerror = () => resolve();
|
|
149
|
+
tx.onabort = () => resolve();
|
|
150
|
+
}
|
|
151
|
+
catch (_a) {
|
|
152
|
+
resolve();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// Pattern-based: enumerate all keys and delete matching ones.
|
|
158
|
+
await new Promise((resolve) => {
|
|
159
|
+
try {
|
|
160
|
+
const tx = db.transaction(STORE_NAME, 'readwrite');
|
|
161
|
+
const store = tx.objectStore(STORE_NAME);
|
|
162
|
+
const req = store.getAllKeys();
|
|
163
|
+
req.onsuccess = () => {
|
|
164
|
+
for (const key of req.result) {
|
|
165
|
+
if (key.includes(pattern))
|
|
166
|
+
store.delete(key);
|
|
167
|
+
}
|
|
168
|
+
resolve();
|
|
169
|
+
};
|
|
170
|
+
req.onerror = () => resolve();
|
|
171
|
+
}
|
|
172
|
+
catch (_a) {
|
|
173
|
+
resolve();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
catch (_a) { }
|
|
178
|
+
}
|
package/dist/types/error.d.ts
CHANGED
|
@@ -73,3 +73,39 @@ export declare class SmartlinksApiError extends Error {
|
|
|
73
73
|
*/
|
|
74
74
|
toJSON(): Record<string, any>;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Thrown when a GET request fails due to network unavailability AND the
|
|
78
|
+
* persistent cache (IndexedDB) has a previously stored response for that
|
|
79
|
+
* resource. The `staleData` property contains the cached payload so the
|
|
80
|
+
* application can render in a degraded/offline state.
|
|
81
|
+
*
|
|
82
|
+
* Only thrown when persistence is enabled:
|
|
83
|
+
* `configureSdkCache({ persistence: 'indexeddb' })`
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* import { SmartlinksOfflineError } from '@proveanything/smartlinks'
|
|
88
|
+
*
|
|
89
|
+
* try {
|
|
90
|
+
* const data = await collection.get('abc123')
|
|
91
|
+
* } catch (err) {
|
|
92
|
+
* if (err instanceof SmartlinksOfflineError) {
|
|
93
|
+
* showOfflineBanner()
|
|
94
|
+
* renderWithData(err.staleData) // use the cached payload
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export declare class SmartlinksOfflineError extends Error {
|
|
100
|
+
/** The stale cached payload available when the network request failed. */
|
|
101
|
+
readonly staleData: any;
|
|
102
|
+
/** Unix ms timestamp of when the stale data was originally fetched from the server. */
|
|
103
|
+
readonly cachedAt: number;
|
|
104
|
+
constructor(message: string,
|
|
105
|
+
/** The stale cached payload available when the network request failed. */
|
|
106
|
+
staleData: any,
|
|
107
|
+
/** Unix ms timestamp of when the stale data was originally fetched from the server. */
|
|
108
|
+
cachedAt: number);
|
|
109
|
+
/** Age of the stale data in milliseconds at the time this error was thrown. */
|
|
110
|
+
get staleAgeMs(): number;
|
|
111
|
+
}
|
package/dist/types/error.js
CHANGED
|
@@ -96,3 +96,45 @@ export class SmartlinksApiError extends Error {
|
|
|
96
96
|
};
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Thrown when a GET request fails due to network unavailability AND the
|
|
101
|
+
* persistent cache (IndexedDB) has a previously stored response for that
|
|
102
|
+
* resource. The `staleData` property contains the cached payload so the
|
|
103
|
+
* application can render in a degraded/offline state.
|
|
104
|
+
*
|
|
105
|
+
* Only thrown when persistence is enabled:
|
|
106
|
+
* `configureSdkCache({ persistence: 'indexeddb' })`
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* import { SmartlinksOfflineError } from '@proveanything/smartlinks'
|
|
111
|
+
*
|
|
112
|
+
* try {
|
|
113
|
+
* const data = await collection.get('abc123')
|
|
114
|
+
* } catch (err) {
|
|
115
|
+
* if (err instanceof SmartlinksOfflineError) {
|
|
116
|
+
* showOfflineBanner()
|
|
117
|
+
* renderWithData(err.staleData) // use the cached payload
|
|
118
|
+
* }
|
|
119
|
+
* }
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export class SmartlinksOfflineError extends Error {
|
|
123
|
+
constructor(message,
|
|
124
|
+
/** The stale cached payload available when the network request failed. */
|
|
125
|
+
staleData,
|
|
126
|
+
/** Unix ms timestamp of when the stale data was originally fetched from the server. */
|
|
127
|
+
cachedAt) {
|
|
128
|
+
super(message);
|
|
129
|
+
this.staleData = staleData;
|
|
130
|
+
this.cachedAt = cachedAt;
|
|
131
|
+
this.name = 'SmartlinksOfflineError';
|
|
132
|
+
if (Error.captureStackTrace) {
|
|
133
|
+
Error.captureStackTrace(this, SmartlinksOfflineError);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Age of the stale data in milliseconds at the time this error was thrown. */
|
|
137
|
+
get staleAgeMs() {
|
|
138
|
+
return Date.now() - this.cachedAt;
|
|
139
|
+
}
|
|
140
|
+
}
|
package/docs/API_SUMMARY.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Smartlinks API Summary
|
|
2
2
|
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0 | Generated: 2026-02-20T14:52:16.722Z
|
|
4
4
|
|
|
5
5
|
This is a concise summary of all available API functions and types.
|
|
6
6
|
|
|
@@ -87,7 +87,10 @@ Return whether proxy mode is currently enabled.
|
|
|
87
87
|
extraHeaders?: Record<string, string>
|
|
88
88
|
iframeAutoResize?: boolean // default true when in iframe
|
|
89
89
|
logger?: Logger // optional console-like or function to enable verbose logging
|
|
90
|
-
|
|
90
|
+
/**
|
|
91
|
+
* When true, bypasses the idempotency guard and forces a full re-initialization.
|
|
92
|
+
* Use only when you intentionally need to reset all SDK state (e.g. in tests or
|
|
93
|
+
* when switching accounts) → `void`
|
|
91
94
|
Call this once (e.g. at app startup) to configure baseURL/auth.
|
|
92
95
|
|
|
93
96
|
**setNgrokSkipBrowserWarning**(flag: boolean) → `void`
|
|
@@ -102,13 +105,32 @@ Allows setting the bearerToken at runtime (e.g. after login/logout).
|
|
|
102
105
|
**getBaseURL**() → `string | null`
|
|
103
106
|
Get the currently configured API base URL. Returns null if initializeApi() has not been called yet.
|
|
104
107
|
|
|
108
|
+
**isInitialized**() → `boolean`
|
|
109
|
+
Returns true if initializeApi() has been called at least once. Useful for guards in widgets or shared modules that want to skip initialization when another module has already done it. ```ts if (!isInitialized()) { initializeApi({ baseURL: 'https://smartlinks.app/api/v1' }) } ```
|
|
110
|
+
|
|
111
|
+
**hasAuthCredentials**() → `boolean`
|
|
112
|
+
Returns true if the SDK currently has any auth credential set (bearer token or API key). Use this as a cheap pre-flight check before calling endpoints that require authentication, to avoid issuing a network request that you already know will return a 401. ```ts if (hasAuthCredentials()) { const account = await auth.getAccount() } ```
|
|
113
|
+
|
|
114
|
+
**configureSdkCache**(options: {
|
|
115
|
+
enabled?: boolean
|
|
116
|
+
ttlMs?: number
|
|
117
|
+
maxEntries?: number
|
|
118
|
+
persistence?: 'none' | 'indexeddb'
|
|
119
|
+
persistenceTtlMs?: number
|
|
120
|
+
serveStaleOnOffline?: boolean
|
|
121
|
+
}) → `void`
|
|
122
|
+
Configure the SDK's built-in in-memory GET cache. The cache is transparent — it sits inside the HTTP layer and requires no changes to your existing API calls. All GET requests benefit automatically. Per-resource rules (collections/products → 1 h, proofs → 30 s, etc.) override this value. in-memory only (`'none'`, default). Ignored in Node.js. fallback, from the original fetch time (default: 7 days). `SmartlinksOfflineError` with stale data instead of propagating the network error. ```ts // Enable IndexedDB persistence for offline support configureSdkCache({ persistence: 'indexeddb' }) // Disable cache entirely in test environments configureSdkCache({ enabled: false }) ```
|
|
123
|
+
|
|
124
|
+
**invalidateCache**(urlPattern?: string) → `void`
|
|
125
|
+
Manually invalidate entries in the SDK's GET cache. *contains* this string is removed. Omit (or pass `undefined`) to wipe the entire cache. ```ts invalidateCache() // clear everything invalidateCache('/collection/abc123') // one specific collection invalidateCache('/product/') // all product responses ```
|
|
126
|
+
|
|
105
127
|
**proxyUploadFormData**(path: string,
|
|
106
128
|
formData: FormData,
|
|
107
129
|
onProgress?: (percent: number) → `void`
|
|
108
130
|
Upload a FormData payload via proxy with progress events using chunked postMessage. Parent is expected to implement the counterpart protocol.
|
|
109
131
|
|
|
110
132
|
**request**(path: string) → `Promise<T>`
|
|
111
|
-
Internal helper that performs a GET request to
|
|
133
|
+
Internal helper that performs a GET request to `${baseURL}${path}`, injecting headers for apiKey or bearerToken if present. Cache pipeline (when caching is not skipped): L1 hit → return from memory (no I/O) L2 hit → return from IndexedDB, promote to L1 (no network) Miss → fetch from network, store in L1 + L2 Offline → serve stale L2 entry via SmartlinksOfflineError (if persistence enabled) Concurrent identical GETs share one in-flight promise (deduplication). Node-safe: IndexedDB calls are no-ops when IDB is unavailable.
|
|
112
134
|
|
|
113
135
|
**post**(path: string,
|
|
114
136
|
body: any,
|
|
@@ -4122,7 +4144,7 @@ Tries to register a new user account. Can return a bearer token, or a Firebase t
|
|
|
4122
4144
|
Admin: Get a user bearer token (impersonation/automation). POST /admin/auth/userToken All fields are optional; at least one identifier should be provided.
|
|
4123
4145
|
|
|
4124
4146
|
**getAccount**() → `Promise<AccountInfoResponse>`
|
|
4125
|
-
Gets current account information for the logged in user. Returns user, owner, account, and location objects.
|
|
4147
|
+
Gets current account information for the logged in user. Returns user, owner, account, and location objects. Short-circuits immediately (no network request) when the SDK has no bearer token or API key set — the server would return 401 anyway. Throws a `SmartlinksApiError` with `statusCode 401` and `details.local = true` so callers can distinguish "never authenticated" from an actual server-side token rejection.
|
|
4126
4148
|
|
|
4127
4149
|
### authKit
|
|
4128
4150
|
|