@olenbetong/appframe-vite 6.12.2 → 6.13.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.
- package/README.md +4 -4
- package/lib/cli-resources-add.js +4 -3
- package/lib/cli-resources-edit.js +4 -3
- package/lib/cli-resources-generate.js +4 -3
- package/lib/cli-resources-migrate.js +4 -3
- package/lib/cli.js +1 -2
- package/lib/devServer.d.ts +11 -1
- package/lib/devServer.js +20 -8
- package/lib/devtoolsServer.js +3 -7
- package/lib/generateTypes.d.ts +9 -1
- package/lib/generateTypes.js +13 -6
- package/lib/index.js +10 -8
- package/lib/localization.js +2 -2
- package/lib/proxy.d.ts +9 -0
- package/lib/proxy.js +50 -8
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -33,10 +33,10 @@ Then run your app:
|
|
|
33
33
|
pnpm --filter <app> start
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
The dev server logs in with the credentials `@olenbetong/appframe-auth` finds for the site:
|
|
37
|
+
`APPFRAME_LOGIN_<ENV>` / `APPFRAME_PWD_<ENV>`, then `APPFRAME_LOGIN` / `APPFRAME_PWD`, then the
|
|
38
|
+
password `af auth <env>` saved in the OS keychain. It never asks for one; without credentials it
|
|
39
|
+
stops and says which `af auth` to run.
|
|
40
40
|
|
|
41
41
|
On first run, the plugin logs in, configures Vite’s proxy for Appframe routes, and serves the article HTML with your local entry injected.
|
|
42
42
|
|
package/lib/cli-resources-add.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
|
+
import { authenticate } from "@olenbetong/appframe-auth";
|
|
2
3
|
import { Client, generateApiDataHandler } from "@olenbetong/appframe-data";
|
|
3
4
|
import { config } from "dotenv";
|
|
4
5
|
import fuzzy from "fuzzy";
|
|
5
6
|
import inquirer from "inquirer";
|
|
6
7
|
import autocomplete from "inquirer-autocomplete-standalone";
|
|
7
|
-
import {
|
|
8
|
+
import { getAppHostname } from "./devServer.js";
|
|
8
9
|
import { fetchAndGenerate, fetchResourceDefinition, writeGeneratedFile } from "./resourceGenerate.js";
|
|
9
10
|
import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
|
|
10
11
|
config({ quiet: true });
|
|
@@ -21,9 +22,9 @@ async function getResources(client) {
|
|
|
21
22
|
return ds.retrieve({ maxRecords: -1 });
|
|
22
23
|
}
|
|
23
24
|
export async function addResource() {
|
|
24
|
-
let { hostname
|
|
25
|
+
let { hostname } = await getAppHostname();
|
|
25
26
|
let client = new Client(hostname);
|
|
26
|
-
await client
|
|
27
|
+
await authenticate(client, { hostname });
|
|
27
28
|
// Step 1: pick a resource
|
|
28
29
|
let resources = await getResources(client);
|
|
29
30
|
let resourceChoices = resources.map((r) => ({
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { relative, resolve } from "node:path";
|
|
2
|
+
import { authenticate } from "@olenbetong/appframe-auth";
|
|
2
3
|
import { Client } from "@olenbetong/appframe-data";
|
|
3
4
|
import { config } from "dotenv";
|
|
4
5
|
import fuzzy from "fuzzy";
|
|
5
6
|
import inquirer from "inquirer";
|
|
6
7
|
import autocomplete from "inquirer-autocomplete-standalone";
|
|
7
|
-
import {
|
|
8
|
+
import { getAppHostname } from "./devServer.js";
|
|
8
9
|
import { fetchAndGenerate, fetchResourceDefinition, writeGeneratedFile } from "./resourceGenerate.js";
|
|
9
10
|
import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
|
|
10
11
|
config({ quiet: true });
|
|
@@ -44,9 +45,9 @@ export async function editResource(id) {
|
|
|
44
45
|
selected = allEntries.find((e) => e.entry.id === chosenId);
|
|
45
46
|
}
|
|
46
47
|
let { entry, isView } = selected;
|
|
47
|
-
let { hostname
|
|
48
|
+
let { hostname } = await getAppHostname();
|
|
48
49
|
let client = new Client(hostname);
|
|
49
|
-
await client
|
|
50
|
+
await authenticate(client, { hostname });
|
|
50
51
|
// Fetch definition for field picker
|
|
51
52
|
let definition = await fetchResourceDefinition(client, entry.resource);
|
|
52
53
|
let allFields = definition.Parameters?.filter((p) => !["CUT", "CDL"].includes(p.Name)).map((p) => isView ? p.Name : p.ParamName) ?? [];
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { dirname, relative, resolve, sep } from "node:path";
|
|
2
|
+
import { authenticate } from "@olenbetong/appframe-auth";
|
|
2
3
|
import { Client } from "@olenbetong/appframe-data";
|
|
3
4
|
import { config } from "dotenv";
|
|
4
|
-
import {
|
|
5
|
+
import { getAppHostname } from "./devServer.js";
|
|
5
6
|
import { fetchAndGenerate, writeGeneratedFile } from "./resourceGenerate.js";
|
|
6
7
|
import { entryToCLIOptions, readResourcesConfig } from "./resourcesConfig.js";
|
|
7
8
|
config({ quiet: true });
|
|
@@ -27,7 +28,7 @@ function resolveMasterImport(entry, allDataObjects) {
|
|
|
27
28
|
return { ...entry, master: `${masterId}:${rel}` };
|
|
28
29
|
}
|
|
29
30
|
export async function generateFromConfig(configPath) {
|
|
30
|
-
let { hostname
|
|
31
|
+
let { hostname } = await getAppHostname();
|
|
31
32
|
let resourcesConfig = await readResourcesConfig(configPath);
|
|
32
33
|
let allEntries = [
|
|
33
34
|
...(resourcesConfig.dataObjects ?? []).map((e) => ({ type: "dataObject", entry: e })),
|
|
@@ -38,7 +39,7 @@ export async function generateFromConfig(configPath) {
|
|
|
38
39
|
return;
|
|
39
40
|
}
|
|
40
41
|
let client = new Client(hostname);
|
|
41
|
-
await client
|
|
42
|
+
await authenticate(client, { hostname });
|
|
42
43
|
let errors = [];
|
|
43
44
|
let allDataObjects = resourcesConfig.dataObjects ?? [];
|
|
44
45
|
for (let { entry } of allEntries) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { authenticate } from "@olenbetong/appframe-auth";
|
|
2
3
|
import { Client, generateApiDataHandler } from "@olenbetong/appframe-data";
|
|
3
4
|
import { config } from "dotenv";
|
|
4
|
-
import {
|
|
5
|
+
import { getAppHostname } from "./devServer.js";
|
|
5
6
|
import { fetchAndGenerate, writeGeneratedFile } from "./resourceGenerate.js";
|
|
6
7
|
import { entryToCLIOptions, readResourcesConfig, writeResourcesConfig, } from "./resourcesConfig.js";
|
|
7
8
|
config({ quiet: true });
|
|
@@ -223,7 +224,7 @@ function upsertEntry(entries, entry) {
|
|
|
223
224
|
return "updated";
|
|
224
225
|
}
|
|
225
226
|
export async function migrateArticleResources(id, options = {}) {
|
|
226
|
-
let { hostname,
|
|
227
|
+
let { hostname, appframe } = await getAppHostname();
|
|
227
228
|
let articleId = appframe.article?.id ?? appframe.article;
|
|
228
229
|
let articleHost = appframe.article?.hostname ?? hostname;
|
|
229
230
|
if (!articleId) {
|
|
@@ -232,7 +233,7 @@ export async function migrateArticleResources(id, options = {}) {
|
|
|
232
233
|
let outputDir = (options.outputDir ?? "src/data").replace(/\\/g, "/").replace(/\/$/, "");
|
|
233
234
|
let dryRun = options.dryRun ?? false;
|
|
234
235
|
let client = new Client(hostname);
|
|
235
|
-
await client
|
|
236
|
+
await authenticate(client, { hostname });
|
|
236
237
|
let dsApiResources = createApiResourcesHandler(client);
|
|
237
238
|
let registered = new Map();
|
|
238
239
|
// The CMS configuration views are not exposed through the data API by default.
|
package/lib/cli.js
CHANGED
|
@@ -12,13 +12,12 @@ const pkg = await importJson("../package.json");
|
|
|
12
12
|
async function generateTypes() {
|
|
13
13
|
const appPkg = await importJson("./package.json", true);
|
|
14
14
|
const hostname = appPkg.appframe.proxy?.hostname ?? "dev.obet.no";
|
|
15
|
-
const { APPFRAME_LOGIN: username = "", APPFRAME_PWD: password = "" } = process.env;
|
|
16
15
|
const logger = {
|
|
17
16
|
info: (msg) => console.log(msg),
|
|
18
17
|
error: (msg) => console.error(msg),
|
|
19
18
|
};
|
|
20
19
|
logger.info(createLogMessage("Generating types…", { source: hostname }));
|
|
21
|
-
await runGenerateTypes(hostname,
|
|
20
|
+
await runGenerateTypes(hostname, appPkg.appframe, logger);
|
|
22
21
|
}
|
|
23
22
|
// ---------------------------------------------------------------------------
|
|
24
23
|
// Program
|
package/lib/devServer.d.ts
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import type { Connect, ViteDevServer } from "vite";
|
|
2
|
+
/** The site the app talks to in dev (`appframe.proxy.hostname`, then `appframe.deploy.hostname`). */
|
|
3
|
+
export declare function getAppHostname(): Promise<{
|
|
4
|
+
hostname: string;
|
|
5
|
+
appframe: any;
|
|
6
|
+
}>;
|
|
7
|
+
/**
|
|
8
|
+
* The site and its credentials: `APPFRAME_LOGIN_<ENV>` / `APPFRAME_PWD_<ENV>`, then
|
|
9
|
+
* `APPFRAME_LOGIN` / `APPFRAME_PWD`, then what `af auth` saved (`@olenbetong/appframe-auth`).
|
|
10
|
+
* Throws an `AuthenticationError` that says to run `af auth` when there are none.
|
|
11
|
+
*/
|
|
2
12
|
export declare function getLoginInfo(): Promise<{
|
|
3
|
-
hostname:
|
|
13
|
+
hostname: string;
|
|
4
14
|
username: string;
|
|
5
15
|
password: string;
|
|
6
16
|
appframe: any;
|
package/lib/devServer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
+
import { AuthenticationError, authenticate, loginHint, resolveCredentials } from "@olenbetong/appframe-auth";
|
|
4
5
|
import { Client } from "@olenbetong/appframe-data";
|
|
5
6
|
import dotenv from "dotenv";
|
|
6
7
|
import { JSDOM } from "jsdom";
|
|
@@ -10,14 +11,24 @@ import { diagnoseServerResponse } from "./utils.js";
|
|
|
10
11
|
dotenv.config({ quiet: true });
|
|
11
12
|
let appPkg = await importJson("./package.json", true);
|
|
12
13
|
let { appframe } = appPkg;
|
|
13
|
-
|
|
14
|
+
/** The site the app talks to in dev (`appframe.proxy.hostname`, then `appframe.deploy.hostname`). */
|
|
15
|
+
export async function getAppHostname() {
|
|
14
16
|
let { appframe } = await importJson("./package.json", true);
|
|
15
17
|
const hostname = appframe.proxy?.hostname ?? appframe.deploy?.hostname ?? "dev.obet.no";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
return { hostname, appframe };
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The site and its credentials: `APPFRAME_LOGIN_<ENV>` / `APPFRAME_PWD_<ENV>`, then
|
|
22
|
+
* `APPFRAME_LOGIN` / `APPFRAME_PWD`, then what `af auth` saved (`@olenbetong/appframe-auth`).
|
|
23
|
+
* Throws an `AuthenticationError` that says to run `af auth` when there are none.
|
|
24
|
+
*/
|
|
25
|
+
export async function getLoginInfo() {
|
|
26
|
+
let { hostname, appframe } = await getAppHostname();
|
|
27
|
+
let credentials = await resolveCredentials(hostname);
|
|
28
|
+
if (!credentials) {
|
|
29
|
+
throw new AuthenticationError(`No Appframe credentials for ${hostname}. ${loginHint(hostname)}`);
|
|
19
30
|
}
|
|
20
|
-
return { hostname, username, password, appframe };
|
|
31
|
+
return { hostname, username: credentials.username, password: credentials.password, appframe };
|
|
21
32
|
}
|
|
22
33
|
export function getProxyRoutes() {
|
|
23
34
|
const proxyRoutes = [
|
|
@@ -68,7 +79,7 @@ async function transformArticleHtml(html) {
|
|
|
68
79
|
entry = "/src/index.js";
|
|
69
80
|
}
|
|
70
81
|
let article = appframe.article?.id;
|
|
71
|
-
let { hostname } = await
|
|
82
|
+
let { hostname } = await getAppHostname();
|
|
72
83
|
let dom = new JSDOM(html);
|
|
73
84
|
let nodesToRemove = [];
|
|
74
85
|
let mainScriptAdded = false;
|
|
@@ -127,16 +138,17 @@ async function transformArticleHtml(html) {
|
|
|
127
138
|
return dom.serialize();
|
|
128
139
|
}
|
|
129
140
|
async function getArticleHtml() {
|
|
130
|
-
let { hostname
|
|
141
|
+
let { hostname } = await getAppHostname();
|
|
131
142
|
let article = appframe.article?.id;
|
|
132
143
|
let client = new Client(hostname);
|
|
133
144
|
try {
|
|
134
|
-
await client
|
|
145
|
+
await authenticate(client, { hostname, interactive: false });
|
|
135
146
|
}
|
|
136
147
|
catch (loginError) {
|
|
137
148
|
const isJsonError = loginError instanceof SyntaxError &&
|
|
138
149
|
(loginError.message.includes("not valid JSON") || loginError.message.includes("Unexpected token"));
|
|
139
150
|
if (isJsonError) {
|
|
151
|
+
let { username, password } = await getLoginInfo();
|
|
140
152
|
let loginBody = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&remember=true&RequireTwoFactor=0`;
|
|
141
153
|
let diagnostic = await diagnoseServerResponse(`https://${hostname}/login`, {
|
|
142
154
|
method: "POST",
|
package/lib/devtoolsServer.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createReleaseManager, handleReleaseApi, } from "./releaseServer.js";
|
|
|
2
2
|
import { rmSync } from "node:fs";
|
|
3
3
|
import https from "node:https";
|
|
4
4
|
import bodyParser from "body-parser";
|
|
5
|
-
import {
|
|
5
|
+
import { authenticatedLogin } from "./proxy.js";
|
|
6
6
|
import { readResourcesConfig, writeResourcesConfig } from "./resourcesConfig.js";
|
|
7
7
|
const ALL_SERVERS = ["dev.obet.no", "stage.obet.no", "test.obet.no"];
|
|
8
8
|
/**
|
|
@@ -99,13 +99,9 @@ async function handleApi(req, res, apiPath, info, release, next) {
|
|
|
99
99
|
if (!dbObjectId) {
|
|
100
100
|
return sendJson(res, 400, { error: "dbObjectId is required" });
|
|
101
101
|
}
|
|
102
|
-
|
|
103
|
-
const password = process.env.APPFRAME_PWD ?? "";
|
|
104
|
-
if (!username || !password) {
|
|
105
|
-
return sendJson(res, 500, { error: "APPFRAME_LOGIN and APPFRAME_PWD environment variables must be set" });
|
|
106
|
-
}
|
|
102
|
+
// Each site with its own credentials; one without any fails on its own row.
|
|
107
103
|
const settled = await Promise.allSettled(ALL_SERVERS.map(async (server) => {
|
|
108
|
-
const cookies = await
|
|
104
|
+
const cookies = await authenticatedLogin(server, { silent: true });
|
|
109
105
|
return addResourceOnServer(server, cookies, dbObjectId, name);
|
|
110
106
|
}));
|
|
111
107
|
const results = settled.map((r, i) => r.status === "fulfilled"
|
package/lib/generateTypes.d.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
1
|
import type { ViteDevServer } from "vite";
|
|
2
2
|
type Logger = Pick<ViteDevServer["config"]["logger"], "info" | "error">;
|
|
3
|
-
export
|
|
3
|
+
export type GenerateTypesOptions = {
|
|
4
|
+
/**
|
|
5
|
+
* May ask for a password in the terminal (`appframe-vite generate-types`). Default: when stdin
|
|
6
|
+
* and stdout are a terminal. The dev server passes `false`.
|
|
7
|
+
*/
|
|
8
|
+
interactive?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Logs in with the site's credentials (`@olenbetong/appframe-auth`) and writes `src/appframe.d.ts`. */
|
|
11
|
+
export declare function runGenerateTypes(hostname: string, appframe: any, logger: Logger, options?: GenerateTypesOptions): Promise<void>;
|
|
4
12
|
export {};
|
package/lib/generateTypes.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import vm from "node:vm";
|
|
5
|
+
import { authenticate, resolveCredentials } from "@olenbetong/appframe-auth";
|
|
5
6
|
import { Client, DataObject, generateApiDataHandler, getDefaultClient, Procedure, setDefaultClient, } from "@olenbetong/appframe-data";
|
|
6
7
|
import { formatFile, lintFile } from "./format.js";
|
|
7
8
|
import { importJson } from "./importJson.js";
|
|
@@ -122,16 +123,20 @@ function withTimeout(promise, ms, label) {
|
|
|
122
123
|
const TIMEOUT_MS = 30_000;
|
|
123
124
|
const MAX_RETRIES = 3;
|
|
124
125
|
const RETRY_DELAY_BASE_MS = 2_000;
|
|
125
|
-
async function doGenerateTypes(hostname,
|
|
126
|
+
async function doGenerateTypes(hostname, appframe, logger, options) {
|
|
126
127
|
const client = new Client(hostname);
|
|
127
128
|
setDefaultClient(client);
|
|
128
129
|
try {
|
|
129
|
-
|
|
130
|
+
let login = authenticate(client, { hostname, interactive: options.interactive });
|
|
131
|
+
// No timeout while the login may be waiting for someone to type a password.
|
|
132
|
+
await (options.interactive ? login : withTimeout(login, TIMEOUT_MS, "login"));
|
|
130
133
|
}
|
|
131
134
|
catch (loginError) {
|
|
132
135
|
const isJsonError = loginError instanceof SyntaxError &&
|
|
133
136
|
(loginError.message.includes("not valid JSON") || loginError.message.includes("Unexpected token"));
|
|
134
|
-
|
|
137
|
+
let credentials = isJsonError ? await resolveCredentials(hostname) : undefined;
|
|
138
|
+
if (credentials) {
|
|
139
|
+
let { username, password } = credentials;
|
|
135
140
|
let loginBody = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&remember=true&RequireTwoFactor=0`;
|
|
136
141
|
let diagnostic = await diagnoseServerResponse(`https://${hostname}/login`, {
|
|
137
142
|
method: "POST",
|
|
@@ -223,15 +228,17 @@ async function doGenerateTypes(hostname, username, password, appframe, logger) {
|
|
|
223
228
|
logger.info(createLogMessage(message, { source: hostname, type: "warn" }));
|
|
224
229
|
}
|
|
225
230
|
}
|
|
226
|
-
|
|
231
|
+
/** Logs in with the site's credentials (`@olenbetong/appframe-auth`) and writes `src/appframe.d.ts`. */
|
|
232
|
+
export async function runGenerateTypes(hostname, appframe, logger, options = {}) {
|
|
227
233
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
228
234
|
try {
|
|
229
|
-
await doGenerateTypes(hostname,
|
|
235
|
+
await doGenerateTypes(hostname, appframe, logger, options);
|
|
230
236
|
logger.info(createLogMessage("typescript types generated successfully", { source: hostname }));
|
|
231
237
|
return;
|
|
232
238
|
}
|
|
233
239
|
catch (error) {
|
|
234
|
-
|
|
240
|
+
// Missing or rejected credentials do not get better by trying again.
|
|
241
|
+
if (attempt < MAX_RETRIES && error?.name !== "AuthenticationError") {
|
|
235
242
|
// Exponential backoff before retry
|
|
236
243
|
await new Promise((r) => setTimeout(r, RETRY_DELAY_BASE_MS * attempt));
|
|
237
244
|
}
|
package/lib/index.js
CHANGED
|
@@ -5,13 +5,13 @@ import { watch } from "chokidar";
|
|
|
5
5
|
import { appframeDataGlobal } from "./appframeDataGlobal.js";
|
|
6
6
|
import { addAppframeBuildConfig, PLATFORM_EXTERNALS, PLATFORM_GLOBALS, PLATFORM_UPSTREAM, platformManifest, usesPlatform, } from "./build.js";
|
|
7
7
|
import { generateFromConfig } from "./cli-resources-generate.js";
|
|
8
|
-
import { createDevMiddleware,
|
|
8
|
+
import { createDevMiddleware, getAppHostname, getProxyRoutes } from "./devServer.js";
|
|
9
9
|
import { createDevtoolsMiddleware } from "./devtoolsServer.js";
|
|
10
10
|
import { importJson } from "./importJson.js";
|
|
11
11
|
import { runGenerateTypes } from "./generateTypes.js";
|
|
12
12
|
import { getMuiXLicenseKey } from "./licenses.js";
|
|
13
13
|
import { localizeMiddleware } from "./localization.js";
|
|
14
|
-
import { checkSession, getLastSession
|
|
14
|
+
import { authenticatedLogin, checkSession, getLastSession } from "./proxy.js";
|
|
15
15
|
import { RESOURCES_CONFIG_FILE } from "./resourcesConfig.js";
|
|
16
16
|
import { createLogMessage, getServerName } from "./utils.js";
|
|
17
17
|
import { publishSafeOutput } from "./publishSafeOutput.js";
|
|
@@ -33,7 +33,7 @@ function watchAppPackage() {
|
|
|
33
33
|
let appPkgUrl = `file://${process.cwd()}/package.json`;
|
|
34
34
|
watcher = watch(appPkgUrl).on("all", async () => {
|
|
35
35
|
if (server) {
|
|
36
|
-
let { hostname } = await
|
|
36
|
+
let { hostname } = await getAppHostname();
|
|
37
37
|
if (lastHostname !== hostname) {
|
|
38
38
|
server.restart(false);
|
|
39
39
|
}
|
|
@@ -135,9 +135,11 @@ export default function appframe(options = {}) {
|
|
|
135
135
|
watchAppPackage();
|
|
136
136
|
let proxy = config.server?.proxy ?? {};
|
|
137
137
|
let routes = getProxyRoutes();
|
|
138
|
-
let { hostname
|
|
138
|
+
let { hostname } = await getAppHostname();
|
|
139
139
|
lastHostname = hostname;
|
|
140
|
-
|
|
140
|
+
// Never prompts (the dev server owns the terminal): without credentials it stops with
|
|
141
|
+
// "Run af auth <env>", and a rejected saved password is removed first.
|
|
142
|
+
await authenticatedLogin(hostname);
|
|
141
143
|
console.log(createLogMessage(`connected to '${getServerName(hostname)}'`, { source: hostname }));
|
|
142
144
|
if (interval)
|
|
143
145
|
clearInterval(interval);
|
|
@@ -148,7 +150,7 @@ export default function appframe(options = {}) {
|
|
|
148
150
|
try {
|
|
149
151
|
const alive = await checkSession(hostname);
|
|
150
152
|
if (!alive) {
|
|
151
|
-
await
|
|
153
|
+
await authenticatedLogin(hostname, { silent: true });
|
|
152
154
|
}
|
|
153
155
|
}
|
|
154
156
|
catch (error) {
|
|
@@ -186,11 +188,11 @@ export default function appframe(options = {}) {
|
|
|
186
188
|
return config;
|
|
187
189
|
},
|
|
188
190
|
async configureServer(_server) {
|
|
189
|
-
let { appframe, hostname
|
|
191
|
+
let { appframe, hostname } = await getAppHostname();
|
|
190
192
|
server = _server;
|
|
191
193
|
// Run type generation in the background — doesn't block the dev server from starting.
|
|
192
194
|
if (generateTypes) {
|
|
193
|
-
runGenerateTypes(hostname,
|
|
195
|
+
runGenerateTypes(hostname, appframe, _server.config.logger, { interactive: false });
|
|
194
196
|
}
|
|
195
197
|
// Watch resources.yaml and regenerate on change
|
|
196
198
|
let resourcesConfigPath = resolve(process.cwd(), RESOURCES_CONFIG_FILE);
|
package/lib/localization.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
15
15
|
import { resolve } from "node:path";
|
|
16
|
-
import {
|
|
16
|
+
import { getAppHostname } from "./devServer.js";
|
|
17
17
|
import { importJson } from "./importJson.js";
|
|
18
18
|
import { getLastSession } from "./proxy.js";
|
|
19
19
|
function debounce(callback, delay = 300) {
|
|
@@ -49,7 +49,7 @@ export function addStringToCache(text, translation) {
|
|
|
49
49
|
export const localizeMiddleware = async (req, res, _next) => {
|
|
50
50
|
// @ts-expect-error - Vite's types don't include the body property, but it is there because we use the bodyParser middleware before this.
|
|
51
51
|
let text = req.body.text;
|
|
52
|
-
let { hostname, appframe } = await
|
|
52
|
+
let { hostname, appframe } = await getAppHostname();
|
|
53
53
|
let authCookies = getLastSession(hostname)?.authCookies;
|
|
54
54
|
if (authCookies) {
|
|
55
55
|
let cookies = [];
|
package/lib/proxy.d.ts
CHANGED
|
@@ -13,4 +13,13 @@ export declare function checkSession(hostname: string): Promise<boolean>;
|
|
|
13
13
|
export declare function login(hostname: string, username: string, password: string, loginOptions?: {
|
|
14
14
|
silent?: boolean;
|
|
15
15
|
}): Promise<CookieObject>;
|
|
16
|
+
/**
|
|
17
|
+
* `login` with the site's credentials from `@olenbetong/appframe-auth`: `APPFRAME_LOGIN_<ENV>` /
|
|
18
|
+
* `APPFRAME_PWD_<ENV>`, `APPFRAME_LOGIN` / `APPFRAME_PWD`, or what `af auth` saved. A rejected
|
|
19
|
+
* saved password is removed, and a missing or rejected one throws an `AuthenticationError` that
|
|
20
|
+
* says to run `af auth`. Never prompts: it runs inside the dev server.
|
|
21
|
+
*/
|
|
22
|
+
export declare function authenticatedLogin(hostname: string, loginOptions?: {
|
|
23
|
+
silent?: boolean;
|
|
24
|
+
}): Promise<CookieObject>;
|
|
16
25
|
export {};
|
package/lib/proxy.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
2
2
|
import { readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import https from "node:https";
|
|
4
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { authenticate, createLoginRejectedError, isRejectedLogin } from "@olenbetong/appframe-auth";
|
|
5
6
|
import { createLogMessage } from "./utils.js";
|
|
6
7
|
const lastLogin = new Map();
|
|
7
8
|
let currentLogin = null;
|
|
@@ -108,7 +109,8 @@ export async function login(hostname, username, password, loginOptions) {
|
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
111
|
if (currentLogin) {
|
|
111
|
-
|
|
112
|
+
// Wait for a login in flight; its failure is its caller's to report.
|
|
113
|
+
await currentLogin.catch(() => { });
|
|
112
114
|
}
|
|
113
115
|
let loginPromise = new Promise((resolve, reject) => {
|
|
114
116
|
if (lastLogin.has(hostname)) {
|
|
@@ -163,10 +165,26 @@ export async function login(hostname, username, password, loginOptions) {
|
|
|
163
165
|
}
|
|
164
166
|
}
|
|
165
167
|
}, 400);
|
|
166
|
-
let request = https.request(options, (response) => {
|
|
168
|
+
let request = https.request(options, async (response) => {
|
|
169
|
+
let body = "";
|
|
170
|
+
try {
|
|
171
|
+
for await (let chunk of response)
|
|
172
|
+
body += chunk;
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// The status and cookies are what matter; a broken body is treated as empty.
|
|
176
|
+
}
|
|
167
177
|
clearInterval(interval);
|
|
168
178
|
let status = response.statusCode ?? 600;
|
|
169
|
-
|
|
179
|
+
// A wrong password is answered with 200 and `{ error }`, and no session cookies.
|
|
180
|
+
let rejection;
|
|
181
|
+
try {
|
|
182
|
+
rejection = JSON.parse(body)?.error;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
// Not JSON: a redirect or a page, which is what a successful login looks like.
|
|
186
|
+
}
|
|
187
|
+
if (status < 400 && !rejection) {
|
|
170
188
|
let cookies = response.headers["set-cookie"] ?? [];
|
|
171
189
|
let cookieObj = {};
|
|
172
190
|
for (let cookie of cookies) {
|
|
@@ -188,6 +206,11 @@ export async function login(hostname, username, password, loginOptions) {
|
|
|
188
206
|
writeSessionStore(hostname, entry);
|
|
189
207
|
resolve(cookieObj);
|
|
190
208
|
}
|
|
209
|
+
else if (rejection || (status >= 400 && status < 500)) {
|
|
210
|
+
if (!silent)
|
|
211
|
+
process.stdout.write("\n");
|
|
212
|
+
reject(createLoginRejectedError(rejection ?? `Login failed: ${status} ${response.statusMessage}`));
|
|
213
|
+
}
|
|
191
214
|
else {
|
|
192
215
|
reject(Error(createLogMessage(`authentication failed: ${response.statusCode} ${response.statusMessage}`, {
|
|
193
216
|
source: hostname,
|
|
@@ -196,22 +219,41 @@ export async function login(hostname, username, password, loginOptions) {
|
|
|
196
219
|
}
|
|
197
220
|
});
|
|
198
221
|
request.on("error", (error) => {
|
|
199
|
-
|
|
222
|
+
clearInterval(interval);
|
|
223
|
+
reject(error);
|
|
200
224
|
});
|
|
201
225
|
request.write(data);
|
|
202
226
|
request.end();
|
|
203
227
|
});
|
|
204
228
|
try {
|
|
205
229
|
currentLogin = loginPromise;
|
|
206
|
-
|
|
207
|
-
currentLogin = null;
|
|
208
|
-
return result;
|
|
230
|
+
return await loginPromise;
|
|
209
231
|
}
|
|
210
232
|
catch (error) {
|
|
233
|
+
// A server that is down should not stop a dev server that still has a session, but a
|
|
234
|
+
// rejected password must be reported rather than hidden behind the old cookies.
|
|
211
235
|
let cookies = lastLogin.get(hostname)?.authCookies;
|
|
212
|
-
if (cookies) {
|
|
236
|
+
if (cookies && !isRejectedLogin(error)) {
|
|
213
237
|
return cookies;
|
|
214
238
|
}
|
|
215
239
|
throw error;
|
|
216
240
|
}
|
|
241
|
+
finally {
|
|
242
|
+
currentLogin = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* `login` with the site's credentials from `@olenbetong/appframe-auth`: `APPFRAME_LOGIN_<ENV>` /
|
|
247
|
+
* `APPFRAME_PWD_<ENV>`, `APPFRAME_LOGIN` / `APPFRAME_PWD`, or what `af auth` saved. A rejected
|
|
248
|
+
* saved password is removed, and a missing or rejected one throws an `AuthenticationError` that
|
|
249
|
+
* says to run `af auth`. Never prompts: it runs inside the dev server.
|
|
250
|
+
*/
|
|
251
|
+
export async function authenticatedLogin(hostname, loginOptions) {
|
|
252
|
+
let cookies = {};
|
|
253
|
+
await authenticate({
|
|
254
|
+
async login(username, password) {
|
|
255
|
+
cookies = await login(hostname, username, password, loginOptions);
|
|
256
|
+
},
|
|
257
|
+
}, { hostname, interactive: false });
|
|
258
|
+
return cookies;
|
|
217
259
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@olenbetong/appframe-vite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.13.1",
|
|
4
4
|
"description": "Tools to use and deploy Vite applications to Appframe",
|
|
5
5
|
"main": "./lib/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -29,9 +29,10 @@
|
|
|
29
29
|
"author": "Bjørnar Vister Hansen <bvh@olenbetong.no>",
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@olenbetong/appframe-
|
|
32
|
+
"@olenbetong/appframe-auth": "0.1.0",
|
|
33
|
+
"@olenbetong/appframe-data": "1.8.1",
|
|
33
34
|
"body-parser": "^2.3.0",
|
|
34
|
-
"chalk": "6.0.
|
|
35
|
+
"chalk": "6.0.1",
|
|
35
36
|
"chokidar": "^5.0.0",
|
|
36
37
|
"commander": "15.0.0",
|
|
37
38
|
"dotenv": "^17.4.2",
|
|
@@ -44,9 +45,9 @@
|
|
|
44
45
|
},
|
|
45
46
|
"devDependencies": {
|
|
46
47
|
"@types/jsdom": "^30.0.0",
|
|
47
|
-
"@types/node": "26.6.
|
|
48
|
+
"@types/node": "26.6.3",
|
|
48
49
|
"typescript": "7.0.2",
|
|
49
|
-
"vite": "8.3.
|
|
50
|
+
"vite": "8.3.1"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"vite": ">=8.1.5"
|