@centia-io/sdk 0.0.57 → 0.1.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/centia-io-sdk.cjs +227 -2
- package/dist/centia-io-sdk.d.cts +136 -3
- package/dist/centia-io-sdk.d.cts.map +1 -1
- package/dist/centia-io-sdk.d.ts +136 -3
- package/dist/centia-io-sdk.d.ts.map +1 -1
- package/dist/centia-io-sdk.js +224 -3
- package/dist/centia-io-sdk.js.map +1 -1
- package/dist/centia-io-sdk.umd.js +227 -2
- package/package.json +12 -2
|
@@ -2184,12 +2184,15 @@
|
|
|
2184
2184
|
basePath(schema) {
|
|
2185
2185
|
return `api/v4/schemas/${encodeURIComponent(schema)}/tables`;
|
|
2186
2186
|
}
|
|
2187
|
-
async getTable(schema, table) {
|
|
2187
|
+
async getTable(schema, table, opts) {
|
|
2188
2188
|
var _this = this;
|
|
2189
2189
|
const path = table ? `${_this.basePath(schema)}/${encodeURIComponent(table)}` : _this.basePath(schema);
|
|
2190
|
+
const query = {};
|
|
2191
|
+
if (opts === null || opts === void 0 ? void 0 : opts.namesOnly) query.namesOnly = "true";
|
|
2190
2192
|
return _this.client.request({
|
|
2191
2193
|
path,
|
|
2192
|
-
method: "GET"
|
|
2194
|
+
method: "GET",
|
|
2195
|
+
query: Object.keys(query).length > 0 ? query : void 0
|
|
2193
2196
|
});
|
|
2194
2197
|
}
|
|
2195
2198
|
async postTable(schema, body) {
|
|
@@ -2567,6 +2570,224 @@
|
|
|
2567
2570
|
};
|
|
2568
2571
|
}
|
|
2569
2572
|
|
|
2573
|
+
//#endregion
|
|
2574
|
+
//#region src/auth/errors.ts
|
|
2575
|
+
/**
|
|
2576
|
+
* @author Martin Høgh <mh@mapcentia.com>
|
|
2577
|
+
* @copyright 2013-2026 MapCentia ApS
|
|
2578
|
+
* @license https://opensource.org/license/mit The MIT License
|
|
2579
|
+
*
|
|
2580
|
+
*/
|
|
2581
|
+
/**
|
|
2582
|
+
* Thrown by {@link createTokenProvider} when the {@link TokenStore} has no
|
|
2583
|
+
* access token. Indicates the user has never logged in (or has logged out)
|
|
2584
|
+
* and a fresh interactive login is required. Distinct from
|
|
2585
|
+
* {@link SessionExpiredError}, which means the user *did* log in but the
|
|
2586
|
+
* refresh token has since expired.
|
|
2587
|
+
*/
|
|
2588
|
+
var NotLoggedInError = class extends Error {
|
|
2589
|
+
constructor(message = "Not logged in: no access token in store") {
|
|
2590
|
+
super(message);
|
|
2591
|
+
this.name = "NotLoggedInError";
|
|
2592
|
+
}
|
|
2593
|
+
};
|
|
2594
|
+
/**
|
|
2595
|
+
* Thrown by {@link createTokenProvider} when the access token is expired and
|
|
2596
|
+
* the refresh token is either missing or also expired. Indicates the
|
|
2597
|
+
* stored credentials cannot be silently revived; the user must run an
|
|
2598
|
+
* interactive login again.
|
|
2599
|
+
*/
|
|
2600
|
+
var SessionExpiredError = class extends Error {
|
|
2601
|
+
constructor(message = "Session expired: refresh token is missing or expired") {
|
|
2602
|
+
super(message);
|
|
2603
|
+
this.name = "SessionExpiredError";
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
|
|
2607
|
+
//#endregion
|
|
2608
|
+
//#region src/auth/tokenProvider.ts
|
|
2609
|
+
/**
|
|
2610
|
+
* @author Martin Høgh <mh@mapcentia.com>
|
|
2611
|
+
* @copyright 2013-2026 MapCentia ApS
|
|
2612
|
+
* @license https://opensource.org/license/mit The MIT License
|
|
2613
|
+
*
|
|
2614
|
+
*/
|
|
2615
|
+
const DEFAULT_SKEW_SECONDS = 30;
|
|
2616
|
+
/**
|
|
2617
|
+
* Build a {@link TokenProvider} that returns a fresh access token, refreshing
|
|
2618
|
+
* via `opts.authService` and persisting the result to `opts.store` whenever
|
|
2619
|
+
* the cached token is within `expirySkewSeconds` of expiry.
|
|
2620
|
+
*
|
|
2621
|
+
* **Behaviour:**
|
|
2622
|
+
* - If the store has no access token, throws {@link NotLoggedInError}.
|
|
2623
|
+
* - If the access token is fresh, returns it immediately.
|
|
2624
|
+
* - If expired and the refresh token is missing or expired, throws
|
|
2625
|
+
* {@link SessionExpiredError}.
|
|
2626
|
+
* - Otherwise calls `opts.authService.getRefreshToken(refresh_token)`,
|
|
2627
|
+
* persists `{ token, refresh_token? }` via `opts.store.set()`, and
|
|
2628
|
+
* returns the new access token.
|
|
2629
|
+
*
|
|
2630
|
+
* **In-process concurrency.** Multiple concurrent `getAccessToken()` calls
|
|
2631
|
+
* during a refresh share a single in-flight promise; the auth service is
|
|
2632
|
+
* called exactly once per refresh cycle. On failure the in-flight slot
|
|
2633
|
+
* clears so the next call retries.
|
|
2634
|
+
*
|
|
2635
|
+
* **Cross-process concurrency — known limitation.** This function does NOT
|
|
2636
|
+
* coordinate refresh across processes. Two processes that share a
|
|
2637
|
+
* configstore-backed {@link TokenStore} can each independently observe an
|
|
2638
|
+
* expired access token, both call `getRefreshToken` against the same
|
|
2639
|
+
* refresh token, and the OAuth provider will reject the second call with
|
|
2640
|
+
* `invalid_grant` once the refresh token rotates. Callers that run
|
|
2641
|
+
* multiple processes concurrently against the same store should treat
|
|
2642
|
+
* `invalid_grant` as a transient failure and re-read the store before
|
|
2643
|
+
* retrying. Closing this gap requires holding the file lock across the
|
|
2644
|
+
* network refresh, which the current implementation does not do.
|
|
2645
|
+
*
|
|
2646
|
+
* @param opts - Provider configuration.
|
|
2647
|
+
* @param opts.store - Where credentials are read from and written to.
|
|
2648
|
+
* @param opts.authService - Object exposing
|
|
2649
|
+
* `getRefreshToken(refreshToken)`. The existing
|
|
2650
|
+
* `Gc2Service` (returned by `CodeFlow#service`)
|
|
2651
|
+
* satisfies this structurally.
|
|
2652
|
+
* @param opts.expirySkewSeconds - How many seconds before `exp` to treat a
|
|
2653
|
+
* JWT as expired. Default `30`.
|
|
2654
|
+
* @returns A {@link TokenProvider} whose `getAccessToken()` resolves to a
|
|
2655
|
+
* non-expired access token, refreshing if necessary.
|
|
2656
|
+
*/
|
|
2657
|
+
function createTokenProvider(opts) {
|
|
2658
|
+
var _opts$expirySkewSecon;
|
|
2659
|
+
const skew = (_opts$expirySkewSecon = opts.expirySkewSeconds) !== null && _opts$expirySkewSecon !== void 0 ? _opts$expirySkewSecon : DEFAULT_SKEW_SECONDS;
|
|
2660
|
+
let inFlight = null;
|
|
2661
|
+
async function refresh(refreshToken) {
|
|
2662
|
+
const refreshed = await opts.authService.getRefreshToken(refreshToken);
|
|
2663
|
+
const patch = { token: refreshed.access_token };
|
|
2664
|
+
if (refreshed.refresh_token) patch.refresh_token = refreshed.refresh_token;
|
|
2665
|
+
await opts.store.set(patch);
|
|
2666
|
+
return refreshed.access_token;
|
|
2667
|
+
}
|
|
2668
|
+
return { async getAccessToken() {
|
|
2669
|
+
const creds = await opts.store.get();
|
|
2670
|
+
if (!creds.token) throw new NotLoggedInError();
|
|
2671
|
+
if (!isExpired(creds.token, skew)) return creds.token;
|
|
2672
|
+
if (!creds.refresh_token || isExpired(creds.refresh_token, skew)) throw new SessionExpiredError();
|
|
2673
|
+
if (!inFlight) inFlight = refresh(creds.refresh_token).finally(() => {
|
|
2674
|
+
inFlight = null;
|
|
2675
|
+
});
|
|
2676
|
+
return inFlight;
|
|
2677
|
+
} };
|
|
2678
|
+
}
|
|
2679
|
+
function isExpired(jwt, skewSeconds) {
|
|
2680
|
+
const { exp } = jwtDecode(jwt);
|
|
2681
|
+
if (!exp) return false;
|
|
2682
|
+
return Math.floor(Date.now() / 1e3) + skewSeconds >= exp;
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
//#endregion
|
|
2686
|
+
//#region src/auth/configstoreTokenStore.ts
|
|
2687
|
+
const LOCK_RETRIES = 5;
|
|
2688
|
+
const LOCK_RETRY_MIN_MS = 100;
|
|
2689
|
+
const LOCK_RETRY_MAX_MS = 500;
|
|
2690
|
+
const LOCK_STALE_MS = 1e4;
|
|
2691
|
+
/**
|
|
2692
|
+
* Build a Node-only file-backed {@link TokenStore} that persists OAuth
|
|
2693
|
+
* credentials at `~/.config/configstore/<name>.json` (or
|
|
2694
|
+
* `$XDG_CONFIG_HOME/configstore/<name>.json` if set), with both in-process
|
|
2695
|
+
* and cross-process write safety.
|
|
2696
|
+
*
|
|
2697
|
+
* **Shared-state intent.** The `name` is the file name on disk. Two processes
|
|
2698
|
+
* (e.g. `gc2-cli` and a local MCP server) that pass the same name share the
|
|
2699
|
+
* same on-disk credentials and therefore the same login session. The default
|
|
2700
|
+
* `'gc2-env'` matches the name `gc2-cli` already uses, so a one-time
|
|
2701
|
+
* `gc2 login` is observable to every process that calls
|
|
2702
|
+
* `createConfigstoreTokenStore()` with no argument. Pass a different name
|
|
2703
|
+
* to isolate.
|
|
2704
|
+
*
|
|
2705
|
+
* **In-process correctness.** A serial promise chain on `set()` ensures
|
|
2706
|
+
* concurrent same-process calls do not race on the shared configstore cache.
|
|
2707
|
+
*
|
|
2708
|
+
* **Cross-process correctness.** `proper-lockfile` serializes the
|
|
2709
|
+
* read-merge-write critical section across processes so two simultaneous
|
|
2710
|
+
* `set()` calls from different processes cannot corrupt the file.
|
|
2711
|
+
*
|
|
2712
|
+
* **Node-only.** The dynamic imports keep `configstore` and `proper-lockfile`
|
|
2713
|
+
* out of browser bundles even when this module is imported through the SDK
|
|
2714
|
+
* barrel. Calling this function in a browser environment will fail at
|
|
2715
|
+
* runtime when the deferred `await import('configstore')` cannot resolve.
|
|
2716
|
+
*
|
|
2717
|
+
* @param name - configstore file name (without `.json`). Default `'gc2-env'`
|
|
2718
|
+
* matches `gc2-cli`'s configstore so credentials are shared.
|
|
2719
|
+
* @returns A {@link TokenStore} suitable for passing to {@link createTokenProvider}.
|
|
2720
|
+
*/
|
|
2721
|
+
function createConfigstoreTokenStore(name = "gc2-env") {
|
|
2722
|
+
let configstoreInstance = null;
|
|
2723
|
+
let setChain = Promise.resolve();
|
|
2724
|
+
async function getConfigstore() {
|
|
2725
|
+
var _default;
|
|
2726
|
+
if (configstoreInstance) return configstoreInstance;
|
|
2727
|
+
const mod = await import("configstore");
|
|
2728
|
+
const Configstore = (_default = mod.default) !== null && _default !== void 0 ? _default : mod;
|
|
2729
|
+
const { homedir } = await import("node:os");
|
|
2730
|
+
const { join } = await import("node:path");
|
|
2731
|
+
configstoreInstance = new Configstore(name, void 0, { configPath: join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "configstore", `${name}.json`) });
|
|
2732
|
+
return configstoreInstance;
|
|
2733
|
+
}
|
|
2734
|
+
async function getLockfile() {
|
|
2735
|
+
var _default2;
|
|
2736
|
+
const mod = await import("proper-lockfile");
|
|
2737
|
+
return (_default2 = mod.default) !== null && _default2 !== void 0 ? _default2 : mod;
|
|
2738
|
+
}
|
|
2739
|
+
function readAll(cs) {
|
|
2740
|
+
const result = {};
|
|
2741
|
+
const token = cs.get("token");
|
|
2742
|
+
const refresh_token = cs.get("refresh_token");
|
|
2743
|
+
const host = cs.get("host");
|
|
2744
|
+
if (token !== void 0) result.token = token;
|
|
2745
|
+
if (refresh_token !== void 0) result.refresh_token = refresh_token;
|
|
2746
|
+
if (host !== void 0) result.host = host;
|
|
2747
|
+
return result;
|
|
2748
|
+
}
|
|
2749
|
+
async function doLockedSet(patch) {
|
|
2750
|
+
const cs = await getConfigstore();
|
|
2751
|
+
const lockfile = await getLockfile();
|
|
2752
|
+
const filePath = cs.path;
|
|
2753
|
+
const { mkdirSync, writeFileSync } = await import("node:fs");
|
|
2754
|
+
const { dirname } = await import("node:path");
|
|
2755
|
+
try {
|
|
2756
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
2757
|
+
writeFileSync(filePath, "{}", { flag: "wx" });
|
|
2758
|
+
} catch (e) {
|
|
2759
|
+
if ((e === null || e === void 0 ? void 0 : e.code) !== "EEXIST") throw e;
|
|
2760
|
+
}
|
|
2761
|
+
const release = await lockfile.lock(filePath, {
|
|
2762
|
+
retries: {
|
|
2763
|
+
retries: LOCK_RETRIES,
|
|
2764
|
+
minTimeout: LOCK_RETRY_MIN_MS,
|
|
2765
|
+
maxTimeout: LOCK_RETRY_MAX_MS,
|
|
2766
|
+
factor: 2
|
|
2767
|
+
},
|
|
2768
|
+
stale: LOCK_STALE_MS,
|
|
2769
|
+
realpath: false
|
|
2770
|
+
});
|
|
2771
|
+
try {
|
|
2772
|
+
configstoreInstance = null;
|
|
2773
|
+
const fresh = await getConfigstore();
|
|
2774
|
+
fresh.all = _objectSpread2(_objectSpread2({}, readAll(fresh)), patch);
|
|
2775
|
+
} finally {
|
|
2776
|
+
await release();
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
return {
|
|
2780
|
+
async get() {
|
|
2781
|
+
return readAll(await getConfigstore());
|
|
2782
|
+
},
|
|
2783
|
+
async set(patch) {
|
|
2784
|
+
const next = setChain.then(() => doLockedSet(patch));
|
|
2785
|
+
setChain = next.catch(() => {});
|
|
2786
|
+
return next;
|
|
2787
|
+
}
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2570
2791
|
//#endregion
|
|
2571
2792
|
//#region src/index.ts
|
|
2572
2793
|
/**
|
|
@@ -2582,8 +2803,10 @@ exports.Claims = Claims;
|
|
|
2582
2803
|
exports.CodeFlow = CodeFlow;
|
|
2583
2804
|
exports.Gql = Gql;
|
|
2584
2805
|
exports.Meta = Meta;
|
|
2806
|
+
exports.NotLoggedInError = NotLoggedInError;
|
|
2585
2807
|
exports.PasswordFlow = PasswordFlow;
|
|
2586
2808
|
exports.Rpc = Rpc;
|
|
2809
|
+
exports.SessionExpiredError = SessionExpiredError;
|
|
2587
2810
|
exports.SignUp = SignUp;
|
|
2588
2811
|
exports.Sql = Sql;
|
|
2589
2812
|
exports.SqlNoToken = SqlNoToken;
|
|
@@ -2595,6 +2818,8 @@ exports.Ws = Ws;
|
|
|
2595
2818
|
exports.createApi = createApi;
|
|
2596
2819
|
exports.createCentiaAdminClient = createCentiaAdminClient;
|
|
2597
2820
|
exports.createCentiaClient = createCentiaClient;
|
|
2821
|
+
exports.createConfigstoreTokenStore = createConfigstoreTokenStore;
|
|
2598
2822
|
exports.createSqlBuilder = createSqlBuilder;
|
|
2823
|
+
exports.createTokenProvider = createTokenProvider;
|
|
2599
2824
|
exports.isCentiaApiError = isCentiaApiError;
|
|
2600
2825
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@centia-io/sdk",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Centia-io TypeScript SDK",
|
|
5
5
|
"author": "Martin Høgh",
|
|
6
6
|
"license": "MIT",
|
|
@@ -24,10 +24,20 @@
|
|
|
24
24
|
"test": "vitest run",
|
|
25
25
|
"test:watch": "vitest"
|
|
26
26
|
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"configstore": "^7.0.0",
|
|
29
|
+
"proper-lockfile": "^4.1.2"
|
|
30
|
+
},
|
|
27
31
|
"devDependencies": {
|
|
32
|
+
"@types/configstore": "^6.0.2",
|
|
33
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
28
34
|
"tsdown": "^0.17.3",
|
|
35
|
+
"tsx": "^4.19.0",
|
|
29
36
|
"typescript": "^5.6.3",
|
|
30
37
|
"vitest": "^4.0.18"
|
|
31
38
|
},
|
|
32
|
-
"private": false
|
|
39
|
+
"private": false,
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=18"
|
|
42
|
+
}
|
|
33
43
|
}
|