@everysk/apps-devkit 0.1.0 → 0.3.0-beta-20261016180126

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.
@@ -0,0 +1,153 @@
1
+ import { createApiTokenSource } from "../apiToken.js";
2
+ /** Every path under the platform's file route, claimed whole so none falls through to `index.html`. */
3
+ const FILE_ROUTE = /^\/file\//;
4
+ /**
5
+ * A file's url as the platform serves it: the hex of a version-4 UUID, as everysk/client's
6
+ * `FileRequestHandler` matches it (`RE_FILE_URL`).
7
+ */
8
+ const FILE_URL = /^\/file\/([0-9a-f]{12}4[0-9a-f]{3}[89ab][0-9a-f]{15})\/?(?:\?.*)?$/;
9
+ /** Why a file could not be served, with the status to answer it with. */
10
+ class FileRouteError extends Error {
11
+ status;
12
+ constructor(status, message) {
13
+ super(message);
14
+ this.status = status;
15
+ }
16
+ }
17
+ /** The error and, when Node gives one, its cause, which is where fetch keeps ECONNREFUSED or DNS. */
18
+ const describeError = (error) => {
19
+ if (!(error instanceof Error))
20
+ return String(error);
21
+ return error.cause instanceof Error
22
+ ? `${error.message}: ${error.cause.message}`
23
+ : error.message;
24
+ };
25
+ /** The API's own `message` from a refusal, or the start of whatever came back instead. */
26
+ const refusalOf = async (response) => {
27
+ const text = await response.text();
28
+ try {
29
+ return JSON.parse(text).message ?? text.slice(0, 200);
30
+ }
31
+ catch {
32
+ return text.slice(0, 200);
33
+ }
34
+ };
35
+ /**
36
+ * The file whose stored url is `/<hash>`, read with its content. A file stores its url as
37
+ * `/<hash>` and the API adds `/file` only when it returns it, and the file list accepts a `url`
38
+ * filter without a workspace (everysk-lib 1.13.4 `sdk/entities/file`, everysk/client
39
+ * `File.validate_query`).
40
+ */
41
+ const findFile = async (fetchApi, apiUrl, accessToken, hash) => {
42
+ const query = JSON.stringify({ filters: [["url", `/${hash}`]], page_size: 1 });
43
+ let response;
44
+ try {
45
+ response = await fetchApi(`${apiUrl}/files?query=${encodeURIComponent(query)}`, {
46
+ headers: { Authorization: `Bearer ${accessToken}` },
47
+ });
48
+ }
49
+ catch (error) {
50
+ throw new FileRouteError(502, `The API could not be reached: ${describeError(error)}`);
51
+ }
52
+ if (!response.ok) {
53
+ const refusal = await refusalOf(response);
54
+ throw new FileRouteError(502, `The API refused the file: ${response.status} ${response.statusText}: ${refusal}`);
55
+ }
56
+ let body;
57
+ try {
58
+ body = (await response.json());
59
+ }
60
+ catch {
61
+ throw new FileRouteError(502, `The API answered with ${response.headers.get("content-type")}, not JSON.`);
62
+ }
63
+ if (!Array.isArray(body.files)) {
64
+ throw new FileRouteError(502, `The file list came back without files: ${JSON.stringify(body).slice(0, 200)}`);
65
+ }
66
+ return body.files[0];
67
+ };
68
+ /** The content of a found file, refusing what would be served as an empty or corrupt file. */
69
+ const contentOf = (file, hash) => {
70
+ if (typeof file.data !== "string") {
71
+ throw new FileRouteError(502, `The API returned ${file.name} (/file/${hash}) without its content.`);
72
+ }
73
+ if (file.data.startsWith("data:")) {
74
+ throw new FileRouteError(422, `${file.name} was stored as a data URL, so its bytes are not the file: upload it again, read with fileToBase64.`);
75
+ }
76
+ return Buffer.from(file.data, "base64");
77
+ };
78
+ /**
79
+ * createDevFileRoute
80
+ *
81
+ * The dev server's stand-in for the platform's `/file/<hash>` route. On the platform a file's
82
+ * `url` is that route, served with the user's session and shown inline; the dev server has no
83
+ * session, so the file is found through the API with the dev proxy's OAuth2 token instead, by
84
+ * its stored `url`, and its base64 content is decoded here and sent with its own type. So an app
85
+ * shows a file through its `url` wherever it runs.
86
+ *
87
+ * Read with the OAuth2 client's permissions, not a user's, as everything through the dev proxy is.
88
+ * Every path under `/file/` is answered here, never handed on: Vite would answer it with the
89
+ * app's `index.html` and a 200. A file that cannot be served gets a status that says why, and a
90
+ * line in the terminal, since an `<img>` or `<iframe>` never shows the body. Nothing escapes the
91
+ * middleware: Connect ignores its promise, and a rejection would end the dev server.
92
+ *
93
+ * @param params - The API, its token and last failure, and a `fetch` and a log to replace in tests.
94
+ * @returns A Connect middleware that answers `/file/` and passes everything else on.
95
+ */
96
+ export const createDevFileRoute = ({ apiUrl, token, lastError, fetch: fetchApi = fetch, log = console.warn, }) => async (req, res, next) => {
97
+ const url = req.url ?? "";
98
+ if (!FILE_ROUTE.test(url))
99
+ return next();
100
+ const fail = (status, message) => {
101
+ log(`[dev server] ${url}: ${message}`);
102
+ res.statusCode = status;
103
+ res.setHeader("Content-Type", "text/plain; charset=utf-8");
104
+ res.end(message);
105
+ };
106
+ if (req.method !== "GET" && req.method !== "HEAD")
107
+ return fail(405, "A file is only read here.");
108
+ const hash = FILE_URL.exec(url)?.[1];
109
+ if (!hash)
110
+ return fail(400, "Not a file url: /file/ and the 32-character hex hash of a file.");
111
+ const accessToken = token();
112
+ if (!accessToken) {
113
+ return fail(503, `No OAuth2 token for EVERYSK_API_URL: ${lastError()?.message ?? "not there yet"}`);
114
+ }
115
+ try {
116
+ const file = await findFile(fetchApi, apiUrl, accessToken, hash);
117
+ if (!file) {
118
+ return fail(404, `No file has the url /file/${hash}, as the OAuth2 client EVERYSK_API_CLIENT_ID sees.`);
119
+ }
120
+ const content = contentOf(file, hash);
121
+ const disposition = `inline; filename*=UTF-8''${encodeURIComponent(file.name)}`;
122
+ res.setHeader("Content-Type", file.content_type || "application/octet-stream");
123
+ res.setHeader("Content-Length", content.length);
124
+ res.setHeader("Content-Disposition", disposition);
125
+ res.statusCode = 200;
126
+ res.end(req.method === "HEAD" ? undefined : content);
127
+ }
128
+ catch (error) {
129
+ fail(error instanceof FileRouteError ? error.status : 502, describeError(error));
130
+ }
131
+ };
132
+ /**
133
+ * serveDevFilesPlugin
134
+ *
135
+ * Mounts `createDevFileRoute` on the dev server and on `vite preview`, neither of which has the
136
+ * platform's `/file/` route, logging through Vite's own logger.
137
+ *
138
+ * @param {ApiTokenSource} [tokens] - The token to send; created when the server starts if
139
+ * omitted. `defineAppConfig` passes the one it shares with the `/api` proxy.
140
+ */
141
+ export function serveDevFilesPlugin(tokens) {
142
+ const route = (log) => createDevFileRoute({ ...(tokens ?? createApiTokenSource()), log });
143
+ return {
144
+ name: "serve-dev-files",
145
+ configureServer(server) {
146
+ server.middlewares.use(route((message) => server.config.logger.warn(message)));
147
+ },
148
+ configurePreviewServer(server) {
149
+ server.middlewares.use(route((message) => server.config.logger.warn(message)));
150
+ },
151
+ };
152
+ }
153
+ //# sourceMappingURL=serveDevFiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serveDevFiles.js","sourceRoot":"","sources":["../../../src/vite/plugins/serveDevFiles.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,16 @@
1
1
  import type { ProxyOptions } from "vite";
2
+ import { type ApiTokenSource } from "./apiToken.js";
2
3
  /**
3
4
  * createDevServerProxy — Vite dev proxy for `/api`, authenticated with OAuth2 client credentials.
4
5
  *
5
6
  * Forwards `/api/*` to `EVERYSK_API_URL` (stripping the `/api` prefix) and injects an
6
- * `Authorization: Bearer <access_token>` header. The token is obtained from
7
- * `${EVERYSK_API_URL}/oauth2/token` with the `client_credentials` grant, cached in memory, and
8
- * refreshed in the background shortly before it expires. It is fetched eagerly at startup so a
9
- * token is ready before the first API call.
7
+ * `Authorization: Bearer <access_token>` header, from `createApiTokenSource`.
10
8
  *
9
+ * @param {ApiTokenSource} [tokens] - The token to send; a new source when omitted. The preset
10
+ * passes the one it shares with the `/file/` route.
11
11
  * @returns {Record<string, string | ProxyOptions>} The Vite `server.proxy` config for `/api`.
12
- * @throws {Error} If `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or `EVERYSK_API_CLIENT_SECRET`
13
- * is missing.
12
+ * @throws {Error} If `tokens` is omitted and `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or
13
+ * `EVERYSK_API_CLIENT_SECRET` is missing.
14
14
  */
15
- export declare function createDevServerProxy(): Record<string, string | ProxyOptions>;
15
+ export declare function createDevServerProxy(tokens?: ApiTokenSource): Record<string, string | ProxyOptions>;
16
16
  //# sourceMappingURL=serverProxy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"serverProxy.d.ts","sourceRoot":"","sources":["../../src/vite/serverProxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAMzC;;;;;;;;;;;;GAYG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,CAoE5E"}
1
+ {"version":3,"file":"serverProxy.d.ts","sourceRoot":"","sources":["../../src/vite/serverProxy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,MAAM,CAAC;AAEzC,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAChC,MAAM,GAAE,cAAuC,GAChD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,CAAC,CAqBvC"}
@@ -1,68 +1,20 @@
1
- // Refresh the OAuth2 token this many ms before it actually expires, so an in-flight
2
- // request never races a just-expired token.
3
- const REFRESH_SKEW_MS = 60_000;
1
+ import { createApiTokenSource } from "./apiToken.js";
4
2
  /**
5
3
  * createDevServerProxy — Vite dev proxy for `/api`, authenticated with OAuth2 client credentials.
6
4
  *
7
5
  * Forwards `/api/*` to `EVERYSK_API_URL` (stripping the `/api` prefix) and injects an
8
- * `Authorization: Bearer <access_token>` header. The token is obtained from
9
- * `${EVERYSK_API_URL}/oauth2/token` with the `client_credentials` grant, cached in memory, and
10
- * refreshed in the background shortly before it expires. It is fetched eagerly at startup so a
11
- * token is ready before the first API call.
6
+ * `Authorization: Bearer <access_token>` header, from `createApiTokenSource`.
12
7
  *
8
+ * @param {ApiTokenSource} [tokens] - The token to send; a new source when omitted. The preset
9
+ * passes the one it shares with the `/file/` route.
13
10
  * @returns {Record<string, string | ProxyOptions>} The Vite `server.proxy` config for `/api`.
14
- * @throws {Error} If `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or `EVERYSK_API_CLIENT_SECRET`
15
- * is missing.
11
+ * @throws {Error} If `tokens` is omitted and `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or
12
+ * `EVERYSK_API_CLIENT_SECRET` is missing.
16
13
  */
17
- export function createDevServerProxy() {
18
- const EVERYSK_API_URL = process.env.EVERYSK_API_URL;
19
- if (!EVERYSK_API_URL)
20
- throw new Error("EVERYSK_API_URL must be set.");
21
- const clientId = process.env.EVERYSK_API_CLIENT_ID;
22
- const clientSecret = process.env.EVERYSK_API_CLIENT_SECRET;
23
- if (!clientId || !clientSecret) {
24
- throw new Error("EVERYSK_API_CLIENT_ID and EVERYSK_API_CLIENT_SECRET must be set (OAuth2 client credentials).");
25
- }
26
- // Captured into an object so the narrowed `string` type survives inside the async closure below.
27
- const credentials = { clientId, clientSecret };
28
- const tokenUrl = `${EVERYSK_API_URL.replace(/\/$/, "")}/oauth2/token`;
29
- let accessToken = null;
30
- let refreshTimer = null;
31
- /**
32
- * refreshToken — fetch a fresh access token and schedule the next refresh before it expires.
33
- *
34
- * @returns {Promise<void>} Resolves once `accessToken` is updated.
35
- * @throws {Error} If the token request fails or returns no `access_token`.
36
- */
37
- async function refreshToken() {
38
- const res = await fetch(tokenUrl, {
39
- method: "POST",
40
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
41
- body: new URLSearchParams({
42
- grant_type: "client_credentials",
43
- client_id: credentials.clientId,
44
- client_secret: credentials.clientSecret,
45
- }),
46
- });
47
- if (!res.ok)
48
- throw new Error(`OAuth2 token request failed: ${res.status} ${res.statusText}`);
49
- const data = (await res.json());
50
- if (!data.access_token)
51
- throw new Error("OAuth2 token response had no access_token.");
52
- accessToken = data.access_token;
53
- const ttlMs = (data.expires_in ?? 3600) * 1000;
54
- if (refreshTimer)
55
- clearTimeout(refreshTimer);
56
- refreshTimer = setTimeout(() => {
57
- void refreshToken().catch((error) => console.error("[dev proxy] OAuth2 token refresh failed:", error));
58
- }, Math.max(ttlMs - REFRESH_SKEW_MS, 30_000));
59
- refreshTimer.unref?.();
60
- }
61
- // Warm the token before the first request; a failure is logged, not fatal (the request will 401).
62
- void refreshToken().catch((error) => console.error("[dev proxy] initial OAuth2 token fetch failed:", error));
14
+ export function createDevServerProxy(tokens = createApiTokenSource()) {
63
15
  return {
64
16
  "/api": {
65
- target: EVERYSK_API_URL,
17
+ target: tokens.apiUrl,
66
18
  changeOrigin: true,
67
19
  xfwd: true,
68
20
  rewrite: (p) => p.replace(/^\/api(?=\/|$)/, ""),
@@ -70,6 +22,7 @@ export function createDevServerProxy() {
70
22
  proxy.on("proxyReq", (proxyReq) => {
71
23
  proxyReq.removeHeader("origin");
72
24
  proxyReq.removeHeader("referer");
25
+ const accessToken = tokens.token();
73
26
  if (accessToken) {
74
27
  proxyReq.setHeader("Authorization", `Bearer ${accessToken}`);
75
28
  }
@@ -1 +1 @@
1
- {"version":3,"file":"serverProxy.js","sourceRoot":"","sources":["../../src/vite/serverProxy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serverProxy.js","sourceRoot":"","sources":["../../src/vite/serverProxy.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAE1E;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,oBAAoB,CAChC,SAAyB,oBAAoB,EAAE;IAE/C,OAAO;QACH,MAAM,EAAE;YACJ,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,YAAY,EAAE,IAAI;YAClB,IAAI,EAAE,IAAI;YACV,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC;YAE/C,SAAS,EAAE,CAAC,KAAK,EAAE,EAAE;gBACjB,KAAK,CAAC,EAAE,CAAC,UAAU,EAAE,CAAC,QAAQ,EAAE,EAAE;oBAC9B,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;oBAChC,QAAQ,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;oBAEjC,MAAM,WAAW,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC;oBACnC,IAAI,WAAW,EAAE,CAAC;wBACd,QAAQ,CAAC,SAAS,CAAC,eAAe,EAAE,UAAU,WAAW,EAAE,CAAC,CAAC;oBACjE,CAAC;gBACL,CAAC,CAAC,CAAC;YACP,CAAC;SACJ;KACJ,CAAC;AACN,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@everysk/apps-devkit",
3
- "version": "0.1.0",
3
+ "version": "0.3.0-beta-20261016180126",
4
4
  "description": "Build-time presets for Everysk client applications: Vite, ESLint, Vitest, TypeScript and Prettier.",
5
5
  "keywords": [
6
6
  "everysk",
@@ -55,15 +55,13 @@
55
55
  },
56
56
  "dependencies": {
57
57
  "@babel/core": "^7.29.7",
58
- "@eslint/js": "^9.39.4",
58
+ "@eslint/js": "^10.0.1",
59
59
  "@rolldown/plugin-babel": "^0.2.3",
60
60
  "@tailwindcss/vite": "^4.3.0",
61
61
  "@vitejs/plugin-react": "^6.0.2",
62
- "@vitest/coverage-v8": "^4.1.8",
62
+ "@vitest/coverage-v8": "^5.0.1",
63
63
  "babel-plugin-react-compiler": "^1.0.0",
64
- "eslint": "^9.39.4",
65
- "eslint-config-prettier": "^10.1.8",
66
- "eslint-plugin-prettier": "^5.5.6",
64
+ "eslint": "^10.10.0",
67
65
  "eslint-plugin-react-hooks": "^7.1.1",
68
66
  "eslint-plugin-react-refresh": "^0.5.2",
69
67
  "globals": "^17.6.0",
@@ -73,7 +71,7 @@
73
71
  "typescript": "^6.0.3",
74
72
  "typescript-eslint": "^8.60.1",
75
73
  "vite": "^8.0.16",
76
- "vitest": "^4.1.8"
74
+ "vitest": "^5.0.1"
77
75
  },
78
76
  "devDependencies": {
79
77
  "@types/babel__core": "^7.20.5",
@@ -84,7 +82,7 @@
84
82
  },
85
83
  "sideEffects": false,
86
84
  "engines": {
87
- "node": ">=22",
85
+ "node": "^22.13.0 || >=24",
88
86
  "npm": ">=12"
89
87
  }
90
88
  }
@@ -22,6 +22,17 @@ const noBarrelPaths = [
22
22
  const dataLayerMessage =
23
23
  "Data access only via entity hooks (@everysk/apps-sdk/<entity> or @/hooks/...). Pages and components must not call axios or an api module directly.";
24
24
 
25
+ export type EveryskConfigOptions = {
26
+ /**
27
+ * The app's directory, `import.meta.dirname` in its `eslint.config.js`. An editor lints a
28
+ * repository of several apps in one ESLint process, where typescript-eslint refuses to pick
29
+ * among their tsconfigs; only the app's own config knows which directory is its root.
30
+ */
31
+ tsconfigRootDir?: string;
32
+ /** Appended after the preset, so an app can add or override rules for its own files. */
33
+ extra?: ReturnType<typeof defineConfig>;
34
+ };
35
+
25
36
  /**
26
37
  * everyskConfig
27
38
  *
@@ -29,18 +40,23 @@ const dataLayerMessage =
29
40
  *
30
41
  * ```js
31
42
  * import { everyskConfig } from "@everysk/apps-devkit/eslint";
32
- * export default everyskConfig();
43
+ * export default everyskConfig({ tsconfigRootDir: import.meta.dirname });
33
44
  * ```
34
45
  *
35
46
  * It encodes the conventions that lint can actually check: no MUI barrel imports, and no
36
47
  * direct data access from pages or components. The rules that lint cannot express — which
37
48
  * subpath to import, which provider to wire — belong to the `everysk-apps` generator.
38
49
  *
39
- * @param {ReturnType<typeof defineConfig>} [extra] - Appended after the preset, so an app can
40
- * add or override rules for its own files.
50
+ * Takes the options, or the app's own blocks alone, as it did before there were options.
41
51
  */
42
- export const everyskConfig = (extra: ReturnType<typeof defineConfig> = []) =>
43
- defineConfig([
52
+ export const everyskConfig = (
53
+ optionsOrExtra: EveryskConfigOptions | ReturnType<typeof defineConfig> = {},
54
+ ) => {
55
+ const { tsconfigRootDir, extra = [] } = Array.isArray(optionsOrExtra)
56
+ ? { extra: optionsOrExtra }
57
+ : optionsOrExtra;
58
+
59
+ return defineConfig([
44
60
  globalIgnores(["dist", "coverage"]),
45
61
  {
46
62
  files: ["**/*.{ts,tsx}"],
@@ -53,6 +69,7 @@ export const everyskConfig = (extra: ReturnType<typeof defineConfig> = []) =>
53
69
  languageOptions: {
54
70
  ecmaVersion: 2020,
55
71
  globals: globals.browser,
72
+ ...(tsconfigRootDir ? { parserOptions: { tsconfigRootDir } } : {}),
56
73
  },
57
74
  rules: {
58
75
  "@typescript-eslint/no-restricted-imports": ["error", { paths: noBarrelPaths }],
@@ -88,3 +105,4 @@ export const everyskConfig = (extra: ReturnType<typeof defineConfig> = []) =>
88
105
  },
89
106
  ...extra,
90
107
  ]);
108
+ };
@@ -0,0 +1,107 @@
1
+ /** How long before a token expires it is renewed, so a request in flight never meets an expired one. */
2
+ const REFRESH_SKEW_MS = 60_000;
3
+
4
+ /** The first wait after a failed token request; it doubles on each failure, up to the maximum. */
5
+ const FIRST_RETRY_MS = 5_000;
6
+ const MAX_RETRY_MS = 60_000;
7
+
8
+ /** The API the dev server talks to, and the token it talks to it with. */
9
+ export type ApiTokenSource = {
10
+ /** `EVERYSK_API_URL`, without a trailing slash, so a path can be appended as is. */
11
+ apiUrl: string;
12
+ /** The current access token; `null` until one arrives, and once it has expired. */
13
+ token: () => string | null;
14
+ /** Why there is no token: the last failed request, until one succeeds. */
15
+ lastError: () => Error | null;
16
+ };
17
+
18
+ /**
19
+ * createApiTokenSource
20
+ *
21
+ * An OAuth2 access token for `EVERYSK_API_URL`, from the `client_credentials` grant at
22
+ * `${EVERYSK_API_URL}/oauth2/token`, cached in memory and renewed in the background a minute
23
+ * before it expires. It is fetched at once, so a token is ready before the first request. A
24
+ * failed request is logged and tried again, waiting 5 seconds and doubling up to a minute, so a
25
+ * VPN or API that comes back later does not need a restart of the dev server; until then the
26
+ * `/api` proxy sends no token (the API answers 401) and the `/file/` route says why, from
27
+ * `lastError`. `defineAppConfig` creates one source and hands it to both, so the dev server asks
28
+ * for one token, not one per use.
29
+ *
30
+ * @returns {ApiTokenSource} The API URL, the current token, and the last failure.
31
+ * @throws {Error} If `EVERYSK_API_URL`, `EVERYSK_API_CLIENT_ID`, or `EVERYSK_API_CLIENT_SECRET`
32
+ * is missing.
33
+ */
34
+ export function createApiTokenSource(): ApiTokenSource {
35
+ const apiUrl = process.env.EVERYSK_API_URL?.replace(/\/$/, "");
36
+ if (!apiUrl) throw new Error("EVERYSK_API_URL must be set.");
37
+
38
+ const clientId = process.env.EVERYSK_API_CLIENT_ID;
39
+ const clientSecret = process.env.EVERYSK_API_CLIENT_SECRET;
40
+ if (!clientId || !clientSecret) {
41
+ throw new Error(
42
+ "EVERYSK_API_CLIENT_ID and EVERYSK_API_CLIENT_SECRET must be set (OAuth2 client credentials).",
43
+ );
44
+ }
45
+ const credentials = { clientId, clientSecret };
46
+
47
+ const tokenUrl = `${apiUrl}/oauth2/token`;
48
+ let accessToken: string | null = null;
49
+ let expiresAt = 0;
50
+ let lastError: Error | null = null;
51
+ let retryMs = FIRST_RETRY_MS;
52
+ let timer: ReturnType<typeof setTimeout> | null = null;
53
+
54
+ const schedule = (delayMs: number) => {
55
+ if (timer) clearTimeout(timer);
56
+ timer = setTimeout(() => void refreshToken(), delayMs);
57
+ timer.unref?.();
58
+ };
59
+
60
+ /** Asks for a token, then schedules the next request: a renewal on success, a retry on failure. */
61
+ async function requestToken(): Promise<number> {
62
+ const res = await fetch(tokenUrl, {
63
+ method: "POST",
64
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
65
+ body: new URLSearchParams({
66
+ grant_type: "client_credentials",
67
+ client_id: credentials.clientId,
68
+ client_secret: credentials.clientSecret,
69
+ }),
70
+ });
71
+ if (!res.ok)
72
+ throw new Error(`OAuth2 token request failed: ${res.status} ${res.statusText}`);
73
+
74
+ const data = (await res.json()) as { access_token?: string; expires_in?: number };
75
+ if (!data.access_token) throw new Error("OAuth2 token response had no access_token.");
76
+
77
+ const ttlMs = (data.expires_in ?? 3600) * 1000;
78
+ accessToken = data.access_token;
79
+ expiresAt = Date.now() + ttlMs;
80
+ return ttlMs;
81
+ }
82
+
83
+ async function refreshToken(): Promise<void> {
84
+ try {
85
+ const ttlMs = await requestToken();
86
+ lastError = null;
87
+ retryMs = FIRST_RETRY_MS;
88
+ schedule(Math.max(ttlMs - REFRESH_SKEW_MS, 30_000));
89
+ } catch (error) {
90
+ lastError = error instanceof Error ? error : new Error(String(error));
91
+ console.error(
92
+ `[dev server] OAuth2 token request failed; trying again in ${retryMs / 1000}s:`,
93
+ lastError,
94
+ );
95
+ schedule(retryMs);
96
+ retryMs = Math.min(retryMs * 2, MAX_RETRY_MS);
97
+ }
98
+ }
99
+
100
+ void refreshToken();
101
+
102
+ return {
103
+ apiUrl,
104
+ token: () => (accessToken && Date.now() < expiresAt ? accessToken : null),
105
+ lastError: () => lastError,
106
+ };
107
+ }
package/src/vite/index.ts CHANGED
@@ -6,13 +6,17 @@ import react, { reactCompilerPreset } from "@vitejs/plugin-react";
6
6
  import babel from "@rolldown/plugin-babel";
7
7
  import tailwindcss from "@tailwindcss/vite";
8
8
 
9
+ import { createApiTokenSource } from "./apiToken.js";
9
10
  import { createDevServerProxy } from "./serverProxy.js";
10
11
  import { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
11
12
  import { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
13
+ import { serveDevFilesPlugin } from "./plugins/serveDevFiles.js";
12
14
 
15
+ export { createApiTokenSource, type ApiTokenSource } from "./apiToken.js";
13
16
  export { createDevServerProxy } from "./serverProxy.js";
14
17
  export { envVarsLocationPlugin } from "./plugins/envVarsLocation.js";
15
18
  export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
19
+ export { serveDevFilesPlugin } from "./plugins/serveDevFiles.js";
16
20
 
17
21
  /**
18
22
  * defineAppConfig
@@ -34,6 +38,8 @@ export { serveDevAppConfigPlugin } from "./plugins/serveDevAppConfig.js";
34
38
  * authenticates against `EVERYSK_API_URL` using OAuth2 client credentials.
35
39
  * - React with the compiler preset, Tailwind, and the two plugins that carry the app's
36
40
  * `config.json` `env_vars` into the page — a meta tag in build, `/app-config.json` in dev.
41
+ * - The platform's `/file/<hash>` route, on the dev server and `vite preview`, so a file's own
42
+ * `url` shows it there too. It shares the `/api` proxy's OAuth2 token.
37
43
  *
38
44
  * @param {UserConfig} [overrides] - Merged over the preset with Vite's own `mergeConfig`, so
39
45
  * arrays (plugins, aliases) are concatenated rather than replaced.
@@ -47,6 +53,8 @@ export const defineAppConfig = (overrides: UserConfig = {}) =>
47
53
  const env = loadEnv(mode, root, "");
48
54
  Object.assign(process.env, env);
49
55
 
56
+ const tokens = command === "serve" ? createApiTokenSource() : undefined;
57
+
50
58
  const preset: UserConfig = {
51
59
  base: "./",
52
60
  envDir: root,
@@ -66,7 +74,7 @@ export const defineAppConfig = (overrides: UserConfig = {}) =>
66
74
  host: true,
67
75
  port: Number(process.env.PORT) || 5173,
68
76
  strictPort: true,
69
- proxy: command === "serve" ? createDevServerProxy() : undefined,
77
+ proxy: tokens ? createDevServerProxy(tokens) : undefined,
70
78
  },
71
79
 
72
80
  plugins: [
@@ -75,6 +83,7 @@ export const defineAppConfig = (overrides: UserConfig = {}) =>
75
83
  tailwindcss(),
76
84
  envVarsLocationPlugin(),
77
85
  serveDevAppConfigPlugin(),
86
+ serveDevFilesPlugin(tokens),
78
87
  ],
79
88
  };
80
89
 
@@ -1,11 +1,11 @@
1
1
  import type { Plugin } from "vite";
2
2
 
3
3
  export function envVarsLocationPlugin(): Plugin {
4
- return {
5
- name: "env_vars_location",
6
- apply: "build",
7
- transformIndexHtml() {
8
- return [{ tag: "meta", attrs: { name: "app-config" }, injectTo: "head" }];
9
- },
10
- };
4
+ return {
5
+ name: "env_vars_location",
6
+ apply: "build",
7
+ transformIndexHtml() {
8
+ return [{ tag: "meta", attrs: { name: "app-config" }, injectTo: "head" }];
9
+ },
10
+ };
11
11
  }
@@ -16,36 +16,36 @@ import type { Plugin } from "vite";
16
16
  * on change — a full reload re-runs the bootstrap fetch, picking up edits to `env_vars` live.
17
17
  */
18
18
  export function serveDevAppConfigPlugin(): Plugin {
19
- return {
20
- name: "serve-dev-app-config",
21
- apply: "serve",
22
- configureServer(server) {
23
- // apps/<name>/config.json — one level up from the Vite root (the app's project/ dir).
24
- const configPath = path.resolve(process.cwd(), "..", "config.json");
19
+ return {
20
+ name: "serve-dev-app-config",
21
+ apply: "serve",
22
+ configureServer(server) {
23
+ // apps/<name>/config.json — one level up from the Vite root (the app's project/ dir).
24
+ const configPath = path.resolve(process.cwd(), "..", "config.json");
25
25
 
26
- server.middlewares.use(async (req, res, next) => {
27
- if (req.url !== "/app-config.json") return next();
26
+ server.middlewares.use(async (req, res, next) => {
27
+ if (req.url !== "/app-config.json") return next();
28
28
 
29
- try {
30
- const content = await fs.readFile(configPath, "utf-8");
31
- const envVars = (JSON.parse(content).env_vars ?? {}) as Record<string, unknown>;
29
+ try {
30
+ const content = await fs.readFile(configPath, "utf-8");
31
+ const envVars = (JSON.parse(content).env_vars ?? {}) as Record<string, unknown>;
32
32
 
33
- res.setHeader("Content-Type", "application/json; charset=utf-8");
34
- res.statusCode = 200;
35
- res.end(JSON.stringify(envVars));
36
- } catch {
37
- res.statusCode = 404;
38
- res.end();
39
- }
40
- });
33
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
34
+ res.statusCode = 200;
35
+ res.end(JSON.stringify(envVars));
36
+ } catch {
37
+ res.statusCode = 404;
38
+ res.end();
39
+ }
40
+ });
41
41
 
42
- // config.json is outside the Vite root, so watch it explicitly and full-reload on change.
43
- server.watcher.add(configPath);
44
- server.watcher.on("change", (file) => {
45
- if (path.resolve(file) === configPath) {
46
- server.hot.send({ type: "full-reload" });
47
- }
48
- });
49
- },
50
- };
42
+ // config.json is outside the Vite root, so watch it explicitly and full-reload on change.
43
+ server.watcher.add(configPath);
44
+ server.watcher.on("change", (file) => {
45
+ if (path.resolve(file) === configPath) {
46
+ server.hot.send({ type: "full-reload" });
47
+ }
48
+ });
49
+ },
50
+ };
51
51
  }