@centia-io/sdk 0.0.58 → 0.1.1

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