@skyvexsoftware/stratos-sdk 0.7.8 → 0.8.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/README.md CHANGED
@@ -130,11 +130,31 @@ Individual hooks are also available:
130
130
  | `useTrackingSession()` | High-level tracking state with derived fields |
131
131
  | `useLandingAnalysis()` | Landing rate, bounces, and settled analysis |
132
132
  | `useShellAuth()` | Authentication state and token |
133
+ | `useVaApi()` | Shared axios instance for the bound VA's API |
133
134
  | `useShellConfig()` | Scoped configuration access |
134
135
  | `useShellNavigation()` | Route navigation utilities |
135
136
  | `useShellToast()` | Toast/notification functions |
136
137
  | `usePluginLogger()` | Scoped renderer-side logger |
137
138
 
139
+ ### VA API client
140
+
141
+ For HTTP calls to the bound airline's API, use `useVaApi()` (renderer) or `ctx.airline.createClient()` (background). Both return an [axios](https://axios-http.com/) instance that the shell pre-configures with the airline's `base_url`, the pilot's bearer token, and a `401` → refresh → retry interceptor:
142
+
143
+ ```tsx
144
+ import { useVaApi } from "@skyvexsoftware/stratos-sdk";
145
+
146
+ const va = useVaApi();
147
+ const { data } = await va.get("/pilot/verify");
148
+ ```
149
+
150
+ The renderer-side instance is provided by `PluginShellProvider` and **shared across the shell and every plugin UI** — concurrent `401`s coalesce onto a single refresh roundtrip rather than each plugin firing its own.
151
+
152
+ #### About the axios dependency
153
+
154
+ `axios` is a regular dependency of this SDK, so plugins **don't need to declare it** in their own `package.json` if they only consume it through `useVaApi()` / `ctx.airline.createClient()`. The plugin's bundle won't ship its own copy either — the shell's instance is reused at runtime through the plugin context.
155
+
156
+ Only add `axios` to your plugin's deps if you import it directly — for example to call `axios.isAxiosError(err)` for narrowing or to build your own custom instance. If you do, pin the same major version as the SDK to avoid runtime surprises.
157
+
138
158
  ### Background Module
139
159
 
140
160
  Optional main-process code with access to IPC, Express routes, SQLite, and more. Must use the `createPlugin` helper (imported from the `/helpers` subpath) with a default export:
@@ -0,0 +1,33 @@
1
+ /**
2
+ * VA API client — singleton axios instance for plugins to call the bound
3
+ * airline's API. Transparently refreshes the access token on 401.
4
+ *
5
+ * Usage from a plugin component:
6
+ *
7
+ * import { useVaApi } from "@skyvexsoftware/stratos-sdk";
8
+ *
9
+ * const va = useVaApi();
10
+ * const profile = await va.get<{ pilotID: string }>("/pilot/verify");
11
+ *
12
+ * Refresh behaviour:
13
+ * - On 401 we ask the shell to refresh (POST /api/auth/va/refresh).
14
+ * - If refresh succeeds, the request is retried exactly once.
15
+ * - If refresh fails or the VA isn't configured for refresh, the 401
16
+ * surfaces to the caller and the shell's existing re-auth flow takes
17
+ * over (the auth state will flip on the next /api/auth poll).
18
+ *
19
+ * The token and baseURL are pulled from the local shell server on every
20
+ * request, so plugins don't need to subscribe to auth changes — the
21
+ * latest token is always used.
22
+ *
23
+ * Concurrency: a single in-flight refresh promise is shared across all
24
+ * callers of this client. Concurrent 401s coalesce onto the same refresh,
25
+ * and outbound requests issued while a refresh is running wait for it to
26
+ * settle before reading the bearer — otherwise they'd send the stale token
27
+ * and get a redundant 401.
28
+ */
29
+ import { AxiosInstance } from "axios";
30
+ /** Singleton axios instance for VA API calls with auto-refresh on 401. */
31
+ export declare const vaApi: AxiosInstance;
32
+ export default vaApi;
33
+ //# sourceMappingURL=vaApi.d.ts.map
@@ -0,0 +1,107 @@
1
+ /**
2
+ * VA API client — singleton axios instance for plugins to call the bound
3
+ * airline's API. Transparently refreshes the access token on 401.
4
+ *
5
+ * Usage from a plugin component:
6
+ *
7
+ * import { useVaApi } from "@skyvexsoftware/stratos-sdk";
8
+ *
9
+ * const va = useVaApi();
10
+ * const profile = await va.get<{ pilotID: string }>("/pilot/verify");
11
+ *
12
+ * Refresh behaviour:
13
+ * - On 401 we ask the shell to refresh (POST /api/auth/va/refresh).
14
+ * - If refresh succeeds, the request is retried exactly once.
15
+ * - If refresh fails or the VA isn't configured for refresh, the 401
16
+ * surfaces to the caller and the shell's existing re-auth flow takes
17
+ * over (the auth state will flip on the next /api/auth poll).
18
+ *
19
+ * The token and baseURL are pulled from the local shell server on every
20
+ * request, so plugins don't need to subscribe to auth changes — the
21
+ * latest token is always used.
22
+ *
23
+ * Concurrency: a single in-flight refresh promise is shared across all
24
+ * callers of this client. Concurrent 401s coalesce onto the same refresh,
25
+ * and outbound requests issued while a refresh is running wait for it to
26
+ * settle before reading the bearer — otherwise they'd send the stale token
27
+ * and get a redundant 401.
28
+ */
29
+ import axios from "axios";
30
+ import { STRATOS_APP_BASE } from "../helpers/server";
31
+ const SHELL_API = `${STRATOS_APP_BASE}/api`;
32
+ async function readAuth() {
33
+ try {
34
+ const res = await axios.get(`${SHELL_API}/auth`);
35
+ return {
36
+ vaBaseUrl: res.data.data?.vaBaseUrl ?? null,
37
+ vaToken: res.data.data?.vaToken ?? null,
38
+ };
39
+ }
40
+ catch {
41
+ return { vaBaseUrl: null, vaToken: null };
42
+ }
43
+ }
44
+ let refreshInflight = null;
45
+ function ensureRefresh() {
46
+ if (refreshInflight)
47
+ return refreshInflight;
48
+ refreshInflight = (async () => {
49
+ try {
50
+ const res = await axios.post(`${SHELL_API}/auth/va/refresh`);
51
+ return !!res.data.refreshed;
52
+ }
53
+ catch {
54
+ return false;
55
+ }
56
+ })().finally(() => {
57
+ refreshInflight = null;
58
+ });
59
+ return refreshInflight;
60
+ }
61
+ class VaApiClient {
62
+ constructor() {
63
+ this.axiosInstance = axios.create({
64
+ timeout: 15000,
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ Accept: "application/json",
68
+ },
69
+ });
70
+ this.axiosInstance.interceptors.request.use(async (config) => {
71
+ // Wait for any in-flight refresh so we read the post-refresh bearer
72
+ // instead of racing the write.
73
+ if (refreshInflight)
74
+ await refreshInflight;
75
+ const auth = await readAuth();
76
+ if (auth.vaBaseUrl)
77
+ config.baseURL = auth.vaBaseUrl;
78
+ if (auth.vaToken)
79
+ config.headers.Authorization = `Bearer ${auth.vaToken}`;
80
+ return config;
81
+ });
82
+ this.axiosInstance.interceptors.response.use((response) => response, async (error) => {
83
+ const original = error.config;
84
+ if (error.response?.status !== 401 ||
85
+ !original ||
86
+ original.__vaRetried) {
87
+ return Promise.reject(error);
88
+ }
89
+ original.__vaRetried = true;
90
+ const refreshed = await ensureRefresh();
91
+ if (!refreshed)
92
+ return Promise.reject(error);
93
+ return await this.axiosInstance.request(original);
94
+ });
95
+ }
96
+ static getInstance() {
97
+ if (!VaApiClient.instance)
98
+ VaApiClient.instance = new VaApiClient();
99
+ return VaApiClient.instance;
100
+ }
101
+ get axios() {
102
+ return this.axiosInstance;
103
+ }
104
+ }
105
+ /** Singleton axios instance for VA API calls with auto-refresh on 401. */
106
+ export const vaApi = VaApiClient.getInstance().axios;
107
+ export default vaApi;
@@ -7,6 +7,7 @@ export { useSimData, simDataKeys } from "./useSimData";
7
7
  export { useTrackingSession, trackingSessionKeys } from "./useTrackingSession";
8
8
  export { usePluginLogger } from "./usePluginLogger";
9
9
  export { useShellAuth } from "./useShellAuth";
10
+ export { useVaApi } from "./useVaApi";
10
11
  export { useShellConfig } from "./useShellConfig";
11
12
  export { useShellNavigation } from "./useShellNavigation";
12
13
  export { useShellToast } from "./useShellToast";
@@ -7,6 +7,7 @@ export { useSimData, simDataKeys } from "./useSimData";
7
7
  export { useTrackingSession, trackingSessionKeys } from "./useTrackingSession";
8
8
  export { usePluginLogger } from "./usePluginLogger";
9
9
  export { useShellAuth } from "./useShellAuth";
10
+ export { useVaApi } from "./useVaApi";
10
11
  export { useShellConfig } from "./useShellConfig";
11
12
  export { useShellNavigation } from "./useShellNavigation";
12
13
  export { useShellToast } from "./useShellToast";
@@ -0,0 +1,27 @@
1
+ import type { AxiosInstance } from "axios";
2
+ /**
3
+ * Returns the shared axios instance for the bound airline's API.
4
+ *
5
+ * The shell provides a single instance through PluginShellProvider so all
6
+ * plugin UIs (and the shell itself) coalesce concurrent 401s onto one
7
+ * refresh — the in-flight refresh promise lives on the shared instance,
8
+ * so a 401 in plugin A and plugin B at the same time fires exactly one
9
+ * `POST /api/auth/va/refresh`.
10
+ *
11
+ * Behaviour (set up by the shell):
12
+ * - baseURL is the airline's base_url (resolved per request).
13
+ * - Authorization is the current pilot's VA bearer.
14
+ * - On 401, the shell refreshes the access token at the VA's refresh URL.
15
+ * Success → request retried once. Failure → 401 propagates and the
16
+ * shell's existing re-auth flow takes over.
17
+ *
18
+ * Must be used inside a PluginShellProvider tree. For non-React callers
19
+ * (helpers, services, tests), import the standalone `vaApi` export instead.
20
+ *
21
+ * Example:
22
+ *
23
+ * const va = useVaApi();
24
+ * const { data } = await va.get<{ pilotID: string }>("/pilot/verify");
25
+ */
26
+ export declare function useVaApi(): AxiosInstance;
27
+ //# sourceMappingURL=useVaApi.d.ts.map
@@ -0,0 +1,28 @@
1
+ import { usePluginContext } from "./context";
2
+ /**
3
+ * Returns the shared axios instance for the bound airline's API.
4
+ *
5
+ * The shell provides a single instance through PluginShellProvider so all
6
+ * plugin UIs (and the shell itself) coalesce concurrent 401s onto one
7
+ * refresh — the in-flight refresh promise lives on the shared instance,
8
+ * so a 401 in plugin A and plugin B at the same time fires exactly one
9
+ * `POST /api/auth/va/refresh`.
10
+ *
11
+ * Behaviour (set up by the shell):
12
+ * - baseURL is the airline's base_url (resolved per request).
13
+ * - Authorization is the current pilot's VA bearer.
14
+ * - On 401, the shell refreshes the access token at the VA's refresh URL.
15
+ * Success → request retried once. Failure → 401 propagates and the
16
+ * shell's existing re-auth flow takes over.
17
+ *
18
+ * Must be used inside a PluginShellProvider tree. For non-React callers
19
+ * (helpers, services, tests), import the standalone `vaApi` export instead.
20
+ *
21
+ * Example:
22
+ *
23
+ * const va = useVaApi();
24
+ * const { data } = await va.get<{ pilotID: string }>("/pilot/verify");
25
+ */
26
+ export function useVaApi() {
27
+ return usePluginContext().vaApi;
28
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export type { PluginAuthor, PluginManifest } from "./types/manifest";
2
- export type { PluginAirline, PluginAirlineAccessor, PluginAuthAccessor, PluginConfigStore, PluginContext, PluginDatabaseAccessor, PluginIPCRegistrar, PluginLogger, PluginNavigationHelper, PluginPilotUser, PluginServerRegistrar, PluginToastAPI, PluginUIContext, PluginSettingType, PluginSettingOption, PluginSettingDefinition, PluginAvailableSettings, BooleanSettingDef, TextSettingDef, LongtextSettingDef, NumberSettingDef, RangeSettingDef, ListSettingDef, RadioSettingDef, DateSettingDef, JsonSettingDef, } from "./types/context";
2
+ export type { PluginAirline, PluginAirlineAccessor, PluginAuthAccessor, PluginVaApiClient, PluginConfigStore, PluginContext, PluginDatabaseAccessor, PluginIPCRegistrar, PluginLogger, PluginNavigationHelper, PluginPilotUser, PluginServerRegistrar, PluginToastAPI, PluginUIContext, PluginSettingType, PluginSettingOption, PluginSettingDefinition, PluginAvailableSettings, BooleanSettingDef, TextSettingDef, LongtextSettingDef, NumberSettingDef, RangeSettingDef, ListSettingDef, RadioSettingDef, DateSettingDef, JsonSettingDef, } from "./types/context";
3
3
  export type { PluginBackgroundModule, PluginRouteComponent, PluginUIModule, } from "./types/module";
4
4
  export { FlightPhase, SimulatorType, EventCategory, } from "./shared-types/simulator";
5
5
  export type { BounceData, CapturePoint, FlightData, FlightDataSnapshot, FlightEventPayload, FlightEventsSnapshot, FlightLandingPayload, FlightLogEvent, FlightPhasePayload, FlightPhaseSnapshot, FlightStateDebugInfo, FlightTrends, HistoryReportEntry, LandingAnalysis, LandingAnalysisSnapshot, LandingAnalyzerDebugState, LandingSample, PendingPhaseTransition, SimDataSnapshot, SimulatorStatus, } from "./shared-types/simulator";
@@ -7,6 +7,7 @@ export type { ThemeMode } from "./shared-types/theme";
7
7
  export type { FlightPlan, FlightStatus, CurrentFlight, FlightComment, PreflightCheck, PreflightCheckResult, StartFlightOptions, FlightManagerPayload, StartFlightResult, RecoverableFlight, } from "./shared-types/flight-manager";
8
8
  export { createPlugin } from "./helpers/createPlugin";
9
9
  export { STRATOS_APP_PORT, STRATOS_APP_BASE } from "./helpers/server";
10
+ export { vaApi } from "./api/vaApi";
10
11
  export { weightToLbs, weightFromLbs, altitudeToFt, altitudeFromFt, verticalSpeedFromFpm, verticalSpeedToFpm, distanceToNm, distanceFromNm, formatWeight, formatAltitude, formatDistance, formatVerticalSpeed, } from "./helpers/units";
11
12
  export type { WeightUnit, AltitudeUnit, DistanceUnit, UnitPreferences, } from "./helpers/units";
12
13
  export { PluginUICtx, usePluginContext } from "./hooks/context";
@@ -18,6 +19,7 @@ export { useSimData, simDataKeys } from "./hooks/useSimData";
18
19
  export { useTrackingSession, trackingSessionKeys, } from "./hooks/useTrackingSession";
19
20
  export { usePluginLogger } from "./hooks/usePluginLogger";
20
21
  export { useShellAuth } from "./hooks/useShellAuth";
22
+ export { useVaApi } from "./hooks/useVaApi";
21
23
  export { useShellConfig } from "./hooks/useShellConfig";
22
24
  export { useShellNavigation } from "./hooks/useShellNavigation";
23
25
  export { useShellToast } from "./hooks/useShellToast";
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ export { FlightPhase, SimulatorType, EventCategory, } from "./shared-types/simul
3
3
  // ── Helper Functions ───────────────────────────────────────────────────
4
4
  export { createPlugin } from "./helpers/createPlugin";
5
5
  export { STRATOS_APP_PORT, STRATOS_APP_BASE } from "./helpers/server";
6
+ export { vaApi } from "./api/vaApi";
6
7
  export { weightToLbs, weightFromLbs, altitudeToFt, altitudeFromFt, verticalSpeedFromFpm, verticalSpeedToFpm, distanceToNm, distanceFromNm, formatWeight, formatAltitude, formatDistance, formatVerticalSpeed, } from "./helpers/units";
7
8
  // ── React Hooks ────────────────────────────────────────────────────────
8
9
  export { PluginUICtx, usePluginContext } from "./hooks/context";
@@ -14,6 +15,7 @@ export { useSimData, simDataKeys } from "./hooks/useSimData";
14
15
  export { useTrackingSession, trackingSessionKeys, } from "./hooks/useTrackingSession";
15
16
  export { usePluginLogger } from "./hooks/usePluginLogger";
16
17
  export { useShellAuth } from "./hooks/useShellAuth";
18
+ export { useVaApi } from "./hooks/useVaApi";
17
19
  export { useShellConfig } from "./hooks/useShellConfig";
18
20
  export { useShellNavigation } from "./hooks/useShellNavigation";
19
21
  export { useShellToast } from "./hooks/useShellToast";
@@ -4,6 +4,7 @@
4
4
  * Defines the context objects the shell provides to plugins
5
5
  * for both background (main process) and UI (renderer) usage.
6
6
  */
7
+ import type { AxiosInstance } from "axios";
7
8
  /** All supported setting control types */
8
9
  export type PluginSettingType = "boolean" | "text" | "longtext" | "number" | "range" | "list" | "radio" | "date" | "json";
9
10
  /** Option entry for list and radio settings */
@@ -129,10 +130,32 @@ export type PluginAirline = {
129
130
  /** Bearer for this airline's API. Null if the VA session has expired. */
130
131
  token: string | null;
131
132
  };
133
+ /**
134
+ * Pre-configured axios instance for the bound airline's API.
135
+ *
136
+ * - baseURL is the airline's base_url
137
+ * - Authorization header is the current VA bearer (resolved per-request)
138
+ * - On 401, the shell refreshes the VA access token if a refresh URL is
139
+ * configured, then retries the request exactly once. If refresh fails
140
+ * or isn't supported, the 401 surfaces to the caller.
141
+ *
142
+ * Use this instead of raw fetch(`${airline.baseUrl}/...`) so plugins get
143
+ * refresh-token handling for free.
144
+ *
145
+ * Aliased here for clarity at the plugin contract surface — at runtime
146
+ * this is the same shape as an axios `AxiosInstance`.
147
+ */
148
+ export type PluginVaApiClient = AxiosInstance;
132
149
  /** Read-only accessor for the currently-bound airline. */
133
150
  export type PluginAirlineAccessor = {
134
151
  /** Returns the current airline, or null if no VA session is active. */
135
152
  getCurrent(): Promise<PluginAirline | null>;
153
+ /**
154
+ * Returns an axios instance pre-configured for this airline's API
155
+ * with auto-refresh on 401. Safe to cache for the lifetime of the
156
+ * background module.
157
+ */
158
+ createClient(): PluginVaApiClient;
136
159
  };
137
160
  /** Express route registration helper for background plugins */
138
161
  export type PluginServerRegistrar = {
@@ -203,6 +226,13 @@ export type PluginToastAPI = {
203
226
  * Provides shared hooks and utilities from the shell.
204
227
  */
205
228
  export type PluginUIContext = {
229
+ /**
230
+ * Shared axios instance for the bound airline's API, with auto-refresh on
231
+ * 401. Provided by the shell so all plugin UIs and the shell itself share
232
+ * one in-flight refresh promise — concurrent 401s coalesce to a single
233
+ * `POST /api/auth/va/refresh`. Reach this through `useVaApi()`.
234
+ */
235
+ vaApi: AxiosInstance;
206
236
  /** The plugin's unique ID */
207
237
  pluginId: string;
208
238
  /** Auth state from the shell */
@@ -1,14 +1,14 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "pill-pause" | "pill-resume" | "pill-end" | "pill-submit" | null | undefined;
4
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "pill-pause" | "pill-resume" | "pill-end" | "pill-submit" | null | undefined;
5
5
  size?: "default" | "sm" | "lg" | "icon" | "pill" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<typeof buttonVariants> & {
8
8
  asChild?: boolean;
9
9
  };
10
10
  declare const Button: React.ForwardRefExoticComponent<React.ButtonHTMLAttributes<HTMLButtonElement> & VariantProps<(props?: ({
11
- variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "pill-pause" | "pill-resume" | "pill-end" | "pill-submit" | null | undefined;
11
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | "pill-pause" | "pill-resume" | "pill-end" | "pill-submit" | null | undefined;
12
12
  size?: "default" | "sm" | "lg" | "icon" | "pill" | null | undefined;
13
13
  } & import("class-variance-authority/types").ClassProp) | undefined) => string> & {
14
14
  asChild?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyvexsoftware/stratos-sdk",
3
- "version": "0.7.8",
3
+ "version": "0.8.0",
4
4
  "description": "Plugin SDK for Stratos — types, hooks, and UI components",
5
5
  "author": {
6
6
  "name": "Skyvex Software Pty Ltd",
@@ -94,7 +94,7 @@
94
94
  "@radix-ui/react-switch": "^1.2.0",
95
95
  "@radix-ui/react-tabs": "^1.1.0",
96
96
  "@radix-ui/react-tooltip": "^1.1.0",
97
- "@tanstack/react-query": "^5.0.0",
97
+ "@tanstack/react-query": "^5.99.0",
98
98
  "@vitejs/plugin-react": "^5.0.0",
99
99
  "class-variance-authority": "^0.7.0",
100
100
  "lucide-react": ">=0.300.0",
@@ -103,6 +103,7 @@
103
103
  "vite": "^7.0.0"
104
104
  },
105
105
  "dependencies": {
106
+ "axios": "^1.15.0",
106
107
  "clsx": "^2.1.1",
107
108
  "magic-string": "^0.30.21",
108
109
  "tailwind-merge": "^3.2.0"